Squire WYSIWYG should be a bit faster now

This commit is contained in:
the-djmaze 2022-09-14 23:17:02 +02:00
parent f2ef5f2df8
commit 916d7e6d88

View file

@ -91,16 +91,12 @@ const
return UNKNOWN; return UNKNOWN;
} }
let nodeCategory; let nodeCategory =
if ( !Array.prototype.every.call( node.childNodes, isInline ) ) { Array.prototype.every.call( node.childNodes, isInline )
? ( inlineNodeNames.test( node.nodeName ) ? INLINE : BLOCK )
// Malformed HTML can have block tags inside inline tags. Need to treat // Malformed HTML can have block tags inside inline tags. Need to treat
// these as containers rather than inline. See #239. // these as containers rather than inline. See #239.
nodeCategory = CONTAINER; : CONTAINER;
} else if ( inlineNodeNames.test( node.nodeName ) ) {
nodeCategory = INLINE;
} else /*if ( blockElementNames.test( node.nodeName ) )*/ {
nodeCategory = BLOCK;
}
nodeCategoryCache.set( node, nodeCategory ); nodeCategoryCache.set( node, nodeCategory );
return nodeCategory; return nodeCategory;
}, },
@ -135,12 +131,11 @@ const
node.nodeName === node2.nodeName && node.nodeName === node2.nodeName &&
node.nodeName !== 'A' && node.nodeName !== 'A' &&
node.className === node2.className && node.className === node2.className &&
( ( !node.style && !node2.style ) || node.style?.cssText === node2.style?.cssText
node.style.cssText === node2.style.cssText )
); );
}, },
hasTagAttributes = ( node, tag, attributes ) => { hasTagAttributes = ( node, tag, attributes ) => {
return node.nodeName === tag && Object.entries(attributes).every(([k,v]) => node.getAttribute(k) === v); return node.nodeName === tag && Object.entries(attributes || {}).every(([k,v]) => node.getAttribute(k) === v);
}, },
getClosest = ( node, root, selector ) => { getClosest = ( node, root, selector ) => {
node = (node && !node.closest) ? node.parentElement : node; node = (node && !node.closest) ? node.parentElement : node;
@ -1455,7 +1450,7 @@ const
allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/, allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/,
blacklist = /^(?:HEAD|META|STYLE)/, blacklist = /^(?:HEAD|META|STYLE)/,
/*
// Previous node in post-order. // Previous node in post-order.
previousPONode = walker => { previousPONode = walker => {
let current = walker.currentNode, let current = walker.currentNode,
@ -1477,7 +1472,7 @@ const
} }
return null; return null;
}, },
*/
/* /*
Two purposes: Two purposes:
@ -1487,14 +1482,14 @@ const
*/ */
cleanTree = ( node, config, preserveWS ) => { cleanTree = ( node, config, preserveWS ) => {
let children = node.childNodes, let children = node.childNodes,
nonInlineParent, i, l, child, nodeName, nodeType, childLength, nonInlineParent, i, l, child, nodeName, nodeType, childLength;
startsWithWS, endsWithWS, data, sibling; // startsWithWS, endsWithWS, data, sibling;
nonInlineParent = node; nonInlineParent = node;
while ( isInline( nonInlineParent ) ) { while ( isInline( nonInlineParent ) ) {
nonInlineParent = nonInlineParent.parentNode; nonInlineParent = nonInlineParent.parentNode;
} }
let walker = createTreeWalker( nonInlineParent, SHOW_ELEMENT_OR_TEXT ); // let walker = createTreeWalker( nonInlineParent, SHOW_ELEMENT_OR_TEXT );
for ( i = 0, l = children.length; i < l; ++i ) { for ( i = 0, l = children.length; i < l; ++i ) {
child = children[i]; child = children[i];
@ -1519,8 +1514,8 @@ const
cleanTree( child, config, cleanTree( child, config,
preserveWS || ( nodeName === 'PRE' ) ); preserveWS || ( nodeName === 'PRE' ) );
} }
} else {
/* /*
} else {
if ( nodeType === TEXT_NODE ) { if ( nodeType === TEXT_NODE ) {
data = child.data; data = child.data;
startsWithWS = !notWS.test( data.charAt( 0 ) ); startsWithWS = !notWS.test( data.charAt( 0 ) );
@ -1585,7 +1580,7 @@ const
child = children[l]; child = children[l];
if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) { if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
removeEmptyInlines( child ); removeEmptyInlines( child );
if ( isInline( child ) && !child.firstChild ) { if ( !child.firstChild && isInline( child ) ) {
child.remove( ); child.remove( );
} }
} else if ( child.nodeType === TEXT_NODE && !child.data ) { } else if ( child.nodeType === TEXT_NODE && !child.data ) {
@ -1598,8 +1593,7 @@ const
notWSTextNode = node => node.nodeType === ELEMENT_NODE ? node.nodeName === 'BR' : notWS.test( node.data ), notWSTextNode = node => node.nodeType === ELEMENT_NODE ? node.nodeName === 'BR' : notWS.test( node.data ),
isLineBreak = ( br, isLBIfEmptyBlock ) => { isLineBreak = ( br, isLBIfEmptyBlock ) => {
let block = br.parentNode; let walker, block = br.parentNode;
let walker;
while ( isInline( block ) ) { while ( isInline( block ) ) {
block = block.parentNode; block = block.parentNode;
} }
@ -1657,8 +1651,7 @@ const
node.setAttribute( 'style', node.setAttribute( 'style',
'position:fixed;overflow:hidden;bottom:100%;right:100%;' ); 'position:fixed;overflow:hidden;bottom:100%;right:100%;' );
body.append( node ); body.append( node );
text = node.innerText || node.textContent; text = (node.innerText || node.textContent).replace( NBSP, ' ' ); // Replace nbsp with regular space
text = text.replace( NBSP, ' ' ); // Replace nbsp with regular space
node.remove( ); node.remove( );
if ( text !== html ) { if ( text !== html ) {
@ -1748,7 +1741,7 @@ const
nodeAfterSplit = split( node, offset, block.parentNode, self._root ); nodeAfterSplit = split( node, offset, block.parentNode, self._root );
// Make sure the new node is the correct type. // Make sure the new node is the correct type.
if ( !hasTagAttributes( nodeAfterSplit, splitTag, {} ) ) { if ( !hasTagAttributes( nodeAfterSplit, splitTag ) ) {
block = createElement( splitTag ); block = createElement( splitTag );
if ( nodeAfterSplit.dir ) { if ( nodeAfterSplit.dir ) {
block.dir = nodeAfterSplit.dir; block.dir = nodeAfterSplit.dir;
@ -2148,34 +2141,31 @@ let keyHandlers = {
// If at end of block, merge next into this block // If at end of block, merge next into this block
else if ( rangeDoesEndAtBlockBoundary( range, root ) ) { else if ( rangeDoesEndAtBlockBoundary( range, root ) ) {
event.preventDefault(); event.preventDefault();
current = getStartBlockOfRange( range, root ); if ( current = getStartBlockOfRange( range, root ) ) {
if ( !current ) { // In case inline data has somehow got between blocks.
return; fixContainer( current.parentNode, root );
} // Now get next block
// In case inline data has somehow got between blocks. // Must not be at the very end of the text area.
fixContainer( current.parentNode, root ); if ( next = getNextBlock( current, root ) ) {
// Now get next block // If not editable, just delete whole block.
next = getNextBlock( current, root ); if ( !next.isContentEditable ) {
// Must not be at the very end of the text area. detachUneditableNode( next, root );
if ( next ) { return;
// If not editable, just delete whole block. }
if ( !next.isContentEditable ) { // Otherwise merge.
detachUneditableNode( next, root ); mergeWithBlock( current, next, range, root );
return; // If deleted line between containers, merge newly adjacent
// containers.
next = current.parentNode;
while ( next !== root && !next.nextSibling ) {
next = next.parentNode;
}
if ( next !== root && ( next = next.nextSibling ) ) {
mergeContainers( next, root );
}
self.setSelection( range );
self._updatePath( range, true );
} }
// Otherwise merge.
mergeWithBlock( current, next, range, root );
// If deleted line between containers, merge newly adjacent
// containers.
next = current.parentNode;
while ( next !== root && !next.nextSibling ) {
next = next.parentNode;
}
if ( next !== root && ( next = next.nextSibling ) ) {
mergeContainers( next, root );
}
self.setSelection( range );
self._updatePath( range, true );
} }
} }
// Otherwise, leave to browser but check afterwards whether it has // Otherwise, leave to browser but check afterwards whether it has
@ -2237,18 +2227,18 @@ let keyHandlers = {
} }
}, },
space: ( self, _, range ) => { space: ( self, _, range ) => {
let node;
let root = self._root; let root = self._root;
self._recordUndoState( range, false ); self._recordUndoState( range, false );
if ( self._config.addLinks ) { if ( self._config.addLinks ) {
addLinks( range.startContainer, root, self ); addLinks( range.startContainer, root, self );
} }
self._getRangeAndRemoveBookmark( range ); self._getRangeAndRemoveBookmark( range );
/*
// If the cursor is at the end of a link (<a>foo|</a>) then move it // If the cursor is at the end of a link (<a>foo|</a>) then move it
// outside of the link (<a>foo</a>|) so that the space is not part of // outside of the link (<a>foo</a>|) so that the space is not part of
// the link text. // the link text.
node = range.endContainer; // SnappyMail: disabled as it fails in Firefox
let node = range.endContainer;
if ( range.collapsed && range.endOffset === getLength( node ) ) { if ( range.collapsed && range.endOffset === getLength( node ) ) {
do { do {
if ( node.nodeName === 'A' ) { if ( node.nodeName === 'A' ) {
@ -2258,6 +2248,7 @@ let keyHandlers = {
} while ( !node.nextSibling && } while ( !node.nextSibling &&
( node = node.parentNode ) && node !== root ); ( node = node.parentNode ) && node !== root );
} }
*/
// Delete the selection if not collapsed // Delete the selection if not collapsed
if ( !range.collapsed ) { if ( !range.collapsed ) {
deleteContentsOfRange( range, root ); deleteContentsOfRange( range, root );
@ -2590,8 +2581,6 @@ class Squire
addEventListener ( type, fn ) { addEventListener ( type, fn ) {
type.split(/\s+/).forEach(type=>{ type.split(/\s+/).forEach(type=>{
let handlers = this._events[ type ],
target = this._root;
if ( !fn ) { if ( !fn ) {
didError({ didError({
name: 'Squire: addEventListener with null or undefined fn', name: 'Squire: addEventListener with null or undefined fn',
@ -2599,14 +2588,12 @@ class Squire
}); });
return this; return this;
} }
let handlers = this._events[ type ];
if ( !handlers ) { if ( !handlers ) {
handlers = this._events[ type ] = []; handlers = this._events[ type ] = [];
if ( !customEvents[ type ] ) { customEvents[ type ]
if ( type === 'selectionchange' ) { || ( type === 'selectionchange' ? doc : this._root )
target = doc; .addEventListener( type, this, {capture:true,passive:'touchstart'===type} );
}
target.addEventListener( type, this, {capture:true,passive:'touchstart'===type} );
}
} }
handlers.push( fn ); handlers.push( fn );
}); });
@ -2615,7 +2602,6 @@ class Squire
removeEventListener ( type, fn ) { removeEventListener ( type, fn ) {
let handlers = this._events[ type ]; let handlers = this._events[ type ];
let target = this._root;
let l; let l;
if ( handlers ) { if ( handlers ) {
if ( fn ) { if ( fn ) {
@ -2630,12 +2616,9 @@ class Squire
} }
if ( !handlers.length ) { if ( !handlers.length ) {
delete this._events[ type ]; delete this._events[ type ];
if ( !customEvents[ type ] ) { customEvents[ type ]
if ( type === 'selectionchange' ) { || ( type === 'selectionchange' ? doc : this._root)
target = doc; .removeEventListener( type, this, true );
}
target.removeEventListener( type, this, true );
}
} }
} }
return this; return this;
@ -2679,7 +2662,7 @@ class Squire
getSelection () { getSelection () {
let sel = win.getSelection(); let sel = win.getSelection();
let root = this._root; let root = this._root;
let range, startContainer, endContainer, node; let range, startContainer, endContainer;
// If not focused, always rely on cached range; another function may // If not focused, always rely on cached range; another function may
// have set it but the DOM is not modified until focus again // have set it but the DOM is not modified until focus again
if ( this._isFocused && sel?.rangeCount ) { if ( this._isFocused && sel?.rangeCount ) {
@ -2698,10 +2681,9 @@ class Squire
this._lastRange = range; this._lastRange = range;
} else { } else {
range = this._lastRange; range = this._lastRange;
node = range.commonAncestorContainer;
// Check the editor is in the live document; if not, the range has // Check the editor is in the live document; if not, the range has
// probably been rewritten by the browser and is bogus // probably been rewritten by the browser and is bogus
if ( !doc.contains( node ) ) { if ( !doc.contains( range.commonAncestorContainer ) ) {
range = null; range = null;
} }
} }
@ -2735,26 +2717,25 @@ class Squire
// --- Path change events --- // --- Path change events ---
_updatePath ( range, force ) { _updatePath ( range, force ) {
if ( !range ) { if ( range ) {
return; let anchor = range.startContainer,
} focus = range.endContainer,
let anchor = range.startContainer, newPath;
focus = range.endContainer, if ( force || anchor !== this._lastAnchorNode ||
newPath; focus !== this._lastFocusNode ) {
if ( force || anchor !== this._lastAnchorNode || this._lastAnchorNode = anchor;
focus !== this._lastFocusNode ) { this._lastFocusNode = focus;
this._lastAnchorNode = anchor; newPath = ( anchor && focus ) ? ( anchor === focus ?
this._lastFocusNode = focus; getPath( focus, this._root, this._config ) : '(selection)' ) : '';
newPath = ( anchor && focus ) ? ( anchor === focus ) ? if ( this._path !== newPath ) {
getPath( focus, this._root, this._config ) : '(selection)' : ''; this._path = newPath;
if ( this._path !== newPath ) { this.fireEvent( 'pathChange', { path: newPath } );
this._path = newPath; }
this.fireEvent( 'pathChange', { path: newPath } );
} }
this.fireEvent( range.collapsed ? 'cursor' : 'select', {
range: range
});
} }
this.fireEvent( range.collapsed ? 'cursor' : 'select', {
range: range
});
} }
// --- Focus --- // --- Focus ---
@ -2879,7 +2860,6 @@ class Squire
hasFormat ( tag, attributes, range ) { hasFormat ( tag, attributes, range ) {
// 1. Normalise the arguments and get selection // 1. Normalise the arguments and get selection
tag = tag.toUpperCase(); tag = tag.toUpperCase();
if ( !attributes ) { attributes = {}; }
if ( !range && !( range = this.getSelection() ) ) { if ( !range && !( range = this.getSelection() ) ) {
return false; return false;
} }
@ -2980,7 +2960,7 @@ class Squire
// it round the range and focus it. // it round the range and focus it.
let root = this._root; let root = this._root;
let el, walker, startContainer, endContainer, startOffset, endOffset, let el, walker, startContainer, endContainer, startOffset, endOffset,
node, needsFormat, block; node, block;
if ( range.collapsed ) { if ( range.collapsed ) {
el = fixCursor( createElement( tag, attributes ), root ); el = fixCursor( createElement( tag, attributes ), root );
@ -3034,51 +3014,48 @@ class Squire
startOffset = 0; startOffset = 0;
} }
// If there are no interesting nodes in the selection, abort // If there are interesting nodes in the selection
if ( !startContainer ) { if ( startContainer ) {
return range; do {
} node = walker.currentNode;
if ( !getNearest( node, root, tag, attributes ) ) {
do { // <br> can never be a container node, so must have a text node
node = walker.currentNode; // if node == (end|start)Container
needsFormat = !getNearest( node, root, tag, attributes ); if ( node === endContainer && node.length > endOffset ) {
if ( needsFormat ) { node.splitText( endOffset );
// <br> can never be a container node, so must have a text node
// if node == (end|start)Container
if ( node === endContainer && node.length > endOffset ) {
node.splitText( endOffset );
}
if ( node === startContainer && startOffset ) {
node = node.splitText( startOffset );
if ( endContainer === startContainer ) {
endContainer = node;
endOffset -= startOffset;
} }
startContainer = node; if ( node === startContainer && startOffset ) {
startOffset = 0; node = node.splitText( startOffset );
if ( endContainer === startContainer ) {
endContainer = node;
endOffset -= startOffset;
}
startContainer = node;
startOffset = 0;
}
el = createElement( tag, attributes );
node.replaceWith( el );
el.append( node );
} }
el = createElement( tag, attributes ); } while ( walker.nextNode() );
node.replaceWith( el );
el.append( node );
}
} while ( walker.nextNode() );
// If we don't finish inside a text node, offset may have changed. // If we don't finish inside a text node, offset may have changed.
if ( endContainer.nodeType !== TEXT_NODE ) { if ( endContainer.nodeType !== TEXT_NODE ) {
if ( node.nodeType === TEXT_NODE ) { if ( node.nodeType === TEXT_NODE ) {
endContainer = node; endContainer = node;
endOffset = node.length; endOffset = node.length;
} else { } else {
// If <br>, we must have just wrapped it, so it must have only // If <br>, we must have just wrapped it, so it must have only
// one child // one child
endContainer = node.parentNode; endContainer = node.parentNode;
endOffset = 1; endOffset = 1;
}
} }
// Now set the selection to as it was before
range = createRange(
startContainer, startOffset, endContainer, endOffset );
} }
// Now set the selection to as it was before
range = createRange(
startContainer, startOffset, endContainer, endOffset );
} }
return range; return range;
} }
@ -3154,15 +3131,11 @@ class Squire
el => isNodeContainedInRange( range, el ) && hasTagAttributes( el, tag, attributes ) el => isNodeContainedInRange( range, el ) && hasTagAttributes( el, tag, attributes )
); );
if ( !partial ) { partial || formatTags.forEach( node => examineNode( node, node ) );
formatTags.forEach( node => examineNode( node, node ) );
}
// Now wrap unselected nodes in the tag // Now wrap unselected nodes in the tag
toWrap.forEach( item => { toWrap.forEach( ([exemplar, node]) => {
// [ exemplar, node ] tuple let el = exemplar.cloneNode( false );
let el = item[0].cloneNode( false ),
node = item[1];
node.replaceWith( el ); node.replaceWith( el );
el.append( node ); el.append( node );
}); });
@ -3171,9 +3144,7 @@ class Squire
// Merge adjacent inlines: // Merge adjacent inlines:
this._getRangeAndRemoveBookmark( range ); this._getRangeAndRemoveBookmark( range );
if ( fixer ) { fixer && range.collapse( false );
range.collapse( false );
}
mergeInlines( root, range ); mergeInlines( root, range );
return range; return range;
@ -3190,205 +3161,172 @@ class Squire
changeFormat ( add, remove, range, partial ) { changeFormat ( add, remove, range, partial ) {
// Normalise the arguments and get selection // Normalise the arguments and get selection
if ( !range && !( range = this.getSelection() ) ) { if ( range || ( range = this.getSelection() ) ) {
return this; // Save undo checkpoint
this.saveUndoState( range );
if ( remove ) {
range = this._removeFormat( remove.tag.toUpperCase(),
remove.attributes || {}, range, partial );
}
if ( add ) {
range = this._addFormat( add.tag.toUpperCase(),
add.attributes || {}, range );
}
this.setSelection( range );
this._updatePath( range, true );
} }
// Save undo checkpoint
this.saveUndoState( range );
if ( remove ) {
range = this._removeFormat( remove.tag.toUpperCase(),
remove.attributes || {}, range, partial );
}
if ( add ) {
range = this._addFormat( add.tag.toUpperCase(),
add.attributes || {}, range );
}
this.setSelection( range );
this._updatePath( range, true );
return this; return this;
} }
// --- Block formatting --- // --- Block formatting ---
forEachBlock ( fn, range ) { forEachBlock ( fn, range ) {
if ( !range && !( range = this.getSelection() ) ) { if ( range || ( range = this.getSelection() ) ) {
return this; // Save undo checkpoint
this.saveUndoState( range );
let root = this._root;
let start = getStartBlockOfRange( range, root );
let end = getEndBlockOfRange( range, root );
if ( start && end ) {
do {
if ( fn( start ) || start === end ) { break; }
} while ( start = getNextBlock( start, root ) );
}
this.setSelection( range );
// Path may have changed
this._updatePath( range, true );
} }
// Save undo checkpoint
this.saveUndoState( range );
let root = this._root;
let start = getStartBlockOfRange( range, root );
let end = getEndBlockOfRange( range, root );
if ( start && end ) {
do {
if ( fn( start ) || start === end ) { break; }
} while ( start = getNextBlock( start, root ) );
}
this.setSelection( range );
// Path may have changed
this._updatePath( range, true );
return this; return this;
} }
modifyBlocks ( modify, range ) { modifyBlocks ( modify, range ) {
if ( !range && !( range = this.getSelection() ) ) { if ( range || ( range = this.getSelection() ) ) {
return this; // 1. Save undo checkpoint and bookmark selection
this._recordUndoState( range );
let root = this._root;
let frag;
// 2. Expand range to block boundaries
expandRangeToBlockBoundaries( range, root );
// 3. Remove range.
moveRangeBoundariesUpTree( range, root, root, root );
frag = extractContentsOfRange( range, root, root );
// 4. Modify tree of fragment and reinsert.
insertNodeInRange( range, modify.call( this, frag ) );
// 5. Merge containers at edges
if ( range.endOffset < range.endContainer.childNodes.length ) {
mergeContainers( range.endContainer.childNodes[ range.endOffset ], root );
}
mergeContainers( range.startContainer.childNodes[ range.startOffset ], root );
// 6. Restore selection
this._getRangeAndRemoveBookmark( range );
this.setSelection( range );
this._updatePath( range, true );
} }
// 1. Save undo checkpoint and bookmark selection
this._recordUndoState( range );
let root = this._root;
let frag;
// 2. Expand range to block boundaries
expandRangeToBlockBoundaries( range, root );
// 3. Remove range.
moveRangeBoundariesUpTree( range, root, root, root );
frag = extractContentsOfRange( range, root, root );
// 4. Modify tree of fragment and reinsert.
insertNodeInRange( range, modify.call( this, frag ) );
// 5. Merge containers at edges
if ( range.endOffset < range.endContainer.childNodes.length ) {
mergeContainers( range.endContainer.childNodes[ range.endOffset ], root );
}
mergeContainers( range.startContainer.childNodes[ range.startOffset ], root );
// 6. Restore selection
this._getRangeAndRemoveBookmark( range );
this.setSelection( range );
this._updatePath( range, true );
return this; return this;
} }
increaseListLevel ( range ) { increaseListLevel ( range ) {
if ( !range && !( range = this.getSelection() ) ) { if ( range || ( range = this.getSelection() ) ) {
return this.focus(); let root = this._root;
let listSelection = getListSelection( range, root );
if ( listSelection ) {
let list = listSelection[0];
let startLi = listSelection[1];
let endLi = listSelection[2];
if ( startLi && startLi !== list.firstChild ) {
// Save undo checkpoint and bookmark selection
this._recordUndoState( range );
// Increase list depth
let type = list.nodeName;
let newParent = startLi.previousSibling;
let next;
if ( newParent.nodeName !== type ) {
newParent = createElement( type );
startLi.before( newParent );
}
do {
next = startLi === endLi ? null : startLi.nextSibling;
newParent.append( startLi );
} while ( ( startLi = next ) );
next = newParent.nextSibling;
next && mergeContainers( next, root );
// Restore selection
this._getRangeAndRemoveBookmark( range );
this.setSelection( range );
this._updatePath( range, true );
}
}
} }
let root = this._root;
let listSelection = getListSelection( range, root );
if ( !listSelection ) {
return this.focus();
}
let list = listSelection[0];
let startLi = listSelection[1];
let endLi = listSelection[2];
if ( !startLi || startLi === list.firstChild ) {
return this.focus();
}
// Save undo checkpoint and bookmark selection
this._recordUndoState( range );
// Increase list depth
let type = list.nodeName;
let newParent = startLi.previousSibling;
let next;
if ( newParent.nodeName !== type ) {
newParent = createElement( type );
startLi.before( newParent );
}
do {
next = startLi === endLi ? null : startLi.nextSibling;
newParent.append( startLi );
} while ( ( startLi = next ) );
next = newParent.nextSibling;
if ( next ) {
mergeContainers( next, root );
}
// Restore selection
this._getRangeAndRemoveBookmark( range );
this.setSelection( range );
this._updatePath( range, true );
return this.focus(); return this.focus();
} }
decreaseListLevel ( range ) { decreaseListLevel ( range ) {
if ( !range && !( range = this.getSelection() ) ) { if ( range || ( range = this.getSelection() ) ) {
return this.focus(); let root = this._root;
} let listSelection = getListSelection( range, root );
if ( listSelection ) {
let list = listSelection[0];
let startLi = listSelection[1] || list.firstChild;
let endLi = listSelection[2] || list.lastChild;
let newParent, next, insertBefore, makeNotList;
let root = this._root; // Save undo checkpoint and bookmark selection
let listSelection = getListSelection( range, root ); this._recordUndoState( range );
if ( !listSelection ) {
return this.focus();
}
let list = listSelection[0]; if ( startLi ) {
let startLi = listSelection[1]; // Find the new parent list node
let endLi = listSelection[2]; newParent = list.parentNode;
let newParent, next, insertBefore, makeNotList;
if ( !startLi ) {
startLi = list.firstChild;
}
if ( !endLi ) {
endLi = list.lastChild;
}
// Save undo checkpoint and bookmark selection // Split list if necesary
this._recordUndoState( range ); insertBefore = !endLi.nextSibling ?
list.nextSibling :
split( list, endLi.nextSibling, newParent, root );
if ( startLi ) { if ( newParent !== root && newParent.nodeName === 'LI' ) {
// Find the new parent list node newParent = newParent.parentNode;
newParent = list.parentNode; while ( insertBefore ) {
next = insertBefore.nextSibling;
endLi.append( insertBefore );
insertBefore = next;
}
insertBefore = list.parentNode.nextSibling;
}
// Split list if necesary makeNotList = !/^[OU]L$/.test( newParent.nodeName );
insertBefore = !endLi.nextSibling ? do {
list.nextSibling : next = startLi === endLi ? null : startLi.nextSibling;
split( list, endLi.nextSibling, newParent, root ); startLi.remove( );
if ( makeNotList && startLi.nodeName === 'LI' ) {
if ( newParent !== root && newParent.nodeName === 'LI' ) { startLi = this.createDefaultBlock([ empty( startLi ) ]);
newParent = newParent.parentNode; }
while ( insertBefore ) { newParent.insertBefore( startLi, insertBefore );
next = insertBefore.nextSibling; } while (( startLi = next ));
endLi.append( insertBefore );
insertBefore = next;
} }
insertBefore = list.parentNode.nextSibling;
list.firstChild || detach( list );
insertBefore && mergeContainers( insertBefore, root );
// Restore selection
this._getRangeAndRemoveBookmark( range );
this.setSelection( range );
this._updatePath( range, true );
} }
makeNotList = !/^[OU]L$/.test( newParent.nodeName );
do {
next = startLi === endLi ? null : startLi.nextSibling;
startLi.remove( );
if ( makeNotList && startLi.nodeName === 'LI' ) {
startLi = this.createDefaultBlock([ empty( startLi ) ]);
}
newParent.insertBefore( startLi, insertBefore );
} while (( startLi = next ));
} }
if ( !list.firstChild ) {
detach( list );
}
if ( insertBefore ) {
mergeContainers( insertBefore, root );
}
// Restore selection
this._getRangeAndRemoveBookmark( range );
this.setSelection( range );
this._updatePath( range, true );
return this.focus(); return this.focus();
} }
@ -3429,9 +3367,7 @@ class Squire
this._saveRangeToBookmark( range ); this._saveRangeToBookmark( range );
} }
html = this._getHTML().replace( /\u200B/g, '' ); html = this._getHTML().replace( /\u200B/g, '' );
if ( range ) { range && this._getRangeAndRemoveBookmark( range );
this._getRangeAndRemoveBookmark( range );
}
return html; return html;
} }
@ -3602,9 +3538,7 @@ class Squire
fixCursor( node, root ); fixCursor( node, root );
} }
if ( isPaste ) { isPaste && this.fireEvent( 'willPaste', event );
this.fireEvent( 'willPaste', event );
}
if ( !event.defaultPrevented ) { if ( !event.defaultPrevented ) {
insertTreeFragmentIntoRange( range, event.fragment, root ); insertTreeFragmentIntoRange( range, event.fragment, root );
@ -3621,9 +3555,7 @@ class Squire
this.setSelection( range ); this.setSelection( range );
this._updatePath( range, true ); this._updatePath( range, true );
// Safari sometimes loses focus after paste. Weird. // Safari sometimes loses focus after paste. Weird.
if ( isPaste ) { isPaste && this.focus();
this.focus();
}
} catch ( error ) { } catch ( error ) {
didError( error ); didError( error );
} }
@ -3637,7 +3569,7 @@ class Squire
let node = range.startContainer; let node = range.startContainer;
let offset = range.startOffset; let offset = range.startOffset;
let text, event; let text, event;
if ( !node || node.nodeType !== TEXT_NODE ) { if ( node?.nodeType !== TEXT_NODE ) {
text = doc.createTextNode( '' ); text = doc.createTextNode( '' );
node?.childNodes[ offset ].before( text ); node?.childNodes[ offset ].before( text );
node = text; node = text;
@ -3650,9 +3582,7 @@ class Squire
}, },
defaultPrevented: false defaultPrevented: false
}; };
if ( isPaste ) { isPaste && this.fireEvent( 'willPaste', event );
this.fireEvent( 'willPaste', event );
}
if ( !event.defaultPrevented ) { if ( !event.defaultPrevented ) {
plainText = event.text; plainText = event.text;
@ -3668,11 +3598,10 @@ class Squire
let tag = config.blockTag; let tag = config.blockTag;
let closeBlock = '</' + tag + '>'; let closeBlock = '</' + tag + '>';
let openBlock = '<' + tag + '>'; let openBlock = '<' + tag + '>';
let i, l, line; let i = lines.length, line;
for ( i = 0, l = lines.length; i < l; ++i ) { while (i--) {
line = lines[i]; line = escapeHTML( lines[i] ).replace( / (?= )/g, '&nbsp;' );
line = escapeHTML( line ).replace( / (?= )/g, '&nbsp;' );
// We don't wrap the first line in the block, so if it gets inserted // We don't wrap the first line in the block, so if it gets inserted
// into a blank line it keeps that line's formatting. // into a blank line it keeps that line's formatting.
// Wrap each line in <div></div> // Wrap each line in <div></div>
@ -3724,7 +3653,7 @@ class Squire
setStyle ( style ) { setStyle ( style ) {
let range = this.getSelection(); let range = this.getSelection();
let start = range ? range.startContainer : 0; let start = range?.startContainer || {};
let end = range ? range.endContainer : 0; let end = range ? range.endContainer : 0;
// When the selection is all the text inside an element, set style on the element itself // When the selection is all the text inside an element, set style on the element itself
if ( TEXT_NODE === start?.nodeType && 0 === range.startOffset && start === end && end.length === range.endOffset ) { if ( TEXT_NODE === start?.nodeType && 0 === range.startOffset && start === end && end.length === range.endOffset ) {
@ -3758,24 +3687,15 @@ class Squire
// 1. Extract inline content; drop all blocks and contains. // 1. Extract inline content; drop all blocks and contains.
while (( node = walker.nextNode() )) { while (( node = walker.nextNode() )) {
// 2. Replace <br> with \n in content // 2. Replace <br> with \n in content
let nodes = node.querySelectorAll( 'BR' ); node.querySelectorAll( 'BR' ).forEach( br => {
let l = nodes.length;
let br;
while ( l-- ) {
br = nodes[l];
if ( !isLineBreak( br, false ) ) { if ( !isLineBreak( br, false ) ) {
detach( br ); detach( br );
} else { } else {
br.replaceWith( doc.createTextNode( '\n' ) ); br.replaceWith( doc.createTextNode( '\n' ) );
} }
} });
// 3. Remove <code>; its format clashes with <pre> // 3. Remove <code>; its format clashes with <pre>
nodes = node.querySelectorAll( 'CODE' ); node.querySelectorAll( 'CODE' ).forEach( el => detach( el ) );
l = nodes.length;
while ( l-- ) {
detach( nodes[l] );
}
if ( output.childNodes.length ) { if ( output.childNodes.length ) {
output.append( doc.createTextNode( '\n' ) ); output.append( doc.createTextNode( '\n' ) );
} }
@ -3868,8 +3788,7 @@ class Squire
decreaseQuoteLevel ( range ) { decreaseQuoteLevel ( range ) {
this.modifyBlocks( this.modifyBlocks(
frag => { frag => {
var blockquotes = frag.querySelectorAll( 'blockquote' ); Array.prototype.filter.call( frag.querySelectorAll( 'blockquote' ), el =>
Array.prototype.filter.call( blockquotes, el =>
!getClosest( el.parentNode, frag, 'BLOCKQUOTE' ) !getClosest( el.parentNode, frag, 'BLOCKQUOTE' )
).forEach( el => el.replaceWith( empty( el ) ) ); ).forEach( el => el.replaceWith( empty( el ) ) );
return frag; return frag;
@ -3891,19 +3810,15 @@ class Squire
removeList () { removeList () {
this.modifyBlocks( frag => { this.modifyBlocks( frag => {
let lists = frag.querySelectorAll( 'UL, OL' ), let root = this._root,
items = frag.querySelectorAll( 'LI' ), listFrag;
root = this._root, frag.querySelectorAll( 'UL, OL' ).forEach( list => {
i, l, list, listFrag, item;
for ( i = 0, l = lists.length; i < l; ++i ) {
list = lists[i];
listFrag = empty( list ); listFrag = empty( list );
fixContainer( listFrag, root ); fixContainer( listFrag, root );
list.replaceWith( listFrag ); list.replaceWith( listFrag );
} });
for ( i = 0, l = items.length; i < l; ++i ) { frag.querySelectorAll( 'LI' ).forEach( item => {
item = items[i];
if ( isBlock( item ) ) { if ( isBlock( item ) ) {
item.replaceWith( item.replaceWith(
this.createDefaultBlock([ empty( item ) ]) this.createDefaultBlock([ empty( item ) ])
@ -3912,7 +3827,7 @@ class Squire
fixContainer( item, root ); fixContainer( item, root );
item.replaceWith( empty( item ) ); item.replaceWith( empty( item ) );
} }
} });
return frag; return frag;
}); });