0
Fork 0
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:
Neil Jenkins 2014-12-27 13:48:15 +07:00
parent 5b5d65f684
commit b69a1635de
4 changed files with 112 additions and 46 deletions

View file

@ -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

View file

@ -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]/;

View file

@ -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();