Improve Squire edit history stack

This commit is contained in:
djmaze 2021-08-16 13:13:33 +02:00
parent ed1dc4dbc1
commit d4f55a02a4

View file

@ -17,6 +17,7 @@ const
DOCUMENT_FRAGMENT_NODE = 11, // Node.DOCUMENT_FRAGMENT_NODE,
SHOW_ELEMENT = 1, // NodeFilter.SHOW_ELEMENT,
SHOW_TEXT = 4, // NodeFilter.SHOW_TEXT,
SHOW_ELEMENT_OR_TEXT = 5,
START_TO_START = 0, // Range.START_TO_START
START_TO_END = 1, // Range.START_TO_END
@ -1034,7 +1035,7 @@ const
},
newContentWalker = root => createTreeWalker( root,
SHOW_TEXT|SHOW_ELEMENT,
SHOW_ELEMENT_OR_TEXT,
node => node.nodeType === TEXT_NODE ? notWS.test( node.data ) : node.nodeName === 'IMG'
),
@ -1206,7 +1207,7 @@ const
// Save undo checkpoint and add any links in the preceding section.
// Remove any zws so we don't think there's content in an empty
// block.
self._recordUndoState( range );
self._recordUndoState( range, false );
if ( self._config.addLinks ) {
addLinks( range.startContainer, root, self );
}
@ -1505,7 +1506,7 @@ const
while ( isInline( nonInlineParent ) ) {
nonInlineParent = nonInlineParent.parentNode;
}
let walker = createTreeWalker( nonInlineParent, SHOW_TEXT|SHOW_ELEMENT );
let walker = createTreeWalker( nonInlineParent, SHOW_ELEMENT_OR_TEXT );
for ( i = 0, l = children.length; i < l; ++i ) {
child = children[i];
@ -1612,7 +1613,7 @@ const
while ( isInline( block ) ) {
block = block.parentNode;
}
walker = createTreeWalker( block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
walker = createTreeWalker( block, SHOW_ELEMENT_OR_TEXT, notWSTextNode );
walker.currentNode = br;
return !!walker.nextNode() ||
( isLBIfEmptyBlock && !walker.previousNode() );
@ -1929,7 +1930,7 @@ let contentWalker,
TreeWalker.prototype.previousPONode = function () {
let current = this.currentNode,
root = this.root,
nodeType = this.nodeType,
nodeType = this.nodeType, // whatToShow?
filter = this.filter,
node;
while ( current ) {
@ -2405,7 +2406,7 @@ let keyHandlers = {
space: ( self, _, range ) => {
let node;
let root = self._root;
self._recordUndoState( range );
self._recordUndoState( range, false );
if ( self._config.addLinks ) {
addLinks( range.startContainer, root, self );
}
@ -2470,6 +2471,123 @@ keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
keyHandlers[ 'undo' ] = mapKeyTo( 'undo' );
keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
class EditStack extends Array
{
constructor(squire) {
super();
this.squire = squire;
this.index = -1;
this.inUndoState = false;
this.threshold = -1; // -1 means no threshold
this.limit = -1; // -1 means no limit
}
clear() {
this.index = -1;
this.length = 0;
}
stateChanged ( /*canUndo, canRedo*/ ) {
this.squire.fireEvent( 'undoStateChange', {
canUndo: this.index > 0,
canRedo: this.index + 1 < this.length
});
this.squire.fireEvent( 'input' );
}
docWasChanged () {
if ( this.inUndoState ) {
this.inUndoState = false;
this.stateChanged ( /*true, false*/ );
} else
this.squire.fireEvent( 'input' );
}
// Leaves bookmark
recordUndoState ( range, replace ) {
replace = replace !== false && this.inUndoState;
// Don't record if we're already in an undo state
if ( !this.inUndoState || replace ) {
// Advance pointer to new position
let undoIndex = this.index;
let undoThreshold = this.threshold;
let undoLimit = this.limit;
let squire = this.squire;
let html;
if ( !replace ) {
++undoIndex;
}
undoIndex = Math.max(0, undoIndex);
// Truncate stack if longer (i.e. if has been previously undone)
this.length = Math.min(undoIndex + 1, this.length);
// Get data
if ( range ) {
squire._saveRangeToBookmark( range );
}
html = squire._getHTML();
// If this document is above the configured size threshold,
// limit the number of saved undo states.
// Threshold is in bytes, JS uses 2 bytes per character
if ( undoThreshold > -1 && html.length * 2 > undoThreshold
&& undoLimit > -1 && undoIndex > undoLimit ) {
this.splice( 0, undoIndex - undoLimit );
}
// Save data
this[ undoIndex ] = html;
this.index = undoIndex;
this.inUndoState = true;
}
}
saveUndoState ( range ) {
let squire = this.squire;
if ( range === undefined ) {
range = squire.getSelection();
}
this.recordUndoState( range );
squire._getRangeAndRemoveBookmark( range );
}
undo () {
let squire = this.squire,
undoIndex = this.index - 1;
// Sanity check: must not be at beginning of the history stack
if ( undoIndex >= 0 || !this.inUndoState ) {
// Make sure any changes since last checkpoint are saved.
this.recordUndoState( squire.getSelection(), false );
this.index = undoIndex;
squire._setHTML( this[ undoIndex ] );
let range = squire._getRangeAndRemoveBookmark();
if ( range ) {
squire.setSelection( range );
}
this.inUndoState = true;
this.stateChanged ( /*undoIndex > 0, true*/ );
}
}
redo () {
// Sanity check: must not be at end of stack and must be in an undo state.
let squire = this.squire,
undoIndex = this.index + 1;
if ( undoIndex < this.length && this.inUndoState ) {
this.index = undoIndex;
squire._setHTML( this[ undoIndex ] );
let range = squire._getRangeAndRemoveBookmark();
if ( range ) {
squire.setSelection( range );
}
this.stateChanged ( /*true, undoIndex + 1 < this.length*/ );
}
}
}
class Squire
{
constructor (root, config) {
@ -2501,10 +2619,7 @@ class Squire
};
this.addEventListener('selectstart', () => this.addEventListener('selectionchange', selectionchange));
this._undoIndex = -1;
this._undoStack = [];
this._undoStackLength = 0;
this._isInUndoState = false;
this.editStack = new EditStack(this);
this._ignoreChange = false;
this._ignoreAllChanges = false;
@ -2558,10 +2673,6 @@ class Squire
setConfig ( config ) {
config = mergeObjects({
blockTag: 'DIV',
undo: {
documentSizeThreshold: -1, // -1 means no threshold
undoLimit: -1 // -1 means no limit
},
addLinks: true
}, config, true );
@ -2663,24 +2774,6 @@ class Squire
return this;
}
destroy () {
let events = this._events;
let type;
for ( type in events ) {
this.removeEventListener( type );
}
if ( this._mutation ) {
this._mutation.disconnect();
}
delete this._root.__squire__;
// Destroy undo stack
this._undoIndex = -1;
this._undoStack = [];
this._undoStackLength = 0;
}
handleEvent ( event ) {
this.fireEvent( event.type, event );
}
@ -2849,7 +2942,7 @@ class Squire
}
let walker = createTreeWalker(
range.commonAncestorContainer,
SHOW_TEXT|SHOW_ELEMENT,
SHOW_ELEMENT_OR_TEXT,
node => isNodeContainedInRange( range, node )
);
let startContainer = range.startContainer;
@ -3024,121 +3117,29 @@ class Squire
_docWasChanged () {
nodeCategoryCache = new WeakMap();
if ( this._ignoreAllChanges ) {
return;
if ( !this._ignoreAllChanges ) {
if ( this._ignoreChange ) {
this._ignoreChange = false;
} else {
this.editStack.docWasChanged();
}
}
if ( this._ignoreChange ) {
this._ignoreChange = false;
return;
}
if ( this._isInUndoState ) {
this._isInUndoState = false;
this.fireEvent( 'undoStateChange', {
canUndo: true,
canRedo: false
});
}
this.fireEvent( 'input' );
}
// Leaves bookmark
_recordUndoState ( range, replace ) {
// Don't record if we're already in an undo state
if ( !this._isInUndoState|| replace ) {
// Advance pointer to new position
let undoIndex = this._undoIndex;
let undoStack = this._undoStack;
let undoConfig = this._config.undo;
let undoThreshold = undoConfig.documentSizeThreshold;
let undoLimit = undoConfig.undoLimit;
let html;
if ( !replace ) {
++undoIndex;
}
// Truncate stack if longer (i.e. if has been previously undone)
if ( undoIndex < this._undoStackLength ) {
undoStack.length = this._undoStackLength = undoIndex;
}
// Get data
if ( range ) {
this._saveRangeToBookmark( range );
}
html = this._getHTML();
// If this document is above the configured size threshold,
// limit the number of saved undo states.
// Threshold is in bytes, JS uses 2 bytes per character
if ( undoThreshold > -1 && html.length * 2 > undoThreshold ) {
if ( undoLimit > -1 && undoIndex > undoLimit ) {
undoStack.splice( 0, undoIndex - undoLimit );
undoIndex = undoLimit;
this._undoStackLength = undoLimit;
}
}
// Save data
undoStack[ undoIndex ] = html;
this._undoIndex = undoIndex;
++this._undoStackLength;
this._isInUndoState = true;
}
this.editStack.recordUndoState( range, replace );
}
saveUndoState ( range ) {
if ( range === undefined ) {
range = this.getSelection();
}
this._recordUndoState( range, this._isInUndoState );
this._getRangeAndRemoveBookmark( range );
return this;
this.editStack.saveUndoState( range );
}
undo () {
// Sanity check: must not be at beginning of the history stack
if ( this._undoIndex !== 0 || !this._isInUndoState ) {
// Make sure any changes since last checkpoint are saved.
this._recordUndoState( this.getSelection(), false );
--this._undoIndex;
this._setHTML( this._undoStack[ this._undoIndex ] );
let range = this._getRangeAndRemoveBookmark();
if ( range ) {
this.setSelection( range );
}
this._isInUndoState = true;
this.fireEvent( 'undoStateChange', {
canUndo: this._undoIndex !== 0,
canRedo: true
});
this.fireEvent( 'input' );
}
return this;
this.editStack.undo();
}
redo () {
// Sanity check: must not be at end of stack and must be in an undo
// state.
let undoIndex = this._undoIndex,
undoStackLength = this._undoStackLength;
if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
++this._undoIndex;
this._setHTML( this._undoStack[ this._undoIndex ] );
let range = this._getRangeAndRemoveBookmark();
if ( range ) {
this.setSelection( range );
}
this.fireEvent( 'undoStateChange', {
canUndo: true,
canRedo: undoIndex + 2 < undoStackLength
});
this.fireEvent( 'input' );
}
return this;
this.editStack.redo();
}
// --- Inline formatting ---
@ -3282,7 +3283,7 @@ class Squire
// them, and adding other styles is harmless.
walker = createTreeWalker(
range.commonAncestorContainer,
SHOW_TEXT|SHOW_ELEMENT,
SHOW_ELEMENT_OR_TEXT,
node => ( node.nodeType === TEXT_NODE ||
node.nodeName === 'BR' ||
node.nodeName === 'IMG'
@ -3515,7 +3516,7 @@ class Squire
}
// 1. Save undo checkpoint and bookmark selection
this._recordUndoState( range, this._isInUndoState );
this._recordUndoState( range );
let root = this._root;
let frag;
@ -3563,7 +3564,7 @@ class Squire
}
// Save undo checkpoint and bookmark selection
this._recordUndoState( range, this._isInUndoState );
this._recordUndoState( range );
// Increase list depth
let type = list.nodeName;
@ -3613,7 +3614,7 @@ class Squire
}
// Save undo checkpoint and bookmark selection
this._recordUndoState( range, this._isInUndoState );
this._recordUndoState( range );
if ( startLi ) {
// Find the new parent list node
@ -3670,13 +3671,6 @@ class Squire
}
}
// --- Keyboard interaction ---
setKeyHandler ( key, fn ) {
this._keyHandlers[ key ] = fn;
return this;
}
// --- Get/Set data ---
_getHTML () {
@ -3751,10 +3745,7 @@ class Squire
fixCursor( root, root );
// Reset the undo stack
this._undoIndex = -1;
this._undoStack.length = 0;
this._undoStackLength = 0;
this._isInUndoState = false;
this.editStack.clear();
// Record undo state
let range = this._getRangeAndRemoveBookmark() ||