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
459 lines (458 loc) • 20.5 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 p = Object.defineProperty;
var y = (d, e, t) => e in d ? p(d, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : d[e] = t;
var r = (d, e, t) => y(d, typeof e != "symbol" ? e + "" : e, t);
import { DefaultPluginManager as v } from "./src/core/Plugin.mjs";
import { HTMLFormatter as b } from "./src/core/services/HTMLFormatter.mjs";
import { LocaleManager as C } from "./src/core/services/LocaleManager.mjs";
import { TextFormatter as E } from "./src/core/services/TextFormatter/index.mjs";
import { Selector as S } from "./src/core/services/Selector.mjs";
import { NotificationManager as D } from "./src/core/ui/NotificationManager.mjs";
import { DOMContext as u } from "./src/core/DOMContext.mjs";
const l = class l {
constructor(e, t = { mode: "direct" }) {
r(this, "innerContainer");
r(this, "container");
r(this, "plugins");
r(this, "eventHandlers");
r(this, "formatter");
r(this, "localeManager");
r(this, "contentChangeCallbacks", []);
r(this, "mutationObserver");
r(this, "textFormatter", null);
r(this, "selector");
r(this, "_disableObserver", !1);
r(this, "notificationManager");
r(this, "boundHandleSelectionChange");
r(this, "boundClickToFocus");
r(this, "boundDragStart");
r(this, "boundDragEnd");
r(this, "boundDragEnter");
r(this, "boundDragOver");
r(this, "boundDragLeave");
r(this, "boundDrop");
r(this, "boundPaste");
r(this, "domContext");
r(this, "options");
this.options = t, this.domContext = new u(t.shadowRoot || void 0, t.iframe || void 0);
let n = e;
t.mode === "shadowRoot" ? n = this.setupShadowRootMode(e) : t.mode === "iframe" && (n = this.setupIframeMode(e)), this.container = document.createElement("div"), this.container.className = "html-editor", this.container.contentEditable = "true", this.localeManager = new C(), this.notificationManager = D.getInstance(), this.localeManager.initialize().catch((o) => {
console.error("Failed to initialize LocaleManager:", o);
}), n.appendChild(this.container), this.innerContainer = e, this.plugins = new v(), this.eventHandlers = /* @__PURE__ */ new Map(), this.formatter = new b(), this.textFormatter = new E(
this.container,
t.shadowRoot || void 0,
t.formatDebug
), this.selector = new S(this.container), this.boundClickToFocus = (o) => {
o.target.closest(".editor-block") || this.container.focus();
}, this.boundDragStart = (o) => this.handleDragStart(o), this.boundDragEnd = (o) => this.handleDragEnd(o), this.boundDragEnter = (o) => this.handleDragEnter(o), this.boundDragOver = (o) => this.handleDragOver(o), this.boundDragLeave = (o) => this.handleDragLeave(o), this.boundDrop = (o) => this.handleDrop(o), this.boundPaste = (o) => this.handlePaste(o), this.container.addEventListener("click", this.boundClickToFocus), this.container.addEventListener("dragstart", this.boundDragStart), this.container.addEventListener("dragend", this.boundDragEnd), this.container.addEventListener("dragenter", this.boundDragEnter), this.container.addEventListener("dragover", this.boundDragOver), this.container.addEventListener("dragleave", this.boundDragLeave), this.container.addEventListener("drop", this.boundDrop), this.container.addEventListener("paste", this.boundPaste), this.boundHandleSelectionChange = (o) => this.handleSelectionChange(o), this.domContext.addEventListener("selectionchange", this.boundHandleSelectionChange), this.mutationObserver = new MutationObserver((o) => this.handleMutations(o)), this.mutationObserver.observe(this.container, {
childList: !0,
// Отслеживаем изменения в дочерних элементах
subtree: !0,
// Отслеживаем изменения во всем поддереве
characterData: !0,
// Отслеживаем изменения текста
attributes: !0
// Отслеживаем изменения атрибутов
});
}
syncDocumentStylesToShadowRoot(e) {
var t;
e.querySelectorAll(`[${l.STYLE_SYNC_ATTR}]`).forEach((n) => n.remove()), (t = document.adoptedStyleSheets) != null && t.length && (e.adoptedStyleSheets = Array.from(document.adoptedStyleSheets)), document.querySelectorAll("style").forEach((n) => {
const o = document.createElement("style");
o.setAttribute(l.STYLE_SYNC_ATTR, ""), o.textContent = n.textContent || "", e.appendChild(o);
}), document.querySelectorAll(
'link[rel="stylesheet"], link[rel="modulepreload"], link[rel="preload"], link[rel="preload stylesheet"]'
).forEach((n) => {
const o = document.createElement("link");
o.setAttribute(l.STYLE_SYNC_ATTR, ""), o.rel = "stylesheet", o.href = n.href, e.appendChild(o);
});
}
// Применение стилей к Shadow DOM
applyStylesToShadowDOM(e) {
const t = () => this.syncDocumentStylesToShadowRoot(e);
document.readyState === "complete" ? t() : window.addEventListener("load", t, { once: !0 });
}
// Применение стилей к iframe
applyStylesToIframe(e) {
const t = () => {
try {
document.querySelectorAll("style").forEach((i) => {
const a = e.createElement("style");
a.textContent = i.textContent || "", e.head.appendChild(a);
}), document.querySelectorAll(
'link[rel="modulepreload"], link[rel="preload"], link[rel="preload stylesheet"]'
).forEach((i) => {
const a = e.createElement("link");
a.rel = "stylesheet", a.href = i.href, e.head.appendChild(a);
}), document.adoptedStyleSheets && e.adoptedStyleSheets && (e.adoptedStyleSheets = Array.from(document.adoptedStyleSheets));
} catch (n) {
console.warn("Failed to apply styles to iframe:", n);
}
};
document.readyState === "complete" ? t() : window.addEventListener("load", t);
}
setupShadowRootMode(e) {
if (this.options.shadowRoot)
return this.applyStylesToShadowDOM(this.options.shadowRoot), e;
{
const t = e.attachShadow({ mode: "open" });
this.options.shadowRoot = t, this.domContext = new u(t), this.applyStylesToShadowDOM(t);
const n = document.createElement("div");
return n.className = "shadow-editor-container", t.appendChild(n), n;
}
}
setupIframeMode(e) {
var t;
if (this.options.iframe)
return (t = this.options.iframe.contentDocument) != null && t.body ? this.options.iframe.contentDocument.body : e;
{
const n = document.createElement("iframe");
n.style.border = "none", n.style.width = "100%", n.style.height = "100%", n.style.minHeight = "500px", n.style.display = "block", n.style.overflow = "hidden", e.appendChild(n);
const o = new Promise((i) => {
n.onload = () => {
n.contentDocument && (n.contentDocument.body.style.margin = "0", n.contentDocument.body.style.padding = "0", n.contentDocument.body.style.minHeight = "100vh", n.contentDocument.body.style.height = "100%", n.contentDocument.body.style.width = "100%", n.contentDocument.body.style.overflow = "auto", this.applyStylesToIframe(n.contentDocument), this.container.parentElement && n.contentDocument.body.appendChild(this.container), this.domContext = new u(void 0, n), i());
};
});
return this.iframeReady = o, this.options.iframe = n, e;
}
}
async handleSelectionChange(e) {
var i;
const t = (i = this.textFormatter) == null ? void 0 : i.getSelection();
if (!t || t.rangeCount === 0) return;
const o = t.getRangeAt(0).commonAncestorContainer;
this.isSelectionInsideEditor(o) && this.triggerEvent("selectionchange", { event: e });
}
/**
* Проверяет, находится ли выделение внутри контейнера редактора.
*/
isSelectionInsideEditor(e) {
return e.nodeType === Node.TEXT_NODE ? this.container.contains(e.parentElement) : e.nodeType === Node.ELEMENT_NODE ? this.container.contains(e) : !1;
}
handleMutations(e) {
e.some((n) => n.type === "childList" || // Изменения в дочерних элементах
n.type === "characterData" || // Изменения в тексте
n.type === "attributes") && this.handleContentChange();
}
async handlePaste(e) {
e.preventDefault();
const t = e.clipboardData;
if (!t) return;
const n = t.files, o = t.getData("text");
n.length > 0 ? this.handleFileDrop(n, e) : o && this.handleTextDrop(o);
}
handleDragStart(e) {
this.triggerEvent("drag-start", { e });
}
handleDragEnd(e) {
this.triggerEvent("drag-end", { e });
}
handleDragEnter(e) {
e.preventDefault(), this.container.classList.add("drag-overlay"), this.container.draggable = !0, this.triggerEvent("drag-enter", { e });
}
handleDragOver(e) {
e.preventDefault(), e.dataTransfer && (e.dataTransfer.dropEffect = "copy"), this.triggerEvent("drag-over", { e });
}
handleDragLeave(e) {
e.preventDefault(), this.container.classList.remove("drag-overlay"), this.container.draggable = !1, this.triggerEvent("drag-leave", { e });
}
handleDrop(e) {
if (e.preventDefault(), this.container.classList.remove("drag-overlay"), this.container.draggable = !1, e.dataTransfer) {
const t = e.dataTransfer.files, n = e.dataTransfer.getData("text");
t.length > 0 ? this.handleFileDrop(t, e) : n && this.handleTextDrop(n);
}
this.triggerEvent("drag-drop", { e });
}
async handleFileDrop(e, t) {
for (let n = 0; n < e.length; n++) {
const o = e[n], i = o.type, a = o.name, s = await this.readFileContent(o);
this.triggerEvent("file-drop", { type: i, name: a, content: s, e: t });
}
}
handleTextDrop(e) {
this.insertTextAtCursor(e), this.triggerEvent("text-drop", e);
}
async readFileContent(e) {
return new Promise((t, n) => {
const o = new FileReader();
o.onload = () => {
o.result ? t(o.result) : n(new Error("Failed to read file"));
}, o.onerror = () => n(o.error), e.type.startsWith("text/") ? o.readAsText(e) : o.readAsDataURL(e);
});
}
insertTextAtCursor(e) {
var n;
const t = (n = this.textFormatter) == null ? void 0 : n.getSelection();
if (t && t.rangeCount > 0) {
const o = t.getRangeAt(0);
o.deleteContents(), o.insertNode(document.createTextNode(e)), o.collapse(!1);
}
}
getContainer() {
return this.container;
}
getInnerContainer() {
return this.innerContainer;
}
getTextFormatter() {
return this.textFormatter;
}
getSelector() {
return this.selector;
}
getToolbar() {
const e = this.plugins.getPlugin("toolbar");
return e && "getToolbar" in e ? e.getToolbar() : null;
}
getDOMContext() {
return this.domContext;
}
getMode() {
return this.options.mode;
}
getShadowRoot() {
return this.options.shadowRoot;
}
getIframe() {
return this.options.iframe;
}
/**
* Adds CSS styles depending on the editor's operating mode
* @param styles - CSS strings or CSS object
*/
addStyle(e) {
var t;
if (this.options.mode === "direct") {
const n = document.createElement("style");
n.textContent = e, document.head.appendChild(n);
} else if (this.options.shadowRoot) {
const n = document.createElement("style");
n.textContent = e, this.options.shadowRoot.appendChild(n);
} else if ((t = this.options.iframe) != null && t.contentDocument) {
const n = this.options.iframe.contentDocument.createElement("style");
n.textContent = e, this.options.iframe.contentDocument.head.appendChild(n);
}
}
/**
* Ожидает готовности iframe (только для iframe режима)
*/
async waitForIframeReady() {
this.options.mode === "iframe" && this.iframeReady && await this.iframeReady;
}
isSelectionInsideContainer(e) {
if (!e || e.rangeCount === 0) return !1;
const t = e.getRangeAt(0);
return this.container.contains(t.startContainer) && this.container.contains(t.endContainer);
}
getSelectionInsideContainer() {
var t;
const e = (t = this.textFormatter) == null ? void 0 : t.getSelection();
return e && this.isSelectionInsideContainer(e) ? e : null;
}
saveCursorPosition() {
const e = this.getSelectionInsideContainer();
if (!e || e.rangeCount === 0) return null;
const t = e.getRangeAt(0), n = t.cloneRange();
return n.selectNodeContents(this.container), n.setEnd(t.startContainer, t.startOffset), { offset: n.toString().length };
}
restoreCursorPosition(e) {
var g;
const t = (g = this.textFormatter) == null ? void 0 : g.getSelection();
if (!t) return;
const n = document.createRange();
let o = e.offset;
const i = document.createTreeWalker(this.container, NodeFilter.SHOW_TEXT);
for (; i.nextNode(); ) {
const c = i.currentNode, h = c.length;
if (o < h) {
n.setStart(c, o), n.collapse(!0), t.removeAllRanges(), t.addRange(n), this.container.focus();
return;
}
if (o === h) {
const f = this.findNextTextNode(c);
if (f) {
n.setStart(f, 0), n.collapse(!0), t.removeAllRanges(), t.addRange(n), this.container.focus();
return;
}
}
o -= h;
}
const a = document.createTreeWalker(this.container, NodeFilter.SHOW_TEXT);
let s = null;
for (; a.nextNode(); )
s = a.currentNode;
s ? (n.setStart(s, s.length), n.collapse(!0)) : (n.selectNodeContents(this.container), n.collapse(!1)), t.removeAllRanges(), t.addRange(n), this.container.focus();
}
findNextTextNode(e) {
const t = document.createTreeWalker(this.container, NodeFilter.SHOW_TEXT);
let n = !1;
for (; t.nextNode(); ) {
if (n)
return t.currentNode;
t.currentNode === e && (n = !0);
}
return null;
}
use(e) {
try {
return this.plugins.register(e), e.initialize(this), !0;
} catch (t) {
return console.warn(`Failed to remove plugin ${e.name}:`, t), !1;
}
}
remove(e) {
try {
const t = typeof e == "string" ? e : e.name;
return this.plugins.unregister(t), !0;
} catch (t) {
const n = typeof e == "string" ? e : e.name;
return console.warn(`Failed to remove plugin ${n}:`, t), !1;
}
}
getPlugins() {
return this.plugins.getPlugins();
}
getHotkeys() {
return this.plugins.getHotkeys();
}
on(e, t) {
return this.eventHandlers.has(e) || this.eventHandlers.set(e, []), this.eventHandlers.get(e).push(t), () => {
const n = this.eventHandlers.get(e);
if (n) {
const o = n.indexOf(t);
o !== -1 && n.splice(o, 1), n.length === 0 && this.eventHandlers.delete(e);
}
};
}
triggerEvent(e, ...t) {
const n = this.eventHandlers.get(e);
n && n.forEach((o) => o(...t));
}
off(e, t) {
if (this.eventHandlers.has(e))
if (t) {
const n = this.eventHandlers.get(e), o = n.indexOf(t);
o !== -1 && n.splice(o, 1), n.length === 0 && this.eventHandlers.delete(e);
} else
this.eventHandlers.delete(e);
}
disableObserver() {
this._disableObserver = !0;
}
enableObserver() {
this._disableObserver = !1;
}
/**
* Обработчик изменений контента
*/
handleContentChange() {
if (this._disableObserver) return;
const e = this.getHtml();
this.contentChangeCallbacks.forEach((t) => t(e));
}
/**
* Подписка на изменения контента
* @param callback Функция, которая будет вызвана при изменении контента
*/
subscribeToContentChange(e) {
return this.contentChangeCallbacks.push(e), () => {
const t = this.contentChangeCallbacks.indexOf(e);
t !== -1 && this.contentChangeCallbacks.splice(t, 1);
};
}
ensureEditorFocus() {
var n;
const e = this.getContainer(), t = (n = this.textFormatter) == null ? void 0 : n.getSelection();
if (t && t.rangeCount > 0) {
const o = t.getRangeAt(0);
if (this.isSelectionInsideEditor(o.commonAncestorContainer))
return;
}
if (e.focus(), !t || !t.rangeCount) {
const o = document.createRange();
o.selectNodeContents(e), o.collapse(!0), t == null || t.removeAllRanges(), t == null || t.addRange(o);
}
}
getHtml() {
const e = this.getContainer();
if (!e) return "";
const n = e.cloneNode(!0).innerHTML;
return this.formatter.format(n);
}
waitForDOMStabilization(e, t = 100) {
return new Promise((n) => {
let o;
const i = new MutationObserver(() => {
clearTimeout(o), o = setTimeout(() => {
i.disconnect(), n();
}, t);
});
i.observe(e, {
childList: !0,
// Отслеживаем изменения в дочерних элементах
subtree: !0,
// Отслеживаем изменения во всем поддереве
attributes: !0,
// Отслеживаем изменения атрибутов
characterData: !0
// Отслеживаем изменения текста
}), o = setTimeout(() => {
i.disconnect(), n();
}, t);
});
}
async setHtml(e) {
await this.waitForDOMStabilization(this.container), this.container.innerHTML = this.formatter.format(e), await new Promise((t) => requestAnimationFrame(t));
}
insertContent(e) {
var n;
this.ensureEditorFocus();
const t = (n = this.textFormatter) == null ? void 0 : n.getSelection();
if (t && t.rangeCount > 0) {
const o = t.getRangeAt(0);
if (o.deleteContents(), typeof e == "string") {
const i = document.createElement("div");
for (i.innerHTML = e; i.firstChild; )
o.insertNode(i.firstChild);
} else
o.insertNode(e);
o.collapse(!1), t.removeAllRanges(), t.addRange(o);
} else {
const o = this.getContainer();
typeof e == "string" ? o.insertAdjacentHTML("beforeend", e) : o.appendChild(e);
}
}
t(e, t = {}) {
return this.localeManager.isReady() ? this.localeManager.translate(e, t) : e;
}
async setLocale(e) {
return await this.localeManager.setLocale(e);
}
getLocale() {
return this.localeManager.getCurrentLocale();
}
// Notification methods
showNotification(e, t = "info", n = 3e3) {
this.notificationManager.show({ message: e, type: t, duration: n });
}
showSuccessNotification(e, t) {
this.notificationManager.success(e, { duration: t });
}
showErrorNotification(e, t) {
this.notificationManager.error(e, { duration: t });
}
showWarningNotification(e, t) {
this.notificationManager.warning(e, { duration: t });
}
showInfoNotification(e, t) {
this.notificationManager.info(e, { duration: t });
}
destroy() {
this.mutationObserver.disconnect(), this.boundClickToFocus && this.container.removeEventListener("click", this.boundClickToFocus), this.boundDragStart && this.container.removeEventListener("dragstart", this.boundDragStart), this.boundDragEnd && this.container.removeEventListener("dragend", this.boundDragEnd), this.boundDragEnter && this.container.removeEventListener("dragenter", this.boundDragEnter), this.boundDragOver && this.container.removeEventListener("dragover", this.boundDragOver), this.boundDragLeave && this.container.removeEventListener("dragleave", this.boundDragLeave), this.boundDrop && this.container.removeEventListener("drop", this.boundDrop), this.boundPaste && this.container.removeEventListener("paste", this.boundPaste), this.boundHandleSelectionChange && (this.domContext.removeEventListener("selectionchange", this.boundHandleSelectionChange), this.boundHandleSelectionChange = void 0), this.contentChangeCallbacks = [], this.plugins.destroy(), this.container.parentElement && this.container.parentElement.removeChild(this.container), this.innerContainer = null, this.container = null, this.plugins = null, this.eventHandlers = null, this.formatter = null, this.localeManager = null, this.mutationObserver = null, this.textFormatter = null;
}
};
r(l, "STYLE_SYNC_ATTR", "data-cm-style-sync");
let m = l;
export {
m as HTMLEditor
};