refactoring van de cmap structuren bijna compleet
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Undo/redo history for an editor whose state can be represented as JSON.
|
||||
*
|
||||
* The history owns snapshots, stack limits and the asynchronous commit boundary.
|
||||
* It does not know how a document is rendered or restored: those concerns are
|
||||
* supplied by the editor through the constructor callbacks.
|
||||
*/
|
||||
export class CmapHistory {
|
||||
/**
|
||||
* goal : Create a history coordinator for one editor state.
|
||||
* pre : snapshot and restore are functions for the same serialized state.
|
||||
* post : The coordinator is ready to track state after reset() is called.
|
||||
* result : A CmapHistory instance with empty undo and redo stacks.
|
||||
* internals : The callbacks keep this controller independent of the editor's
|
||||
* model and view; the stacks contain only serialized snapshots.
|
||||
*
|
||||
* @param {object} options History callbacks and configuration.
|
||||
* @param {Function} options.snapshot Returns the current serialized state.
|
||||
* @param {Function} options.restore Restores one serialized state.
|
||||
* @param {Function} [options.onChange] Receives undo/redo availability changes.
|
||||
* @param {number} [options.limit=100] Maximum number of undo snapshots.
|
||||
*/
|
||||
constructor({ snapshot, restore, onChange = null, limit = 100 }) {
|
||||
if (typeof snapshot !== "function") throw new TypeError("A snapshot function is required");
|
||||
if (typeof restore !== "function") throw new TypeError("A restore function is required");
|
||||
this.snapshot = snapshot;
|
||||
this.restoreDocument = restore;
|
||||
this.onChange = onChange;
|
||||
this.limit = Math.max(1, Number(limit) || 100);
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
this.currentSnapshot = null;
|
||||
this.timer = null;
|
||||
this.ready = false;
|
||||
this.isRestoring = false;
|
||||
}
|
||||
|
||||
/** Notify the host about the current availability of undo and redo. */
|
||||
notify() {
|
||||
if (this.onChange) {
|
||||
this.onChange({
|
||||
canUndo: this.canUndo(),
|
||||
canRedo: this.canRedo()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* goal : Start a new history session at the current editor state.
|
||||
* pre : The snapshot callback returns the current serialized state.
|
||||
* post : Both stacks are empty and the current state is the history baseline.
|
||||
* result : Undefined; the host is notified of the empty stacks.
|
||||
* internals : A pending timer is cancelled before the baseline is captured.
|
||||
*/
|
||||
reset() {
|
||||
this.cancelScheduledCommit();
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
this.ready = true;
|
||||
this.currentSnapshot = this.snapshot();
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/** Cancel a pending asynchronous history commit. */
|
||||
cancelScheduledCommit() {
|
||||
if (this.timer !== null) {
|
||||
window.clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule one commit for the current mutation transaction.
|
||||
* The zero-delay timer groups synchronous editor changes into one undo step.
|
||||
*/
|
||||
scheduleCommit() {
|
||||
if (!this.ready || this.isRestoring) return;
|
||||
this.cancelScheduledCommit();
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.timer = null;
|
||||
this.commit();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/** Update the baseline after renderer-only normalization. */
|
||||
refreshSnapshot() {
|
||||
if (!this.ready || this.isRestoring || this.timer !== null) return;
|
||||
this.currentSnapshot = this.snapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the current state when it differs from the baseline.
|
||||
* @returns {boolean} Whether a new undo step was recorded.
|
||||
*/
|
||||
commit() {
|
||||
if (!this.ready || this.isRestoring) return false;
|
||||
this.cancelScheduledCommit();
|
||||
const nextSnapshot = this.snapshot();
|
||||
if (nextSnapshot === this.currentSnapshot) return false;
|
||||
if (this.currentSnapshot !== null) {
|
||||
this.undoStack.push(this.currentSnapshot);
|
||||
if (this.undoStack.length > this.limit) this.undoStack.shift();
|
||||
}
|
||||
this.currentSnapshot = nextSnapshot;
|
||||
this.redoStack = [];
|
||||
this.notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Return whether an undo operation is available. */
|
||||
canUndo() {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
/** Return whether a redo operation is available. */
|
||||
canRedo() {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
hasPendingCommit() {
|
||||
return this.timer !== null;
|
||||
}
|
||||
|
||||
get undoCount() {
|
||||
return this.undoStack.length;
|
||||
}
|
||||
|
||||
get redoCount() {
|
||||
return this.redoStack.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore one snapshot while suppressing history commits caused by loading.
|
||||
* The editor callback performs the actual model and view reconstruction.
|
||||
*/
|
||||
restoreSnapshot(snapshot) {
|
||||
this.isRestoring = true;
|
||||
try {
|
||||
this.restoreDocument(snapshot);
|
||||
} finally {
|
||||
this.isRestoring = false;
|
||||
}
|
||||
this.currentSnapshot = snapshot;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/** Restore the previous committed state, if one exists. */
|
||||
undo() {
|
||||
this.commit();
|
||||
if (!this.canUndo()) return false;
|
||||
this.redoStack.push(this.currentSnapshot);
|
||||
const snapshot = this.undoStack.pop();
|
||||
this.restoreSnapshot(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Restore the most recently undone state, if one exists. */
|
||||
redo() {
|
||||
this.commit();
|
||||
if (!this.canRedo()) return false;
|
||||
this.undoStack.push(this.currentSnapshot);
|
||||
const snapshot = this.redoStack.pop();
|
||||
this.restoreSnapshot(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* goal : Replace the current document as one undoable operation.
|
||||
* pre : replaceDocument performs the complete document replacement.
|
||||
* post : The replacement is current and redo history has been discarded.
|
||||
* result : Undefined; the host receives the new undo/redo availability.
|
||||
* internals : The old baseline is pushed before the callback runs, while
|
||||
* isRestoring prevents loading callbacks from creating nested history steps.
|
||||
*/
|
||||
replace(replaceDocument) {
|
||||
this.commit();
|
||||
const previousSnapshot = this.currentSnapshot || this.snapshot();
|
||||
this.isRestoring = true;
|
||||
try {
|
||||
replaceDocument();
|
||||
} finally {
|
||||
this.isRestoring = false;
|
||||
}
|
||||
const nextSnapshot = this.snapshot();
|
||||
if (previousSnapshot !== nextSnapshot) {
|
||||
this.undoStack.push(previousSnapshot);
|
||||
if (this.undoStack.length > this.limit) this.undoStack.shift();
|
||||
}
|
||||
this.currentSnapshot = nextSnapshot;
|
||||
this.redoStack = [];
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/** Release the timer when the owning editor is destroyed. */
|
||||
destroy() {
|
||||
this.cancelScheduledCommit();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user