- // This seems a reasonable approximation of user intent. - - block = getStartBlockOfRange(range, root); - firstBlockInFrag = getNextBlock(frag, frag); - replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock(block); - if (block && firstBlockInFrag && !replaceBlock && // Don't merge table cells or PRE elements into block - !getClosest(firstBlockInFrag, frag, "PRE,TABLE")) { - moveRangeBoundariesUpTree(range, block, block, root); - range.collapse(true); // collapse to start - container = range.endContainer; - offset = range.endOffset; - // Remove trailingFoo
; its format clashes with
- node.querySelectorAll("CODE").forEach(el => detach(el));
- if (output.childNodes.length) {
- output.append(doc.createTextNode("\n"));
- }
- output.append(empty(node));
- }
- // 4. Replace nbsp with regular sp
- 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() {
- let range = this.getSelection();
- let ancestor = range.commonAncestorContainer;
- let inPre = getClosest(ancestor, this._root, "PRE");
- if (inPre) {
- return this.modifyBlocks(frag => {
- let root = this._root;
- let 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 = doc.createDocumentFragment();
- while ((index = value.indexOf("\n")) > -1) {
- contents.append(
- doc.createTextNode(value.slice(0, index))
- );
- contents.append(createElement("BR"));
- value = value.slice(index + 1);
- }
- node.before(contents);
- node.data = value;
- }
- fixContainer(pre, root);
- pre.replaceWith(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();
- }
-
- 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);
- }
- }
-
- // --- Formatting ---
-
- setStyle(style) {
- this.setAttribute("style", style);
- }
-
- setAttribute(name, value) {
- let range = this.getSelection();
- let start = range?.startContainer || {};
- let end = range ? range.endContainer : 0;
- // When the selection is all the text inside an element, set style on the element itself
- if ("dir" == name || (isTextNode(start) && 0 === range.startOffset && start === end && end.length === range.endOffset)) {
- this._recordUndoState(range);
- setAttributes(start.parentNode, {[name]: value});
-// this.setRange(range);
- this._docWasChanged();
- }
- // Else when it should remove the attribute
- 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.setRange(range);
- this._docWasChanged();
- }
- // Else create a span element
- else {
- this.changeFormat({
- tag: "SPAN",
- attributes: {[name]: value}
- }, null, range);
- }
- return this.focus();
- }
-
- // ---
-
- changeIndentationLevel(direction) {
- let parent = this.getSelectionClosest('UL,OL,BLOCKQUOTE');
- if (parent || "increase" === direction) {
- let method = (!parent || "BLOCKQUOTE" === parent.nodeName) ? "Quote" : "List";
- return this[direction + method + "Level"]();
- }
- }
-
- bidi(dir) {
- return this.modifyBlocks(frag => setDirection(this, frag, dir)).focus();
- }
-
- setRange(range) {
- this.setSelection(range);
- // Path may have changed
- this._updatePath(range, true);
- }
-
- setConfig(config) {
- this._config = mergeObjects({
- addLinks: true
- }, config, true);
- return this;
- }
-}
-
-win.Squire = Squire;
-
-})(document);
+ 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 = "" + blockTag + ">",
+ 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;
+})();