From f33e2315fc7ce93fb60ac1d403ae1331b81075d5 Mon Sep 17 00:00:00 2001 From: Sergey Mosin Date: Wed, 8 May 2024 20:34:43 -0400 Subject: [PATCH 1/3] update squire to v2.2.8 --- plugins/compact-composer/js/squire-raw.js | 787 ++++++++++++---------- 1 file changed, 441 insertions(+), 346 deletions(-) diff --git a/plugins/compact-composer/js/squire-raw.js b/plugins/compact-composer/js/squire-raw.js index 1f079a9a6e..949efa7d7a 100644 --- a/plugins/compact-composer/js/squire-raw.js +++ b/plugins/compact-composer/js/squire-raw.js @@ -1,5 +1,5 @@ -"use strict"; -// v2.2.7 +'use strict'; +// v2.2.8 (() => { // source/node/TreeIterator.ts var SHOW_ELEMENT = 1; @@ -13,11 +13,13 @@ this.nodeType = nodeType; this.filter = filter || always; } + isAcceptableNode(node) { const nodeType = node.nodeType; const nodeFilterType = nodeType === Node.ELEMENT_NODE ? SHOW_ELEMENT : nodeType === Node.TEXT_NODE ? SHOW_TEXT : 0; return !!(nodeFilterType & this.nodeType) && this.filter(node); } + nextNode() { const root = this.root; let current = this.currentNode; @@ -43,6 +45,7 @@ current = node; } } + previousNode() { const root = this.root; let current = this.currentNode; @@ -69,6 +72,7 @@ current = node; } } + // Previous node in post-order. previousPONode() { const root = this.root; @@ -101,7 +105,7 @@ var ELEMENT_NODE = 1; var TEXT_NODE = 3; var DOCUMENT_FRAGMENT_NODE = 11; - var ZWS = "\u200B"; + var ZWS = '\u200B'; var ua = navigator.userAgent; var isMac = /Mac OS X/.test(ua); var isWin = /Windows NT/.test(ua); @@ -110,14 +114,14 @@ var isGecko = /Gecko\//.test(ua); var isLegacyEdge = /Edge\//.test(ua); var isWebKit = !isLegacyEdge && /WebKit\//.test(ua); - var ctrlKey = isMac || isIOS ? "Meta-" : "Ctrl-"; + var ctrlKey = isMac || isIOS ? 'Meta-' : 'Ctrl-'; var cantFocusEmptyTextNodes = isWebKit; - var supportsInputEvents = "onbeforeinput" in document && "inputType" in new InputEvent("input"); + var supportsInputEvents = 'onbeforeinput' in document && 'inputType' in new InputEvent('input'); var notWS = /[^ \t\r\n]/; // source/node/Category.ts var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/; - var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IFRAME", "IMG", "INPUT"]); + var leafNodeNames = /* @__PURE__ */ new Set(['BR', 'HR', 'IFRAME', 'IMG', 'INPUT']); var UNKNOWN = 0; var INLINE = 1; var BLOCK = 2; @@ -191,7 +195,7 @@ return false; } if (node instanceof HTMLElement && node2 instanceof HTMLElement) { - return node.nodeName !== "A" && node.className === node2.className && node.style.cssText === node2.style.cssText; + return node.nodeName !== 'A' && node.className === node2.className && node.style.cssText === node2.style.cssText; } return true; }; @@ -200,7 +204,7 @@ return false; } for (const attr in attributes) { - if (!("getAttribute" in node) || node.getAttribute(attr) !== attributes[attr]) { + if (!('getAttribute' in node) || node.getAttribute(attr) !== attributes[attr]) { return false; } } @@ -269,7 +273,7 @@ // source/node/Whitespace.ts var notWSTextNode = (node) => { - return node instanceof Element ? node.nodeName === "BR" : ( + return node instanceof Element ? node.nodeName === 'BR' : ( // okay if data is 'undefined' here. notWS.test(node.data) ); @@ -359,7 +363,7 @@ while (!(endContainer instanceof Text)) { const child = endContainer.childNodes[endOffset - 1]; if (!child || isLeaf(child)) { - if (child && child.nodeName === "BR" && !isLineBreak(child, false)) { + if (child && child.nodeName === 'BR' && !isLineBreak(child, false)) { endOffset -= 1; continue; } @@ -403,7 +407,7 @@ if (endContainer === endMax || endContainer === root) { break; } - if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === "BR" && !isLineBreak(endContainer.childNodes[endOffset], false)) { + if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === 'BR' && !isLineBreak(endContainer.childNodes[endOffset], false)) { endOffset += 1; } if (endOffset !== getLength(endContainer)) { @@ -447,11 +451,11 @@ if (cantFocusEmptyTextNodes) { fixer = document.createTextNode(ZWS); } else { - fixer = document.createTextNode(""); + fixer = document.createTextNode(''); } } - } else if ((node instanceof Element || node instanceof DocumentFragment) && !node.querySelector("BR")) { - fixer = createElement("BR"); + } else if ((node instanceof Element || node instanceof DocumentFragment) && !node.querySelector('BR')) { + fixer = createElement('BR'); let parent = node; let child; while ((child = parent.lastElementChild) && !isInline(child)) { @@ -470,15 +474,15 @@ var fixContainer = (container, root) => { let wrapper = null; Array.from(container.childNodes).forEach((child) => { - const isBR = child.nodeName === "BR"; + const isBR = child.nodeName === 'BR'; if (!isBR && isInline(child)) { if (!wrapper) { - wrapper = createElement("DIV"); + wrapper = createElement('DIV'); } wrapper.appendChild(child); } else if (isBR || wrapper) { if (!wrapper) { - wrapper = createElement("DIV"); + wrapper = createElement('DIV'); } fixCursor(wrapper); if (isBR) { @@ -499,15 +503,15 @@ }; var split = (node, offset, stopNode, root) => { if (node instanceof Text && node !== stopNode) { - if (typeof offset !== "number") { - throw new Error("Offset must be a number to split text node!"); + 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!"); + 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; + let nodeAfterSplit = typeof offset === 'number' ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset; const parent = node.parentNode; if (!parent || node === stopNode || !(node instanceof Element)) { return nodeAfterSplit; @@ -518,7 +522,7 @@ clone.appendChild(nodeAfterSplit); nodeAfterSplit = next; } - if (node instanceof HTMLOListElement && getNearest(node, root, "BLOCKQUOTE")) { + if (node instanceof HTMLOListElement && getNearest(node, root, 'BLOCKQUOTE')) { clone.start = (+node.start || 1) + node.childNodes.length - 1; } fixCursor(node); @@ -597,7 +601,7 @@ detach(container); offset = block.childNodes.length; const last = block.lastChild; - if (last && last.nodeName === "BR") { + if (last && last.nodeName === 'BR') { block.removeChild(last); offset -= 1; } @@ -609,14 +613,14 @@ var mergeContainers = (node, root) => { const prev = node.previousSibling; const first = node.firstChild; - const isListItem = node.nodeName === "LI"; + const isListItem = node.nodeName === 'LI'; if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) { return; } if (prev && areAlike(prev, node)) { if (!isContainer(prev)) { if (isListItem) { - const block = createElement("DIV"); + const block = createElement('DIV'); block.appendChild(empty(prev)); prev.appendChild(block); } else { @@ -633,7 +637,7 @@ mergeContainers(first, root); } } else if (isListItem) { - const block = createElement("DIV"); + const block = createElement('DIV'); node.insertBefore(block, first); fixCursor(block); } @@ -641,40 +645,40 @@ // source/Clean.ts var styleToSemantic = { - "font-weight": { + 'font-weight': { regexp: /^bold|^700/i, replace() { - return createElement("B"); + return createElement('B'); } }, - "font-style": { + 'font-style': { regexp: /^italic/i, replace() { - return createElement("I"); + return createElement('I'); } }, - "font-family": { + 'font-family': { regexp: notWS, replace(classNames, family) { - return createElement("SPAN", { + return createElement('SPAN', { class: classNames.fontFamily, - style: "font-family:" + family + style: 'font-family:' + family }); } }, - "font-size": { + 'font-size': { regexp: notWS, replace(classNames, size) { - return createElement("SPAN", { + return createElement('SPAN', { class: classNames.fontSize, - style: "font-size:" + size + style: 'font-size:' + size }); } }, - "text-decoration": { + 'text-decoration': { regexp: /^underline/i, replace() { - return createElement("U"); + return createElement('U'); } } }; @@ -724,19 +728,19 @@ }; }; var fontSizes = { - "1": "10", - "2": "13", - "3": "16", - "4": "18", - "5": "24", - "6": "32", - "7": "48" + '1': '10', + '2': '13', + '3': '16', + '4': '18', + '5': '24', + '6': '32', + '7': '48' }; var stylesRewriters = { - STRONG: replaceWithTag("B"), - EM: replaceWithTag("I"), - INS: replaceWithTag("U"), - STRIKE: replaceWithTag("S"), + STRONG: replaceWithTag('B'), + EM: replaceWithTag('I'), + INS: replaceWithTag('U'), + STRIKE: replaceWithTag('S'), SPAN: replaceStyles, FONT: (node, parent, config) => { const font = node; @@ -750,17 +754,17 @@ let newTreeBottom; let newTreeTop; if (face) { - fontSpan = createElement("SPAN", { + fontSpan = createElement('SPAN', { class: classNames.fontFamily, - style: "font-family:" + face + style: 'font-family:' + face }); newTreeTop = fontSpan; newTreeBottom = fontSpan; } if (size) { - sizeSpan = createElement("SPAN", { + sizeSpan = createElement('SPAN', { class: classNames.fontSize, - style: "font-size:" + fontSizes[size] + "px" + style: 'font-size:' + fontSizes[size] + 'px' }); if (!newTreeTop) { newTreeTop = sizeSpan; @@ -771,12 +775,12 @@ newTreeBottom = sizeSpan; } if (color && /^#?([\dA-F]{3}){1,2}$/i.test(color)) { - if (color.charAt(0) !== "#") { - color = "#" + color; + if (color.charAt(0) !== '#') { + color = '#' + color; } - colorSpan = createElement("SPAN", { + colorSpan = createElement('SPAN', { class: classNames.color, - style: "color:" + color + style: 'color:' + color }); if (!newTreeTop) { newTreeTop = colorSpan; @@ -787,14 +791,14 @@ newTreeBottom = colorSpan; } if (!newTreeTop || !newTreeBottom) { - newTreeTop = newTreeBottom = createElement("SPAN"); + newTreeTop = newTreeBottom = createElement('SPAN'); } parent.replaceChild(newTreeTop, font); newTreeBottom.appendChild(empty(font)); return newTreeBottom; }, TT: (node, parent, config) => { - const el = createElement("SPAN", { + const el = createElement('SPAN', { class: config.classNames.fontFamily, style: 'font-family:menlo,consolas,"courier new",monospace' }); @@ -835,7 +839,7 @@ continue; } if (childLength) { - cleanTree(child, config, preserveWS || nodeName === "PRE"); + cleanTree(child, config, preserveWS || nodeName === 'PRE'); } } else { if (child instanceof Text) { @@ -849,7 +853,7 @@ walker.currentNode = child; let sibling; while (sibling = walker.previousPONode()) { - if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { + if (sibling.nodeName === 'IMG' || sibling instanceof Text && notWS.test(sibling.data)) { break; } if (!isInline(sibling)) { @@ -857,13 +861,13 @@ break; } } - data = data.replace(/^[ \t\r\n]+/g, sibling ? " " : ""); + data = data.replace(/^[ \t\r\n]+/g, sibling ? ' ' : ''); } if (endsWithWS) { walker.currentNode = child; let sibling; while (sibling = walker.nextNode()) { - if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { + if (sibling.nodeName === 'IMG' || sibling instanceof Text && notWS.test(sibling.data)) { break; } if (!isInline(sibling)) { @@ -871,7 +875,7 @@ break; } } - data = data.replace(/[ \t\r\n]+$/g, sibling ? " " : ""); + data = data.replace(/[ \t\r\n]+$/g, sibling ? ' ' : ''); } if (data) { child.data = data; @@ -901,7 +905,7 @@ } }; var cleanupBRs = (node, root, keepForBlankLine) => { - const brs = node.querySelectorAll("BR"); + const brs = node.querySelectorAll('BR'); const brBreaksLine = []; let l = brs.length; for (let i = 0; i < l; i += 1) { @@ -921,7 +925,7 @@ } }; var escapeHTML = (text) => { - return text.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join("""); + return text.split('&').join('&').split('<').join('<').split('>').join('>').split('"').join('"'); }; // source/node/Block.ts @@ -939,7 +943,7 @@ return block !== root ? block : null; }; var isEmptyBlock = (block) => { - return !block.textContent && !block.querySelector("IMG"); + return !block.textContent && !block.querySelector('IMG'); }; // source/range/Block.ts @@ -977,7 +981,7 @@ return block && isNodeContainedInRange(range, block, true) ? block : null; }; var isContent = (node) => { - return node instanceof Text ? notWS.test(node.data) : node.nodeName === "IMG"; + return node instanceof Text ? notWS.test(node.data) : node.nodeName === 'IMG'; }; var rangeDoesStartAtBlockBoundary = (range, root) => { const startContainer = range.startContainer; @@ -1059,13 +1063,14 @@ function createRange(startContainer, startOffset, endContainer, endOffset) { const range = document.createRange(); range.setStart(startContainer, startOffset); - if (endContainer && typeof endOffset === "number") { + 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; @@ -1178,7 +1183,7 @@ fixCursor(startBlock); } const child = root.firstChild; - if (!child || child.nodeName === "BR") { + if (!child || child.nodeName === 'BR') { fixCursor(root); if (root.firstChild) { range.selectNodeContents(root.firstChild); @@ -1191,7 +1196,7 @@ let afterNode = startContainer; let afterOffset = startOffset; if (!(afterNode instanceof Text) || afterOffset === afterNode.data.length) { - afterNode = getAdjacentInlineNode(iterator, "nextNode", afterNode); + afterNode = getAdjacentInlineNode(iterator, 'nextNode', afterNode); afterOffset = 0; } let beforeNode = startContainer; @@ -1199,7 +1204,7 @@ if (!(beforeNode instanceof Text) || beforeOffset === -1) { beforeNode = getAdjacentInlineNode( iterator, - "previousPONode", + 'previousPONode', afterNode || (startContainer instanceof Text ? startContainer : startContainer.childNodes[startOffset] || startContainer) ); if (beforeNode instanceof Text) { @@ -1208,17 +1213,17 @@ } let node = null; let offset = 0; - if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === " " && rangeDoesStartAtBlockBoundary(range, root)) { + if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === ' ' && rangeDoesStartAtBlockBoundary(range, root)) { node = afterNode; offset = afterOffset; - } else if (beforeNode instanceof Text && beforeNode.data.charAt(beforeOffset) === " ") { - if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === " " || rangeDoesEndAtBlockBoundary(range, root)) { + } else if (beforeNode instanceof Text && beforeNode.data.charAt(beforeOffset) === ' ') { + if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === ' ' || rangeDoesEndAtBlockBoundary(range, root)) { node = beforeNode; offset = beforeOffset; } } if (node) { - node.replaceData(offset, 1, "\xA0"); + node.replaceData(offset, 1, '\xA0'); } range.setStart(startContainer, startOffset); range.collapse(true); @@ -1237,13 +1242,13 @@ } moveRangeBoundariesDownTree(range); range.collapse(false); - const stopPoint = getNearest(range.endContainer, root, "BLOCKQUOTE") || root; + const stopPoint = getNearest(range.endContainer, root, 'BLOCKQUOTE') || root; let block = getStartBlockOfRange(range, root); let blockContentsAfterSplit = null; const firstBlockInFrag = getNextBlock(frag, frag); const replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock(block); if (block && firstBlockInFrag && !replaceBlock && // Don't merge table cells or PRE elements into block - !getNearest(firstBlockInFrag, frag, "PRE") && !getNearest(firstBlockInFrag, frag, "TABLE")) { + !getNearest(firstBlockInFrag, frag, 'PRE') && !getNearest(firstBlockInFrag, frag, 'TABLE')) { moveRangeBoundariesUpTree(range, block, block, root); range.collapse(true); let container = range.endContainer; @@ -1322,7 +1327,7 @@ // source/range/Contents.ts var getTextContentsOfRange = (range) => { if (range.collapsed) { - return ""; + return ''; } const startContainer = range.startContainer; const endContainer = range.endContainer; @@ -1335,7 +1340,7 @@ ); walker.currentNode = startContainer; let node = startContainer; - let textContent = ""; + let textContent = ''; let addedTextInBlock = false; let value; if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) { @@ -1354,13 +1359,13 @@ textContent += value; addedTextInBlock = true; } - } else if (node.nodeName === "BR" || addedTextInBlock && !isInline(node)) { - textContent += "\n"; + } else if (node.nodeName === 'BR' || addedTextInBlock && !isInline(node)) { + textContent += '\n'; addedTextInBlock = false; } node = walker.nextNode(); } - textContent = textContent.replace(/ /g, " "); + textContent = textContent.replace(/ /g, ' '); return textContent; }; @@ -1371,7 +1376,7 @@ if (isLegacyEdge || !clipboardData) { return false; } - let text = toPlainText ? "" : getTextContentsOfRange(range); + let text = toPlainText ? '' : getTextContentsOfRange(range); const startBlock = getStartBlockOfRange(range, root); const endBlock = getEndBlockOfRange(range, root); let copyRoot = root; @@ -1399,10 +1404,10 @@ } let html; if (contents.childNodes.length === 1 && contents.childNodes[0] instanceof Text) { - text = contents.childNodes[0].data.replace(/ /g, " "); + text = contents.childNodes[0].data.replace(/ /g, ' '); plainTextOnly = true; } else { - const node = createElement("DIV"); + const node = createElement('DIV'); node.appendChild(contents); html = node.innerHTML; if (toCleanHTML) { @@ -1413,12 +1418,12 @@ text = toPlainText(html); } if (isWin) { - text = text.replace(/\r?\n/g, "\r\n"); + text = text.replace(/\r?\n/g, '\r\n'); } if (!plainTextOnly && html && text !== html) { - clipboardData.setData("text/html", html); + clipboardData.setData('text/html', html); } - clipboardData.setData("text/plain", text); + clipboardData.setData('text/plain', text); event.preventDefault(); return true; }; @@ -1477,11 +1482,11 @@ while (l--) { const item = items[l]; const type = item.type; - if (type === "text/html") { + if (type === 'text/html') { htmlItem = item; - } else if (type === "text/plain" || type === "text/uri-list") { + } else if (type === 'text/plain' || type === 'text/uri-list') { plainItem = item; - } else if (type === "text/rtf") { + } else if (type === 'text/rtf') { hasRTF = true; } else if (/^image\/.*/.test(type)) { hasImage = true; @@ -1489,7 +1494,7 @@ } if (hasImage && !(hasRTF && htmlItem)) { event.preventDefault(); - this.fireEvent("pasteImage", { + this.fireEvent('pasteImage', { clipboardData }); return; @@ -1519,12 +1524,12 @@ } } const types = clipboardData == null ? void 0 : clipboardData.types; - if (!isLegacyEdge && types && (indexOf.call(types, "text/html") > -1 || !isGecko && indexOf.call(types, "text/plain") > -1 && indexOf.call(types, "text/rtf") < 0)) { + if (!isLegacyEdge && types && (indexOf.call(types, 'text/html') > -1 || !isGecko && indexOf.call(types, 'text/plain') > -1 && indexOf.call(types, 'text/rtf') < 0)) { event.preventDefault(); let data; - if (!choosePlain && (data = clipboardData.getData("text/html"))) { + if (!choosePlain && (data = clipboardData.getData('text/html'))) { this.insertHTML(data, true); - } else if ((data = clipboardData.getData("text/plain")) || (data = clipboardData.getData("text/uri-list"))) { + } else if ((data = clipboardData.getData('text/plain')) || (data = clipboardData.getData('text/uri-list'))) { this.insertPlainText(data, true); } return; @@ -1535,16 +1540,16 @@ const startOffset = range.startOffset; const endContainer = range.endContainer; const endOffset = range.endOffset; - let pasteArea = createElement("DIV", { - contenteditable: "true", - style: "position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;" + let pasteArea = createElement('DIV', { + contenteditable: 'true', + style: 'position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;' }); body.appendChild(pasteArea); range.selectNodeContents(pasteArea); this.setSelection(range); setTimeout(() => { try { - let html = ""; + let html = ''; let next = pasteArea; let first; while (pasteArea = next) { @@ -1582,10 +1587,10 @@ let hasHTML = false; while (l--) { switch (types[l]) { - case "text/plain": + case 'text/plain': hasPlain = true; break; - case "text/html": + case 'text/html': hasHTML = true; break; default: @@ -1631,7 +1636,7 @@ fixCursor(parent); moveRangeBoundariesDownTree(range); } - if (node === self._root && (node = node.firstChild) && node.nodeName === "BR") { + if (node === self._root && (node = node.firstChild) && node.nodeName === 'BR') { detach(node); } self._ensureBottomLine(); @@ -1652,13 +1657,13 @@ detach(node); }; var linkifyText = (self, textNode, offset) => { - if (getNearest(textNode, self._root, "A")) { + if (getNearest(textNode, self._root, 'A')) { return; } - const data = textNode.data || ""; + const data = textNode.data || ''; const searchFrom = Math.max( - data.lastIndexOf(" ", offset - 1), - data.lastIndexOf("\xA0", offset - 1) + data.lastIndexOf(' ', offset - 1), + data.lastIndexOf('\xA0', offset - 1) ) + 1; const searchText = data.slice(searchFrom, offset); const match = self.linkRegExp.exec(searchText); @@ -1676,10 +1681,10 @@ } const defaultAttributes = self._config.tagAttributes.a; const link = createElement( - "A", + 'A', Object.assign( { - href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0] + href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : 'http://' + match[1] : 'mailto:' + match[0] }, defaultAttributes ) @@ -1728,10 +1733,10 @@ } self.setSelection(range); } else if (current) { - if (getNearest(current, root, "UL") || getNearest(current, root, "OL")) { + if (getNearest(current, root, 'UL') || getNearest(current, root, 'OL')) { self.decreaseListLevel(range); return; - } else if (getNearest(current, root, "BLOCKQUOTE")) { + } else if (getNearest(current, root, 'BLOCKQUOTE')) { self.removeQuote(range); return; } @@ -1803,7 +1808,7 @@ cursorOffset = range.endOffset; if (cursorContainer instanceof Element) { nodeAfterCursor = cursorContainer.childNodes[cursorOffset]; - if (nodeAfterCursor && nodeAfterCursor.nodeName === "IMG") { + if (nodeAfterCursor && nodeAfterCursor.nodeName === 'IMG') { event.preventDefault(); detach(nodeAfterCursor); moveRangeBoundariesDownTree(range); @@ -1826,7 +1831,7 @@ let node = getStartBlockOfRange(range, root); let parent; while (parent = node.parentNode) { - if (parent.nodeName === "UL" || parent.nodeName === "OL") { + if (parent.nodeName === 'UL' || parent.nodeName === 'OL') { event.preventDefault(); self.increaseListLevel(range); break; @@ -1840,7 +1845,7 @@ self._removeZWS(); if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) { const node = range.startContainer; - if (getNearest(node, root, "UL") || getNearest(node, root, "OL")) { + if (getNearest(node, root, 'UL') || getNearest(node, root, 'OL')) { event.preventDefault(); self.decreaseListLevel(range); } @@ -1861,11 +1866,11 @@ 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.") { + if (block && block.nodeName !== 'PRE') { + const text = (_a = block.textContent) == null ? void 0 : _a.trimEnd().replace(ZWS, ''); + if (text === '*' || text === '1.') { event.preventDefault(); - self.insertPlainText(" ", false); + self.insertPlainText(' ', false); self._docWasChanged(); self.saveUndoState(range); const walker = new TreeIterator(block, SHOW_TEXT); @@ -1873,7 +1878,7 @@ while (textNode = walker.nextNode()) { detach(textNode); } - if (text === "*") { + if (text === '*') { self.makeUnorderedList(); } else { self.makeOrderedList(); @@ -1885,7 +1890,7 @@ node = range.endContainer; if (range.endOffset === getLength(node)) { do { - if (node.nodeName === "A") { + if (node.nodeName === 'A') { range.setStartAfter(node); break; } @@ -1909,23 +1914,27 @@ return; } let key = event.key; - let modifiers = ""; - if (key !== "Backspace" && key !== "Delete") { + let modifiers = ''; + const code = event.code; + if (/^Digit\d$/.test(code)) { + key = code.slice(-1); + } + if (key !== 'Backspace' && key !== 'Delete') { if (event.altKey) { - modifiers += "Alt-"; + modifiers += 'Alt-'; } if (event.ctrlKey) { - modifiers += "Ctrl-"; + modifiers += 'Ctrl-'; } if (event.metaKey) { - modifiers += "Meta-"; + modifiers += 'Meta-'; } if (event.shiftKey) { - modifiers += "Shift-"; + modifiers += 'Shift-'; } } - if (isWin && event.shiftKey && key === "Delete") { - modifiers += "Shift-"; + if (isWin && event.shiftKey && key === 'Delete') { + modifiers += 'Shift-'; } key = modifiers + key; const range = this.getSelection(); @@ -1940,25 +1949,25 @@ } }; var keyHandlers = { - "Backspace": Backspace, - "Delete": Delete, - "Tab": Tab, - "Shift-Tab": ShiftTab, - " ": Space, - "ArrowLeft"(self) { + 'Backspace': Backspace, + 'Delete': Delete, + 'Tab': Tab, + 'Shift-Tab': ShiftTab, + ' ': Space, + 'ArrowLeft'(self) { self._removeZWS(); }, - "ArrowRight"(self, event, range) { + 'ArrowRight'(self, event, range) { self._removeZWS(); const root = self.getRoot(); if (rangeDoesEndAtBlockBoundary(range, root)) { moveRangeBoundariesDownTree(range); let node = range.endContainer; do { - if (node.nodeName === "CODE") { + if (node.nodeName === 'CODE') { let next = node.nextSibling; if (!(next instanceof Text)) { - const textNode = document.createTextNode("\xA0"); + const textNode = document.createTextNode('\xA0'); node.parentNode.insertBefore(textNode, next); next = textNode; } @@ -1973,7 +1982,7 @@ }; if (!supportsInputEvents) { keyHandlers.Enter = Enter; - keyHandlers["Shift-Enter"] = Enter; + keyHandlers['Shift-Enter'] = Enter; } if (!isMac && !isIOS) { keyHandlers.PageUp = (self) => { @@ -1995,13 +2004,13 @@ } }; }; - keyHandlers[ctrlKey + "b"] = mapKeyToFormat("B"); - keyHandlers[ctrlKey + "i"] = mapKeyToFormat("I"); - keyHandlers[ctrlKey + "u"] = mapKeyToFormat("U"); - keyHandlers[ctrlKey + "Shift-7"] = mapKeyToFormat("S"); - keyHandlers[ctrlKey + "Shift-5"] = mapKeyToFormat("SUB", { tag: "SUP" }); - keyHandlers[ctrlKey + "Shift-6"] = mapKeyToFormat("SUP", { tag: "SUB" }); - keyHandlers[ctrlKey + "Shift-8"] = (self, event) => { + keyHandlers[ctrlKey + 'b'] = mapKeyToFormat('B'); + keyHandlers[ctrlKey + 'i'] = mapKeyToFormat('I'); + keyHandlers[ctrlKey + 'u'] = mapKeyToFormat('U'); + keyHandlers[ctrlKey + 'Shift-7'] = mapKeyToFormat('S'); + keyHandlers[ctrlKey + 'Shift-5'] = mapKeyToFormat('SUB', { tag: 'SUP' }); + keyHandlers[ctrlKey + 'Shift-6'] = mapKeyToFormat('SUP', { tag: 'SUB' }); + keyHandlers[ctrlKey + 'Shift-8'] = (self, event) => { event.preventDefault(); const path = self.getPath(); if (!/(?:^|>)UL/.test(path)) { @@ -2010,7 +2019,7 @@ self.removeList(); } }; - keyHandlers[ctrlKey + "Shift-9"] = (self, event) => { + keyHandlers[ctrlKey + 'Shift-9'] = (self, event) => { event.preventDefault(); const path = self.getPath(); if (!/(?:^|>)OL/.test(path)) { @@ -2019,7 +2028,7 @@ self.removeList(); } }; - keyHandlers[ctrlKey + "["] = (self, event) => { + keyHandlers[ctrlKey + '['] = (self, event) => { event.preventDefault(); const path = self.getPath(); if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) { @@ -2028,7 +2037,7 @@ self.decreaseListLevel(); } }; - keyHandlers[ctrlKey + "]"] = (self, event) => { + keyHandlers[ctrlKey + ']'] = (self, event) => { event.preventDefault(); const path = self.getPath(); if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) { @@ -2037,18 +2046,21 @@ self.increaseListLevel(); } }; - keyHandlers[ctrlKey + "d"] = (self, event) => { + keyHandlers[ctrlKey + 'd'] = (self, event) => { event.preventDefault(); self.toggleCode(); }; - keyHandlers[ctrlKey + "z"] = (self, event) => { + keyHandlers[ctrlKey + 'z'] = (self, event) => { event.preventDefault(); self.undo(); }; - keyHandlers[ctrlKey + "y"] = keyHandlers[ctrlKey + "Shift-z"] = (self, event) => { - event.preventDefault(); - self.redo(); - }; + keyHandlers[ctrlKey + 'y'] = // Depending on platform, the Shift may cause the key to come through as + // upper case, but sometimes not. Just add both as shortcuts — the browser + // will only ever fire one or the other. + keyHandlers[ctrlKey + 'Shift-z'] = keyHandlers[ctrlKey + 'Shift-Z'] = (self, event) => { + event.preventDefault(); + self.redo(); + }; // source/Editor.ts var Squire = class { @@ -2059,15 +2071,15 @@ * editor code. */ this.customEvents = /* @__PURE__ */ new Set([ - "pathChange", - "select", - "input", - "pasteImage", - "undoStateChange" + 'pathChange', + 'select', + 'input', + 'pasteImage', + 'undoStateChange' ]); // --- - this.startSelectionId = "squire-selection-start"; - this.endSelectionId = "squire-selection-end"; + this.startSelectionId = 'squire-selection-start'; + this.endSelectionId = 'squire-selection-end'; /* linkRegExp = new RegExp( // Only look on boundaries @@ -2121,10 +2133,10 @@ */ this.linkRegExp = /\b(?:((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9][a-z0-9.\-]*[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:[^\s?&`!()\[\]{};:'".,<>«»“”‘’]|\([^\s()<>]+\)))|([\w\-.%+]+@(?:[\w\-]+\.)+[a-z]{2,}\b(?:[?][^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+(?:&[^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+)*)?))/i; this.tagAfterSplit = { - DT: "DD", - DD: "DT", - LI: "LI", - PRE: "PRE" + DT: 'DD', + DD: 'DT', + LI: 'LI', + PRE: 'PRE' }; this._root = root; this._config = this._makeConfig(config); @@ -2134,7 +2146,7 @@ this._mayHaveZWS = false; this._lastAnchorNode = null; this._lastFocusNode = null; - this._path = ""; + this._path = ''; this._events = /* @__PURE__ */ new Map(); this._undoIndex = -1; this._undoStack = []; @@ -2142,22 +2154,22 @@ this._isInUndoState = false; this._ignoreChange = false; this._ignoreAllChanges = false; - this.addEventListener("selectionchange", this._updatePathOnEvent); - this.addEventListener("blur", this._enableRestoreSelection); - this.addEventListener("mousedown", this._disableRestoreSelection); - this.addEventListener("touchstart", this._disableRestoreSelection); - this.addEventListener("focus", this._restoreSelection); + this.addEventListener('selectionchange', this._updatePathOnEvent); + this.addEventListener('blur', this._enableRestoreSelection); + this.addEventListener('mousedown', this._disableRestoreSelection); + this.addEventListener('touchstart', this._disableRestoreSelection); + this.addEventListener('focus', this._restoreSelection); this._isShiftDown = false; - this.addEventListener("cut", _onCut); - this.addEventListener("copy", _onCopy); - this.addEventListener("paste", _onPaste); - this.addEventListener("drop", _onDrop); + this.addEventListener('cut', _onCut); + this.addEventListener('copy', _onCopy); + this.addEventListener('paste', _onPaste); + this.addEventListener('drop', _onDrop); this.addEventListener( - "keydown", + 'keydown', _monitorShiftKey ); - this.addEventListener("keyup", _monitorShiftKey); - this.addEventListener("keydown", _onKey); + this.addEventListener('keyup', _monitorShiftKey); + this.addEventListener('keydown', _onKey); this._keyHandlers = Object.create(keyHandlers); const mutation = new MutationObserver(() => this._docWasChanged()); mutation.observe(root, { @@ -2167,13 +2179,14 @@ subtree: true }); this._mutation = mutation; - root.setAttribute("contenteditable", "true"); + root.setAttribute('contenteditable', 'true'); this.addEventListener( - "beforeinput", + 'beforeinput', this._beforeInput ); - this.setHTML(""); + this.setHTML(''); } + destroy() { this._events.forEach((_, type) => { this.removeEventListener(type); @@ -2183,16 +2196,17 @@ this._undoStack = []; this._undoStackLength = 0; } + _makeConfig(userConfig) { const config = { - blockTag: "DIV", + blockTag: 'DIV', blockAttributes: null, tagAttributes: {}, classNames: { - color: "color", - fontFamily: "font", - fontSize: "size", - highlight: "highlight" + color: 'color', + fontFamily: 'font', + fontSize: 'size', + highlight: 'highlight' }, undo: { documentSizeThreshold: -1, @@ -2221,113 +2235,112 @@ } return config; } + setKeyHandler(key, fn) { this._keyHandlers[key] = fn; return this; } + _beforeInput(event) { switch (event.inputType) { - case "insertText": - if (isAndroid && event.data && event.data.includes("\n")) { - event.preventDefault(); - } - break; - case "insertLineBreak": + case 'insertLineBreak': event.preventDefault(); this.splitBlock(true); break; - case "insertParagraph": + case 'insertParagraph': event.preventDefault(); this.splitBlock(false); break; - case "insertOrderedList": + case 'insertOrderedList': event.preventDefault(); this.makeOrderedList(); break; - case "insertUnoderedList": + case 'insertUnoderedList': event.preventDefault(); this.makeUnorderedList(); break; - case "historyUndo": + case 'historyUndo': event.preventDefault(); this.undo(); break; - case "historyRedo": + case 'historyRedo': event.preventDefault(); this.redo(); break; - case "formatBold": + case 'formatBold': event.preventDefault(); this.bold(); break; - case "formaItalic": + case 'formaItalic': event.preventDefault(); this.italic(); break; - case "formatUnderline": + case 'formatUnderline': event.preventDefault(); this.underline(); break; - case "formatStrikeThrough": + case 'formatStrikeThrough': event.preventDefault(); this.strikethrough(); break; - case "formatSuperscript": + case 'formatSuperscript': event.preventDefault(); this.superscript(); break; - case "formatSubscript": + case 'formatSubscript': event.preventDefault(); this.subscript(); break; - case "formatJustifyFull": - case "formatJustifyCenter": - case "formatJustifyRight": - case "formatJustifyLeft": { + case 'formatJustifyFull': + case 'formatJustifyCenter': + case 'formatJustifyRight': + case 'formatJustifyLeft': { event.preventDefault(); let alignment = event.inputType.slice(13).toLowerCase(); - if (alignment === "full") { - alignment = "justify"; + if (alignment === 'full') { + alignment = 'justify'; } this.setTextAlignment(alignment); break; } - case "formatRemove": + case 'formatRemove': event.preventDefault(); this.removeAllFormatting(); break; - case "formatSetBlockTextDirection": { + case 'formatSetBlockTextDirection': { event.preventDefault(); let dir = event.data; - if (dir === "null") { + if (dir === 'null') { dir = null; } this.setTextDirection(dir); break; } - case "formatBackColor": + case 'formatBackColor': event.preventDefault(); this.setHighlightColor(event.data); break; - case "formatFontColor": + case 'formatFontColor': event.preventDefault(); this.setTextColor(event.data); break; - case "formatFontName": + case 'formatFontName': event.preventDefault(); this.setFontFace(event.data); break; } } + // --- Events handleEvent(event) { this.fireEvent(event.type, event); } + fireEvent(type, detail) { let handlers = this._events.get(type); if (/^(?:focus|blur)/.test(type)) { const isFocused = this._root === document.activeElement; - if (type === "focus") { + if (type === 'focus') { if (!isFocused || this._isFocused) { return this; } @@ -2346,7 +2359,7 @@ handlers = handlers.slice(); for (const handler of handlers) { try { - if ("handleEvent" in handler) { + if ('handleEvent' in handler) { handler.handleEvent(event); } else { handler.call(this, event); @@ -2358,6 +2371,7 @@ } return this; } + addEventListener(type, fn) { let handlers = this._events.get(type); let target = this._root; @@ -2365,7 +2379,7 @@ handlers = []; this._events.set(type, handlers); if (!this.customEvents.has(type)) { - if (type === "selectionchange") { + if (type === 'selectionchange') { target = document; } target.addEventListener(type, this, true); @@ -2374,6 +2388,7 @@ handlers.push(fn); return this; } + removeEventListener(type, fn) { const handlers = this._events.get(type); let target = this._root; @@ -2391,7 +2406,7 @@ if (!handlers.length) { this._events.delete(type); if (!this.customEvents.has(type)) { - if (type === "selectionchange") { + if (type === 'selectionchange') { target = document; } target.removeEventListener(type, this, true); @@ -2400,27 +2415,33 @@ } return this; } + // --- Focus focus() { this._root.focus({ preventScroll: true }); return this; } + blur() { this._root.blur(); return this; } + // --- Selection and bookmarking _enableRestoreSelection() { this._willRestoreSelection = true; } + _disableRestoreSelection() { this._willRestoreSelection = false; } + _restoreSelection() { if (this._willRestoreSelection) { this.setSelection(this._lastSelection); } } + // --- _removeZWS() { if (!this._mayHaveZWS) { @@ -2429,14 +2450,15 @@ removeZWS(this._root); this._mayHaveZWS = false; } + _saveRangeToBookmark(range) { - let startNode = createElement("INPUT", { + let startNode = createElement('INPUT', { id: this.startSelectionId, - type: "hidden" + type: 'hidden' }); - let endNode = createElement("INPUT", { + let endNode = createElement('INPUT', { id: this.endSelectionId, - type: "hidden" + type: 'hidden' }); let temp; insertNodeInRange(range, startNode); @@ -2452,10 +2474,11 @@ range.setStartAfter(startNode); range.setEndBefore(endNode); } + _getRangeAndRemoveBookmark(range) { const root = this._root; - const start = root.querySelector("#" + this.startSelectionId); - const end = root.querySelector("#" + this.endSelectionId); + const start = root.querySelector('#' + this.startSelectionId); + const end = root.querySelector('#' + this.endSelectionId); if (start && end) { let startContainer = start.parentNode; let endContainer = end.parentNode; @@ -2493,6 +2516,7 @@ } return range || null; } + getSelection() { const selection = window.getSelection(); const root = this._root; @@ -2521,6 +2545,7 @@ } return range; } + setSelection(range) { this._lastSelection = range; if (!this._isFocused) { @@ -2528,7 +2553,7 @@ } else { const selection = window.getSelection(); if (selection) { - if ("setBaseAndExtent" in Selection.prototype) { + if ('setBaseAndExtent' in Selection.prototype) { selection.setBaseAndExtent( range.startContainer, range.startOffset, @@ -2543,6 +2568,7 @@ } return this; } + // --- _moveCursorTo(toStart) { const root = this._root; @@ -2551,19 +2577,22 @@ this.setSelection(range); return this; } + moveCursorToStart() { return this._moveCursorTo(true); } + moveCursorToEnd() { return this._moveCursorTo(false); } + // --- getCursorPosition() { const range = this.getSelection(); let rect = range.getBoundingClientRect(); if (rect && !rect.top) { this._ignoreChange = true; - const node = createElement("SPAN"); + const node = createElement('SPAN'); node.textContent = ZWS; insertNodeInRange(range, node); rect = node.getBoundingClientRect(); @@ -2573,15 +2602,18 @@ } return rect; } + // --- Path getPath() { return this._path; } + _updatePathOnEvent() { if (this._isFocused) { this._updatePath(this.getSelection()); } } + _updatePath(range, force) { const anchor = range.startContainer; const focus = range.endContainer; @@ -2589,58 +2621,60 @@ if (force || anchor !== this._lastAnchorNode || focus !== this._lastFocusNode) { this._lastAnchorNode = anchor; this._lastFocusNode = focus; - newPath = anchor && focus ? anchor === focus ? this._getPath(focus) : "(selection)" : ""; + newPath = anchor && focus ? anchor === focus ? this._getPath(focus) : '(selection)' : ''; if (this._path !== newPath) { this._path = newPath; - this.fireEvent("pathChange", { + this.fireEvent('pathChange', { path: newPath }); } } - this.fireEvent(range.collapsed ? "cursor" : "select", { + this.fireEvent(range.collapsed ? 'cursor' : 'select', { range }); } + _getPath(node) { const root = this._root; const config = this._config; - let path = ""; + let path = ''; if (node && node !== root) { const parent = node.parentNode; - path = parent ? this._getPath(parent) : ""; + path = parent ? this._getPath(parent) : ''; if (node instanceof HTMLElement) { const id = node.id; const classList = node.classList; const classNames = Array.from(classList).sort(); const dir = node.dir; const styleNames = config.classNames; - path += (path ? ">" : "") + node.nodeName; + path += (path ? '>' : '') + node.nodeName; if (id) { - path += "#" + id; + path += '#' + id; } if (classNames.length) { - path += "."; - path += classNames.join("."); + path += '.'; + path += classNames.join('.'); } if (dir) { - path += "[dir=" + dir + "]"; + path += '[dir=' + dir + ']'; } if (classList.contains(styleNames.highlight)) { - path += "[backgroundColor=" + node.style.backgroundColor.replace(/ /g, "") + "]"; + path += '[backgroundColor=' + node.style.backgroundColor.replace(/ /g, '') + ']'; } if (classList.contains(styleNames.color)) { - path += "[color=" + node.style.color.replace(/ /g, "") + "]"; + path += '[color=' + node.style.color.replace(/ /g, '') + ']'; } if (classList.contains(styleNames.fontFamily)) { - path += "[fontFamily=" + node.style.fontFamily.replace(/ /g, "") + "]"; + path += '[fontFamily=' + node.style.fontFamily.replace(/ /g, '') + ']'; } if (classList.contains(styleNames.fontSize)) { - path += "[fontSize=" + node.style.fontSize + "]"; + path += '[fontSize=' + node.style.fontSize + ']'; } } } return path; } + // --- History modifyDocument(modificationFn) { const mutation = this._mutation; @@ -2664,6 +2698,7 @@ } return this; } + _docWasChanged() { resetNodeCategoryCache(); this._mayHaveZWS = true; @@ -2676,13 +2711,14 @@ } if (this._isInUndoState) { this._isInUndoState = false; - this.fireEvent("undoStateChange", { + this.fireEvent('undoStateChange', { canUndo: true, canRedo: false }); } - this.fireEvent("input"); + this.fireEvent('input'); } + /** * Leaves bookmark. */ @@ -2721,6 +2757,7 @@ } return this; } + saveUndoState(range) { if (!range) { range = this.getSelection(); @@ -2729,6 +2766,7 @@ this._getRangeAndRemoveBookmark(range); return this; } + undo() { if (this._undoIndex !== 0 || !this._isInUndoState) { this._recordUndoState(this.getSelection(), false); @@ -2739,14 +2777,15 @@ this.setSelection(range); } this._isInUndoState = true; - this.fireEvent("undoStateChange", { + this.fireEvent('undoStateChange', { canUndo: this._undoIndex !== 0, canRedo: true }); - this.fireEvent("input"); + this.fireEvent('input'); } return this.focus(); } + redo() { const undoIndex = this._undoIndex; const undoStackLength = this._undoStackLength; @@ -2757,27 +2796,30 @@ if (range) { this.setSelection(range); } - this.fireEvent("undoStateChange", { + this.fireEvent('undoStateChange', { canUndo: true, canRedo: undoIndex + 2 < undoStackLength }); - this.fireEvent("input"); + this.fireEvent('input'); } return this.focus(); } + // --- Get and set data getRoot() { return this._root; } + _getRawHTML() { return this._root.innerHTML; } + _setRawHTML(html) { const root = this._root; root.innerHTML = html; let node = root; const child = node.firstChild; - if (!child || child.nodeName === "BR") { + if (!child || child.nodeName === 'BR') { const block = this.createDefaultBlock(); if (child) { node.replaceChild(block, child); @@ -2792,18 +2834,20 @@ this._ignoreChange = true; return this; } + getHTML(withBookmark) { let range; if (withBookmark) { range = this.getSelection(); this._saveRangeToBookmark(range); } - const html = this._getRawHTML().replace(/\u200B/g, ""); + const html = this._getRawHTML().replace(/\u200B/g, ''); if (withBookmark) { this._getRangeAndRemoveBookmark(range); } return html; } + setHTML(html) { const frag = this._config.sanitizeToDOMFragment(html, this); const root = this._root; @@ -2812,7 +2856,7 @@ fixContainer(frag, root); let node = frag; let child = node.firstChild; - if (!child || child.nodeName === "BR") { + if (!child || child.nodeName === 'BR') { const block = this.createDefaultBlock(); if (child) { node.replaceChild(block, child); @@ -2839,6 +2883,7 @@ this._updatePath(range, true); return this; } + /** * Insert HTML at the cursor location. If the selection is not collapsed * insertTreeFragmentIntoRange will delete the selection so that it is @@ -2864,20 +2909,20 @@ } let doInsert = true; if (isPaste) { - const event = new CustomEvent("willPaste", { + const event = new CustomEvent('willPaste', { cancelable: true, detail: { fragment: frag } }); - this.fireEvent("willPaste", event); + this.fireEvent('willPaste', event); frag = event.detail.fragment; doInsert = !event.defaultPrevented; } if (doInsert) { insertTreeFragmentIntoRange(range, frag, root); range.collapse(false); - moveRangeBoundaryOutOf(range, "A", root); + moveRangeBoundaryOutOf(range, 'A', root); this._ensureBottomLine(); } this.setSelection(range); @@ -2890,6 +2935,7 @@ } return this; } + insertElement(el, range) { if (!range) { range = this.getSelection(); @@ -2933,9 +2979,10 @@ this._updatePath(range); return this; } + insertImage(src, attributes) { const img = createElement( - "IMG", + 'IMG', Object.assign( { src @@ -2946,14 +2993,15 @@ this.insertElement(img); return img; } + insertPlainText(plainText, isPaste) { const range = this.getSelection(); - if (range.collapsed && getNearest(range.startContainer, this._root, "PRE")) { + if (range.collapsed && getNearest(range.startContainer, this._root, 'PRE')) { const startContainer = range.startContainer; let offset = range.startOffset; let textNode; if (!startContainer || !(startContainer instanceof Text)) { - const text = document.createTextNode(""); + const text = document.createTextNode(''); startContainer.insertBefore( text, startContainer.childNodes[offset] @@ -2965,13 +3013,13 @@ } let doInsert = true; if (isPaste) { - const event = new CustomEvent("willPaste", { + const event = new CustomEvent('willPaste', { cancelable: true, detail: { text: plainText } }); - this.fireEvent("willPaste", event); + this.fireEvent('willPaste', event); plainText = event.detail.text; doInsert = !event.defaultPrevented; } @@ -2983,29 +3031,31 @@ this.setSelection(range); return this; } - const lines = plainText.split("\n"); + const lines = plainText.split('\n'); const config = this._config; const tag = config.blockTag; const attributes = config.blockAttributes; - const closeBlock = ""; - let openBlock = "<" + tag; + const closeBlock = ''; + let openBlock = '<' + tag; for (const attr in attributes) { - openBlock += " " + attr + '="' + escapeHTML(attributes[attr]) + '"'; + openBlock += ' ' + attr + '="' + escapeHTML(attributes[attr]) + '"'; } - openBlock += ">"; + openBlock += '>'; for (let i = 0, l = lines.length; i < l; i += 1) { let line = lines[i]; - line = escapeHTML(line).replace(/ (?=(?: |$))/g, " "); + line = escapeHTML(line).replace(/ (?=(?: |$))/g, ' '); if (i) { - line = openBlock + (line || "
") + closeBlock; + line = openBlock + (line || '
') + closeBlock; } lines[i] = line; } - return this.insertHTML(lines.join(""), isPaste); + return this.insertHTML(lines.join(''), isPaste); } + getSelectedText(range) { return getTextContentsOfRange(range || this.getSelection()); } + // --- Inline formatting /** * Extracts the font-family and font-size (if any) of the element @@ -3056,6 +3106,7 @@ } return fontInfo; } + /** * Looks for matching tag and attributes, so won't work if * instead of etc. @@ -3095,6 +3146,7 @@ } return seenNode; } + changeFormat(add, remove, range, partial) { if (!range) { range = this.getSelection(); @@ -3119,6 +3171,7 @@ this._updatePath(range, true); return this.focus(); } + _addFormat(tag, attributes, range) { const root = this._root; if (range.collapsed) { @@ -3138,7 +3191,7 @@ range.commonAncestorContainer, SHOW_ELEMENT_OR_TEXT, (node) => { - return (node instanceof Text || node.nodeName === "BR" || node.nodeName === "IMG") && isNodeContainedInRange(range, node, true); + return (node instanceof Text || node.nodeName === 'BR' || node.nodeName === 'IMG') && isNodeContainedInRange(range, node, true); } ); let { startContainer, startOffset, endContainer, endOffset } = range; @@ -3183,6 +3236,7 @@ } return range; } + _removeFormat(tag, attributes, range, partial) { this._saveRangeToBookmark(range); let fixer; @@ -3190,7 +3244,7 @@ if (cantFocusEmptyTextNodes) { fixer = document.createTextNode(ZWS); } else { - fixer = document.createTextNode(""); + fixer = document.createTextNode(''); } insertNodeInRange(range, fixer); } @@ -3265,50 +3319,63 @@ mergeInlines(root, range); return range; } + // --- bold() { - return this.changeFormat({ tag: "B" }); + return this.changeFormat({ tag: 'B' }); } + removeBold() { - return this.changeFormat(null, { tag: "B" }); + return this.changeFormat(null, { tag: 'B' }); } + italic() { - return this.changeFormat({ tag: "I" }); + return this.changeFormat({ tag: 'I' }); } + removeItalic() { - return this.changeFormat(null, { tag: "I" }); + return this.changeFormat(null, { tag: 'I' }); } + underline() { - return this.changeFormat({ tag: "U" }); + return this.changeFormat({ tag: 'U' }); } + removeUnderline() { - return this.changeFormat(null, { tag: "U" }); + return this.changeFormat(null, { tag: 'U' }); } + strikethrough() { - return this.changeFormat({ tag: "S" }); + return this.changeFormat({ tag: 'S' }); } + removeStrikethrough() { - return this.changeFormat(null, { tag: "S" }); + return this.changeFormat(null, { tag: 'S' }); } + subscript() { - return this.changeFormat({ tag: "SUB" }, { tag: "SUP" }); + return this.changeFormat({ tag: 'SUB' }, { tag: 'SUP' }); } + removeSubscript() { - return this.changeFormat(null, { tag: "SUB" }); + return this.changeFormat(null, { tag: 'SUB' }); } + superscript() { - return this.changeFormat({ tag: "SUP" }, { tag: "SUB" }); + return this.changeFormat({ tag: 'SUP' }, { tag: 'SUB' }); } + removeSuperscript() { - return this.changeFormat(null, { tag: "SUP" }); + return this.changeFormat(null, { tag: 'SUP' }); } + // --- makeLink(url, attributes) { const range = this.getSelection(); if (range.collapsed) { - let protocolEnd = url.indexOf(":") + 1; + let protocolEnd = url.indexOf(':') + 1; if (protocolEnd) { - while (url[protocolEnd] === "/") { + while (url[protocolEnd] === '/') { protocolEnd += 1; } } @@ -3326,30 +3393,32 @@ ); return this.changeFormat( { - tag: "A", + tag: 'A', attributes }, { - tag: "A" + tag: 'A' }, range ); } + removeLink() { return this.changeFormat( null, { - tag: "A" + tag: 'A' }, this.getSelection(), true ); } + addDetectedLinks(searchInNode, root) { const walker = new TreeIterator( searchInNode, SHOW_TEXT, - (node2) => !getNearest(node2, root || this._root, "A") + (node2) => !getNearest(node2, root || this._root, 'A') ); const linkRegExp = this.linkRegExp; const defaultAttributes = this._config.tagAttributes.a; @@ -3368,10 +3437,10 @@ ); } const child = createElement( - "A", + 'A', Object.assign( { - href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0] + href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : 'http://' + match[1] : 'mailto:' + match[0] }, defaultAttributes ) @@ -3383,71 +3452,76 @@ } return this; } + // --- setFontFace(name) { const className = this._config.classNames.fontFamily; return this.changeFormat( name ? { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className, - style: "font-family: " + name + ", sans-serif;" + style: 'font-family: ' + name + ', sans-serif;' } } : null, { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className } } ); } + setFontSize(size) { const className = this._config.classNames.fontSize; return this.changeFormat( size ? { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className, - style: "font-size: " + (typeof size === "number" ? size + "px" : size) + style: 'font-size: ' + (typeof size === 'number' ? size + 'px' : size) } } : null, { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className } } ); } + setTextColor(color) { const className = this._config.classNames.color; return this.changeFormat( color ? { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className, - style: "color:" + color + style: 'color:' + color } } : null, { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className } } ); } + setHighlightColor(color) { const className = this._config.classNames.highlight; return this.changeFormat( color ? { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className, - style: "background-color:" + color + style: 'background-color:' + color } } : null, { - tag: "SPAN", + tag: 'SPAN', attributes: { class: className } } ); } + // --- Block formatting _ensureBottomLine() { const root = this._root; @@ -3456,12 +3530,14 @@ root.appendChild(this.createDefaultBlock()); } } + createDefaultBlock(children) { const config = this._config; return fixCursor( createElement(config.blockTag, config.blockAttributes, children) ); } + splitBlock(lineBreakOnly, range) { if (!range) { range = this.getSelection(); @@ -3486,15 +3562,15 @@ }, 0); } block = getStartBlockOfRange(range, root); - if (block && (parent = getNearest(block, root, "PRE"))) { + if (block && (parent = getNearest(block, root, 'PRE'))) { moveRangeBoundariesDownTree(range); node = range.startContainer; const offset2 = range.startOffset; if (!(node instanceof Text)) { - node = document.createTextNode(""); + node = document.createTextNode(''); parent.insertBefore(node, parent.firstChild); } - if (!lineBreakOnly && node instanceof Text && (node.data.charAt(offset2 - 1) === "\n" || rangeDoesStartAtBlockBoundary(range, root)) && (node.data.charAt(offset2) === "\n" || rangeDoesEndAtBlockBoundary(range, root))) { + if (!lineBreakOnly && node instanceof Text && (node.data.charAt(offset2 - 1) === '\n' || rangeDoesStartAtBlockBoundary(range, root)) && (node.data.charAt(offset2) === '\n' || rangeDoesEndAtBlockBoundary(range, root))) { node.deleteData(offset2 && offset2 - 1, offset2 ? 2 : 1); nodeAfterSplit = split( node, @@ -3513,7 +3589,7 @@ } range.setStart(node, 0); } else { - node.insertData(offset2, "\n"); + node.insertData(offset2, '\n'); fixCursor(parent); if (node.length === offset2 + 1) { range.setStartAfter(node); @@ -3528,21 +3604,21 @@ return this; } if (!block || lineBreakOnly || /^T[HD]$/.test(block.nodeName)) { - moveRangeBoundaryOutOf(range, "A", root); - insertNodeInRange(range, createElement("BR")); + moveRangeBoundaryOutOf(range, 'A', root); + insertNodeInRange(range, createElement('BR')); range.collapse(false); this.setSelection(range); this._updatePath(range, true); return this; } - if (parent = getNearest(block, root, "LI")) { + if (parent = getNearest(block, root, 'LI')) { block = parent; } if (isEmptyBlock(block)) { - if (getNearest(block, root, "UL") || getNearest(block, root, "OL")) { + if (getNearest(block, root, 'UL') || getNearest(block, root, 'OL')) { this.decreaseListLevel(range); return this; - } else if (getNearest(block, root, "BLOCKQUOTE")) { + } else if (getNearest(block, root, 'BLOCKQUOTE')) { this.removeQuote(range); return this; } @@ -3577,21 +3653,21 @@ while (nodeAfterSplit instanceof Element) { let child = nodeAfterSplit.firstChild; let next; - if (nodeAfterSplit.nodeName === "A" && (!nodeAfterSplit.textContent || nodeAfterSplit.textContent === ZWS)) { - child = document.createTextNode(""); + if (nodeAfterSplit.nodeName === 'A' && (!nodeAfterSplit.textContent || nodeAfterSplit.textContent === ZWS)) { + child = document.createTextNode(''); replaceWith(nodeAfterSplit, child); nodeAfterSplit = child; break; } while (child && child instanceof Text && !child.data) { next = child.nextSibling; - if (!next || next.nodeName === "BR") { + if (!next || next.nodeName === 'BR') { break; } detach(child); child = next; } - if (!child || child.nodeName === "BR" || child instanceof Text) { + if (!child || child.nodeName === 'BR' || child instanceof Text) { break; } nodeAfterSplit = child; @@ -3601,6 +3677,7 @@ this._updatePath(range, true); return this; } + forEachBlock(fn, mutates, range) { if (!range) { range = this.getSelection(); @@ -3624,6 +3701,7 @@ } return this; } + modifyBlocks(modify, range) { if (!range) { range = this.getSelection(); @@ -3661,32 +3739,35 @@ this._updatePath(range, true); return this; } + // --- setTextAlignment(alignment) { this.forEachBlock((block) => { const className = block.className.split(/\s+/).filter((klass) => { return !!klass && !/^align/.test(klass); - }).join(" "); + }).join(' '); if (alignment) { - block.className = className + " align-" + alignment; + block.className = className + ' align-' + alignment; block.style.textAlign = alignment; } else { block.className = className; - block.style.textAlign = ""; + block.style.textAlign = ''; } }, true); return this.focus(); } + setTextDirection(direction) { this.forEachBlock((block) => { if (direction) { block.dir = direction; } else { - block.removeAttribute("dir"); + block.removeAttribute('dir'); } }, true); return this.focus(); } + // --- _getListSelection(range, root) { let list = range.commonAncestorContainer; @@ -3712,6 +3793,7 @@ } return [list, startLi, endLi]; } + increaseListLevel(range) { if (!range) { range = this.getSelection(); @@ -3748,6 +3830,7 @@ this._updatePath(range, true); return this.focus(); } + decreaseListLevel(range) { if (!range) { range = this.getSelection(); @@ -3770,7 +3853,7 @@ if (startLi) { let newParent = list.parentNode; insertBefore = !endLi.nextSibling ? list.nextSibling : split(list, endLi.nextSibling, newParent, root); - if (newParent !== root && newParent.nodeName === "LI") { + if (newParent !== root && newParent.nodeName === 'LI') { newParent = newParent.parentNode; while (insertBefore) { next = insertBefore.nextSibling; @@ -3783,7 +3866,7 @@ do { next = startLi === endLi ? null : startLi.nextSibling; list.removeChild(startLi); - if (makeNotList && startLi.nodeName === "LI") { + if (makeNotList && startLi.nodeName === 'LI') { startLi = this.createDefaultBlock([empty(startLi)]); } newParent.insertBefore(startLi, insertBefore); @@ -3800,6 +3883,7 @@ this._updatePath(range, true); return this.focus(); } + _makeList(frag, type) { const walker = getBlockWalker(frag, this._root); const tagAttributes = this._config.tagAttributes; @@ -3812,7 +3896,7 @@ walker.currentNode = node.lastChild; } if (!(node instanceof HTMLLIElement)) { - const newLi = createElement("LI", listItemAttrs); + const newLi = createElement('LI', listItemAttrs); if (node.dir) { newLi.dir = node.dir; } @@ -3838,18 +3922,21 @@ } return frag; } + makeUnorderedList() { - this.modifyBlocks((frag) => this._makeList(frag, "UL")); + this.modifyBlocks((frag) => this._makeList(frag, 'UL')); return this.focus(); } + makeOrderedList() { - this.modifyBlocks((frag) => this._makeList(frag, "OL")); + this.modifyBlocks((frag) => this._makeList(frag, 'OL')); return this.focus(); } + removeList() { this.modifyBlocks((frag) => { - const lists = frag.querySelectorAll("UL, OL"); - const items = frag.querySelectorAll("LI"); + const lists = frag.querySelectorAll('UL, OL'); + const items = frag.querySelectorAll('LI'); const root = this._root; for (let i = 0, l = lists.length; i < l; i += 1) { const list = lists[i]; @@ -3870,11 +3957,12 @@ }); return this.focus(); } + // --- increaseQuoteLevel(range) { this.modifyBlocks( (frag) => createElement( - "BLOCKQUOTE", + 'BLOCKQUOTE', this._config.tagAttributes.blockquote, [frag] ), @@ -3882,10 +3970,11 @@ ); return this.focus(); } + decreaseQuoteLevel(range) { this.modifyBlocks((frag) => { - Array.from(frag.querySelectorAll("blockquote")).filter((el) => { - return !getNearest(el.parentNode, frag, "BLOCKQUOTE"); + Array.from(frag.querySelectorAll('blockquote')).filter((el) => { + return !getNearest(el.parentNode, frag, 'BLOCKQUOTE'); }).forEach((el) => { replaceWith(el, empty(el)); }); @@ -3893,22 +3982,24 @@ }, range); return this.focus(); } + removeQuote(range) { this.modifyBlocks( () => this.createDefaultBlock([ - createElement("INPUT", { + createElement('INPUT', { id: this.startSelectionId, - type: "hidden" + type: 'hidden' }), - createElement("INPUT", { + createElement('INPUT', { id: this.endSelectionId, - type: "hidden" + type: 'hidden' }) ]), range ); return this.focus(); } + // --- code() { const range = this.getSelection(); @@ -3919,7 +4010,7 @@ const blockWalker = getBlockWalker(frag, root); let node; while (node = blockWalker.nextNode()) { - let nodes = node.querySelectorAll("BR"); + let nodes = node.querySelectorAll('BR'); const brBreaksLine = []; let l = nodes.length; for (let i = 0; i < l; i += 1) { @@ -3930,26 +4021,26 @@ if (!brBreaksLine[l]) { detach(br); } else { - replaceWith(br, document.createTextNode("\n")); + replaceWith(br, document.createTextNode('\n')); } } - nodes = node.querySelectorAll("CODE"); + nodes = node.querySelectorAll('CODE'); l = nodes.length; while (l--) { replaceWith(nodes[l], empty(nodes[l])); } if (output.childNodes.length) { - output.appendChild(document.createTextNode("\n")); + output.appendChild(document.createTextNode('\n')); } output.appendChild(empty(node)); } const textWalker = new TreeIterator(output, SHOW_TEXT); while (node = textWalker.nextNode()) { - node.data = node.data.replace(/ /g, " "); + node.data = node.data.replace(/ /g, ' '); } output.normalize(); return fixCursor( - createElement("PRE", this._config.tagAttributes.pre, [ + createElement('PRE', this._config.tagAttributes.pre, [ output ]) ); @@ -3958,7 +4049,7 @@ } else { this.changeFormat( { - tag: "CODE", + tag: 'CODE', attributes: this._config.tagAttributes.code }, null, @@ -3967,14 +4058,15 @@ } return this; } + removeCode() { const range = this.getSelection(); const ancestor = range.commonAncestorContainer; - const inPre = getNearest(ancestor, this._root, "PRE"); + const inPre = getNearest(ancestor, this._root, 'PRE'); if (inPre) { this.modifyBlocks((frag) => { const root = this._root; - const pres = frag.querySelectorAll("PRE"); + const pres = frag.querySelectorAll('PRE'); let l = pres.length; while (l--) { const pre = pres[l]; @@ -3982,14 +4074,14 @@ let node; while (node = walker.nextNode()) { let value = node.data; - value = value.replace(/ (?= )/g, "\xA0"); + value = value.replace(/ (?= )/g, '\xA0'); const contents = document.createDocumentFragment(); let index; - while ((index = value.indexOf("\n")) > -1) { + while ((index = value.indexOf('\n')) > -1) { contents.appendChild( document.createTextNode(value.slice(0, index)) ); - contents.appendChild(createElement("BR")); + contents.appendChild(createElement('BR')); value = value.slice(index + 1); } node.parentNode.insertBefore(contents, node); @@ -4002,24 +4094,26 @@ }, range); this.focus(); } else { - this.changeFormat(null, { tag: "CODE" }, range); + this.changeFormat(null, { tag: 'CODE' }, range); } return this; } + toggleCode() { - if (this.hasFormat("PRE") || this.hasFormat("CODE")) { + if (this.hasFormat('PRE') || this.hasFormat('CODE')) { this.removeCode(); } else { this.code(); } return this; } + // --- _removeFormatting(root, clean) { for (let node = root.firstChild, next; node; node = next) { next = node.nextSibling; if (isInline(node)) { - if (node instanceof Text || node.nodeName === "BR" || node.nodeName === "IMG") { + if (node instanceof Text || node.nodeName === 'BR' || node.nodeName === 'IMG') { clean.appendChild(node); continue; } @@ -4038,6 +4132,7 @@ } return clean; } + removeAllFormatting(range) { if (!range) { range = this.getSelection(); From 3c178a5d1d67f74d2dae75e5d6bf4bb7c6d42631 Mon Sep 17 00:00:00 2001 From: Sergey Mosin Date: Wed, 8 May 2024 20:41:36 -0400 Subject: [PATCH 2/3] fix caret in the root node and wrong field focused on compose dialog open --- .../compact-composer/js/CompactComposer.js | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/plugins/compact-composer/js/CompactComposer.js b/plugins/compact-composer/js/CompactComposer.js index c94377c2b0..288fb497c7 100644 --- a/plugins/compact-composer/js/CompactComposer.js +++ b/plugins/compact-composer/js/CompactComposer.js @@ -155,6 +155,20 @@ pasteImageHandler(e, squire); }); + wysiwyg.addEventListener('focus', () => { + const range = this.squire.getSelection(); + if (range.collapsed && range.startContainer === wysiwyg) { + // when the caret is directly in the wysiwyg a bunch of stuff + // (like lists, blockquotes, etc...) do not work, + // so we need to place it inside the nearest element + if (wysiwyg.children[range.startOffset] !== undefined) { + const newRange = document.createRange(); + newRange.setStart(wysiwyg.children[range.startOffset], 0); + this.squire.setSelection(newRange); + } + } + }); + // squire.addEventListener('focus', () => shortcuts.off()); // squire.addEventListener('blur', () => shortcuts.on()); @@ -598,7 +612,7 @@ type: 'menu_item', label: 'HTML Mode', id: 'menu-item-mode-wysiwyg', - cmd: () => this.setMode('wysiwyg'), + cmd: () => this.setModeCmd('wysiwyg'), showInPlainMode: true, icon: '' }, @@ -606,7 +620,7 @@ type: 'menu_item', label: 'Edit Source', id: 'menu-item-mode-source', - cmd: () => this.setMode('source'), + cmd: () => this.setModeCmd('source'), showInPlainMode: true, icon: '' }, @@ -614,7 +628,7 @@ type: 'menu_item', label: 'Plain Text Mode', id: 'menu-item-mode-plain', - cmd: () => this.setMode('plain'), + cmd: () => this.setModeCmd('plain'), showInPlainMode: true, icon: '' } @@ -871,6 +885,12 @@ return validation.test(this.squire.getPath()) || this.squire.hasFormat(format); } */ + + setModeCmd(mode) { + this.setMode(mode); + setTimeout(() => this.focus(), 1); + } + setMode(mode) { if (this.mode !== mode) { let cl = this.container.classList, @@ -894,7 +914,6 @@ this.mode = mode; cl.add('squire2-mode-' + mode); this.onModeChange?.(); - setTimeout(() => this.focus(), 1); } } From c0d3c352b868e1b342a88c76fba3af2052a96640 Mon Sep 17 00:00:00 2001 From: Sergey Mosin Date: Wed, 8 May 2024 20:42:36 -0400 Subject: [PATCH 3/3] version bump --- plugins/compact-composer/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/compact-composer/index.php b/plugins/compact-composer/index.php index e40ab5d2ac..ae743ca792 100644 --- a/plugins/compact-composer/index.php +++ b/plugins/compact-composer/index.php @@ -6,7 +6,7 @@ class CompactComposerPlugin extends \RainLoop\Plugins\AbstractPlugin NAME = 'Compact Composer', AUTHOR = 'Sergey Mosin', URL = 'https://github.com/the-djmaze/snappymail/pull/1466', - VERSION = '1.0.3', + VERSION = '1.0.4', RELEASE = '2024-04-23', REQUIRED = '2.34.0', LICENSE = 'AGPL v3',