@azlib/editor 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1278 @@
1
+ import DOMPurify from "isomorphic-dompurify";
2
+ import { Schema } from "prosemirror-model";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/core/errors.ts
6
+ const createDiagnostic = (code, severity, message, location) => ({
7
+ code,
8
+ severity,
9
+ message,
10
+ location
11
+ });
12
+ const createRecoverableError = (code, message, details) => ({
13
+ code,
14
+ message,
15
+ severity: "warning",
16
+ recoverable: true,
17
+ details
18
+ });
19
+ //#endregion
20
+ //#region src/core/commandDiagnostics.ts
21
+ const createUnknownCommandDiagnostic = (commandName) => createDiagnostic("UNKNOWN_COMMAND", "warning", `Command '${commandName}' is not available in current editor capabilities.`);
22
+ const createNoHistoryDiagnostic = (type) => createDiagnostic("NO_HISTORY", "info", `No ${type} history entry is available for this editor session.`);
23
+ //#endregion
24
+ //#region src/core/formattingCommands.ts
25
+ const withCommandMetadata = (commandName) => ({ document }) => ({ document: {
26
+ ...document,
27
+ revision: document.revision + 1,
28
+ metadata: {
29
+ ...document.metadata,
30
+ lastCommand: commandName
31
+ }
32
+ } });
33
+ const createFormattingCommandHandlers = () => ({
34
+ bold: withCommandMetadata("bold"),
35
+ italic: withCommandMetadata("italic"),
36
+ underline: withCommandMetadata("underline"),
37
+ heading: withCommandMetadata("heading"),
38
+ list: withCommandMetadata("list"),
39
+ align: withCommandMetadata("align"),
40
+ link: withCommandMetadata("link")
41
+ });
42
+ //#endregion
43
+ //#region src/core/commands.ts
44
+ const createCommandRegistry = (capabilities) => {
45
+ const allowed = new Set(capabilities && capabilities.length > 0 ? capabilities : [
46
+ "bold",
47
+ "italic",
48
+ "underline",
49
+ "heading",
50
+ "list",
51
+ "align",
52
+ "link",
53
+ "undo",
54
+ "redo"
55
+ ]);
56
+ const registry = /* @__PURE__ */ new Map();
57
+ const formattingHandlers = createFormattingCommandHandlers();
58
+ const registerIfAllowed = (name, handler) => {
59
+ if (allowed.has(name)) registry.set(name, handler);
60
+ };
61
+ Object.entries(formattingHandlers).forEach(([commandName, handler]) => {
62
+ registerIfAllowed(commandName, handler);
63
+ });
64
+ return registry;
65
+ };
66
+ const executeCommand = (registry, commandName, context, params) => {
67
+ const handler = registry.get(commandName);
68
+ if (!handler) return {
69
+ document: context.document,
70
+ diagnostics: [createUnknownCommandDiagnostic(commandName)]
71
+ };
72
+ return handler(context, params);
73
+ };
74
+ //#endregion
75
+ //#region src/core/history.ts
76
+ var EditorHistory = class {
77
+ undoStack = [];
78
+ redoStack = [];
79
+ record(snapshot) {
80
+ this.undoStack.push(snapshot);
81
+ this.redoStack.length = 0;
82
+ }
83
+ undo(current) {
84
+ const previous = this.undoStack.pop();
85
+ if (!previous) return current;
86
+ this.redoStack.push(current);
87
+ return {
88
+ ...previous,
89
+ revision: current.revision + 1,
90
+ metadata: {
91
+ ...previous.metadata,
92
+ lastCommand: "undo"
93
+ }
94
+ };
95
+ }
96
+ redo(current) {
97
+ const next = this.redoStack.pop();
98
+ if (!next) return current;
99
+ this.undoStack.push(current);
100
+ return {
101
+ ...next,
102
+ revision: current.revision + 1,
103
+ metadata: {
104
+ ...next.metadata,
105
+ lastCommand: "redo"
106
+ }
107
+ };
108
+ }
109
+ };
110
+ //#endregion
111
+ //#region src/transforms/html.ts
112
+ const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
113
+ const allowedStyleProperties = new Set([
114
+ "font-family",
115
+ "font-size",
116
+ "text-align",
117
+ "margin-left"
118
+ ]);
119
+ const normalizeStyleValue = (property, value) => {
120
+ const next = value.trim().toLowerCase();
121
+ if (property === "font-family") return next === "serif" || next === "monospace" ? next : null;
122
+ if (property === "font-size") return [
123
+ "12px",
124
+ "16px",
125
+ "20px",
126
+ "28px"
127
+ ].includes(next) ? next : null;
128
+ if (property === "text-align") return [
129
+ "left",
130
+ "center",
131
+ "right",
132
+ "justify"
133
+ ].includes(next) ? next : null;
134
+ if (property === "margin-left") {
135
+ const match = /^([0-9]{1,3})px$/.exec(next);
136
+ if (!match) return null;
137
+ const amount = Number.parseInt(match[1], 10);
138
+ return amount >= 0 && amount % 24 === 0 ? `${amount}px` : null;
139
+ }
140
+ return null;
141
+ };
142
+ const normalizeStyleAttribute = (styleValue) => {
143
+ const entries = styleValue.split(";").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => {
144
+ const separator = entry.indexOf(":");
145
+ if (separator < 0) return null;
146
+ const property = entry.slice(0, separator).trim().toLowerCase();
147
+ const value = entry.slice(separator + 1).trim();
148
+ if (!allowedStyleProperties.has(property)) return null;
149
+ const normalized = normalizeStyleValue(property, value);
150
+ if (!normalized) return null;
151
+ return `${property}:${normalized}`;
152
+ }).filter((entry) => Boolean(entry));
153
+ if (entries.length === 0) return null;
154
+ return entries.join(";");
155
+ };
156
+ const normalizeHtml = (value) => {
157
+ if (typeof DOMParser === "undefined") return value.trim().length > 0 ? value : "<p></p>";
158
+ const doc = new DOMParser().parseFromString(`<div>${value}</div>`, "text/html");
159
+ const root = doc.body.firstElementChild;
160
+ if (!root) return "<p></p>";
161
+ root.querySelectorAll("b").forEach((element) => {
162
+ const replacement = doc.createElement("strong");
163
+ replacement.innerHTML = element.innerHTML;
164
+ element.replaceWith(replacement);
165
+ });
166
+ root.querySelectorAll("i").forEach((element) => {
167
+ const replacement = doc.createElement("em");
168
+ replacement.innerHTML = element.innerHTML;
169
+ element.replaceWith(replacement);
170
+ });
171
+ root.querySelectorAll("strike").forEach((element) => {
172
+ const replacement = doc.createElement("s");
173
+ replacement.innerHTML = element.innerHTML;
174
+ element.replaceWith(replacement);
175
+ });
176
+ root.querySelectorAll("*").forEach((element) => {
177
+ const tag = element.tagName.toLowerCase();
178
+ Array.from(element.attributes).forEach((attribute) => {
179
+ const name = attribute.name.toLowerCase();
180
+ const valueAttr = attribute.value;
181
+ if (name === "style") {
182
+ const normalized = normalizeStyleAttribute(valueAttr);
183
+ if (normalized) element.setAttribute("style", normalized);
184
+ else element.removeAttribute("style");
185
+ return;
186
+ }
187
+ if (name === "href" || name === "src") {
188
+ if (!/^(https?:|mailto:|tel:)/i.test(valueAttr.trim())) element.removeAttribute(attribute.name);
189
+ return;
190
+ }
191
+ if (name === "target" || name === "rel" || name === "alt" || name === "data-formula" || name === "controls" || name === "dir") return;
192
+ element.removeAttribute(attribute.name);
193
+ });
194
+ if (tag === "a") {
195
+ element.setAttribute("rel", "noopener noreferrer");
196
+ element.setAttribute("target", "_blank");
197
+ }
198
+ if (tag === "video") element.setAttribute("controls", "true");
199
+ });
200
+ Array.from(root.childNodes).forEach((node) => {
201
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent?.trim().length ?? 0) > 0) {
202
+ const paragraph = doc.createElement("p");
203
+ paragraph.textContent = node.textContent ?? "";
204
+ root.replaceChild(paragraph, node);
205
+ }
206
+ });
207
+ const normalized = root.innerHTML.trim();
208
+ return normalized.length > 0 ? normalized : "<p></p>";
209
+ };
210
+ const sanitizeHtml = (payload) => DOMPurify.sanitize(payload, {
211
+ ALLOWED_TAGS: [
212
+ "p",
213
+ "br",
214
+ "b",
215
+ "i",
216
+ "strike",
217
+ "strong",
218
+ "em",
219
+ "u",
220
+ "s",
221
+ "code",
222
+ "pre",
223
+ "blockquote",
224
+ "h1",
225
+ "h2",
226
+ "ul",
227
+ "ol",
228
+ "li",
229
+ "a",
230
+ "img",
231
+ "video",
232
+ "span",
233
+ "sub",
234
+ "sup",
235
+ "div"
236
+ ],
237
+ ALLOWED_ATTR: [
238
+ "href",
239
+ "src",
240
+ "alt",
241
+ "target",
242
+ "rel",
243
+ "style",
244
+ "data-formula",
245
+ "controls",
246
+ "dir"
247
+ ],
248
+ FORBID_TAGS: [
249
+ "script",
250
+ "style",
251
+ "iframe",
252
+ "object",
253
+ "embed",
254
+ "form",
255
+ "input",
256
+ "button",
257
+ "textarea",
258
+ "select"
259
+ ]
260
+ });
261
+ const importHtml = (payload) => {
262
+ const diagnostics = [];
263
+ const normalized = normalizeHtml(sanitizeHtml(payload));
264
+ if (normalized !== payload) diagnostics.push(createDiagnostic("SANITIZED_CONTENT", "warning", "Input HTML was sanitized to remove unsupported or unsafe markup."));
265
+ if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_HTML", "info", "The provided HTML content is empty after normalization."));
266
+ return {
267
+ richText: stripHtml(normalized),
268
+ sanitizedHtml: normalized,
269
+ diagnostics
270
+ };
271
+ };
272
+ const exportHtml = (richText) => {
273
+ return normalizeHtml(`<p>${richText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")}</p>`);
274
+ };
275
+ //#endregion
276
+ //#region src/transforms/diagnostics.ts
277
+ const createUnsupportedMarkupDiagnostic = (details) => createDiagnostic("UNSUPPORTED_MARKUP", "warning", details ?? "Some markup could not be fully represented and was normalized.");
278
+ const createRecoverableParseDiagnostic = (format, details) => createDiagnostic("RECOVERABLE_PARSE", "warning", details ?? `Some ${format.toUpperCase()} content was partially recovered during import.`);
279
+ //#endregion
280
+ //#region src/transforms/importFallback.ts
281
+ const applyImportFallback = (input, base) => {
282
+ const payload = input.payload.trim();
283
+ const diagnostics = [createRecoverableParseDiagnostic(input.format === "html" ? "html" : "markdown")];
284
+ const stripped = input.format === "html" ? payload.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim() : payload;
285
+ if (stripped.length === 0) diagnostics.push(createUnsupportedMarkupDiagnostic("Input produced no recoverable text content after normalization."));
286
+ return {
287
+ document: {
288
+ ...base,
289
+ content: {
290
+ richText: stripped,
291
+ html: input.format === "html" ? payload : base.content.html
292
+ },
293
+ metadata: {
294
+ ...base.metadata,
295
+ importFallback: true
296
+ }
297
+ },
298
+ diagnostics
299
+ };
300
+ };
301
+ //#endregion
302
+ //#region src/transforms/markdown.ts
303
+ const normalizeMarkdown = (value) => value.replace(/\r\n?/g, "\n");
304
+ const importMarkdown = (payload) => {
305
+ const diagnostics = [];
306
+ const normalized = normalizeMarkdown(payload);
307
+ if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_MARKDOWN", "info", "The provided markdown content is empty."));
308
+ return {
309
+ richText: normalized,
310
+ diagnostics
311
+ };
312
+ };
313
+ const exportMarkdown = (richText) => normalizeMarkdown(richText);
314
+ //#endregion
315
+ //#region src/transforms/representationSwitch.ts
316
+ const importRepresentation = (input, base) => {
317
+ if (input.format === "rich") {
318
+ const normalizedHtml = normalizeHtml(exportHtml(input.payload));
319
+ return {
320
+ document: {
321
+ ...base,
322
+ content: {
323
+ richText: input.payload,
324
+ html: normalizedHtml
325
+ }
326
+ },
327
+ diagnostics: []
328
+ };
329
+ }
330
+ if (input.format === "markdown") try {
331
+ const result = importMarkdown(input.payload);
332
+ return {
333
+ document: {
334
+ ...base,
335
+ content: { richText: result.richText },
336
+ metadata: {
337
+ ...base.metadata,
338
+ importedFrom: "markdown"
339
+ }
340
+ },
341
+ diagnostics: result.diagnostics
342
+ };
343
+ } catch {
344
+ return applyImportFallback(input, base);
345
+ }
346
+ try {
347
+ const result = importHtml(input.payload);
348
+ return {
349
+ document: {
350
+ ...base,
351
+ content: {
352
+ richText: result.richText,
353
+ html: result.sanitizedHtml
354
+ },
355
+ metadata: {
356
+ ...base.metadata,
357
+ importedFrom: "html"
358
+ }
359
+ },
360
+ diagnostics: result.diagnostics
361
+ };
362
+ } catch {
363
+ return applyImportFallback(input, base);
364
+ }
365
+ };
366
+ const exportRepresentation = (document, format) => {
367
+ if (format === "rich") return {
368
+ format,
369
+ payload: document.content.richText,
370
+ diagnostics: []
371
+ };
372
+ if (format === "markdown") return {
373
+ format,
374
+ payload: exportMarkdown(document.content.richText),
375
+ diagnostics: []
376
+ };
377
+ return {
378
+ format,
379
+ payload: document.content.html ? normalizeHtml(document.content.html) : normalizeHtml(exportHtml(document.content.richText)),
380
+ diagnostics: []
381
+ };
382
+ };
383
+ //#endregion
384
+ //#region src/core/createEditor.ts
385
+ const createDefaultDocument = () => ({
386
+ id: crypto.randomUUID(),
387
+ content: { richText: "" },
388
+ metadata: {},
389
+ revision: 0
390
+ });
391
+ const fromInput = (input, base) => importRepresentation(input, base);
392
+ const toExport = (document, format) => exportRepresentation(document, format);
393
+ const createEditor = (config = {}) => {
394
+ const registry = createCommandRegistry(config.commandCapabilities);
395
+ const history = new EditorHistory();
396
+ let isDestroyed = false;
397
+ let currentDocument = createDefaultDocument();
398
+ if (config.initialContent) {
399
+ const initialized = fromInput(config.initialContent, currentDocument);
400
+ currentDocument = initialized.document;
401
+ if (initialized.diagnostics.length > 0) config.onError?.(createRecoverableError("INITIAL_CONTENT_DIAGNOSTIC", "Initial content was normalized during editor initialization.", { diagnostics: initialized.diagnostics }));
402
+ }
403
+ const ensureActive = () => {
404
+ if (!isDestroyed) return true;
405
+ config.onError?.(createRecoverableError("EDITOR_DESTROYED", "This editor instance has already been destroyed."));
406
+ return false;
407
+ };
408
+ const emitChange = () => {
409
+ config.onChange?.(currentDocument);
410
+ };
411
+ return {
412
+ getDocument: () => currentDocument,
413
+ execute: (commandName, params) => {
414
+ if (!ensureActive()) return {
415
+ ok: false,
416
+ document: currentDocument,
417
+ diagnostics: []
418
+ };
419
+ const result = executeCommand(registry, commandName, { document: currentDocument }, params);
420
+ if (commandName === "undo") {
421
+ const next = history.undo(currentDocument);
422
+ if (next === currentDocument) return {
423
+ ok: false,
424
+ document: currentDocument,
425
+ diagnostics: [createNoHistoryDiagnostic("undo")]
426
+ };
427
+ currentDocument = next;
428
+ emitChange();
429
+ return {
430
+ ok: true,
431
+ document: currentDocument,
432
+ diagnostics: []
433
+ };
434
+ }
435
+ if (commandName === "redo") {
436
+ const next = history.redo(currentDocument);
437
+ if (next === currentDocument) return {
438
+ ok: false,
439
+ document: currentDocument,
440
+ diagnostics: [createNoHistoryDiagnostic("redo")]
441
+ };
442
+ currentDocument = next;
443
+ emitChange();
444
+ return {
445
+ ok: true,
446
+ document: currentDocument,
447
+ diagnostics: []
448
+ };
449
+ }
450
+ history.record(currentDocument);
451
+ currentDocument = result.document;
452
+ emitChange();
453
+ return {
454
+ ok: (result.diagnostics?.length ?? 0) === 0,
455
+ document: currentDocument,
456
+ diagnostics: result.diagnostics ?? []
457
+ };
458
+ },
459
+ export: (format) => toExport(currentDocument, format),
460
+ import: (input) => {
461
+ if (!ensureActive()) return {
462
+ ok: false,
463
+ document: currentDocument,
464
+ diagnostics: []
465
+ };
466
+ const result = fromInput(input, {
467
+ ...currentDocument,
468
+ revision: currentDocument.revision + 1
469
+ });
470
+ currentDocument = result.document;
471
+ emitChange();
472
+ return {
473
+ ok: result.diagnostics.length === 0,
474
+ document: currentDocument,
475
+ diagnostics: result.diagnostics
476
+ };
477
+ },
478
+ mount: (target) => {
479
+ if (!ensureActive()) return;
480
+ },
481
+ unmount: () => {},
482
+ destroy: () => {
483
+ isDestroyed = true;
484
+ }
485
+ };
486
+ };
487
+ //#endregion
488
+ //#region src/core/schema.ts
489
+ const editorNodeNames = {
490
+ doc: "doc",
491
+ paragraph: "paragraph",
492
+ heading: "heading",
493
+ text: "text",
494
+ bulletList: "bullet_list",
495
+ orderedList: "ordered_list",
496
+ listItem: "list_item",
497
+ hardBreak: "hard_break"
498
+ };
499
+ const editorMarkNames = {
500
+ strong: "strong",
501
+ em: "em",
502
+ underline: "underline",
503
+ link: "link"
504
+ };
505
+ const createEditorSchema = () => new Schema({
506
+ nodes: {
507
+ doc: { content: "block+" },
508
+ paragraph: {
509
+ content: "inline*",
510
+ group: "block",
511
+ parseDOM: [{ tag: "p" }],
512
+ toDOM: () => ["p", 0]
513
+ },
514
+ heading: {
515
+ attrs: { level: { default: 1 } },
516
+ content: "inline*",
517
+ group: "block",
518
+ defining: true,
519
+ parseDOM: [
520
+ {
521
+ tag: "h1",
522
+ attrs: { level: 1 }
523
+ },
524
+ {
525
+ tag: "h2",
526
+ attrs: { level: 2 }
527
+ },
528
+ {
529
+ tag: "h3",
530
+ attrs: { level: 3 }
531
+ }
532
+ ],
533
+ toDOM: (node) => [`h${Math.max(1, Math.min(3, Number(node.attrs.level) || 1))}`, 0]
534
+ },
535
+ bullet_list: {
536
+ content: "list_item+",
537
+ group: "block",
538
+ parseDOM: [{ tag: "ul" }],
539
+ toDOM: () => ["ul", 0]
540
+ },
541
+ ordered_list: {
542
+ attrs: { order: { default: 1 } },
543
+ content: "list_item+",
544
+ group: "block",
545
+ parseDOM: [{
546
+ tag: "ol",
547
+ getAttrs: (dom) => {
548
+ if (!(dom instanceof HTMLOListElement)) return { order: 1 };
549
+ return { order: dom.start || 1 };
550
+ }
551
+ }],
552
+ toDOM: (node) => [
553
+ "ol",
554
+ { start: node.attrs.order || 1 },
555
+ 0
556
+ ]
557
+ },
558
+ list_item: {
559
+ content: "paragraph block*",
560
+ parseDOM: [{ tag: "li" }],
561
+ toDOM: () => ["li", 0]
562
+ },
563
+ text: { group: "inline" },
564
+ hard_break: {
565
+ inline: true,
566
+ group: "inline",
567
+ selectable: false,
568
+ parseDOM: [{ tag: "br" }],
569
+ toDOM: () => ["br"]
570
+ }
571
+ },
572
+ marks: {
573
+ strong: {
574
+ parseDOM: [{ tag: "strong" }, {
575
+ tag: "b",
576
+ getAttrs: () => null
577
+ }],
578
+ toDOM: () => ["strong", 0]
579
+ },
580
+ em: {
581
+ parseDOM: [{ tag: "em" }, {
582
+ tag: "i",
583
+ getAttrs: () => null
584
+ }],
585
+ toDOM: () => ["em", 0]
586
+ },
587
+ underline: {
588
+ parseDOM: [{ tag: "u" }],
589
+ toDOM: () => ["u", 0]
590
+ },
591
+ link: {
592
+ attrs: {
593
+ href: {},
594
+ title: { default: null }
595
+ },
596
+ inclusive: false,
597
+ parseDOM: [{
598
+ tag: "a[href]",
599
+ getAttrs: (dom) => {
600
+ if (!(dom instanceof HTMLAnchorElement)) return false;
601
+ return {
602
+ href: dom.getAttribute("href"),
603
+ title: dom.getAttribute("title")
604
+ };
605
+ }
606
+ }],
607
+ toDOM: (node) => [
608
+ "a",
609
+ {
610
+ href: node.attrs.href,
611
+ title: node.attrs.title
612
+ },
613
+ 0
614
+ ]
615
+ }
616
+ }
617
+ });
618
+ const editorSchema = createEditorSchema();
619
+ //#endregion
620
+ //#region src/core/toolbarModel.ts
621
+ const defaultToolbarActions = [
622
+ {
623
+ command: "bold",
624
+ label: "Bold",
625
+ group: "inline"
626
+ },
627
+ {
628
+ command: "italic",
629
+ label: "Italic",
630
+ group: "inline"
631
+ },
632
+ {
633
+ command: "underline",
634
+ label: "Underline",
635
+ group: "inline"
636
+ },
637
+ {
638
+ command: "heading",
639
+ label: "Heading",
640
+ group: "block"
641
+ },
642
+ {
643
+ command: "list",
644
+ label: "List",
645
+ group: "block"
646
+ },
647
+ {
648
+ command: "align",
649
+ label: "Align",
650
+ group: "block"
651
+ },
652
+ {
653
+ command: "link",
654
+ label: "Link",
655
+ group: "inline"
656
+ },
657
+ {
658
+ command: "undo",
659
+ label: "Undo",
660
+ group: "history"
661
+ },
662
+ {
663
+ command: "redo",
664
+ label: "Redo",
665
+ group: "history"
666
+ }
667
+ ];
668
+ //#endregion
669
+ //#region src/rich-text/mountEditor.ts
670
+ const commandLabels = {
671
+ bold: "Bold",
672
+ italic: "Italic",
673
+ underline: "Underline",
674
+ strike: "Strike",
675
+ code: "Inline code",
676
+ blockquote: "Blockquote",
677
+ "code-block": "Code block",
678
+ "indent-": "Outdent",
679
+ "indent+": "Indent",
680
+ link: "Link",
681
+ image: "Image",
682
+ video: "Video",
683
+ formula: "Formula",
684
+ clean: "Remove formatting"
685
+ };
686
+ const commandIconSvg = {
687
+ bold: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 2.5h4.5a2.5 2.5 0 010 5H4z\"/><path d=\"M4 7.5h5a3 3 0 010 6H4z\"/></svg>",
688
+ italic: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\"><path d=\"M9.8 2.5H5.5\"/><path d=\"M10.5 13.5H6.2\"/><path d=\"M9.3 2.5 6.7 13.5\"/></svg>",
689
+ underline: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M4 2.5v4.2a4 4 0 008 0V2.5\"/><path d=\"M3 13.5h10\"/></svg>",
690
+ strike: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M4 4.5c.8-1.2 2-1.9 3.7-1.9 2 0 3.3 1 3.3 2.5 0 3-6.5 1.8-6.5 4.6 0 1.5 1.3 2.3 3.4 2.3 1.6 0 2.9-.6 3.8-1.7\"/><path d=\"M2.5 8h11\"/></svg>",
691
+ code: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4 2.5 8 6 12\"/><path d=\"M10 4 13.5 8 10 12\"/><path d=\"M8.7 2.8 7.3 13.2\"/></svg>",
692
+ blockquote: "<svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M3 3h4v4H4.8c.1 1.8-.7 3.2-1.8 4L2 10c1-.7 1.5-1.7 1.4-3H3zM9 3h4v4h-2.2c.1 1.8-.7 3.2-1.8 4L8 10c1-.7 1.5-1.7 1.4-3H9z\"/></svg>",
693
+ "code-block": "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"2.5\" width=\"11\" height=\"11\" rx=\"1.8\"/><path d=\"M6.2 6 4.5 8l1.7 2\"/><path d=\"M9.8 6 11.5 8l-1.7 2\"/><path d=\"M8.6 5.5 7.4 10.5\"/></svg>",
694
+ "indent-": "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 4h4\"/><path d=\"M9 8h4\"/><path d=\"M9 12h4\"/><path d=\"M7 5.5 4 8l3 2.5\"/></svg>",
695
+ "indent+": "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 4h4\"/><path d=\"M3 8h4\"/><path d=\"M3 12h4\"/><path d=\"M9 5.5 12 8l-3 2.5\"/></svg>",
696
+ link: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M6.3 9.7 4.9 11a2.4 2.4 0 103.4 3.4l1.5-1.4\"/><path d=\"M9.7 6.3 11 4.9a2.4 2.4 0 10-3.4-3.4L6.1 2.9\"/><path d=\"M5.9 10.1 10.1 5.9\"/></svg>",
697
+ image: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.3\" y=\"2.5\" width=\"11.4\" height=\"11\" rx=\"1.8\"/><circle cx=\"6\" cy=\"6\" r=\"1.2\" fill=\"currentColor\" stroke=\"none\"/><path d=\"m3.8 11 2.8-2.8 2.2 2.2 1.6-1.6 1.8 2.2\"/></svg>",
698
+ video: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.2\" y=\"3\" width=\"8.8\" height=\"10\" rx=\"1.6\"/><path d=\"M11 6.2 13.8 4.8v6.4L11 9.8z\"/><path d=\"M6.2 6.4v3.2l2.7-1.6z\" fill=\"currentColor\" stroke=\"none\"/></svg>",
699
+ formula: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 4h5\"/><path d=\"M3 12h5\"/><path d=\"M8 4 5 8l3 4\"/><path d=\"M10.3 5.8v4.4\"/><path d=\"M12.5 8h-4.4\"/></svg>",
700
+ clean: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.8 12.5h6.8\"/><path d=\"m4.2 3.3 6 6\"/><path d=\"m10.2 2.7 3 3-2 2-3-3z\"/><path d=\"M6.2 11.2 4.1 13.3\"/></svg>"
701
+ };
702
+ const createToolbarIcon = (command, fallback) => {
703
+ const icon = document.createElement("span");
704
+ icon.className = "az-rich-editor-toolbar-icon";
705
+ const svgMarkup = commandIconSvg[command];
706
+ if (svgMarkup) icon.innerHTML = svgMarkup;
707
+ else icon.textContent = fallback;
708
+ return icon;
709
+ };
710
+ const selectOptionLabels = {
711
+ font: {
712
+ default: "A",
713
+ serif: "As",
714
+ monospace: "Am"
715
+ },
716
+ size: {
717
+ small: "S",
718
+ normal: "N",
719
+ large: "L",
720
+ huge: "XL"
721
+ },
722
+ header: {
723
+ normal: "P",
724
+ h1: "H1",
725
+ h2: "H2"
726
+ },
727
+ script: {
728
+ normal: "x",
729
+ sub: "x_",
730
+ super: "x^"
731
+ },
732
+ align: {
733
+ left: "L",
734
+ center: "C",
735
+ right: "R",
736
+ justify: "J"
737
+ },
738
+ list: {
739
+ none: "-",
740
+ ordered: "1.",
741
+ bullet: "o"
742
+ },
743
+ direction: {
744
+ ltr: "->",
745
+ rtl: "<-"
746
+ }
747
+ };
748
+ const safeUrl = (value) => {
749
+ const trimmed = value.trim();
750
+ if (!trimmed) return null;
751
+ if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return trimmed;
752
+ return null;
753
+ };
754
+ const closestElement = (node, root) => {
755
+ let current = node;
756
+ while (current && current !== root) {
757
+ if (current instanceof HTMLElement) return current;
758
+ current = current.parentNode;
759
+ }
760
+ return null;
761
+ };
762
+ const closestByTag = (node, tagName, root) => {
763
+ const desired = tagName.toLowerCase();
764
+ let current = node;
765
+ while (current && current !== root) {
766
+ if (current instanceof HTMLElement && current.tagName.toLowerCase() === desired) return current;
767
+ current = current.parentNode;
768
+ }
769
+ return null;
770
+ };
771
+ const unwrapElement = (element) => {
772
+ const parent = element.parentNode;
773
+ if (!parent) return;
774
+ while (element.firstChild) parent.insertBefore(element.firstChild, element);
775
+ parent.removeChild(element);
776
+ };
777
+ const placeCaretInside = (element) => {
778
+ const selection = window.getSelection();
779
+ if (!selection) return;
780
+ const range = document.createRange();
781
+ range.selectNodeContents(element);
782
+ range.collapse(false);
783
+ selection.removeAllRanges();
784
+ selection.addRange(range);
785
+ };
786
+ const getSelectionRange = (root) => {
787
+ const selection = window.getSelection();
788
+ if (!selection || selection.rangeCount === 0) return null;
789
+ const range = selection.getRangeAt(0);
790
+ if (!root.contains(range.commonAncestorContainer)) return null;
791
+ return range;
792
+ };
793
+ const wrapRange = (range, tagName, attrs) => {
794
+ const wrapper = document.createElement(tagName);
795
+ if (attrs) Object.entries(attrs).forEach(([key, value]) => wrapper.setAttribute(key, value));
796
+ if (range.collapsed) {
797
+ wrapper.append(document.createTextNode("​"));
798
+ range.insertNode(wrapper);
799
+ placeCaretInside(wrapper);
800
+ return wrapper;
801
+ }
802
+ const fragment = range.extractContents();
803
+ wrapper.append(fragment);
804
+ range.insertNode(wrapper);
805
+ const selection = window.getSelection();
806
+ if (selection) {
807
+ const nextRange = document.createRange();
808
+ nextRange.selectNodeContents(wrapper);
809
+ selection.removeAllRanges();
810
+ selection.addRange(nextRange);
811
+ }
812
+ return wrapper;
813
+ };
814
+ const blockTags = new Set([
815
+ "p",
816
+ "div",
817
+ "h1",
818
+ "h2",
819
+ "blockquote",
820
+ "pre",
821
+ "li"
822
+ ]);
823
+ const getBlockElement = (range, root) => {
824
+ const start = closestElement(range.startContainer, root);
825
+ if (!start) return root;
826
+ let current = start;
827
+ while (current && current !== root) {
828
+ if (blockTags.has(current.tagName.toLowerCase())) return current;
829
+ current = current.parentElement;
830
+ }
831
+ return root;
832
+ };
833
+ const replaceBlockTag = (block, tagName) => {
834
+ if (block.tagName.toLowerCase() === tagName.toLowerCase()) return block;
835
+ const replacement = document.createElement(tagName);
836
+ while (block.firstChild) replacement.append(block.firstChild);
837
+ Array.from(block.attributes).forEach((attribute) => {
838
+ if (attribute.name === "style" || attribute.name === "dir") replacement.setAttribute(attribute.name, attribute.value);
839
+ });
840
+ block.parentNode?.replaceChild(replacement, block);
841
+ return replacement;
842
+ };
843
+ const applyInlineTag = (tagName, root, range) => {
844
+ const existing = closestByTag(range.commonAncestorContainer, tagName, root);
845
+ if (existing) {
846
+ unwrapElement(existing);
847
+ return;
848
+ }
849
+ wrapRange(range, tagName);
850
+ };
851
+ const applyScript = (value, root, range) => {
852
+ const sub = closestByTag(range.commonAncestorContainer, "sub", root);
853
+ const sup = closestByTag(range.commonAncestorContainer, "sup", root);
854
+ if (sub) unwrapElement(sub);
855
+ if (sup) unwrapElement(sup);
856
+ if (value === "sub") wrapRange(range, "sub");
857
+ if (value === "super") wrapRange(range, "sup");
858
+ };
859
+ const applyHeader = (value, root, range) => {
860
+ const block = getBlockElement(range, root);
861
+ if (value === "h1" || value === "h2") {
862
+ replaceBlockTag(block, value);
863
+ return;
864
+ }
865
+ replaceBlockTag(block, "p");
866
+ };
867
+ const toggleBlockTag = (tagName, root, range) => {
868
+ const block = getBlockElement(range, root);
869
+ if (block.tagName.toLowerCase() === tagName) {
870
+ replaceBlockTag(block, "p");
871
+ return;
872
+ }
873
+ replaceBlockTag(block, tagName);
874
+ };
875
+ const ensureList = (tagName, root, range, remove) => {
876
+ const block = getBlockElement(range, root);
877
+ const li = closestByTag(block, "li", root);
878
+ if (li) {
879
+ const list = li.parentElement;
880
+ if (!list) return;
881
+ if (remove || list.tagName.toLowerCase() === tagName) {
882
+ const paragraph = document.createElement("p");
883
+ paragraph.textContent = li.textContent ?? "";
884
+ list.parentNode?.insertBefore(paragraph, list);
885
+ li.remove();
886
+ if (!list.querySelector("li")) list.remove();
887
+ return;
888
+ }
889
+ const nextList = document.createElement(tagName);
890
+ const nextLi = document.createElement("li");
891
+ nextLi.textContent = li.textContent ?? "";
892
+ nextList.append(nextLi);
893
+ list.parentNode?.insertBefore(nextList, list.nextSibling);
894
+ li.remove();
895
+ if (!list.querySelector("li")) list.remove();
896
+ return;
897
+ }
898
+ if (remove) return;
899
+ const list = document.createElement(tagName);
900
+ const listItem = document.createElement("li");
901
+ listItem.innerHTML = block.innerHTML;
902
+ list.append(listItem);
903
+ block.parentNode?.replaceChild(list, block);
904
+ };
905
+ const applyIndent = (root, range, direction) => {
906
+ const block = getBlockElement(range, root);
907
+ const current = Number.parseInt(block.style.marginLeft || "0", 10) || 0;
908
+ const next = direction === "+" ? current + 24 : Math.max(0, current - 24);
909
+ block.style.marginLeft = next === 0 ? "" : `${next}px`;
910
+ };
911
+ const applyAlign = (root, range, align) => {
912
+ const block = getBlockElement(range, root);
913
+ block.style.textAlign = align;
914
+ };
915
+ const applyDirection = (root, range, value) => {
916
+ getBlockElement(range, root).setAttribute("dir", value);
917
+ };
918
+ const clearFormatting = (root, range) => {
919
+ if (range.collapsed) {
920
+ const block = getBlockElement(range, root);
921
+ const text = block.textContent ?? "";
922
+ block.replaceChildren(document.createTextNode(text));
923
+ return;
924
+ }
925
+ const selectedText = range.toString();
926
+ range.deleteContents();
927
+ range.insertNode(document.createTextNode(selectedText));
928
+ };
929
+ const insertNodeAtRange = (range, node) => {
930
+ range.deleteContents();
931
+ range.insertNode(node);
932
+ const selection = window.getSelection();
933
+ if (selection) {
934
+ const after = document.createRange();
935
+ after.setStartAfter(node);
936
+ after.collapse(true);
937
+ selection.removeAllRanges();
938
+ selection.addRange(after);
939
+ }
940
+ };
941
+ const applyEmbed = async (type, range, requestEmbedValue) => {
942
+ const value = await Promise.resolve(requestEmbedValue?.(type) ?? null);
943
+ if (!value || value.trim().length === 0) return;
944
+ if (type === "formula") {
945
+ const formula = document.createElement("span");
946
+ formula.setAttribute("data-formula", "true");
947
+ formula.textContent = value.trim();
948
+ insertNodeAtRange(range, formula);
949
+ return;
950
+ }
951
+ const url = safeUrl(value);
952
+ if (!url) return;
953
+ if (type === "link") {
954
+ const selected = range.toString().trim();
955
+ const anchor = document.createElement("a");
956
+ anchor.href = url;
957
+ anchor.rel = "noopener noreferrer";
958
+ anchor.target = "_blank";
959
+ anchor.textContent = selected.length > 0 ? selected : url;
960
+ insertNodeAtRange(range, anchor);
961
+ return;
962
+ }
963
+ if (type === "image") {
964
+ const image = document.createElement("img");
965
+ image.src = url;
966
+ image.alt = "embedded image";
967
+ insertNodeAtRange(range, image);
968
+ return;
969
+ }
970
+ const video = document.createElement("video");
971
+ video.setAttribute("controls", "true");
972
+ video.src = url;
973
+ insertNodeAtRange(range, video);
974
+ };
975
+ const applyButtonCommand = async (command, editable, requestEmbedValue) => {
976
+ editable.focus();
977
+ const range = getSelectionRange(editable);
978
+ if (!range) return;
979
+ switch (command) {
980
+ case "bold":
981
+ applyInlineTag("strong", editable, range);
982
+ break;
983
+ case "italic":
984
+ applyInlineTag("em", editable, range);
985
+ break;
986
+ case "underline":
987
+ applyInlineTag("u", editable, range);
988
+ break;
989
+ case "strike":
990
+ applyInlineTag("s", editable, range);
991
+ break;
992
+ case "code":
993
+ applyInlineTag("code", editable, range);
994
+ break;
995
+ case "blockquote":
996
+ toggleBlockTag("blockquote", editable, range);
997
+ break;
998
+ case "code-block":
999
+ toggleBlockTag("pre", editable, range);
1000
+ break;
1001
+ case "indent-":
1002
+ applyIndent(editable, range, "-");
1003
+ break;
1004
+ case "indent+":
1005
+ applyIndent(editable, range, "+");
1006
+ break;
1007
+ case "link":
1008
+ await applyEmbed("link", range, requestEmbedValue);
1009
+ break;
1010
+ case "image":
1011
+ await applyEmbed("image", range, requestEmbedValue);
1012
+ break;
1013
+ case "video":
1014
+ await applyEmbed("video", range, requestEmbedValue);
1015
+ break;
1016
+ case "formula":
1017
+ await applyEmbed("formula", range, requestEmbedValue);
1018
+ break;
1019
+ case "clean":
1020
+ clearFormatting(editable, range);
1021
+ break;
1022
+ default: break;
1023
+ }
1024
+ };
1025
+ const applySelectCommand = (command, value, editable) => {
1026
+ editable.focus();
1027
+ const range = getSelectionRange(editable);
1028
+ if (!range) return;
1029
+ switch (command) {
1030
+ case "font":
1031
+ if (value === "default") clearFormatting(editable, range);
1032
+ else wrapRange(range, "span", { style: `font-family:${value === "serif" ? "serif" : "monospace"}` });
1033
+ break;
1034
+ case "size":
1035
+ wrapRange(range, "span", { style: value === "small" ? "font-size:12px" : value === "large" ? "font-size:20px" : value === "huge" ? "font-size:28px" : "font-size:16px" });
1036
+ break;
1037
+ case "header":
1038
+ applyHeader(value, editable, range);
1039
+ break;
1040
+ case "script":
1041
+ applyScript(value, editable, range);
1042
+ break;
1043
+ case "align":
1044
+ applyAlign(editable, range, value);
1045
+ break;
1046
+ case "list":
1047
+ ensureList(value === "ordered" ? "ol" : "ul", editable, range, value === "none");
1048
+ break;
1049
+ case "direction":
1050
+ applyDirection(editable, range, value === "rtl" ? "rtl" : "ltr");
1051
+ break;
1052
+ default: break;
1053
+ }
1054
+ };
1055
+ const renderButton = (command, editable, requestEmbedValue, onMutate) => {
1056
+ const button = document.createElement("button");
1057
+ button.type = "button";
1058
+ button.className = "az-rich-editor-toolbar-button";
1059
+ button.setAttribute("aria-label", commandLabels[command] ?? command);
1060
+ button.title = commandLabels[command] ?? command;
1061
+ button.append(createToolbarIcon(command, command));
1062
+ button.addEventListener("click", async () => {
1063
+ await applyButtonCommand(command, editable, requestEmbedValue);
1064
+ onMutate();
1065
+ });
1066
+ return button;
1067
+ };
1068
+ const createSelect = (format, options, editable, onMutate) => {
1069
+ const select = document.createElement("select");
1070
+ select.className = "az-rich-editor-toolbar-select";
1071
+ select.setAttribute("aria-label", format);
1072
+ options.forEach((optionValue) => {
1073
+ const option = document.createElement("option");
1074
+ option.value = optionValue;
1075
+ option.textContent = selectOptionLabels[format]?.[optionValue] ?? optionValue;
1076
+ option.title = optionValue;
1077
+ select.append(option);
1078
+ });
1079
+ select.addEventListener("change", () => {
1080
+ applySelectCommand(format, select.value, editable);
1081
+ onMutate();
1082
+ });
1083
+ return select;
1084
+ };
1085
+ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
1086
+ const toolbar = document.createElement("div");
1087
+ toolbar.className = "az-rich-editor-toolbar";
1088
+ toolbar.setAttribute("role", "toolbar");
1089
+ toolbar.setAttribute("aria-label", "Rich editor toolbar");
1090
+ [
1091
+ {
1092
+ format: "font",
1093
+ options: [
1094
+ "default",
1095
+ "serif",
1096
+ "monospace"
1097
+ ]
1098
+ },
1099
+ {
1100
+ format: "size",
1101
+ options: [
1102
+ "small",
1103
+ "normal",
1104
+ "large",
1105
+ "huge"
1106
+ ]
1107
+ },
1108
+ {
1109
+ format: "header",
1110
+ options: [
1111
+ "normal",
1112
+ "h1",
1113
+ "h2"
1114
+ ]
1115
+ },
1116
+ {
1117
+ format: "script",
1118
+ options: [
1119
+ "normal",
1120
+ "sub",
1121
+ "super"
1122
+ ]
1123
+ },
1124
+ {
1125
+ format: "align",
1126
+ options: [
1127
+ "left",
1128
+ "center",
1129
+ "right",
1130
+ "justify"
1131
+ ]
1132
+ },
1133
+ {
1134
+ format: "list",
1135
+ options: [
1136
+ "none",
1137
+ "ordered",
1138
+ "bullet"
1139
+ ]
1140
+ },
1141
+ {
1142
+ format: "direction",
1143
+ options: ["ltr", "rtl"]
1144
+ }
1145
+ ].forEach((config) => toolbar.append(createSelect(config.format, config.options, editable, onMutate)));
1146
+ [
1147
+ "bold",
1148
+ "italic",
1149
+ "underline",
1150
+ "strike",
1151
+ "code",
1152
+ "blockquote",
1153
+ "code-block",
1154
+ "indent-",
1155
+ "indent+",
1156
+ "link",
1157
+ "image",
1158
+ "video",
1159
+ "formula",
1160
+ "clean"
1161
+ ].forEach((command) => toolbar.append(renderButton(command, editable, requestEmbedValue, onMutate)));
1162
+ return toolbar;
1163
+ };
1164
+ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue }) => {
1165
+ host.innerHTML = "";
1166
+ const wrapper = document.createElement("div");
1167
+ wrapper.className = "az-rich-editor";
1168
+ const editable = document.createElement("div");
1169
+ editable.className = "az-rich-editor-input";
1170
+ editable.setAttribute("aria-label", "Rich editor input");
1171
+ editable.setAttribute("role", "textbox");
1172
+ editable.setAttribute("aria-multiline", "true");
1173
+ editable.setAttribute("contenteditable", disabled ? "false" : "true");
1174
+ editable.dataset.placeholder = "Compose an epic...";
1175
+ editable.innerHTML = initialHtml;
1176
+ const emitChange = () => onChange(editable.innerHTML);
1177
+ const toolbar = buildToolbar(editable, requestEmbedValue, emitChange);
1178
+ const inputHandler = () => emitChange();
1179
+ editable.addEventListener("input", inputHandler);
1180
+ wrapper.append(toolbar, editable);
1181
+ host.append(wrapper);
1182
+ return {
1183
+ destroy: () => {
1184
+ editable.removeEventListener("input", inputHandler);
1185
+ host.innerHTML = "";
1186
+ },
1187
+ getHtml: () => editable.innerHTML,
1188
+ setHtml: (html) => {
1189
+ editable.innerHTML = html;
1190
+ }
1191
+ };
1192
+ };
1193
+ //#endregion
1194
+ //#region src/adapters/dom/createDomAdapter.ts
1195
+ const createDomAdapter = (editor) => {
1196
+ let target = null;
1197
+ let mountedEditor = null;
1198
+ return {
1199
+ mount: (nextTarget) => {
1200
+ target = nextTarget;
1201
+ editor.mount(nextTarget);
1202
+ if (!(nextTarget instanceof HTMLElement)) return;
1203
+ mountedEditor = mountRichTextEditor({
1204
+ host: nextTarget,
1205
+ initialHtml: editor.export("html").payload,
1206
+ requestEmbedValue: () => null,
1207
+ onChange: (html) => {
1208
+ editor.import({
1209
+ format: "html",
1210
+ payload: html
1211
+ });
1212
+ }
1213
+ });
1214
+ },
1215
+ unmount: () => {
1216
+ if (!target) return;
1217
+ mountedEditor?.destroy();
1218
+ mountedEditor = null;
1219
+ editor.unmount();
1220
+ target = null;
1221
+ },
1222
+ destroy: () => {
1223
+ mountedEditor?.destroy();
1224
+ mountedEditor = null;
1225
+ editor.destroy();
1226
+ target = null;
1227
+ }
1228
+ };
1229
+ };
1230
+ //#endregion
1231
+ //#region src/adapters/react/useEditorAdapter.tsx
1232
+ const useEditorAdapter = (config) => {
1233
+ const editor = useMemo(() => createEditor(config), [config]);
1234
+ useEffect(() => () => {
1235
+ editor.destroy();
1236
+ }, [editor]);
1237
+ return editor;
1238
+ };
1239
+ function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue }) {
1240
+ const editor = useEditorAdapter({
1241
+ initialContent,
1242
+ onChange,
1243
+ onError
1244
+ });
1245
+ const hostRef = useRef(null);
1246
+ useEffect(() => {
1247
+ const host = hostRef.current;
1248
+ if (!host) return;
1249
+ const mounted = mountRichTextEditor({
1250
+ host,
1251
+ initialHtml: editor.export("html").payload,
1252
+ disabled,
1253
+ requestEmbedValue: onRequestEmbedValue,
1254
+ onChange: (html) => {
1255
+ editor.import({
1256
+ format: "html",
1257
+ payload: html
1258
+ });
1259
+ }
1260
+ });
1261
+ return () => {
1262
+ mounted.destroy();
1263
+ };
1264
+ }, [
1265
+ disabled,
1266
+ editor,
1267
+ onRequestEmbedValue
1268
+ ]);
1269
+ return /* @__PURE__ */ jsx("div", {
1270
+ "aria-label": "Rich editor",
1271
+ className,
1272
+ ref: hostRef
1273
+ });
1274
+ }
1275
+ //#endregion
1276
+ export { RichEditorAdapter, createDomAdapter, createEditor, createEditorSchema, defaultToolbarActions, editorMarkNames, editorNodeNames, editorSchema, exportRepresentation, importRepresentation, useEditorAdapter };
1277
+
1278
+ //# sourceMappingURL=index.mjs.map