mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-20 01:02:22 +00:00
698c9d4e40
Summary: Grist should not prevent read-only viewers from opening cell editors since they usually provide much more information than is visible in a cell. Every editor was enhanced with a read-only mode that provides the same information available for an editor but doesn't allow to change the underlying data. Test Plan: Browser tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2842
182 lines
5.3 KiB
TypeScript
182 lines
5.3 KiB
TypeScript
import { getStorage } from "app/client/lib/localStorageObs";
|
|
import { Disposable, Emitter, Holder, IDisposableOwner } from "grainjs";
|
|
import { GristDoc } from "app/client/components/GristDoc";
|
|
import { FieldEditor, FieldEditorStateEvent } from "app/client/widgets/FieldEditor";
|
|
import { CellPosition, toCursor } from "app/client/components/CellPosition";
|
|
|
|
/**
|
|
* Feature for GristDoc that allows it to keep track of current editor's state.
|
|
* State is stored in local storage by default.
|
|
*/
|
|
export class EditorMonitor extends Disposable {
|
|
|
|
// abstraction to work with local storage
|
|
private _store: EditMemoryStorage;
|
|
// Holds a listener that is attached to the current view.
|
|
// It will be cleared after first trigger.
|
|
private _currentViewListener = Holder.create(this);
|
|
|
|
constructor(
|
|
doc: GristDoc,
|
|
store?: Storage) {
|
|
super();
|
|
|
|
// create store
|
|
this._store = new EditMemoryStorage(doc.docId(), store);
|
|
|
|
// listen to document events to handle view load event
|
|
this._listenToReload(doc);
|
|
}
|
|
|
|
/**
|
|
* Monitors a field editor and updates latest edit position
|
|
* @param editor Field editor to track
|
|
*/
|
|
public monitorEditor(editor: FieldEditor) {
|
|
// typed helper to connect to the emitter
|
|
const on = typedListener(this);
|
|
// When user cancels the edit process, discard the memory of the last edited cell.
|
|
on(editor.cancelEmitter, (event) => {
|
|
this._store.clear();
|
|
});
|
|
// When saves a cell, discard the memory of the last edited cell.
|
|
on(editor.saveEmitter, (event) => {
|
|
this._store.clear();
|
|
});
|
|
// When user types in the editor, store its state
|
|
on(editor.changeEmitter, (event) => {
|
|
this._store.updateValue(event.position, event.currentState);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When document gets reloaded, restore last cursor position and a state of the editor.
|
|
* Returns last edited cell position and saved editor state or undefined.
|
|
*/
|
|
private _listenToReload(doc: GristDoc) {
|
|
// subscribe to the current view event on the GristDoc, but make sure that the handler
|
|
// will be invoked only once
|
|
let executed = false;
|
|
|
|
// don't restore on readonly mode
|
|
if (doc.isReadonly.get()) { return; }
|
|
|
|
// on view shown
|
|
this._currentViewListener.autoDispose(doc.currentView.addListener(async view => {
|
|
if (executed) {
|
|
// remove the listener - we can't do it while the listener is actively executing
|
|
setImmediate(() => this._currentViewListener.clear());
|
|
return;
|
|
}
|
|
executed = true;
|
|
// if view wasn't rendered (page is displaying history or code view) do nothing
|
|
if (!view) { return; }
|
|
const lastEdit = this._restorePosition();
|
|
if (lastEdit) {
|
|
// set the cursor at right cell
|
|
await doc.recursiveMoveToCursorPos(toCursor(lastEdit.position, doc.docModel), true);
|
|
// activate the editor
|
|
await doc.activateEditorAtCursor({ state: lastEdit.value });
|
|
}
|
|
}));
|
|
}
|
|
|
|
// read the value from the storage
|
|
private _restorePosition() {
|
|
const entry = this._store.readValue();
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
// Internal implementation, not relevant to the main use case
|
|
|
|
// typed listener for the Emitter class
|
|
function typedListener(owner: IDisposableOwner) {
|
|
return function (emitter: Emitter, clb: (e: FieldEditorStateEvent) => any) {
|
|
owner.autoDispose(emitter.addListener(clb));
|
|
};
|
|
}
|
|
|
|
// Marker for a editor state - each editor can report any data as long as it is serialized
|
|
type EditorState = any;
|
|
|
|
// Schema for value stored in the local storage
|
|
interface LastEditData {
|
|
// absolute position for a cell
|
|
position: CellPosition;
|
|
// editor's state
|
|
value: EditorState;
|
|
}
|
|
|
|
// Abstraction for working with local storage
|
|
class EditMemoryStorage {
|
|
|
|
private _entry: LastEditData | null = null;
|
|
private _timestamp = 0;
|
|
|
|
constructor(private _docId: string, private _storage = getStorage()) {
|
|
}
|
|
|
|
public updateValue(pos: CellPosition, value: EditorState): void {
|
|
this._entry = { position: pos, value: value };
|
|
this.save();
|
|
}
|
|
|
|
public readValue(): LastEditData | null {
|
|
this.load();
|
|
return this._entry;
|
|
}
|
|
|
|
public clear(): void {
|
|
this._entry = null;
|
|
this.save();
|
|
}
|
|
|
|
public timestamp(): number {
|
|
return this._timestamp;
|
|
}
|
|
|
|
protected _key() {
|
|
return `grist-last-edit-${this._docId}`;
|
|
}
|
|
|
|
protected load() {
|
|
const storage = this._storage;
|
|
const data = storage.getItem(this._key());
|
|
this._entry = null;
|
|
this._timestamp = 0;
|
|
|
|
if (data) {
|
|
try {
|
|
const { entry, timestamp } = JSON.parse(data);
|
|
if (typeof entry === 'undefined' || typeof timestamp != 'number') {
|
|
console.error("[EditMemory] Data in local storage has a different structure");
|
|
return;
|
|
}
|
|
this._entry = entry;
|
|
this._timestamp = timestamp;
|
|
} catch (e) {
|
|
console.error("[EditMemory] Can't deserialize date from local storage");
|
|
}
|
|
}
|
|
}
|
|
|
|
protected save(): void {
|
|
const storage = this._storage;
|
|
|
|
// if entry was removed - clear the storage
|
|
if (!this._entry) {
|
|
storage.removeItem(this._key());
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this._timestamp = Date.now();
|
|
const data = { timestamp: this._timestamp, entry: this._entry };
|
|
storage.setItem(this._key(), JSON.stringify(data));
|
|
} catch (ex) {
|
|
console.error("Can't save current edited cell state. Error message: " + ex?.message);
|
|
}
|
|
}
|
|
}
|