UNPKG

on-codemerge

Version:

A WYSIWYG editor for on-codemerge is a user-friendly interface that allows users to edit and view their code in real time, exactly as it will appear in the final product

262 lines (261 loc) 10.7 kB
/*! on-codemerge v1.3.1 @author Pavel Kuzmin @license MIT @homepage https://s00d.github.io/on-codemerge/ @repository git+https://github.com/s00d/on-codemerge.git Copyright (c) 2026 Pavel Kuzmin - Built on 2026-07-02T13:39:17.947Z */ var x = Object.defineProperty; var y = (d, t, e) => t in d ? x(d, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : d[t] = e; var C = (d, t, e) => y(d, typeof t != "symbol" ? t + "" : t, e); import { getNodePath as u } from "./FormatDebugMap.mjs"; class S { constructor(t) { C(this, "tracer", null); this.container = t; } setTracer(t) { this.tracer = t; } getContainer() { return this.container; } getSelectedRoot(t, e = !1, i) { const s = t.getRangeAt(0); return !s.collapsed || this.hasContentInRange(s) ? this.getDeepestNodes(s, e, i) : e ? this.getCollapsedReadNodes(t) : this.handleCollapsedRange(t, s); } findBlockElementsInRange(t) { const e = /* @__PURE__ */ new Set(), i = document.createTreeWalker(this.container, NodeFilter.SHOW_ELEMENT, { acceptNode: (s) => t.intersectsNode(s) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT }); for (; i.nextNode(); ) { const s = i.currentNode; this.isBlockNode(s) && e.add(s); } return e.size === 0 && t.intersectsNode(this.container) && Array.from(this.container.childNodes).some( (n) => { var r; return n.nodeType === Node.TEXT_NODE && ((r = n.textContent) == null ? void 0 : r.trim()); } ) && !this.container.querySelector("div, p, li, h1, h2, h3, h4, h5, h6") ? [] : Array.from(e); } getDeepestNodes(t, e, i) { var a, l, h, N, p, m, T, E; const s = []; if (!(this.container.contains(t.commonAncestorContainer) || this.container.getRootNode().contains(t.commonAncestorContainer))) return (a = this.tracer) == null || a.step("getDeepestNodes", [], "outside_container"), []; e || ((l = this.tracer) == null || l.step("splitBoundaryNodes", { startOffset: t.startOffset, endOffset: t.endOffset }), this.splitBoundaryNodes(t)); const r = this.collectTextNodesInRange(t); (h = this.tracer) == null || h.step( "collectTextNodes", r.map((f) => { var o; return { path: u(f, this.container), text: (o = f.textContent) == null ? void 0 : o.slice(0, 30) }; }) ); for (const f of r) { if (!((N = f.textContent) != null && N.trim())) continue; const o = this.findFormattingParent(f); if (o) { const c = this.isNodeFullySelected(o, t); (p = this.tracer) == null || p.step( "formattingParent", { path: u(o, this.container), tag: o.tagName }, c ? "fully_selected" : "partial_selection" ); } if (o && this.isNodeFullySelected(o, t) && !s.includes(o)) { s.push(o); continue; } if (!e && o && i && this.styleCommandOnElement(o, i) && !this.isNodeFullySelected(o, t)) { const c = this.isolatePartialFormattedSegment(o, t); (m = this.tracer) == null || m.step( "isolatePartialFormattedSegment", c ? u(c, this.container) : null, "partial_in_formatted_parent" ), c && !s.includes(c) && s.push(c); continue; } if (e && o && !s.includes(o)) { s.push(o); continue; } if (e) o && !s.includes(o) && s.push(o); else { const c = this.createSpanIfNeeded(f, t); c && ((T = this.tracer) == null || T.step( "createSpanIfNeeded", u(c, this.container), c === o ? "reuse_parent" : "new_span" )), c && !s.includes(c) && s.push(c); } } return (E = this.tracer) == null || E.setNodesToModify(s, this.container), s; } collectTextNodesInRange(t) { const e = [], i = document.createTreeWalker(t.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (n) => t.intersectsNode(n) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT }), s = t.startContainer; if (s.nodeType === Node.TEXT_NODE) t.intersectsNode(s) && e.push(s); else if (s.nodeType === Node.ELEMENT_NODE) { const n = s.childNodes[t.startOffset]; (n == null ? void 0 : n.nodeType) === Node.TEXT_NODE && t.intersectsNode(n) && e.push(n); } for (i.currentNode = s; i.nextNode(); ) { const n = i.currentNode; if (!t.intersectsNode(n) || t.endContainer === n && t.endOffset === 0) break; if (n === t.endContainer && t.endContainer.nodeType === Node.TEXT_NODE) { t.endOffset > 0 && !e.includes(n) && e.push(n); break; } e.includes(n) || e.push(n); } return e.length === 0 && t.startContainer.nodeType === Node.TEXT_NODE && t.intersectsNode(t.startContainer) && e.push(t.startContainer), e; } splitBoundaryNodes(t) { const e = t.endContainer, i = t.endOffset; e.nodeType === Node.TEXT_NODE && i > 0 && i < e.length && e.splitText(i); const s = t.startContainer, n = t.startOffset; if (s.nodeType === Node.TEXT_NODE && n > 0 && n < s.length) { const r = s.splitText(n); e === s && t.setEnd(r, i - n), t.setStart(r, 0); } } isolatePartialFormattedSegment(t, e) { var r, a; const i = document.createElement("span"); this.copyFormattingAttributes(t, i); const s = e.extractContents(); return i.appendChild(s), e.startContainer === t && e.startOffset === 0 || e.startContainer === t.firstChild && e.startOffset === 0 && t.firstChild === t.childNodes[0] ? (r = t.parentNode) == null || r.insertBefore(i, t) : (a = t.parentNode) == null || a.insertBefore(i, t.nextSibling), this.unwrapIfEmpty(t), i; } styleCommandOnElement(t, e) { const s = { bold: "format-bold", italic: "format-italic", underline: "format-underline", strikethrough: "format-strikethrough", superscript: "format-superscript", subscript: "format-subscript" }[e]; return !!(s && t.classList.contains(s) || e === "bold" && t.tagName === "B" || e === "italic" && t.tagName === "I" || e === "underline" && t.tagName === "U"); } collectTextNodesInRangeForRead(t) { return this.collectTextNodesInRange(t); } isNodeFullySelected(t, e) { var r; if (!((r = t.textContent) != null && r.trim())) return !1; const i = document.createTreeWalker(t, NodeFilter.SHOW_TEXT); let s, n = !1; for (; s = i.nextNode(); ) { const a = s.textContent; if (!(a != null && a.trim())) continue; if (n = !0, !e.intersectsNode(s)) return !1; const l = a.length, h = s === e.startContainer ? e.startOffset : 0, N = s === e.endContainer ? e.endOffset : l; if (h > 0 || N < l) return !1; } return n; } findFormattingParent(t) { let e = t.parentElement; for (; e && e !== this.container; ) { if (this.isFormattingElement(e)) return e; e = e.parentElement; } return null; } isFormattingElement(t) { if (this.isTableNode(t) || this.isBlockNode(t)) return !1; const e = t.tagName; return ["B", "I", "U", "STRIKE", "S", "SUB", "SUP"].includes(e) || e === "A" && t.hasAttribute("href") ? !0 : e === "SPAN" ? t.classList.length > 0 || !!t.getAttribute("style") || Array.from(t.classList).some((i) => i.startsWith("format-")) : Array.from(t.classList).some((i) => i.startsWith("format-")); } getCollapsedReadNodes(t) { const e = t.anchorNode; if (!e) return []; if (!(this.container.contains(e) || this.container.getRootNode().contains(e))) return []; const s = []; let n = e.nodeType === Node.TEXT_NODE ? e.parentElement : e; for (; n && n !== this.container; ) this.isFormattingElement(n) && s.push(n), n = n.parentElement; return s; } handleCollapsedRange(t, e) { var a, l; const i = t.anchorNode; if (!i) return []; if (!(this.container.contains(i) || this.container.getRootNode().contains(i))) return []; const n = this.findFormattingParent(i); if (n) return (a = this.tracer) == null || a.step( "handleCollapsedRange", u(n, this.container), "reuse_formatting_parent" ), [n]; const r = document.createElement("span"); return r.textContent = "​", e.insertNode(r), e.setStart(r, 1), e.setEnd(r, 1), t.removeAllRanges(), t.addRange(e), (l = this.tracer) == null || l.step( "handleCollapsedRange", u(r, this.container), "insert_zwsp_span" ), [r]; } hasContentInRange(t) { return t.toString().trim().length > 0 || t.cloneContents().childNodes.length > 0; } createSpanIfNeeded(t, e) { if (t.nodeType !== Node.TEXT_NODE || !t.textContent) return null; const i = t.textContent, s = t === e.startContainer ? e.startOffset : 0, n = t === e.endContainer ? e.endOffset : i.length; if (n === 0 || s === i.length || s === n) return null; const r = this.findFormattingParent(t); if (s === 0 && n === i.length && r && this.isNodeFullySelected(r, e)) return r; const a = document.createElement("span"), l = i.substring(s, n); return a.textContent = l, this.replaceNodeWithSpan(t, a, s, n), a; } copyFormattingAttributes(t, e) { Array.from(t.classList).forEach((i) => e.classList.add(i)), t.style.cssText && (e.style.cssText = t.style.cssText); } replaceNodeWithSpan(t, e, i, s) { const n = t.textContent, r = t.parentNode; if (!r || !n) return; const a = document.createTextNode(n.substring(0, i)), l = document.createTextNode(n.substring(s)); r.insertBefore(a, t), r.insertBefore(e, t), r.insertBefore(l, t), r.removeChild(t); } isTableNode(t) { return t ? !!(t.closest("table") || t.closest("tr") || t.closest("td") || t.closest("th")) : !1; } isBlockNode(t) { return t ? t.classList.contains("block-content") ? !0 : ["DIV", "P", "LI", "H1", "H2", "H3", "H4", "H5", "H6"].includes(t.tagName) : !1; } applyStyleToNode(t, e, i) { if (t.nodeType === Node.ELEMENT_NODE) { const s = t; (!this.isBlockNode(s) || this.isFormattingElement(s)) && i(s, e); } } removeStyleFromNode(t, e, i) { if (t.nodeType !== Node.ELEMENT_NODE) return; const s = t; !this.isFormattingElement(s) && !s.classList.length || (i(s, e), this.unwrapIfEmpty(s)); } unwrapIfEmpty(t) { if (!t.getAttribute("style") && t.classList.length === 0 && t.tagName === "SPAN") { const e = t.parentNode; for (; t.firstChild; ) e == null || e.insertBefore(t.firstChild, t); e == null || e.removeChild(t); } } } export { S as DomUtils };