* drop isWin and isEdge (legacy)
* simplify isWebKit
* some code to ES 2015
This commit is contained in:
djmaze 2021-08-16 15:31:25 +02:00
parent d4f55a02a4
commit 5efbcf5ad3

View file

@ -32,12 +32,10 @@ const
ua = navigator.userAgent, ua = navigator.userAgent,
isMac = /Mac OS X/.test( ua ), isMac = /Mac OS X/.test( ua ),
isWin = /Windows NT/.test( ua ),
isIOS = /iP(?:ad|hone|od)/.test( ua ) || ( isMac && !!navigator.maxTouchPoints ), isIOS = /iP(?:ad|hone|od)/.test( ua ) || ( isMac && !!navigator.maxTouchPoints ),
isGecko = /Gecko\//.test( ua ), isGecko = /Gecko\//.test( ua ),
isEdge = /Edge\//.test( ua ), isWebKit = /WebKit\//.test( ua ),
isWebKit = !isEdge && /WebKit\//.test( ua ),
ctrlKey = isMac ? 'meta-' : 'ctrl-', ctrlKey = isMac ? 'meta-' : 'ctrl-',
osKey = isMac ? 'metaKey' : 'ctrlKey', osKey = isMac ? 'metaKey' : 'ctrlKey',
@ -143,15 +141,7 @@ const
); );
}, },
hasTagAttributes = ( node, tag, attributes ) => { hasTagAttributes = ( node, tag, attributes ) => {
if ( node.nodeName !== tag ) { return node.nodeName === tag && Object.entries(attributes).every(([k,v]) => node.getAttribute(k) === v);
return false;
}
for ( let attr in attributes ) {
if ( node.getAttribute( attr ) !== attributes[ attr ] ) {
return false;
}
}
return true;
}, },
getClosest = ( node, root, selector ) => { getClosest = ( node, root, selector ) => {
node = (!node || node.closest ? node : node.parentElement); node = (!node || node.closest ? node : node.parentElement);
@ -213,31 +203,25 @@ const
setStyle = ( node, style ) => { setStyle = ( node, style ) => {
if (typeof style === 'object') { if (typeof style === 'object') {
for ( let p in style ) { Object.entries(style).forEach(([k,v]) => node.style[k] = v);
node.style[p] = style[p];
}
} else if ( style !== undefined ) { } else if ( style !== undefined ) {
node.setAttribute( 'style', style ); node.setAttribute( 'style', style );
} }
}, },
createElement = ( tag, props, children ) => { createElement = ( tag, props, children ) => {
let el = doc.createElement( tag ), let el = doc.createElement( tag );
attr, value;
if ( props instanceof Array ) { if ( props instanceof Array ) {
children = props; children = props;
props = null; props = null;
} }
if ( props ) { props && Object.entries(props).forEach(([k,v]) => {
for ( attr in props ) { if ('style' === k) {
value = props[ attr ]; setStyle( el, v );
if ('style' === attr) { } else if ( v !== undefined ) {
setStyle( el, value ); el.setAttribute( k, v );
} else if ( value !== undefined ) {
el.setAttribute( attr, value );
}
}
} }
});
children && el.append( ...children ); children && el.append( ...children );
return el; return el;
}, },
@ -530,13 +514,12 @@ const
if ( prev && areAlike( prev, node ) ) { if ( prev && areAlike( prev, node ) ) {
if ( !isContainer( prev ) ) { if ( !isContainer( prev ) ) {
if ( isListItem ) { if ( !isListItem ) {
return;
}
block = createElement( 'DIV' ); block = createElement( 'DIV' );
block.append( empty( prev ) ); block.append( empty( prev ) );
prev.append( block ); prev.append( block );
} else {
return;
}
} }
detach( node ); detach( node );
needsFix = !isContainer( node ); needsFix = !isContainer( node );
@ -727,11 +710,11 @@ const
// Ensure root has a block-level element in it. // Ensure root has a block-level element in it.
child = root.firstChild; child = root.firstChild;
if ( !child || child.nodeName === 'BR' ) { if ( child && child.nodeName !== 'BR' ) {
range.collapse( true );
} else {
fixCursor( root, root ); fixCursor( root, root );
range.selectNodeContents( root.firstChild ); range.selectNodeContents( root.firstChild );
} else {
range.collapse( true );
} }
return frag; return frag;
}, },
@ -861,24 +844,13 @@ const
nodeRange.selectNode( node ); nodeRange.selectNode( node );
if ( partial ) { return partial
// Node must not finish before range starts or start after range // Node must not finish before range starts or start after range finishes.
// finishes. ? range.compareBoundaryPoints( END_TO_START, nodeRange ) < 0
let nodeEndBeforeStart = ( range.compareBoundaryPoints( && range.compareBoundaryPoints( START_TO_END, nodeRange ) > 0
END_TO_START, nodeRange ) > -1 ), // Node must start after range starts and finish before range finishes
nodeStartAfterEnd = ( range.compareBoundaryPoints( : range.compareBoundaryPoints( START_TO_START, nodeRange ) < 1
START_TO_END, nodeRange ) < 1 ); && range.compareBoundaryPoints( END_TO_END, nodeRange ) > -1;
return ( !nodeEndBeforeStart && !nodeStartAfterEnd );
}
else {
// Node must start after range starts and finish before range
// finishes
let nodeStartAfterStart = ( range.compareBoundaryPoints(
START_TO_START, nodeRange ) < 1 ),
nodeEndBeforeEnd = ( range.compareBoundaryPoints(
END_TO_END, nodeRange ) > -1 );
return ( nodeStartAfterStart && nodeEndBeforeEnd );
}
}, },
moveRangeBoundariesDownTree = range => { moveRangeBoundariesDownTree = range => {
@ -1002,8 +974,7 @@ const
} else if ( container !== root && isBlock( container ) ) { } else if ( container !== root && isBlock( container ) ) {
block = container; block = container;
} else { } else {
block = getNodeBefore( container, range.startOffset ); block = getNextBlock( getNodeBefore( container, range.startOffset ), root );
block = getNextBlock( block, root );
} }
// Check the block actually intersects the range // Check the block actually intersects the range
return block && isNodeContainedInRange( range, block ) ? block : null; return block && isNodeContainedInRange( range, block ) ? block : null;
@ -1414,16 +1385,15 @@ const
replaceStyles = node => { replaceStyles = node => {
let style = node.style; let style = node.style;
let attr, converter, css, newTreeBottom, newTreeTop, el; let css, newTreeBottom, newTreeTop, el;
for ( attr in styleToSemantic ) { Object.entries(styleToSemantic).forEach(([attr,converter])=>{
converter = styleToSemantic[ attr ];
css = style[ attr ]; css = style[ attr ];
if ( css && converter.regexp.test( css ) ) { if ( css && converter.regexp.test( css ) ) {
el = converter.replace( doc, css ); el = converter.replace( doc, css );
if ( el.nodeName === node.nodeName && if ( el.nodeName === node.nodeName &&
el.className === node.className ) { el.className === node.className ) {
continue; return;
} }
if ( !newTreeTop ) { if ( !newTreeTop ) {
newTreeTop = el; newTreeTop = el;
@ -1434,7 +1404,7 @@ const
newTreeBottom = el; newTreeBottom = el;
node.style[ attr ] = ''; node.style[ attr ] = '';
} }
} });
if ( newTreeTop ) { if ( newTreeTop ) {
newTreeBottom.append( empty( node ) ); newTreeBottom.append( empty( node ) );
@ -1681,13 +1651,6 @@ const
text = text.replace( NBSP, ' ' ); // Replace nbsp with regular space text = text.replace( NBSP, ' ' ); // Replace nbsp with regular space
node.remove( ); node.remove( );
// Firefox (and others?) returns unix line endings (\n) even on Windows.
// If on Windows, normalise to \r\n, since Notepad and some other crappy
// apps do not understand just \n.
if ( isWin ) {
text = text.replace( /\r?\n/g, '\r\n' );
}
if ( text !== html ) { if ( text !== html ) {
clipboardData.setData( 'text/html', html ); clipboardData.setData( 'text/html', html );
} }
@ -1696,20 +1659,14 @@ const
}, },
mergeObjects = ( base, extras, mayOverride ) => { mergeObjects = ( base, extras, mayOverride ) => {
let prop, value; base = base || {};
if ( !base ) { extras && Object.entries(extras).forEach(([prop,value])=>{
base = {};
}
if ( extras ) {
for ( prop in extras ) {
if ( mayOverride || !( prop in base ) ) { if ( mayOverride || !( prop in base ) ) {
value = extras[ prop ];
base[ prop ] = ( value && value.constructor === Object ) ? base[ prop ] = ( value && value.constructor === Object ) ?
mergeObjects( base[ prop ], value, mayOverride ) : mergeObjects( base[ prop ], value, mayOverride ) :
value; value;
} }
} });
}
return base; return base;
}, },
@ -1777,13 +1734,8 @@ const
}, },
splitBlock = ( self, block, node, offset ) => { splitBlock = ( self, block, node, offset ) => {
let splitTag = tagAfterSplit[ block.nodeName ], let splitTag = tagAfterSplit[ block.nodeName ] || self._config.blockTag,
nodeAfterSplit = split( node, offset, block.parentNode, self._root ), nodeAfterSplit = split( node, offset, block.parentNode, self._root );
config = self._config;
if ( !splitTag ) {
splitTag = config.blockTag;
}
// 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, {} ) ) {
@ -1964,11 +1916,6 @@ function onKey ( event ) {
if ( event[osKey] ) { modifiers += ctrlKey; } if ( event[osKey] ) { modifiers += ctrlKey; }
if ( event.shiftKey ) { modifiers += 'shift-'; } if ( event.shiftKey ) { modifiers += 'shift-'; }
} }
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so
// we want to let the browser handle shift-delete in this situation.
if ( isWin && event.shiftKey && key === 'delete' ) {
modifiers += 'shift-';
}
key = modifiers + key; key = modifiers + key;
@ -2004,7 +1951,7 @@ function onCut ( event ) {
this.saveUndoState( range ); this.saveUndoState( range );
// 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.
if ( !isEdge && event.clipboardData ) { if ( event.clipboardData ) {
// Clipboard content should include all parents within block, or all // Clipboard content should include all parents within block, or all
// parents up to root if selection across blocks // parents up to root if selection across blocks
startBlock = getStartBlockOfRange( range, root ); startBlock = getStartBlockOfRange( range, root );
@ -2041,7 +1988,7 @@ function onCut ( event ) {
function onCopy ( event ) { function onCopy ( event ) {
// 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.
if ( !isEdge && event.clipboardData ) { if ( event.clipboardData ) {
let range = this.getSelection(), root = this._root, let range = this.getSelection(), root = this._root,
// Clipboard content should include all parents within block, or all // Clipboard content should include all parents within block, or all
// parents up to root if selection across blocks // parents up to root if selection across blocks
@ -2141,7 +2088,6 @@ function onPaste ( event ) {
// indication there should be an HTML part. However, it does support // indication there should be an HTML part. However, it does support
// access to image data, so we check for that first. Otherwise though, // access to image data, so we check for that first. Otherwise though,
// fall through to fallback clipboard handling methods // fall through to fallback clipboard handling methods
if ( !isEdge ) {
event.preventDefault(); event.preventDefault();
if ( htmlItem && ( !choosePlain || !plainItem ) ) { if ( htmlItem && ( !choosePlain || !plainItem ) ) {
htmlItem.getAsString( html => self.insertHTML( html, true ) ); htmlItem.getAsString( html => self.insertHTML( html, true ) );
@ -2150,18 +2096,17 @@ function onPaste ( event ) {
} }
return; return;
} }
}
// Safari (and indeed many other OS X apps) copies stuff as text/rtf // Safari (and indeed many other OS X apps) copies stuff as text/rtf
// rather than text/html; even from a webpage in Safari. The only way // rather than text/html; even from a webpage in Safari. The only way
// to get an HTML version is to fallback to letting the browser insert // to get an HTML version is to fallback to letting the browser insert
// the content. Same for getting image data. *Sigh*. // the content. Same for getting image data. *Sigh*.
types = clipboardData && clipboardData.types; types = clipboardData && clipboardData.types;
if ( !isEdge && types && ( if ( types && (
indexOf( types, 'text/html' ) > -1 || ( types.includes( 'text/html' ) || (
!isGecko && !isGecko &&
indexOf( types, 'text/plain' ) > -1 && types.includes( 'text/plain') &&
indexOf( types, 'text/rtf' ) < 0 ) !types.includes( 'text/rtf' ))
)) { )) {
event.preventDefault(); event.preventDefault();
// Abiword on Linux copies a plain text and html version, but the HTML // Abiword on Linux copies a plain text and html version, but the HTML
@ -2171,9 +2116,7 @@ function onPaste ( event ) {
// text/plain item onto the clipboard. Why? Who knows. // text/plain item onto the clipboard. Why? Who knows.
if ( !choosePlain && ( data = clipboardData.getData( 'text/html' ) ) ) { if ( !choosePlain && ( data = clipboardData.getData( 'text/html' ) ) ) {
this.insertHTML( data, true ); this.insertHTML( data, true );
} else if ( } else if ( data = clipboardData.getData( 'text/plain' ) || clipboardData.getData( 'text/uri-list' ) ) {
( data = clipboardData.getData( 'text/plain' ) ) ||
( data = clipboardData.getData( 'text/uri-list' ) ) ) {
this.insertPlainText( data, true ); this.insertPlainText( data, true );
} }
return; return;
@ -2186,21 +2129,20 @@ function onPaste ( event ) {
function onDrop ( event ) { function onDrop ( event ) {
let types = event.dataTransfer.types; let types = event.dataTransfer.types;
let l = types.length; let l = types.length;
let hasPlain = false; let hasData = false;
let hasHTML = false;
while ( l-- ) { while ( l-- ) {
switch ( types[l] ) { switch ( types[l] ) {
case 'text/plain': case 'text/plain':
hasPlain = true;
break;
case 'text/html': case 'text/html':
hasHTML = true; hasData = true;
break; break;
default: default:
return; return;
} }
} }
if ( hasHTML || hasPlain ) {
// if ( types.includes('text/plain') || types.includes('text/html') ) {
if ( hasData ) {
this.saveUndoState(); this.saveUndoState();
} }
} }
@ -2859,9 +2801,7 @@ class Squire
this._lastRange = range; this._lastRange = range;
// If we're setting selection, that automatically, and synchronously, // triggers a focus event. So just store the selection and mark it as // 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. // needing restore on focus.
if ( !this._isFocused ) { if ( this._isFocused ) {
this._restoreSelection = true;
} else {
// iOS bug: if you don't focus the iframe before setting the // 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 // 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 // doesn't get directed into the contenteditable area but is instead
@ -2882,6 +2822,8 @@ class Squire
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange( range ); sel.addRange( range );
} }
} else {
this._restoreSelection = true;
} }
} }
return this; return this;
@ -2989,11 +2931,8 @@ class Squire
// WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256 // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
_addZWS () { _addZWS () {
if ( isWebKit ) { this._hasZWS = isWebKit;
this._hasZWS = true; return doc.createTextNode( isWebKit ? ZWS : '' );
return doc.createTextNode( ZWS );
}
return doc.createTextNode( '' );
} }
_removeZWS () { _removeZWS () {
if ( this._hasZWS ) { if ( this._hasZWS ) {