/* Copyright © 2011-2015 by Neil Jenkins. MIT Licensed. */ /* 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 */ (() => { // 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); // 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/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/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); } }); }; // 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/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/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/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; } /* const walker = createTreeWalker( nonInlineParent, SHOW_ELEMENT_OR_TEXT ); */ 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")); } /* } 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); */ } } 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('"', """); // 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"); // 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); } }; // 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); }; // source/range/Contents.ts /* 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; }; */ // 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) => { /* 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); */ const root = self._root; self._recordUndoState(range); self._config.addLinks && addLinks(range.startContainer, root); self._getRangeAndRemoveBookmark(range); /* 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 }, */ 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); }; 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; }; var createBookmarkNodes = () => [ createElement("INPUT", { id: startSelectionId, type: "hidden" }), createElement("INPUT", { id: endSelectionId, type: "hidden" }) ]; var getListSelection = (range, root) => { 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]; }; var setDirection = (self, frag, dir) => { let walker = getBlockWalker(frag, self._root), node; while (node = walker.nextNode()) { if (node.nodeName === "LI") { node.parentNode.setAttribute("dir", dir); break; } node.setAttribute("dir", dir); } return frag; }; var decreaseLevel = (self, range, node) => getClosest(node, self._root, "UL,OL,BLOCKQUOTE") && self.changeIndentationLevel("decrease"); var addLinks = (frag, root) => { let walker = createTreeWalker(frag, SHOW_TEXT, node => !getClosest(node, root, "A")); let node, data, parent, match, index, endIndex, child; while (node = walker.nextNode()) { data = node.data; parent = node.parentNode; while (match = linkRegExp.exec(data)) { index = match.index; endIndex = index + match[0].length; if (index) { child = document.createTextNode(data.slice(0, index)); parent.insertBefore(child, node); } child = createElement("A", { href: match[1] ? (match[2] ? match[1] : "https://" + match[1]) : "mailto:" + match[0] }, [data.slice(index, endIndex)]); parent.insertBefore(child, node); node.data = data = data.slice(endIndex); } } }; keyHandlers[ctrlKey + "b"] = mapKeyToFormat("B"); keyHandlers[ctrlKey + "i"] = mapKeyToFormat("I"); keyHandlers[ctrlKey + "u"] = mapKeyToFormat("U"); keyHandlers[ctrlKey + "Shift-7"] = mapKeyToFormat("S"); keyHandlers[ctrlKey + "Shift-5"] = mapKeyToFormat("SUB", "SUP"); keyHandlers[ctrlKey + "Shift-6"] = mapKeyToFormat("SUP", "SUB"); keyHandlers[ctrlKey + "Shift-8"] = toggleList("UL", "makeUnorderedList"); keyHandlers[ctrlKey + "Shift-9"] = toggleList("OL", "makeOrderedList"); keyHandlers[ctrlKey + "["] = changeIndentationLevel("decrease"); keyHandlers[ctrlKey + "]"] = changeIndentationLevel("increase"); keyHandlers[ctrlKey + "d"] = mapKeyTo("toggleCode"); keyHandlers[ctrlKey + "y"] = mapKeyTo("redo"); keyHandlers[ctrlKey + "Shift-Z"] = mapKeyTo("redo"); keyHandlers["Redo"] = mapKeyTo("redo"); class EditStack extends Array { constructor(squire) { super(); this.squire = squire; this.index = -1; this.inUndoState = false; this.limit = 0; // -1 means no limit } clear() { this.index = -1; this.length = 0; } stateChanged(/*canUndo, canRedo*/) { this.squire.fireEvent("undoStateChange", { canUndo: this.index > 0, canRedo: this.index + 1 < this.length }); this.squire.fireEvent("input"); } docWasChanged() { if (this.inUndoState) { this.inUndoState = false; this.stateChanged(/*true, false*/); } else this.squire.fireEvent("input"); } /** * Leaves bookmark. */ recordUndoState(range, replace) { if (!this.inUndoState || replace) { let undoIndex = this.index; let undoLimit = this.limit; let squire = this.squire; replace || ++undoIndex; undoIndex = Math.max(0, undoIndex); this.length = Math.min(undoIndex + 1, this.length); range && squire._saveRangeToBookmark(range); const html = squire._getRawHTML(); if (undoLimit > 0 && undoIndex > undoLimit) { this.splice(0, undoIndex - undoLimit); undoIndex = undoLimit; } this[undoIndex] = html; this.index = undoIndex; this.inUndoState = true; } } saveUndoState(range) { let squire = this.squire; range = range || squire.getSelection(); this.recordUndoState(range, true); squire._getRangeAndRemoveBookmark(range); } undo() { let squire = this.squire; if (this.index > 0 || !this.inUndoState) { this.recordUndoState(squire.getSelection()); const undoIndex = this.index - 1; this.index = undoIndex; squire._setRawHTML(this[undoIndex]); let range = squire._getRangeAndRemoveBookmark(); if (range) { squire.setSelection(range); } this.stateChanged(/*undoIndex > 0, true*/); } } redo() { let squire = this.squire, undoIndex = this.index + 1; if (undoIndex < this.length && this.inUndoState) { this.index = undoIndex; squire._setRawHTML(this[undoIndex]); let range = squire._getRangeAndRemoveBookmark(); if (range) { squire.setSelection(range); } this.stateChanged(/*true, undoIndex + 1 < this.length*/); } } } // source/Editor.ts var customEvents = new Set([ "pathChange", "select", "input", "pasteImage", "undoStateChange" ]); var startSelectionId = "squire-selection-start"; var endSelectionId = "squire-selection-end"; var tagAfterSplit = { DT: "DD", DD: "DT", LI: "LI", PRE: "PRE" }; var linkRegExp = /\b(?:((https?:\/\/)?(?: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; var Squire = class { constructor(root, config) { this._root = root; this.setConfig(config); this._isFocused = false; this._lastSelection = null; this._willRestoreSelection = false; this._mayHaveZWS = false; this._path = ""; this._pathRange = null; this._events = new Map(); this.editStack = new EditStack(this); this._ignoreChange = false; this.addEventListener("selectionchange", () => this._isFocused && this._updatePath(this.getSelection())) .addEventListener("blur", () => this._willRestoreSelection = true) .addEventListener("pointerdown mousedown touchstart", () => this._willRestoreSelection = false) .addEventListener("focus", () => this._willRestoreSelection && this.setSelection(this._lastSelection)) .addEventListener("cut", _onCut) .addEventListener("copy", _onCopy) .addEventListener("paste", _onPaste) .addEventListener("drop", (event) => { let types = event.dataTransfer.types; if (types.includes("text/plain") || types.includes("text/html")) { this.saveUndoState(); } }) .addEventListener("keydown keyup", (event) => this.isShiftDown = event.shiftKey) .addEventListener("keydown", _onKey) .addEventListener("pointerup keyup mouseup touchend", () => this.getSelection()) .addEventListener("beforeinput", this._beforeInput); this._keyHandlers = Object.create(keyHandlers); this._mutation = new MutationObserver(() => this._docWasChanged()); this._mutation.observe(root, { childList: true, attributes: true, characterData: true, subtree: true }); root.setAttribute("contenteditable", "true"); this.setHTML(""); this._beforeInputTypes = { insertText: (event) => { if (isAndroid && event.data && event.data.includes("\n")) { event.preventDefault(); } }, insertLineBreak: (event) => { event.preventDefault(); this.splitBlock(true); }, insertParagraph: (event) => { event.preventDefault(); this.splitBlock(false); }, insertOrderedList: (event) => { event.preventDefault(); this.makeOrderedList(); }, insertUnoderedList: (event) => { event.preventDefault(); this.makeUnorderedList(); }, historyUndo: (event) => { event.preventDefault(); this.undo(); }, historyRedo: (event) => { event.preventDefault(); }, formatRemove: (event) => { event.preventDefault(); this.setStyle(); }, formatSetBlockTextDirection: (event) => { event.preventDefault(); let dir = event.data; this.setTextDirection(dir === "null" ? null : dir); }, formatBackColor: (event) => { event.preventDefault(); this.setStyle({ backgroundColor: event.data }); }, formatFontColor: (event) => { event.preventDefault(); this.setStyle({ color: event.data }); }, formatFontName: (event) => { event.preventDefault(); this.setStyle({ fontFamily: event.data }); }, /* formatIndent: event => { event.preventDefault(); this.changeIndentationLevel("increase"); }, formatOutdent: event => { event.preventDefault(); this.changeIndentationLevel("decrease"); }, this.saveUndoState(); }, */ deleteContentBackward: (event) => { Backspace(this, event, this.getSelection()); }, deleteContentForward: (event) => { Delete(this, event, this.getSelection()); } }; } _beforeInput(event) { let type = event.isComposing ? "" : event.inputType; switch (type) { case "formatBold": case "formatItalic": case "formatUnderline": case "formatStrikeThrough": case "formatSuperscript": case "formatSubscript": event.preventDefault(); this[type.slice(6).toLowerCase()](); break; case "formatJustifyFull": case "formatJustifyCenter": case "formatJustifyRight": case "formatJustifyLeft": { event.preventDefault(); let alignment = type.slice(13).toLowerCase(); this.setStyle({textAlign:alignment === "full" ? "justify" : alignment}); break; } default: this._beforeInputTypes[type]?.(event); } } // --- 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 (!isFocused || this._isFocused) { return this; } this._isFocused = true; } else { if (isFocused || !this._isFocused) { return this; } this._isFocused = false; } } if (handlers) { const event = detail instanceof Event ? detail : new CustomEvent(type, { detail }); handlers = handlers.slice(); for (const handler of handlers) { try { handler.handleEvent ? handler.handleEvent(event) : handler.call(this, event); } catch (error) { error.details = 'Squire: fireEvent error. Event type: ' + type; didError(error); } } } return this; } addEventListener(types, fn) { if (!fn) { didError({ name: 'Squire: addEventListener with null or undefined fn', message: 'Event type: ' + types }); return this; } types.split(/\s+/).forEach((type) => { let handlers = this._events.get(type); let target = this._root; if (!handlers) { handlers = []; this._events.set(type, handlers); customEvents.has(type) || (type === "selectionchange" ? document : target).addEventListener(type, this, {capture:true,passive:"touchstart"===type}); } handlers.push(fn); }); return this; } removeEventListener(type, fn) { const handlers = this._events.get(type); if (handlers) { if (fn) { let l = handlers.length; while (l--) { if (handlers[l] === fn) { handlers.splice(l, 1); } } } else { handlers.length = 0; } if (!handlers.length) { this._events.delete(type); customEvents.has(type) || (type === "selectionchange" ? document : this._root).removeEventListener(type, this, true); } } return this; } // --- Focus focus() { this._root.focus({ preventScroll: true }); return this; } blur() { this._root.blur(); return this; } // --- _removeZWS() { if (this._mayHaveZWS) { removeZWS(this._root); this._mayHaveZWS = false; } } // --- _saveRangeToBookmark(range) { let [startNode, endNode] = createBookmarkNodes(), temp; insertNodeInRange(range, startNode); range.collapse(); insertNodeInRange(range, endNode); if (startNode.compareDocumentPosition(endNode) & DOCUMENT_POSITION_PRECEDING) { startNode.id = endSelectionId; endNode.id = startSelectionId; temp = startNode; startNode = endNode; endNode = temp; } range.setStartAfter(startNode); range.setEndBefore(endNode); } _getRangeAndRemoveBookmark(range) { const root = this._root; const start = root.querySelector("#" + startSelectionId); const end = root.querySelector("#" + endSelectionId); if (start && end) { let startContainer = start.parentNode; let endContainer = end.parentNode; const startOffset = indexOf(startContainer.childNodes, start); let endOffset = indexOf(endContainer.childNodes, end); if (startContainer === endContainer) { --endOffset; } detach(start); detach(end); range = range || document.createRange(); range.setStart(startContainer, startOffset); range.setEnd(endContainer, endOffset); mergeInlines(startContainer, range); if (startContainer !== endContainer) { mergeInlines(endContainer, range); } if (range.collapsed) { startContainer = range.startContainer; if (isTextNode(startContainer)) { endContainer = startContainer.childNodes[range.startOffset]; if (!endContainer || !isTextNode(endContainer)) { endContainer = startContainer.childNodes[range.startOffset - 1]; } if (isTextNode(endContainer)) { range.setStart(endContainer, 0); range.collapse(true); } } } } return range || null; } getSelection() { const sel = win.getSelection(); const root = this._root; let range; if (this._isFocused && sel?.rangeCount) { range = sel.getRangeAt(0).cloneRange(); const startContainer = range.startContainer; const endContainer = range.endContainer; if (startContainer && isLeaf(startContainer)) { range.setStartBefore(startContainer); } if (endContainer && isLeaf(endContainer)) { range.setEndBefore(endContainer); } } if (range && root.contains(range.commonAncestorContainer)) { this._lastSelection = range; } else { range = this._lastSelection; if (!document.contains(range.commonAncestorContainer)) { range = null; } } return range || createRange(root.firstChild, 0); } setSelection(range) { this._lastSelection = range; if (this._isFocused) { const selection = win.getSelection(); if (selection) { selection.setBaseAndExtent( range.startContainer, range.startOffset, range.endContainer, range.endOffset ); } } else { this._willRestoreSelection = true; } return this; } // --- // --- Path getPath() { return this._path; } _updatePath(range, force) { const anchor = range.startContainer, focus = range.endContainer; if (force || anchor !== this._pathRange.startContainer || focus !== this._pathRange.endContainer) { this._pathRange = range.cloneRange(); let node = anchor === focus ? focus : null, newPath = (anchor && focus) ? (node ? this._getPath(focus) : "(selection)") : ""; if (this._path !== newPath) { this._path = newPath; this.fireEvent("pathChange", { path: newPath, element: (!node || isElement(node)) ? node : node.parentElement }); } } this.fireEvent(range.collapsed ? "cursor" : "select", { range }); } _getPath(node) { const root = this._root; let path = "", style; if (node && node !== root) { path = this._getPath(node.parentNode, root); if (isElement(node)) { path += (path ? ">" : "") + node.nodeName; if (node.id) { path += "#" + node.id; } if (node.dir) { path += "[dir=" + node.dir + "]"; } if (style = node.style.cssText) { path += "[style=" + style + "]"; } } } return path; } // --- History _docWasChanged() { cache = new WeakMap(); this._mayHaveZWS = cantFocusEmptyTextNodes; if (this._ignoreChange) { this._ignoreChange = false; } else { this.editStack.docWasChanged(); } } /** * Leaves bookmark. */ _recordUndoState(range, replace) { this.editStack.recordUndoState(range, replace); } saveUndoState(range) { this.editStack.saveUndoState(range); } undo() { this.editStack.undo(); } redo() { this.editStack.redo(); } // --- Get and set data getRoot() { return this._root; } _getRawHTML() { return this._root.innerHTML; } _setRawHTML(html) { if (html !== undefined) { const root = this._root; let node = root; root.innerHTML = html; do { fixCursor(node); } while (node = getNextBlock(node, root)); this._ignoreChange = true; } } getHTML(withBookMark) { let html, range; if (withBookMark) { range = this.getSelection(); this._saveRangeToBookmark(range); } html = this._getRawHTML().replace(/\u200B/g, ""); withBookMark && this._getRangeAndRemoveBookmark(range); return html; } setHTML(html) { const root = this._root, frag = this._config.sanitizeToDOMFragment(html, false); cleanTree(frag); cleanupBRs(frag, root, false); fixContainer(frag, root); let node, walker = getBlockWalker(frag, root); while ((node = walker.nextNode()) && node !== root) { fixCursor(node); } this._ignoreChange = true; if (root.replaceChildren) { root.replaceChildren(frag); } else { while (root.lastChild) detach(root.lastChild); root.append(frag); } fixCursor(root); this.editStack.clear(); const range = this._getRangeAndRemoveBookmark() || createRange(root.firstElementChild || root, 0); this.saveUndoState(range); this.setRange(range); return this; } /** * Insert HTML at the cursor location. If the selection is not collapsed * insertTreeFragmentIntoRange will delete the selection so that it is * replaced by the html being inserted. */ insertHTML(html, isPaste) { let range = this.getSelection(); if (isPaste) { let startFragmentIndex = html.indexOf(""), endFragmentIndex = html.lastIndexOf(""); if (startFragmentIndex > -1 && endFragmentIndex > -1) { html = html.slice(startFragmentIndex + 20, endFragmentIndex); } } let frag = this._config.sanitizeToDOMFragment(html, isPaste); this.saveUndoState(range); try { let root = this._root, node = frag; addLinks(frag, frag); cleanTree(frag); cleanupBRs(frag, root, false); removeEmptyInlines(frag); frag.normalize(); while (node = getNextBlock(node, frag)) { fixCursor(node); } insertTreeFragmentIntoRange(range, frag, root); range.collapse(); moveRangeBoundaryOutOf(range, "A", root); this._ensureBottomLine(); this.setRange(range); isPaste && this.focus(); } catch (error) { didError(error); } return this; } insertElement(el, range) { range = range || this.getSelection(); range.collapse(true); if (isInline(el)) { insertNodeInRange(range, el); range.setStartAfter(el); } else { const root = this._root; let splitNode = getStartBlockOfRange(range, root) || root; let nodeAfterSplit; while (splitNode !== root && !splitNode.nextSibling) { splitNode = splitNode.parentNode; } if (splitNode !== root) { const parent = splitNode.parentNode; nodeAfterSplit = split(parent, splitNode.nextSibling, root, root); } if (nodeAfterSplit) { nodeAfterSplit.before(el); } else { root.append(el); nodeAfterSplit = this.createDefaultBlock(); root.append(nodeAfterSplit); } range.setStart(nodeAfterSplit, 0); range.setEnd(nodeAfterSplit, 0); moveRangeBoundariesDownTree(range); } this.focus(); this.setSelection(range); this._updatePath(range); return this; } insertImage(src, attributes) { const img = createElement("IMG", mergeObjects({ src: src }, attributes, true)); this.insertElement(img); return img; } insertPlainText(plainText, isPaste) { const range = this.getSelection(); if (range.collapsed && getClosest(range.startContainer, this._root, "PRE")) { let node = range.startContainer; let offset = range.startOffset; let text; if (!isTextNode(node)) { text = document.createTextNode(""); node?.childNodes[offset].before(text); node = text; offset = 0; } node.insertData(offset, plainText); range.setStart(node, offset + plainText.length); range.collapse(true); this.setSelection(range); return this; } const lines = plainText.split(/\r?\n/), closeBlock = "", openBlock = "<" + blockTag + ">"; lines.forEach((line, i) => { line = escapeHTML(line).replace(/ (?=(?: |$))/g, NBSP); lines[i] = i ? openBlock + (line || "
") + closeBlock : line; }); return this.insertHTML(lines.join(""), isPaste); } // --- Inline formatting /** * Extracts the font-family and font-size (if any) of the element * holding the cursor. If there's a selection, returns an empty object. */ getFontInfo(range) { const fontInfo = { color: void 0, backgroundColor: void 0, family: void 0, size: void 0 }; range = range || this.getSelection(); let seenAttributes = 0; let element = range.commonAncestorContainer, style, attr; if (range.collapsed || isTextNode(element)) { if (isTextNode(element)) { element = element.parentNode; } while (seenAttributes < 4 && element) { if (style = element.style) { if (!fontInfo.color && (attr = style.color)) { fontInfo.color = attr; ++seenAttributes; } if (!fontInfo.backgroundColor && (attr = style.backgroundColor)) { fontInfo.backgroundColor = attr; ++seenAttributes; } if (!fontInfo.family && (attr = style.fontFamily)) { fontInfo.family = attr; ++seenAttributes; } if (!fontInfo.size && (attr = style.fontSize)) { fontInfo.size = attr; ++seenAttributes; } } element = element.parentNode; } } return fontInfo; } /** * Looks for matching tag and attributes, so won't work if * instead of etc. */ hasFormat(tag, attributes, range) { tag = tag.toUpperCase(); range = range || this.getSelection(); if (!range.collapsed && isTextNode(range.startContainer) && range.startOffset === range.startContainer.length && range.startContainer.nextSibling) { range.setStartBefore(range.startContainer.nextSibling); } if (!range.collapsed && isTextNode(range.endContainer) && range.endOffset === 0 && range.endContainer.previousSibling) { range.setEndAfter(range.endContainer.previousSibling); } const root = this._root; const common = range.commonAncestorContainer; if (getNearest(common, root, tag, attributes)) { return true; } if (isTextNode(common)) { return false; } const walker = createTreeWalker(common, SHOW_TEXT, (node2) => isNodeContainedInRange(range, node2)); let seenNode = false; let node; while (node = walker.nextNode()) { if (!getNearest(node, root, tag, attributes)) { return false; } seenNode = true; } return seenNode; } changeFormat(add, remove, range, partial) { range = range || this.getSelection(); this.saveUndoState(range); if (remove) { range = this._removeFormat( remove.tag.toUpperCase(), remove.attributes || {}, range, partial ); } if (add) { range = this._addFormat( add.tag.toUpperCase(), add.attributes || {}, range ); } this.setRange(range); return this.focus(); } _addFormat(tag, attributes, range) { const root = this._root; let node; if (range.collapsed) { const el = fixCursor(createElement(tag, attributes)); insertNodeInRange(range, el); range.setStart(el.firstChild, el.firstChild.length); range.collapse(true); let block = el; while (isInline(block)) { block = block.parentNode; } removeZWS(block, el); } else { const filter = (node) => (isTextNode(node) || isBrElement(node) || node.nodeName === "IMG") && isNodeContainedInRange(range, node); const walker = createTreeWalker( range.commonAncestorContainer, SHOW_ELEMENT_OR_TEXT, filter ); let { startContainer, startOffset, endContainer, endOffset } = range; walker.currentNode = startContainer; if (!isElement(startContainer) && !isTextNode(startContainer) || !filter(startContainer)) { startContainer = walker.nextNode(); startOffset = 0; } if (startContainer) { do { node = walker.currentNode; if (!getNearest(node, root, tag, attributes)) { if (node === endContainer && node.length > endOffset) { node.splitText(endOffset); } if (node === startContainer && startOffset) { node = node.splitText(startOffset); if (endContainer === startContainer) { endContainer = node; endOffset -= startOffset; } startContainer = node; startOffset = 0; } const el = createElement(tag, attributes); replaceWith(node, el); el.append(node); } } while (walker.nextNode()); if (!isTextNode(endContainer)) { if (isTextNode(node)) { endContainer = node; endOffset = node.length; } else { endContainer = node.parentNode; endOffset = 1; } } range = createRange( startContainer, startOffset, endContainer, endOffset ); } } return range; } _removeFormat(tag, attributes, range, partial) { this._saveRangeToBookmark(range); let fixer; if (range.collapsed) { this._mayHaveZWS = cantFocusEmptyTextNodes; fixer = document.createTextNode(cantFocusEmptyTextNodes ? ZWS : ""); insertNodeInRange(range, fixer); } let root = range.commonAncestorContainer; while (isInline(root)) { root = root.parentNode; } const { startContainer, startOffset, endContainer, endOffset } = range; const toWrap = []; const examineNode = (node, exemplar) => { if (isNodeContainedInRange(range, node, false)) { return; } let isText = isTextNode(node); let child; let next; if (!isNodeContainedInRange(range, node)) { if (node.nodeName !== "INPUT" && (!isText || node.data)) { toWrap.push([exemplar, node]); } return; } if (isText) { if (node === endContainer && endOffset !== node.length) { toWrap.push([exemplar, node.splitText(endOffset)]); } if (node === startContainer && startOffset) { node.splitText(startOffset); toWrap.push([exemplar, node]); } } else { for (child = node.firstChild; child; child = next) { next = child.nextSibling; examineNode(child, exemplar); } } }; const formatTags = Array.prototype.filter.call( root.getElementsByTagName(tag), (el) => isNodeContainedInRange(range, el, true) && hasTagAttributes(el, tag, attributes) ); partial || formatTags.forEach((node) => examineNode(node, node)); toWrap.forEach(([el, node]) => { el = el.cloneNode(false); replaceWith(node, el); el.append(node); }); formatTags.forEach((el) => replaceWith(el, empty(el))); if (cantFocusEmptyTextNodes && fixer) { fixer = fixer.parentNode; let block = fixer; while (block && isInline(block)) { block = block.parentNode; } if (block) { removeZWS(block, fixer); } } this._getRangeAndRemoveBookmark(range); fixer && range.collapse(); mergeInlines(root, range); return range; } // --- bold() { this.toggleTag("B"); } italic() { this.toggleTag("I"); } underline() { this.toggleTag("U"); } strikethrough() { this.toggleTag("S"); } subscript() { this.toggleTag("SUB", "SUP"); } superscript() { this.toggleTag("SUP", "SUB"); } // --- makeLink(url, attributes) { const range = this.getSelection(); if (range.collapsed) { insertNodeInRange( range, document.createTextNode(url.replace(/^[^:]*:\/*/, "")) ); } attributes = mergeObjects( mergeObjects({ href: url }, attributes, true), null, false ); return this.changeFormat( { tag: "A", attributes }, { tag: "A" }, range ); } removeLink() { return this.changeFormat( null, { tag: "A" }, this.getSelection(), true ); } // --- Block formatting _ensureBottomLine() { const root = this._root; const last = root.lastElementChild; if (!last || last.nodeName !== blockTag || !isBlock(last)) { root.append(this.createDefaultBlock()); } } createDefaultBlock(children) { return fixCursor( createElement(blockTag, null, children) ); } splitBlock(lineBreakOnly, range) { range = range || this.getSelection(); const root = this._root; let block; let parent; let node; let nodeAfterSplit; this.editStack.inUndoState && this._docWasChanged(); this._recordUndoState(range); this._removeZWS(); this._getRangeAndRemoveBookmark(range); if (!range.collapsed) { deleteContentsOfRange(range, root); } if (this._config.addLinks) { moveRangeBoundariesDownTree(range); setTimeout(() => { addLinks(range.startContainer, root); }, 0); } block = getStartBlockOfRange(range, root); if (block && (parent = getClosest(block, root, "PRE"))) { moveRangeBoundariesDownTree(range); node = range.startContainer; const offset2 = range.startOffset; if (!isTextNode(node)) { node = document.createTextNode(""); parent.insertBefore(node, parent.firstChild); } if (!lineBreakOnly && isTextNode(node) && (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, offset2 && offset2 - 1, root, root ); node = nodeAfterSplit.previousSibling; if (!node.textContent) { detach(node); } node = this.createDefaultBlock(); nodeAfterSplit.before(node); if (!nodeAfterSplit.textContent) { detach(nodeAfterSplit); } range.setStart(node, 0); } else { node.insertData(offset2, "\n"); fixCursor(parent); if (node.length === offset2 + 1) { range.setStartAfter(node); } else { range.setStart(node, offset2 + 1); } } range.collapse(true); this.setRange(range); this._docWasChanged(); return; } if (!block || lineBreakOnly || /^T[HD]$/.test(block.nodeName)) { moveRangeBoundaryOutOf(range, "A", root); insertNodeInRange(range, createElement("BR")); range.collapse(); this.setRange(range); return; } block = getClosest(block, root, "LI") || block; if (isEmptyBlock(block) && (parent = getClosest(block, root, "UL,OL,BLOCKQUOTE"))) { return "BLOCKQUOTE" === parent.nodeName ? this.modifyBlocks((/* frag */) => this.createDefaultBlock(createBookmarkNodes()), range) : this.decreaseListLevel(range); } node = range.startContainer; const offset = range.startOffset; let splitTag = tagAfterSplit[block.nodeName] || blockTag; nodeAfterSplit = split( node, offset, block.parentNode, root ); if (!hasTagAttributes(nodeAfterSplit, splitTag)) { block = createElement(splitTag); if (nodeAfterSplit.dir) { block.dir = nodeAfterSplit.dir; } replaceWith(nodeAfterSplit, block); block.append(empty(nodeAfterSplit)); nodeAfterSplit = block; } removeZWS(block); removeEmptyInlines(block); fixCursor(block); while (isElement(nodeAfterSplit)) { let child = nodeAfterSplit.firstChild; let next; if (nodeAfterSplit.nodeName === "A" && (!nodeAfterSplit.textContent || nodeAfterSplit.textContent === ZWS)) { child = document.createTextNode(""); replaceWith(nodeAfterSplit, child); nodeAfterSplit = child; break; } while (isTextNode(child) && !child.data) { next = child.nextSibling; if (!next || isBrElement(next)) { break; } detach(child); child = next; } if (!child || isBrElement(child) || isTextNode(child)) { break; } nodeAfterSplit = child; } range = createRange(nodeAfterSplit, 0); this.setRange(range); } modifyBlocks(modify, range) { range = range || this.getSelection(); this._recordUndoState(range, true); const root = this._root; expandRangeToBlockBoundaries(range, root); moveRangeBoundariesUpTree(range, root, root, root); const frag = extractContentsOfRange(range, root, root); insertNodeInRange(range, modify.call(this, frag)); if (range.endOffset < range.endContainer.childNodes.length) { mergeContainers( range.endContainer.childNodes[range.endOffset], root ); } mergeContainers( range.startContainer.childNodes[range.startOffset], root ); this._getRangeAndRemoveBookmark(range); this.setRange(range); return this; } // --- setTextDirection(direction) { return this.modifyBlocks(frag => setDirection(this, frag, direction)).focus(); } increaseListLevel(range) { range = range || this.getSelection(); const root = this._root; const listSelection = getListSelection(range, root); if (listSelection) { let [list, startLi, endLi] = listSelection; if (startLi && startLi !== list.firstChild) { this._recordUndoState(range, true); const type = list.nodeName; let newParent = startLi.previousSibling; let next; if (newParent.nodeName !== type) { newParent = createElement(type); startLi.before(newParent); } do { next = startLi === endLi ? null : startLi.nextSibling; newParent.append(startLi); } while (startLi = next); next = newParent.nextSibling; next && mergeContainers(next, root); this._getRangeAndRemoveBookmark(range); this.setRange(range); } } return this.focus(); } decreaseListLevel(range) { range = range || this.getSelection(); const root = this._root; const listSelection = getListSelection(range, root); if (listSelection) { let list = listSelection[0]; let startLi = listSelection[1] || list.firstChild; let endLi = listSelection[2] || list.lastChild; let next, insertBefore; this._recordUndoState(range, true); if (startLi) { let newParent = list.parentNode; insertBefore = !endLi.nextSibling ? list.nextSibling : split(list, endLi.nextSibling, newParent, root); if (newParent !== root && newParent.nodeName === "LI") { newParent = newParent.parentNode; while (insertBefore) { next = insertBefore.nextSibling; endLi.append(insertBefore); insertBefore = next; } insertBefore = list.parentNode.nextSibling; } const makeNotList = !/^[OU]L$/.test(newParent.nodeName); do { next = startLi === endLi ? null : startLi.nextSibling; startLi.remove(); if (makeNotList && startLi.nodeName === "LI") { startLi = this.createDefaultBlock([empty(startLi)]); } newParent.insertBefore(startLi, insertBefore); } while (startLi = next); } list.firstChild || detach(list); insertBefore && mergeContainers(insertBefore, root); this._getRangeAndRemoveBookmark(range); this.setRange(range); } return this.focus(); } _makeList(frag, type) { let walker = getBlockWalker(frag, this._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; } if ((prev = node.previousSibling) && prev.nodeName === type) { prev.append(newLi); detach(node); } else { replaceWith(node, createElement(type, null, [newLi])); } newLi.append(empty(node)); walker.currentNode = newLi; } else { node = node.parentNode; tag = node.nodeName; if (tag !== type && listNodeNames.has(tag)) { replaceWith(node, createElement(type, null, [empty(node)]) ); } } } return frag; } makeUnorderedList() { return this.modifyBlocks((frag) => this._makeList(frag, "UL")).focus(); } makeOrderedList() { return this.modifyBlocks((frag) => this._makeList(frag, "OL")).focus(); } removeList() { return this.modifyBlocks((frag) => { const root = this._root; frag.querySelectorAll("LI").forEach((item) => { if (isBlock(item)) { replaceWith(item, this.createDefaultBlock([empty(item)])); } else { fixContainer(item, root); replaceWith(item, empty(item)); } }); frag.querySelectorAll("UL, OL").forEach((list) => { const listFrag = empty(list); fixContainer(listFrag, root); replaceWith(list, listFrag); }); return frag; }).focus(); } // --- increaseQuoteLevel(range) { return this.modifyBlocks( (frag) => createElement( "BLOCKQUOTE", null, [frag] ), range ).focus(); } decreaseQuoteLevel(range) { return this.modifyBlocks((frag) => { Array.prototype.filter.call( frag.querySelectorAll("blockquote"), (el) => !getClosest(el.parentNode, frag, "BLOCKQUOTE") ).forEach( (el) => replaceWith(el, empty(el)) ); return frag; }, range).focus(); } // --- code() { const range = this.getSelection(); if (range.collapsed || isContainer(range.commonAncestorContainer)) { return this.modifyBlocks((frag) => { const root = this._root; const output = document.createDocumentFragment(); let walker = getBlockWalker(frag, root); let node; while (node = walker.nextNode()) { node.querySelectorAll("BR").forEach(br => { if (!isLineBreak(br, false)) { detach(br); } else { replaceWith(br, document.createTextNode("\n")); } }); node.querySelectorAll("CODE").forEach(el => detach(el)); if (output.childNodes.length) { output.append(document.createTextNode("\n")); } output.append(empty(node)); } walker = createTreeWalker(output, SHOW_TEXT); while (node = walker.nextNode()) { node.data = node.data.replace(NBSP, " "); // nbsp -> sp } output.normalize(); return fixCursor( createElement("PRE", null, [ output ]) ); }, range).focus(); } return this.changeFormat({ tag: "CODE" }, null, range); } removeCode() { const range = this.getSelection(); const ancestor = range.commonAncestorContainer; const inPre = getClosest(ancestor, this._root, "PRE"); if (inPre) { return this.modifyBlocks((frag) => { const root = this._root; const pres = frag.querySelectorAll("PRE"); let l = pres.length; let pre, walker, node, value, contents, index; while (l--) { pre = pres[l]; walker = createTreeWalker(pre, SHOW_TEXT); while (node = walker.nextNode()) { value = node.data; value = value.replace(/ (?=)/g, NBSP); // sp -> nbsp contents = document.createDocumentFragment(); while ((index = value.indexOf("\n")) > -1) { contents.append( document.createTextNode(value.slice(0, index)) ); contents.append(createElement("BR")); value = value.slice(index + 1); } node.before(contents); node.data = value; } fixContainer(pre, root); replaceWith(pre, empty(pre)); } return frag; }, range).focus(); } return this.changeFormat(null, { tag: "CODE" }, range); } toggleCode() { return (this.hasFormat("PRE") || this.hasFormat("CODE")) ? this.removeCode() : this.code(); } // SnappyMail changeIndentationLevel(direction) { let parent = this.getSelectionClosest("UL,OL,BLOCKQUOTE"); if (parent || "increase" === direction) { direction += (!parent || "BLOCKQUOTE" === parent.nodeName) ? "Quote" : "List"; return this[direction + "Level"](); } } getSelectionClosest(selector) { return getClosest(this.getSelection().commonAncestorContainer, this._root, selector); } setAttribute(name, value) { let range = this.getSelection(); let start = range?.startContainer || {}; let end = range?.endContainer || {}; if ("dir" == name || (isTextNode(start) && 0 === range.startOffset && start === end && end.length === range.endOffset)) { this._recordUndoState(range); setAttributes(start.parentNode, { [name]: value }); this._docWasChanged(); } else if (null == value) { this._recordUndoState(range); let node = getClosest(range.commonAncestorContainer, this._root, "*"); range.collapsed ? setAttributes(node, { [name]: value }) : node.querySelectorAll("*").forEach((el) => setAttributes(el, { [name]: value })); this._docWasChanged(); } else { this.changeFormat({ tag: "SPAN", attributes: { [name]: value } }, null, range); } return this.focus(); } setStyle(style) { this.setAttribute("style", style); } toggleTag(name, remove) { let range = this.getSelection(); if (this.hasFormat(name, null, range)) { this.changeFormat(null, { tag: name }, range); } else { this.changeFormat({ tag: name }, remove ? { tag: remove } : null, range); } } setRange(range) { this.setSelection(range); this._updatePath(range, true); } setConfig(config) { this._config = mergeObjects({ addLinks: true }, config, true); return this; } } // source/Legacy.ts win.Squire = Squire; })();