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

459 lines (458 loc) 20.5 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 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 };