@phenix1024/editor 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1360 @@
1
+ import { Editor as e, Extension as t, Mark as n, Node as r, getMarkRange as i } from "@tiptap/core";
2
+ import a from "@tiptap/starter-kit";
3
+ import o from "@tiptap/extension-underline";
4
+ import s from "@tiptap/extension-link";
5
+ import c from "@tiptap/extension-placeholder";
6
+ import { Table as l } from "@tiptap/extension-table";
7
+ import { TableRow as u } from "@tiptap/extension-table-row";
8
+ import { TableHeader as d } from "@tiptap/extension-table-header";
9
+ import { TableCell as f } from "@tiptap/extension-table-cell";
10
+ import p from "jszip";
11
+ import m from "@tiptap/extension-heading";
12
+ import h from "@tiptap/extension-image";
13
+ import { Plugin as g, PluginKey as _ } from "@tiptap/pm/state";
14
+ import { Decoration as v, DecorationSet as y } from "@tiptap/pm/view";
15
+ // src/editor/toolbar.ts
16
+ var ee = [
17
+ {
18
+ action: "bold",
19
+ label: "Ж",
20
+ title: "Жирный (⌘/Ctrl+B)"
21
+ },
22
+ {
23
+ action: "italic",
24
+ label: "К",
25
+ title: "Курсив (⌘/Ctrl+I)"
26
+ },
27
+ {
28
+ action: "underline",
29
+ label: "Ч",
30
+ title: "Подчёркнутый (⌘/Ctrl+U)"
31
+ },
32
+ {
33
+ action: "strike",
34
+ label: "З",
35
+ title: "Зачёркнутый"
36
+ },
37
+ {
38
+ action: "bullet",
39
+ label: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"7\" cy=\"6\" r=\"1.3\"/><circle cx=\"7\" cy=\"12\" r=\"1.3\"/><circle cx=\"7\" cy=\"18\" r=\"1.3\"/><path d=\"M11 6h8M11 12h8M11 18h8\"/></svg>",
40
+ title: "Маркированный список"
41
+ },
42
+ {
43
+ action: "ordered",
44
+ label: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M10 6h10M10 12h10M10 18h10\"/><path d=\"M4 4.8 5.2 4v4.2M3.8 8.2h2.8M3.8 10.8c.2-.5.6-.8 1.2-.8.8 0 1.2.4 1.2 1 0 .4-.3.8-.7 1.2l-1.6 1.5h2.5M3.8 16.2c.2-.5.6-.8 1.2-.8.8 0 1.2.4 1.2.9 0 .5-.4.8-1 .8.7 0 1.1.3 1.1.9 0 .6-.5 1-1.3 1-.6 0-1-.2-1.3-.7\"/></svg>",
45
+ title: "Нумерованный список"
46
+ },
47
+ {
48
+ action: "outdent",
49
+ label: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 6h14M5 12h10M5 18h14\"/><path d=\"M9 9l-4 3 4 3\"/></svg>",
50
+ title: "Уменьшить отступ"
51
+ },
52
+ {
53
+ action: "indent",
54
+ label: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 6h14M9 12h10M5 18h14\"/><path d=\"M15 9l4 3-4 3\"/></svg>",
55
+ title: "Увеличить отступ"
56
+ },
57
+ {
58
+ action: "link",
59
+ label: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M10 13.5a4.5 4.5 0 0 0 6.4 0l3-3a4.5 4.5 0 0 0-6.4-6.4l-1.7 1.7\"/><path d=\"M14 10.5a4.5 4.5 0 0 0-6.4 0l-3 3A4.5 4.5 0 0 0 11 19.9l1.7-1.7\"/></svg>",
60
+ title: "Добавить ссылку"
61
+ },
62
+ {
63
+ action: "image",
64
+ label: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><circle cx=\"8.5\" cy=\"9\" r=\"1.5\"/><path d=\"m21 15-5-5L5 20\"/></svg>",
65
+ title: "Вставить изображение"
66
+ },
67
+ {
68
+ action: "table",
69
+ label: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"1.5\"/><path d=\"M3 10h18M9 4v16M15 4v16\"/></svg>",
70
+ title: "Вставить таблицу"
71
+ },
72
+ {
73
+ action: "label",
74
+ label: "Метка",
75
+ title: "Выделить текст меткой"
76
+ },
77
+ {
78
+ action: "toc",
79
+ label: "Содержание",
80
+ title: "Содержание документа"
81
+ },
82
+ {
83
+ action: "undo",
84
+ label: "↶",
85
+ title: "Отменить (⌘/Ctrl+Z)"
86
+ },
87
+ {
88
+ action: "redo",
89
+ label: "↷",
90
+ title: "Повторить (⌘/Ctrl+Shift+Z)"
91
+ }
92
+ ];
93
+ function b(e, t, n = {}) {
94
+ e.classList.add("ophx-toolbar"), e.replaceChildren();
95
+ let r = (e, t) => {
96
+ e.style.display = t ? "" : "none", e.hidden = !t;
97
+ }, a = () => {
98
+ f = !1, r(l, !1), s.setAttribute("aria-expanded", "false"), r(P, !1), h.get("toc")?.setAttribute("aria-expanded", "false");
99
+ }, o = document.createElement("div");
100
+ o.className = "ophx-toolbar__style-wrap";
101
+ let s = document.createElement("button");
102
+ s.type = "button", s.className = "ophx-toolbar__style", s.setAttribute("aria-label", "Стиль текста"), s.setAttribute("aria-haspopup", "listbox"), s.setAttribute("aria-expanded", "false");
103
+ let c = document.createElement("span");
104
+ c.className = "ophx-toolbar__style-label", s.append(c);
105
+ let l = document.createElement("div");
106
+ l.className = "ophx-toolbar__style-menu", r(l, !1);
107
+ let u = [
108
+ {
109
+ value: "p",
110
+ label: "Обычный текст",
111
+ className: "ophx-toolbar__style-preview--p"
112
+ },
113
+ {
114
+ value: "1",
115
+ label: "Заголовок 1",
116
+ className: "ophx-toolbar__style-preview--h1"
117
+ },
118
+ {
119
+ value: "2",
120
+ label: "Заголовок 2",
121
+ className: "ophx-toolbar__style-preview--h2"
122
+ },
123
+ {
124
+ value: "3",
125
+ label: "Заголовок 3",
126
+ className: "ophx-toolbar__style-preview--h3"
127
+ }
128
+ ], d = (e) => {
129
+ let n = Number(e), r = t.chain().focus();
130
+ e === "p" ? r.setParagraph().run() : r.setHeading({ level: n }).run(), m();
131
+ };
132
+ for (let e of u) {
133
+ let t = document.createElement("button");
134
+ t.type = "button", t.className = "ophx-toolbar__style-option", t.dataset.value = e.value, t.setAttribute("role", "option"), t.setAttribute("aria-selected", "false"), t.innerHTML = `<span class="ophx-toolbar__style-preview ${e.className}">${e.label}</span>`, t.addEventListener("mousedown", (e) => {
135
+ e.preventDefault(), e.stopPropagation();
136
+ }), t.addEventListener("click", () => d(e.value)), l.append(t);
137
+ }
138
+ let f = !1, p = () => {
139
+ f = !0, r(l, !0), s.setAttribute("aria-expanded", "true");
140
+ }, m = () => {
141
+ f = !1, r(l, !1), s.setAttribute("aria-expanded", "false");
142
+ };
143
+ s.addEventListener("click", (e) => {
144
+ e.stopPropagation(), f ? m() : (r(P, !1), p());
145
+ }), s.addEventListener("keydown", (e) => {
146
+ e.key === "Escape" && m();
147
+ }), document.addEventListener("click", (e) => {
148
+ let t = e.target, n = o.contains(t), r = P.contains(t);
149
+ !n && !r && a();
150
+ }), o.append(s, l), e.append(o);
151
+ let h = /* @__PURE__ */ new Map();
152
+ for (let n of ee) {
153
+ let r = document.createElement("button");
154
+ r.type = "button", r.className = "ophx-toolbar__button", r.dataset.action = n.action, r.innerHTML = n.label, r.title = n.title, r.setAttribute("aria-label", n.title), r.addEventListener("mousedown", (e) => e.preventDefault()), r.addEventListener("click", (e) => {
155
+ n.action === "toc" ? e.stopPropagation() : n.action === "link" ? U() : n.action === "table" ? se() : n.action === "label" ? (e.stopPropagation(), Te()) : t.isEditable && ie(n.action, t, _, P);
156
+ }), h.set(n.action, r), e.append(r);
157
+ }
158
+ let g = document.createElement("input");
159
+ g.type = "search", g.className = "ophx-toolbar__search", g.placeholder = "Поиск…", g.setAttribute("aria-label", "Поиск по тексту"), g.addEventListener("input", () => t.commands.setSearchQuery(g.value)), g.addEventListener("keydown", (e) => {
160
+ e.key === "Escape" && (g.value = "", t.commands.setSearchQuery(""), t.commands.focus());
161
+ }), e.append(g);
162
+ let _ = document.createElement("input");
163
+ _.type = "file", _.accept = "image/*", _.hidden = !0, _.addEventListener("change", () => {
164
+ let e = _.files?.[0];
165
+ if (e) {
166
+ if (n.onInsertImage) n.onInsertImage(e);
167
+ else {
168
+ let n = new FileReader();
169
+ n.addEventListener("load", () => {
170
+ typeof n.result == "string" && t.chain().focus().setImage({
171
+ src: n.result,
172
+ alt: e.name
173
+ }).run();
174
+ }), n.readAsDataURL(e);
175
+ }
176
+ }
177
+ _.value = "";
178
+ }), e.append(_);
179
+ let v = document.createElement("form");
180
+ v.className = "ophx-table-popover", v.setAttribute("aria-label", "Создать таблицу"), v.hidden = !0, v.innerHTML = "\n <label class=\"ophx-table-popover__label\">Строки<input name=\"rows\" type=\"number\" min=\"1\" max=\"30\" value=\"3\" required></label>\n <label class=\"ophx-table-popover__label\">Столбцы<input name=\"cols\" type=\"number\" min=\"1\" max=\"20\" value=\"3\" required></label>\n <label class=\"ophx-table-popover__header\"><input name=\"header\" type=\"checkbox\" checked> Заголовок таблицы</label>\n <button class=\"ophx-table-popover__insert\" type=\"submit\">Вставить таблицу</button>\n ", document.body.append(v);
181
+ let y = h.get("table"), b = v.querySelector("input[name=\"rows\"]"), ae = v.querySelector("input[name=\"cols\"]"), oe = v.querySelector("input[name=\"header\"]"), x = () => {
182
+ if (v.hidden) return;
183
+ let e = y.getBoundingClientRect(), t = v.offsetWidth, n = Math.max(8, Math.min(e.left, window.innerWidth - t - 8));
184
+ v.style.left = `${n}px`, v.style.top = `${Math.min(e.bottom + 8, window.innerHeight - v.offsetHeight - 8)}px`;
185
+ };
186
+ function S() {
187
+ v.hidden = !0, y.setAttribute("aria-expanded", "false"), window.removeEventListener("resize", x), document.removeEventListener("scroll", x, !0);
188
+ }
189
+ function se() {
190
+ t.isEditable && (v.hidden ? (v.hidden = !1, y.setAttribute("aria-expanded", "true"), x(), window.addEventListener("resize", x), document.addEventListener("scroll", x, !0), b.focus(), b.select()) : S());
191
+ }
192
+ y.setAttribute("aria-haspopup", "dialog"), y.setAttribute("aria-expanded", "false"), v.addEventListener("mousedown", (e) => e.stopPropagation()), v.addEventListener("submit", (e) => {
193
+ e.preventDefault();
194
+ let n = Math.max(1, Math.min(30, Number(b.value) || 1)), r = Math.max(1, Math.min(20, Number(ae.value) || 1));
195
+ b.value = String(n), ae.value = String(r), t.chain().focus().insertTable({
196
+ rows: n,
197
+ cols: r,
198
+ withHeaderRow: oe.checked
199
+ }).run(), S();
200
+ }), v.addEventListener("keydown", (e) => {
201
+ e.key === "Escape" && (e.preventDefault(), S(), y.focus());
202
+ });
203
+ let ce = (e) => {
204
+ let t = e.target;
205
+ !v.contains(t) && !y.contains(t) && S();
206
+ };
207
+ document.addEventListener("click", ce);
208
+ let C = document.createElement("div");
209
+ C.className = "ophx-table-context", C.setAttribute("role", "toolbar"), C.setAttribute("aria-label", "Действия с таблицей"), C.hidden = !0, C.innerHTML = "\n <button type=\"button\" data-command=\"addRow\" aria-label=\"Добавить строку ниже\" title=\"Добавить строку ниже\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M4 4h16v12H4zM4 10h16M10 4v12M12 20v-4M10 18h4\"/></svg></button>\n <button type=\"button\" data-command=\"deleteRow\" aria-label=\"Удалить строку\" title=\"Удалить строку\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M4 4h16v12H4zM4 10h16M10 4v12M10 20h4\"/></svg></button>\n <span class=\"ophx-table-context__divider\" aria-hidden=\"true\"></span>\n <button type=\"button\" data-command=\"addColumn\" aria-label=\"Добавить столбец справа\" title=\"Добавить столбец справа\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M4 4h12v16H4zM10 4v16M20 12h-4M18 10v4\"/></svg></button>\n <button type=\"button\" data-command=\"deleteColumn\" aria-label=\"Удалить столбец\" title=\"Удалить столбец\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M4 4h12v16H4zM10 4v16M18 10v4\"/></svg></button>\n ", document.body.append(C);
210
+ let le = () => {
211
+ let { $from: e } = t.state.selection;
212
+ for (let n = e.depth; n > 0; --n) {
213
+ if (e.node(n).type.name !== "table") continue;
214
+ let r = t.view.nodeDOM(e.before(n));
215
+ return r instanceof HTMLTableElement ? r : r instanceof HTMLElement ? r.querySelector("table") : null;
216
+ }
217
+ return null;
218
+ }, w = () => {
219
+ if (C.hidden) return;
220
+ let e = le();
221
+ if (!e) return;
222
+ let t = e.getBoundingClientRect(), n = C.offsetWidth, r = C.offsetHeight, i = Math.max(8, Math.min(t.left, window.innerWidth - n - 8)), a = t.top - r - 8 >= 8 ? t.top - r - 8 : Math.min(t.bottom + 8, window.innerHeight - r - 8);
223
+ C.style.left = `${i}px`, C.style.top = `${Math.max(8, a)}px`;
224
+ }, T = () => {
225
+ !t.isEditable || !t.isActive("table") ? (C.hidden = !0, window.removeEventListener("resize", w), document.removeEventListener("scroll", w, !0)) : (C.hidden = !1, w(), window.addEventListener("resize", w), document.addEventListener("scroll", w, !0));
226
+ };
227
+ C.addEventListener("mousedown", (e) => e.preventDefault()), C.addEventListener("click", (e) => {
228
+ let n = e.target.closest("[data-command]");
229
+ if (!n) return;
230
+ let r = n.dataset.command, i = t.chain().focus();
231
+ r === "addRow" ? i.addRowAfter().run() : r === "deleteRow" ? i.deleteRow().run() : r === "addColumn" ? i.addColumnAfter().run() : r === "deleteColumn" && i.deleteColumn().run();
232
+ });
233
+ let E = (e) => {
234
+ if (C.hidden) return;
235
+ let t = e.target, n = le();
236
+ !C.contains(t) && !n?.contains(t) && (C.hidden = !0);
237
+ };
238
+ document.addEventListener("click", E);
239
+ let D = document.createElement("div");
240
+ D.className = "ophx-image-popover", D.setAttribute("role", "dialog"), D.setAttribute("aria-label", "Размер и обтекание изображения"), D.hidden = !0, D.innerHTML = "\n <span class=\"ophx-image-popover__arrow\" aria-hidden=\"true\"></span>\n <div class=\"ophx-image-popover__heading\">Обтекание текстом</div>\n <div class=\"ophx-image-popover__choices\" role=\"group\" aria-label=\"Обтекание текстом\">\n <button class=\"ophx-image-popover__choice\" type=\"button\" data-wrap=\"none\" aria-label=\"Без обтекания\" title=\"Без обтекания\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"8\" height=\"7\" rx=\"1\"/><path d=\"M14 5h7M14 8h7M3 14h18M3 18h18\"/></svg></button>\n <button class=\"ophx-image-popover__choice\" type=\"button\" data-wrap=\"left\" aria-label=\"Изображение слева, текст справа\" title=\"Изображение слева, текст справа\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"8\" height=\"10\" rx=\"1\"/><path d=\"M14 5h7M14 8h7M14 11h7M3 18h18\"/></svg></button>\n <button class=\"ophx-image-popover__choice\" type=\"button\" data-wrap=\"right\" aria-label=\"Изображение справа, текст слева\" title=\"Изображение справа, текст слева\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><rect x=\"13\" y=\"4\" width=\"8\" height=\"10\" rx=\"1\"/><path d=\"M3 5h7M3 8h7M3 11h7M3 18h18\"/></svg></button>\n </div>\n <div class=\"ophx-image-popover__heading\">Выравнивание</div>\n <div class=\"ophx-image-popover__choices\" role=\"group\" aria-label=\"Выравнивание изображения\">\n <button class=\"ophx-image-popover__choice\" type=\"button\" data-align=\"left\" aria-label=\"Прижать влево\" title=\"Прижать влево\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M4 3v18M8 6h12v5H8zM8 15h12\"/></svg></button>\n <button class=\"ophx-image-popover__choice\" type=\"button\" data-align=\"center\" aria-label=\"По центру\" title=\"По центру\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M12 3v18M4 6h16v5H4zM4 15h16\"/></svg></button>\n <button class=\"ophx-image-popover__choice\" type=\"button\" data-align=\"right\" aria-label=\"Прижать вправо\" title=\"Прижать вправо\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M20 3v18M4 6h12v5H4zM4 15h12\"/></svg></button>\n </div>\n <div class=\"ophx-image-popover__size\" aria-live=\"polite\"></div>\n ", document.body.append(D);
241
+ let ue = D.querySelector(".ophx-image-popover__size"), O = null, k = () => {
242
+ if (!t.isActive("image")) return null;
243
+ let e = t.view.nodeDOM(t.state.selection.from);
244
+ return e instanceof HTMLElement ? e.querySelector("img") : null;
245
+ }, A = () => {
246
+ let e = k();
247
+ if (D.hidden || !e) return;
248
+ let t = e.getBoundingClientRect(), n = t.left + t.width / 2, r = D.offsetWidth, i = D.offsetHeight, a = Math.max(8, Math.min(n - r / 2, window.innerWidth - r - 8)), o = t.bottom + i + 12 <= window.innerHeight - 8;
249
+ D.style.left = `${a}px`, D.style.top = `${o ? t.bottom + 10 : Math.max(8, t.top - i - 10)}px`, D.dataset.placement = o ? "bottom" : "top", D.style.setProperty("--ophx-image-arrow-left", `${Math.max(14, Math.min(n - a, r - 14))}px`);
250
+ };
251
+ function j() {
252
+ D.hidden = !0, window.removeEventListener("resize", A), document.removeEventListener("scroll", A, !0);
253
+ }
254
+ function de() {
255
+ if (!t.isEditable || !D.hidden) return;
256
+ let e = k();
257
+ e && (O = t.state.selection.from, D.hidden = !1, N(), A(), e.complete || e.addEventListener("load", () => {
258
+ N(), A();
259
+ }, { once: !0 }), window.addEventListener("resize", A), document.addEventListener("scroll", A, !0));
260
+ }
261
+ let M = (e) => {
262
+ O !== null && t.chain().focus().setNodeSelection(O).updateAttributes("image", e).run();
263
+ }, N = () => {
264
+ if (D.hidden || !t.isActive("image")) return;
265
+ let e = t.getAttributes("image"), n = k();
266
+ n && (ue.textContent = `${Math.round(n.getBoundingClientRect().width)} px · потяните за боковой маркер, чтобы изменить размер`);
267
+ for (let t of D.querySelectorAll("[data-wrap]")) {
268
+ let n = t.dataset.wrap === (e.textWrap ?? "none");
269
+ t.classList.toggle("is-selected", n), t.setAttribute("aria-pressed", String(n));
270
+ }
271
+ for (let t of D.querySelectorAll("[data-align]")) {
272
+ let n = t.dataset.align === (e.alignment ?? "left");
273
+ t.classList.toggle("is-selected", n), t.setAttribute("aria-pressed", String(n));
274
+ }
275
+ };
276
+ D.addEventListener("mousedown", (e) => e.stopPropagation()), D.addEventListener("click", (e) => {
277
+ let t = e.target.closest(".ophx-image-popover__choice");
278
+ t && ((t.dataset.wrap === "none" || t.dataset.wrap === "left" || t.dataset.wrap === "right") && M({ textWrap: t.dataset.wrap }), (t.dataset.align === "left" || t.dataset.align === "center" || t.dataset.align === "right") && M({
279
+ alignment: t.dataset.align,
280
+ textWrap: "none"
281
+ }));
282
+ }), D.addEventListener("keydown", (e) => {
283
+ e.key === "Escape" && (e.preventDefault(), j());
284
+ });
285
+ let fe = (e) => {
286
+ let n = e.target, r = e.target.closest?.(".ophx-image-node"), i = !!(r && t.view.dom.contains(r));
287
+ !D.contains(n) && !i && j();
288
+ };
289
+ document.addEventListener("click", fe);
290
+ let P = document.createElement("nav");
291
+ P.className = "ophx-toc", P.setAttribute("aria-label", "Содержание документа"), r(P, !1), e.append(P);
292
+ let F = h.get("toc");
293
+ F && (F.setAttribute("aria-haspopup", "true"), F.setAttribute("aria-expanded", "false"), F.addEventListener("click", () => {
294
+ P.hidden || P.style.display === "none" ? (m(), r(P, !0), F.setAttribute("aria-expanded", "true")) : a();
295
+ }));
296
+ let I = document.createElement("form");
297
+ I.className = "ophx-link-popover", I.setAttribute("role", "dialog"), I.setAttribute("aria-label", "Добавить ссылку"), I.hidden = !0, I.innerHTML = "\n <span class=\"ophx-link-popover__arrow\" aria-hidden=\"true\"></span>\n <label class=\"ophx-link-popover__label\" for=\"ophx-link-url\">Адрес ссылки</label>\n <div class=\"ophx-link-popover__row\">\n <input class=\"ophx-link-popover__input\" id=\"ophx-link-url\" type=\"url\" placeholder=\"https://example.com\" autocomplete=\"url\">\n </div>\n <div class=\"ophx-link-popover__actions\">\n <button class=\"ophx-link-popover__go\" type=\"button\">Перейти</button>\n <button class=\"ophx-link-popover__remove\" type=\"button\">Удалить ссылку</button>\n <button class=\"ophx-link-popover__submit\" type=\"submit\">ОК</button>\n </div>\n ", document.body.append(I);
298
+ let L = I.querySelector(".ophx-link-popover__input"), R = I.querySelector(".ophx-link-popover__go"), pe = I.querySelector(".ophx-link-popover__remove"), z = h.get("link"), B = null, V = () => {
299
+ if (!I.hidden && B) try {
300
+ let e = t.view.coordsAtPos(B.from), n = t.view.coordsAtPos(B.to), r = (e.left + n.right) / 2, i = I.offsetWidth, a = I.offsetHeight, o = Math.max(8, Math.min(r - i / 2, window.innerWidth - i - 8)), s = n.bottom + a + 14 <= window.innerHeight - 8, c = s ? n.bottom + 12 : Math.max(8, e.top - a - 12);
301
+ I.style.left = `${o}px`, I.style.top = `${c}px`, I.dataset.placement = s ? "bottom" : "top", I.style.setProperty("--ophx-link-arrow-left", `${Math.max(14, Math.min(r - o, i - 14))}px`);
302
+ } catch {
303
+ H();
304
+ }
305
+ };
306
+ function H() {
307
+ I.hidden = !0, z.setAttribute("aria-expanded", "false"), window.removeEventListener("resize", V), document.removeEventListener("scroll", V, !0);
308
+ }
309
+ function U(e = !0) {
310
+ if (!t.isEditable) return;
311
+ if (!I.hidden && e) {
312
+ H();
313
+ return;
314
+ }
315
+ t.state.selection.empty && t.isActive("link") && t.chain().extendMarkRange("link").run();
316
+ let n = t.state.selection;
317
+ n.empty || (B = {
318
+ from: n.from,
319
+ to: n.to
320
+ }, L.value = t.getAttributes("link").href ?? "", pe.hidden = !t.isActive("link"), R.hidden = !L.value, I.hidden = !1, z.setAttribute("aria-expanded", "true"), V(), window.addEventListener("resize", V), document.addEventListener("scroll", V, !0), L.focus(), L.select());
321
+ }
322
+ z.setAttribute("aria-haspopup", "dialog"), z.setAttribute("aria-expanded", "false");
323
+ let W = (e) => {
324
+ let n = e.target.closest("a[href]");
325
+ n && t.view.dom.contains(n) && (n.title = "⌘/Ctrl+клик — открыть ссылку; клик — выделить для редактирования через кнопку «Ссылка»");
326
+ }, G = (e) => {
327
+ let n = e.target.closest("a[href]");
328
+ if (!n || !t.view.dom.contains(n)) return;
329
+ if (e.preventDefault(), e.stopPropagation(), e.metaKey || e.ctrlKey) {
330
+ ne(n.href);
331
+ return;
332
+ }
333
+ let r = t.view.posAtCoords({
334
+ left: e.clientX,
335
+ top: e.clientY
336
+ });
337
+ r && (t.chain().focus().setTextSelection(r.pos).extendMarkRange("link").run(), U(!1));
338
+ };
339
+ t.view.dom.addEventListener("pointerover", W), t.view.dom.addEventListener("click", G), I.addEventListener("mousedown", (e) => e.stopPropagation()), I.addEventListener("submit", (e) => {
340
+ if (e.preventDefault(), !B) return;
341
+ let n = L.value.trim();
342
+ n ? (t.chain().focus().setTextSelection(B).setLink({ href: n }).run(), H()) : L.focus();
343
+ }), R.addEventListener("click", () => ne(L.value)), L.addEventListener("input", () => {
344
+ R.hidden = !L.value.trim();
345
+ }), pe.addEventListener("click", () => {
346
+ B && (t.chain().focus().setTextSelection(B).extendMarkRange("link").unsetLink().run(), H());
347
+ }), I.addEventListener("keydown", (e) => {
348
+ e.key === "Escape" && (e.preventDefault(), H(), z.focus());
349
+ });
350
+ let K = (e) => {
351
+ let t = e.target;
352
+ !I.contains(t) && !z.contains(t) && H();
353
+ };
354
+ document.addEventListener("click", K);
355
+ let me = ["важно", "идея"], q = document.createElement("form");
356
+ q.className = "ophx-link-popover ophx-label-popover", q.setAttribute("role", "dialog"), q.hidden = !0, q.innerHTML = "\n <span class=\"ophx-link-popover__arrow\" aria-hidden=\"true\"></span>\n <div class=\"ophx-label-popover__view\">\n <span class=\"ophx-link-popover__label\">Метка</span>\n <div class=\"ophx-label-popover__name\"><mark class=\"ophx-label-popover__chip\"></mark></div>\n <div class=\"ophx-link-popover__actions ophx-label-popover__view-actions\">\n <button class=\"ophx-link-popover__remove\" type=\"button\" data-label-remove>Удалить метку</button>\n <button class=\"ophx-link-popover__submit\" type=\"button\" data-label-edit>Изменить</button>\n </div>\n </div>\n <div class=\"ophx-label-popover__edit\">\n <label class=\"ophx-link-popover__label\" for=\"ophx-label-name\">Название метки</label>\n <div class=\"ophx-link-popover__row\">\n <input class=\"ophx-link-popover__input\" id=\"ophx-label-name\" type=\"text\" placeholder=\"Например, важно\" autocomplete=\"off\" maxlength=\"60\">\n </div>\n <div class=\"ophx-label-popover__presets\" role=\"group\" aria-label=\"Быстрый выбор метки\"></div>\n <div class=\"ophx-link-popover__actions\">\n <button class=\"ophx-link-popover__remove\" type=\"button\" data-label-remove>Удалить метку</button>\n <button class=\"ophx-link-popover__submit\" type=\"submit\">ОК</button>\n </div>\n </div>\n ", document.body.append(q);
357
+ let J = q.querySelector("#ophx-label-name"), he = q.querySelector(".ophx-label-popover__chip"), ge = q.querySelector(".ophx-label-popover__presets"), _e = q.querySelector("[data-label-edit]"), ve = q.querySelectorAll("[data-label-remove]"), ye = q.querySelector(".ophx-label-popover__view-actions"), Y = h.get("label"), X = null, be = "edit", xe = !1, Z = () => {
358
+ !q.hidden && X && (te(t, q, X) || Q());
359
+ };
360
+ function Q() {
361
+ q.hidden = !0, Y.setAttribute("aria-expanded", "false"), window.removeEventListener("resize", Z), document.removeEventListener("scroll", Z, !0);
362
+ }
363
+ function Se() {
364
+ let e = new Set(me);
365
+ return t.state.doc.descendants((t) => {
366
+ for (let n of t.marks) n.type.name === "label" && n.attrs.label && e.add(String(n.attrs.label));
367
+ }), [...e];
368
+ }
369
+ function Ce(e) {
370
+ ge.replaceChildren();
371
+ for (let t of Se()) {
372
+ let n = document.createElement("button");
373
+ n.type = "button", n.className = "ophx-label-popover__preset", n.classList.toggle("is-selected", t === e);
374
+ let r = document.createElement("mark");
375
+ r.dataset.label = t, r.textContent = t, n.append(r), n.addEventListener("click", () => Ee(t)), ge.append(n);
376
+ }
377
+ }
378
+ function we(e, n, r) {
379
+ X = e, be = r, q.dataset.mode = r, q.setAttribute("aria-label", r === "view" ? `Метка «${n}»` : "Установить метку"), he.textContent = n, he.dataset.label = n, ye.hidden = !t.isEditable, ve.forEach((e) => {
380
+ e.hidden = !xe;
381
+ }), J.value = n, Ce(n), q.hidden = !1, Y.setAttribute("aria-expanded", String(r === "edit")), Z(), window.addEventListener("resize", Z), document.addEventListener("scroll", Z, !0), r === "edit" && (J.focus(), J.select());
382
+ }
383
+ function Te() {
384
+ if (!t.isEditable) return;
385
+ if (!q.hidden && be === "edit") {
386
+ Q();
387
+ return;
388
+ }
389
+ t.state.selection.empty && t.isActive("label") && t.chain().extendMarkRange("label").run();
390
+ let { from: e, to: n, empty: r } = t.state.selection;
391
+ if (r) return;
392
+ xe = t.isActive("label");
393
+ let i = xe ? String(t.getAttributes("label").label ?? "") : "";
394
+ we({
395
+ from: e,
396
+ to: n
397
+ }, i, "edit");
398
+ }
399
+ function Ee(e) {
400
+ let n = e.trim();
401
+ X && (n ? (t.chain().focus().setTextSelection(X).setMark("label", { label: n }).run(), Q()) : J.focus());
402
+ }
403
+ Y.setAttribute("aria-haspopup", "dialog"), Y.setAttribute("aria-expanded", "false");
404
+ let De = (e) => {
405
+ let n = e.target, r = n.closest("mark");
406
+ if (!r || !t.view.dom.contains(r) || n.closest("a[href]")) return null;
407
+ let a = t.view.posAtCoords({
408
+ left: e.clientX,
409
+ top: e.clientY
410
+ }), o = t.schema.marks.label;
411
+ if (!a || !o) return null;
412
+ let s = t.state.doc.resolve(a.pos), c = i(s, o) ?? (a.pos > 0 ? i(t.state.doc.resolve(a.pos - 1), o) : void 0);
413
+ if (!c) return null;
414
+ let l = r.dataset.label ?? "";
415
+ return t.state.doc.nodesBetween(c.from, c.to, (e) => {
416
+ let t = e.marks.find((e) => e.type === o);
417
+ t && (l = String(t.attrs.label ?? l));
418
+ }), {
419
+ range: c,
420
+ name: l
421
+ };
422
+ }, Oe = (e) => {
423
+ let n = e.target.closest("mark[data-label]");
424
+ n && t.view.dom.contains(n) && (n.title = `Метка: ${n.dataset.label}`);
425
+ }, ke = (e) => {
426
+ if (e.metaKey || e.ctrlKey || e.shiftKey) return;
427
+ let n = De(e);
428
+ n && (t.state.selection.empty || !t.isEditable) && (e.stopPropagation(), xe = !0, we(n.range, n.name, "view"));
429
+ };
430
+ t.view.dom.addEventListener("pointerover", Oe), t.view.dom.addEventListener("click", ke), q.addEventListener("mousedown", (e) => e.stopPropagation()), q.addEventListener("submit", (e) => {
431
+ e.preventDefault(), Ee(J.value);
432
+ }), _e.addEventListener("click", () => {
433
+ X && t.isEditable && we(X, he.textContent ?? "", "edit");
434
+ }), ve.forEach((e) => e.addEventListener("click", () => {
435
+ X && (t.chain().focus().setTextSelection(X).unsetMark("label").run(), Q());
436
+ })), q.addEventListener("keydown", (e) => {
437
+ e.key === "Escape" && (e.preventDefault(), Q(), t.commands.focus());
438
+ });
439
+ let Ae = (e) => {
440
+ let t = e.target;
441
+ !q.contains(t) && !Y.contains(t) && Q();
442
+ };
443
+ document.addEventListener("click", Ae);
444
+ let $ = () => {
445
+ let e = t.isEditable;
446
+ s.disabled = !e;
447
+ for (let [t, n] of h) t !== "toc" && (n.disabled = !e);
448
+ h.get("undo").disabled = !e || !t.can().undo(), h.get("redo").disabled = !e || !t.can().redo(), T(), !e || !t.isActive("image") ? j() : D.hidden ? de() : (N(), A()), h.get("bold").classList.toggle("is-active", t.isActive("bold")), h.get("italic").classList.toggle("is-active", t.isActive("italic")), h.get("underline").classList.toggle("is-active", t.isActive("underline")), h.get("strike").classList.toggle("is-active", t.isActive("strike")), h.get("label").classList.toggle("is-active", t.isActive("label")), e || H(), !e && be === "edit" ? Q() : q.hidden || Z();
449
+ let n = t.getAttributes("heading").level, r = n ? String(n) : (t.isActive("paragraph"), "p"), i = n ? `Заголовок ${n}` : "Обычный текст";
450
+ c.textContent = i;
451
+ for (let e of l.querySelectorAll(".ophx-toolbar__style-option")) {
452
+ let t = e.dataset.value === r;
453
+ e.setAttribute("aria-selected", String(t)), e.classList.toggle("is-selected", t);
454
+ }
455
+ P.replaceChildren(), t.state.doc.descendants((e, n) => {
456
+ if (e.type.name !== "heading") return;
457
+ let r = document.createElement("button");
458
+ r.type = "button", r.className = "ophx-toc__item", r.style.paddingInlineStart = `${Math.max(0, Number(e.attrs.level) - 1) * 12}px`, r.textContent = e.textContent || "Без названия", r.addEventListener("mousedown", (e) => e.preventDefault()), r.addEventListener("click", () => {
459
+ t.commands.setTextSelection(n + 1), t.commands.focus(), t.commands.scrollIntoView();
460
+ }), P.append(r);
461
+ });
462
+ }, je = ({ transaction: e }) => {
463
+ if ($(), !e.docChanged) return;
464
+ let n = re(e.before, e.doc);
465
+ n !== null && t.commands.setNodeSelection(n);
466
+ }, Me = () => $();
467
+ return t.on("transaction", je), t.on("selectionUpdate", Me), t.view.dom.addEventListener("ophx:editablechange", $), $(), () => {
468
+ t.off("transaction", je), t.off("selectionUpdate", Me), t.view.dom.removeEventListener("ophx:editablechange", $), document.removeEventListener("click", ce), S(), v.remove(), document.removeEventListener("click", E), window.removeEventListener("resize", w), document.removeEventListener("scroll", w, !0), C.remove(), document.removeEventListener("click", fe), j(), D.remove(), t.view.dom.removeEventListener("pointerover", W), t.view.dom.removeEventListener("click", G), document.removeEventListener("click", K), H(), I.remove(), t.view.dom.removeEventListener("pointerover", Oe), t.view.dom.removeEventListener("click", ke), document.removeEventListener("click", Ae), Q(), q.remove(), e.replaceChildren(), e.classList.remove("ophx-toolbar");
469
+ };
470
+ }
471
+ function te(e, t, n) {
472
+ try {
473
+ let r = e.view.coordsAtPos(n.from), i = e.view.coordsAtPos(n.to), a = (r.left + i.right) / 2, o = t.offsetWidth, s = t.offsetHeight, c = Math.max(8, Math.min(a - o / 2, window.innerWidth - o - 8)), l = i.bottom + s + 14 <= window.innerHeight - 8, u = l ? i.bottom + 12 : Math.max(8, r.top - s - 12);
474
+ return t.style.left = `${c}px`, t.style.top = `${u}px`, t.dataset.placement = l ? "bottom" : "top", t.style.setProperty("--ophx-link-arrow-left", `${Math.max(14, Math.min(a - c, o - 14))}px`), !0;
475
+ } catch {
476
+ return !1;
477
+ }
478
+ }
479
+ function ne(e) {
480
+ try {
481
+ let t = new URL(e.trim(), window.location.href);
482
+ [
483
+ "http:",
484
+ "https:",
485
+ "mailto:"
486
+ ].includes(t.protocol) && window.open(t.href, "_blank", "noopener,noreferrer");
487
+ } catch {}
488
+ }
489
+ function re(e, t) {
490
+ let n = /* @__PURE__ */ new Map();
491
+ e.descendants((e) => {
492
+ if (e.type.name !== "image") return;
493
+ let t = String(e.attrs.src ?? "");
494
+ n.set(t, (n.get(t) ?? 0) + 1);
495
+ });
496
+ let r = null;
497
+ return t.descendants((e, t) => {
498
+ if (e.type.name !== "image" || r !== null) return;
499
+ let i = String(e.attrs.src ?? ""), a = n.get(i) ?? 0;
500
+ a > 0 ? n.set(i, a - 1) : r = t;
501
+ }), r;
502
+ }
503
+ function ie(e, t, n, r) {
504
+ let i = t.chain().focus();
505
+ switch (e) {
506
+ case "bold":
507
+ i.toggleBold().run();
508
+ break;
509
+ case "italic":
510
+ i.toggleItalic().run();
511
+ break;
512
+ case "underline":
513
+ i.toggleUnderline().run();
514
+ break;
515
+ case "strike":
516
+ i.toggleStrike().run();
517
+ break;
518
+ case "bullet":
519
+ i.toggleBulletList().run();
520
+ break;
521
+ case "ordered":
522
+ i.toggleOrderedList().run();
523
+ break;
524
+ case "indent":
525
+ ae(t, 1);
526
+ break;
527
+ case "outdent":
528
+ ae(t, -1);
529
+ break;
530
+ case "image":
531
+ n.click();
532
+ break;
533
+ case "toc":
534
+ r.hidden = !r.hidden;
535
+ break;
536
+ case "undo":
537
+ i.undo().run();
538
+ break;
539
+ case "redo": i.redo().run();
540
+ }
541
+ }
542
+ function ae(e, t) {
543
+ let { $from: n } = e.state.selection;
544
+ if (n.parent.type.name === "listItem") {
545
+ let n = e.chain().focus();
546
+ t > 0 ? n.sinkListItem("listItem").run() : n.liftListItem("listItem").run();
547
+ return;
548
+ }
549
+ let r = n.parent.type.name;
550
+ if (r !== "paragraph" && r !== "heading") return;
551
+ let i = Number(e.getAttributes(r).indent ?? 0);
552
+ e.chain().focus().updateAttributes(r, { indent: Math.max(0, Math.min(8, i + t)) }).run();
553
+ }
554
+ // src/editor/ophx-archive.ts
555
+ var oe = {
556
+ avif: "image/avif",
557
+ bmp: "image/bmp",
558
+ gif: "image/gif",
559
+ ico: "image/x-icon",
560
+ jpeg: "image/jpeg",
561
+ jpg: "image/jpeg",
562
+ png: "image/png",
563
+ svg: "image/svg+xml",
564
+ tif: "image/tiff",
565
+ tiff: "image/tiff",
566
+ webp: "image/webp"
567
+ };
568
+ async function x(e, t = {}) {
569
+ let n = new p(), r = new DOMParser().parseFromString(e, "text/html");
570
+ C(r);
571
+ let i = le(t.filename ?? t.title ?? "Документ"), a = t.title ?? r.title ?? i, o = t.assets ?? {}, s = new Set(Object.keys(o).map(T)), c = 0;
572
+ for (let e of Array.from(r.querySelectorAll("img[src]"))) {
573
+ let t = e.getAttribute("src")?.trim();
574
+ if (!t || /^https?:\/\//i.test(t) || /^data:(?!image\/)/i.test(t)) continue;
575
+ if (/^data:image\//i.test(t) || /^blob:/i.test(t)) {
576
+ try {
577
+ let r = await fetch(t);
578
+ if (!r.ok) throw Error(`Не удалось прочитать изображение: ${t}`);
579
+ let i = await r.blob();
580
+ if (!i.type.startsWith("image/")) throw Error("Вложение с data/blob URL не является изображением");
581
+ let a = `image-${++c}.${ue(i.type, t)}`;
582
+ for (; s.has(a);) a = `image-${++c}.${ue(i.type, t)}`;
583
+ n.file(a, i), s.add(a), e.setAttribute("src", encodeURIComponent(a));
584
+ } catch (e) {
585
+ throw Error("Не удалось добавить изображение в архив ophx", { cause: e });
586
+ }
587
+ continue;
588
+ }
589
+ let r = T(t);
590
+ if (!w(r)) throw Error(`Путь изображения должен быть простым именем файла: ${t}`);
591
+ let i = o[r];
592
+ if (!i && !s.has(r)) throw Error(`Для локального изображения «${r}» не передан файл media`);
593
+ i && n.file(r, i), e.setAttribute("src", encodeURIComponent(r));
594
+ }
595
+ let l = ce(t.styles ?? {});
596
+ for (let [e, t] of Object.entries(l)) n.file(e, t);
597
+ let u = Object.keys(l).map((e) => `<link rel="stylesheet" href="${k(encodeURIComponent(e))}">`).join(""), d = r.documentElement.lang || "ru", f = t.bodyContentEditable ? ` contenteditable="${t.bodyContentEditable}"` : "", m = `<!doctype html>\n<html lang="${k(d)}"><head><meta charset="utf-8"><title>${k(a)}</title>${u}</head><body${f}>${r.body.innerHTML}</body></html>`;
598
+ return n.file(`${i}.html`, m), n.generateAsync({
599
+ type: "blob",
600
+ mimeType: "application/zip",
601
+ compression: "DEFLATE",
602
+ compressionOptions: { level: 6 }
603
+ });
604
+ }
605
+ async function S(e) {
606
+ await D(e);
607
+ let t;
608
+ try {
609
+ t = await p.loadAsync(e);
610
+ } catch (e) {
611
+ throw Error("Не удалось прочитать ZIP-архив .ophx (шифрование и неподдерживаемые ZIP-режимы не поддерживаются)", { cause: e });
612
+ }
613
+ if (Object.values(t.files).some((e) => e.dir || /[\\/]/.test(e.name))) throw Error("В текущей версии .ophx все файлы должны находиться плоским списком в корне архива");
614
+ let n = Object.values(t.files).filter((e) => !e.dir), r = n.filter((e) => e.name.toLowerCase().endsWith(".html"));
615
+ if (r.length !== 1) throw r.length === 0 && n.some((e) => e.name.toLowerCase().endsWith(".xml")) ? Error("Обнаружен устаревший XML-вариант ophx; поддерживается только версия v2 с HTML5 внутри ZIP") : Error(`В архиве должен быть ровно один HTML-файл в корне; найдено: ${r.length}`);
616
+ let i = r[0], a = i.name.slice(0, -5), o = "name" in e && typeof e.name == "string" ? e.name : "";
617
+ if (/\.ophx$/i.test(o) && o.replace(/\.ophx$/i, "") !== a) throw Error(`Имя HTML-файла «${i.name}» должно совпадать с именем архива «${o}»`);
618
+ let s = await i.async("string");
619
+ if (!/^\s*<!doctype\s+html\b/i.test(s)) throw Error("Основной файл .ophx должен начинаться с <!doctype html>");
620
+ let c = new DOMParser().parseFromString(s, "text/html");
621
+ if (c.querySelector("head meta[charset]")?.getAttribute("charset")?.toLowerCase() !== "utf-8") throw Error("В <head> документа требуется <meta charset=\"utf-8\">");
622
+ let l = {};
623
+ for (let e of Array.from(c.querySelectorAll("head link[rel~=\"stylesheet\"][href]"))) {
624
+ let n = e.getAttribute("href")?.trim();
625
+ if (!n) continue;
626
+ if (E(n)) throw Error(`CSS должен быть встроенным плоским файлом архива: ${n}`);
627
+ let r = T(n);
628
+ if (!w(r) || !r.toLowerCase().endsWith(".css")) throw Error(`Ссылка на стиль должна указывать на плоский .css-файл: ${n}`);
629
+ let i = t.file(r);
630
+ if (!i) throw Error(`Файл стиля «${r}» отсутствует в архиве`);
631
+ l[r] = await i.async("string");
632
+ }
633
+ for (let e of Array.from(c.querySelectorAll("img[src]"))) {
634
+ let n = e.getAttribute("src")?.trim();
635
+ if (!n || E(n)) continue;
636
+ let r = T(n);
637
+ if (!w(r)) throw Error(`Ссылка на изображение должна быть простым именем файла: ${n}`);
638
+ let i = t.file(r);
639
+ if (!i) throw Error(`Файл изображения «${r}» отсутствует в архиве`);
640
+ let a = await i.async("base64"), o = r.split(".").pop()?.toLowerCase() ?? "", s = oe[o] ?? (o ? `image/${o}` : "application/octet-stream");
641
+ e.setAttribute("src", `data:${s};base64,${a}`);
642
+ }
643
+ let u = c.body.getAttribute("contenteditable");
644
+ if (u === "false") for (let e of Array.from(c.body.children)) e.hasAttribute("contenteditable") || e.setAttribute("contenteditable", "false");
645
+ O(c);
646
+ let d = Object.entries(l);
647
+ return {
648
+ html: c.body.innerHTML,
649
+ title: c.title || a,
650
+ filename: a,
651
+ styles: l,
652
+ css: d[0]?.[1] ?? "",
653
+ bodyContentEditable: u === "true" || u === "false" ? u : void 0
654
+ };
655
+ }
656
+ function se(e, t) {
657
+ let n = URL.createObjectURL(e), r = window.document.createElement("a");
658
+ r.href = n, r.download = t, r.click(), window.setTimeout(() => URL.revokeObjectURL(n), 0);
659
+ }
660
+ function ce(e) {
661
+ let t = {};
662
+ for (let [n, r] of Object.entries(e)) {
663
+ let e = T(n);
664
+ if (!w(e) || !e.toLowerCase().endsWith(".css")) throw Error(`Имя файла стиля должно быть плоским и оканчиваться на .css: ${n}`);
665
+ if (Object.hasOwn(t, e)) throw Error(`Повторяется имя файла стиля: ${e}`);
666
+ t[e] = r;
667
+ }
668
+ return t;
669
+ }
670
+ function C(e) {
671
+ let t = Array.from(e.querySelectorAll("[data-ophx-field], .ophx-field")), n = /* @__PURE__ */ new Set();
672
+ for (let e of t) {
673
+ let t = e.getAttribute("data-ophx-field") ?? "", r = e.tagName.toLowerCase();
674
+ if (!e.classList.contains("ophx-field") || e.getAttribute("contenteditable") !== "true") throw Error("Каждое поле должно иметь класс ophx-field и contenteditable=\"true\"");
675
+ if (![
676
+ "span",
677
+ "div",
678
+ "p"
679
+ ].includes(r) || !/^[A-Za-z0-9._-]+$/.test(t)) throw Error(`Некорректная обёртка или идентификатор поля: ${t || "(пусто)"}`);
680
+ if (n.has(t)) throw Error(`Идентификатор поля должен быть уникальным: ${t}`);
681
+ if (e.parentElement?.closest("[data-ophx-field]")) throw Error(`Поля не могут быть вложенными: ${t}`);
682
+ n.add(t);
683
+ }
684
+ }
685
+ function le(e) {
686
+ let t = e.trim().replace(/\.(?:ophx|html)$/i, "");
687
+ if (!t) return "Документ";
688
+ if (!w(t) || /[\u0000-\u001f]/.test(t)) throw Error("Имя .ophx должно быть простым именем файла без каталогов");
689
+ return t;
690
+ }
691
+ function w(e) {
692
+ return !!e && e !== "." && e !== ".." && !/[\\/]/.test(e);
693
+ }
694
+ function T(e) {
695
+ try {
696
+ return decodeURIComponent(e);
697
+ } catch {
698
+ throw Error(`Некорректное percent-экранирование в имени ресурса: ${e}`);
699
+ }
700
+ }
701
+ function E(e) {
702
+ return /^(?:data:|https?:\/\/)/i.test(e);
703
+ }
704
+ async function D(e) {
705
+ if (e.size < 2) throw Error("Файл не является ZIP-архивом .ophx (возможен устаревший формат)");
706
+ let t = new Uint8Array(await e.slice(0, 4).arrayBuffer());
707
+ if (t[0] !== 80 || t[1] !== 75) throw Error("Неподдерживаемый устаревший ophx: ожидается ZIP-архив версии v2");
708
+ }
709
+ function ue(e, t) {
710
+ let n = Object.entries(oe).find(([, t]) => t === e)?.[0];
711
+ if (n) return n;
712
+ let r = t.match(/\.([a-z0-9]+)(?:[?#]|$)/i)?.[1]?.toLowerCase();
713
+ return r && /^[a-z0-9]{1,6}$/.test(r) ? r : "bin";
714
+ }
715
+ function O(e) {
716
+ e.querySelectorAll("script, iframe, object, embed, style, link").forEach((e) => e.remove()), e.querySelectorAll("*").forEach((e) => {
717
+ for (let t of Array.from(e.attributes)) /^on/i.test(t.name) && e.removeAttribute(t.name), /^(href|src|xlink:href)$/i.test(t.name) && /^\s*(javascript|vbscript):/i.test(t.value) && e.removeAttribute(t.name), /^(src|xlink:href)$/i.test(t.name) && /^\s*data:(?!image\/)/i.test(t.value) && e.removeAttribute(t.name);
718
+ });
719
+ }
720
+ function k(e) {
721
+ return e.replace(/[&<>"']/g, (e) => ({
722
+ "&": "&amp;",
723
+ "<": "&lt;",
724
+ ">": "&gt;",
725
+ "\"": "&quot;",
726
+ "'": "&#39;"
727
+ })[e]);
728
+ }
729
+ // src/editor/extensions.ts
730
+ var A = n.create({
731
+ name: "label",
732
+ inclusive: !1,
733
+ addAttributes() {
734
+ return { label: {
735
+ default: "highlight",
736
+ parseHTML: (e) => e.dataset.label ?? "highlight",
737
+ renderHTML: (e) => ({ "data-label": e.label })
738
+ } };
739
+ },
740
+ parseHTML() {
741
+ return [{ tag: "mark[data-label]" }, { tag: "mark" }];
742
+ },
743
+ renderHTML({ HTMLAttributes: e }) {
744
+ return [
745
+ "mark",
746
+ e,
747
+ 0
748
+ ];
749
+ }
750
+ });
751
+ function j() {
752
+ return {
753
+ fieldId: {
754
+ default: "",
755
+ parseHTML: (e) => e.getAttribute("data-ophx-field") ?? "",
756
+ renderHTML: (e) => ({ "data-ophx-field": e.fieldId ?? "" })
757
+ },
758
+ fieldLabel: {
759
+ default: null,
760
+ parseHTML: (e) => e.getAttribute("data-ophx-label"),
761
+ renderHTML: (e) => e.fieldLabel ? { "data-ophx-label": e.fieldLabel } : {}
762
+ },
763
+ required: {
764
+ default: !1,
765
+ parseHTML: (e) => e.getAttribute("data-ophx-required") === "true",
766
+ renderHTML: (e) => e.required ? { "data-ophx-required": "true" } : {}
767
+ }
768
+ };
769
+ }
770
+ var de = /^[A-Za-z0-9._-]+$/, M = 50, N = 1200;
771
+ function fe(e) {
772
+ if (!e || !/^\d+$/.test(e)) return null;
773
+ let t = Number(e);
774
+ return Number.isInteger(t) && t >= M && t <= N ? t : null;
775
+ }
776
+ var P = h.extend({
777
+ addAttributes() {
778
+ return {
779
+ ...this.parent?.(),
780
+ width: {
781
+ default: null,
782
+ parseHTML: (e) => fe(e.getAttribute("width")),
783
+ renderHTML: (e) => {
784
+ let t = e.width;
785
+ return Number.isInteger(t) && Number(t) >= M && Number(t) <= N ? { width: String(t) } : {};
786
+ }
787
+ },
788
+ textWrap: {
789
+ default: "none",
790
+ parseHTML: (e) => {
791
+ let t = e.getAttribute("data-ophx-wrap");
792
+ return t === "left" || t === "right" ? t : "none";
793
+ },
794
+ renderHTML: (e) => ({ "data-ophx-wrap": e.textWrap === "left" || e.textWrap === "right" ? e.textWrap : "none" })
795
+ },
796
+ alignment: {
797
+ default: "left",
798
+ parseHTML: (e) => {
799
+ let t = e.getAttribute("data-ophx-align");
800
+ return t === "center" || t === "right" ? t : "left";
801
+ },
802
+ renderHTML: (e) => ({ "data-ophx-align": e.alignment === "center" || e.alignment === "right" ? e.alignment : "left" })
803
+ }
804
+ };
805
+ },
806
+ addNodeView() {
807
+ return ({ node: e, getPos: t, editor: n }) => {
808
+ let r = e, i = 0, a = 0, o = 0, s = document.createElement("div");
809
+ s.className = "ophx-image-node", s.contentEditable = "false";
810
+ let c = document.createElement("img"), l = document.createElement("button");
811
+ l.type = "button", l.className = "ophx-image-node__resize-handle", l.setAttribute("role", "slider"), l.setAttribute("aria-label", "Изменить размер изображения перетаскиванием"), l.setAttribute("aria-valuemin", String(M)), l.setAttribute("aria-valuemax", String(N)), l.title = "Потяните влево или вправо, чтобы изменить размер", l.innerHTML = "<svg viewBox=\"0 0 12 28\" aria-hidden=\"true\"><path d=\"M4 3v22M8 3v22\"/></svg>", s.append(c, l);
812
+ let u = () => {
813
+ let e = String(r.attrs.src ?? "");
814
+ c.getAttribute("src") !== e && c.setAttribute("src", e), c.alt = r.attrs.alt ?? "", r.attrs.title ? c.title = r.attrs.title : c.removeAttribute("title"), r.attrs.width ? c.width = Number(r.attrs.width) : c.removeAttribute("width");
815
+ let t = Number(r.attrs.width) || Math.round(c.getBoundingClientRect().width);
816
+ l.setAttribute("aria-valuenow", String(t)), l.setAttribute("aria-valuetext", `${t} пикселей`), s.dataset.ophxWrap = r.attrs.textWrap ?? "none", s.dataset.ophxAlign = r.attrs.alignment ?? "left";
817
+ }, d = (e) => {
818
+ if (!n.isEditable) return;
819
+ let r = typeof t == "function" ? t() : void 0;
820
+ if (typeof r != "number") return;
821
+ let i = n.state.doc.nodeAt(r);
822
+ if (!i || i.type.name !== "image") return;
823
+ let a = getComputedStyle(n.view.dom), o = Number.parseFloat(a.paddingLeft) + Number.parseFloat(a.paddingRight), s = Math.max(M, n.view.dom.clientWidth - o), c = Math.min(e, N, Math.floor(s));
824
+ n.view.dispatch(n.state.tr.setNodeMarkup(r, void 0, {
825
+ ...i.attrs,
826
+ width: c
827
+ }));
828
+ }, f = (e) => {
829
+ if (!l.hasPointerCapture(e.pointerId)) return;
830
+ let t = Math.max(50, Math.round((a + e.clientX - i) / 5) * 5);
831
+ t !== o && (o = t, d(t));
832
+ }, p = (e) => {
833
+ l.hasPointerCapture(e.pointerId) && l.releasePointerCapture(e.pointerId);
834
+ };
835
+ return l.addEventListener("pointerdown", (e) => {
836
+ n.isEditable && (e.preventDefault(), e.stopPropagation(), i = e.clientX, a = c.getBoundingClientRect().width, o = Math.round(a), l.setPointerCapture(e.pointerId));
837
+ }), l.addEventListener("pointermove", f), l.addEventListener("pointerup", p), l.addEventListener("pointercancel", p), l.addEventListener("keydown", (e) => {
838
+ if (!n.isEditable || e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
839
+ e.preventDefault();
840
+ let t = Number(r.attrs.width) || Math.round(c.getBoundingClientRect().width), i = e.key === "ArrowRight" ? 10 : -10;
841
+ d(Math.max(M, t + i));
842
+ }), c.addEventListener("load", () => {
843
+ if (r.attrs.width) return;
844
+ let e = Math.round(c.getBoundingClientRect().width);
845
+ l.setAttribute("aria-valuenow", String(e)), l.setAttribute("aria-valuetext", `${e} пикселей`);
846
+ }), u(), {
847
+ dom: s,
848
+ update(e) {
849
+ return e.type === r.type && (r = e, u(), !0);
850
+ },
851
+ stopEvent(e) {
852
+ return l.contains(e.target);
853
+ },
854
+ ignoreMutation() {
855
+ return !0;
856
+ }
857
+ };
858
+ };
859
+ }
860
+ }), F = r.create({
861
+ name: "ophxInlineField",
862
+ priority: 1e3,
863
+ group: "inline",
864
+ inline: !0,
865
+ content: "text*",
866
+ defining: !0,
867
+ addAttributes: j,
868
+ parseHTML() {
869
+ return [{
870
+ tag: "span.ophx-field[contenteditable=\"true\"][data-ophx-field]",
871
+ priority: 1e3
872
+ }];
873
+ },
874
+ renderHTML({ HTMLAttributes: e }) {
875
+ return [
876
+ "span",
877
+ {
878
+ ...e,
879
+ class: "ophx-field",
880
+ contenteditable: "true"
881
+ },
882
+ 0
883
+ ];
884
+ }
885
+ }), I = r.create({
886
+ name: "ophxBlockField",
887
+ priority: 1e3,
888
+ group: "block",
889
+ content: "text*",
890
+ defining: !0,
891
+ addAttributes() {
892
+ return {
893
+ ...j(),
894
+ tagName: {
895
+ default: "div",
896
+ parseHTML: (e) => e.tagName.toLowerCase(),
897
+ renderHTML: () => ({})
898
+ }
899
+ };
900
+ },
901
+ parseHTML() {
902
+ return [{
903
+ tag: "p.ophx-field[contenteditable=\"true\"][data-ophx-field]",
904
+ priority: 1e3
905
+ }, {
906
+ tag: "div.ophx-field[contenteditable=\"true\"][data-ophx-field]",
907
+ priority: 1e3
908
+ }];
909
+ },
910
+ renderHTML({ node: e, HTMLAttributes: t }) {
911
+ return [
912
+ e.attrs.tagName === "p" ? "p" : "div",
913
+ {
914
+ ...t,
915
+ class: "ophx-field",
916
+ contenteditable: "true"
917
+ },
918
+ 0
919
+ ];
920
+ }
921
+ }), L = n.create({
922
+ name: "ophxReadonlyInline",
923
+ inclusive: !1,
924
+ addAttributes() {
925
+ return { className: {
926
+ default: null,
927
+ parseHTML: (e) => e.getAttribute("class"),
928
+ renderHTML: (e) => e.className ? { class: e.className } : {}
929
+ } };
930
+ },
931
+ parseHTML() {
932
+ return [{
933
+ tag: "span[contenteditable=\"false\"]",
934
+ priority: 100
935
+ }];
936
+ },
937
+ renderHTML({ HTMLAttributes: e }) {
938
+ return [
939
+ "span",
940
+ {
941
+ ...e,
942
+ contenteditable: "false"
943
+ },
944
+ 0
945
+ ];
946
+ }
947
+ }), R = r.create({
948
+ name: "ophxReadonlyBlock",
949
+ priority: 1e3,
950
+ group: "block",
951
+ content: "block*",
952
+ defining: !0,
953
+ isolating: !0,
954
+ addAttributes() {
955
+ return {
956
+ id: {
957
+ default: null,
958
+ parseHTML: (e) => e.getAttribute("id"),
959
+ renderHTML: (e) => e.id ? { id: e.id } : {}
960
+ },
961
+ className: {
962
+ default: null,
963
+ parseHTML: (e) => e.getAttribute("class"),
964
+ renderHTML: (e) => e.className ? { class: e.className } : {}
965
+ },
966
+ title: {
967
+ default: null,
968
+ parseHTML: (e) => e.getAttribute("title"),
969
+ renderHTML: (e) => e.title ? { title: e.title } : {}
970
+ }
971
+ };
972
+ },
973
+ parseHTML() {
974
+ return [{
975
+ tag: "div[contenteditable=\"false\"]",
976
+ priority: 1e3
977
+ }];
978
+ },
979
+ renderHTML({ HTMLAttributes: e }) {
980
+ return [
981
+ "div",
982
+ {
983
+ ...e,
984
+ contenteditable: "false"
985
+ },
986
+ 0
987
+ ];
988
+ }
989
+ }), pe = new _("ophxHeadingCollapse"), z = m.extend({
990
+ addAttributes() {
991
+ return {
992
+ ...this.parent?.(),
993
+ collapsed: {
994
+ default: !1,
995
+ parseHTML: (e) => e.dataset.collapsed === "true",
996
+ renderHTML: (e) => e.collapsed ? { "data-collapsed": "true" } : {}
997
+ }
998
+ };
999
+ },
1000
+ addNodeView() {
1001
+ return ({ node: e, getPos: t, editor: n }) => {
1002
+ let r = e, i = document.createElement(`h${e.attrs.level}`);
1003
+ i.className = "ophx-heading";
1004
+ let a = document.createElement("button");
1005
+ a.type = "button", a.className = "ophx-heading__toggle", a.innerHTML = "<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"m5.5 3.5 4.5 4.5-4.5 4.5\"/></svg>";
1006
+ let o = document.createElement("span");
1007
+ o.className = "ophx-heading__content", i.append(o, a);
1008
+ let s = () => {
1009
+ let e = !!r.attrs.collapsed;
1010
+ a.setAttribute("aria-expanded", String(!e)), a.setAttribute("aria-label", e ? "Развернуть раздел" : "Свернуть раздел"), a.title = e ? "Развернуть раздел" : "Свернуть раздел", i.dataset.collapsed = String(e);
1011
+ };
1012
+ return a.addEventListener("mousedown", (e) => e.preventDefault()), a.addEventListener("click", (e) => {
1013
+ if (e.preventDefault(), e.stopPropagation(), !n.isEditable) return;
1014
+ let r = typeof t == "function" ? t() : void 0;
1015
+ if (typeof r != "number") return;
1016
+ let i = n.state.doc.nodeAt(r);
1017
+ i && i.type.name === "heading" && n.view.dispatch(n.state.tr.setNodeMarkup(r, void 0, {
1018
+ ...i.attrs,
1019
+ collapsed: !i.attrs.collapsed
1020
+ }));
1021
+ }), s(), {
1022
+ dom: i,
1023
+ contentDOM: o,
1024
+ update(e) {
1025
+ return e.type !== r.type || e.attrs.level !== r.attrs.level ? !1 : (r = e, s(), !0);
1026
+ },
1027
+ stopEvent(e) {
1028
+ return a.contains(e.target);
1029
+ }
1030
+ };
1031
+ };
1032
+ },
1033
+ addProseMirrorPlugins() {
1034
+ return [new g({
1035
+ key: pe,
1036
+ props: { decorations(e) {
1037
+ let t = [], n = null;
1038
+ return e.doc.forEach((e, r) => {
1039
+ if (e.type.name === "heading") {
1040
+ let i = Number(e.attrs.level);
1041
+ n !== null && i <= n && (n = null), n !== null && t.push(v.node(r, r + e.nodeSize, { class: "ophx-collapsed-content" })), e.attrs.collapsed && (n = i);
1042
+ } else n !== null && t.push(v.node(r, r + e.nodeSize, { class: "ophx-collapsed-content" }));
1043
+ }), y.create(e.doc, t);
1044
+ } }
1045
+ })];
1046
+ }
1047
+ }), B = new _("ophxTextSearch"), V = t.create({
1048
+ name: "ophxSearch",
1049
+ addStorage() {
1050
+ return { query: "" };
1051
+ },
1052
+ addCommands() {
1053
+ return { setSearchQuery: (e) => ({ tr: t, dispatch: n }) => (this.storage.query = e, n?.(t.setMeta(B, e)), !0) };
1054
+ },
1055
+ addProseMirrorPlugins() {
1056
+ let e = this;
1057
+ return [new g({
1058
+ key: B,
1059
+ state: {
1060
+ init: (t, n) => H(n.doc, e.storage.query),
1061
+ apply: (t, n, r, i) => {
1062
+ let a = t.getMeta(B);
1063
+ return a !== void 0 && (e.storage.query = a), !t.docChanged && a === void 0 ? n : H(i.doc, e.storage.query);
1064
+ }
1065
+ },
1066
+ props: { decorations(e) {
1067
+ return B.getState(e);
1068
+ } }
1069
+ })];
1070
+ }
1071
+ });
1072
+ function H(e, t) {
1073
+ if (!t.trim()) return y.empty;
1074
+ let n = [], r = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), i = new RegExp(r, "gi");
1075
+ return e.descendants((e, t) => {
1076
+ if (e.isText && e.text) {
1077
+ i.lastIndex = 0;
1078
+ for (let r of e.text.matchAll(i)) r.index !== void 0 && n.push(v.inline(t + r.index, t + r.index + r[0].length, { class: "ophx-search-match" }));
1079
+ }
1080
+ }), y.create(e, n);
1081
+ }
1082
+ var U = t.create({
1083
+ name: "ophxWysiwymRules",
1084
+ addProseMirrorPlugins() {
1085
+ return [new g({ props: {
1086
+ handleTextInput(e, t, n, r) {
1087
+ return r === " " && /\s/.test(e.state.doc.textBetween(Math.max(0, t - 1), t));
1088
+ },
1089
+ handleKeyDown(e, t) {
1090
+ if (t.key !== "Enter" || t.shiftKey) return !1;
1091
+ let { $from: n } = e.state.selection;
1092
+ if (!n.parent.isTextblock || n.parent.content.size !== 0 || n.depth === 0) return !1;
1093
+ let r = n.index(n.depth - 1), i = n.node(n.depth - 1);
1094
+ return [i.maybeChild(r - 1), i.maybeChild(r + 1)].some((e) => e?.isTextblock && e.content.size === 0);
1095
+ }
1096
+ } })];
1097
+ }
1098
+ }), W = t.create({
1099
+ name: "ophxIndentation",
1100
+ addGlobalAttributes() {
1101
+ return [{
1102
+ types: ["paragraph", "heading"],
1103
+ attributes: { indent: {
1104
+ default: 0,
1105
+ parseHTML: (e) => Number(e.dataset.indent ?? 0),
1106
+ renderHTML: (e) => e.indent ? { "data-indent": String(e.indent) } : {}
1107
+ } }
1108
+ }];
1109
+ }
1110
+ }), G = t.create({
1111
+ name: "ophxEditability",
1112
+ addGlobalAttributes() {
1113
+ return [{
1114
+ types: [
1115
+ "paragraph",
1116
+ "heading",
1117
+ "blockquote",
1118
+ "codeBlock",
1119
+ "bulletList",
1120
+ "orderedList",
1121
+ "listItem",
1122
+ "horizontalRule",
1123
+ "image",
1124
+ "table",
1125
+ "tableRow",
1126
+ "tableHeader",
1127
+ "tableCell",
1128
+ "bold",
1129
+ "italic",
1130
+ "underline",
1131
+ "strike",
1132
+ "link",
1133
+ "label"
1134
+ ],
1135
+ attributes: { contenteditable: {
1136
+ default: null,
1137
+ parseHTML: (e) => {
1138
+ let t = e.getAttribute("contenteditable");
1139
+ return t === "true" || t === "false" ? t : null;
1140
+ },
1141
+ renderHTML: (e) => e.contenteditable ? { contenteditable: e.contenteditable } : {}
1142
+ } }
1143
+ }];
1144
+ }
1145
+ });
1146
+ function K(e) {
1147
+ return de.test(e);
1148
+ }
1149
+ // src/editor/ophx-editor.ts
1150
+ function me(e, t) {
1151
+ if (typeof e != "string") return e;
1152
+ let n = document.querySelector(e);
1153
+ if (!n) throw Error(`OphxEditor: элемент для опции "${t}" не найден по селектору "${e}"`);
1154
+ return n;
1155
+ }
1156
+ var q = "ophx-editor-editable", J = class {
1157
+ editor;
1158
+ archiveDefaults = null;
1159
+ editableStorageKey;
1160
+ unmountToolbar = null;
1161
+ constructor(t) {
1162
+ this.editableStorageKey = t.editableStorageKey === void 0 ? q : t.editableStorageKey, this.editor = new e({
1163
+ element: me(t.element, "element"),
1164
+ content: t.content ?? "<p></p>",
1165
+ editable: this.readStoredEditable() ?? t.editable ?? !0,
1166
+ extensions: [
1167
+ a.configure({
1168
+ heading: !1,
1169
+ underline: !1,
1170
+ link: !1
1171
+ }),
1172
+ z.configure({ levels: [
1173
+ 1,
1174
+ 2,
1175
+ 3,
1176
+ 4,
1177
+ 5,
1178
+ 6
1179
+ ] }),
1180
+ o,
1181
+ s.configure({
1182
+ openOnClick: !1,
1183
+ autolink: !0,
1184
+ linkOnPaste: !0
1185
+ }),
1186
+ P.configure({
1187
+ allowBase64: !0,
1188
+ inline: !1
1189
+ }),
1190
+ l.configure({ resizable: !0 }),
1191
+ u,
1192
+ d,
1193
+ f,
1194
+ F,
1195
+ I,
1196
+ R,
1197
+ L,
1198
+ A,
1199
+ W,
1200
+ G,
1201
+ V,
1202
+ U,
1203
+ c.configure({ placeholder: t.placeholder ?? "Начните писать…" }),
1204
+ ...t.extensions ?? []
1205
+ ],
1206
+ editorProps: {
1207
+ attributes: {
1208
+ class: "ophx-editor-surface",
1209
+ spellcheck: "true",
1210
+ lang: "ru",
1211
+ "aria-label": "Редактор документа"
1212
+ },
1213
+ handleDOMEvents: { mousedown: (e, t) => {
1214
+ if (e.editable) return !1;
1215
+ let n = t.target instanceof Element ? t.target : null;
1216
+ if (!n) return !1;
1217
+ if (n.closest(".ophx-image-node, img")) return t.preventDefault(), !0;
1218
+ let r = n.closest("table"), i = n.closest("p, span, a, strong, em, code");
1219
+ return r && (!i || t.metaKey || t.ctrlKey) ? (t.preventDefault(), !0) : !1;
1220
+ } }
1221
+ },
1222
+ onUpdate: t.onUpdate
1223
+ }), this.syncEditableControls(), t.toolbar && (this.unmountToolbar = b(me(t.toolbar, "toolbar"), this.editor, t.toolbarOptions));
1224
+ }
1225
+ getHTML() {
1226
+ return this.editor.getHTML();
1227
+ }
1228
+ getJSON() {
1229
+ return this.editor.getJSON();
1230
+ }
1231
+ getText() {
1232
+ return this.editor.getText();
1233
+ }
1234
+ get isEmpty() {
1235
+ return this.editor.isEmpty;
1236
+ }
1237
+ get isEditable() {
1238
+ return this.editor.isEditable;
1239
+ }
1240
+ setContent(e, t = {}) {
1241
+ this.archiveDefaults = null, this.editor.commands.setContent(e, { emitUpdate: t.emitUpdate ?? !1 });
1242
+ }
1243
+ clearContent(e = {}) {
1244
+ this.setContent("<p></p>", e);
1245
+ }
1246
+ focus(e) {
1247
+ return this.editor.commands.focus(e), this;
1248
+ }
1249
+ blur() {
1250
+ return this.editor.commands.blur(), this;
1251
+ }
1252
+ setEditable(e) {
1253
+ return this.editor.setEditable(e, !1), this.writeStoredEditable(e), this.syncEditableControls(), this;
1254
+ }
1255
+ readStoredEditable() {
1256
+ if (!this.editableStorageKey) return null;
1257
+ try {
1258
+ let e = localStorage.getItem(this.editableStorageKey);
1259
+ return e === "true" || e !== "false" && null;
1260
+ } catch {
1261
+ return null;
1262
+ }
1263
+ }
1264
+ writeStoredEditable(e) {
1265
+ if (this.editableStorageKey) try {
1266
+ localStorage.setItem(this.editableStorageKey, String(e));
1267
+ } catch {}
1268
+ }
1269
+ undo() {
1270
+ return this.editor.commands.undo();
1271
+ }
1272
+ redo() {
1273
+ return this.editor.commands.redo();
1274
+ }
1275
+ canUndo() {
1276
+ return this.editor.can().undo();
1277
+ }
1278
+ canRedo() {
1279
+ return this.editor.can().redo();
1280
+ }
1281
+ syncEditableControls() {
1282
+ let e = this.editor.isEditable;
1283
+ this.editor.view.dom.querySelectorAll(".ophx-heading__toggle, .ophx-image-node__resize-handle").forEach((t) => {
1284
+ t.disabled = !e;
1285
+ }), this.editor.view.dom.dispatchEvent(new Event("ophx:editablechange"));
1286
+ }
1287
+ async loadOphx(e) {
1288
+ let t = await S(e);
1289
+ return this.editor.commands.setContent(t.html, { emitUpdate: !1 }), this.archiveDefaults = {
1290
+ filename: t.filename,
1291
+ title: t.title,
1292
+ styles: t.styles,
1293
+ bodyContentEditable: t.bodyContentEditable
1294
+ }, t;
1295
+ }
1296
+ saveOphx(e = {}) {
1297
+ return x(this.getHTML(), {
1298
+ ...this.archiveDefaults,
1299
+ ...e
1300
+ });
1301
+ }
1302
+ async downloadOphx(e = {}) {
1303
+ let t = e.filename ?? this.archiveDefaults?.filename ?? e.title ?? this.archiveDefaults?.title ?? "Документ";
1304
+ se(await this.saveOphx(e), `${t.replace(/\.ophx$/i, "")}.ophx`);
1305
+ }
1306
+ setSearchQuery(e) {
1307
+ this.editor.commands.setSearchQuery(e);
1308
+ }
1309
+ getFields() {
1310
+ let e = Array.from(this.editor.view.dom.querySelectorAll("[data-ophx-field]")).filter((e) => {
1311
+ let t = e.getAttribute("data-ophx-field") ?? "";
1312
+ return e.classList.contains("ophx-field") && e.getAttribute("contenteditable") === "true" && K(t);
1313
+ }), t = /* @__PURE__ */ new Map();
1314
+ for (let n of e) {
1315
+ let e = n.getAttribute("data-ophx-field");
1316
+ t.set(e, (t.get(e) ?? 0) + 1);
1317
+ }
1318
+ return e.filter((e) => t.get(e.getAttribute("data-ophx-field")) === 1).map((e) => ({
1319
+ id: e.getAttribute("data-ophx-field"),
1320
+ label: e.getAttribute("data-ophx-label"),
1321
+ required: e.getAttribute("data-ophx-required") === "true",
1322
+ value: e.textContent ?? ""
1323
+ }));
1324
+ }
1325
+ getMissingRequiredFields() {
1326
+ return this.getFields().filter((e) => e.required && !e.value.trim());
1327
+ }
1328
+ destroy() {
1329
+ this.unmountToolbar?.(), this.unmountToolbar = null, this.editor.destroy();
1330
+ }
1331
+ };
1332
+ // src/editor/template-fields.ts
1333
+ function he(e, t = "f") {
1334
+ if (!/^[A-Za-z0-9._-]+$/.test(t)) throw Error("Префикс идентификаторов полей содержит недопустимые символы");
1335
+ let n = new DOMParser().parseFromString(e, "text/html"), r = new Set(Array.from(n.querySelectorAll("[data-ophx-field]")).map((e) => e.getAttribute("data-ophx-field") ?? "").filter(Boolean)), i = n.createTreeWalker(n.body, 4), a = [];
1336
+ for (; i.nextNode();) {
1337
+ let e = i.currentNode, t = e.parentElement;
1338
+ t && !t.closest("code, pre, script, style, [contenteditable=\"false\"], [data-ophx-field]") && /_{2,}/.test(e.data) && a.push(e);
1339
+ }
1340
+ let o = [], s = 1;
1341
+ for (let e of a) {
1342
+ let i = n.createDocumentFragment(), a = 0;
1343
+ for (let c of e.data.matchAll(/_{2,}/g)) {
1344
+ if (c.index === void 0) continue;
1345
+ c.index > a && i.append(n.createTextNode(e.data.slice(a, c.index)));
1346
+ let l = `${t}${s++}`;
1347
+ for (; r.has(l);) l = `${t}${s++}`;
1348
+ r.add(l), o.push(l);
1349
+ let u = n.createElement("span");
1350
+ u.className = "ophx-field", u.setAttribute("contenteditable", "true"), u.setAttribute("data-ophx-field", l), i.append(u), a = c.index + c[0].length;
1351
+ }
1352
+ a < e.data.length && i.append(n.createTextNode(e.data.slice(a))), e.replaceWith(i);
1353
+ }
1354
+ return {
1355
+ html: n.body.innerHTML,
1356
+ count: o.length,
1357
+ ids: o
1358
+ };
1359
+ }
1360
+ export { z as CollapsibleHeading, W as Indentation, A as Label, I as OphxBlockField, G as OphxEditability, J as OphxEditor, F as OphxInlineField, R as OphxReadonlyBlock, L as OphxReadonlyInline, V as TextSearch, U as WysiwymRules, he as convertUnderscoresToFields, se as downloadBlob, x as exportOphx, S as importOphx, K as isValidOphxFieldId, b as mountToolbar };