0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-31 11:54:03 -05:00

Update to latest Squire

This commit is contained in:
Neil Jenkins 2016-09-05 09:20:41 -04:00
parent 4ec6f03d68
commit 06ef2f41fb
2 changed files with 387 additions and 188 deletions

View file

@ -17,6 +17,11 @@ var START_TO_END = 1; // Range.START_TO_END
var END_TO_END = 2; // Range.END_TO_END
var END_TO_START = 3; // Range.END_TO_START
var HIGHLIGHT_CLASS = 'highlight';
var COLOUR_CLASS = 'colour';
var FONT_FAMILY_CLASS = 'font';
var FONT_SIZE_CLASS = 'size';
var ZWS = '\u200B';
var win = doc.defaultView;
@ -26,11 +31,14 @@ var ua = navigator.userAgent;
var isIOS = /iP(?:ad|hone|od)/.test( ua );
var isMac = /Mac OS X/.test( ua );
var isAndroid = /Android/.test( ua );
var isGecko = /Gecko\//.test( ua );
var isIElt11 = /Trident\/[456]\./.test( ua );
var isPresto = !!win.opera;
var isEdge = /Edge\//.test( ua );
var isWebKit = !isEdge && /WebKit\//.test( ua );
var isIE = /Trident\/[4567]\./.test( ua );
var ctrlKey = isMac ? 'meta-' : 'ctrl-';
@ -171,7 +179,7 @@ TreeWalker.prototype.previousPONode = function () {
}
};
var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|U|VAR|WBR)$/;
var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/;
var leafNodeNames = {
BR: 1,
@ -284,6 +292,23 @@ function getPath ( node, root ) {
if ( dir = node.dir ) {
path += '[dir=' + dir + ']';
}
if ( classNames ) {
if ( indexOf.call( classNames, HIGHLIGHT_CLASS ) > -1 ) {
path += '[backgroundColor=' +
node.style.backgroundColor.replace( / /g,'' ) + ']';
}
if ( indexOf.call( classNames, COLOUR_CLASS ) > -1 ) {
path += '[color=' +
node.style.color.replace( / /g,'' ) + ']';
}
if ( indexOf.call( classNames, FONT_FAMILY_CLASS ) > -1 ) {
path += '[fontFamily=' +
node.style.fontFamily.replace( / /g,'' ) + ']';
}
if ( indexOf.call( classNames, FONT_SIZE_CLASS ) > -1 ) {
path += '[fontSize=' + node.style.fontSize + ']';
}
}
}
}
return path;
@ -522,10 +547,7 @@ function split ( node, offset, stopNode, root ) {
return offset;
}
function mergeInlines ( node, range ) {
if ( node.nodeType !== ELEMENT_NODE ) {
return;
}
function _mergeInlines ( node, fakeRange ) {
var children = node.childNodes,
l = children.length,
frags = [],
@ -535,30 +557,30 @@ function mergeInlines ( node, range ) {
prev = l && children[ l - 1 ];
if ( l && isInline( child ) && areAlike( child, prev ) &&
!leafNodeNames[ child.nodeName ] ) {
if ( range.startContainer === child ) {
range.startContainer = prev;
range.startOffset += getLength( prev );
if ( fakeRange.startContainer === child ) {
fakeRange.startContainer = prev;
fakeRange.startOffset += getLength( prev );
}
if ( range.endContainer === child ) {
range.endContainer = prev;
range.endOffset += getLength( prev );
if ( fakeRange.endContainer === child ) {
fakeRange.endContainer = prev;
fakeRange.endOffset += getLength( prev );
}
if ( range.startContainer === node ) {
if ( range.startOffset > l ) {
range.startOffset -= 1;
if ( fakeRange.startContainer === node ) {
if ( fakeRange.startOffset > l ) {
fakeRange.startOffset -= 1;
}
else if ( range.startOffset === l ) {
range.startContainer = prev;
range.startOffset = getLength( prev );
else if ( fakeRange.startOffset === l ) {
fakeRange.startContainer = prev;
fakeRange.startOffset = getLength( prev );
}
}
if ( range.endContainer === node ) {
if ( range.endOffset > l ) {
range.endOffset -= 1;
if ( fakeRange.endContainer === node ) {
if ( fakeRange.endOffset > l ) {
fakeRange.endOffset -= 1;
}
else if ( range.endOffset === l ) {
range.endContainer = prev;
range.endOffset = getLength( prev );
else if ( fakeRange.endOffset === l ) {
fakeRange.endContainer = prev;
fakeRange.endOffset = getLength( prev );
}
}
detach( child );
@ -574,14 +596,31 @@ function mergeInlines ( node, range ) {
while ( len-- ) {
child.appendChild( frags.pop() );
}
mergeInlines( child, range );
_mergeInlines( child, fakeRange );
}
}
}
function mergeInlines ( node, range ) {
if ( node.nodeType === TEXT_NODE ) {
node = node.parentNode;
}
if ( node.nodeType === ELEMENT_NODE ) {
var fakeRange = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
_mergeInlines( node, fakeRange );
range.setStart( fakeRange.startContainer, fakeRange.startOffset );
range.setEnd( fakeRange.endContainer, fakeRange.endOffset );
}
}
function mergeWithBlock ( block, next, range ) {
var container = next,
last, offset, _range;
last, offset;
while ( container.parentNode.childNodes.length === 1 ) {
container = container.parentNode;
}
@ -596,18 +635,11 @@ function mergeWithBlock ( block, next, range ) {
offset -= 1;
}
_range = {
startContainer: block,
startOffset: offset,
endContainer: block,
endOffset: offset
};
block.appendChild( empty( next ) );
mergeInlines( block, _range );
range.setStart( _range.startContainer, _range.startOffset );
range.setStart( block, offset );
range.collapse( true );
mergeInlines( block, range );
// Opera inserts a BR if you delete the last piece of text
// in a block-level element. Unfortunately, it then gets
@ -862,6 +894,10 @@ var insertTreeFragmentIntoRange = function ( range, frag, root ) {
if ( allInline ) {
// If inline, just insert at the current position.
insertNodeInRange( range, frag );
if ( range.startContainer !== range.endContainer ) {
mergeInlines( range.endContainer, range );
}
mergeInlines( range.startContainer, range );
range.collapse( false );
} else {
// Otherwise...
@ -1300,7 +1336,7 @@ var afterDelete = function ( self, range ) {
node = parent;
parent = node.parentNode;
}
// If focussed in empty inline element
// If focused in empty inline element
if ( node !== parent ) {
// Move focus to just before empty inline(s)
range.setStart( parent,
@ -1622,6 +1658,13 @@ var keyHandlers = {
!node.nextSibling && range.endOffset === getLength( node ) ) {
range.setStartAfter( parent );
}
// Delete the selection if not collapsed
else if ( !range.collapsed ) {
deleteContentsOfRange( range, self._root );
self._ensureBottomLine();
self.setSelection( range );
self._updatePath( range, true );
}
self.setSelection( range );
},
@ -1690,12 +1733,12 @@ var fontSizes = {
7: 48
};
var spanToSemantic = {
var styleToSemantic = {
backgroundColor: {
regexp: notWS,
replace: function ( doc, colour ) {
return createElement( doc, 'SPAN', {
'class': 'highlight',
'class': HIGHLIGHT_CLASS,
style: 'background-color:' + colour
});
}
@ -1704,7 +1747,7 @@ var spanToSemantic = {
regexp: notWS,
replace: function ( doc, colour ) {
return createElement( doc, 'SPAN', {
'class': 'colour',
'class': COLOUR_CLASS,
style: 'color:' + colour
});
}
@ -1725,7 +1768,7 @@ var spanToSemantic = {
regexp: notWS,
replace: function ( doc, family ) {
return createElement( doc, 'SPAN', {
'class': 'font',
'class': FONT_FAMILY_CLASS,
style: 'font-family:' + family
});
}
@ -1734,10 +1777,16 @@ var spanToSemantic = {
regexp: notWS,
replace: function ( doc, size ) {
return createElement( doc, 'SPAN', {
'class': 'size',
'class': FONT_SIZE_CLASS,
style: 'font-size:' + size
});
}
},
textDecoration: {
regexp: /^underline/i,
replace: function ( doc ) {
return createElement( doc, 'U' );
}
}
};
@ -1750,36 +1799,45 @@ var replaceWithTag = function ( tag ) {
};
};
var stylesRewriters = {
SPAN: function ( span, parent ) {
var style = span.style,
doc = span.ownerDocument,
attr, converter, css, newTreeBottom, newTreeTop, el;
var replaceStyles = function ( node, parent ) {
var style = node.style;
var doc = node.ownerDocument;
var attr, converter, css, newTreeBottom, newTreeTop, el;
for ( attr in spanToSemantic ) {
converter = spanToSemantic[ attr ];
for ( attr in styleToSemantic ) {
converter = styleToSemantic[ attr ];
css = style[ attr ];
if ( css && converter.regexp.test( css ) ) {
el = converter.replace( doc, css );
if ( !newTreeTop ) {
newTreeTop = el;
}
if ( newTreeBottom ) {
newTreeBottom.appendChild( el );
}
newTreeBottom = el;
if ( !newTreeTop ) {
newTreeTop = el;
}
node.style[ attr ] = '';
}
}
if ( newTreeTop ) {
newTreeBottom.appendChild( empty( span ) );
parent.replaceChild( newTreeTop, span );
newTreeBottom.appendChild( empty( node ) );
if ( node.nodeName === 'SPAN' ) {
parent.replaceChild( newTreeTop, node );
} else {
node.appendChild( newTreeTop );
}
}
return newTreeBottom || span;
},
return newTreeBottom || node;
};
var stylesRewriters = {
P: replaceStyles,
SPAN: replaceStyles,
STRONG: replaceWithTag( 'B' ),
EM: replaceWithTag( 'I' ),
INS: replaceWithTag( 'U' ),
STRIKE: replaceWithTag( 'S' ),
FONT: function ( node, parent ) {
var face = node.face,
@ -2059,12 +2117,32 @@ var onCopy = function ( event ) {
var clipboardData = event.clipboardData;
var range = this.getSelection();
var node = this.createElement( 'div' );
var root = this._root;
var startBlock, contents, parent, newContents;
// Edge only seems to support setting plain text as of 2016-03-11.
// Mobile Safari flat out doesn't work:
// https://bugs.webkit.org/show_bug.cgi?id=143776
if ( !isEdge && !isIOS && clipboardData ) {
node.appendChild( range.cloneContents() );
range = range.cloneRange();
startBlock = getStartBlockOfRange( range, root );
if ( startBlock === getEndBlockOfRange( range, root ) ) {
// Copy all inline formatting, but that's it.
moveRangeBoundariesDownTree( range );
moveRangeBoundariesUpTree( range, startBlock );
contents = range.cloneContents();
} else {
moveRangeBoundariesUpTree( range, root );
contents = range.cloneContents();
parent = range.commonAncestorContainer;
while ( parent && parent !== root ) {
newContents = parent.cloneNode( false );
newContents.appendChild( contents );
contents = newContents;
parent = parent.parentNode;
}
}
node.appendChild( contents );
clipboardData.setData( 'text/html', node.innerHTML );
clipboardData.setData( 'text/plain',
node.innerText || node.textContent );
@ -2072,14 +2150,21 @@ var onCopy = function ( event ) {
}
};
// Need to monitor for shift key like this, as event.shiftKey is not available
// in paste event.
function monitorShiftKey ( event ) {
this.isShiftDown = event.shiftKey;
}
var onPaste = function ( event ) {
var clipboardData = event.clipboardData,
items = clipboardData && clipboardData.items,
fireDrop = false,
hasImage = false,
plainItem = null,
self = this,
l, item, type, types, data;
var clipboardData = event.clipboardData;
var items = clipboardData && clipboardData.items;
var choosePlain = this.isShiftDown;
var fireDrop = false;
var hasImage = false;
var plainItem = null;
var self = this;
var l, item, type, types, data;
// Current HTML5 Clipboard interface
// ---------------------------------
@ -2092,7 +2177,7 @@ var onPaste = function ( event ) {
while ( l-- ) {
item = items[l];
type = item.type;
if ( type === 'text/html' ) {
if ( !choosePlain && type === 'text/html' ) {
/*jshint loopfunc: true */
item.getAsString( function ( html ) {
self.insertHTML( html, true );
@ -2103,7 +2188,7 @@ var onPaste = function ( event ) {
if ( type === 'text/plain' ) {
plainItem = item;
}
if ( /^image\/.*/.test( type ) ) {
if ( !choosePlain && /^image\/.*/.test( type ) ) {
hasImage = true;
}
}
@ -2155,7 +2240,7 @@ var onPaste = function ( event ) {
// insert plain text instead. On iOS, Facebook (and possibly other
// apps?) copy links as type text/uri-list, but also insert a **blank**
// text/plain item onto the clipboard. Why? Who knows.
if (( data = clipboardData.getData( 'text/html' ) )) {
if ( !choosePlain && ( data = clipboardData.getData( 'text/html' ) ) ) {
this.insertHTML( data, true );
} else if (
( data = clipboardData.getData( 'text/plain' ) ) ||
@ -2267,17 +2352,21 @@ function getSquireInstance ( doc ) {
return null;
}
function mergeObjects ( base, extras ) {
function mergeObjects ( base, extras, mayOverride ) {
var prop, value;
if ( !base ) {
base = {};
}
if ( extras ) {
for ( prop in extras ) {
if ( mayOverride || !( prop in base ) ) {
value = extras[ prop ];
base[ prop ] = ( value && value.constructor === Object ) ?
mergeObjects( base[ prop ], value ) :
mergeObjects( base[ prop ], value, mayOverride ) :
value;
}
}
}
return base;
}
@ -2295,6 +2384,7 @@ function Squire ( root, config ) {
this._events = {};
this._isFocused = false;
this._lastSelection = null;
// IE loses selection state of iframe on blur, so make sure we
@ -2308,6 +2398,7 @@ function Squire ( root, config ) {
this._lastAnchorNode = null;
this._lastFocusNode = null;
this._path = '';
this._willUpdatePath = false;
if ( 'onselectionchange' in doc ) {
this.addEventListener( 'selectionchange', this._updatePathOnEvent );
@ -2336,13 +2427,11 @@ function Squire ( root, config ) {
this.addEventListener( 'keyup', this._keyUpDetectChange );
}
// On blur, restore focus except if there is any change to the content, or
// the user taps or clicks to focus a specific point. Can't actually use
// click event because focus happens before click, so use
// mousedown/touchstart
// On blur, restore focus except if the user taps or clicks to focus a
// specific point. Can't actually use click event because focus happens
// before click, so use mousedown/touchstart
this._restoreSelection = false;
this.addEventListener( 'blur', enableRestoreSelection );
this.addEventListener( 'input', disableRestoreSelection );
this.addEventListener( 'mousedown', disableRestoreSelection );
this.addEventListener( 'touchstart', disableRestoreSelection );
this.addEventListener( 'focus', restoreSelection );
@ -2352,6 +2441,8 @@ function Squire ( root, config ) {
this._awaitingPaste = false;
this.addEventListener( isIElt11 ? 'beforecut' : 'cut', onCut );
this.addEventListener( 'copy', onCopy );
this.addEventListener( 'keydown', monitorShiftKey );
this.addEventListener( 'keyup', monitorShiftKey );
this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', onPaste );
this.addEventListener( 'drop', onDrop );
@ -2420,8 +2511,13 @@ proto.setConfig = function ( config ) {
ol: null,
li: null,
a: null
},
leafNodeNames: leafNodeNames,
undo: {
documentSizeThreshold: -1, // -1 means no threshold
undoLimit: -1 // -1 means no limit
}
}, config );
}, config, true );
// Users may specify block tag in lower case
config.blockTag = config.blockTag.toUpperCase();
@ -2483,8 +2579,26 @@ var customEvents = {
};
proto.fireEvent = function ( type, event ) {
var handlers = this._events[ type ],
l, obj;
var handlers = this._events[ type ];
var isFocused, l, obj;
// UI code, especially modal views, may be monitoring for focus events and
// immediately removing focus. In certain conditions, this can cause the
// focus event to fire after the blur event, which can cause an infinite
// loop. So we detect whether we're actually focused/blurred before firing.
if ( /^(?:focus|blur)/.test( type ) ) {
isFocused = isOrContains( this._root, this._doc.activeElement );
if ( type === 'focus' ) {
if ( !isFocused || this._isFocused ) {
return this;
}
this._isFocused = true;
} else {
if ( isFocused || !this._isFocused ) {
return this;
}
this._isFocused = false;
}
}
if ( handlers ) {
if ( !event ) {
event = {};
@ -2516,6 +2630,7 @@ proto.destroy = function () {
var l = instances.length;
var events = this._events;
var type;
for ( type in events ) {
this.removeEventListener( type );
}
@ -2527,6 +2642,11 @@ proto.destroy = function () {
instances.splice( l, 1 );
}
}
// Destroy undo stack
this._undoIndex = -1;
this._undoStack = [];
this._undoStackLength = 0;
};
proto.handleEvent = function ( event ) {
@ -2617,12 +2737,7 @@ proto.getCursorPosition = function ( range ) {
rect = node.getBoundingClientRect();
parent = node.parentNode;
parent.removeChild( node );
mergeInlines( parent, {
startContainer: range.startContainer,
endContainer: range.endContainer,
startOffset: range.startOffset,
endOffset: range.endOffset
});
mergeInlines( parent, range );
}
return rect;
};
@ -2647,9 +2762,22 @@ var getWindowSelection = function ( self ) {
proto.setSelection = function ( range ) {
if ( range ) {
// If we're setting selection, that automatically, and synchronously, // triggers a focus event. Don't want a reentrant call to setSelection.
this._restoreSelection = false;
this._lastSelection = range;
// If we're setting selection, that automatically, and synchronously, // triggers a focus event. So just store the selection and mark it as
// needing restore on focus.
if ( !this._isFocused ) {
enableRestoreSelection.call( this );
} else if ( isAndroid && !this._restoreSelection ) {
// Android closes the keyboard on removeAllRanges() and doesn't
// open it again when addRange() is called, sigh.
// Since Android doesn't trigger a focus event in setSelection(),
// use a blur/focus dance to work around this by letting the
// selection be restored on focus.
// Need to check for !this._restoreSelection to avoid infinite loop
enableRestoreSelection.call( this );
this.blur();
this.focus();
} else {
// iOS bug: if you don't focus the iframe before setting the
// selection, you can end up in a state where you type but the input
// doesn't get directed into the contenteditable area but is instead
@ -2663,6 +2791,7 @@ proto.setSelection = function ( range ) {
sel.addRange( range );
}
}
}
return this;
};
@ -2758,13 +2887,18 @@ proto.getPath = function () {
// WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
var removeZWS = function ( root ) {
// Walk down the tree starting at the root and remove any ZWS. If the node only
// contained ZWS space then remove it too. We may want to keep one ZWS node at
// the bottom of the tree so the block can be selected. Define that node as the
// keepNode.
var removeZWS = function ( root, keepNode ) {
var walker = new TreeWalker( root, SHOW_TEXT, function () {
return true;
}, false ),
parent, node, index;
while ( node = walker.nextNode() ) {
while ( ( index = node.data.indexOf( ZWS ) ) > -1 ) {
while ( ( index = node.data.indexOf( ZWS ) ) > -1 &&
( !keepNode || node.parentNode !== keepNode ) ) {
if ( node.length === 1 ) {
do {
parent = node.parentNode;
@ -2813,19 +2947,39 @@ proto._updatePath = function ( range, force ) {
}
};
// selectionchange is fired synchronously in IE when removing current selection
// and when setting new selection; keyup/mouseup may have processing we want
// to do first. Either way, send to next event loop.
proto._updatePathOnEvent = function () {
this._updatePath( this.getSelection() );
var self = this;
if ( !self._willUpdatePath ) {
self._willUpdatePath = true;
setTimeout( function () {
self._willUpdatePath = false;
self._updatePath( self.getSelection() );
}, 0 );
}
};
// --- Focus ---
proto.focus = function () {
this._root.focus();
if ( isIE ) {
this.fireEvent( 'focus' );
}
return this;
};
proto.blur = function () {
this._root.blur();
if ( isIE ) {
this.fireEvent( 'blur' );
}
return this;
};
@ -2871,38 +3025,31 @@ proto._getRangeAndRemoveBookmark = function ( range ) {
if ( start && end ) {
var startContainer = start.parentNode,
endContainer = end.parentNode,
collapsed;
var _range = {
startContainer: startContainer,
endContainer: endContainer,
startOffset: indexOf.call( startContainer.childNodes, start ),
endOffset: indexOf.call( endContainer.childNodes, end )
};
startOffset = indexOf.call( startContainer.childNodes, start ),
endOffset = indexOf.call( endContainer.childNodes, end );
if ( startContainer === endContainer ) {
_range.endOffset -= 1;
endOffset -= 1;
}
detach( start );
detach( end );
// Merge any text nodes we split
mergeInlines( startContainer, _range );
if ( startContainer !== endContainer ) {
mergeInlines( endContainer, _range );
}
if ( !range ) {
range = this._doc.createRange();
}
range.setStart( _range.startContainer, _range.startOffset );
range.setEnd( _range.endContainer, _range.endOffset );
collapsed = range.collapsed;
range.setStart( startContainer, startOffset );
range.setEnd( endContainer, endOffset );
// Merge any text nodes we split
mergeInlines( startContainer, range );
if ( startContainer !== endContainer ) {
mergeInlines( endContainer, range );
}
// If we didn't split a text node, we should move into any adjacent
// text node to current selection point
if ( collapsed ) {
if ( range.collapsed ) {
startContainer = range.startContainer;
if ( startContainer.nodeType === TEXT_NODE ) {
endContainer = startContainer.childNodes[ range.startOffset ];
@ -2959,19 +3106,37 @@ proto._recordUndoState = function ( range ) {
// Don't record if we're already in an undo state
if ( !this._isInUndoState ) {
// Advance pointer to new position
var undoIndex = this._undoIndex += 1,
undoStack = this._undoStack;
var undoIndex = this._undoIndex += 1;
var undoStack = this._undoStack;
var undoConfig = this._config.undo;
var undoThreshold = undoConfig.documentSizeThreshold;
var undoLimit = undoConfig.undoLimit;
var html;
// Truncate stack if longer (i.e. if has been previously undone)
if ( undoIndex < this._undoStackLength ) {
undoStack.length = this._undoStackLength = undoIndex;
}
// Write out data
// Get data
if ( range ) {
this._saveRangeToBookmark( range );
}
undoStack[ undoIndex ] = this._getHTML();
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 = this._undoIndex = undoLimit;
this._undoStackLength = undoLimit;
}
}
// Save data
undoStack[ undoIndex ] = html;
this._undoStackLength += 1;
this._isInUndoState = true;
}
@ -3141,13 +3306,21 @@ proto._addFormat = function ( tag, attributes, range ) {
// it round the range and focus it.
var root = this._root;
var el, walker, startContainer, endContainer, startOffset, endOffset,
node, needsFormat;
node, needsFormat, block;
if ( range.collapsed ) {
el = fixCursor( this.createElement( tag, attributes ), root );
insertNodeInRange( range, el );
range.setStart( el.firstChild, el.firstChild.length );
range.collapse( true );
// Clean up any previous formats that may have been set on this block
// that are unused.
block = el;
while ( isInline( block ) ) {
block = block.parentNode;
}
removeZWS( block, el );
}
// Otherwise we find all the textnodes in the range (splitting
// partially selected nodes) and if they're not already formatted
@ -3162,8 +3335,8 @@ proto._addFormat = function ( tag, attributes, range ) {
// to apply when the user types something in the block, which is
// presumably what was intended.
//
// IMG tags are included because we may want to create a link around them,
// and adding other styles is harmless.
// IMG tags are included because we may want to create a link around
// them, and adding other styles is harmless.
walker = new TreeWalker(
range.commonAncestorContainer,
SHOW_TEXT|SHOW_ELEMENT,
@ -3342,15 +3515,7 @@ proto._removeFormat = function ( tag, attributes, range, partial ) {
if ( fixer ) {
range.collapse( false );
}
var _range = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
mergeInlines( root, _range );
range.setStart( _range.startContainer, _range.startOffset );
range.setEnd( _range.endContainer, _range.endOffset );
mergeInlines( root, range );
return range;
};
@ -3358,7 +3523,7 @@ proto._removeFormat = function ( tag, attributes, range, partial ) {
proto.changeFormat = function ( add, remove, range, partial ) {
// Normalise the arguments and get selection
if ( !range && !( range = this.getSelection() ) ) {
return;
return this;
}
// Save undo checkpoint
@ -3769,6 +3934,7 @@ proto.setHTML = function ( html ) {
// anything calls getSelection before first focus, we have a range
// to return.
this._lastSelection = range;
enableRestoreSelection.call( this );
this._updatePath( range, true );
return this;
@ -3820,7 +3986,7 @@ proto.insertElement = function ( el, range ) {
proto.insertImage = function ( src, attributes ) {
var img = this.createElement( 'IMG', mergeObjects({
src: src
}, attributes ));
}, attributes, true ));
this.insertElement( img );
return img;
};
@ -3851,7 +4017,7 @@ var addLinks = function ( frag, root, self ) {
match[1] :
'http://' + match[1] :
'mailto:' + match[2]
}, defaultAttributes ));
}, defaultAttributes, false ));
child.textContent = data.slice( index, endIndex );
parent.insertBefore( child, node );
node.data = data = data.slice( endIndex );
@ -3864,15 +4030,21 @@ var addLinks = function ( frag, root, self ) {
// by the html being inserted.
proto.insertHTML = function ( html, isPaste ) {
var range = this.getSelection();
var frag = this._doc.createDocumentFragment();
var div = this.createElement( 'DIV' );
var doc = this._doc;
var startFragmentIndex, endFragmentIndex;
var root, node, event;
var div, frag, root, node, event;
// Edge doesn't just copy the fragment, but includes the surrounding guff
// including the full <head> of the page. Need to strip this out. In the
// future should probably run all pastes through DOMPurify, but this will
// do for now
// including the full <head> of the page. Need to strip this out. If
// available use DOMPurify to parse and sanitise.
if ( typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ) {
frag = DOMPurify.sanitize( html, {
WHOLE_DOCUMENT: false,
RETURN_DOM: true,
RETURN_DOM_FRAGMENT: true
});
frag = doc.importNode( frag, true );
} else {
if ( isPaste ) {
startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
endFragmentIndex = html.lastIndexOf( '<!--EndFragment-->' );
@ -3880,10 +4052,12 @@ proto.insertHTML = function ( html, isPaste ) {
html = html.slice( startFragmentIndex + 20, endFragmentIndex );
}
}
// Parse HTML into DOM tree
div = this.createElement( 'DIV' );
div.innerHTML = html;
frag = doc.createDocumentFragment();
frag.appendChild( empty( div ) );
}
// Record undo checkpoint
this.saveUndoState( range );
@ -3924,6 +4098,10 @@ proto.insertHTML = function ( html, isPaste ) {
this.setSelection( range );
this._updatePath( range, true );
// Safari sometimes loses focus after paste. Weird.
if ( isPaste ) {
this.focus();
}
} catch ( error ) {
this.didError( error );
}
@ -4012,12 +4190,13 @@ proto.makeLink = function ( url, attributes ) {
this._doc.createTextNode( url.slice( protocolEnd ) )
);
}
if ( !attributes ) {
attributes = {};
}
mergeObjects( attributes, this._config.tagAttributes.a );
attributes.href = url;
attributes = mergeObjects(
mergeObjects({
href: url
}, attributes, true ),
this._config.tagAttributes.a,
false
);
this.changeFormat({
tag: 'A',
@ -4035,27 +4214,27 @@ proto.removeLink = function () {
};
proto.setFontFace = function ( name ) {
this.changeFormat({
this.changeFormat( name ? {
tag: 'SPAN',
attributes: {
'class': 'font',
style: 'font-family: ' + name + ', sans-serif;'
}
}, {
} : null, {
tag: 'SPAN',
attributes: { 'class': 'font' }
});
return this.focus();
};
proto.setFontSize = function ( size ) {
this.changeFormat({
this.changeFormat( size ? {
tag: 'SPAN',
attributes: {
'class': 'size',
style: 'font-size: ' +
( typeof size === 'number' ? size + 'px' : size )
}
}, {
} : null, {
tag: 'SPAN',
attributes: { 'class': 'size' }
});
@ -4063,13 +4242,13 @@ proto.setFontSize = function ( size ) {
};
proto.setTextColour = function ( colour ) {
this.changeFormat({
this.changeFormat( colour ? {
tag: 'SPAN',
attributes: {
'class': 'colour',
style: 'color:' + colour
}
}, {
} : null, {
tag: 'SPAN',
attributes: { 'class': 'colour' }
});
@ -4077,13 +4256,13 @@ proto.setTextColour = function ( colour ) {
};
proto.setHighlightColour = function ( colour ) {
this.changeFormat({
this.changeFormat( colour ? {
tag: 'SPAN',
attributes: {
'class': 'highlight',
style: 'background-color:' + colour
}
}, {
} : colour, {
tag: 'SPAN',
attributes: { 'class': 'highlight' }
});
@ -4170,7 +4349,7 @@ proto.removeAllFormatting = function ( range ) {
var cleanNodes = doc.createDocumentFragment();
var nodeAfterSplit = split( endContainer, endOffset, stopNode, root );
var nodeInSplit = split( startContainer, startOffset, stopNode, root );
var nextNode, _range, childNodes;
var nextNode, childNodes;
// Then replace contents in split with a cleaned version of the same:
// blocks become default blocks, text and leaf nodes survive, everything
@ -4197,15 +4376,9 @@ proto.removeAllFormatting = function ( range ) {
}
// Merge text nodes at edges, if possible
_range = {
startContainer: stopNode,
startOffset: startOffset,
endContainer: stopNode,
endOffset: endOffset
};
mergeInlines( stopNode, _range );
range.setStart( _range.startContainer, _range.startOffset );
range.setEnd( _range.endContainer, _range.endOffset );
range.setStart( stopNode, startOffset );
range.setEnd( stopNode, endOffset );
mergeInlines( stopNode, range );
// And move back down the tree
moveRangeBoundariesDownTree( range );
@ -4226,6 +4399,32 @@ proto.removeList = command( 'modifyBlocks', removeList );
proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
// Range.js exports
Squire.getNodeBefore = getNodeBefore;
Squire.getNodeAfter = getNodeAfter;
Squire.insertNodeInRange = insertNodeInRange;
Squire.extractContentsOfRange = extractContentsOfRange;
Squire.deleteContentsOfRange = deleteContentsOfRange;
Squire.insertTreeFragmentIntoRange = insertTreeFragmentIntoRange;
Squire.isNodeContainedInRange = isNodeContainedInRange;
Squire.moveRangeBoundariesDownTree = moveRangeBoundariesDownTree;
Squire.moveRangeBoundariesUpTree = moveRangeBoundariesUpTree;
Squire.getStartBlockOfRange = getStartBlockOfRange;
Squire.getEndBlockOfRange = getEndBlockOfRange;
Squire.contentWalker = contentWalker;
Squire.rangeDoesStartAtBlockBoundary = rangeDoesStartAtBlockBoundary;
Squire.rangeDoesEndAtBlockBoundary = rangeDoesEndAtBlockBoundary;
Squire.expandRangeToBlockBoundaries = expandRangeToBlockBoundaries;
// Clipboard.js exports
Squire.onPaste = onPaste;
// Editor.js exports
Squire.addLinks = addLinks;
Squire.splitBlock = splitBlock;
Squire.startSelectionId = startSelectionId;
Squire.endSelectionId = endSelectionId;
if ( typeof exports === 'object' ) {
module.exports = Squire;
} else if ( typeof define === 'function' && define.amd ) {

File diff suppressed because one or more lines are too long