diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js index 374ea795f..563f8562f 100644 --- a/dev/External/SquireUI.js +++ b/dev/External/SquireUI.js @@ -133,11 +133,11 @@ class SquireUI dir: { dir_ltr: { html: '⁋', - cmd: () => squire.bidi('ltr') + cmd: () => squire.setTextDirection('ltr') }, dir_rtl: { html: '¶', - cmd: () => squire.bidi('rtl') + cmd: () => squire.setTextDirection('rtl') } }, colors: { @@ -237,7 +237,7 @@ class SquireUI cmd: () => { let node = squire.getSelectionClosest('IMG'), src = prompt("Image", node?.src || "https://"); - src?.length ? squire.insertImage(src) : (node && squire.detach(node)); + src?.length ? squire.insertImage(src) : node?.remove(); }, matches: 'IMG' }, @@ -405,9 +405,9 @@ class SquireUI let changes = actions.changes; changes.undo.input.disabled = changes.redo.input.disabled = true; - squire.addEventListener('undoStateChange', state => { - changes.undo.input.disabled = !state.canUndo; - changes.redo.input.disabled = !state.canRedo; + squire.addEventListener('undoStateChange', e => { + changes.undo.input.disabled = !e.detail.canUndo; + changes.redo.input.disabled = !e.detail.canRedo; }); actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex; @@ -480,21 +480,21 @@ class SquireUI squire.addEventListener('pathChange', e => { const squireRoot = squire.getRoot(); + let elm = e.detail.element; forEachObjectValue(actions, entries => { forEachObjectValue(entries, cfg => { -// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches)); - cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot)); +// cfg.matches && cfg.input.classList.toggle('active', elm && elm.matches(cfg.matches)); + cfg.matches && cfg.input.classList.toggle('active', elm && elm.closestWithin(cfg.matches, squireRoot)); }); }); - if (e.element) { + if (elm) { // try to find font-family and/or font-size and set "select" elements' values let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex; let familySelectedIndex = defaultFontFamilyIndex; - let elm = e.element; let familyFound = false; let sizeFound = false; do { @@ -526,10 +526,10 @@ class SquireUI }); /* squire.addEventListener('cursor', e => { - console.dir({cursor:e.range}); + console.dir({cursor:e.detail.range}); }); squire.addEventListener('select', e => { - console.dir({select:e.range}); + console.dir({select:e.detail.range}); }); */ } diff --git a/vendors/squire/build/squire-raw.js b/vendors/squire/build/squire-raw.js index 4a44cd92c..cecf71428 100644 --- a/vendors/squire/build/squire-raw.js +++ b/vendors/squire/build/squire-raw.js @@ -2,3722 +2,3067 @@ /* eslint max-len: 0 */ /** - TODO: modifyBlocks function doesn't work very good. - For example you have: UL > LI > [cursor here in text] - Then create blockquote at cursor, the result is: BLOCKQUOTE > UL > LI - not UL > LI > BLOCKQUOTE + TODO: modifyBlocks function doesn't work very good. + For example you have: UL > LI > [cursor here in text] + Then create blockquote at cursor, the result is: BLOCKQUOTE > UL > LI + not UL > LI > BLOCKQUOTE */ -(doc => { +(() => { + // source/node/TreeIterator.ts + var SHOW_ELEMENT = 1; + var SHOW_TEXT = 4; + var SHOW_ELEMENT_OR_TEXT = 5; + var filterAccept = NodeFilter.FILTER_ACCEPT; + TreeWalker.prototype.previousPONode = function() { + let current = this.currentNode; + let node = current.lastChild; + while (!node && current) { + if (current === this.root) { + break; + } + node = this.previousSibling(); + if (!node) { + current = this.parentNode(); + } + } + node && (this.currentNode = node); + return node; + }; + var createTreeWalker = (root, whatToShow, filter) => document.createTreeWalker(root, whatToShow, filter ? { + acceptNode: (node) => filter(node) ? filterAccept : NodeFilter.FILTER_SKIP + } : null); -const - SHOW_ELEMENT = 1, // NodeFilter.SHOW_ELEMENT, - SHOW_TEXT = 4, // NodeFilter.SHOW_TEXT, - SHOW_ELEMENT_OR_TEXT = 5, + // source/Constants.ts + var ELEMENT_NODE = 1; + var TEXT_NODE = 3; + var DOCUMENT_FRAGMENT_NODE = 11; + var ZWS = "\u200B"; + var ua = navigator.userAgent; + var isMac = /Mac OS X/.test(ua); + var isIOS = /iP(?:ad|hone)/.test(ua) || (isMac && !!navigator.maxTouchPoints); + var isAndroid = /Android/.test(ua); + var isWebKit = /WebKit\//.test(ua); + var ctrlKey = isMac || isIOS ? "Meta-" : "Ctrl-"; + var cantFocusEmptyTextNodes = isWebKit; + var notWS = /[^ \t\r\n]/; + var indexOf = (array, value) => Array.prototype.indexOf.call(array, value); - // source/Constants.ts - ELEMENT_NODE = 1, // Node.ELEMENT_NODE, - TEXT_NODE = 3, // Node.TEXT_NODE, - DOCUMENT_FRAGMENT_NODE = 11, // Node.DOCUMENT_FRAGMENT_NODE, - ZWS = '\u200B', - ua = navigator.userAgent, - isMac = /Mac OS X/.test(ua), - isIOS = /iP(?:ad|hone)/.test(ua) || (isMac && !!navigator.maxTouchPoints), - isAndroid = /Android/.test(ua), - isWebKit = /WebKit\//.test(ua), - ctrlKey = isMac || isIOS ? 'meta-' : 'ctrl-', - cantFocusEmptyTextNodes = isWebKit, - // Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space - notWS = /[^ \t\r\n]/, + // source/node/Category.ts + var inlineNodeNames = /^(?:#text|A|ABBR|ACRONYM|B|BR|BD[IO]|CITE|CODE|DATA|DEL|DFN|EM|FONT|HR|I|IMG|INPUT|INS|KBD|Q|RP|RT|RUBY|S|SAMP|SMALL|SPAN|STR(IKE|ONG)|SU[BP]|TIME|U|VAR|WBR)$/; + var leafNodeNames = new Set(["BR", "HR", "IMG"]); + var listNodeNames = new Set(["OL", "UL"]); + var UNKNOWN = 0; + var INLINE = 1; + var BLOCK = 2; + var CONTAINER = 3; + var cache = new WeakMap(); + var isLeaf = (node) => isElement(node) && leafNodeNames.has(node.nodeName); + var getNodeCategory = (node) => { + switch (node.nodeType) { + case TEXT_NODE: + return INLINE; + case ELEMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: + if (cache.has(node)) { + return cache.get(node); + } + break; + default: + return UNKNOWN; + } + let nodeCategory = Array.prototype.every.call(node.childNodes, isInline) ? (inlineNodeNames.test(node.nodeName) ? INLINE : BLOCK) : CONTAINER; + cache.set(node, nodeCategory); + return nodeCategory; + }; + var isInline = (node) => getNodeCategory(node) === INLINE; + var isBlock = (node) => getNodeCategory(node) === BLOCK; + var isContainer = (node) => getNodeCategory(node) === CONTAINER; - // source/node/Category.ts -// phrasingElements = 'ABBR,AUDIO,B,BDO,BR,BUTTON,CANVAS,CITE,CODE,COMMAND,DATA,DATALIST,DFN,EM,EMBED,I,IFRAME,IMG,INPUT,KBD,KEYGEN,LABEL,MARK,MATH,METER,NOSCRIPT,OBJECT,OUTPUT,PROGRESS,Q,RUBY,SAMP,SCRIPT,SELECT,SMALL,SPAN,STRONG,SUB,SUP,SVG,TEXTAREA,TIME,VAR,VIDEO,WBR', - inlineNodeNames = /^(?:#text|A|ABBR|ACRONYM|B|BR|BD[IO]|CITE|CODE|DATA|DEL|DFN|EM|FONT|HR|I|IMG|INPUT|INS|KBD|Q|RP|RT|RUBY|S|SAMP|SMALL|SPAN|STR(IKE|ONG)|SU[BP]|TIME|U|VAR|WBR)$/, - leafNodeNames = new Set(["BR", "HR", "IMG"]), - listNodeNames = new Set(["OL", "UL"]), - UNKNOWN = 0, - INLINE = 1, - BLOCK = 2, - CONTAINER = 3, - isLeaf = node => isElement(node) && leafNodeNames.has(node.nodeName), - getNodeCategory = node => { - switch (node.nodeType) { - case TEXT_NODE: - return INLINE; - case ELEMENT_NODE: - case DOCUMENT_FRAGMENT_NODE: - if (nodeCategoryCache.has(node)) { - return nodeCategoryCache.get(node); - } - break; - default: - return UNKNOWN; - } + // source/node/Node.ts + var createElement = (tag, props, children) => { + const el = document.createElement(tag); + if (props instanceof Array) { + children = props; + props = null; + } + setAttributes(el, props); + children && el.append(...children); + return el; + }; + var areAlike = (node, node2) => !isLeaf(node) && (node.nodeType === node2.nodeType && node.nodeName === node2.nodeName && node.nodeName !== "A" && node.className === node2.className && node.style?.cssText === node2.style?.cssText); + var hasTagAttributes = (node, tag, attributes) => node.nodeName === tag && Object.entries(attributes || {}).every(([k,v]) => node.getAttribute(k) === v); + var getNearest = (node, root, tag, attributes) => { + while (node && node !== root) { + if (hasTagAttributes(node, tag, attributes)) { + return node; + } + node = node.parentNode; + } + return null; + }; + var getNodeBeforeOffset = (node, offset) => { + let children = node.childNodes; + while (offset && isElement(node)) { + node = children[offset - 1]; + children = node.childNodes; + offset = children.length; + } + return node; + }; + var getNodeAfterOffset = (node, offset) => { + if (isElement(node)) { + const children = node.childNodes; + if (offset < children.length) { + node = children[offset]; + } else { + while (node && !node.nextSibling) { + node = node.parentNode; + } + node && (node = node.nextSibling); + } + } + return node; + }; + var getLength = (node) => isElement(node) || node instanceof DocumentFragment ? node.childNodes.length : node.length || 0; + var empty = (node) => { + const frag = document.createDocumentFragment(), childNodes = node.childNodes; + childNodes && frag.append(...childNodes); + return frag; + }; + var detach = (node) => node.parentNode?.removeChild(node); + var replaceWith = (node, node2) => node.parentNode?.replaceChild(node2, node); + var getClosest = (node, root, selector) => { + node = (node && !node.closest) ? node.parentElement : node; + node = node?.closest(selector); + return (node && root.contains(node)) ? node : null; + }; + var isElement = (node) => node instanceof Element; + var isTextNode = (node) => node instanceof Text; + var isBrElement = (node) => "BR" === node?.nodeName; + var setAttributes = (node, props) => { + props && Object.entries(props).forEach(([k, v]) => { + if (null == v) { + node.removeAttribute(k); + } else if ("style" === k && typeof v === "object") { + Object.entries(v).forEach(([k2, v2]) => node.style[k2] = v2); + } else { + node.setAttribute(k, v); + } + }); + }; - let nodeCategory = - 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 - // these as containers rather than inline. See #239. - : CONTAINER; - nodeCategoryCache.set(node, nodeCategory); - return nodeCategory; - }, - isInline = node => getNodeCategory(node) === INLINE, - isBlock = node => getNodeCategory(node) === BLOCK, - isContainer = node => getNodeCategory(node) === CONTAINER, + // source/node/Whitespace.ts + var notWSTextNode = (node) => isElement(node) ? isBrElement(node) : notWS.test(node.data); + var isLineBreak = (br, isLBIfEmptyBlock) => { + let block = br.parentNode; + while (isInline(block)) { + block = block.parentNode; + } + const walker = createTreeWalker( + block, + SHOW_ELEMENT_OR_TEXT, + notWSTextNode + ); + walker.currentNode = br; + return !!walker.nextNode() || (isLBIfEmptyBlock && !walker.previousNode()); + }; + var removeZWS = (root, keepNode) => { + const walker = createTreeWalker(root, SHOW_TEXT); + let textNode; + let index; + while (textNode = walker.nextNode()) { + while ((index = textNode.data.indexOf(ZWS)) > -1 && (!keepNode || textNode.parentNode !== keepNode)) { + if (textNode.length === 1) { + do { + let parent = textNode.parentNode; + textNode.remove(); + textNode = parent; + walker.currentNode = parent; + } while (isInline(textNode) && !getLength(textNode)); + break; + } else { + textNode.deleteData(index, 1); + } + } + } + }; - // source/node/Node.ts - createElement = (tag, props, children) => { - const el = doc.createElement(tag); - if (props instanceof Array) { - children = props; - props = null; - } - setAttributes(el, props); - children && el.append(...children); - return el; - }, - areAlike = (node, node2) => { - return !isLeaf(node) && ( - node.nodeType === node2.nodeType && - node.nodeName === node2.nodeName && - node.nodeName !== "A" && - node.className === node2.className && - node.style?.cssText === node2.style?.cssText - ); - }, - hasTagAttributes = (node, tag, attributes) => { - return node.nodeName === tag && Object.entries(attributes || {}).every(([k,v]) => node.getAttribute(k) === v); - }, - getNearest = (node, root, tag, attributes) => { - while (node && node !== root) { - if (hasTagAttributes(node, tag, attributes)) { - return node; - } - node = node.parentNode; - } - return null; - }, - getNodeBefore = (node, offset) => { - let children = node.childNodes; - while (offset && isElement(node)) { - node = children[offset - 1]; - children = node.childNodes; - offset = children.length; - } - return node; - }, - getNodeAfterOffset = (node, offset) => { - if (isElement(node)) { - const children = node.childNodes; - if (offset < children.length) { - node = children[offset]; - } else { - while (node && !node.nextSibling) { - node = node.parentNode; - } - node && (node = node.nextSibling); - } - } - return node; - }, - getLength = node => - isElement(node) || node instanceof DocumentFragment ? node.childNodes.length : node.length || 0, - empty = node => { - const frag = doc.createDocumentFragment(), - childNodes = node.childNodes; - childNodes && frag.append(...childNodes); - return frag; - }, - detach = node => { -// node.remove(); - node.parentNode?.removeChild(node); - }, - getClosest = (node, root, selector) => { - node = (node && !node.closest) ? node.parentElement : node; - node = node?.closest(selector); - return (node && root.contains(node)) ? node : null; - }, + // source/range/Boundaries.ts + var START_TO_START = 0; + var START_TO_END = 1; + var END_TO_END = 2; + var END_TO_START = 3; + var isNodeContainedInRange = (range, node, partial = true) => { + const nodeRange = document.createRange(); + nodeRange.selectNode(node); + return partial + ? range.compareBoundaryPoints(END_TO_START, nodeRange) < 0 + && range.compareBoundaryPoints(START_TO_END, nodeRange) > 0 + : range.compareBoundaryPoints(START_TO_START, nodeRange) < 1 + && range.compareBoundaryPoints(END_TO_END, nodeRange) > -1; + }; + var moveRangeBoundariesDownTree = range => { + let { startContainer, startOffset, endContainer, endOffset } = range; + while (!isTextNode(startContainer)) { + let child = startContainer.childNodes[startOffset]; + if (!child || isLeaf(child)) { + if (startOffset) { + child = startContainer.childNodes[startOffset - 1]; + if (isTextNode(child)) { + let prev; + while (!child.length && (prev = child.previousSibling) && isTextNode(prev)) { + child.remove(); + child = prev; + } + startContainer = child; + startOffset = child.data.length; + } + } + break; + } + startContainer = child; + startOffset = 0; + } + if (endOffset) { + while (!isTextNode(endContainer)) { + const child = endContainer.childNodes[endOffset - 1]; + if (!child || isLeaf(child)) { + if (isBrElement(child) && !isLineBreak(child)) { + --endOffset; + continue; + } + break; + } + endContainer = child; + endOffset = getLength(endContainer); + } + } else { + while (!isTextNode(endContainer)) { + const child = endContainer.firstChild; + if (!child || isLeaf(child)) { + break; + } + endContainer = child; + } + } + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + }; + var moveRangeBoundariesUpTree = (range, startMax, endMax, root) => { + let startContainer = range.startContainer; + let startOffset = range.startOffset; + let endContainer = range.endContainer; + let endOffset = range.endOffset; + let parent; + if (!startMax) { + startMax = range.commonAncestorContainer; + } + if (!endMax) { + endMax = startMax; + } + while (!startOffset && startContainer !== startMax && startContainer !== root) { + parent = startContainer.parentNode; + startOffset = indexOf(parent.childNodes, startContainer); + startContainer = parent; + } + while (endContainer !== endMax && endContainer !== root) { + if (!isTextNode(endContainer) && isBrElement(endContainer.childNodes[endOffset]) && !isLineBreak(endContainer.childNodes[endOffset])) { + ++endOffset; + } + if (endOffset !== getLength(endContainer)) { + break; + } + parent = endContainer.parentNode; + endOffset = indexOf(parent.childNodes, endContainer) + 1; + endContainer = parent; + } + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + }; + var moveRangeBoundaryOutOf = (range, tag, root) => { + let parent = getClosest(range.endContainer, root, tag); + if (parent && (parent = parent.parentNode)) { + const clone = range.cloneRange(); + moveRangeBoundariesUpTree(clone, parent, parent, root); + if (clone.endContainer === parent) { + range.setStart(clone.endContainer, clone.endOffset); + range.setEnd(clone.endContainer, clone.endOffset); + } + } + return range; + }; - // source/node/Whitespace.ts - notWSTextNode = node => isElement(node) ? isBrElement(node) : 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()); - }, - // --- 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) => { - const 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); - } - } - } - }, + // source/node/MergeSplit.ts + var fixCursor = (node) => { + let fixer = null; + if (!isTextNode(node)) { + if (isInline(node)) { + let child = node.firstChild; + if (cantFocusEmptyTextNodes) { + while (isTextNode(child) && !child.data) { + node.removeChild(child); + child = node.firstChild; + } + } + if (!child) { + fixer = document.createTextNode(cantFocusEmptyTextNodes ? ZWS : ""); + } + } else if (isElement(node) && !node.querySelector("BR")) { + fixer = createElement("BR"); + } + if (fixer) { + try { + node.appendChild(fixer); + } catch (error) { + didError({ + name: 'Squire: fixCursor – ' + error, + message: 'Parent: ' + node.nodeName + '/' + node.innerHTML + + ' appendChild: ' + fixer.nodeName + }); + } + } + } + return node; + }; + var fixContainer = (container, root) => { + let wrapper; + [...container.childNodes].forEach((child) => { + const isBR = isBrElement(child); + if (!isBR && child.parentNode == root && isInline(child)) { + wrapper = wrapper || createElement("DIV"); + wrapper.append(child); + } else if (isBR || wrapper) { + wrapper = wrapper || createElement("DIV"); + fixCursor(wrapper); + child[isBR ? "replaceWith" : "before"](wrapper); + wrapper = null; + } + isContainer(child) && fixContainer(child, root); + }); + wrapper && container.append(fixCursor(wrapper)); + return container; + }; + var split = (node, offset, stopNode, root) => { + if (isTextNode(node) && node !== stopNode) { + if (typeof offset !== "number") { + throw new Error("Offset must be a number to split text node!"); + } + if (!node.parentNode) { + throw new Error("Cannot split text node with no parent!"); + } + return split(node.parentNode, node.splitText(offset), stopNode, root); + } + let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset; + const parent = node.parentNode; + if (!parent || node === stopNode || !isElement(node)) { + return nodeAfterSplit; + } + const clone = node.cloneNode(false); + while (nodeAfterSplit) { + const next = nodeAfterSplit.nextSibling; + clone.append(nodeAfterSplit); + nodeAfterSplit = next; + } + if (node instanceof HTMLOListElement && getClosest(node, root, "BLOCKQUOTE")) { + clone.start = (+node.start || 1) + node.childNodes.length - 1; + } + fixCursor(node); + fixCursor(clone); + node.after(clone); + return split(parent, clone, stopNode, root); + }; + var _mergeInlines = (node, fakeRange) => { + const children = node.childNodes; + let l = children.length; + let frags = []; + while (l--) { + const child = children[l]; + const prev = l && children[l - 1]; + if (prev && isInline(child) && areAlike(child, prev)/* && !leafNodeNames.has(child.nodeName)*/) { + if (fakeRange.startContainer === child) { + fakeRange.startContainer = prev; + fakeRange.startOffset += getLength(prev); + } + if (fakeRange.endContainer === child) { + fakeRange.endContainer = prev; + fakeRange.endOffset += getLength(prev); + } + if (fakeRange.startContainer === node) { + if (fakeRange.startOffset > l) { + --fakeRange.startOffset; + } else if (fakeRange.startOffset === l) { + fakeRange.startContainer = prev; + fakeRange.startOffset = getLength(prev); + } + } + if (fakeRange.endContainer === node) { + if (fakeRange.endOffset > l) { + --fakeRange.endOffset; + } else if (fakeRange.endOffset === l) { + fakeRange.endContainer = prev; + fakeRange.endOffset = getLength(prev); + } + } + detach(child); + if (isTextNode(child)) { + prev.appendData(child.data); + } else { + frags.unshift(empty(child)); + } + } else if (isElement(child)) { + child.append(...frags); + frags = []; + _mergeInlines(child, fakeRange); + } + } + }; + var mergeInlines = (node, range) => { + node = isTextNode(node) ? node.parentNode : node; + if (isElement(node)) { + const fakeRange = { + startContainer: range.startContainer, + startOffset: range.startOffset, + endContainer: range.endContainer, + endOffset: range.endOffset + }; + _mergeInlines(node, fakeRange); + range.setStart(fakeRange.startContainer, fakeRange.startOffset); + range.setEnd(fakeRange.endContainer, fakeRange.endOffset); + } + }; + var mergeWithBlock = (block, next, range, root) => { + let container = next; + let parent; + let offset; + while ((parent = container.parentNode) && parent !== root && isElement(parent) && parent.childNodes.length === 1) { + container = parent; + } + detach(container); + offset = block.childNodes.length; + const last = block.lastChild; + if (isBrElement(last)) { + last.remove(); + --offset; + } + block.append(empty(next)); + range.setStart(block, offset); + range.collapse(true); + mergeInlines(block, range); + }; + var mergeContainers = (node, root) => { + const prev = node.previousSibling; + const first = node.firstChild; + const isListItem = node.nodeName === "LI"; + if (isListItem && (!first || !listNodeNames.has(first.nodeName))) { + return; + } + if (prev && areAlike(prev, node)) { + if (!isContainer(prev)) { + if (!isListItem) { + return; + } + const block = createElement("DIV"); + block.append(empty(prev)); + prev.append(block); + } + detach(node); + const needsFix = !isContainer(node); + prev.append(empty(node)); + needsFix && fixContainer(prev, root); + first && mergeContainers(first, root); + } else if (isListItem) { + const block = createElement("DIV"); + node.insertBefore(block, first); + fixCursor(block); + } + }; - // source/range/Boundaries.ts - START_TO_START = 0, // Range.START_TO_START - START_TO_END = 1, // Range.START_TO_END - END_TO_END = 2, // Range.END_TO_END - END_TO_START = 3, // Range.END_TO_START - isNodeContainedInRange = (range, node, partial = true) => { - const nodeRange = doc.createRange(); - - nodeRange.selectNode(node); - - 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; - }, - moveRangeBoundariesDownTree = range => { - let { startContainer, startOffset, endContainer, endOffset } = range; - let child; - - while (!isTextNode(startContainer)) { - child = startContainer.childNodes[startOffset]; - if (!child || isLeaf(child)) { - if (startOffset) { - child = startContainer.childNodes[startOffset - 1]; - if (isTextNode(child)) { - let textChild = child; - let prev; - while (!textChild.length && (prev = textChild.previousSibling) && isTextNode(prev)) { - textChild.remove(); - textChild = prev; - } - startContainer = textChild; - startOffset = textChild.data.length; - } - } - break; - } - startContainer = child; - startOffset = 0; - } - if (endOffset) { - while (!isTextNode(endContainer)) { - child = endContainer.childNodes[endOffset - 1]; - if (!child || isLeaf(child)) { - if (isBrElement(child) && !isLineBreak(child)) { - --endOffset; - continue; - } - break; - } - endContainer = child; - endOffset = getLength(endContainer); - } - } else { - while (!isTextNode(endContainer)) { - child = endContainer.firstChild; - if (!child || isLeaf(child)) { - break; - } - endContainer = child; - } - } - - 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 parent; - - if (!startMax) { - startMax = range.commonAncestorContainer; - } - if (!endMax) { - endMax = startMax; - } - - while (!startOffset && startContainer !== startMax && startContainer !== root) { - parent = startContainer.parentNode; - startOffset = indexOf(parent.childNodes, startContainer); - startContainer = parent; - } - - while (endContainer !== endMax && endContainer !== root) { - if (!isTextNode(endContainer) && isBrElement(endContainer.childNodes[endOffset]) && !isLineBreak(endContainer.childNodes[endOffset])) { - ++endOffset; - } - if (endOffset !== getLength(endContainer)) { - break; - } - parent = endContainer.parentNode; - endOffset = indexOf(parent.childNodes, endContainer) + 1; - endContainer = parent; - } - - range.setStart(startContainer, startOffset); - range.setEnd(endContainer, endOffset); - }, - moveRangeBoundaryOutOf = (range, tag, root) => { - let parent = getClosest(range.endContainer, root, tag); - if (parent && (parent = parent.parentNode)) { - const clone = range.cloneRange(); - moveRangeBoundariesUpTree(clone, parent, parent, root); - if (clone.endContainer === parent) { - range.setStart(clone.endContainer, clone.endOffset); - range.setEnd(clone.endContainer, clone.endOffset); - } - } - return range; - }, - - // source/node/MergeSplit.ts - fixCursor = (node) => { - let fixer = null; - if (!isTextNode(node)) { - if (isInline(node)) { - let child = node.firstChild; - if (cantFocusEmptyTextNodes) { - while (child && isTextNode(child) && !child.data) { - node.removeChild(child); - child = node.firstChild; - } - } - if (!child) { - fixer = doc.createTextNode(cantFocusEmptyTextNodes ? ZWS : ""); - } - } else if (isElement(node) && !node.querySelector("BR")) { - fixer = createElement("BR"); - } - if (fixer) { - try { - node.appendChild(fixer); - } catch (error) { - didError({ - name: 'Squire: fixCursor – ' + error, - message: 'Parent: ' + node.nodeName + '/' + node.innerHTML + - ' appendChild: ' + fixer.nodeName - }); - } - } - } - return node; - }, - // Recursively examine container nodes and wrap any inline children. - fixContainer = (container, root) => { - let wrapper, isBR; - // Not live, and fast - [...container.childNodes].forEach(child => { - isBR = isBrElement(child); - if (!isBR && child.parentNode == root && isInline(child) -// && (blockTag !== "DIV" || (child.matches && !child.matches(phrasingElements))) - ) { - wrapper = wrapper || createElement("DIV"); - wrapper.append(child); - } else if (isBR || wrapper) { - wrapper = wrapper || createElement("DIV"); - fixCursor(wrapper); - child[isBR ? "replaceWith" : "before"](wrapper); - wrapper = null; - } - isContainer(child) && fixContainer(child, root); - }); - wrapper && container.append(fixCursor(wrapper)); - return container; - }, - split = (node, offset, stopNode, root) => { - if (isTextNode(node) && node !== stopNode) { - if (typeof offset !== "number") { - throw new Error("Offset must be a number to split text node!"); - } - if (!node.parentNode) { - throw new Error("Cannot split text node with no parent!"); - } - return split(node.parentNode, node.splitText(offset), stopNode, root); - } - let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset; - const parent = node.parentNode; - if (!parent || node === stopNode || !isElement(node)) { - return nodeAfterSplit; - } - const clone = node.cloneNode(false); - while (nodeAfterSplit) { - const next = nodeAfterSplit.nextSibling; - clone.append(nodeAfterSplit); - nodeAfterSplit = next; - } - if (node instanceof HTMLOListElement && getClosest(node, root, "BLOCKQUOTE")) { - clone.start = (+node.start || 1) + node.childNodes.length - 1; - } - fixCursor(node); - fixCursor(clone); - node.after(clone); - return split(parent, clone, stopNode, root); - }, - _mergeInlines = (node, fakeRange) => { - const children = node.childNodes; - let l = children.length; - let frags = []; - let child, prev; - while (l--) { - child = children[l]; - prev = l && children[l - 1]; - if (prev && isInline(child) && areAlike(child, prev)/* && !leafNodeNames.has(child.nodeName)*/) { - if (fakeRange.startContainer === child) { - fakeRange.startContainer = prev; - fakeRange.startOffset += getLength(prev); - } - if (fakeRange.endContainer === child) { - fakeRange.endContainer = prev; - fakeRange.endOffset += getLength(prev); - } - if (fakeRange.startContainer === node) { - if (fakeRange.startOffset > l) { - --fakeRange.startOffset; - } else if (fakeRange.startOffset === l) { - fakeRange.startContainer = prev; - fakeRange.startOffset = getLength(prev); - } - } - if (fakeRange.endContainer === node) { - if (fakeRange.endOffset > l) { - --fakeRange.endOffset; - } else if (fakeRange.endOffset === l) { - fakeRange.endContainer = prev; - fakeRange.endOffset = getLength(prev); - } - } - detach(child); - if (isTextNode(child)) { - prev.appendData(child.data); - } else { - frags.unshift(empty(child)); - } - } else if (isElement(child)) { - child.append(...frags); - frags = []; - _mergeInlines(child, fakeRange); - } - } - }, - mergeInlines = (node, range) => { - node = isTextNode(node) ? node.parentNode : node; - if (isElement(node)) { - const fakeRange = { - startContainer: range.startContainer, - startOffset: range.startOffset, - endContainer: range.endContainer, - endOffset: range.endOffset - }; - _mergeInlines(node, fakeRange); - range.setStart(fakeRange.startContainer, fakeRange.startOffset); - range.setEnd(fakeRange.endContainer, fakeRange.endOffset); - } - }, - mergeWithBlock = (block, next, range, root) => { - let container = next; - let parent; - let offset; - while ((parent = container.parentNode) && parent !== root && isElement(parent) && parent.childNodes.length === 1) { - container = parent; - } - detach(container); - offset = block.childNodes.length; - // Remove extra
fixer if present. - const last = block.lastChild; - if (isBrElement(last)) { - last.remove(); - --offset; - } - block.append(empty(next)); - range.setStart(block, offset); - range.collapse(true); - mergeInlines(block, range); - }, - mergeContainers = (node, root) => { - const prev = node.previousSibling; - const first = node.firstChild; - const isListItem = node.nodeName === "LI"; - // Do not merge LIs, unless it only contains a UL - if (isListItem && (!first || !listNodeNames.has(first.nodeName))) { - return; - } - let needsFix, block; - if (prev && areAlike(prev, node)) { - if (!isContainer(prev)) { - if (!isListItem) { - return; - } - block = createElement("DIV"); - block.append(empty(prev)); - prev.append(block); - } - detach(node); - needsFix = !isContainer(node); - prev.append(empty(node)); - if (needsFix) { - fixContainer(prev, root); - } - if (first) { - mergeContainers(first, root); - } - } else if (isListItem) { - block = createElement("DIV"); - node.insertBefore(block, first); - fixCursor(block); - } - }, - - // source/Clean.ts - 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") - } - */ - }, - replaceStyles = node => { - const style = node.style; - let newTreeBottom, newTreeTop, css, 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; - }, - 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; - }, - fontSizes = { - 1: 'x-small', - 2: "small", - 3: "medium", - 4: "large", - 5: 'x-large', - 6: 'xx-large', - 7: 'xxx-large', - '-1': "smaller", - '+1': "larger" - }, - stylesRewriters = { - STRONG: replaceWithTag("B"), - EM: replaceWithTag("I"), - INS: replaceWithTag("U"), - STRIKE: replaceWithTag("S"), - SPAN: replaceStyles, - FONT: node => { - const face = node.face; - const size = node.size; - let color = node.color; - let newTag = createElement("SPAN"); - let 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 = new Set(["HEAD", "META", "STYLE"]), - /* - 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) => { - const children = node.childNodes; - let nonInlineParent, i = children.length, child, nodeName, childLength; -// startsWithWS, endsWithWS, data, sibling; - - nonInlineParent = node; - while (isInline(nonInlineParent)) { - nonInlineParent = nonInlineParent.parentNode; - } -// const walker = createTreeWalker(nonInlineParent, SHOW_ELEMENT_OR_TEXT); - - while (i--) { - child = children[i]; - nodeName = child.nodeName; - if (isElement(child)) { - childLength = child.childNodes.length; - if (stylesRewriters[nodeName]) { - child = stylesRewriters[nodeName](child); - } else if (blacklist.has(nodeName)) { - child.remove(); - continue; - } else if (!allowedBlock.test(nodeName) && !isInline(child)) { - i += childLength; - child.replaceWith(empty(child)); - continue; - } - if (childLength) { - cleanTree(child, preserveWS || (nodeName === "PRE")); - } + // source/Clean.ts + var styleToSemantic = { + fontWeight: { + regexp: /^bold|^700/i, + replace: () => createElement("B") + }, + fontStyle: { + regexp: /^italic/i, + replace: () => createElement("I") + }, + fontFamily: { + regexp: notWS, + replace: (family) => createElement("SPAN", { + style: "font-family:" + family + }) + }, + fontSize: { + regexp: notWS, + replace: (size) => createElement("SPAN", { + style: "font-size:" + size + }) + }, + textDecoration: { + regexp: /^underline/i, + replace: () => createElement("U") + } + /* + textDecoration: { + regexp: /^line-through/i, + replace: doc => createElement("S") + } + */ + }; + var replaceStyles = (node) => { + const style = node.style; + let newTreeBottom; + let newTreeTop; + Object.entries(styleToSemantic).forEach(([attr, converter]) => { + const css = style[attr]; + if (css && converter.regexp.test(css)) { + const el = converter.replace(css); + if (el.nodeName !== node.nodeName || el.className !== node.className) { + if (!newTreeTop) { + newTreeTop = el; + } + if (newTreeBottom) { + newTreeBottom.append(el); + } + newTreeBottom = el; + node.style.removeProperty(attr); + } + } + }); + if (newTreeTop && newTreeBottom) { + newTreeBottom.append(empty(node)); + if (node.style.cssText) { + node.append(newTreeTop); + } else { + replaceWith(node, newTreeTop); + } + } + return newTreeBottom || node; + }; + var replaceWithTag = (tag) => (node) => { + const el = createElement(tag); + Array.prototype.forEach.call(node.attributes, (attr) => el.setAttribute(attr.name, attr.value)); + replaceWith(node, el); + el.append(empty(node)); + return el; + }; + var fontSizes = { + 1: "x-small", + 2: "small", + 3: "medium", + 4: "large", + 5: "x-large", + 6: "xx-large", + 7: "xxx-large", + "-1": "smaller", + "+1": "larger" + }; + var stylesRewriters = { + STRONG: replaceWithTag("B"), + EM: replaceWithTag("I"), + INS: replaceWithTag("U"), + STRIKE: replaceWithTag("S"), + SPAN: replaceStyles, + FONT: (node) => { + const face = node.face; + const size = node.size; + let color = node.color; + let newTag = createElement("SPAN"); + let 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; + } + replaceWith(node, newTag); + newTag.append(empty(node)); + return newTag; + }, + TT: (node) => { + const el = createElement("SPAN", { + style: 'font-family:menlo,consolas,"courier new",monospace' + }); + replaceWith(node, el); + el.append(empty(node)); + return el; + } + }; + var 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)$/; + var blacklist = new Set(["HEAD", "META", "STYLE"]); + var cleanTree = (node, preserveWS) => { + const children = node.childNodes; + let nonInlineParent = node; + while (isInline(nonInlineParent)) { + nonInlineParent = nonInlineParent.parentNode; + } /* - } else { - if (isTextNode(child)) { - 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 = walker.previousPONode()) { - 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(); + const walker = createTreeWalker( + nonInlineParent, + SHOW_ELEMENT_OR_TEXT + ); */ - } - } - return node; - }, - removeEmptyInlines = node => { - const children = node.childNodes; - let 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 (isTextNode(child) && !child.data) { - child.remove(); - } - } - }, - //
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) => { - const 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) { - // 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) => { - const 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) => { - const container = range.startContainer; - let block; - if (isInline(container)) { - block = getPreviousBlock(container, root); - } else if (container !== root && container instanceof HTMLElement && isBlock(container)) { - block = container; - } else { - block = getNextBlock(getNodeBefore(container, range.startOffset), root); - } - 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) => { - const container = range.endContainer; - let block, child; - if (isInline(container)) { - block = getPreviousBlock(container, root); - } else if (container !== root && container instanceof HTMLElement && isBlock(container)) { - block = container; - } else { - block = getNodeAfterOffset(container, range.endOffset); - if (!block || !root.contains(block)) { - block = root; - while (child = block.lastChild) { - block = child; - } - } - block = getPreviousBlock(block, root); - } - return block && isNodeContainedInRange(range, block) ? block : null; - }, - rangeDoesStartAtBlockBoundary = (range, root) => { - const startContainer = range.startContainer; - const startOffset = range.startOffset; - let nodeAfterCursor; - - // If in the middle or end of a text node, we're not at the boundary. - if (isTextNode(startContainer)) { - const text = startContainer.data; - let i = startOffset; - while (i--) { - if (text.charAt(i) !== ZWS) { - return false; - } - } - nodeAfterCursor = startContainer; - } else { - nodeAfterCursor = getNodeAfterOffset(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; - } - } - } - const block = getStartBlockOfRange(range, root); - if (block) { - const contentWalker = newContentWalker(block); - contentWalker.currentNode = nodeAfterCursor; - return !contentWalker.previousNode(); - } - }, - rangeDoesEndAtBlockBoundary = (range, root) => { - const endContainer = range.endContainer; - const endOffset = range.endOffset; - let currentNode; - // If in a text node with content, and not at the end, we're not at the boundary - if (isTextNode(endContainer)) { - const text = endContainer.data; - const length = text.length; - for (let i = endOffset; i < length; ++i) { - if (text.charAt(i) !== ZWS) { - return false; - } - } - currentNode = endContainer; - } else { - currentNode = getNodeBefore(endContainer, endOffset); - } - const block = getEndBlockOfRange(range, root); - if (block) { - const contentWalker = newContentWalker(block); - contentWalker.currentNode = currentNode; - return !contentWalker.nextNode(); - } - }, - expandRangeToBlockBoundaries = (range, root) => { - const start = getStartBlockOfRange(range, root); - const end = getEndBlockOfRange(range, root); -// let parent; - 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 - createRange = (startContainer, startOffset, endContainer, endOffset) => { - const range = doc.createRange(); - range.setStart(startContainer, startOffset); - if (endContainer && typeof endOffset === "number") { - range.setEnd(endContainer, endOffset); - } else { - range.setEnd(startContainer, startOffset); - } - return range; - }, - - insertNodeInRange = (range, node) => { - let { startContainer, startOffset, endContainer, endOffset } = range; - let children, parent; - - // If part way through a text node, split it. - if (isTextNode(startContainer)) { - parent = startContainer.parentNode; - children = parent.childNodes; - if (startOffset === startContainer.length) { - startOffset = indexOf(children, startContainer) + 1; - if (range.collapsed) { - endContainer = parent; - endOffset = startOffset; - } - } else { - if (startOffset) { - const afterSplit = startContainer.splitText(startOffset); - if (endContainer === startContainer) { - endOffset -= startOffset; - endContainer = afterSplit; - } else if (endContainer === parent) { - ++endOffset; - } - startContainer = afterSplit; - } - startOffset = indexOf(children, - startContainer - ); - } - startContainer = parent; - } else { - children = startContainer.childNodes; - } - const childCount = children.length; - if (startOffset === childCount) { - startContainer.append(node); - } else { - startContainer.insertBefore(node, children[startOffset]); - } - if (startContainer === endContainer) { - endOffset += children.length - childCount; - } - range.setStart(startContainer, startOffset); - range.setEnd(endContainer, endOffset); - }, - - extractContentsOfRange = (range, common, root) => { - common = common || range.commonAncestorContainer; - if (isTextNode(common)) { - common = common.parentNode; - } - - let endNode = split(range.endContainer, range.endOffset, common, root), - frag = range.extractContents(), - startContainer = common, - startOffset = endNode ? indexOf(common.childNodes, endNode) : common.childNodes.length, - after = common.childNodes[startOffset], - before = after?.previousSibling, - beforeText, afterText; - - // Merge text nodes if adjacent. - if (isTextNode(before) && isTextNode(after)) { - startContainer = before; - startOffset = before.length; - beforeText = before.data; - afterText = after.data; - - // If we now have two adjacent spaces, the second one needs to become - // a nbsp, otherwise the browser will swallow it due to HTML whitespace - // collapsing. - if (beforeText.charAt(beforeText.length - 1) === ' ' && afterText.charAt(0) === ' ') { - afterText = NBSP + afterText.slice(1); - } - before.appendData(afterText); - detach(after); - } - - range.setStart(startContainer, startOffset); - range.collapse(true); - - fixCursor(common); - - return frag; - }, - getAdjacentInlineNode = (iterator, method, node) => { - iterator.currentNode = node; - let nextNode; - while (nextNode = iterator[method]()) { - if (isTextNode(nextNode) || isLeaf(nextNode)) { - return nextNode; - } - if (!isInline(nextNode)) { - return null; - } - } - return null; - }, - deleteContentsOfRange = (range, root) => { - const startBlock = getStartBlockOfRange(range, root); - let endBlock = getEndBlockOfRange(range, root); - const needsMerge = startBlock !== endBlock; - if (startBlock && endBlock) { - moveRangeBoundariesDownTree(range); - moveRangeBoundariesUpTree(range, startBlock, endBlock, root); - } - - // Remove selected range - const frag = extractContentsOfRange(range, null, root); - - // Move boundaries back down tree as far as possible. - moveRangeBoundariesDownTree(range); - - // If we split into two different blocks, merge the blocks. - if (needsMerge) { - // endBlock will have been split, so need to refetch - endBlock = getEndBlockOfRange(range, root); - if (startBlock && endBlock && startBlock !== endBlock) { - mergeWithBlock(startBlock, endBlock, range, root); - } - } - - // Ensure block has necessary children - if (startBlock) { - fixCursor(startBlock); - } - - // Ensure root has a block-level element in it. - const child = root.firstChild; - if (!child || isBrElement(child)) { - fixCursor(root); - root.firstChild && range.selectNodeContents(root.firstChild); - } - range.collapse(true); - const startContainer = range.startContainer; - const startOffset = range.startOffset; - const iterator = newContentWalker(root); - let afterNode = startContainer; - let afterOffset = startOffset; - if (!isTextNode(afterNode) || afterOffset === afterNode.data.length) { - afterNode = getAdjacentInlineNode(iterator, "nextNode", afterNode); - afterOffset = 0; - } - let beforeNode = startContainer; - let beforeOffset = startOffset - 1; - if (!isTextNode(beforeNode) || beforeOffset === -1) { - beforeNode = getAdjacentInlineNode( - iterator, - "previousPONode", - afterNode || (isTextNode(startContainer) ? startContainer : startContainer.childNodes[startOffset] || startContainer) - ); - if (isTextNode(beforeNode)) { - beforeOffset = beforeNode.data.length; - } - } - let node = null; - let offset = 0; - if (isTextNode(afterNode) && afterNode.data.charAt(afterOffset) === " " && rangeDoesStartAtBlockBoundary(range, root)) { - node = afterNode; - offset = afterOffset; - } else if (isTextNode(beforeNode) && beforeNode.data.charAt(beforeOffset) === " ") { - if (isTextNode(afterNode) && afterNode.data.charAt(afterOffset) === " " || rangeDoesEndAtBlockBoundary(range, root)) { - node = beforeNode; - offset = beforeOffset; - } - } - node && node.replaceData(offset, 1, "\xA0"); - range.setStart(startContainer, startOffset); - range.collapse(true); - return frag; - }, - - // Contents of range will be deleted. - // After method, range will be around inserted content - insertTreeFragmentIntoRange = (range, frag, root) => { - const firstInFragIsInline = frag.firstChild && isInline(frag.firstChild); - let node, block, blockContentsAfterSplit, stopPoint, container, offset; - let replaceBlock, firstBlockInFrag, nodeAfterSplit, nodeBeforeSplit; - let tempRange; - - // Fixup content: ensure no top-level inline, and add cursor fix elements. - fixContainer(frag, root); - node = frag; - while (node = getNextBlock(node, root)) { - fixCursor(node); - } - - // Delete any selected content. - if (!range.collapsed) { - deleteContentsOfRange(range, root); - } - - // Move range down into text nodes. - moveRangeBoundariesDownTree(range); - range.collapse(); // collapse to end - - // Where will we split up to? First blockquote parent, otherwise root. - stopPoint = getClosest(range.endContainer, root, "BLOCKQUOTE") || root; - - // Merge the contents of the first block in the frag with the focused block. - // If there are contents in the block after the focus point, collect this - // up to insert in the last block later. This preserves the style that was - // present in this bit of the page. - // - // If the block being inserted into is empty though, replace it instead of - // merging if the fragment had block contents. - // e.g.

Foo

- // This seems a reasonable approximation of user intent. - - block = getStartBlockOfRange(range, root); - firstBlockInFrag = getNextBlock(frag, frag); - replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock(block); - if (block && firstBlockInFrag && !replaceBlock && // Don't merge table cells or PRE elements into block - !getClosest(firstBlockInFrag, frag, "PRE,TABLE")) { - moveRangeBoundariesUpTree(range, block, block, root); - range.collapse(true); // collapse to start - container = range.endContainer; - offset = range.endOffset; - // Remove trailing
– we don't want this considered content to be - // inserted again later - cleanupBRs(block, root, false); - if (isInline(container)) { - // Split up to block parent. - nodeAfterSplit = split( - container, - offset, - getPreviousBlock(container, root) || root, - root - ); - container = nodeAfterSplit.parentNode; - offset = indexOf(container.childNodes, nodeAfterSplit); - } - 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]) { - blockContentsAfterSplit.append(node); - } - } - // And merge the first block in. - mergeWithBlock(container, firstBlockInFrag, range, root); - - // And where we will insert - offset = indexOf(container.parentNode.childNodes, - container - ) + 1; - container = container.parentNode; - range.setEnd(container, offset); - } - - // Is there still any content in the fragment? - if (getLength(frag)) { - if (replaceBlock && block) { - range.setEndBefore(block); - range.collapse(); - detach(block); - } - 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; - stopPoint.insertBefore(frag, nodeAfterSplit); - if (nodeAfterSplit) { - range.setEndBefore(nodeAfterSplit); - } else { - range.setEnd(stopPoint, getLength(stopPoint)); - } - block = getEndBlockOfRange(range, root); - - // Get a reference that won't be invalidated if we merge containers. - moveRangeBoundariesDownTree(range); - container = range.endContainer; - offset = range.endOffset; - - // Merge inserted containers with edges of split - if (nodeAfterSplit && isContainer(nodeAfterSplit)) { - mergeContainers(nodeAfterSplit, root); - } - nodeAfterSplit = nodeBeforeSplit?.nextSibling; - if (nodeAfterSplit && isContainer(nodeAfterSplit)) { - mergeContainers(nodeAfterSplit, root); - } - range.setEnd(container, offset); - } - - // Insert inline content saved from before. - if (blockContentsAfterSplit && block) { - tempRange = range.cloneRange(); - mergeWithBlock(block, blockContentsAfterSplit, tempRange, root); - range.setEnd(tempRange.endContainer, tempRange.endOffset); - } - moveRangeBoundariesDownTree(range); - }, + let i = children.length; + while (i--) { + let child = children[i]; + const nodeName = child.nodeName; + if (isElement(child)) { + const childLength = child.childNodes.length; + if (stylesRewriters[nodeName]) { + child = stylesRewriters[nodeName](child); + } else if (blacklist.has(nodeName)) { + child.remove(); + continue; + } else if (!allowedBlock.test(nodeName) && !isInline(child)) { + i += childLength; + replaceWith(child, empty(child)); + continue; + } + if (childLength) { + cleanTree(child, preserveWS || (nodeName === "PRE")); + } /* - // source/range/Contents.ts - getTextContentsOfRange = (range) => { - if (range.collapsed) { - return ""; - } - const startContainer = range.startContainer; - const endContainer = range.endContainer; - const walker = new TreeIterator( - range.commonAncestorContainer, - SHOW_ELEMENT_OR_TEXT, - (node2) => { - return isNodeContainedInRange(range, node2, true); - } - ); - walker.currentNode = startContainer; - let node = startContainer; - let textContent = ""; - let addedTextInBlock = false; - let value; - if (!isElement(node) && !isTextNode(node) || !walker.filter(node)) { - node = walker.nextNode(); - } - while (node) { - if (isTextNode(node)) { - value = node.data; - if (value && /\S/.test(value)) { - if (node === endContainer) { - value = value.slice(0, range.endOffset); - } - if (node === startContainer) { - value = value.slice(range.startOffset); - } - textContent += value; - addedTextInBlock = true; - } - } else if (isBrElement(node) || addedTextInBlock && !isInline(node)) { - textContent += "\n"; - addedTextInBlock = false; - } - node = walker.nextNode(); - } - textContent = textContent.replace(/\xA0/g, " "); - return textContent; - }, + } else { + if (isTextNode(child)) { + let data = child.data; + const startsWithWS = !notWS.test(data.charAt(0)); + const endsWithWS = !notWS.test(data.charAt(data.length - 1)); + if (preserveWS || (!startsWithWS && !endsWithWS)) { + continue; + } + if (startsWithWS) { + walker.currentNode = child; + let sibling; + while (sibling = walker.previousPONode()) { + if (sibling.nodeName === "IMG" || isTextNode(sibling) && notWS.test(sibling.data)) { + break; + } + if (!isInline(sibling)) { + sibling = null; + break; + } + } + data = data.replace(/^[ \t\r\n]+/g, sibling ? " " : ""); + } + if (endsWithWS) { + walker.currentNode = child; + let sibling; + while (sibling = walker.nextNode()) { + if (sibling.nodeName === "IMG" || (isTextNode(sibling) && notWS.test(sibling.data))) { + break; + } + if (!isInline(sibling)) { + sibling = null; + break; + } + } + data = data.replace(/[ \t\r\n]+$/g, sibling ? " " : ""); + } + if (data) { + child.data = data; + continue; + } + } + node.removeChild(child); */ - // source/Clipboard.ts - indexOf = (array, value) => Array.prototype.indexOf.call(array, value), - extractRangeToClipboard = (event, range, root) => { - // 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 - let 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); - } - }, - // 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; + } + } + return node; + }; + var removeEmptyInlines = (node) => { + const children = node.childNodes; + let l = children.length; + while (l--) { + const child = children[l]; + if (isElement(child) && !isLeaf(child)) { + removeEmptyInlines(child); + if (!child.firstChild && isInline(child)) { + child.remove(); + } + } else if (!child.data && isTextNode(child)) { + node.removeChild(child); + } + } + }; + var cleanupBRs = (node, root, keepForBlankLine) => { + const brs = node.querySelectorAll("BR"); + let l = brs.length; + while (l--) { + const br = brs[l]; + const parent = br.parentNode; + if (parent) { + if (!isLineBreak(br, keepForBlankLine)) { + detach(br); + } else if (!isInline(parent)) { + fixContainer(parent, root); + } + } + } + }; + var escapeHTML = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); - node.append(contents); + // source/node/Block.ts + var getBlockWalker = (node, root) => { + const walker = createTreeWalker(root, SHOW_ELEMENT, isBlock); + walker.currentNode = node; + return walker; + }; + var getPreviousBlock = (node, root) => { + node = getBlockWalker(node, root).previousNode(); + return node !== root ? node : null; + }; + var getNextBlock = (node, root) => { + node = getBlockWalker(node, root).nextNode(); + return node !== root ? node : null; + }; + var isEmptyBlock = (block) => !block.textContent && !block.querySelector("IMG"); - html = node.innerHTML; + // source/range/Block.ts + var getStartBlockOfRange = (range, root) => { + const container = range.startContainer; + let block; + if (isInline(container)) { + block = getPreviousBlock(container, root); + } else if (container !== root && container instanceof HTMLElement && isBlock(container)) { + block = container; + } else { + block = getNextBlock(getNodeBeforeOffset(container, range.startOffset), root); + } + return block && isNodeContainedInRange(range, block) ? block : null; + }; + var getEndBlockOfRange = (range, root) => { + const container = range.endContainer; + let block; + if (isInline(container)) { + block = getPreviousBlock(container, root); + } else if (container !== root && container instanceof HTMLElement && isBlock(container)) { + block = container; + } else { + let node = getNodeAfterOffset(container, range.endOffset); + if (!node || !root.contains(node)) { + node = root; + let child; + while (child = node.lastChild) { + node = child; + } + } + block = getPreviousBlock(node, root); + } + return block && isNodeContainedInRange(range, block) ? block : null; + }; + var createContentWalker = (root) => createTreeWalker( + root, + SHOW_ELEMENT_OR_TEXT, + (node) => isTextNode(node) ? notWS.test(node.data) : node.nodeName === "IMG" + ); + var rangeDoesStartAtBlockBoundary = (range, root) => { + const startContainer = range.startContainer; + const startOffset = range.startOffset; + let nodeAfterCursor; + if (isTextNode(startContainer)) { + let i = startOffset; + while (i--) { + if (startContainer.data.charAt(i) !== ZWS) { + return false; + } + } + nodeAfterCursor = startContainer; + } else { + nodeAfterCursor = getNodeAfterOffset(startContainer, startOffset); + if (!nodeAfterCursor || !root.contains(nodeAfterCursor)) { + nodeAfterCursor = getNodeBeforeOffset(startContainer, startOffset); + if (isTextNode(nodeAfterCursor) && nodeAfterCursor.length) { + return false; + } + } + } + const block = getStartBlockOfRange(range, root); + if (block) { + const contentWalker = createContentWalker(block); + contentWalker.currentNode = nodeAfterCursor; + return !contentWalker.previousNode(); + } + }; + var rangeDoesEndAtBlockBoundary = (range, root) => { + const endContainer = range.endContainer; + const endOffset = range.endOffset; + let currentNode; + if (isTextNode(endContainer)) { + const text = endContainer.data; + const length = text.length; + for (let i = endOffset; i < length; ++i) { + if (text.charAt(i) !== ZWS) { + return false; + } + } + currentNode = endContainer; + } else { + currentNode = getNodeBeforeOffset(endContainer, endOffset); + } + const block = getEndBlockOfRange(range, root); + if (block) { + const contentWalker = createContentWalker(block); + contentWalker.currentNode = currentNode; + return !contentWalker.nextNode(); + } + }; + var expandRangeToBlockBoundaries = (range, root) => { + const start = getStartBlockOfRange(range, root); + const end = getEndBlockOfRange(range, root); + if (start && end) { + range.setStart(start, 0); + range.setEnd(end, end.childNodes.length); + } + }; - // 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(); + // source/range/InsertDelete.ts + var createRange = (startContainer, startOffset, endContainer, endOffset) => { + const range = document.createRange(); + range.setStart(startContainer, startOffset); + if (endContainer && typeof endOffset === "number") { + range.setEnd(endContainer, endOffset); + } else { + range.setEnd(startContainer, startOffset); + } + return range; + }; + var insertNodeInRange = (range, node) => { + let { startContainer, startOffset, endContainer, endOffset } = range; + let children; + if (isTextNode(startContainer)) { + const parent = startContainer.parentNode; + children = parent.childNodes; + if (startOffset === startContainer.length) { + startOffset = indexOf(children, startContainer) + 1; + if (range.collapsed) { + endContainer = parent; + endOffset = startOffset; + } + } else { + if (startOffset) { + const afterSplit = startContainer.splitText(startOffset); + if (endContainer === startContainer) { + endOffset -= startOffset; + endContainer = afterSplit; + } else if (endContainer === parent) { + ++endOffset; + } + startContainer = afterSplit; + } + startOffset = indexOf(children, startContainer); + } + startContainer = parent; + } else { + children = startContainer.childNodes; + } + const childCount = children.length; + if (startOffset === childCount) { + startContainer.append(node); + } else { + startContainer.insertBefore(node, children[startOffset]); + } + if (startContainer === endContainer) { + endOffset += children.length - childCount; + } + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + }; + var extractContentsOfRange = (range, common, root) => { + common = common || range.commonAncestorContainer; + if (isTextNode(common)) { + common = common.parentNode; + } + let endNode = split(range.endContainer, range.endOffset, common, root), + frag = range.extractContents(), + startContainer = common, + startOffset = endNode ? indexOf(common.childNodes, endNode) : common.childNodes.length, + after = common.childNodes[startOffset], + before = after?.previousSibling, + beforeText, afterText; + if (isTextNode(before) && isTextNode(after)) { + startContainer = before; + startOffset = before.length; + beforeText = before.data; + afterText = after.data; + if (beforeText.charAt(beforeText.length - 1) === ' ' && afterText.charAt(0) === ' ') { + afterText = NBSP + afterText.slice(1); + } + before.appendData(afterText); + detach(after); + } + range.setStart(startContainer, startOffset); + range.collapse(true); + fixCursor(common); + return frag; + }; + var getAdjacentInlineNode = (iterator, method, node) => { + iterator.currentNode = node; + let nextNode; + while (nextNode = iterator[method]()) { + if (isTextNode(nextNode) || isLeaf(nextNode)) { + return nextNode; + } + if (!isInline(nextNode)) { + return null; + } + } + return null; + }; + var deleteContentsOfRange = (range, root) => { + const startBlock = getStartBlockOfRange(range, root); + let endBlock = getEndBlockOfRange(range, root); + const needsMerge = startBlock !== endBlock; + if (startBlock && endBlock) { + moveRangeBoundariesDownTree(range); + moveRangeBoundariesUpTree(range, startBlock, endBlock, root); + } + const frag = extractContentsOfRange(range, null, root); + moveRangeBoundariesDownTree(range); + if (needsMerge) { + endBlock = getEndBlockOfRange(range, root); + if (startBlock && endBlock && startBlock !== endBlock) { + mergeWithBlock(startBlock, endBlock, range, root); + } + } + if (startBlock) { + fixCursor(startBlock); + } + const child = root.firstChild; + if (!child || isBrElement(child)) { + fixCursor(root); + root.firstChild && range.selectNodeContents(root.firstChild); + } + range.collapse(true); + const startContainer = range.startContainer; + const startOffset = range.startOffset; + const iterator = createTreeWalker(root, SHOW_ELEMENT_OR_TEXT); + let afterNode = startContainer; + let afterOffset = startOffset; + if (!isTextNode(afterNode) || afterOffset === afterNode.data.length) { + afterNode = getAdjacentInlineNode(iterator, "nextNode", afterNode); + afterOffset = 0; + } + let beforeNode = startContainer; + let beforeOffset = startOffset - 1; + if (!isTextNode(beforeNode) || beforeOffset === -1) { + beforeNode = getAdjacentInlineNode( + iterator, + "previousPONode", + afterNode || (isTextNode(startContainer) ? startContainer : startContainer.childNodes[startOffset] || startContainer) + ); + if (isTextNode(beforeNode)) { + beforeOffset = beforeNode.data.length; + } + } + let node = null; + let offset = 0; + if (isTextNode(afterNode) && afterNode.data.charAt(afterOffset) === " " && rangeDoesStartAtBlockBoundary(range, root)) { + node = afterNode; + offset = afterOffset; + } else if (isTextNode(beforeNode) && beforeNode.data.charAt(beforeOffset) === " ") { + if (isTextNode(afterNode) && afterNode.data.charAt(afterOffset) === " " || rangeDoesEndAtBlockBoundary(range, root)) { + node = beforeNode; + offset = beforeOffset; + } + } + node && node.replaceData(offset, 1, "\xA0"); + range.setStart(startContainer, startOffset); + range.collapse(true); + return frag; + }; + var insertTreeFragmentIntoRange = (range, frag, root) => { + const firstInFragIsInline = frag.firstChild && isInline(frag.firstChild); + let node, blockContentsAfterSplit; + fixContainer(frag, root); + node = frag; + while (node = getNextBlock(node, root)) { + fixCursor(node); + } + if (!range.collapsed) { + deleteContentsOfRange(range, root); + } + moveRangeBoundariesDownTree(range); + range.collapse(); + const stopPoint = getClosest(range.endContainer, root, "BLOCKQUOTE") || root; + let block = getStartBlockOfRange(range, root); + const firstBlockInFrag = getNextBlock(frag, frag); + const replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock(block); + if (block && firstBlockInFrag && !replaceBlock && + !getClosest(firstBlockInFrag, frag, "PRE,TABLE")) { + moveRangeBoundariesUpTree(range, block, block, root); + range.collapse(true); + let container = range.endContainer; + let offset = range.endOffset; + cleanupBRs(block, root, false); + if (isInline(container)) { + const nodeAfterSplit = split( + container, + offset, + getPreviousBlock(container, root) || root, + root + ); + container = nodeAfterSplit.parentNode; + offset = indexOf(container.childNodes, nodeAfterSplit); + } + if ( + /*isBlock(container) && */ + offset !== getLength(container) + ) { + blockContentsAfterSplit = document.createDocumentFragment(); + while (node = container.childNodes[offset]) { + blockContentsAfterSplit.append(node); + } + } + mergeWithBlock(container, firstBlockInFrag, range, root); + offset = indexOf(container.parentNode.childNodes, container) + 1; + container = container.parentNode; + range.setEnd(container, offset); + } + if (getLength(frag)) { + if (replaceBlock && block) { + range.setEndBefore(block); + range.collapse(); + detach(block); + } + moveRangeBoundariesUpTree(range, stopPoint, stopPoint, root); + let nodeAfterSplit = split( + range.endContainer, + range.endOffset, + stopPoint, + root + ); + const nodeBeforeSplit = nodeAfterSplit ? nodeAfterSplit.previousSibling : stopPoint.lastChild; + stopPoint.insertBefore(frag, nodeAfterSplit); + if (nodeAfterSplit) { + range.setEndBefore(nodeAfterSplit); + } else { + range.setEnd(stopPoint, getLength(stopPoint)); + } + block = getEndBlockOfRange(range, root); + moveRangeBoundariesDownTree(range); + const container = range.endContainer; + const offset = range.endOffset; + if (nodeAfterSplit && isContainer(nodeAfterSplit)) { + mergeContainers(nodeAfterSplit, root); + } + nodeAfterSplit = nodeBeforeSplit?.nextSibling; + if (nodeAfterSplit && isContainer(nodeAfterSplit)) { + mergeContainers(nodeAfterSplit, root); + } + range.setEnd(container, offset); + } + if (blockContentsAfterSplit && block) { + const tempRange = range.cloneRange(); + mergeWithBlock(block, blockContentsAfterSplit, tempRange, root); + range.setEnd(tempRange.endContainer, tempRange.endOffset); + } + moveRangeBoundariesDownTree(range); + }; - 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; - - // Nothing to do - if (range.collapsed) { - event.preventDefault(); - return; - } - - // Save undo checkpoint - self.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 (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); - } else { - setTimeout(() => { - try { - // If all content removed, ensure div at start of root. - self._ensureBottomLine(); - } catch (error) { - didError(error); - } - }, 0); - } - - self.setSelection(range); - }, - onCopy = function(event) { - extractRangeToClipboard( - event, - this.getSelection(), - this._root - ); - }, - onPaste = function(event) { - const clipboardData = event.clipboardData; - const 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)); - } - } - } - }, - - // source/keyboard/KeyHelpers.ts - // 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 - // you delete all text inside an inline tag, remove the inline tag. - afterDelete = (self, range) => { - try { - range = range || self.getSelection(); - let node = range.startContainer, - parent; - // Climb the tree from the focus point while we are inside an empty - // inline element - if (isTextNode(node)) { - node = node.parentNode; - } - parent = node; - while (isInline(parent) && (!parent.textContent || parent.textContent === ZWS)) { - node = parent; - parent = node.parentNode; - } - // If focused in empty inline element - if (node !== parent) { - // Move focus to just before empty inline(s) - range.setStart(parent, indexOf(parent.childNodes, node)); - range.collapse(true); - // Remove empty inline(s) - node.remove(); - // Fix cursor in block - if (!isBlock(parent)) { - parent = getPreviousBlock(parent, self._root) || parent; - } - fixCursor(parent); - // Move cursor into text node - moveRangeBoundariesDownTree(range); - } - // If you delete the last character in the sole
in Chrome, - // it removes the div and replaces it with just a
inside the - // root. Detach the
; the _ensureBottomLine call will insert a new - // block. - if (node === self._root && (node = node.firstChild) && isBrElement(node)) { - detach(node); - } - self._ensureBottomLine(); - self.setRange(range); - } catch (error) { - didError(error); - } - }, - - // source/keyboard/Backspace.ts - Backspace = (self, event, range) => { - const 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) { - if (decreaseLevel(self, range, current)) { - return; - } - self.setRange(range); - } - } - // Otherwise, leave to browser but check afterwards whether it has - // left behind an empty inline tag. - else { - self.setSelection(range); - setTimeout(() => afterDelete(self), 0); - } - }, - - // source/keyboard/Delete.ts - Delete = (self, event, range) => { - const root = self._root; - let current; - let next; - let originalRange; - let cursorContainer; - let cursorOffset; - let nodeAfterCursor; - self._removeZWS(); - // Record undo checkpoint. - self.saveUndoState(range); - if (!range.collapsed) { - // If not collapsed, delete contents - event.preventDefault(); - deleteContentsOfRange(range, root); - afterDelete(self, range); - } else if (rangeDoesEndAtBlockBoundary(range, root)) { - // If at end of block, merge next into this block - 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.setRange(range); - } - } - } else { - // Otherwise, leave to browser but check afterwards whether it has - // left behind an empty inline tag. - // 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); - } - }, - - // source/keyboard/Tab.ts - Tab = (self, event, range) => { - self._removeZWS(); - // If no selection and at start of block - if (range.collapsed && rangeDoesStartAtBlockBoundary(range, self._root)) { - getClosest(range.startContainer, self._root, "UL,OL,BLOCKQUOTE") - && self.changeIndentationLevel('increase') - && event.preventDefault(); - } - }, - ShiftTab = (self, event, range) => { - self._removeZWS(); - // If no selection and at start of block - if (range.collapsed && rangeDoesStartAtBlockBoundary(range, self._root)) { - // Break list - decreaseLevel(self, range, range.startContainer) - && event.preventDefault(); - } - }, - - // source/keyboard/Space.ts - Space = (self, event, range) => { + // source/range/Contents.ts /* - var _a; - let node; - const root = self._root; - self._recordUndoState(range); - self._getRangeAndRemoveBookmark(range); - if (!range.collapsed) { - deleteContentsOfRange(range, root); - self._ensureBottomLine(); - self.setSelection(range); - self._updatePath(range, true); - } else if (rangeDoesEndAtBlockBoundary(range, root)) { - const block = getStartBlockOfRange(range, root); - if (block && block.nodeName !== "PRE") { - const text = (_a = block.textContent) == null ? void 0 : _a.trimEnd().replace(ZWS, ""); - if (text === "*" || text === "1.") { - event.preventDefault(); - const walker = new TreeIterator(block, SHOW_TEXT); - let textNode; - while (textNode = walker.nextNode()) { - textNode.data = cantFocusEmptyTextNodes ? ZWS : ""; - } - if (text === "*") { - self.makeUnorderedList(); - } else { - self.makeOrderedList(); - } - return; - } - } - } - node = range.endContainer; - if (range.endOffset === getLength(node)) { - do { - if (node.nodeName === "A") { - range.setStartAfter(node); - break; - } - } while (!node.nextSibling && (node = node.parentNode) && node !== root); - } - if (self._config.addLinks) { - const linkRange = range.cloneRange(); - moveRangeBoundariesDownTree(linkRange); - const textNode = linkRange.startContainer; - const offset = linkRange.startOffset; - setTimeout(() => { - linkifyText(self, textNode, offset); - }, 0); - } - self.setSelection(range); + var getTextContentsOfRange = (range) => { + if (range.collapsed) { + return ""; + } + const startContainer = range.startContainer; + const endContainer = range.endContainer; + const filter = (node2) => isNodeContainedInRange(range, node2, true); + const walker = createTreeWalker( + range.commonAncestorContainer, + SHOW_ELEMENT_OR_TEXT, + filter + ); + walker.currentNode = startContainer; + let node = startContainer; + let textContent = ""; + let addedTextInBlock = false; + let value; + if (!isElement(node) && !isTextNode(node) || !filter(node)) { + node = walker.nextNode(); + } + while (node) { + if (isTextNode(node)) { + value = node.data; + if (value && /\S/.test(value)) { + if (node === endContainer) { + value = value.slice(0, range.endOffset); + } + if (node === startContainer) { + value = value.slice(range.startOffset); + } + textContent += value; + addedTextInBlock = true; + } + } else if (isBrElement(node) || addedTextInBlock && !isInline(node)) { + textContent += "\n"; + addedTextInBlock = false; + } + node = walker.nextNode(); + } + textContent = textContent.replace(/\xA0/g, " "); + return textContent; + }; */ - const root = self._root; - self._recordUndoState(range); - self._config.addLinks && addLinks(range.startContainer, root); - self._getRangeAndRemoveBookmark(range); + + // source/Clipboard.ts + var extractRangeToClipboard = (event, range, root, cut) => { + if (event.clipboardData) { + let startBlock = getStartBlockOfRange(range, root), + endBlock = getEndBlockOfRange(range, root), + copyRoot = ((startBlock === endBlock) && startBlock) || root, + contents, parent, newContents; + if (cut) { + contents = deleteContentsOfRange(range, root); + } else { + range = range.cloneRange(); + moveRangeBoundariesDownTree(range); + moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root); + contents = range.cloneContents(); + } + 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; + } + let clipboardData = event.clipboardData; + let body = document.body; + let node = createElement("div"); + let html, text; + node.append(contents); + html = node.innerHTML; + 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(); + return true; + } + }; + var _onCut = function(event) { + const range = this.getSelection(); + const root = this._root; + if (range.collapsed) { + event.preventDefault(); + return; + } + this.saveUndoState(range); + if (!extractRangeToClipboard(event, range, root, true)) { + setTimeout(() => { + try { + this._ensureBottomLine(); + } catch (error) { + didError(error); + } + }, 0); + } + this.setSelection(range); + }; + var _onCopy = function(event) { + extractRangeToClipboard( + event, + this.getSelection(), + this._root + ); + }; + var _onPaste = function(event) { + const clipboardData = event.clipboardData; + const items = clipboardData?.items; + let imageItem = null; + let plainItem = null; + let htmlItem = null; + let self = this; + let type; + if (items) { + [...items].forEach(item => { + type = item.type; + if (type === "text/html") { + htmlItem = item; + } 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) { + if (width > 1024) { + height = height * 1024 / width; + width = 1024; + } + } else if (height > 1024) { + 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)); + } + } + } + }; + + // source/keyboard/KeyHelpers.ts + var afterDelete = (self, range) => { + try { + range = range || self.getSelection(); + let node = range.startContainer; + if (isTextNode(node)) { + node = node.parentNode; + } + let parent = node; + while (isInline(parent) && (!parent.textContent || parent.textContent === ZWS)) { + node = parent; + parent = node.parentNode; + } + if (node !== parent) { + range.setStart( + parent, + indexOf(parent.childNodes, node) + ); + range.collapse(true); + node.remove(); + if (!isBlock(parent)) { + parent = getPreviousBlock(parent, self._root) || parent; + } + fixCursor(parent); + moveRangeBoundariesDownTree(range); + } + if (node === self._root && (node = node.firstChild) && isBrElement(node)) { + detach(node); + } + self._ensureBottomLine(); + self.setRange(range); + } catch (error) { + didError(error); + } + }; + + // source/keyboard/Backspace.ts + var Backspace = (self, event, range) => { + const root = self._root; + self._removeZWS(); + self.saveUndoState(range); + if (!range.collapsed) { + event.preventDefault(); + deleteContentsOfRange(range, root); + afterDelete(self, range); + } else if (rangeDoesStartAtBlockBoundary(range, root)) { + event.preventDefault(); + let current = getStartBlockOfRange(range, root); + let previous; + if (!current) { + return; + } + fixContainer(current.parentNode, root); + previous = getPreviousBlock(current, root); + if (previous) { + if (!previous.isContentEditable) { + detachUneditableNode(previous, root); + return; + } + mergeWithBlock(previous, current, range, root); + current = previous.parentNode; + while (current !== root && !current.nextSibling) { + current = current.parentNode; + } + if (current !== root && (current = current.nextSibling)) { + mergeContainers(current, root); + } + self.setSelection(range); + } else if (current) { + if (decreaseLevel(self, range, current)) { + return; + } + self.setRange(range); + } + } else { + self.setSelection(range); + setTimeout(() => afterDelete(self), 0); + } + }; + + // source/keyboard/Delete.ts + var Delete = (self, event, range) => { + const root = self._root; + let current; + let next; + let originalRange; + let cursorContainer; + let cursorOffset; + let nodeAfterCursor; + self._removeZWS(); + self.saveUndoState(range); + if (!range.collapsed) { + event.preventDefault(); + deleteContentsOfRange(range, root); + afterDelete(self, range); + } else if (rangeDoesEndAtBlockBoundary(range, root)) { + event.preventDefault(); + if (current = getStartBlockOfRange(range, root)) { + fixContainer(current.parentNode, root); + if (next = getNextBlock(current, root)) { + if (!next.isContentEditable) { + detachUneditableNode(next, root); + return; + } + mergeWithBlock(current, next, range, root); + next = current.parentNode; + while (next !== root && !next.nextSibling) { + next = next.parentNode; + } + if (next !== root && (next = next.nextSibling)) { + mergeContainers(next, root); + } + self.setRange(range); + } + } + } else { + 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); + } + }; + + // source/keyboard/Tab.ts + var Tab = (self, event, range) => { + self._removeZWS(); + range.collapsed + && rangeDoesStartAtBlockBoundary(range, self._root) + && getClosest(range.startContainer, self._root, "UL,OL,BLOCKQUOTE") + && self.changeIndentationLevel("increase") + && event.preventDefault(); + }; + var ShiftTab = (self, event, range) => { + self._removeZWS(); + range.collapsed + && rangeDoesStartAtBlockBoundary(range, self._root) + && decreaseLevel(self, range, range.startContainer) + && event.preventDefault(); + }; + + // source/keyboard/Space.ts + var Space = (self, event, 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); - } + var _a; + let node; + const root = self._root; + self._recordUndoState(range); + self._getRangeAndRemoveBookmark(range); + if (!range.collapsed) { + deleteContentsOfRange(range, root); + self._ensureBottomLine(); + self.setSelection(range); + self._updatePath(range, true); + } else if (rangeDoesEndAtBlockBoundary(range, root)) { + const block = getStartBlockOfRange(range, root); + if (block && block.nodeName !== "PRE") { + const text = block.textContent?.trimEnd().replace(ZWS, ""); + if (text === "*" || text === "1.") { + event.preventDefault(); + const walker = createTreeWalker(block, SHOW_TEXT); + let textNode; + while (textNode = walker.nextNode()) { + textNode.data = cantFocusEmptyTextNodes ? ZWS : ""; + } + if (text === "*") { + self.makeUnorderedList(); + } else { + self.makeOrderedList(); + } + return; + } + } + } + node = range.endContainer; + if (range.endOffset === getLength(node)) { + do { + if (node.nodeName === "A") { + range.setStartAfter(node); + break; + } + } while (!node.nextSibling && (node = node.parentNode) && node !== root); + } + if (self._config.addLinks) { + const linkRange = range.cloneRange(); + moveRangeBoundariesDownTree(linkRange); + const textNode = linkRange.startContainer; + const offset = linkRange.startOffset; + setTimeout(() => { + linkifyText(self, textNode, offset); + }, 0); + } + self.setSelection(range); */ - // Delete the selection if not collapsed - if (!range.collapsed) { - deleteContentsOfRange(range, root); - self._ensureBottomLine(); - } - self.setRange(range); - }, - - // source/keyboard/KeyHandlers.ts - onKey = function(event) { - if (event.defaultPrevented) { - return; - } - - let key = event.key.toLowerCase(), - range = this.getSelection(), - root = this._root; - - // We need to apply the backspace/delete handlers regardless of control key modifiers. - // ctrl-shift-key or meta-shift-key - if (key !== "backspace" && key !== "delete") { - if (event.shiftKey) { key = 'shift-' + key; } - if (event[osKey]) { key = ctrlKey + key; } -// if (event.altKey) { key = 'alt-' + 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.setRange(range); - } 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 (isBrElement(nextElement)) { - // delete it because a new
is created by createDefaultBlock() - root.removeChild(nextElement); - } - const restore = this._restoreSelection; - this.setSelection(range); - this._restoreSelection = restore; - } - } - }, - keyHandlers = { -// "backspace": Backspace, -// "delete": Delete, - tab: Tab, - 'shift-tab': ShiftTab, - space: Space, - 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", - - filterAccept = NodeFilter.FILTER_ACCEPT, + const root = self._root; + self._recordUndoState(range); + self._config.addLinks && addLinks(range.startContainer, root); + self._getRangeAndRemoveBookmark(range); /* - 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 - }, + 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); + } +*/ + if (!range.collapsed) { + deleteContentsOfRange(range, root); + self._ensureBottomLine(); + } + self.setRange(range); + }; + + // source/keyboard/KeyHandlers.ts + var _onKey = function(event) { + if (event.defaultPrevented) { + return; + } + let key = event.key, + range = this.getSelection(), + root = this._root; + if (key !== "Backspace" && key !== "Delete") { + if (event.shiftKey) { + key = "Shift-" + key; + } + if (event[osKey]) { key = ctrlKey + key; } + } + if (this._keyHandlers[key]) { + this._keyHandlers[key](this, event, range); + } else if (!range.collapsed && !event.isComposing && !event[osKey] && key.length === 1) { + this.saveUndoState(range); + deleteContentsOfRange(range, root); + this._ensureBottomLine(); + this.setRange(range); + } else if (range.collapsed && range.startContainer === root && root.children.length > 0) { + const nextElement = root.children[range.startOffset]; + if (nextElement && !isBlock(nextElement)) { + range = createRange(root.insertBefore( + this.createDefaultBlock(), nextElement + ), 0); + if (isBrElement(nextElement)) { + root.removeChild(nextElement); + } + const restore = this._willRestoreSelection; + this.setSelection(range); + this._willRestoreSelection = restore; + } + } + }; + var mapKeyToFormat = (tag, remove) => { + return (self, event) => { + event.preventDefault(); + self.toggleTag(tag, remove); + }; + }; + var mapKeyTo = (method) => (self, event) => { + event.preventDefault(); + self[method](); + }; + var toggleList = (type, methodIfNotInList) => (self, event) => { + event.preventDefault(); + let parent = self.getSelectionClosest("UL,OL"); + if (type == parent?.nodeName) { + self.removeList(); + } else { + self[methodIfNotInList](); + } + }; + var changeIndentationLevel = (direction) => (self, event) => { + event.preventDefault(); + self.changeIndentationLevel(direction); + }; + var keyHandlers = { + Tab, + "Shift-Tab": ShiftTab, + Space, + ArrowLeft(self) { + self._removeZWS(); + }, + ArrowRight(self) { + self._removeZWS() + }, + [ctrlKey + "b"]: mapKeyToFormat("B"), + [ctrlKey + "i"]: mapKeyToFormat("I"), + [ctrlKey + "u"]: mapKeyToFormat("U"), + [ctrlKey + "Shift-7"]: mapKeyToFormat("S"), + [ctrlKey + "Shift-5"]: mapKeyToFormat("SUB", "SUP"), + [ctrlKey + "Shift-6"]: mapKeyToFormat("SUP", "SUB"), + [ctrlKey + "Shift-8"]: toggleList("UL", "makeUnorderedList"), + [ctrlKey + "Shift-9"]: toggleList("OL", "makeOrderedList"), + [ctrlKey + "["]: changeIndentationLevel("decrease"), + [ctrlKey + "]"]: changeIndentationLevel("increase"), + [ctrlKey + "d"]: mapKeyTo("toggleCode"), +// [ctrlKey + "z"]: mapKeyTo("undo"), // historyUndo + [ctrlKey + "y"]: mapKeyTo("redo"), // historyRedo + [ctrlKey + "Shift-Z"]: mapKeyTo("redo"), + ["Redo"]: mapKeyTo("redo") + }; + var blockTag = "DIV"; + var DOCUMENT_POSITION_PRECEDING = 2; + + var NBSP = '\u00A0'; + var win = document.defaultView; + var osKey = isMac ? "metaKey" : "ctrlKey"; +/* + typeToBitArray = { + 1: 1, + 2: 2, + 3: 4, + 8: 128, + 9: 256, + 11: 1024 + }, */ - isElement = node => node instanceof Element, - isTextNode = node => node instanceof Text, -// isBrElement = node => node instanceof HTMLBRElement, - isBrElement = node => "BR" === node?.nodeName, + var didError = error => console.error(error); + var detachUneditableNode = (node, root) => { + let parent; + while (parent = node.parentNode) { + if (parent === root || parent.isContentEditable) { + break; + } + node = parent; + } + detach(node); + }; - createTreeWalker = (root, whatToShow, filter) => { - const walker = doc.createTreeWalker(root, whatToShow, filter ? { - acceptNode: node => filter(node) ? filterAccept : NodeFilter.FILTER_SKIP - } : null); - walker.previousPONode = () => previousPONode(walker, filter); - return walker; - }, + var 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) : + value; + } + }); + return base; + }; - setAttributes = (node, props) => { - props && Object.entries(props).forEach(([k,v]) => { - if (null == v) { - node.removeAttribute(k); - } else if ("style" === k && typeof v === "object") { - Object.entries(v).forEach(([k,v]) => node.style[k] = v); - } else { - node.setAttribute(k, v); - } - }); - }, + var createBookmarkNodes = () => [ + createElement("INPUT", { + id: startSelectionId, + type: "hidden" + }), + createElement("INPUT", { + id: endSelectionId, + type: "hidden" + }) + ]; - newContentWalker = root => createTreeWalker(root, - SHOW_ELEMENT_OR_TEXT, - node => isTextNode(node) ? notWS.test(node.data) : node.nodeName === "IMG" - ), - - didError = error => console.error(error), - - detachUneditableNode = (node, root) => { - let parent; - while (parent = node.parentNode) { - if (parent === root || parent.isContentEditable) { - break; - } - node = parent; - } - detach(node); - }, - - changeIndentationLevel = direction => (self, event) => { - event.preventDefault(); - self.changeIndentationLevel(direction); - }, - - toggleList = (type, methodIfNotInList) => (self, event) => { - event.preventDefault(); - let parent = self.getSelectionClosest('UL,OL'); - if (type == parent?.nodeName) { - self.removeList(); - } else { - self[methodIfNotInList](); - } - }, - - // TreeIterator Previous node in post-order. - previousPONode = (walker, filter) => { - let current = walker.currentNode, - root = walker.root, - node; - const isAcceptableNode = node => { - const nodeType = node.nodeType; - const nodeFilterType = nodeType === ELEMENT_NODE ? SHOW_ELEMENT : nodeType === TEXT_NODE ? SHOW_TEXT : 0; - return !!(nodeFilterType & walker.whatToShow) && filter(node); - } - while (true) { - if (current === root) { - return null; - } - node = current.previousSibling; - if (node) { - while (current = node.lastChild) { - node = current; - } - } else { - node = current.parentNode; - } - if (!node) { - return null; - } - if (isAcceptableNode(node)) { - walker.currentNode = node; - return node; - } - current = node; - } - }, - - 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) : - value; - } - }); - return base; - }, - - // --- Events --- - - // Subscribing to these events won't automatically add a listener to the - // document node, since these events are fired in a custom manner by the - // editor code. - customEvents = { - pathChange: 1, select: 1, input: 1, undoStateChange: 1 - }, - - // --- Bookmarking --- - - startSelectionId = 'squire-selection-start', - endSelectionId = 'squire-selection-end', - - createBookmarkNodes = () => [ - createElement("INPUT", { - id: startSelectionId, - type: "hidden" - }), - createElement("INPUT", { - id: endSelectionId, - type: "hidden" - }) - ], - - // --- Block formatting --- - - tagAfterSplit = { - DT: "DD", - DD: "DT", - LI: "LI", - PRE: "PRE" - }, - - getListSelection = (range, root) => { - // Get start+end li in single common ancestor - let list = range.commonAncestorContainer; - let startLi = range.startContainer; - let endLi = range.endContainer; - 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]; - } - if (endLi === list) { - endLi = endLi.childNodes[range.endOffset]; - } - while (startLi && startLi.parentNode !== list) { - startLi = startLi.parentNode; - } - while (endLi && endLi.parentNode !== list) { - endLi = endLi.parentNode; - } - return [list, startLi, endLi]; - }, - - makeList = (self, frag, type) => { - let walker = getBlockWalker(frag, self._root), - node, tag, prev, newLi; - - while (node = walker.nextNode()) { - if (node.parentNode.nodeName === "LI") { - node = node.parentNode; - walker.currentNode = node.lastChild; - } - if (node.nodeName !== "LI") { - newLi = createElement("LI"); - if (node.dir) { - newLi.dir = node.dir; - } - - // Have we replaced the previous block with a new