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
398 lines (397 loc) • 18.4 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 E = Object.defineProperty;
var T = (x, e, i) => e in x ? E(x, e, { enumerable: !0, configurable: !0, writable: !0, value: i }) : x[e] = i;
var l = (x, e, i) => T(x, typeof e != "symbol" ? e + "" : e, i);
import { PopupManager as L } from "../../../core/ui/PopupManager.mjs";
import { ChartRenderer as S } from "../services/ChartRenderer.mjs";
import { MultiSeriesDataEditor as A } from "./MultiSeriesDataEditor.mjs";
import { ChartDataEditor as v } from "./ChartDataEditor.mjs";
import { CHART_TYPE_CONFIGS as f } from "../constants/chartTypes.mjs";
import { createContainer as o, createSpan as m, createButton as P } from "../../../utils/helpers.mjs";
const w = [
{
key: "bar-sales",
name: "Bar: Sales",
type: "bar",
data: [
{ label: "Jan", value: 120 },
{ label: "Feb", value: 90 },
{ label: "Mar", value: 150 },
{ label: "Apr", value: 80 },
{ label: "May", value: 200 }
]
},
{
key: "pie-expenses",
name: "Pie: Expenses",
type: "pie",
data: [
{ label: "Rent", value: 40 },
{ label: "Salary", value: 30 },
{ label: "Marketing", value: 15 },
{ label: "Other", value: 15 }
]
},
{
key: "line-visitors",
name: "Line: Visitors",
type: "line",
data: [
{ label: "Mon", value: 100 },
{ label: "Tue", value: 120 },
{ label: "Wed", value: 90 },
{ label: "Thu", value: 140 },
{ label: "Fri", value: 180 },
{ label: "Sat", value: 220 },
{ label: "Sun", value: 160 }
]
},
{
key: "doughnut-browsers",
name: "Doughnut: Browsers",
type: "doughnut",
data: [
{ label: "Chrome", value: 65 },
{ label: "Firefox", value: 15 },
{ label: "Safari", value: 10 },
{ label: "Edge", value: 7 },
{ label: "Other", value: 3 }
]
}
];
class U {
constructor(e) {
l(this, "editor");
l(this, "popup");
l(this, "renderer");
l(this, "currentEditor");
l(this, "selectedType", "bar");
l(this, "onInsert", null);
l(this, "previewTimeout", null);
l(this, "editingChart", null);
l(this, "chartTitle", "");
l(this, "xAxisLabel", "");
l(this, "yAxisLabel", "");
l(this, "showLegend", !0);
l(this, "chartWidth", 800);
l(this, "chartHeight", 400);
l(this, "showGrid", !0);
l(this, "chartMode", "default");
l(this, "chartOrientation", "vertical");
this.editor = e, this.renderer = new S(e), this.popup = new L(e, {
title: e.t("Insert"),
className: "chart-menu",
closeOnClickOutside: !0,
buttons: [
{
label: e.t("Cancel"),
variant: "secondary",
onClick: () => this.popup.hide()
},
{
label: e.t("Insert"),
variant: "primary",
onClick: () => this.handleSubmit()
}
],
items: this.createPopupItems()
// Динамически создаем элементы
}), this.currentEditor = new v(
e,
(i) => this.schedulePreviewUpdate([{ name: "Series 1", data: i }])
);
}
createPopupItems() {
return [
// 1. Chart type selector
{
type: "custom",
id: "chart-type-selector",
content: () => this.createChartTypeSelector()
},
// 2. Заголовки (title, x/y axis)
{
type: "custom",
id: "meta-fields",
content: () => this.createMetaFields()
},
// 3. Размеры графика
{
type: "custom",
id: "chart-dimensions",
content: () => this.createDimensionsSelector()
},
// 4. Настройки отображения
{
type: "custom",
id: "display-settings",
content: () => this.createDisplaySettings()
},
// 5. Templates (над редактором данных)
{
type: "custom",
id: "template-selector",
content: () => this.createTemplateSelector()
},
// 6. Data editor (теперь над предпросмотром)
{
type: "custom",
id: "data-editor-container",
content: () => this.createDataEditorContainer()
},
// 7. Preview (график)
{
type: "custom",
id: "preview-container",
content: () => this.createPreviewContainer()
},
// 8. Export (под графиком)
{
type: "custom",
id: "export-btn",
content: () => this.createExportButton()
}
];
}
createMetaFields() {
const e = o("meta-fields flex flex-col gap-3 mb-4"), i = o("flex flex-col gap-1"), r = m("text-sm font-medium text-gray-700");
r.textContent = this.editor.t("Chart Title");
const t = document.createElement("input");
t.type = "text", t.placeholder = this.editor.t("Enter chart title..."), t.value = this.chartTitle, t.className = "meta-input px-3 py-2 rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all", t.oninput = (u) => {
this.chartTitle = u.target.value, this.schedulePreviewUpdate(this.currentEditor.getData());
}, i.appendChild(r), i.appendChild(t);
const s = o("flex flex-col gap-1"), a = m("text-sm font-medium text-gray-700");
a.textContent = this.editor.t("X Axis Label");
const n = document.createElement("input");
n.type = "text", n.placeholder = this.editor.t("Enter X axis label..."), n.value = this.xAxisLabel, n.className = "meta-input px-3 py-2 rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all", n.oninput = (u) => {
this.xAxisLabel = u.target.value, this.schedulePreviewUpdate(this.currentEditor.getData());
}, s.appendChild(a), s.appendChild(n);
const d = o("flex flex-col gap-1"), c = m("text-sm font-medium text-gray-700");
c.textContent = this.editor.t("Y Axis Label");
const h = document.createElement("input");
return h.type = "text", h.placeholder = this.editor.t("Enter Y axis label..."), h.value = this.yAxisLabel, h.className = "meta-input px-3 py-2 rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all", h.oninput = (u) => {
this.yAxisLabel = u.target.value, this.schedulePreviewUpdate(this.currentEditor.getData());
}, d.appendChild(c), d.appendChild(h), e.appendChild(i), e.appendChild(s), e.appendChild(d), e;
}
createTemplateSelector() {
const e = o("template-selector flex flex-col gap-2 mb-4"), i = m("text-sm font-medium text-gray-700");
i.textContent = this.editor.t("Templates"), e.appendChild(i);
const r = document.createElement("select");
r.className = "template-select px-3 py-2 rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all bg-white";
const t = document.createElement("option");
return t.value = "", t.textContent = this.editor.t("Select template..."), r.appendChild(t), w.forEach((s) => {
const a = document.createElement("option");
a.value = s.key, a.textContent = s.name, r.appendChild(a);
}), r.onchange = (s) => {
var n, d;
const a = s.target.value;
if (a) {
const c = w.find((h) => h.key === a);
if (c) {
this.selectedType = c.type, this.updateEditor(c.type);
const h = (n = this.popup.getElement()) == null ? void 0 : n.querySelectorAll("[data-type]");
h == null || h.forEach((y) => y.classList.remove("selected"));
const u = (d = this.popup.getElement()) == null ? void 0 : d.querySelector(`[data-type="${c.type}"]`);
u && u.classList.add("selected"), setTimeout(() => {
this.currentEditor && ("setData" in this.currentEditor && this.currentEditor.setData(c.data), this.updatePreview(c.data));
}, 100);
}
}
}, e.appendChild(r), e;
}
createChartTypeSelector() {
const e = o("chart-type-selector"), i = m("text-sm font-medium text-gray-700 mb-4 block");
i.textContent = this.editor.t("Chart Type"), e.appendChild(i);
const r = o("grid");
return Object.entries(f).forEach(([t, s]) => {
const a = o(
`chart-type-option ${t === this.selectedType ? "selected" : ""}`
);
a.setAttribute("data-type", t);
const n = o("w-10 h-10 mb-3 mx-auto flex items-center justify-center");
n.innerHTML = s.icon;
const d = m();
d.textContent = this.editor.t(s.name), a.appendChild(n), a.appendChild(d), a.onclick = () => {
r.querySelectorAll(".chart-type-option").forEach((c) => {
c.classList.remove("selected");
}), a.classList.add("selected"), this.selectedType = t, this.updateEditor(t);
}, r.appendChild(a);
}), e.appendChild(r), e;
}
createDataEditorContainer() {
const e = o("data-editor-container mb-6");
return this.updateEditor("bar", e), e;
}
createPreviewContainer() {
const e = o(
"preview-container h-64 bg-gray-50 rounded-lg flex items-center justify-center"
);
return e.innerHTML = '<div class="text-gray-400">Chart preview will appear here</div>', e;
}
createExportButton() {
const e = o("export-container flex justify-center mt-4"), i = P("", () => this.exportPreviewAsPNG());
i.className = "export-btn px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-green-600 text-white hover:from-green-600 hover:to-green-700 transition-all duration-200 shadow-md hover:shadow-lg flex items-center gap-2 font-medium";
const r = o("w-4 h-4");
r.innerHTML = '<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>';
const t = m();
return t.textContent = this.editor.t("Export as PNG"), i.appendChild(r), i.appendChild(t), e.appendChild(i), e;
}
exportPreviewAsPNG() {
var r, t, s;
const e = (r = this.popup.getElement()) == null ? void 0 : r.querySelector(".preview-container");
if (!e) return;
const i = e.querySelector("img.svg-chart");
if (i && i.src) {
const a = document.createElement("a");
a.href = i.src, a.download = "chart.png", (this.editor.getInnerContainer() || document.body).appendChild(a), a.click(), (t = a.parentNode) == null || t.removeChild(a);
} else {
const a = e.querySelector("canvas");
if (a) {
const n = document.createElement("a");
n.href = a.toDataURL("image/png"), n.download = "chart.png", (this.editor.getInnerContainer() || document.body).appendChild(n), n.click(), (s = n.parentNode) == null || s.removeChild(n);
}
}
}
updateEditor(e, i) {
const r = f[e], t = i ?? this.popup.getElement().querySelector(".data-editor-container");
t && (t.innerHTML = "", r.supportsMultipleSeries ? this.currentEditor = new A(
this.editor,
(s) => this.schedulePreviewUpdate(s)
) : this.currentEditor = new v(
this.editor,
(s) => this.schedulePreviewUpdate([
{
name: this.editor.t("Series 1"),
data: s
}
]),
r.requiresXY,
e === "scatter"
), t.appendChild(this.currentEditor.getElement()));
}
schedulePreviewUpdate(e) {
this.previewTimeout && window.clearTimeout(this.previewTimeout), this.previewTimeout = window.setTimeout(() => {
this.updatePreview(e);
}, 100);
}
updatePreview(e) {
var s;
const i = (s = this.popup.getElement()) == null ? void 0 : s.querySelector(".preview-container");
if (!i) return;
const r = {
width: 350,
height: 250,
title: this.chartTitle,
xAxis: { title: this.xAxisLabel },
yAxis: { title: this.yAxisLabel },
legend: { show: this.showLegend },
grid: { show: this.showGrid },
mode: this.chartMode,
orientation: this.chartOrientation
}, t = this.renderer.createChart(this.selectedType, e, r);
t.style.position = "absolute", t.style.top = "50%", t.style.left = "50%", t.style.transform = "translate(-50%, -50%)", i.innerHTML = "", i.appendChild(t);
}
handleSubmit() {
const e = this.currentEditor.getData();
if (!e || Array.isArray(e) && e.length === 0) return;
const i = {
width: this.chartWidth,
height: this.chartHeight,
title: this.chartTitle,
xAxis: { title: this.xAxisLabel },
yAxis: { title: this.yAxisLabel },
legend: { show: this.showLegend },
grid: { show: this.showGrid },
mode: this.chartMode,
orientation: this.chartOrientation
};
if (this.editingChart) {
this.editingChart.setAttribute("data-chart-type", this.selectedType), this.editingChart.setAttribute("data-chart-data", JSON.stringify(e));
const r = this.renderer.createChart(this.selectedType, e, i);
this.editingChart.innerHTML = "", this.editingChart.appendChild(r);
} else {
const r = o("chart-container");
r.style.width = i.width + "px", r.style.height = i.height + "px", r.setAttribute("data-chart-type", this.selectedType), r.setAttribute("data-chart-data", JSON.stringify(e));
const t = this.renderer.createChart(this.selectedType, e, i);
r.appendChild(t), this.onInsert && this.onInsert(r);
}
this.popup.hide();
}
show(e) {
this.editingChart = null, this.onInsert = e, this.popup.show();
}
edit(e, i = !1) {
var s;
this.editingChart = e;
const r = e.getAttribute("data-chart-type"), t = e.getAttribute("data-chart-data");
if (r && t)
try {
const a = JSON.parse(t), n = (s = this.popup.getElement()) == null ? void 0 : s.querySelector(`[data-type="${r}"]`);
n && n.classList.add("selected"), this.selectedType = r, this.updateEditor(r), setTimeout(() => {
this.currentEditor.setData(a), this.updatePreview(a);
}, 100), i || this.popup.show();
} catch (a) {
console.error("Failed to parse chart data:", a);
}
}
redrawChart(e, i, r, t) {
const s = this.renderer.createChart(i, r, t);
e.innerHTML = "", e.appendChild(s);
}
createDimensionsSelector() {
const e = o("dimensions-selector flex gap-4 mb-4"), i = o("flex flex-col gap-1"), r = m("text-sm font-medium text-gray-700");
r.textContent = this.editor.t("Width");
const t = document.createElement("input");
t.type = "number", t.min = "200", t.max = "1200", t.step = "50", t.value = this.chartWidth.toString(), t.className = "dimension-input px-2 py-1 rounded border border-gray-300 text-sm", t.onchange = (d) => {
this.chartWidth = parseInt(d.target.value), this.schedulePreviewUpdate(this.currentEditor.getData());
}, i.appendChild(r), i.appendChild(t);
const s = o("flex flex-col gap-1"), a = m("text-sm font-medium text-gray-700");
a.textContent = this.editor.t("Height");
const n = document.createElement("input");
return n.type = "number", n.min = "150", n.max = "800", n.step = "50", n.value = this.chartHeight.toString(), n.className = "dimension-input px-2 py-1 rounded border border-gray-300 text-sm", n.onchange = (d) => {
this.chartHeight = parseInt(d.target.value), this.schedulePreviewUpdate(this.currentEditor.getData());
}, s.appendChild(a), s.appendChild(n), e.appendChild(i), e.appendChild(s), e;
}
createDisplaySettings() {
const e = o(
"display-settings flex flex-col gap-3 mb-4 p-3 bg-gray-50 rounded-lg"
), i = m("text-sm font-medium text-gray-700 mb-2");
i.textContent = this.editor.t("Display Settings"), e.appendChild(i);
const r = o("flex items-center gap-2"), t = document.createElement("input");
t.type = "checkbox", t.checked = this.showLegend, t.className = "setting-checkbox", t.onchange = (p) => {
this.showLegend = p.target.checked, this.schedulePreviewUpdate(this.currentEditor.getData());
};
const s = m("text-sm");
s.textContent = this.editor.t("Show Legend"), r.appendChild(t), r.appendChild(s);
const a = o("flex items-center gap-2"), n = document.createElement("input");
n.type = "checkbox", n.checked = this.showGrid, n.className = "setting-checkbox", n.onchange = (p) => {
this.showGrid = p.target.checked, this.schedulePreviewUpdate(this.currentEditor.getData());
};
const d = m("text-sm");
d.textContent = this.editor.t("Show Grid"), a.appendChild(n), a.appendChild(d);
const c = o("flex items-center gap-2"), h = m("text-sm");
h.textContent = this.editor.t("Bar/Area Mode");
const u = document.createElement("select");
u.className = "mode-select px-2 py-1 rounded border border-gray-300 text-sm", ["default", "stacked", "grouped"].forEach((p) => {
const g = document.createElement("option");
g.value = p, g.textContent = this.editor.t(p.charAt(0).toUpperCase() + p.slice(1)), u.appendChild(g);
}), u.value = this.chartMode, u.onchange = (p) => {
this.chartMode = p.target.value, this.schedulePreviewUpdate(this.currentEditor.getData());
}, c.appendChild(h), c.appendChild(u);
const y = o("flex items-center gap-2"), C = m("text-sm");
C.textContent = this.editor.t("Orientation");
const b = document.createElement("select");
return b.className = "orientation-select px-2 py-1 rounded border border-gray-300 text-sm", ["vertical", "horizontal"].forEach((p) => {
const g = document.createElement("option");
g.value = p, g.textContent = this.editor.t(p.charAt(0).toUpperCase() + p.slice(1)), b.appendChild(g);
}), b.value = this.chartOrientation, b.onchange = (p) => {
this.chartOrientation = p.target.value, this.schedulePreviewUpdate(this.currentEditor.getData());
}, y.appendChild(C), y.appendChild(b), e.appendChild(r), e.appendChild(a), e.appendChild(c), e.appendChild(y), e;
}
destroy() {
this.previewTimeout && (window.clearTimeout(this.previewTimeout), this.previewTimeout = null), this.popup.destroy(), this.currentEditor && this.currentEditor.destroy(), this.editor = null, this.popup = null, this.renderer = null, this.currentEditor = null, this.onInsert = null, this.editingChart = null;
}
}
export {
U as ChartMenu
};