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
JavaScript
/*! 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
};