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,16 +2141,12 @@ 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 ) {
return;
}
// In case inline data has somehow got between blocks. // In case inline data has somehow got between blocks.
fixContainer( current.parentNode, root ); fixContainer( current.parentNode, root );
// Now get next block // Now get next block
next = getNextBlock( current, root );
// Must not be at the very end of the text area. // Must not be at the very end of the text area.
if ( next ) { if ( next = getNextBlock( current, root ) ) {
// If not editable, just delete whole block. // If not editable, just delete whole block.
if ( !next.isContentEditable ) { if ( !next.isContentEditable ) {
detachUneditableNode( next, root ); detachUneditableNode( next, root );
@ -2178,6 +2167,7 @@ let keyHandlers = {
self._updatePath( range, true ); self._updatePath( range, true );
} }
} }
}
// Otherwise, leave to browser but check afterwards whether it has // Otherwise, leave to browser but check afterwards whether it has
// left behind an empty inline tag. // left behind an empty inline tag.
else { else {
@ -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,9 +2717,7 @@ class Squire
// --- Path change events --- // --- Path change events ---
_updatePath ( range, force ) { _updatePath ( range, force ) {
if ( !range ) { if ( range ) {
return;
}
let anchor = range.startContainer, let anchor = range.startContainer,
focus = range.endContainer, focus = range.endContainer,
newPath; newPath;
@ -2745,8 +2725,8 @@ class Squire
focus !== this._lastFocusNode ) { focus !== this._lastFocusNode ) {
this._lastAnchorNode = anchor; this._lastAnchorNode = anchor;
this._lastFocusNode = focus; this._lastFocusNode = focus;
newPath = ( anchor && focus ) ? ( anchor === focus ) ? newPath = ( anchor && focus ) ? ( anchor === focus ?
getPath( focus, this._root, this._config ) : '(selection)' : ''; getPath( focus, this._root, this._config ) : '(selection)' ) : '';
if ( this._path !== newPath ) { if ( this._path !== newPath ) {
this._path = newPath; this._path = newPath;
this.fireEvent( 'pathChange', { path: newPath } ); this.fireEvent( 'pathChange', { path: newPath } );
@ -2756,6 +2736,7 @@ class Squire
range: range 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,15 +3014,11 @@ 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 { do {
node = walker.currentNode; node = walker.currentNode;
needsFormat = !getNearest( node, root, tag, attributes ); if ( !getNearest( node, root, tag, attributes ) ) {
if ( needsFormat ) {
// <br> can never be a container node, so must have a text node // <br> can never be a container node, so must have a text node
// if node == (end|start)Container // if node == (end|start)Container
if ( node === endContainer && node.length > endOffset ) { if ( node === endContainer && node.length > endOffset ) {
@ -3080,6 +3056,7 @@ class Squire
range = createRange( range = createRange(
startContainer, startOffset, endContainer, endOffset ); 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,10 +3161,7 @@ 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 // Save undo checkpoint
this.saveUndoState( range ); this.saveUndoState( range );
@ -3209,17 +3177,14 @@ class Squire
this.setSelection( range ); this.setSelection( range );
this._updatePath( range, true ); 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 // Save undo checkpoint
this.saveUndoState( range ); this.saveUndoState( range );
@ -3236,15 +3201,12 @@ class Squire
// Path may have changed // Path may have changed
this._updatePath( range, true ); 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 // 1. Save undo checkpoint and bookmark selection
this._recordUndoState( range ); this._recordUndoState( range );
@ -3271,28 +3233,19 @@ class Squire
this._getRangeAndRemoveBookmark( range ); this._getRangeAndRemoveBookmark( range );
this.setSelection( range ); this.setSelection( range );
this._updatePath( range, true ); 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 root = this._root;
let listSelection = getListSelection( range, root ); let listSelection = getListSelection( range, root );
if ( !listSelection ) { if ( listSelection ) {
return this.focus();
}
let list = listSelection[0]; let list = listSelection[0];
let startLi = listSelection[1]; let startLi = listSelection[1];
let endLi = listSelection[2]; let endLi = listSelection[2];
if ( !startLi || startLi === list.firstChild ) { if ( startLi && startLi !== list.firstChild ) {
return this.focus();
}
// Save undo checkpoint and bookmark selection // Save undo checkpoint and bookmark selection
this._recordUndoState( range ); this._recordUndoState( range );
@ -3309,39 +3262,27 @@ class Squire
newParent.append( startLi ); newParent.append( startLi );
} while ( ( startLi = next ) ); } while ( ( startLi = next ) );
next = newParent.nextSibling; next = newParent.nextSibling;
if ( next ) { next && mergeContainers( next, root );
mergeContainers( next, root );
}
// Restore selection // Restore selection
this._getRangeAndRemoveBookmark( range ); this._getRangeAndRemoveBookmark( range );
this.setSelection( range ); this.setSelection( range );
this._updatePath( range, true ); 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 root = this._root;
let listSelection = getListSelection( range, root ); let listSelection = getListSelection( range, root );
if ( !listSelection ) { if ( listSelection ) {
return this.focus();
}
let list = listSelection[0]; let list = listSelection[0];
let startLi = listSelection[1]; let startLi = listSelection[1] || list.firstChild;
let endLi = listSelection[2]; let endLi = listSelection[2] || list.lastChild;
let newParent, next, insertBefore, makeNotList; let newParent, next, insertBefore, makeNotList;
if ( !startLi ) {
startLi = list.firstChild;
}
if ( !endLi ) {
endLi = list.lastChild;
}
// Save undo checkpoint and bookmark selection // Save undo checkpoint and bookmark selection
this._recordUndoState( range ); this._recordUndoState( range );
@ -3376,19 +3317,16 @@ class Squire
} while (( startLi = next )); } while (( startLi = next ));
} }
if ( !list.firstChild ) { list.firstChild || detach( list );
detach( list );
}
if ( insertBefore ) { insertBefore && mergeContainers( insertBefore, root );
mergeContainers( insertBefore, root );
}
// Restore selection // Restore selection
this._getRangeAndRemoveBookmark( range ); this._getRangeAndRemoveBookmark( range );
this.setSelection( range ); this.setSelection( range );
this._updatePath( range, true ); 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;
}); });