. Browsers that want
+ // elements at the end of each block will then have them added back in a later
+ // fixCursor method call.
+ cleanupBRs = (node, root, keepForBlankLine) => {
+ let brs = node.querySelectorAll('BR');
+ let l = brs.length;
+ let br, parent;
+ while (l--) {
+ br = brs[l];
+ // Cleanup may have removed it
+ parent = br.parentNode;
+ if (!parent) { continue; }
+ // If it doesn't break a line, just remove it; it's not doing
+ // anything useful. We'll add it back later if required by the
+ // browser. If it breaks a line, wrap the content in div tags
+ // and replace the brs.
+ if (!isLineBreak(br, keepForBlankLine)) {
+ detach(br);
+ } else if (!isInline(parent)) {
+ fixContainer(parent, root);
+ }
+ }
+ },
+ escapeHTML = text => text.replace('&', '&')
+ .replace('<', '<')
+ .replace('>', '>')
+ .replace('"', '"'),
+ // source/node/Block.ts
+ getBlockWalker = (node, root) => {
+ let walker = createTreeWalker(root, SHOW_ELEMENT, isBlock);
+ walker.currentNode = node;
+ return walker;
+ },
+ getPreviousBlock = (node, root) => {
+// node = getClosest(node, root, blockElementNames);
+ node = getBlockWalker(node, root).previousNode();
+ return node !== root ? node : null;
+ },
+ getNextBlock = (node, root) => {
+// node = getClosest(node, root, blockElementNames);
+ node = getBlockWalker(node, root).nextNode();
+ return node !== root ? node : null;
+ },
+ isEmptyBlock = block => !block.textContent && !block.querySelector('IMG'),
+
+ // source/range/Block.ts
+ // Returns the first block at least partially contained by the range,
+ // or null if no block is contained by the range.
+ getStartBlockOfRange = (range, root) => {
+ let container = range.startContainer,
+ block;
+
+ // If inline, get the containing block.
+ if (isInline(container)) {
+ block = getPreviousBlock(container, root);
+ } else if (container !== root && isBlock(container)) {
+ block = container;
+ } else {
+ block = getNextBlock(getNodeBefore(container, range.startOffset), root);
+ }
+ // Check the block actually intersects the range
+ return block && isNodeContainedInRange(range, block) ? block : null;
+ },
+ // Returns the last block at least partially contained by the range,
+ // or null if no block is contained by the range.
+ getEndBlockOfRange = (range, root) => {
+ let container = range.endContainer,
+ block, child;
+
+ // If inline, get the containing block.
+ if (isInline(container)) {
+ block = getPreviousBlock(container, root);
+ } else if (container !== root && isBlock(container)) {
+ block = container;
+ } else {
+ block = getNodeAfter(container, range.endOffset);
+ if (!block || !root.contains(block)) {
+ block = root;
+ while (child = block.lastChild) {
+ block = child;
+ }
+ }
+ block = getPreviousBlock(block, root);
+ }
+ // Check the block actually intersects the range
+ return block && isNodeContainedInRange(range, block) ? block : null;
+ },
+ rangeDoesStartAtBlockBoundary = (range, root) => {
+ let startContainer = range.startContainer;
+ let startOffset = range.startOffset;
+ let startBlock = getStartBlockOfRange(range, root);
+ let nodeAfterCursor;
+
+ if (startBlock) {
+ // If in the middle or end of a text node, we're not at the boundary.
+ if (isTextNode(startContainer)) {
+ if (startOffset) {
+ return false;
+ }
+ nodeAfterCursor = startContainer;
+ } else {
+ nodeAfterCursor = getNodeAfter(startContainer, startOffset);
+ // The cursor was right at the end of the document
+ if (!nodeAfterCursor || !root.contains(nodeAfterCursor)) {
+ nodeAfterCursor = getNodeBefore(startContainer, startOffset);
+ if (isTextNode(nodeAfterCursor) && nodeAfterCursor.length) {
+ return false;
+ }
+ }
+ }
+
+ // Otherwise, look for any previous content in the same block.
+ contentWalker = newContentWalker(getStartBlockOfRange(range, root));
+ contentWalker.currentNode = nodeAfterCursor;
+
+ return !contentWalker.previousNode();
+ }
+ },
+ rangeDoesEndAtBlockBoundary = (range, root) => {
+ let endContainer = range.endContainer,
+ endOffset = range.endOffset,
+ length;
+
+ // Otherwise, look for any further content in the same block.
+ contentWalker = newContentWalker(getStartBlockOfRange(range, root));
+
+ // If in a text node with content, and not at the end, we're not
+ // at the boundary
+ if (isTextNode(endContainer)) {
+ length = endContainer.data.length;
+ if (length && endOffset < length) {
+ return false;
+ }
+ contentWalker.currentNode = endContainer;
+ } else {
+ contentWalker.currentNode = getNodeBefore(endContainer, endOffset);
+ }
+
+ return !contentWalker.nextNode();
+ },
+ expandRangeToBlockBoundaries = (range, root) => {
+ let start = getStartBlockOfRange(range, root),
+ end = getEndBlockOfRange(range, root);
+
+ if (start && end) {
+ range.setStart(start, 0);
+ range.setEnd(end, end.childNodes.length);
+// parent = start.parentNode;
+// range.setStart(parent, indexOf(parent.childNodes, start));
+// parent = end.parentNode;
+// range.setEnd(parent, indexOf(parent.childNodes, end) + 1);
+ }
+ },
+
+ // source/range/InsertDelete.ts
insertNodeInRange = (range, node) => {
// Insert at start.
let startContainer = range.startContainer,
@@ -500,7 +938,7 @@ const
parent, children, childCount, afterSplit;
// If part way through a text node, split it.
- if (startContainer.nodeType === TEXT_NODE) {
+ if (isTextNode(startContainer)) {
parent = startContainer.parentNode;
children = parent.childNodes;
if (startOffset === startContainer.length) {
@@ -515,8 +953,7 @@ const
if (endContainer === startContainer) {
endOffset -= startOffset;
endContainer = afterSplit;
- }
- else if (endContainer === parent) {
+ } else if (endContainer === parent) {
++endOffset;
}
startContainer = afterSplit;
@@ -533,7 +970,7 @@ const
if (startOffset === childCount) {
startContainer.append(node);
} else {
- startContainer.insertBefore(node, children[ startOffset ]);
+ startContainer.insertBefore(node, children[startOffset]);
}
if (startContainer === endContainer) {
@@ -543,7 +980,6 @@ const
range.setStart(startContainer, startOffset);
range.setEnd(endContainer, endOffset);
},
-
extractContentsOfRange = (range, common, root) => {
let startContainer = range.startContainer,
startOffset = range.startOffset,
@@ -554,7 +990,7 @@ const
common = range.commonAncestorContainer;
}
- if (common.nodeType === TEXT_NODE) {
+ if (isTextNode(common)) {
common = common.parentNode;
}
@@ -577,9 +1013,9 @@ const
// Merge text nodes if adjacent. IE10 in particular will not focus
// between two text nodes
- after = common.childNodes[ startOffset ];
+ after = common.childNodes[startOffset];
before = after?.previousSibling;
- if (before?.nodeType === TEXT_NODE && after.nodeType === TEXT_NODE) {
+ if (isTextNode(before) && isTextNode(after)) {
startContainer = before;
startOffset = before.length;
beforeText = before.data;
@@ -603,7 +1039,6 @@ const
return frag;
},
-
deleteContentsOfRange = (range, root) => {
let startBlock = getStartBlockOfRange(range, root);
let endBlock = getEndBlockOfRange(range, root);
@@ -645,7 +1080,6 @@ const
}
return frag;
},
-
// Contents of range will be deleted.
// After method, range will be around inserted content
insertTreeFragmentIntoRange = (range, frag, root) => {
@@ -699,14 +1133,20 @@ const
if (isInline(container)) {
// Split up to block parent.
nodeAfterSplit = split(
- container, offset, getPreviousBlock(container, root), root);
+ container,
+ offset,
+ getPreviousBlock(container, root) || root,
+ root
+ );
container = nodeAfterSplit.parentNode;
offset = indexOf(container.childNodes, nodeAfterSplit);
}
- if (/*isBlock(container) && */offset !== getLength(container)) {
+ if (/*isBlock(container) && */
+ offset !== getLength(container)
+ ) {
// Collect any inline contents of the block after the range point
blockContentsAfterSplit = doc.createDocumentFragment();
- while ((node = container.childNodes[ offset ])) {
+ while ((node = container.childNodes[offset])) {
blockContentsAfterSplit.append(node);
}
}
@@ -721,7 +1161,7 @@ const
// Is there still any content in the fragment?
if (getLength(frag)) {
- if (replaceBlock) {
+ if (replaceBlock && block) {
range.setEndBefore(block);
range.collapse(false);
detach(block);
@@ -729,10 +1169,12 @@ const
moveRangeBoundariesUpTree(range, stopPoint, stopPoint, root);
// Now split after block up to blockquote (if a parent) or root
nodeAfterSplit = split(
- range.endContainer, range.endOffset, stopPoint, root);
- nodeBeforeSplit = nodeAfterSplit ?
- nodeAfterSplit.previousSibling :
- stopPoint.lastChild;
+ range.endContainer,
+ range.endOffset,
+ stopPoint,
+ root
+ );
+ nodeBeforeSplit = nodeAfterSplit ? nodeAfterSplit.previousSibling : stopPoint.lastChild;
stopPoint.insertBefore(frag, nodeAfterSplit);
if (nodeAfterSplit) {
range.setEndBefore(nodeAfterSplit);
@@ -758,7 +1200,7 @@ const
}
// Insert inline content saved from before.
- if (blockContentsAfterSplit) {
+ if (blockContentsAfterSplit && block) {
tempRange = range.cloneRange();
mergeWithBlock(block, blockContentsAfterSplit, tempRange, root);
range.setEnd(tempRange.endContainer, tempRange.endOffset);
@@ -766,256 +1208,568 @@ const
moveRangeBoundariesDownTree(range);
},
- isNodeContainedInRange = (range, node, partial = true) => {
- let nodeRange = doc.createRange();
+ // source/Clipboard.ts
+ // The (non-standard but supported enough) innerText property is based on the
+ // render tree in Firefox and possibly other browsers, so we must insert the
+ // DOM node into the document to ensure the text part is correct.
+ setClipboardData = (event, contents, root) => {
+ let clipboardData = event.clipboardData;
+ let body = doc.body;
+ let node = createElement('div');
+ let html, text;
- nodeRange.selectNode(node);
+ node.append(contents);
- return partial
- // Node must not finish before range starts or start after range finishes.
- ? range.compareBoundaryPoints(END_TO_START, nodeRange) < 0
- && range.compareBoundaryPoints(START_TO_END, nodeRange) > 0
- // Node must start after range starts and finish before range finishes
- : range.compareBoundaryPoints(START_TO_START, nodeRange) < 1
- && range.compareBoundaryPoints(END_TO_END, nodeRange) > -1;
+ html = node.innerHTML;
+
+ // 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, so we need to remove them first.
+ cleanupBRs(node, root, true);
+ node.setAttribute('style',
+ 'position:fixed;overflow:hidden;bottom:100%;right:100%;');
+ body.append(node);
+ text = (node.innerText || node.textContent).replace(NBSP, ' '); // Replace nbsp with regular space
+ node.remove();
+
+ if (text !== html) {
+ clipboardData.setData('text/html', html);
+ }
+ clipboardData.setData('text/plain', text);
+ event.preventDefault();
},
+ onCut = function(event) {
+ let self = this;
+ let range = self.getSelection();
+ let root = self._root;
+ let startBlock, endBlock, copyRoot, contents, parent, newContents;
- moveRangeBoundariesDownTree = range => {
- let { startContainer, startOffset, endContainer, endOffset } = range;
- let maySkipBR = true,
- child;
-
- while (!(startContainer instanceof Text)) {
- child = startContainer.childNodes[startOffset];
- if (!child || isLeaf(child)) {
- if (startOffset) {
- child = startContainer.childNodes[startOffset - 1];
- if (child instanceof Text) {
- let textChild = child;
- let prev;
- while (!textChild.length && (prev = textChild.previousSibling) && prev instanceof Text) {
- textChild.remove();
- textChild = prev;
- }
- startContainer = textChild;
- startOffset = textChild.data.length;
- }
- }
- break;
- }
- startContainer = child;
- startOffset = 0;
- }
- if (endOffset) {
- while (!(endContainer instanceof Text)) {
- child = endContainer.childNodes[endOffset - 1];
- if (!child || isLeaf(child)) {
- if (maySkipBR && child?.nodeName === 'BR') {
- --endOffset;
- maySkipBR = false;
- continue;
- }
- break;
- }
- endContainer = child;
- endOffset = getLength(endContainer);
- }
- } else {
- while (!(endContainer instanceof Text)) {
- child = endContainer.firstChild;
- if (!child || isLeaf(child)) {
- break;
- }
- endContainer = child;
- }
- }
-
- // If collapsed, this algorithm finds the nearest text node positions
- // *outside* the range rather than inside, but also it flips which is
- // assigned to which.
+ // Nothing to do
if (range.collapsed) {
- range.setStart(endContainer, endOffset);
- range.setEnd(startContainer, startOffset);
- } else {
- range.setStart(startContainer, startOffset);
- range.setEnd(endContainer, endOffset);
- }
- },
-
- moveRangeBoundariesUpTree = (range, startMax, endMax, root) => {
- let startContainer = range.startContainer;
- let startOffset = range.startOffset;
- let endContainer = range.endContainer;
- let endOffset = range.endOffset;
- let maySkipBR = true;
- let parent;
-
- if (!startMax) {
- startMax = range.commonAncestorContainer;
- }
- if (!endMax) {
- endMax = startMax;
+ event.preventDefault();
+ return;
}
- while (!startOffset &&
- startContainer !== startMax &&
- startContainer !== root) {
- parent = startContainer.parentNode;
- startOffset = indexOf(parent.childNodes, startContainer);
- startContainer = parent;
- }
+ // Save undo checkpoint
+ self.saveUndoState(range);
- while (endContainer !== endMax && endContainer !== root) {
- if (maySkipBR &&
- endContainer.nodeType !== TEXT_NODE &&
- endContainer.childNodes[ endOffset ] &&
- endContainer.childNodes[ endOffset ].nodeName === 'BR') {
- ++endOffset;
- maySkipBR = false;
+ // Edge only seems to support setting plain text as of 2016-03-11.
+ if (event.clipboardData) {
+ // Clipboard content should include all parents within block, or all
+ // 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 (isTextNode(parent)) {
+ parent = parent.parentNode;
}
- if (endOffset !== getLength(endContainer)) {
- break;
+ while (parent && parent !== copyRoot) {
+ newContents = parent.cloneNode(false);
+ newContents.append(contents);
+ contents = newContents;
+ parent = parent.parentNode;
}
- parent = endContainer.parentNode;
- endOffset = indexOf(parent.childNodes, endContainer) + 1;
- endContainer = parent;
+ // Set clipboard data
+ setClipboardData(event, contents, root);
+ } else {
+ setTimeout(() => {
+ try {
+ // If all content removed, ensure div at start of root.
+ self._ensureBottomLine();
+ } catch (error) {
+ didError(error);
+ }
+ }, 0);
}
- range.setStart(startContainer, startOffset);
- range.setEnd(endContainer, endOffset);
+ self.setSelection(range);
},
-
- moveRangeBoundaryOutOf = (range, nodeName, root) => {
- let parent = getClosest(range.endContainer, root, 'A');
- if (parent) {
- let clone = range.cloneRange();
- parent = parent.parentNode;
- moveRangeBoundariesUpTree(clone, parent, parent, root);
- if (clone.endContainer === parent) {
- range.setStart(clone.endContainer, clone.endOffset);
- range.setEnd(clone.endContainer, clone.endOffset);
+ onCopy = function(event) {
+ // Edge only seems to support setting plain text as of 2016-03-11.
+ if (event.clipboardData) {
+ let range = this.getSelection(), root = this._root,
+ // Clipboard content should include all parents within block, or all
+ // parents up to root if selection across blocks
+ startBlock = getStartBlockOfRange(range, root),
+ endBlock = getEndBlockOfRange(range, root),
+ copyRoot = ((startBlock === endBlock) && startBlock) || root,
+ contents, parent, newContents;
+ // Clone range to mutate, then move up as high as possible without
+ // passing the copy root node.
+ range = range.cloneRange();
+ moveRangeBoundariesDownTree(range);
+ moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
+ // Extract the contents
+ contents = range.cloneContents();
+ // Add any other parents not in extracted content, up to copy root
+ parent = range.commonAncestorContainer;
+ if (isTextNode(parent)) {
+ parent = parent.parentNode;
}
+ while (parent && parent !== copyRoot) {
+ newContents = parent.cloneNode(false);
+ newContents.append(contents);
+ contents = newContents;
+ parent = parent.parentNode;
+ }
+ // Set clipboard data
+ setClipboardData(event, contents, root);
}
- return range;
},
+ onPaste = function(event) {
+ let clipboardData = event.clipboardData;
+ let items = clipboardData?.items;
+ let imageItem = null;
+ let plainItem = null;
+ let htmlItem = null;
+ let self = this;
+ let type;
- // Returns the first block at least partially contained by the range,
- // or null if no block is contained by the range.
- getStartBlockOfRange = (range, root) => {
- let container = range.startContainer,
- block;
-
- // If inline, get the containing block.
- if (isInline(container)) {
- block = getPreviousBlock(container, root);
- } else if (container !== root && isBlock(container)) {
- block = container;
- } else {
- block = getNextBlock(getNodeBefore(container, range.startOffset), root);
- }
- // Check the block actually intersects the range
- return block && isNodeContainedInRange(range, block) ? block : null;
- },
-
- // Returns the last block at least partially contained by the range,
- // or null if no block is contained by the range.
- getEndBlockOfRange = (range, root) => {
- let container = range.endContainer,
- block, child;
-
- // If inline, get the containing block.
- if (isInline(container)) {
- block = getPreviousBlock(container, root);
- } else if (container !== root && isBlock(container)) {
- block = container;
- } else {
- block = getNodeAfter(container, range.endOffset);
- if (!block || !root.contains(block)) {
- block = root;
- while (child = block.lastChild) {
- block = child;
+ // Current HTML5 Clipboard interface
+ // ---------------------------------
+ // https://html.spec.whatwg.org/multipage/interaction.html
+ if (items) {
+ [...items].forEach(item => {
+ type = item.type;
+ if (type === 'text/html') {
+ htmlItem = item;
+ // iOS copy URL gives you type text/uri-list which is just a list
+ // of 1 or more URLs separated by new lines. Can just treat as
+ // plain text.
+ } else if (type === 'text/plain' || type === 'text/uri-list') {
+ plainItem = item;
+ } else if (item.kind === 'file' && /^image\/(png|jpeg|webp)/.test(type)) {
+ imageItem = item;
+ }
+ });
+ if (htmlItem || plainItem || imageItem) {
+ event.preventDefault();
+ if (imageItem) {
+ let reader = new FileReader();
+ reader.onload = event => {
+ let img = createElement('img', {src: event.target.result}),
+ canvas = createElement('canvas'),
+ ctx = canvas.getContext('2d');
+ img.onload = ()=>{
+ ctx.drawImage(img, 0, 0);
+ let width = img.width, height = img.height;
+ if (width > height) {
+ // Landscape
+ if (width > 1024) {
+ height = height * 1024 / width;
+ width = 1024;
+ }
+ } else if (height > 1024) {
+ // Portrait
+ width = width * 1024 / height;
+ height = 1024;
+ }
+ canvas.width = width;
+ canvas.height = height;
+ ctx.drawImage(img, 0, 0, width, height);
+ self.insertHTML('
+')
', true);
+ };
+ }
+ reader.readAsDataURL(imageItem.getAsFile());
+ } else if (htmlItem && (!self.isShiftDown || !plainItem)) {
+ htmlItem.getAsString(html => self.insertHTML(html, true));
+ } else if (plainItem) {
+ plainItem.getAsString(text => self.insertPlainText(text, true));
}
}
- block = getPreviousBlock(block, root);
}
- // Check the block actually intersects the range
- return block && isNodeContainedInRange(range, block) ? block : null;
+ },
+ // On Windows you can drag an drop text. We can't handle this ourselves, because
+ // as far as I can see, there's no way to get the drop insertion point. So just
+ // save an undo state and hope for the best.
+ onDrop = function(event) {
+ let types = event.dataTransfer.types;
+ if (types.includes('text/plain') || types.includes('text/html')) {
+ this.saveUndoState();
+ }
+ },
+
+ // source/keyboard/KeyHandlers.ts
+ onKey = function(event) {
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ let key = event.key.toLowerCase(),
+ modifiers = '',
+ range = this.getSelection(),
+ root = this._root;
+
+ // We need to apply the backspace/delete handlers regardless of
+ // control key modifiers.
+ if (key !== 'backspace' && key !== 'delete') {
+ if (event.altKey) { modifiers += 'alt-'; }
+ if (event[osKey]) { modifiers += ctrlKey; }
+ if (event.shiftKey) { modifiers += 'shift-'; }
+ }
+
+ key = modifiers + key;
+
+ if (this._keyHandlers[key]) {
+ this._keyHandlers[key](this, event, range);
+ // !event.isComposing stops us from blatting Kana-Kanji conversion in Safari
+ } else if (!range.collapsed && !event.isComposing && !event[osKey] && key.length === 1) {
+ // Record undo checkpoint.
+ this.saveUndoState(range);
+ // Delete the selection
+ deleteContentsOfRange(range, root);
+ this._ensureBottomLine();
+ this.setSelection(range);
+ this._updatePath(range, true);
+ } else if (range.collapsed
+ && range.startContainer === root
+ && root.children.length > 0) {
+
+ // Under certain conditions, cursor/range can be positioned directly
+ // under this._root (not wrapped) and when this happens, an inline(TEXT)
+ // element is attached directly to this._root. There might be other
+ // issues, but squire.makeUnorderedList(), squire.makeOrderedList() and
+ // maybe other functions that call squire.modifyBlocks() do NOT work
+ // on inline(TEXT) nodes that are direct children of this._root.
+ // Therefor, we try to detect this case here and wrap the cursor/range
+ // before the text is inserted.
+
+ const nextElement = root.children[range.startOffset];
+ if (nextElement && !isBlock(nextElement)) {
+ // create a new wrapper
+ range = createRange(root.insertBefore(
+ this.createDefaultBlock(), nextElement
+ ), 0);
+ if (nextElement.tagName === 'BR') {
+ // delete it because a new
is created by createDefaultBlock()
+ root.removeChild(nextElement);
+ }
+ const restore = this._restoreSelection;
+ this.setSelection(range);
+ this._restoreSelection = restore;
+ }
+ }
+ },
+ keyHandlers = {
+ // This song and dance is to force iOS to do enable the shift key
+ // automatically on enter. When you do the DOM split manipulation yourself,
+ // WebKit doesn't reset the IME state and so presents auto-complete options
+ // as though you were continuing to type on the previous line, and doesn't
+ // auto-enable the shift key. The old trick of blurring and focussing
+ // again no longer works in iOS 13, and I tried various execCommand options
+ // but they didn't seem to do anything. The only solution I've found is to
+ // let iOS handle the enter key, then after it's done that reset the HTML
+ // to what it was before and handle it properly in Squire; the IME state of
+ // course doesn't reset so you end up in the correct state!
+ enter: isIOS ? (self, event, range) => {
+ self._saveRangeToBookmark(range);
+ let html = self._getHTML();
+ let restoreAndDoEnter = () => {
+ self.removeEventListener('keyup', restoreAndDoEnter);
+ self._setHTML(html);
+ range = self._getRangeAndRemoveBookmark();
+ // Ignore the shift key on iOS, as this is for auto-capitalisation.
+ handleEnter(self, false, range);
+ };
+ self.addEventListener('keyup', restoreAndDoEnter);
+ } : (self, event, range) => {
+ event.preventDefault();
+ handleEnter(self, event.shiftKey, range);
+ },
+
+ 'shift-enter': (self, event, range) => self._keyHandlers.enter(self, event, range),
+
+ backspace: (self, event, range) => {
+ let root = self._root;
+ self._removeZWS();
+ // Record undo checkpoint.
+ self.saveUndoState(range);
+ // If not collapsed, delete contents
+ if (!range.collapsed) {
+ event.preventDefault();
+ deleteContentsOfRange(range, root);
+ afterDelete(self, range);
+ }
+ // If at beginning of block, merge with previous
+ else if (rangeDoesStartAtBlockBoundary(range, root)) {
+ event.preventDefault();
+ let current = getStartBlockOfRange(range, root);
+ let previous;
+ if (!current) {
+ return;
+ }
+ // In case inline data has somehow got between blocks.
+ fixContainer(current.parentNode, root);
+ // Now get previous block
+ previous = getPreviousBlock(current, root);
+ // Must not be at the very beginning of the text area.
+ if (previous) {
+ // If not editable, just delete whole block.
+ if (!previous.isContentEditable) {
+ detachUneditableNode(previous, root);
+ return;
+ }
+ // Otherwise merge.
+ mergeWithBlock(previous, current, range, root);
+ // If deleted line between containers, merge newly adjacent
+ // containers.
+ current = previous.parentNode;
+ while (current !== root && !current.nextSibling) {
+ current = current.parentNode;
+ }
+ if (current !== root && (current = current.nextSibling)) {
+ mergeContainers(current, root);
+ }
+ self.setSelection(range);
+ }
+ // If at very beginning of text area, allow backspace
+ // to break lists/blockquote.
+ else if (current) {
+ let parent = getClosest(current, root, 'UL,OL,BLOCKQUOTE');
+ if (parent) {
+ return ('BLOCKQUOTE' === parent.nodeName)
+ // Break blockquote
+ ? self.decreaseQuoteLevel(range)
+ // Break list
+ : self.decreaseListLevel(range);
+ }
+ self.setSelection(range);
+ self._updatePath(range, true);
+ }
+ }
+ // Otherwise, leave to browser but check afterwards whether it has
+ // left behind an empty inline tag.
+ else {
+ self.setSelection(range);
+ setTimeout(() => afterDelete(self), 0);
+ }
+ },
+ 'delete': (self, event, range) => {
+ let root = self._root;
+ let current, next, originalRange,
+ cursorContainer, cursorOffset, nodeAfterCursor;
+ self._removeZWS();
+ // Record undo checkpoint.
+ self.saveUndoState(range);
+ // If not collapsed, delete contents
+ if (!range.collapsed) {
+ event.preventDefault();
+ deleteContentsOfRange(range, root);
+ afterDelete(self, range);
+ }
+ // If at end of block, merge next into this block
+ else if (rangeDoesEndAtBlockBoundary(range, root)) {
+ event.preventDefault();
+ if (current = getStartBlockOfRange(range, root)) {
+ // In case inline data has somehow got between blocks.
+ fixContainer(current.parentNode, root);
+ // Now get next block
+ // Must not be at the very end of the text area.
+ if (next = getNextBlock(current, root)) {
+ // If not editable, just delete whole block.
+ if (!next.isContentEditable) {
+ detachUneditableNode(next, root);
+ return;
+ }
+ // 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
+ // left behind an empty inline tag.
+ else {
+ // But first check if the cursor is just before an IMG tag. If so,
+ // delete it ourselves, because the browser won't if it is not
+ // inline.
+ originalRange = range.cloneRange();
+ moveRangeBoundariesUpTree(range, root, root, root);
+ cursorContainer = range.endContainer;
+ cursorOffset = range.endOffset;
+ if (isElement(cursorContainer)) {
+ nodeAfterCursor = cursorContainer.childNodes[cursorOffset];
+ if (nodeAfterCursor?.nodeName === 'IMG') {
+ event.preventDefault();
+ detach(nodeAfterCursor);
+ moveRangeBoundariesDownTree(range);
+ afterDelete(self, range);
+ return;
+ }
+ }
+ self.setSelection(originalRange);
+ setTimeout(() => afterDelete(self), 0);
+ }
+ },
+ tab: (self, event, range) => {
+ let root = self._root;
+ let node, parent;
+ self._removeZWS();
+ // If no selection and at start of block
+ if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) {
+ node = getStartBlockOfRange(range, root);
+ // Iterate through the block's parents
+ while ((parent = node.parentNode)) {
+ // If we find a UL or OL (so are in a list, node must be an LI)
+ if (parent.nodeName === 'UL' || parent.nodeName === 'OL') {
+ // Then increase the list level
+ event.preventDefault();
+ self.increaseListLevel(range);
+ break;
+ }
+ node = parent;
+ }
+ }
+ },
+ 'shift-tab': (self, event, range) => {
+ let root = self._root;
+ let node;
+ self._removeZWS();
+ // If no selection and at start of block
+ if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) {
+ // Break list
+ node = range.startContainer;
+ if (getClosest(node, root, 'UL,OL')) {
+ event.preventDefault();
+ self.decreaseListLevel(range);
+ }
+ }
+ },
+ space: (self, _, range) => {
+ let root = self._root;
+ self._recordUndoState(range, false);
+ self._config.addLinks && addLinks(range.startContainer, root, self);
+ self._getRangeAndRemoveBookmark(range);
+ /*
+ // If the cursor is at the end of a link (
foo|) then move it
+ // outside of the link (
foo|) so that the space is not part of
+ // the link text.
+ // SnappyMail: disabled as it fails in Firefox
+ let node = range.endContainer;
+ if (range.collapsed && range.endOffset === getLength(node)) {
+ do {
+ if (node.nodeName === 'A') {
+ range.setStartAfter(node);
+ break;
+ }
+ } while (!node.nextSibling && (node = node.parentNode) && node !== root);
+ }
+ */
+ // Delete the selection if not collapsed
+ if (!range.collapsed) {
+ deleteContentsOfRange(range, root);
+ self._ensureBottomLine();
+ self.setSelection(range);
+ self._updatePath(range, true);
+ }
+
+ self.setSelection(range);
+ },
+ arrowleft: self => self._removeZWS(),
+ arrowright: self => self._removeZWS()
+ },
+ mapKeyToFormat = (tag, remove) => {
+ return (self, event) => {
+ event.preventDefault();
+ self.toggleTag(tag, remove);
+ };
+ },
+ mapKeyTo = method => (self, event) => {
+ event.preventDefault();
+ self[method]();
+ },
+
+ blockTag = 'DIV',
+ DOCUMENT_POSITION_PRECEDING = 2, // Node.DOCUMENT_POSITION_PRECEDING
+
+ NBSP = '\u00A0',
+
+ win = doc.defaultView,
+
+ osKey = isMac ? 'metaKey' : 'ctrlKey',
+
+ indexOf = (array, value) => Array.prototype.indexOf.call(array, value),
+
+ filterAccept = NodeFilter.FILTER_ACCEPT,
+/*
+ typeToBitArray = {
+ // ELEMENT_NODE
+ 1: 1,
+ // ATTRIBUTE_NODE
+ 2: 2,
+ // TEXT_NODE
+ 3: 4,
+ // COMMENT_NODE
+ 8: 128,
+ // DOCUMENT_NODE
+ 9: 256,
+ // DOCUMENT_FRAGMENT_NODE
+ 11: 1024
+ },
+*/
+
+ isElement = node => node instanceof Element,
+ isTextNode = node => node instanceof Text,
+
+ createTreeWalker = (root, whatToShow, filter) => doc.createTreeWalker(root, whatToShow, filter ? {
+ acceptNode: node => filter(node) ? filterAccept : NodeFilter.FILTER_SKIP
+ } : null
+ ),
+
+ getPath = (node, root) => {
+ let path = '', style;
+ if (node && node !== root) {
+ path = getPath(node.parentNode, root);
+ if (isElement(node)) {
+ path += (path ? '>' : '') + node.nodeName;
+ if (node.id) {
+ path += '#' + node.id;
+ }
+ if (node.classList.length) {
+ path += '.' + [...node.classList].sort().join('.');
+ }
+ if (node.dir) {
+ path += '[dir=' + node.dir + ']';
+ }
+ if (style = node.style.cssText) {
+ path += '[style=' + style + ']';
+ }
+ }
+ }
+ return path;
+ },
+
+ setAttributes = (node, props) => {
+ props && Object.entries(props).forEach(([k,v]) => {
+ if ('style' === k && typeof v === 'object') {
+ Object.entries(v).forEach(([k,v]) => node.style[k] = v);
+ } else if (v != null) {
+ node.setAttribute(k, v);
+ } else {
+ node.removeAttribute(k);
+ }
+ });
},
newContentWalker = root => createTreeWalker(root,
SHOW_ELEMENT_OR_TEXT,
- node => node.nodeType === TEXT_NODE ? notWS.test(node.data) : node.nodeName === 'IMG'
+ node => isTextNode(node) ? notWS.test(node.data) : node.nodeName === 'IMG'
),
- rangeDoesStartAtBlockBoundary = (range, root) => {
- let startContainer = range.startContainer;
- let startOffset = range.startOffset;
- let startBlock = getStartBlockOfRange(range, root);
- let nodeAfterCursor;
-
- if (startBlock) {
- // If in the middle or end of a text node, we're not at the boundary.
- if (startContainer.nodeType === TEXT_NODE) {
- if (startOffset) {
- return false;
- }
- nodeAfterCursor = startContainer;
- } else {
- nodeAfterCursor = getNodeAfter(startContainer, startOffset);
- // The cursor was right at the end of the document
- if (!nodeAfterCursor || !root.contains(nodeAfterCursor)) {
- nodeAfterCursor = getNodeBefore(startContainer, startOffset);
- if (nodeAfterCursor.nodeType === TEXT_NODE && nodeAfterCursor.length) {
- return false;
- }
- }
- }
-
- // Otherwise, look for any previous content in the same block.
- contentWalker = newContentWalker(getStartBlockOfRange(range, root));
- contentWalker.currentNode = nodeAfterCursor;
-
- return !contentWalker.previousNode();
- }
- },
-
- rangeDoesEndAtBlockBoundary = (range, root) => {
- let endContainer = range.endContainer,
- endOffset = range.endOffset,
- length;
-
- // Otherwise, look for any further content in the same block.
- contentWalker = newContentWalker(getStartBlockOfRange(range, root));
-
- // If in a text node with content, and not at the end, we're not
- // at the boundary
- if (endContainer.nodeType === TEXT_NODE) {
- length = endContainer.data.length;
- if (length && endOffset < length) {
- return false;
- }
- contentWalker.currentNode = endContainer;
- } else {
- contentWalker.currentNode = getNodeBefore(endContainer, endOffset);
- }
-
- return !contentWalker.nextNode();
- },
-
- expandRangeToBlockBoundaries = (range, root) => {
- let start = getStartBlockOfRange(range, root),
- end = getEndBlockOfRange(range, root);
-
- if (start && end) {
- range.setStart(start, 0);
- range.setEnd(end, end.childNodes.length);
-// parent = start.parentNode;
-// range.setStart(parent, indexOf(parent.childNodes, start));
-// parent = end.parentNode;
-// range.setEnd(parent, indexOf(parent.childNodes, end) + 1);
- }
- },
-
didError = error => console.error(error),
createRange = (range, startOffset, endContainer, endOffset) => {
@@ -1032,18 +1786,6 @@ const
return domRange;
},
- mapKeyTo = method => (self, event) => {
- event.preventDefault();
- self[ method ]();
- },
-
- mapKeyToFormat = (tag, remove) => {
- return (self, event) => {
- event.preventDefault();
- self.toggleTag(tag, remove);
- };
- },
-
// If you delete the content inside a span with a font styling, Webkit will
// replace it with a
tag (!). If you delete all the text inside a
// link in Opera, it won't delete the link. Let's make things consistent. If
@@ -1055,7 +1797,7 @@ const
parent;
// Climb the tree from the focus point while we are inside an empty
// inline element
- if (node.nodeType === TEXT_NODE) {
+ if (isTextNode(node)) {
node = node.parentNode;
}
parent = node;
@@ -1130,7 +1872,7 @@ const
moveRangeBoundariesDownTree(range);
node = range.startContainer;
offset = range.startOffset;
- if (node.nodeType !== TEXT_NODE) {
+ if (!isTextNode(node)) {
node = doc.createTextNode('');
parent.insertBefore(node, parent.firstChild);
}
@@ -1221,7 +1963,7 @@ const
break;
}
- while (child?.nodeType === TEXT_NODE && !child.data) {
+ while (isTextNode(child) && !child.data) {
next = child.nextSibling;
if (!next || next.nodeName === 'BR') {
break;
@@ -1233,8 +1975,7 @@ const
// 'BR's essentially don't count; they're a browser hack.
// If you try to select the contents of a 'BR', FF will not let
// you type anything!
- if (!child || child.nodeName === 'BR' ||
- child.nodeType === TEXT_NODE) {
+ if (!child || child.nodeName === 'BR' || isTextNode(child)) {
break;
}
nodeAfterSplit = child;
@@ -1244,7 +1985,6 @@ const
self._updatePath(range, true);
},
-
changeIndentationLevel = direction => (self, event) => {
event.preventDefault();
self.changeIndentationLevel(direction);
@@ -1256,144 +1996,10 @@ const
if (type == parent?.nodeName) {
self.removeList();
} else {
- self[ methodIfNotInList ]();
+ self[methodIfNotInList]();
}
},
- fontSizes = {
- 1: 'x-small',
- 2: 'small',
- 3: 'medium',
- 4: 'large',
- 5: 'x-large',
- 6: 'xx-large',
- 7: 'xxx-large',
- '-1': 'smaller',
- '+1': 'larger'
- },
-
- styleToSemantic = {
- fontWeight: {
- regexp: /^bold|^700/i,
- replace: () => createElement('B')
- },
- fontStyle: {
- regexp: /^italic/i,
- replace: () => createElement('I')
- },
- fontFamily: {
- regexp: notWS,
- replace: (doc, family) => createElement('SPAN', {
- style: 'font-family:' + family
- })
- },
- fontSize: {
- regexp: notWS,
- replace: (doc, size) => createElement('SPAN', {
- style: 'font-size:' + size
- })
- },
- textDecoration: {
- regexp: /^underline/i,
- replace: () => createElement('U')
- }
- /*
- textDecoration: {
- regexp: /^line-through/i,
- replace: doc => createElement('S')
- }
- */
- },
-
- replaceWithTag = tag => node => {
- let el = createElement(tag);
- Array.prototype.forEach.call(node.attributes, attr => el.setAttribute(attr.name, attr.value));
- node.replaceWith(el);
- el.append(empty(node));
- return el;
- },
-
- replaceStyles = node => {
- let style = node.style;
- let css, newTreeBottom, newTreeTop, el;
-
- Object.entries(styleToSemantic).forEach(([attr,converter])=>{
- css = style[ attr ];
- if (css && converter.regexp.test(css)) {
- el = converter.replace(doc, css);
- if (el.nodeName === node.nodeName &&
- el.className === node.className) {
- return;
- }
- if (!newTreeTop) {
- newTreeTop = el;
- }
- if (newTreeBottom) {
- newTreeBottom.append(el);
- }
- newTreeBottom = el;
- node.style[ attr ] = '';
- }
- });
-
- if (newTreeTop && newTreeBottom) {
- newTreeBottom.append(empty(node));
- if (node.style.cssText) {
- node.append(newTreeTop);
- } else {
- node.replaceWith(newTreeTop);
- }
- }
-
- return newTreeBottom || node;
- },
-
- stylesRewriters = {
- SPAN: replaceStyles,
- STRONG: replaceWithTag('B'),
- EM: replaceWithTag('I'),
- INS: replaceWithTag('U'),
- STRIKE: replaceWithTag('S'),
- FONT: node => {
- let face = node.face,
- size = node.size,
- color = node.color,
- newTag = createElement('SPAN'),
- css = newTag.style;
- newTag.style.cssText = node.style.cssText;
- if (face) {
- css.fontFamily = face;
- }
- if (size) {
- css.fontSize = fontSizes[ size ];
- }
- if (color && /^#?([\dA-F]{3}){1,2}$/i.test(color)) {
- if (color.charAt(0) !== '#') {
- color = '#' + color;
- }
- css.color = color;
- }
- node.replaceWith(newTag);
- newTag.append(empty(node));
- return newTag;
- },
- // KBD:
- // VAR:
- // CODE:
- // SAMP:
- TT: node => {
- let el = createElement('SPAN', {
- style: 'font-family:menlo,consolas,"courier new",monospace'
- });
- node.replaceWith(el);
- el.append(empty(node));
- return el;
- }
- },
-
- 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)/,
/*
// Previous node in post-order.
previousPONode = walker => {
@@ -1408,7 +2014,7 @@ const
node = current.previousSibling;
if (!node) { current = current.parentNode; }
}
- if (node && (typeToBitArray[ node.nodeType ] & nodeType) && filter(node)) {
+ if (node && (typeToBitArray[node.nodeType] & nodeType) && filter(node)) {
walker.currentNode = node;
return node;
}
@@ -1417,193 +2023,13 @@ const
return null;
},
*/
- /*
- Two purposes:
-
- 1. Remove nodes we don't want, such as weird tags, comment nodes
- and whitespace nodes.
- 2. Convert inline tags into our preferred format.
- */
- cleanTree = (node, preserveWS) => {
- let children = node.childNodes,
- nonInlineParent, i, l, child, nodeName, nodeType, childLength;
-// startsWithWS, endsWithWS, data, sibling;
-
- nonInlineParent = node;
- while (isInline(nonInlineParent)) {
- nonInlineParent = nonInlineParent.parentNode;
- }
-// let walker = createTreeWalker(nonInlineParent, SHOW_ELEMENT_OR_TEXT);
-
- for (i = 0, l = children.length; i < l; ++i) {
- child = children[i];
- nodeName = child.nodeName;
- nodeType = child.nodeType;
- if (nodeType === ELEMENT_NODE) {
- childLength = child.childNodes.length;
- if (stylesRewriters[ nodeName ]) {
- child = stylesRewriters[ nodeName ](child);
- } else if (blacklist.test(nodeName)) {
- child.remove();
- --i;
- --l;
- continue;
- } else if (!allowedBlock.test(nodeName) && !isInline(child)) {
- --i;
- l += childLength - 1;
- child.replaceWith(empty(child));
- continue;
- }
- if (childLength) {
- cleanTree(child, preserveWS || (nodeName === 'PRE'));
- }
-/*
- } else {
- if (nodeType === TEXT_NODE) {
- data = child.data;
- startsWithWS = !notWS.test(data.charAt(0));
- endsWithWS = !notWS.test(data.charAt(data.length - 1));
- if (preserveWS || (!startsWithWS && !endsWithWS)) {
- continue;
- }
- // Iterate through the nodes; if we hit some other content
- // before the start of a new block we don't trim
- if (startsWithWS) {
- walker.currentNode = child;
- while (sibling = previousPONode(walker)) {
- nodeName = sibling.nodeName;
- if (nodeName === 'IMG' || (nodeName === '#text' && notWS.test(sibling.data))) {
- break;
- }
- if (!isInline(sibling)) {
- sibling = null;
- break;
- }
- }
- data = data.replace(/^[ \r\n]+/g, sibling ? ' ' : '');
- }
- if (endsWithWS) {
- walker.currentNode = child;
- while (sibling = walker.nextNode()) {
- if (nodeName === 'IMG' || (nodeName === '#text' && notWS.test(sibling.data))) {
- break;
- }
- if (!isInline(sibling)) {
- sibling = null;
- break;
- }
- }
- data = data.replace(/[ \r\n]+$/g, sibling ? ' ' : '');
- }
- if (data) {
- child.data = data;
- continue;
- }
- }
- child.remove();
- --i;
- --l;
-*/
- }
- }
- return node;
- },
-
- // ---
-
- removeEmptyInlines = node => {
- let children = node.childNodes,
- l = children.length,
- child;
- while (l--) {
- child = children[l];
- if (isElement(child) && !isLeaf(child)) {
- removeEmptyInlines(child);
- if (!child.firstChild && isInline(child)) {
- child.remove();
- }
- } else if (child.nodeType === TEXT_NODE && !child.data) {
- child.remove();
- }
- }
- },
-
- // ---
-
- notWSTextNode = node => isElement(node) ? node.nodeName === 'BR' : notWS.test(node.data),
- isLineBreak = (br, isLBIfEmptyBlock) => {
- let walker, block = br.parentNode;
- while (isInline(block)) {
- block = block.parentNode;
- }
- walker = createTreeWalker(block, SHOW_ELEMENT_OR_TEXT, notWSTextNode);
- walker.currentNode = br;
- return !!walker.nextNode() || (isLBIfEmptyBlock && !walker.previousNode());
- },
-
- //
elements are treated specially, and differently depending on the
- // browser, when in rich text editor mode. When adding HTML from external
- // sources, we must remove them, replacing the ones that actually affect
- // line breaks by wrapping the inline text in a . Browsers that want
- // elements at the end of each block will then have them added back in a later
- // fixCursor method call.
- cleanupBRs = (node, root, keepForBlankLine) => {
- let brs = node.querySelectorAll('BR');
- let l = brs.length;
- let br, parent;
- while (l--) {
- br = brs[l];
- // Cleanup may have removed it
- parent = br.parentNode;
- if (!parent) { continue; }
- // If it doesn't break a line, just remove it; it's not doing
- // anything useful. We'll add it back later if required by the
- // browser. If it breaks a line, wrap the content in div tags
- // and replace the brs.
- if (!isLineBreak(br, keepForBlankLine)) {
- detach(br);
- } else if (!isInline(parent)) {
- fixContainer(parent, root);
- }
- }
- },
-
- // The (non-standard but supported enough) innerText property is based on the
- // render tree in Firefox and possibly other browsers, so we must insert the
- // DOM node into the document to ensure the text part is correct.
- setClipboardData = (event, contents, root) => {
- let clipboardData = event.clipboardData;
- let body = doc.body;
- let node = createElement('div');
- let html, text;
-
- node.append(contents);
-
- html = node.innerHTML;
-
- // 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, so we need to remove them first.
- cleanupBRs(node, root, true);
- node.setAttribute('style',
- 'position:fixed;overflow:hidden;bottom:100%;right:100%;');
- body.append(node);
- text = (node.innerText || node.textContent).replace(NBSP, ' '); // Replace nbsp with regular space
- node.remove();
-
- if (text !== html) {
- clipboardData.setData('text/html', html);
- }
- clipboardData.setData('text/plain', text);
- event.preventDefault();
- },
mergeObjects = (base, extras, mayOverride) => {
base = base || {};
extras && Object.entries(extras).forEach(([prop,value])=>{
if (mayOverride || !(prop in base)) {
- base[ prop ] = (value?.constructor === Object) ?
- mergeObjects(base[ prop ], value, mayOverride) :
+ base[prop] = (value?.constructor === Object) ?
+ mergeObjects(base[prop], value, mayOverride) :
value;
}
});
@@ -1619,34 +2045,6 @@ const
pathChange: 1, select: 1, input: 1, undoStateChange: 1
},
- // --- Workaround for browsers that can't focus empty text nodes ---
-
- // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
-
- // Walk down the tree starting at the root and remove any ZWS. If the node only
- // contained ZWS space then remove it too. We may want to keep one ZWS node at
- // the bottom of the tree so the block can be selected. Define that node as the
- // keepNode.
- removeZWS = (root, keepNode) => {
- let walker = createTreeWalker(root, SHOW_TEXT);
- let parent, node, index;
- while (node = walker.nextNode()) {
- while ((index = node.data.indexOf(ZWS)) > -1 && (!keepNode || node.parentNode !== keepNode)) {
- if (node.length === 1) {
- do {
- parent = node.parentNode;
- node.remove();
- node = parent;
- walker.currentNode = parent;
- } while (isInline(node) && !getLength(node));
- break;
- } else {
- node.deleteData(index, 1);
- }
- }
- }
- },
-
// --- Bookmarking ---
startSelectionId = 'squire-selection-start',
@@ -1673,7 +2071,7 @@ const
},
splitBlock = (self, block, node, offset) => {
- let splitTag = tagAfterSplit[ block.nodeName ] || blockTag,
+ let splitTag = tagAfterSplit[block.nodeName] || blockTag,
nodeAfterSplit = split(node, offset, block.parentNode, self._root);
// Make sure the new node is the correct type.
@@ -1694,17 +2092,17 @@ const
let list = range.commonAncestorContainer;
let startLi = range.startContainer;
let endLi = range.endContainer;
- while (list && list !== root && !/^[OU]L$/.test(list.nodeName)) {
+ while (list && list !== root && !listNodeNames.has(list.nodeName)) {
list = list.parentNode;
}
if (!list || list === root) {
return null;
}
if (startLi === list) {
- startLi = startLi.childNodes[ range.startOffset ];
+ startLi = startLi.childNodes[range.startOffset];
}
if (endLi === list) {
- endLi = endLi.childNodes[ range.endOffset ];
+ endLi = endLi.childNodes[range.endOffset];
}
while (startLi && startLi.parentNode !== list) {
startLi = startLi.parentNode;
@@ -1712,7 +2110,7 @@ const
while (endLi && endLi.parentNode !== list) {
endLi = endLi.parentNode;
}
- return [ list, startLi, endLi ];
+ return [list, startLi, endLi];
},
makeList = (self, frag, type) => {
@@ -1748,9 +2146,9 @@ const
} else {
node = node.parentNode;
tag = node.nodeName;
- if (tag !== type && (/^[OU]L$/.test(tag))) {
+ if (tag !== type && listNodeNames.has(tag)) {
node.replaceWith(
- createElement(type, null, [ empty(node) ])
+ createElement(type, null, [empty(node)])
);
}
}
@@ -1763,7 +2161,7 @@ const
let walker = getBlockWalker(frag, self._root),
node;
- while(node = walker.nextNode()){
+ while (node = walker.nextNode()) {
if (node.nodeName === "LI") {
node.parentNode.setAttribute("dir", dir);
@@ -1799,464 +2197,11 @@ const
node.data = data = data.slice(endIndex);
}
}
- },
-
- escapeHTML = text => text.replace('&', '&')
- .replace('<', '<')
- .replace('>', '>')
- .replace('"', '"');
+ };
let contentWalker,
nodeCategoryCache = new WeakMap();
-function onKey(event) {
- if (event.defaultPrevented) {
- return;
- }
-
- let key = event.key.toLowerCase(),
- modifiers = '',
- range = this.getSelection(),
- root = this._root;
-
- // We need to apply the backspace/delete handlers regardless of
- // control key modifiers.
- if (key !== 'backspace' && key !== 'delete') {
- if (event.altKey) { modifiers += 'alt-'; }
- if (event[osKey]) { modifiers += ctrlKey; }
- if (event.shiftKey) { modifiers += 'shift-'; }
- }
-
- key = modifiers + key;
-
- if (this._keyHandlers[ key ]) {
- this._keyHandlers[ key ](this, event, range);
- // !event.isComposing stops us from blatting Kana-Kanji conversion in Safari
- } else if (!range.collapsed && !event.isComposing && !event[osKey] && key.length === 1) {
- // Record undo checkpoint.
- this.saveUndoState(range);
- // Delete the selection
- deleteContentsOfRange(range, root);
- this._ensureBottomLine();
- this.setSelection(range);
- this._updatePath(range, true);
- } else if (range.collapsed
- && range.startContainer === root
- && root.children.length > 0) {
-
- // Under certain conditions, cursor/range can be positioned directly
- // under this._root (not wrapped) and when this happens, an inline(TEXT)
- // element is attached directly to this._root. There might be other
- // issues, but squire.makeUnorderedList(), squire.makeOrderedList() and
- // maybe other functions that call squire.modifyBlocks() do NOT work
- // on inline(TEXT) nodes that are direct children of this._root.
- // Therefor, we try to detect this case here and wrap the cursor/range
- // before the text is inserted.
-
- const nextElement = root.children[range.startOffset];
- if (nextElement && !isBlock(nextElement)) {
- // create a new wrapper
- range = createRange(root.insertBefore(
- this.createDefaultBlock(), nextElement
- ), 0);
- if (nextElement.tagName === 'BR') {
- // delete it because a new
is created by createDefaultBlock()
- root.removeChild(nextElement);
- }
- const restore = this._restoreSelection;
- this.setSelection(range);
- this._restoreSelection = restore;
- }
- }
-}
-
-function onCut(event) {
- let range = this.getSelection();
- let root = this._root;
- let self = this;
- let startBlock, endBlock, copyRoot, contents, parent, newContents;
-
- // Nothing to do
- if (range.collapsed) {
- event.preventDefault();
- return;
- }
-
- // Save undo checkpoint
- this.saveUndoState(range);
-
- // Edge only seems to support setting plain text as of 2016-03-11.
- if (event.clipboardData) {
- // Clipboard content should include all parents within block, or all
- // 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.append(contents);
- contents = newContents;
- parent = parent.parentNode;
- }
- // Set clipboard data
- setClipboardData(event, contents, root);
- } else {
- setTimeout(() => {
- try {
- // If all content removed, ensure div at start of root.
- self._ensureBottomLine();
- } catch (error) {
- didError(error);
- }
- }, 0);
- }
-
- this.setSelection(range);
-}
-
-function onCopy(event) {
- // Edge only seems to support setting plain text as of 2016-03-11.
- if (event.clipboardData) {
- let range = this.getSelection(), root = this._root,
- // Clipboard content should include all parents within block, or all
- // parents up to root if selection across blocks
- startBlock = getStartBlockOfRange(range, root),
- endBlock = getEndBlockOfRange(range, root),
- copyRoot = ((startBlock === endBlock) && startBlock) || root,
- contents, parent, newContents;
- // Clone range to mutate, then move up as high as possible without
- // passing the copy root node.
- range = range.cloneRange();
- moveRangeBoundariesDownTree(range);
- moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);
- // Extract the contents
- contents = range.cloneContents();
- // 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.append(contents);
- contents = newContents;
- parent = parent.parentNode;
- }
- // Set clipboard data
- setClipboardData(event, contents, root);
- }
-}
-
-function onPaste(event) {
- let clipboardData = event.clipboardData;
- let items = clipboardData?.items;
- let imageItem = null;
- let plainItem = null;
- let htmlItem = null;
- let self = this;
- let type;
-
- // Current HTML5 Clipboard interface
- // ---------------------------------
- // https://html.spec.whatwg.org/multipage/interaction.html
- if (items) {
- [...items].forEach(item => {
- type = item.type;
- if (type === 'text/html') {
- htmlItem = item;
- // iOS copy URL gives you type text/uri-list which is just a list
- // of 1 or more URLs separated by new lines. Can just treat as
- // plain text.
- } else if (type === 'text/plain' || type === 'text/uri-list') {
- plainItem = item;
- } else if (item.kind === 'file' && /^image\/(png|jpeg|webp)/.test(type)) {
- imageItem = item;
- }
- });
- if (htmlItem || plainItem || imageItem) {
- event.preventDefault();
- if (imageItem) {
- let reader = new FileReader();
- reader.onload = event => {
- let img = createElement('img', {src: event.target.result}),
- canvas = createElement('canvas'),
- ctx = canvas.getContext('2d');
- img.onload = ()=>{
- ctx.drawImage(img, 0, 0);
- let width = img.width, height = img.height;
- if (width > height) {
- // Landscape
- if (width > 1024) {
- height = height * 1024 / width;
- width = 1024;
- }
- } else if (height > 1024) {
- // Portrait
- width = width * 1024 / height;
- height = 1024;
- }
- canvas.width = width;
- canvas.height = height;
- ctx.drawImage(img, 0, 0, width, height);
- self.insertHTML('
+')
', true);
- };
- }
- reader.readAsDataURL(imageItem.getAsFile());
- } else if (htmlItem && (!self.isShiftDown || !plainItem)) {
- htmlItem.getAsString(html => self.insertHTML(html, true));
- } else if (plainItem) {
- plainItem.getAsString(text => self.insertPlainText(text, true));
- }
- }
- }
-}
-
-// On Windows you can drag an drop text. We can't handle this ourselves, because
-// as far as I can see, there's no way to get the drop insertion point. So just
-// save an undo state and hope for the best.
-function onDrop(event) {
- let types = event.dataTransfer.types;
- if (types.includes('text/plain') || types.includes('text/html')) {
- this.saveUndoState();
- }
-}
-
-let keyHandlers = {
- // This song and dance is to force iOS to do enable the shift key
- // automatically on enter. When you do the DOM split manipulation yourself,
- // WebKit doesn't reset the IME state and so presents auto-complete options
- // as though you were continuing to type on the previous line, and doesn't
- // auto-enable the shift key. The old trick of blurring and focussing
- // again no longer works in iOS 13, and I tried various execCommand options
- // but they didn't seem to do anything. The only solution I've found is to
- // let iOS handle the enter key, then after it's done that reset the HTML
- // to what it was before and handle it properly in Squire; the IME state of
- // course doesn't reset so you end up in the correct state!
- enter: isIOS ? (self, event, range) => {
- self._saveRangeToBookmark(range);
- let html = self._getHTML();
- let restoreAndDoEnter = () => {
- self.removeEventListener('keyup', restoreAndDoEnter);
- self._setHTML(html);
- range = self._getRangeAndRemoveBookmark();
- // Ignore the shift key on iOS, as this is for auto-capitalisation.
- handleEnter(self, false, range);
- };
- self.addEventListener('keyup', restoreAndDoEnter);
- } : (self, event, range) => {
- event.preventDefault();
- handleEnter(self, event.shiftKey, range);
- },
-
- 'shift-enter': (self, event, range) => self._keyHandlers.enter(self, event, range),
-
- backspace: (self, event, range) => {
- let root = self._root;
- self._removeZWS();
- // Record undo checkpoint.
- self.saveUndoState(range);
- // If not collapsed, delete contents
- if (!range.collapsed) {
- event.preventDefault();
- deleteContentsOfRange(range, root);
- afterDelete(self, range);
- }
- // If at beginning of block, merge with previous
- else if (rangeDoesStartAtBlockBoundary(range, root)) {
- event.preventDefault();
- let current = getStartBlockOfRange(range, root);
- let previous;
- if (!current) {
- return;
- }
- // In case inline data has somehow got between blocks.
- fixContainer(current.parentNode, root);
- // Now get previous block
- previous = getPreviousBlock(current, root);
- // Must not be at the very beginning of the text area.
- if (previous) {
- // If not editable, just delete whole block.
- if (!previous.isContentEditable) {
- detachUneditableNode(previous, root);
- return;
- }
- // Otherwise merge.
- mergeWithBlock(previous, current, range, root);
- // If deleted line between containers, merge newly adjacent
- // containers.
- current = previous.parentNode;
- while (current !== root && !current.nextSibling) {
- current = current.parentNode;
- }
- if (current !== root && (current = current.nextSibling)) {
- mergeContainers(current, root);
- }
- self.setSelection(range);
- }
- // If at very beginning of text area, allow backspace
- // to break lists/blockquote.
- else if (current) {
- let parent = getClosest(current, root, 'UL,OL,BLOCKQUOTE');
- if (parent) {
- return ('BLOCKQUOTE' === parent.nodeName)
- // Break blockquote
- ? self.decreaseQuoteLevel(range)
- // Break list
- : self.decreaseListLevel(range);
- }
- self.setSelection(range);
- self._updatePath(range, true);
- }
- }
- // Otherwise, leave to browser but check afterwards whether it has
- // left behind an empty inline tag.
- else {
- self.setSelection(range);
- setTimeout(() => afterDelete(self), 0);
- }
- },
- 'delete': (self, event, range) => {
- let root = self._root;
- let current, next, originalRange,
- cursorContainer, cursorOffset, nodeAfterCursor;
- self._removeZWS();
- // Record undo checkpoint.
- self.saveUndoState(range);
- // If not collapsed, delete contents
- if (!range.collapsed) {
- event.preventDefault();
- deleteContentsOfRange(range, root);
- afterDelete(self, range);
- }
- // If at end of block, merge next into this block
- else if (rangeDoesEndAtBlockBoundary(range, root)) {
- event.preventDefault();
- if (current = getStartBlockOfRange(range, root)) {
- // In case inline data has somehow got between blocks.
- fixContainer(current.parentNode, root);
- // Now get next block
- // Must not be at the very end of the text area.
- if (next = getNextBlock(current, root)) {
- // If not editable, just delete whole block.
- if (!next.isContentEditable) {
- detachUneditableNode(next, root);
- return;
- }
- // 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
- // left behind an empty inline tag.
- else {
- // But first check if the cursor is just before an IMG tag. If so,
- // delete it ourselves, because the browser won't if it is not
- // inline.
- originalRange = range.cloneRange();
- moveRangeBoundariesUpTree(range, root, root, root);
- cursorContainer = range.endContainer;
- cursorOffset = range.endOffset;
- if (isElement(cursorContainer)) {
- nodeAfterCursor = cursorContainer.childNodes[ cursorOffset ];
- if (nodeAfterCursor?.nodeName === 'IMG') {
- event.preventDefault();
- detach(nodeAfterCursor);
- moveRangeBoundariesDownTree(range);
- afterDelete(self, range);
- return;
- }
- }
- self.setSelection(originalRange);
- setTimeout(() => afterDelete(self), 0);
- }
- },
- tab: (self, event, range) => {
- let root = self._root;
- let node, parent;
- self._removeZWS();
- // If no selection and at start of block
- if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) {
- node = getStartBlockOfRange(range, root);
- // Iterate through the block's parents
- while ((parent = node.parentNode)) {
- // If we find a UL or OL (so are in a list, node must be an LI)
- if (parent.nodeName === 'UL' || parent.nodeName === 'OL') {
- // Then increase the list level
- event.preventDefault();
- self.increaseListLevel(range);
- break;
- }
- node = parent;
- }
- }
- },
- 'shift-tab': (self, event, range) => {
- let root = self._root;
- let node;
- self._removeZWS();
- // If no selection and at start of block
- if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) {
- // Break list
- node = range.startContainer;
- if (getClosest(node, root, 'UL,OL')) {
- event.preventDefault();
- self.decreaseListLevel(range);
- }
- }
- },
- space: (self, _, range) => {
- let root = self._root;
- self._recordUndoState(range, false);
- self._config.addLinks && addLinks(range.startContainer, root, self);
- self._getRangeAndRemoveBookmark(range);
-/*
- // If the cursor is at the end of a link (
foo|) then move it
- // outside of the link (
foo|) so that the space is not part of
- // the link text.
- // SnappyMail: disabled as it fails in Firefox
- let node = range.endContainer;
- if (range.collapsed && range.endOffset === getLength(node)) {
- do {
- if (node.nodeName === 'A') {
- range.setStartAfter(node);
- break;
- }
- } while (!node.nextSibling && (node = node.parentNode) && node !== root);
- }
-*/
- // Delete the selection if not collapsed
- if (!range.collapsed) {
- deleteContentsOfRange(range, root);
- self._ensureBottomLine();
- self.setSelection(range);
- self._updatePath(range, true);
- }
-
- self.setSelection(range);
- },
- arrowleft: self => self._removeZWS(),
- arrowright: self => self._removeZWS()
-};
-
// System standard for page up/down on Mac is to just scroll, not move the
// cursor. On Linux/Windows, it should move the cursor, but some browsers don't
// implement this natively. Override to support it.
@@ -2272,22 +2217,22 @@ if (!isMac) {
keyHandlers.pagedown = self => _moveCursorTo(self, false);
}
-keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat('B');
-keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat('I');
-keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat('U');
-keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat('S');
-keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat('SUB', 'SUP');
-keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat('SUP', 'SUB');
-keyHandlers[ ctrlKey + 'shift-8' ] = toggleList('UL', 'makeUnorderedList');
-keyHandlers[ ctrlKey + 'shift-9' ] = toggleList('OL', 'makeOrderedList');
-keyHandlers[ ctrlKey + '[' ] = changeIndentationLevel('decrease');
-keyHandlers[ ctrlKey + ']' ] = changeIndentationLevel('increase');
-keyHandlers[ ctrlKey + 'd' ] = mapKeyTo('toggleCode');
-keyHandlers[ ctrlKey + 'y' ] = mapKeyTo('redo');
-keyHandlers[ 'redo' ] = mapKeyTo('redo');
-keyHandlers[ ctrlKey + 'z' ] = mapKeyTo('undo');
-keyHandlers[ 'undo' ] = mapKeyTo('undo');
-keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo('redo');
+keyHandlers[ctrlKey + 'b'] = mapKeyToFormat('B');
+keyHandlers[ctrlKey + 'i'] = mapKeyToFormat('I');
+keyHandlers[ctrlKey + 'u'] = mapKeyToFormat('U');
+keyHandlers[ctrlKey + 'shift-7'] = mapKeyToFormat('S');
+keyHandlers[ctrlKey + 'shift-5'] = mapKeyToFormat('SUB', 'SUP');
+keyHandlers[ctrlKey + 'shift-6'] = mapKeyToFormat('SUP', 'SUB');
+keyHandlers[ctrlKey + 'shift-8'] = toggleList('UL', 'makeUnorderedList');
+keyHandlers[ctrlKey + 'shift-9'] = toggleList('OL', 'makeOrderedList');
+keyHandlers[ctrlKey + '['] = changeIndentationLevel('decrease');
+keyHandlers[ctrlKey + ']'] = changeIndentationLevel('increase');
+keyHandlers[ctrlKey + 'd'] = mapKeyTo('toggleCode');
+keyHandlers[ctrlKey + 'y'] = mapKeyTo('redo');
+keyHandlers['redo'] = mapKeyTo('redo');
+keyHandlers[ctrlKey + 'z'] = mapKeyTo('undo');
+keyHandlers['undo'] = mapKeyTo('undo');
+keyHandlers[ctrlKey + 'shift-z'] = mapKeyTo('redo');
class EditStack extends Array
{
@@ -2357,7 +2302,7 @@ class EditStack extends Array
}
// Save data
- this[ undoIndex ] = html;
+ this[undoIndex] = html;
this.index = undoIndex;
this.inUndoState = true;
}
@@ -2380,7 +2325,7 @@ class EditStack extends Array
// Make sure any changes since last checkpoint are saved.
this.recordUndoState(squire.getSelection(), false);
this.index = undoIndex;
- squire._setHTML(this[ undoIndex ]);
+ squire._setHTML(this[undoIndex]);
let range = squire._getRangeAndRemoveBookmark();
if (range) {
squire.setSelection(range);
@@ -2396,7 +2341,7 @@ class EditStack extends Array
undoIndex = this.index + 1;
if (undoIndex < this.length && this.inUndoState) {
this.index = undoIndex;
- squire._setHTML(this[ undoIndex ]);
+ squire._setHTML(this[undoIndex]);
let range = squire._getRangeAndRemoveBookmark();
if (range) {
squire.setSelection(range);
@@ -2504,7 +2449,7 @@ class Squire
// --- Events ---
fireEvent(type, event) {
- let handlers = this._events[ type ];
+ let handlers = this._events[type];
let isFocused, l, obj;
// UI code, especially modal views, may be monitoring for focus events and
// immediately removing focus. In certain conditions, this can cause the
@@ -2558,10 +2503,10 @@ class Squire
});
return this;
}
- let handlers = this._events[ type ];
+ let handlers = this._events[type];
if (!handlers) {
- handlers = this._events[ type ] = [];
- customEvents[ type ]
+ handlers = this._events[type] = [];
+ customEvents[type]
|| (type === 'selectionchange' ? doc : this._root)
.addEventListener(type, this, {capture:true,passive:'touchstart'===type});
}
@@ -2571,7 +2516,7 @@ class Squire
}
removeEventListener(type, fn) {
- let handlers = this._events[ type ];
+ let handlers = this._events[type];
let l;
if (handlers) {
if (fn) {
@@ -2585,8 +2530,8 @@ class Squire
handlers.length = 0;
}
if (!handlers.length) {
- delete this._events[ type ];
- customEvents[ type ]
+ delete this._events[type];
+ customEvents[type]
|| (type === 'selectionchange' ? doc : this._root)
.removeEventListener(type, this, true);
}
@@ -2770,13 +2715,13 @@ class Squire
// text node to current selection point
if (range.collapsed) {
startContainer = range.startContainer;
- if (startContainer.nodeType === TEXT_NODE) {
- endContainer = startContainer.childNodes[ range.startOffset ];
- if (!endContainer || endContainer.nodeType !== TEXT_NODE) {
+ if (isTextNode(startContainer)) {
+ endContainer = startContainer.childNodes[range.startOffset];
+ if (!endContainer || !isTextNode(endContainer)) {
endContainer =
- startContainer.childNodes[ range.startOffset - 1 ];
+ startContainer.childNodes[range.startOffset - 1];
}
- if (endContainer?.nodeType === TEXT_NODE) {
+ if (isTextNode(endContainer)) {
range.setStart(endContainer, 0);
range.collapse(true);
}
@@ -2830,13 +2775,13 @@ class Squire
// Sanitize range to prevent weird IE artifacts
if (!range.collapsed &&
- range.startContainer.nodeType === TEXT_NODE &&
+ isTextNode(range.startContainer) &&
range.startOffset === range.startContainer.length &&
range.startContainer.nextSibling) {
range.setStartBefore(range.startContainer.nextSibling);
}
if (!range.collapsed &&
- range.endContainer.nodeType === TEXT_NODE &&
+ isTextNode(range.endContainer) &&
range.endOffset === 0 &&
range.endContainer.previousSibling) {
range.setEndAfter(range.endContainer.previousSibling);
@@ -2853,7 +2798,7 @@ class Squire
// If common ancestor is a text node and doesn't have the format, we
// definitely don't have it.
- if (common.nodeType === TEXT_NODE) {
+ if (isTextNode(common)) {
return false;
}
@@ -2889,8 +2834,8 @@ class Squire
}
element = range.commonAncestorContainer;
- if (range.collapsed || element.nodeType === TEXT_NODE) {
- if (element.nodeType === TEXT_NODE) {
+ if (range.collapsed || isTextNode(element)) {
+ if (isTextNode(element)) {
element = element.parentNode;
}
while (seenAttributes < 4 && element) {
@@ -2957,7 +2902,7 @@ class Squire
walker = createTreeWalker(
range.commonAncestorContainer,
SHOW_ELEMENT_OR_TEXT,
- node => (node.nodeType === TEXT_NODE ||
+ node => (isTextNode(node) ||
node.nodeName === 'BR' ||
node.nodeName === 'IMG'
) && isNodeContainedInRange(range, node)
@@ -3003,8 +2948,8 @@ class Squire
} while (walker.nextNode());
// If we don't finish inside a text node, offset may have changed.
- if (endContainer.nodeType !== TEXT_NODE) {
- if (node.nodeType === TEXT_NODE) {
+ if (!isTextNode(endContainer)) {
+ if (isTextNode(node)) {
endContainer = node;
endOffset = node.length;
} else {
@@ -3055,7 +3000,7 @@ class Squire
return;
}
- let isText = (node.nodeType === TEXT_NODE),
+ let isText = isTextNode(node),
child, next;
// If not at least partially contained, wrap entire contents
@@ -3063,7 +3008,7 @@ class Squire
if (!isNodeContainedInRange(range, node)) {
// Ignore bookmarks and empty text nodes
if (node.nodeName !== 'INPUT' && (!isText || node.data)) {
- toWrap.push([ exemplar, node ]);
+ toWrap.push([exemplar, node]);
}
return;
}
@@ -3071,11 +3016,11 @@ class Squire
// Split any partially selected text nodes.
if (isText) {
if (node === endContainer && endOffset !== node.length) {
- toWrap.push([ exemplar, node.splitText(endOffset) ]);
+ toWrap.push([exemplar, node.splitText(endOffset)]);
}
if (node === startContainer && startOffset) {
node.splitText(startOffset);
- toWrap.push([ exemplar, node ]);
+ toWrap.push([exemplar, node]);
}
}
// If not a text node, recurse onto all children.
@@ -3187,9 +3132,9 @@ class Squire
// 5. Merge containers at edges
if (range.endOffset < range.endContainer.childNodes.length) {
- mergeContainers(range.endContainer.childNodes[ range.endOffset ], root);
+ mergeContainers(range.endContainer.childNodes[range.endOffset], root);
}
- mergeContainers(range.startContainer.childNodes[ range.startOffset ], root);
+ mergeContainers(range.startContainer.childNodes[range.startOffset], root);
// 6. Restore selection
this._getRangeAndRemoveBookmark(range);
@@ -3273,7 +3218,7 @@ class Squire
next = startLi === endLi ? null : startLi.nextSibling;
startLi.remove();
if (makeNotList && startLi.nodeName === 'LI') {
- startLi = this.createDefaultBlock([ empty(startLi) ]);
+ startLi = this.createDefaultBlock([empty(startLi)]);
}
newParent.insertBefore(startLi, insertBefore);
} while ((startLi = next));
@@ -3483,9 +3428,9 @@ class Squire
let node = range.startContainer;
let offset = range.startOffset;
let text;
- if (node?.nodeType !== TEXT_NODE) {
+ if (!isTextNode(node)) {
text = doc.createTextNode('');
- node?.childNodes[ offset ].before(text);
+ node?.childNodes[offset].before(text);
node = text;
offset = 0;
}
@@ -3553,7 +3498,7 @@ class Squire
let start = range?.startContainer || {};
let end = range ? range.endContainer : 0;
// When the selection is all the text inside an element, set style on the element itself
- if ('dir' == name || (TEXT_NODE === start?.nodeType && 0 === range.startOffset && start === end && end.length === range.endOffset)) {
+ if ('dir' == name || (isTextNode(start) && 0 === range.startOffset && start === end && end.length === range.endOffset)) {
this.saveUndoState(range);
setAttributes(start.parentNode, {[name]: value});
this.setSelection(range);
@@ -3665,13 +3610,13 @@ class Squire
let parent = this.getSelectionClosest('UL,OL,BLOCKQUOTE');
if (parent || 'increase' === direction) {
let method = (!parent || 'BLOCKQUOTE' === parent.nodeName) ? 'Quote' : 'List';
- this[ direction + method + 'Level' ]();
+ this[direction + method + 'Level']();
}
}
increaseQuoteLevel(range) {
this.modifyBlocks(
- frag => createElement('BLOCKQUOTE', null, [ frag ]),
+ frag => createElement('BLOCKQUOTE', null, [frag]),
range
);
return this.focus();
@@ -3719,7 +3664,7 @@ class Squire
frag.querySelectorAll('LI').forEach(item => {
if (isBlock(item)) {
item.replaceWith(
- this.createDefaultBlock([ empty(item) ])
+ this.createDefaultBlock([empty(item)])
);
} else {
fixContainer(item, root);