mirror of
https://github.com/fastmail/Squire.git
synced 2025-01-03 05:00:13 -05:00
Use mutation observers where possible to detect change
We need to know when the document is modified in order to fire an "input" event and set the undo/redo state correctly. Observing keyup is imprecise, as it's hard to tell whether the key press actually modified anything. Newer browsers support mutation observers, which tell you precisely when something has changed. For IE9/10, Opera 12 and other older browsers, we fall back to observing keyup again. Fixes #26.
This commit is contained in:
parent
5b5d65f684
commit
b69a1635de
4 changed files with 112 additions and 46 deletions
|
@ -36,6 +36,8 @@ var useTextFixer = isIElt11 || isPresto;
|
|||
var cantFocusEmptyTextNodes = isIElt11 || isWebKit;
|
||||
var losesSelectionOnBlur = isIElt11;
|
||||
|
||||
var canObserveMutations = typeof MutationObserver !== 'undefined';
|
||||
|
||||
// Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space
|
||||
var notWS = /[^ \t\r\n]/;
|
||||
|
||||
|
@ -1081,6 +1083,8 @@ var instances = [];
|
|||
function Squire ( doc ) {
|
||||
var win = doc.defaultView;
|
||||
var body = doc.body;
|
||||
var mutation;
|
||||
|
||||
this._win = win;
|
||||
this._doc = doc;
|
||||
this._body = body;
|
||||
|
@ -1112,11 +1116,23 @@ function Squire ( doc ) {
|
|||
this._undoStack = [];
|
||||
this._undoStackLength = 0;
|
||||
this._isInUndoState = false;
|
||||
this._ignoreChange = false;
|
||||
|
||||
if ( canObserveMutations ) {
|
||||
mutation = new MutationObserver( this._docWasChanged.bind( this ) );
|
||||
mutation.observe( body, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
});
|
||||
this._mutation = mutation;
|
||||
} else {
|
||||
this.addEventListener( 'keyup', this._keyUpDetectChange );
|
||||
}
|
||||
|
||||
this.defaultBlockProperties = undefined;
|
||||
|
||||
this.addEventListener( 'keyup', this._docWasChanged );
|
||||
|
||||
// IE sometimes fires the beforepaste event twice; make sure it is not run
|
||||
// again before our after paste function is called.
|
||||
this._awaitingPaste = false;
|
||||
|
@ -1238,6 +1254,9 @@ proto.destroy = function () {
|
|||
doc.removeEventListener( type, this, true );
|
||||
}
|
||||
}
|
||||
if ( this._mutation ) {
|
||||
this._mutation.disconnect();
|
||||
}
|
||||
var l = instances.length;
|
||||
while ( l-- ) {
|
||||
if ( instances[l] === this ) {
|
||||
|
@ -1514,15 +1533,24 @@ proto._getRangeAndRemoveBookmark = function ( range ) {
|
|||
|
||||
// --- Undo ---
|
||||
|
||||
proto._docWasChanged = function ( event ) {
|
||||
var code = event && event.keyCode;
|
||||
proto._keyUpDetectChange = function ( event ) {
|
||||
var code = event.keyCode;
|
||||
// Presume document was changed if:
|
||||
// 1. A modifier key (other than shift) wasn't held down
|
||||
// 2. The key pressed is not in range 16<=x<=20 (control keys)
|
||||
// 3. The key pressed is not in range 33<=x<=45 (navigation keys)
|
||||
if ( !event || ( !event.ctrlKey && !event.metaKey && !event.altKey &&
|
||||
if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
|
||||
( code < 16 || code > 20 ) &&
|
||||
( code < 33 || code > 45 ) ) ) {
|
||||
( code < 33 || code > 45 ) ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
};
|
||||
|
||||
proto._docWasChanged = function () {
|
||||
if ( canObserveMutations && this._ignoreChange ) {
|
||||
this._ignoreChange = false;
|
||||
return;
|
||||
}
|
||||
if ( this._isInUndoState ) {
|
||||
this._isInUndoState = false;
|
||||
this.fireEvent( 'undoStateChange', {
|
||||
|
@ -1531,7 +1559,6 @@ proto._docWasChanged = function ( event ) {
|
|||
});
|
||||
}
|
||||
this.fireEvent( 'input' );
|
||||
}
|
||||
};
|
||||
|
||||
// Leaves bookmark
|
||||
|
@ -1564,6 +1591,7 @@ proto.undo = function () {
|
|||
this._recordUndoState( this.getSelection() );
|
||||
|
||||
this._undoIndex -= 1;
|
||||
this._ignoreChange = true;
|
||||
this._setHTML( this._undoStack[ this._undoIndex ] );
|
||||
var range = this._getRangeAndRemoveBookmark();
|
||||
if ( range ) {
|
||||
|
@ -1586,6 +1614,7 @@ proto.redo = function () {
|
|||
undoStackLength = this._undoStackLength;
|
||||
if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
|
||||
this._undoIndex += 1;
|
||||
this._ignoreChange = true;
|
||||
this._setHTML( this._undoStack[ this._undoIndex ] );
|
||||
var range = this._getRangeAndRemoveBookmark();
|
||||
if ( range ) {
|
||||
|
@ -1863,7 +1892,9 @@ proto.changeFormat = function ( add, remove, range, partial ) {
|
|||
this._updatePath( range, true );
|
||||
|
||||
// We're not still in an undo state
|
||||
if ( !canObserveMutations ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -1927,8 +1958,10 @@ proto.forEachBlock = function ( fn, mutates, range ) {
|
|||
this._updatePath( range, true );
|
||||
|
||||
// We're not still in an undo state
|
||||
if ( !canObserveMutations ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -1968,7 +2001,9 @@ proto.modifyBlocks = function ( modify, range ) {
|
|||
this._updatePath( range, true );
|
||||
|
||||
// 7. We're not still in an undo state
|
||||
if ( !canObserveMutations ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -2631,7 +2666,9 @@ proto._onPaste = function ( event ) {
|
|||
// Insert pasted data
|
||||
if ( doPaste ) {
|
||||
insertTreeFragmentIntoRange( range, frag );
|
||||
if ( !canObserveMutations ) {
|
||||
self._docWasChanged();
|
||||
}
|
||||
range.collapse( false );
|
||||
self._ensureBottomLine();
|
||||
}
|
||||
|
@ -2760,7 +2797,6 @@ var keyHandlers = {
|
|||
range.collapse( false );
|
||||
self.setSelection( range );
|
||||
self._updatePath( range, true );
|
||||
self._docWasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2872,9 +2908,6 @@ var keyHandlers = {
|
|||
body.offsetHeight ) {
|
||||
nodeAfterSplit.scrollIntoView( false );
|
||||
}
|
||||
|
||||
// We're not still in an undo state
|
||||
self._docWasChanged();
|
||||
},
|
||||
backspace: function ( self, event, range ) {
|
||||
self._removeZWS();
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -31,6 +31,8 @@ var useTextFixer = isIElt11 || isPresto;
|
|||
var cantFocusEmptyTextNodes = isIElt11 || isWebKit;
|
||||
var losesSelectionOnBlur = isIElt11;
|
||||
|
||||
var canObserveMutations = typeof MutationObserver !== 'undefined';
|
||||
|
||||
// Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space
|
||||
var notWS = /[^ \t\r\n]/;
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ var instances = [];
|
|||
function Squire ( doc ) {
|
||||
var win = doc.defaultView;
|
||||
var body = doc.body;
|
||||
var mutation;
|
||||
|
||||
this._win = win;
|
||||
this._doc = doc;
|
||||
this._body = body;
|
||||
|
@ -36,11 +38,23 @@ function Squire ( doc ) {
|
|||
this._undoStack = [];
|
||||
this._undoStackLength = 0;
|
||||
this._isInUndoState = false;
|
||||
this._ignoreChange = false;
|
||||
|
||||
if ( canObserveMutations ) {
|
||||
mutation = new MutationObserver( this._docWasChanged.bind( this ) );
|
||||
mutation.observe( body, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
});
|
||||
this._mutation = mutation;
|
||||
} else {
|
||||
this.addEventListener( 'keyup', this._keyUpDetectChange );
|
||||
}
|
||||
|
||||
this.defaultBlockProperties = undefined;
|
||||
|
||||
this.addEventListener( 'keyup', this._docWasChanged );
|
||||
|
||||
// IE sometimes fires the beforepaste event twice; make sure it is not run
|
||||
// again before our after paste function is called.
|
||||
this._awaitingPaste = false;
|
||||
|
@ -162,6 +176,9 @@ proto.destroy = function () {
|
|||
doc.removeEventListener( type, this, true );
|
||||
}
|
||||
}
|
||||
if ( this._mutation ) {
|
||||
this._mutation.disconnect();
|
||||
}
|
||||
var l = instances.length;
|
||||
while ( l-- ) {
|
||||
if ( instances[l] === this ) {
|
||||
|
@ -438,15 +455,24 @@ proto._getRangeAndRemoveBookmark = function ( range ) {
|
|||
|
||||
// --- Undo ---
|
||||
|
||||
proto._docWasChanged = function ( event ) {
|
||||
var code = event && event.keyCode;
|
||||
proto._keyUpDetectChange = function ( event ) {
|
||||
var code = event.keyCode;
|
||||
// Presume document was changed if:
|
||||
// 1. A modifier key (other than shift) wasn't held down
|
||||
// 2. The key pressed is not in range 16<=x<=20 (control keys)
|
||||
// 3. The key pressed is not in range 33<=x<=45 (navigation keys)
|
||||
if ( !event || ( !event.ctrlKey && !event.metaKey && !event.altKey &&
|
||||
if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
|
||||
( code < 16 || code > 20 ) &&
|
||||
( code < 33 || code > 45 ) ) ) {
|
||||
( code < 33 || code > 45 ) ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
};
|
||||
|
||||
proto._docWasChanged = function () {
|
||||
if ( canObserveMutations && this._ignoreChange ) {
|
||||
this._ignoreChange = false;
|
||||
return;
|
||||
}
|
||||
if ( this._isInUndoState ) {
|
||||
this._isInUndoState = false;
|
||||
this.fireEvent( 'undoStateChange', {
|
||||
|
@ -455,7 +481,6 @@ proto._docWasChanged = function ( event ) {
|
|||
});
|
||||
}
|
||||
this.fireEvent( 'input' );
|
||||
}
|
||||
};
|
||||
|
||||
// Leaves bookmark
|
||||
|
@ -488,6 +513,7 @@ proto.undo = function () {
|
|||
this._recordUndoState( this.getSelection() );
|
||||
|
||||
this._undoIndex -= 1;
|
||||
this._ignoreChange = true;
|
||||
this._setHTML( this._undoStack[ this._undoIndex ] );
|
||||
var range = this._getRangeAndRemoveBookmark();
|
||||
if ( range ) {
|
||||
|
@ -510,6 +536,7 @@ proto.redo = function () {
|
|||
undoStackLength = this._undoStackLength;
|
||||
if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
|
||||
this._undoIndex += 1;
|
||||
this._ignoreChange = true;
|
||||
this._setHTML( this._undoStack[ this._undoIndex ] );
|
||||
var range = this._getRangeAndRemoveBookmark();
|
||||
if ( range ) {
|
||||
|
@ -787,7 +814,9 @@ proto.changeFormat = function ( add, remove, range, partial ) {
|
|||
this._updatePath( range, true );
|
||||
|
||||
// We're not still in an undo state
|
||||
if ( !canObserveMutations ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -851,8 +880,10 @@ proto.forEachBlock = function ( fn, mutates, range ) {
|
|||
this._updatePath( range, true );
|
||||
|
||||
// We're not still in an undo state
|
||||
if ( !canObserveMutations ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -892,7 +923,9 @@ proto.modifyBlocks = function ( modify, range ) {
|
|||
this._updatePath( range, true );
|
||||
|
||||
// 7. We're not still in an undo state
|
||||
if ( !canObserveMutations ) {
|
||||
this._docWasChanged();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -1555,7 +1588,9 @@ proto._onPaste = function ( event ) {
|
|||
// Insert pasted data
|
||||
if ( doPaste ) {
|
||||
insertTreeFragmentIntoRange( range, frag );
|
||||
if ( !canObserveMutations ) {
|
||||
self._docWasChanged();
|
||||
}
|
||||
range.collapse( false );
|
||||
self._ensureBottomLine();
|
||||
}
|
||||
|
@ -1684,7 +1719,6 @@ var keyHandlers = {
|
|||
range.collapse( false );
|
||||
self.setSelection( range );
|
||||
self._updatePath( range, true );
|
||||
self._docWasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1796,9 +1830,6 @@ var keyHandlers = {
|
|||
body.offsetHeight ) {
|
||||
nodeAfterSplit.scrollIntoView( false );
|
||||
}
|
||||
|
||||
// We're not still in an undo state
|
||||
self._docWasChanged();
|
||||
},
|
||||
backspace: function ( self, event, range ) {
|
||||
self._removeZWS();
|
||||
|
|
Loading…
Reference in a new issue