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
177 lines (176 loc) • 11.3 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 y = Object.defineProperty;
var L = (p, e, t) => e in p ? y(p, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : p[e] = t;
var s = (p, e, t) => L(p, typeof e != "symbol" ? e + "" : e, t);
class M {
constructor(e, t, i = {}) {
s(this, "menu");
s(this, "editor");
s(this, "activeElement", null);
s(this, "subMenus", []);
// Для управления вложенными меню
s(this, "orientation");
s(this, "buttons", []);
s(this, "options");
s(this, "isVisible", !1);
s(this, "longPressTimer", null);
s(this, "longPressDelay", 500);
// Задержка для долгого нажатия в миллисекундах
s(this, "touchStartX", 0);
s(this, "touchStartY", 0);
s(this, "handleTouchStart", (e) => {
e.preventDefault(), this.touchStartX = e.touches[0].clientX, this.touchStartY = e.touches[0].clientY, this.longPressTimer = window.setTimeout(() => {
this.showContextMenu(e.touches[0].clientX, e.touches[0].clientY);
}, this.longPressDelay);
});
s(this, "handleTouchMove", (e) => {
const t = e.touches[0].clientX, i = e.touches[0].clientY, a = Math.abs(t - this.touchStartX), c = Math.abs(i - this.touchStartY);
(a > 10 || c > 10) && this.cancelLongPress();
});
s(this, "handleTouchEnd", (e) => {
this.cancelLongPress();
});
s(this, "handleTouchCancel", (e) => {
this.cancelLongPress();
});
s(this, "handleClickOutside", (e) => {
this.menu.contains(e.target) || this.hide();
});
s(this, "handleEscapeKey", (e) => {
e.key === "Escape" && this.hide();
});
this.orientation = i.orientation || "vertical", this.buttons = t, this.editor = e, this.options = {
closeOnClickOutside: !0,
closeOnEscape: !0,
animation: !0,
maxHeight: 400,
maxWidth: 300,
...i
}, this.menu = this.createMenu(), this.editor.getDOMContext().appendChild(this.menu), this.setupEventListeners();
}
createMenu() {
const e = document.createElement("div");
return e.className = `context-menu ${this.orientation === "horizontal" ? "horizontal" : ""}`, this.options.maxHeight && (e.style.maxHeight = `${this.options.maxHeight}px`), this.options.maxWidth && (e.style.maxWidth = `${this.options.maxWidth}px`), this.buttons.forEach((t) => {
if (t.type === "divider") {
const i = document.createElement("div");
i.className = `menu-divider ${this.orientation === "horizontal" ? "horizontal" : ""}`, e.appendChild(i);
} else if (t.type === "group" && t.groupTitle) {
const i = document.createElement("div");
i.className = "menu-group";
const a = document.createElement("div");
a.className = "menu-group-title", a.textContent = t.groupTitle, i.appendChild(a), t.subMenu && t.subMenu.forEach((c) => {
const h = this.createMenuItem(c);
h && i.appendChild(h);
}), e.appendChild(i);
} else if (t.type === "custom" && t.customHTML) {
const i = document.createElement("div");
i.className = "menu-custom", i.innerHTML = t.customHTML, e.appendChild(i);
} else {
const i = this.createMenuItem(t);
i && e.appendChild(i);
}
}), e;
}
createMenuItem(e) {
const t = document.createElement("button");
t.className = `menu-item ${e.className || ""}`, t.dataset.action = e.action || "", t.dataset.type = e.type || "button", e.variant && (t.dataset.type = e.variant), e.title && (t.title = e.title);
const i = e.icon ? `<span class="menu-icon">${e.icon}</span>` : "", a = e.label || e.title || "", c = a ? `<span class="menu-label">${a}</span>` : "", h = e.hotkey ? `<span class="menu-hotkey">${e.hotkey}</span>` : "", m = e.subMenu ? '<span class="menu-submenu-indicator">▶</span>' : "";
if (e.type === "checkbox" || e.type === "radio") {
const o = `<span class="menu-${e.type}"></span>`;
t.innerHTML = `${o}${i}${c}${h}`, e.checked && (t.dataset.checked = "true");
} else
t.innerHTML = e.iconPosition === "right" ? `${c}${i}${h}${m}` : `${i}${c}${h}${m}`;
if (typeof e.disabled == "function" ? t.disabled = e.disabled() : t.disabled = e.disabled || !1, e.onClick && t.addEventListener("click", (o) => {
var n;
o.preventDefault(), o.stopPropagation(), (n = e.onClick) == null || n.call(e, this.activeElement), e.closeOnClick !== !1 && this.hide();
}), e.subMenu) {
const o = new M(this.editor, e.subMenu, {
...this.options,
maxHeight: Math.min(this.options.maxHeight || 400, 300)
});
this.subMenus.push(o), t.addEventListener("mouseenter", () => {
this.subMenus.forEach((u) => {
u !== o && u.hide();
});
const n = t.getBoundingClientRect(), v = n.right + 5, f = n.top;
o.show(t, v, f);
}), t.addEventListener("mouseleave", (n) => {
setTimeout(() => {
const v = document.elementFromPoint(n.clientX, n.clientY), f = o.menu.contains(v), u = o.menu.contains(n.relatedTarget), l = o.menu.getBoundingClientRect(), r = t.getBoundingClientRect(), d = {
left: Math.min(r.right, l.left) - 10,
right: Math.max(r.right, l.left) + 10,
top: Math.min(r.top, l.top) - 10,
bottom: Math.max(r.bottom, l.bottom) + 10
}, E = n.clientX >= d.left && n.clientX <= d.right && n.clientY >= d.top && n.clientY <= d.bottom, g = n.clientY >= Math.min(r.top, l.top) - 20 && n.clientY <= Math.max(r.bottom, l.bottom) + 20;
!f && !u && !E && !g && o.hide();
}, 50);
}), o.menu.addEventListener("mouseenter", () => {
}), o.menu.addEventListener("mouseleave", (n) => {
setTimeout(() => {
const v = document.elementFromPoint(n.clientX, n.clientY), f = t.contains(v), u = t.contains(n.relatedTarget), l = o.menu.getBoundingClientRect(), r = t.getBoundingClientRect(), d = {
left: Math.min(r.right, l.left) - 10,
right: Math.max(r.right, l.left) + 10,
top: Math.min(r.top, l.top) - 10,
bottom: Math.max(r.bottom, l.bottom) + 10
}, E = n.clientX >= d.left && n.clientX <= d.right && n.clientY >= d.top && n.clientY <= d.bottom, g = n.clientY >= Math.min(r.top, l.top) - 20 && n.clientY <= Math.max(r.bottom, l.bottom) + 20;
!f && !u && !E && !g && o.hide();
}, 50);
});
}
return t;
}
setupEventListeners() {
this.options.closeOnClickOutside && this.editor.getDOMContext().addEventListener("click", this.handleClickOutside), this.options.closeOnEscape && this.editor.getDOMContext().addEventListener("keydown", this.handleEscapeKey);
let e = 0, t = 0;
this.menu.addEventListener("touchstart", (i) => {
e = i.touches[0].clientY, t = i.touches[0].clientX;
}), this.menu.addEventListener("touchmove", (i) => {
i.preventDefault();
}), this.menu.addEventListener("touchend", (i) => {
const a = i.changedTouches[0].clientY, c = i.changedTouches[0].clientX, h = Math.abs(a - e), m = Math.abs(c - t);
(h > 50 || m > 50) && this.hide();
});
}
setupLongPressListeners() {
this.activeElement && (this.activeElement.removeEventListener("touchstart", this.handleTouchStart), this.activeElement.removeEventListener("touchmove", this.handleTouchMove), this.activeElement.removeEventListener("touchend", this.handleTouchEnd), this.activeElement.removeEventListener("touchcancel", this.handleTouchCancel)), this.activeElement && (this.activeElement.addEventListener("touchstart", this.handleTouchStart), this.activeElement.addEventListener("touchmove", this.handleTouchMove), this.activeElement.addEventListener("touchend", this.handleTouchEnd), this.activeElement.addEventListener("touchcancel", this.handleTouchCancel));
}
cancelLongPress() {
this.longPressTimer && (clearTimeout(this.longPressTimer), this.longPressTimer = null);
}
showContextMenu(e, t) {
this.activeElement && this.show(this.activeElement, e, t);
}
show(e, t, i) {
this.activeElement = e, this.isVisible = !0, this.setupLongPressListeners(), this.menu.style.display = "flex", this.menu.style.flexDirection = this.orientation === "horizontal" ? "row" : "column", this.options.animation && (this.menu.style.opacity = "0", this.menu.style.transform = "translateY(-10px) scale(0.95)"), this.menu.style.left = `${t}px`, this.menu.style.top = `${i}px`, this.options.animation && requestAnimationFrame(() => {
this.menu.style.opacity = "1", this.menu.style.transform = "translateY(0) scale(1)";
}), requestAnimationFrame(() => {
this.adjustPosition();
});
}
adjustPosition() {
const e = this.menu.getBoundingClientRect(), t = window.innerWidth, i = window.innerHeight, a = parseInt(this.menu.style.left), c = parseInt(this.menu.style.top);
let h = a, m = c;
e.right > t && (h = a - (e.right - t) - 10), e.left < 0 && (h = 10), e.bottom > i && (m = c - (e.bottom - i) - 10), e.top < 0 && (m = 10), (h !== a || m !== c) && (this.menu.style.left = `${h}px`, this.menu.style.top = `${m}px`);
}
hide() {
this.isVisible && (this.isVisible = !1, this.cancelLongPress(), this.activeElement && (this.activeElement.removeEventListener("touchstart", this.handleTouchStart), this.activeElement.removeEventListener("touchmove", this.handleTouchMove), this.activeElement.removeEventListener("touchend", this.handleTouchEnd), this.activeElement.removeEventListener("touchcancel", this.handleTouchCancel)), this.subMenus.forEach((e) => e.hide()), this.options.animation ? (this.menu.style.opacity = "0", this.menu.style.transform = "translateY(-10px) scale(0.95)", setTimeout(() => {
this.menu.style.display = "none", this.activeElement = null;
}, 200)) : (this.menu.style.display = "none", this.activeElement = null));
}
updateButtons(e) {
this.menu.innerHTML = "", this.buttons = e, this.menu.appendChild(this.createMenu());
}
updateButtonState(e, t) {
const i = this.menu.querySelector(`[data-action="${e}"]`);
i && (Object.assign(i.dataset, t), t.disabled !== void 0 && (i.disabled = typeof t.disabled == "function" ? t.disabled() : t.disabled), t.checked !== void 0 && (i.dataset.checked = t.checked.toString()));
}
getElement() {
return this.menu;
}
destroy() {
this.cancelLongPress(), this.activeElement && (this.activeElement.removeEventListener("touchstart", this.handleTouchStart), this.activeElement.removeEventListener("touchmove", this.handleTouchMove), this.activeElement.removeEventListener("touchend", this.handleTouchEnd), this.activeElement.removeEventListener("touchcancel", this.handleTouchCancel)), this.options.closeOnClickOutside && this.editor.getDOMContext().removeEventListener("click", this.handleClickOutside), this.options.closeOnEscape && this.editor.getDOMContext().removeEventListener("keydown", this.handleEscapeKey), this.subMenus.forEach((e) => e.destroy()), this.menu.parentNode && this.menu.parentNode.removeChild(this.menu);
}
}
export {
M as ContextMenu
};