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 2017-02-12 21:09:34 -08:00
parent ba4fb64543
commit 92017329b8
2 changed files with 106 additions and 64 deletions

View file

@ -858,28 +858,25 @@ var extractContentsOfRange = function ( range, common, root ) {
}; };
var deleteContentsOfRange = function ( range, root ) { var deleteContentsOfRange = function ( range, root ) {
// Move boundaries up as much as possible to reduce need to split. var startBlock = getStartBlockOfRange( range, root );
// But we need to check whether we've moved the boundary outside of a var endBlock = getEndBlockOfRange( range, root );
// block. If so, the entire block will be removed, so we shouldn't merge var needsMerge = ( startBlock !== endBlock );
// later. var frag, child;
moveRangeBoundariesUpTree( range );
var startBlock = range.startContainer, // Move boundaries up as much as possible without exiting block,
endBlock = range.endContainer, // to reduce need to split.
needsMerge = ( isInline( startBlock ) || isBlock( startBlock ) ) && moveRangeBoundariesDownTree( range );
( isInline( endBlock ) || isBlock( endBlock ) ); moveRangeBoundariesUpTree( range, startBlock, endBlock, root );
// Remove selected range // Remove selected range
var frag = extractContentsOfRange( range, null, root ); frag = extractContentsOfRange( range, null, root );
// Move boundaries back down tree so that they are inside the blocks. // Move boundaries back down tree as far as possible.
// If we don't do this, the range may be collapsed to a point between
// two blocks, so get(Start|End)BlockOfRange will return null.
moveRangeBoundariesDownTree( range ); moveRangeBoundariesDownTree( range );
// If we split into two different blocks, merge the blocks. // If we split into two different blocks, merge the blocks.
startBlock = getStartBlockOfRange( range, root );
if ( needsMerge ) { if ( needsMerge ) {
// endBlock will have been split, so need to refetch
endBlock = getEndBlockOfRange( range, root ); endBlock = getEndBlockOfRange( range, root );
if ( startBlock && endBlock && startBlock !== endBlock ) { if ( startBlock && endBlock && startBlock !== endBlock ) {
mergeWithBlock( startBlock, endBlock, range ); mergeWithBlock( startBlock, endBlock, range );
@ -892,12 +889,12 @@ var deleteContentsOfRange = function ( range, root ) {
} }
// Ensure root has a block-level element in it. // Ensure root has a block-level element in it.
var child = root.firstChild; child = root.firstChild;
if ( !child || child.nodeName === 'BR' ) { if ( !child || child.nodeName === 'BR' ) {
fixCursor( root, root ); fixCursor( root, root );
range.selectNodeContents( root.firstChild ); range.selectNodeContents( root.firstChild );
} else { } else {
range.collapse( range.endContainer === root ? true : false ); range.collapse( true );
} }
return frag; return frag;
}; };
@ -1113,19 +1110,24 @@ var moveRangeBoundariesDownTree = function ( range ) {
} }
}; };
var moveRangeBoundariesUpTree = function ( range, common ) { var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
var startContainer = range.startContainer, var startContainer = range.startContainer;
startOffset = range.startOffset, var startOffset = range.startOffset;
endContainer = range.endContainer, var endContainer = range.endContainer;
endOffset = range.endOffset, var endOffset = range.endOffset;
maySkipBR = true, var maySkipBR = true;
parent; var parent;
if ( !common ) { if ( !startMax ) {
common = range.commonAncestorContainer; startMax = range.commonAncestorContainer;
}
if ( !endMax ) {
endMax = startMax;
} }
while ( startContainer !== common && !startOffset ) { while ( !startOffset &&
startContainer !== startMax &&
startContainer !== root ) {
parent = startContainer.parentNode; parent = startContainer.parentNode;
startOffset = indexOf.call( parent.childNodes, startContainer ); startOffset = indexOf.call( parent.childNodes, startContainer );
startContainer = parent; startContainer = parent;
@ -1139,7 +1141,8 @@ var moveRangeBoundariesUpTree = function ( range, common ) {
endOffset += 1; endOffset += 1;
maySkipBR = false; maySkipBR = false;
} }
if ( endContainer === common || if ( endContainer === endMax ||
endContainer === root ||
endOffset !== getLength( endContainer ) ) { endOffset !== getLength( endContainer ) ) {
break; break;
} }
@ -1638,7 +1641,7 @@ var keyHandlers = {
// delete it ourselves, because the browser won't if it is not // delete it ourselves, because the browser won't if it is not
// inline. // inline.
originalRange = range.cloneRange(); originalRange = range.cloneRange();
moveRangeBoundariesUpTree( range, self._root ); moveRangeBoundariesUpTree( range, root, root, root );
cursorContainer = range.endContainer; cursorContainer = range.endContainer;
cursorOffset = range.endOffset; cursorOffset = range.endOffset;
if ( cursorContainer.nodeType === ELEMENT_NODE ) { if ( cursorContainer.nodeType === ELEMENT_NODE ) {
@ -2077,7 +2080,7 @@ var notWSTextNode = function ( node ) {
node.nodeName === 'BR' : node.nodeName === 'BR' :
notWS.test( node.data ); notWS.test( node.data );
}; };
var isLineBreak = function ( br ) { var isLineBreak = function ( br, isLBIfEmptyBlock ) {
var block = br.parentNode; var block = br.parentNode;
var walker; var walker;
while ( isInline( block ) ) { while ( isInline( block ) ) {
@ -2086,7 +2089,8 @@ var isLineBreak = function ( br ) {
walker = new TreeWalker( walker = new TreeWalker(
block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode ); block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
walker.currentNode = br; walker.currentNode = br;
return !!walker.nextNode() || !walker.previousNode(); return !!walker.nextNode() ||
( isLBIfEmptyBlock && !walker.previousNode() );
}; };
// <br> elements are treated specially, and differently depending on the // <br> elements are treated specially, and differently depending on the
@ -2095,11 +2099,11 @@ var isLineBreak = function ( br ) {
// line breaks by wrapping the inline text in a <div>. Browsers that want <br> // line breaks by wrapping the inline text in a <div>. Browsers that want <br>
// elements at the end of each block will then have them added back in a later // elements at the end of each block will then have them added back in a later
// fixCursor method call. // fixCursor method call.
var cleanupBRs = function ( node, root ) { var cleanupBRs = function ( node, root, keepForBlankLine ) {
var brs = node.querySelectorAll( 'BR' ), var brs = node.querySelectorAll( 'BR' );
brBreaksLine = [], var brBreaksLine = [];
l = brs.length, var l = brs.length;
i, br, parent; var i, br, parent;
// Must calculate whether the <br> breaks a line first, because if we // Must calculate whether the <br> breaks a line first, because if we
// have two <br>s next to each other, after the first one is converted // have two <br>s next to each other, after the first one is converted
@ -2107,7 +2111,7 @@ var cleanupBRs = function ( node, root ) {
// therefore seem to not be a line break. But in its original context it // therefore seem to not be a line break. But in its original context it
// was, so we should also convert it to a block split. // was, so we should also convert it to a block split.
for ( i = 0; i < l; i += 1 ) { for ( i = 0; i < l; i += 1 ) {
brBreaksLine[i] = isLineBreak( brs[i] ); brBreaksLine[i] = isLineBreak( brs[i], keepForBlankLine );
} }
while ( l-- ) { while ( l-- ) {
br = brs[l]; br = brs[l];
@ -2136,7 +2140,7 @@ var setClipboardData = function ( clipboardData, node, root ) {
// Firefox will add an extra new line for BRs at the end of block when // Firefox will add an extra new line for BRs at the end of block when
// calculating innerText, even though they don't actually affect display. // calculating innerText, even though they don't actually affect display.
// So we need to remove them first. // So we need to remove them first.
cleanupBRs( node, root ); cleanupBRs( node, root, true );
node.setAttribute( 'style', node.setAttribute( 'style',
'position:fixed;overflow:hidden;bottom:100%;right:100%;' ); 'position:fixed;overflow:hidden;bottom:100%;right:100%;' );
@ -2160,9 +2164,15 @@ var setClipboardData = function ( clipboardData, node, root ) {
var onCut = function ( event ) { var onCut = function ( event ) {
var clipboardData = event.clipboardData; var clipboardData = event.clipboardData;
var range = this.getSelection(); var range = this.getSelection();
var node = this.createElement( 'div' );
var root = this._root; var root = this._root;
var self = this; var self = this;
var startBlock, endBlock, copyRoot, contents, parent, newContents, node;
// Nothing to do
if ( range.collapsed ) {
event.preventDefault();
return;
}
// Save undo checkpoint // Save undo checkpoint
this.saveUndoState( range ); this.saveUndoState( range );
@ -2171,8 +2181,27 @@ var onCut = function ( event ) {
// Mobile Safari flat out doesn't work: // Mobile Safari flat out doesn't work:
// https://bugs.webkit.org/show_bug.cgi?id=143776 // https://bugs.webkit.org/show_bug.cgi?id=143776
if ( !isEdge && !isIOS && clipboardData ) { if ( !isEdge && !isIOS && clipboardData ) {
moveRangeBoundariesUpTree( range, root ); // Clipboard content should include all parents within block, or all
node.appendChild( deleteContentsOfRange( range, root ) ); // parents up to root if selection across blocks
startBlock = getStartBlockOfRange( range, root );
endBlock = getEndBlockOfRange( range, root );
copyRoot = ( ( startBlock === endBlock ) && startBlock ) || root;
// Extract the contents
contents = deleteContentsOfRange( range, root );
// Add any other parents not in extracted content, up to copy root
parent = range.commonAncestorContainer;
if ( parent.nodeType === TEXT_NODE ) {
parent = parent.parentNode;
}
while ( parent && parent !== copyRoot ) {
newContents = parent.cloneNode( false );
newContents.appendChild( contents );
contents = newContents;
parent = parent.parentNode;
}
// Set clipboard data
node = this.createElement( 'div' );
node.appendChild( contents );
setClipboardData( clipboardData, node, root ); setClipboardData( clipboardData, node, root );
event.preventDefault(); event.preventDefault();
} else { } else {
@ -2192,21 +2221,26 @@ var onCut = function ( event ) {
var onCopy = function ( event ) { var onCopy = function ( event ) {
var clipboardData = event.clipboardData; var clipboardData = event.clipboardData;
var range = this.getSelection(); var range = this.getSelection();
var node = this.createElement( 'div' );
var root = this._root; var root = this._root;
var startBlock, endBlock, copyRoot, contents, parent, newContents; var startBlock, endBlock, copyRoot, contents, parent, newContents, node;
// Edge only seems to support setting plain text as of 2016-03-11. // Edge only seems to support setting plain text as of 2016-03-11.
// Mobile Safari flat out doesn't work: // Mobile Safari flat out doesn't work:
// https://bugs.webkit.org/show_bug.cgi?id=143776 // https://bugs.webkit.org/show_bug.cgi?id=143776
if ( !isEdge && !isIOS && clipboardData ) { if ( !isEdge && !isIOS && clipboardData ) {
range = range.cloneRange(); // Clipboard content should include all parents within block, or all
// parents up to root if selection across blocks
startBlock = getStartBlockOfRange( range, root ); startBlock = getStartBlockOfRange( range, root );
endBlock = getEndBlockOfRange( range, root ); endBlock = getEndBlockOfRange( range, root );
copyRoot = ( ( startBlock === endBlock ) && startBlock ) || root; copyRoot = ( ( startBlock === endBlock ) && startBlock ) || root;
// Clone range to mutate, then move up as high as possible without
// passing the copy root node.
range = range.cloneRange();
moveRangeBoundariesDownTree( range ); moveRangeBoundariesDownTree( range );
moveRangeBoundariesUpTree( range, copyRoot ); moveRangeBoundariesUpTree( range, copyRoot, copyRoot, root );
// Extract the contents
contents = range.cloneContents(); contents = range.cloneContents();
// Add any other parents not in extracted content, up to copy root
parent = range.commonAncestorContainer; parent = range.commonAncestorContainer;
if ( parent.nodeType === TEXT_NODE ) { if ( parent.nodeType === TEXT_NODE ) {
parent = parent.parentNode; parent = parent.parentNode;
@ -2217,8 +2251,9 @@ var onCopy = function ( event ) {
contents = newContents; contents = newContents;
parent = parent.parentNode; parent = parent.parentNode;
} }
// Set clipboard data
node = this.createElement( 'div' );
node.appendChild( contents ); node.appendChild( contents );
setClipboardData( clipboardData, node, root ); setClipboardData( clipboardData, node, root );
event.preventDefault(); event.preventDefault();
} }
@ -2561,13 +2596,14 @@ function Squire ( root, config ) {
var proto = Squire.prototype; var proto = Squire.prototype;
var sanitizeToDOMFragment = function ( html/*, isPaste*/ ) { var sanitizeToDOMFragment = function ( html, isPaste, self ) {
var frag = DOMPurify.sanitize( html, { var doc = self._doc;
var frag = html ? DOMPurify.sanitize( html, {
WHOLE_DOCUMENT: false, WHOLE_DOCUMENT: false,
RETURN_DOM: true, RETURN_DOM: true,
RETURN_DOM_FRAGMENT: true RETURN_DOM_FRAGMENT: true
}); }) : null;
return doc.importNode( frag, true ); return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
}; };
proto.setConfig = function ( config ) { proto.setConfig = function ( config ) {
@ -2626,16 +2662,20 @@ proto.getRoot = function () {
}; };
proto.modifyDocument = function ( modificationCallback ) { proto.modifyDocument = function ( modificationCallback ) {
this._ignoreAllChanges = true; var mutation = this._mutation;
if ( this._mutation ) { if ( mutation ) {
this._mutation.disconnect(); if ( mutation.takeRecords().length ) {
this._docWasChanged();
}
mutation.disconnect();
} }
this._ignoreAllChanges = true;
modificationCallback(); modificationCallback();
this._ignoreAllChanges = false; this._ignoreAllChanges = false;
if ( this._mutation ) {
this._mutation.observe( this._root, { if ( mutation ) {
mutation.observe( this._root, {
childList: true, childList: true,
attributes: true, attributes: true,
characterData: true, characterData: true,
@ -2870,7 +2910,9 @@ proto.getSelection = function () {
var sel = getWindowSelection( this ); var sel = getWindowSelection( this );
var root = this._root; var root = this._root;
var selection, startContainer, endContainer; var selection, startContainer, endContainer;
if ( sel && sel.rangeCount ) { // If not focused, always rely on cached selection; another function may
// have set it but the DOM is not modified until focus again
if ( this._isFocused && sel && sel.rangeCount ) {
selection = sel.getRangeAt( 0 ).cloneRange(); selection = sel.getRangeAt( 0 ).cloneRange();
startContainer = selection.startContainer; startContainer = selection.startContainer;
endContainer = selection.endContainer; endContainer = selection.endContainer;
@ -3708,7 +3750,7 @@ proto.modifyBlocks = function ( modify, range ) {
expandRangeToBlockBoundaries( range, root ); expandRangeToBlockBoundaries( range, root );
// 3. Remove range. // 3. Remove range.
moveRangeBoundariesUpTree( range, root ); moveRangeBoundariesUpTree( range, root, root, root );
frag = extractContentsOfRange( range, root, root ); frag = extractContentsOfRange( range, root, root );
// 4. Modify tree of fragment and reinsert. // 4. Modify tree of fragment and reinsert.
@ -3990,7 +4032,7 @@ proto.setHTML = function ( html ) {
// Parse HTML into DOM tree // Parse HTML into DOM tree
if ( typeof sanitizeToDOMFragment === 'function' ) { if ( typeof sanitizeToDOMFragment === 'function' ) {
frag = sanitizeToDOMFragment( html, false ); frag = sanitizeToDOMFragment( html, false, this );
} else { } else {
div = this.createElement( 'DIV' ); div = this.createElement( 'DIV' );
div.innerHTML = html; div.innerHTML = html;
@ -3999,7 +4041,7 @@ proto.setHTML = function ( html ) {
} }
cleanTree( frag ); cleanTree( frag );
cleanupBRs( frag, root ); cleanupBRs( frag, root, false );
fixContainer( frag, root ); fixContainer( frag, root );
@ -4143,7 +4185,7 @@ proto.insertHTML = function ( html, isPaste ) {
// including the full <head> of the page. Need to strip this out. If // including the full <head> of the page. Need to strip this out. If
// available use DOMPurify to parse and sanitise. // available use DOMPurify to parse and sanitise.
if ( typeof sanitizeToDOMFragment === 'function' ) { if ( typeof sanitizeToDOMFragment === 'function' ) {
frag = sanitizeToDOMFragment( html, isPaste ); frag = sanitizeToDOMFragment( html, isPaste, this );
} else { } else {
if ( isPaste ) { if ( isPaste ) {
startFragmentIndex = html.indexOf( '<!--StartFragment-->' ); startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
@ -4175,7 +4217,7 @@ proto.insertHTML = function ( html, isPaste ) {
addLinks( frag, frag, this ); addLinks( frag, frag, this );
cleanTree( frag ); cleanTree( frag );
cleanupBRs( frag, root ); cleanupBRs( frag, root, false );
removeEmptyInlines( frag ); removeEmptyInlines( frag );
frag.normalize(); frag.normalize();
@ -4442,7 +4484,7 @@ proto.removeAllFormatting = function ( range ) {
this.saveUndoState( range ); this.saveUndoState( range );
// Avoid splitting where we're already at edges. // Avoid splitting where we're already at edges.
moveRangeBoundariesUpTree( range, stopNode ); moveRangeBoundariesUpTree( range, stopNode, stopNode, root );
// Split the selection up to the block, or if whole selection in same // Split the selection up to the block, or if whole selection in same
// block, expand range boundaries to ends of block and split up to root. // block, expand range boundaries to ends of block and split up to root.

File diff suppressed because one or more lines are too long