react-word-editor 0.0.0-stage → 0.1.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.js ADDED
@@ -0,0 +1,964 @@
1
+ import {
2
+ replaceVariables,
3
+ replaceVariablesInString
4
+ } from "./chunk-DCMBE2J7.js";
5
+ import {
6
+ BLOCK_TYPES,
7
+ COLOR_SWATCHES,
8
+ DEFAULT_FONT_FAMILIES,
9
+ DEFAULT_FONT_SIZES,
10
+ DEFAULT_PDF_OPTIONS,
11
+ DEFAULT_TOOLBAR,
12
+ EMPTY_DOC,
13
+ INDENT_STEP_PX,
14
+ LINE_HEIGHTS,
15
+ NODE_ID_ATTRIBUTE,
16
+ NODE_ID_TYPES,
17
+ NodeIds,
18
+ PARAGRAPH_SPACING,
19
+ PDF_TREE_VERSION,
20
+ ParagraphFormat,
21
+ RichTableCell,
22
+ RichTableHeader,
23
+ SILENT_META,
24
+ STANDARD_FONTS,
25
+ TABLE_BORDER_OPTIONS,
26
+ TABLE_PICKER_SIZE,
27
+ TEXT_CASES,
28
+ buildEditorExtensions,
29
+ buildSchemaExtensions,
30
+ cleanHtml,
31
+ ensureNodeIds,
32
+ getEditorSchema,
33
+ getTableBorderState,
34
+ htmlToJSON,
35
+ htmlToText,
36
+ insertImages,
37
+ isDocJson,
38
+ isPdfTree,
39
+ isWordHtml,
40
+ jsonToHTML,
41
+ jsonToPdfTree,
42
+ jsonToText,
43
+ matchSpacingPreset,
44
+ normalizeHtmlInput,
45
+ readAsDataUrl,
46
+ textToHtml,
47
+ toDocJson
48
+ } from "./chunk-JQEG2JQ7.js";
49
+
50
+ // src/RichTextEditor.jsx
51
+ import React9, { useEffect as useEffect3, useMemo, useRef as useRef4 } from "react";
52
+ import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
53
+
54
+ // src/editor/outputs.js
55
+ var OUTPUT_FORMATS = ["html", "text", "json", "pdf", "all"];
56
+ var isEmptyDoc = (editor) => editor.isEmpty;
57
+ var getOutput = (editor, format = "html", pdfOptions) => {
58
+ if (!editor || editor.isDestroyed) return null;
59
+ const json = () => editor.getJSON();
60
+ const html = () => isEmptyDoc(editor) ? "" : editor.getHTML();
61
+ switch (format) {
62
+ case "text":
63
+ return jsonToText(json());
64
+ case "json":
65
+ return json();
66
+ case "pdf":
67
+ return jsonToPdfTree(json(), pdfOptions);
68
+ case "all": {
69
+ const doc = json();
70
+ return {
71
+ html: html(),
72
+ text: jsonToText(doc),
73
+ json: doc,
74
+ pdf: jsonToPdfTree(doc, pdfOptions)
75
+ };
76
+ }
77
+ default:
78
+ return html();
79
+ }
80
+ };
81
+ var toEditorContent = (value) => {
82
+ if (isDocJson(value)) return value;
83
+ if (value && typeof value === "object" && isDocJson(value.json)) return value.json;
84
+ if (value && typeof value === "object" && typeof value.html === "string") {
85
+ return normalizeHtmlInput(value.html);
86
+ }
87
+ if (value && typeof value === "object") return "";
88
+ return normalizeHtmlInput(value);
89
+ };
90
+ var sameValue = (a, b) => {
91
+ if (a === b) return true;
92
+ if (a && b && typeof a === "object" && typeof b === "object") {
93
+ try {
94
+ return JSON.stringify(a) === JSON.stringify(b);
95
+ } catch (e) {
96
+ return false;
97
+ }
98
+ }
99
+ return false;
100
+ };
101
+
102
+ // src/editor/handle.js
103
+ var createEditorHandle = (editor, { pdfOptions } = {}) => ({
104
+ tiptap: editor,
105
+ getContent: (args = {}) => getOutput(editor, args.format || "html", pdfOptions),
106
+ setContent: (value, { emitUpdate = true } = {}) => {
107
+ editor.commands.setContent(toEditorContent(value), { emitUpdate });
108
+ },
109
+ insertContent: (value) => editor.chain().focus().insertContent(toEditorContent(value)).run(),
110
+ getHTML: () => getOutput(editor, "html"),
111
+ getText: () => getOutput(editor, "text"),
112
+ getJSON: () => editor.getJSON(),
113
+ getPdfTree: (options) => getOutput(editor, "pdf", options || pdfOptions),
114
+ getBody: () => {
115
+ var _a;
116
+ return (_a = editor.view) == null ? void 0 : _a.dom;
117
+ },
118
+ focus: () => editor.commands.focus(),
119
+ isEmpty: () => editor.isEmpty,
120
+ setDisabled: (disabled) => editor.setEditable(!disabled)
121
+ });
122
+
123
+ // src/toolbar/Toolbar.jsx
124
+ import React7, { useRef as useRef3 } from "react";
125
+ import {
126
+ AlignCenter,
127
+ AlignJustify,
128
+ AlignLeft,
129
+ AlignRight,
130
+ Baseline,
131
+ Bold,
132
+ Highlighter,
133
+ CaseSensitive,
134
+ ImagePlus,
135
+ IndentDecrease,
136
+ IndentIncrease,
137
+ Italic,
138
+ Link2,
139
+ List,
140
+ ListOrdered,
141
+ Minus,
142
+ Redo2,
143
+ RemoveFormatting,
144
+ Strikethrough,
145
+ Subscript,
146
+ Superscript,
147
+ Underline,
148
+ Undo2,
149
+ UnfoldVertical
150
+ } from "lucide-react";
151
+
152
+ // src/toolbar/controls.jsx
153
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
154
+ import { jsx, jsxs } from "react/jsx-runtime";
155
+ var keepSelection = (e) => e.preventDefault();
156
+ var ToolbarButton = ({ icon: Icon, label, active, disabled, onClick, children }) => /* @__PURE__ */ jsxs(
157
+ "button",
158
+ {
159
+ type: "button",
160
+ className: `fe-btn${active ? " fe-btn--active" : ""}`,
161
+ title: label,
162
+ "aria-label": label,
163
+ "aria-pressed": active ? "true" : void 0,
164
+ disabled,
165
+ onMouseDown: keepSelection,
166
+ onClick,
167
+ children: [
168
+ Icon ? /* @__PURE__ */ jsx(Icon, { size: 16, strokeWidth: 2 }) : null,
169
+ children
170
+ ]
171
+ }
172
+ );
173
+ var Divider = () => /* @__PURE__ */ jsx("span", { className: "fe-divider", "aria-hidden": "true" });
174
+ var Popover = ({ trigger, children, disabled, className }) => {
175
+ const [open, setOpen] = useState(false);
176
+ const [alignRight, setAlignRight] = useState(false);
177
+ const ref = useRef(null);
178
+ const panelRef = useRef(null);
179
+ useLayoutEffect(() => {
180
+ var _a, _b;
181
+ if (!open) return setAlignRight(false);
182
+ const panel = panelRef.current;
183
+ const bounds = (_b = (_a = ref.current) == null ? void 0 : _a.closest(".fe-editor")) == null ? void 0 : _b.getBoundingClientRect();
184
+ if (panel && bounds) setAlignRight(panel.getBoundingClientRect().right > bounds.right);
185
+ return void 0;
186
+ }, [open]);
187
+ useEffect(() => {
188
+ if (!open) return void 0;
189
+ const close = (e) => {
190
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
191
+ };
192
+ const onKey = (e) => e.key === "Escape" && setOpen(false);
193
+ document.addEventListener("mousedown", close);
194
+ document.addEventListener("keydown", onKey);
195
+ return () => {
196
+ document.removeEventListener("mousedown", close);
197
+ document.removeEventListener("keydown", onKey);
198
+ };
199
+ }, [open]);
200
+ return /* @__PURE__ */ jsxs("span", { className: "fe-popover-anchor", ref, children: [
201
+ trigger({ open, toggle: () => !disabled && setOpen((o) => !o) }),
202
+ open ? /* @__PURE__ */ jsx(
203
+ "div",
204
+ {
205
+ ref: panelRef,
206
+ className: `fe-popover${className ? ` ${className}` : ""}${alignRight ? " fe-popover--right" : ""}`,
207
+ onMouseDown: keepSelection,
208
+ children: children({ close: () => setOpen(false) })
209
+ }
210
+ ) : null
211
+ ] });
212
+ };
213
+
214
+ // src/toolbar/ToolbarSelect.jsx
215
+ import React2, { useEffect as useEffect2, useRef as useRef2 } from "react";
216
+ import { jsx as jsx2 } from "react/jsx-runtime";
217
+ var SelectList = ({ options, value, minWidth, onPick }) => {
218
+ const listRef = useRef2(null);
219
+ useEffect2(() => {
220
+ var _a, _b, _c;
221
+ (_c = (_b = (_a = listRef.current) == null ? void 0 : _a.querySelector(".is-checked")) == null ? void 0 : _b.scrollIntoView) == null ? void 0 : _c.call(_b, { block: "nearest" });
222
+ }, []);
223
+ return /* @__PURE__ */ jsx2("div", { className: "fe-menu fe-menu--select", role: "listbox", ref: listRef, style: { minWidth }, children: options.filter((opt) => !opt.hidden).map((opt) => /* @__PURE__ */ jsx2(
224
+ "button",
225
+ {
226
+ type: "button",
227
+ role: "option",
228
+ "aria-selected": opt.value === value,
229
+ className: `fe-menu__item${opt.value === value ? " is-checked" : ""}`,
230
+ style: opt.style,
231
+ onClick: () => onPick(opt.value),
232
+ children: opt.menuLabel || opt.label
233
+ },
234
+ opt.value
235
+ )) });
236
+ };
237
+ var ToolbarSelect = ({ label, value, options, disabled, onChange, width }) => {
238
+ const current = options.find((opt) => opt.value === value);
239
+ return /* @__PURE__ */ jsx2(
240
+ Popover,
241
+ {
242
+ disabled,
243
+ className: "fe-popover--menu",
244
+ trigger: ({ toggle: toggle2, open }) => {
245
+ var _a;
246
+ return /* @__PURE__ */ jsx2(
247
+ "button",
248
+ {
249
+ type: "button",
250
+ className: `fe-select${open ? " fe-select--open" : ""}`,
251
+ title: label,
252
+ "aria-label": label,
253
+ "aria-haspopup": "listbox",
254
+ "aria-expanded": open,
255
+ disabled,
256
+ style: width ? { width } : void 0,
257
+ onMouseDown: (e) => e.preventDefault(),
258
+ onClick: toggle2,
259
+ children: /* @__PURE__ */ jsx2("span", { className: "fe-select__value", children: (_a = current == null ? void 0 : current.label) != null ? _a : label })
260
+ }
261
+ );
262
+ },
263
+ children: ({ close }) => /* @__PURE__ */ jsx2(
264
+ SelectList,
265
+ {
266
+ options,
267
+ value,
268
+ minWidth: width,
269
+ onPick: (v) => {
270
+ if (v !== value) onChange(v);
271
+ close();
272
+ }
273
+ }
274
+ )
275
+ }
276
+ );
277
+ };
278
+ var ToolbarSelect_default = ToolbarSelect;
279
+
280
+ // src/toolbar/ColorButton.jsx
281
+ import React3 from "react";
282
+ import { ChevronDown } from "lucide-react";
283
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
284
+ var ColorButton = ({ icon: Icon, label, color, disabled, onPick, onClear, clearLabel }) => /* @__PURE__ */ jsx3(
285
+ Popover,
286
+ {
287
+ disabled,
288
+ trigger: ({ toggle: toggle2, open }) => /* @__PURE__ */ jsxs2(
289
+ "button",
290
+ {
291
+ type: "button",
292
+ className: `fe-btn fe-btn--split${open ? " fe-btn--active" : ""}`,
293
+ title: label,
294
+ "aria-label": label,
295
+ disabled,
296
+ onMouseDown: (e) => e.preventDefault(),
297
+ onClick: toggle2,
298
+ children: [
299
+ /* @__PURE__ */ jsxs2("span", { className: "fe-color-icon", children: [
300
+ /* @__PURE__ */ jsx3(Icon, { size: 16 }),
301
+ /* @__PURE__ */ jsx3("span", { className: "fe-color-bar", style: { background: color || "transparent" } })
302
+ ] }),
303
+ /* @__PURE__ */ jsx3(ChevronDown, { size: 12 })
304
+ ]
305
+ }
306
+ ),
307
+ children: ({ close }) => /* @__PURE__ */ jsxs2("div", { className: "fe-colors", children: [
308
+ /* @__PURE__ */ jsx3(
309
+ "button",
310
+ {
311
+ type: "button",
312
+ className: "fe-colors__clear",
313
+ onClick: () => {
314
+ onClear();
315
+ close();
316
+ },
317
+ children: clearLabel
318
+ }
319
+ ),
320
+ /* @__PURE__ */ jsx3("div", { className: "fe-colors__grid", children: COLOR_SWATCHES.map((c) => /* @__PURE__ */ jsx3(
321
+ "button",
322
+ {
323
+ type: "button",
324
+ className: "fe-swatch",
325
+ title: c,
326
+ "aria-label": c,
327
+ style: { background: c },
328
+ onClick: () => {
329
+ onPick(c);
330
+ close();
331
+ }
332
+ },
333
+ c
334
+ )) }),
335
+ /* @__PURE__ */ jsxs2("label", { className: "fe-colors__custom", children: [
336
+ "More colours",
337
+ /* @__PURE__ */ jsx3(
338
+ "input",
339
+ {
340
+ type: "color",
341
+ value: /^#[0-9a-f]{6}$/i.test(color || "") ? color : "#000000",
342
+ onChange: (e) => onPick(e.target.value)
343
+ }
344
+ )
345
+ ] })
346
+ ] })
347
+ }
348
+ );
349
+ var ColorButton_default = ColorButton;
350
+
351
+ // src/toolbar/TablePicker.jsx
352
+ import React4, { useState as useState2 } from "react";
353
+ import { Table2 } from "lucide-react";
354
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
355
+ var TablePicker = ({ editor, disabled }) => {
356
+ const [hover, setHover] = useState2({ rows: 0, cols: 0 });
357
+ const { rows, cols } = TABLE_PICKER_SIZE;
358
+ return /* @__PURE__ */ jsx4(
359
+ Popover,
360
+ {
361
+ disabled,
362
+ trigger: ({ toggle: toggle2, open }) => /* @__PURE__ */ jsx4(
363
+ "button",
364
+ {
365
+ type: "button",
366
+ className: `fe-btn${open ? " fe-btn--active" : ""}`,
367
+ title: "Insert table",
368
+ "aria-label": "Insert table",
369
+ disabled,
370
+ onMouseDown: (e) => e.preventDefault(),
371
+ onClick: toggle2,
372
+ children: /* @__PURE__ */ jsx4(Table2, { size: 16 })
373
+ }
374
+ ),
375
+ children: ({ close }) => /* @__PURE__ */ jsxs3("div", { className: "fe-table-picker", onMouseLeave: () => setHover({ rows: 0, cols: 0 }), children: [
376
+ /* @__PURE__ */ jsx4(
377
+ "div",
378
+ {
379
+ className: "fe-table-picker__grid",
380
+ style: { gridTemplateColumns: `repeat(${cols}, 16px)` },
381
+ children: Array.from({ length: rows * cols }, (_, i) => {
382
+ const r = Math.floor(i / cols) + 1;
383
+ const c = i % cols + 1;
384
+ const on = r <= hover.rows && c <= hover.cols;
385
+ return /* @__PURE__ */ jsx4(
386
+ "button",
387
+ {
388
+ type: "button",
389
+ "aria-label": `${r} by ${c} table`,
390
+ className: `fe-table-picker__cell${on ? " is-on" : ""}`,
391
+ onMouseEnter: () => setHover({ rows: r, cols: c }),
392
+ onClick: () => {
393
+ editor.chain().focus().insertTable({ rows: r, cols: c, withHeaderRow: false }).run();
394
+ close();
395
+ }
396
+ },
397
+ i
398
+ );
399
+ })
400
+ }
401
+ ),
402
+ /* @__PURE__ */ jsx4("div", { className: "fe-table-picker__label", children: hover.rows ? `${hover.rows} \xD7 ${hover.cols} table` : "Insert table" })
403
+ ] })
404
+ }
405
+ );
406
+ };
407
+ var TablePicker_default = TablePicker;
408
+
409
+ // src/toolbar/MenuButton.jsx
410
+ import React5 from "react";
411
+ import { Check, ChevronDown as ChevronDown2 } from "lucide-react";
412
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
413
+ var MenuButton = ({ icon: Icon, label, items, value, disabled, onSelect, showChecks }) => /* @__PURE__ */ jsx5(
414
+ Popover,
415
+ {
416
+ disabled,
417
+ className: "fe-popover--menu",
418
+ trigger: ({ toggle: toggle2, open }) => /* @__PURE__ */ jsxs4(
419
+ "button",
420
+ {
421
+ type: "button",
422
+ className: `fe-btn fe-btn--split${open ? " fe-btn--active" : ""}`,
423
+ title: label,
424
+ "aria-label": label,
425
+ "aria-haspopup": "menu",
426
+ "aria-expanded": open,
427
+ disabled,
428
+ onMouseDown: (e) => e.preventDefault(),
429
+ onClick: toggle2,
430
+ children: [
431
+ /* @__PURE__ */ jsx5(Icon, { size: 16 }),
432
+ /* @__PURE__ */ jsx5(ChevronDown2, { size: 12 })
433
+ ]
434
+ }
435
+ ),
436
+ children: ({ close }) => /* @__PURE__ */ jsx5("div", { className: "fe-menu", role: "menu", "aria-label": label, children: items.map((item, i) => {
437
+ var _a;
438
+ if (item === "|") return /* @__PURE__ */ jsx5("div", { className: "fe-menu__sep", role: "separator" }, `s${i}`);
439
+ const checked = (_a = item.checked) != null ? _a : value !== void 0 && item.value === value;
440
+ return /* @__PURE__ */ jsxs4(
441
+ "button",
442
+ {
443
+ type: "button",
444
+ role: showChecks ? "menuitemcheckbox" : "menuitem",
445
+ "aria-checked": showChecks ? !!checked : void 0,
446
+ className: `fe-menu__item${checked ? " is-checked" : ""}`,
447
+ disabled: item.disabled,
448
+ onClick: () => {
449
+ onSelect(item.value);
450
+ close();
451
+ },
452
+ children: [
453
+ showChecks ? /* @__PURE__ */ jsx5("span", { className: "fe-menu__check", children: checked ? /* @__PURE__ */ jsx5(Check, { size: 14 }) : null }) : null,
454
+ item.icon ? /* @__PURE__ */ jsx5("span", { className: "fe-menu__icon", children: item.icon }) : null,
455
+ /* @__PURE__ */ jsx5("span", { children: item.label })
456
+ ]
457
+ },
458
+ item.value
459
+ );
460
+ }) })
461
+ }
462
+ );
463
+ var MenuButton_default = MenuButton;
464
+
465
+ // src/toolbar/icons.jsx
466
+ import React6 from "react";
467
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
468
+ var LINES = {
469
+ top: "M2 2H14",
470
+ bottom: "M2 14H14",
471
+ left: "M2 2V14",
472
+ right: "M14 2V14",
473
+ insideH: "M2 8H14",
474
+ insideV: "M8 2V14"
475
+ };
476
+ var BorderIcon = ({ lines = [] }) => /* @__PURE__ */ jsxs5("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [
477
+ /* @__PURE__ */ jsx6(
478
+ "path",
479
+ {
480
+ d: Object.values(LINES).join(" "),
481
+ stroke: "currentColor",
482
+ strokeOpacity: ".35",
483
+ strokeDasharray: "1 1.5"
484
+ }
485
+ ),
486
+ lines.length ? /* @__PURE__ */ jsx6("path", { d: lines.map((l) => LINES[l]).join(" "), stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "square" }) : null
487
+ ] });
488
+ var SpacingIcon = ({ gap = 2 }) => {
489
+ const rows = [];
490
+ let y = 2;
491
+ for (let group = 0; group < 3 && y < 15; group += 1) {
492
+ rows.push(y, y + 2);
493
+ y += 4 + gap;
494
+ }
495
+ return /* @__PURE__ */ jsx6("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx6("path", { d: rows.filter((r) => r < 15).map((r) => `M2 ${r}H14`).join(" "), stroke: "currentColor" }) });
496
+ };
497
+
498
+ // src/toolbar/Toolbar.jsx
499
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
500
+ var toggle = (editor, cmd) => () => editor.chain().focus()[cmd]().run();
501
+ var setBlockType = (editor, value) => {
502
+ const chain = editor.chain().focus();
503
+ if (value === "paragraph") chain.setParagraph().run();
504
+ else chain.setHeading({ level: Number(value.slice(1)) }).run();
505
+ };
506
+ var promptLink = (editor) => {
507
+ const previous = editor.getAttributes("link").href || "";
508
+ const url = window.prompt("Link URL (leave empty to remove)", previous);
509
+ if (url === null) return;
510
+ const chain = editor.chain().focus().extendMarkRange("link");
511
+ if (!url.trim()) chain.unsetLink().run();
512
+ else chain.setLink({ href: url.trim() }).run();
513
+ };
514
+ var SPACING_ITEMS = [
515
+ PARAGRAPH_SPACING[0],
516
+ "|",
517
+ ...PARAGRAPH_SPACING.slice(1).map((p, i) => ({ ...p, icon: /* @__PURE__ */ jsx7(SpacingIcon, { gap: i }) }))
518
+ ];
519
+ var ALIGN_ITEMS = {
520
+ alignLeft: { icon: AlignLeft, label: "Align left", value: "left" },
521
+ alignCenter: { icon: AlignCenter, label: "Center", value: "center" },
522
+ alignRight: { icon: AlignRight, label: "Align right", value: "right" },
523
+ alignJustify: { icon: AlignJustify, label: "Justify", value: "justify" }
524
+ };
525
+ var MARK_ITEMS = {
526
+ bold: { icon: Bold, label: "Bold (Ctrl+B)", cmd: "toggleBold" },
527
+ italic: { icon: Italic, label: "Italic (Ctrl+I)", cmd: "toggleItalic" },
528
+ underline: { icon: Underline, label: "Underline (Ctrl+U)", cmd: "toggleUnderline" },
529
+ strike: { icon: Strikethrough, label: "Strikethrough", cmd: "toggleStrike" },
530
+ subscript: { icon: Subscript, label: "Subscript", cmd: "toggleSubscript" },
531
+ superscript: { icon: Superscript, label: "Superscript", cmd: "toggleSuperscript" },
532
+ bulletList: { icon: List, label: "Bullets", cmd: "toggleBulletList" },
533
+ orderedList: { icon: ListOrdered, label: "Numbering", cmd: "toggleOrderedList" }
534
+ };
535
+ var Toolbar = ({ editor, state, items = DEFAULT_TOOLBAR, disabled, fontFamilies, fontSizes, imageOptions }) => {
536
+ const fileRef = useRef3(null);
537
+ if (!editor) return null;
538
+ const sizeOptions = [
539
+ { value: "", label: "Size", menuLabel: "Default" },
540
+ ...fontSizes.map((s) => ({ value: `${s}pt`, label: s })),
541
+ ...state.fontSize && !fontSizes.some((s) => `${s}pt` === state.fontSize) ? [{ value: state.fontSize, label: state.fontSize.replace(/pt$/, ""), hidden: true }] : []
542
+ ];
543
+ const familyOptions = [
544
+ ...fontFamilies.map((f) => f.value ? { ...f, style: { fontFamily: f.value } } : f),
545
+ ...state.fontFamily && !fontFamilies.some((f) => f.value === state.fontFamily) ? [{ value: state.fontFamily, label: state.fontFamily.split(",")[0].replace(/["']/g, ""), hidden: true }] : []
546
+ ];
547
+ const render = (key, i) => {
548
+ if (key === "|") return /* @__PURE__ */ jsx7(Divider, {}, `d${i}`);
549
+ if (MARK_ITEMS[key]) {
550
+ const { icon, label, cmd } = MARK_ITEMS[key];
551
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon, label, active: state[key], disabled, onClick: toggle(editor, cmd) }, key);
552
+ }
553
+ if (ALIGN_ITEMS[key]) {
554
+ const { icon, label, value } = ALIGN_ITEMS[key];
555
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon, label, active: state.textAlign === value, disabled, onClick: () => editor.chain().focus().setTextAlign(value).run() }, key);
556
+ }
557
+ switch (key) {
558
+ case "undo":
559
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon: Undo2, label: "Undo (Ctrl+Z)", disabled: disabled || !state.canUndo, onClick: toggle(editor, "undo") }, key);
560
+ case "redo":
561
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon: Redo2, label: "Redo (Ctrl+Y)", disabled: disabled || !state.canRedo, onClick: toggle(editor, "redo") }, key);
562
+ case "blockType":
563
+ return /* @__PURE__ */ jsx7(ToolbarSelect_default, { label: "Text style", width: 108, value: state.blockType, options: BLOCK_TYPES, disabled, onChange: (v) => setBlockType(editor, v) }, key);
564
+ case "fontFamily":
565
+ return /* @__PURE__ */ jsx7(ToolbarSelect_default, { label: "Font", width: 130, value: state.fontFamily || "", options: familyOptions, disabled, onChange: (v) => v ? editor.chain().focus().setFontFamily(v).run() : editor.chain().focus().unsetFontFamily().run() }, key);
566
+ case "fontSize":
567
+ return /* @__PURE__ */ jsx7(ToolbarSelect_default, { label: "Font size", width: 64, value: state.fontSize || "", options: sizeOptions, disabled, onChange: (v) => v ? editor.chain().focus().setFontSize(v).run() : editor.chain().focus().unsetFontSize().run() }, key);
568
+ case "textColor":
569
+ return /* @__PURE__ */ jsx7(ColorButton_default, { icon: Baseline, label: "Font colour", clearLabel: "Automatic", color: state.color, disabled, onPick: (c) => editor.chain().focus().setColor(c).run(), onClear: () => editor.chain().focus().unsetColor().run() }, key);
570
+ case "highlight":
571
+ return /* @__PURE__ */ jsx7(ColorButton_default, { icon: Highlighter, label: "Highlight", clearLabel: "No colour", color: state.backgroundColor, disabled, onPick: (c) => editor.chain().focus().setBackgroundColor(c).run(), onClear: () => editor.chain().focus().unsetBackgroundColor().run() }, key);
572
+ case "indent":
573
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon: IndentIncrease, label: "Increase indent (Tab)", disabled, onClick: toggle(editor, "indent") }, key);
574
+ case "outdent":
575
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon: IndentDecrease, label: "Decrease indent (Shift+Tab)", disabled, onClick: toggle(editor, "outdent") }, key);
576
+ case "lineHeight":
577
+ return /* @__PURE__ */ jsx7(ToolbarSelect_default, { label: "Line spacing", width: 108, value: state.lineHeight ? String(state.lineHeight) : "", options: LINE_HEIGHTS, disabled, onChange: (v) => editor.chain().focus().setLineHeight(v || null).run() }, key);
578
+ case "paragraphSpacing":
579
+ return /* @__PURE__ */ jsx7(MenuButton_default, { icon: UnfoldVertical, label: "Paragraph spacing", items: SPACING_ITEMS, value: state.paragraphSpacing, showChecks: true, disabled, onSelect: (v) => editor.chain().focus().setParagraphSpacing(v).run() }, key);
580
+ case "textCase":
581
+ return /* @__PURE__ */ jsx7(MenuButton_default, { icon: CaseSensitive, label: "Change case", items: TEXT_CASES, disabled, onSelect: (v) => editor.chain().focus().setTextCase(v).run() }, key);
582
+ case "table":
583
+ return /* @__PURE__ */ jsx7(TablePicker_default, { editor, disabled }, key);
584
+ case "image":
585
+ return /* @__PURE__ */ jsxs6(React7.Fragment, { children: [
586
+ /* @__PURE__ */ jsx7(ToolbarButton, { icon: ImagePlus, label: "Insert picture", disabled, onClick: () => {
587
+ var _a;
588
+ return (_a = fileRef.current) == null ? void 0 : _a.click();
589
+ } }),
590
+ /* @__PURE__ */ jsx7(
591
+ "input",
592
+ {
593
+ ref: fileRef,
594
+ type: "file",
595
+ accept: "image/*",
596
+ hidden: true,
597
+ onChange: (e) => {
598
+ insertImages(editor, Array.from(e.target.files || []), imageOptions);
599
+ e.target.value = "";
600
+ }
601
+ }
602
+ )
603
+ ] }, key);
604
+ case "link":
605
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon: Link2, label: "Link", active: state.link, disabled, onClick: () => promptLink(editor) }, key);
606
+ case "horizontalRule":
607
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon: Minus, label: "Horizontal line", disabled, onClick: toggle(editor, "setHorizontalRule") }, key);
608
+ case "clearFormatting":
609
+ return /* @__PURE__ */ jsx7(ToolbarButton, { icon: RemoveFormatting, label: "Clear formatting", disabled, onClick: () => editor.chain().focus().unsetAllMarks().unsetParagraphFormat().unsetTextAlign().run() }, key);
610
+ default:
611
+ return null;
612
+ }
613
+ };
614
+ return /* @__PURE__ */ jsx7("div", { className: "fe-toolbar", role: "toolbar", "aria-label": "Formatting", children: items.map(render) });
615
+ };
616
+ var Toolbar_default = Toolbar;
617
+
618
+ // src/toolbar/TableTools.jsx
619
+ import React8 from "react";
620
+ import {
621
+ BetweenHorizontalEnd,
622
+ BetweenHorizontalStart,
623
+ BetweenVerticalEnd,
624
+ BetweenVerticalStart,
625
+ PaintBucket,
626
+ PanelTop,
627
+ SquareSplitHorizontal,
628
+ TableCellsMerge,
629
+ Trash2,
630
+ Rows3,
631
+ Columns3,
632
+ AlignVerticalJustifyStart,
633
+ AlignVerticalJustifyCenter,
634
+ AlignVerticalJustifyEnd
635
+ } from "lucide-react";
636
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
637
+ var AllBordersIcon = () => /* @__PURE__ */ jsx8(BorderIcon, { lines: ["top", "bottom", "left", "right", "insideH", "insideV"] });
638
+ var unavailable = (mode, borders) => {
639
+ var _a;
640
+ if (mode === "inside") return !(borders == null ? void 0 : borders.hasInside);
641
+ if (mode === "insideH" || mode === "insideV") return !((_a = borders == null ? void 0 : borders.exists) == null ? void 0 : _a[mode]);
642
+ return false;
643
+ };
644
+ var borderItems = (borders) => TABLE_BORDER_OPTIONS.map(
645
+ (opt) => opt === "|" ? opt : {
646
+ value: opt.value,
647
+ label: opt.label,
648
+ icon: /* @__PURE__ */ jsx8(BorderIcon, { lines: opt.lines }),
649
+ checked: !!(borders == null ? void 0 : borders[opt.value]),
650
+ disabled: unavailable(opt.value, borders)
651
+ }
652
+ );
653
+ var VALIGN = [
654
+ { value: "top", icon: AlignVerticalJustifyStart, label: "Align cell top" },
655
+ { value: "middle", icon: AlignVerticalJustifyCenter, label: "Align cell middle" },
656
+ { value: "bottom", icon: AlignVerticalJustifyEnd, label: "Align cell bottom" }
657
+ ];
658
+ var TableTools = ({ editor, state, disabled }) => {
659
+ const run = (fn) => () => fn(editor.chain().focus()).run();
660
+ const can = editor.can();
661
+ return /* @__PURE__ */ jsxs7("div", { className: "fe-toolbar fe-toolbar--table", role: "toolbar", "aria-label": "Table tools", children: [
662
+ /* @__PURE__ */ jsx8("span", { className: "fe-toolbar__caption", children: "Table" }),
663
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: BetweenHorizontalStart, label: "Insert row above", disabled, onClick: run((c) => c.addRowBefore()) }),
664
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: BetweenHorizontalEnd, label: "Insert row below", disabled, onClick: run((c) => c.addRowAfter()) }),
665
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: BetweenVerticalStart, label: "Insert column left", disabled, onClick: run((c) => c.addColumnBefore()) }),
666
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: BetweenVerticalEnd, label: "Insert column right", disabled, onClick: run((c) => c.addColumnAfter()) }),
667
+ /* @__PURE__ */ jsx8(Divider, {}),
668
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: Rows3, label: "Delete row", disabled, onClick: run((c) => c.deleteRow()) }),
669
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: Columns3, label: "Delete column", disabled, onClick: run((c) => c.deleteColumn()) }),
670
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: Trash2, label: "Delete table", disabled, onClick: run((c) => c.deleteTable()) }),
671
+ /* @__PURE__ */ jsx8(Divider, {}),
672
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: TableCellsMerge, label: "Merge cells", disabled: disabled || !can.mergeCells(), onClick: run((c) => c.mergeCells()) }),
673
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: SquareSplitHorizontal, label: "Split cell", disabled: disabled || !can.splitCell(), onClick: run((c) => c.splitCell()) }),
674
+ /* @__PURE__ */ jsx8(ToolbarButton, { icon: PanelTop, label: "Toggle header row", disabled, onClick: run((c) => c.toggleHeaderRow()) }),
675
+ /* @__PURE__ */ jsx8(Divider, {}),
676
+ /* @__PURE__ */ jsx8(
677
+ ColorButton_default,
678
+ {
679
+ icon: PaintBucket,
680
+ label: "Cell shading",
681
+ clearLabel: "No fill",
682
+ color: state.cellBackground,
683
+ disabled,
684
+ onPick: (color) => editor.chain().focus().setCellAttribute("backgroundColor", color).run(),
685
+ onClear: () => editor.chain().focus().setCellAttribute("backgroundColor", null).run()
686
+ }
687
+ ),
688
+ VALIGN.map(({ value, icon, label }) => /* @__PURE__ */ jsx8(
689
+ ToolbarButton,
690
+ {
691
+ icon,
692
+ label,
693
+ active: state.cellVerticalAlign === value,
694
+ disabled,
695
+ onClick: run((c) => c.setCellAttribute("verticalAlign", value))
696
+ },
697
+ value
698
+ )),
699
+ /* @__PURE__ */ jsx8(
700
+ MenuButton_default,
701
+ {
702
+ icon: AllBordersIcon,
703
+ label: "Borders",
704
+ items: borderItems(state.tableBorders),
705
+ showChecks: true,
706
+ disabled,
707
+ onSelect: (mode) => editor.chain().focus().setTableBorders(mode).run()
708
+ }
709
+ )
710
+ ] });
711
+ };
712
+ var TableTools_default = TableTools;
713
+
714
+ // src/toolbar/toolbarState.js
715
+ var blockTypeOf = (editor) => {
716
+ for (let level = 1; level <= 4; level += 1) {
717
+ if (editor.isActive("heading", { level })) return `h${level}`;
718
+ }
719
+ return "paragraph";
720
+ };
721
+ var currentBlockAttrs = (editor) => {
722
+ const { $from } = editor.state.selection;
723
+ for (let d = $from.depth; d >= 0; d -= 1) {
724
+ const node = $from.node(d);
725
+ if (node.type.name === "paragraph" || node.type.name === "heading") return node.attrs;
726
+ }
727
+ return {};
728
+ };
729
+ var currentCellAttrs = (editor) => {
730
+ const { $from } = editor.state.selection;
731
+ for (let d = $from.depth; d >= 0; d -= 1) {
732
+ const node = $from.node(d);
733
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") return node.attrs;
734
+ }
735
+ return null;
736
+ };
737
+ var selectToolbarState = ({ editor }) => {
738
+ if (!editor) return {};
739
+ const textStyle = editor.getAttributes("textStyle");
740
+ const block = currentBlockAttrs(editor);
741
+ const cell = currentCellAttrs(editor);
742
+ return {
743
+ bold: editor.isActive("bold"),
744
+ italic: editor.isActive("italic"),
745
+ underline: editor.isActive("underline"),
746
+ strike: editor.isActive("strike"),
747
+ subscript: editor.isActive("subscript"),
748
+ superscript: editor.isActive("superscript"),
749
+ bulletList: editor.isActive("bulletList"),
750
+ orderedList: editor.isActive("orderedList"),
751
+ link: editor.isActive("link"),
752
+ blockType: blockTypeOf(editor),
753
+ textAlign: block.textAlign || "left",
754
+ lineHeight: block.lineHeight || null,
755
+ paragraphSpacing: matchSpacingPreset(block),
756
+ fontFamily: textStyle.fontFamily || "",
757
+ fontSize: textStyle.fontSize || "",
758
+ color: textStyle.color || null,
759
+ backgroundColor: textStyle.backgroundColor || null,
760
+ canUndo: editor.can().undo(),
761
+ canRedo: editor.can().redo(),
762
+ inTable: !!cell,
763
+ cellBackground: (cell == null ? void 0 : cell.backgroundColor) || null,
764
+ cellVerticalAlign: (cell == null ? void 0 : cell.verticalAlign) || null,
765
+ tableBorders: cell ? getTableBorderState(editor.state) : null
766
+ };
767
+ };
768
+
769
+ // src/RichTextEditor.jsx
770
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
771
+ var countWords = (text) => text.trim() ? text.trim().split(/\s+/).length : 0;
772
+ var RichTextEditor = ({
773
+ value,
774
+ onChange,
775
+ setValue,
776
+ fnProps = [],
777
+ outputFormat = "html",
778
+ pdfOptions,
779
+ height,
780
+ minHeight = 200,
781
+ disabled = false,
782
+ readOnly = false,
783
+ placeholder,
784
+ toolbar = true,
785
+ fontFamilies = DEFAULT_FONT_FAMILIES,
786
+ fontSizes = DEFAULT_FONT_SIZES,
787
+ dynamicData,
788
+ nodeIds = false,
789
+ onInit,
790
+ onReady,
791
+ onFocus,
792
+ onBlur,
793
+ handleFocusRestore,
794
+ onImageUpload,
795
+ maxImageSize,
796
+ onError,
797
+ autoFocus = false,
798
+ pageView = false,
799
+ showWordCount = true,
800
+ className = "",
801
+ style,
802
+ contentClassName = ""
803
+ }) => {
804
+ const latest = useRef4({});
805
+ latest.current = { onChange, setValue, fnProps, outputFormat, pdfOptions, dynamicData, onFocus, onBlur, handleFocusRestore, onError };
806
+ const lastEmitted = useRef4(value);
807
+ const locked = disabled || readOnly;
808
+ const emit = (editor2) => {
809
+ const l = latest.current;
810
+ const out = getOutput(editor2, l.outputFormat, l.pdfOptions);
811
+ lastEmitted.current = out;
812
+ if (l.onChange) l.onChange(out, ...l.fnProps || []);
813
+ else if (l.setValue) l.setValue(out, ...l.fnProps || []);
814
+ };
815
+ const extensions = useMemo(
816
+ () => buildEditorExtensions({
817
+ nodeIds,
818
+ placeholder,
819
+ onImageUpload: (file) => {
820
+ const upload = latest.current.onImageUploadProp;
821
+ return upload ? upload(file) : readAsDataUrl(file);
822
+ },
823
+ maxImageSize,
824
+ onError: (err) => {
825
+ var _a, _b;
826
+ return (_b = (_a = latest.current).onError) == null ? void 0 : _b.call(_a, err);
827
+ }
828
+ }),
829
+ // Extensions are fixed for the editor's lifetime.
830
+ // eslint-disable-next-line react-hooks/exhaustive-deps
831
+ [nodeIds, placeholder]
832
+ );
833
+ latest.current.onImageUploadProp = onImageUpload;
834
+ const editor = useEditor(
835
+ {
836
+ extensions,
837
+ content: toEditorContent(value),
838
+ editable: !locked,
839
+ autofocus: autoFocus,
840
+ immediatelyRender: true,
841
+ shouldRerenderOnTransaction: false,
842
+ editorProps: {
843
+ transformPastedHTML: cleanHtml,
844
+ attributes: { class: `fe-content ${contentClassName}`.trim(), spellcheck: "true" }
845
+ },
846
+ onUpdate: ({ editor: ed, transaction }) => {
847
+ if (transaction.getMeta(SILENT_META)) return;
848
+ emit(ed);
849
+ },
850
+ onFocus: ({ event }) => {
851
+ var _a, _b, _c, _d;
852
+ (_b = (_a = latest.current).onFocus) == null ? void 0 : _b.call(_a, event);
853
+ (_d = (_c = latest.current).handleFocusRestore) == null ? void 0 : _d.call(_c, event);
854
+ },
855
+ onBlur: ({ editor: ed, event }) => {
856
+ var _a, _b;
857
+ if (latest.current.dynamicData) replaceVariables(ed, latest.current.dynamicData);
858
+ (_b = (_a = latest.current).onBlur) == null ? void 0 : _b.call(_a, event);
859
+ }
860
+ },
861
+ [extensions]
862
+ );
863
+ const handle = useMemo(() => editor ? createEditorHandle(editor, { pdfOptions }) : null, [editor, pdfOptions]);
864
+ useEffect3(() => {
865
+ if (!editor || !handle) return;
866
+ if (nodeIds) ensureNodeIds(editor);
867
+ onInit == null ? void 0 : onInit({ type: "init", target: handle }, handle);
868
+ onReady == null ? void 0 : onReady(handle);
869
+ }, [editor]);
870
+ useEffect3(() => {
871
+ if (!editor || editor.isDestroyed) return;
872
+ if (sameValue(value, lastEmitted.current)) return;
873
+ if (typeof value === "string" && outputFormat === "html" && value === getOutput(editor, "html")) {
874
+ lastEmitted.current = value;
875
+ return;
876
+ }
877
+ lastEmitted.current = value;
878
+ editor.commands.setContent(toEditorContent(value), { emitUpdate: false });
879
+ if (nodeIds) ensureNodeIds(editor);
880
+ }, [editor, value, outputFormat, nodeIds]);
881
+ useEffect3(() => {
882
+ if (editor && !editor.isDestroyed) editor.setEditable(!locked, false);
883
+ }, [editor, locked]);
884
+ const state = useEditorState({ editor, selector: selectToolbarState }) || {};
885
+ const words = useEditorState({
886
+ editor,
887
+ selector: ({ editor: ed }) => showWordCount && ed ? countWords(ed.state.doc.textBetween(0, ed.state.doc.content.size, " ", " ")) : 0
888
+ });
889
+ const toolbarItems = Array.isArray(toolbar) ? toolbar : DEFAULT_TOOLBAR;
890
+ const sized = height !== void 0 && height !== null;
891
+ return /* @__PURE__ */ jsxs8(
892
+ "div",
893
+ {
894
+ className: `fe-editor${pageView ? " fe-editor--page" : ""}${locked ? " fe-editor--disabled" : ""}${sized ? " fe-editor--sized" : ""} ${className}`.trim(),
895
+ style: { ...sized ? { height } : {}, ...style },
896
+ children: [
897
+ toolbar && !readOnly ? /* @__PURE__ */ jsxs8("div", { className: "fe-toolbars", children: [
898
+ /* @__PURE__ */ jsx9(
899
+ Toolbar_default,
900
+ {
901
+ editor,
902
+ state,
903
+ items: toolbarItems,
904
+ disabled: locked,
905
+ fontFamilies,
906
+ fontSizes,
907
+ imageOptions: { onImageUpload, maxImageSize, onError }
908
+ }
909
+ ),
910
+ state.inTable && !locked ? /* @__PURE__ */ jsx9(TableTools_default, { editor, state, disabled: locked }) : null
911
+ ] }) : null,
912
+ /* @__PURE__ */ jsx9("div", { className: "fe-scroll", style: sized ? void 0 : { minHeight }, onClick: () => !locked && (editor == null ? void 0 : editor.isFocused) === false && (editor == null ? void 0 : editor.commands.focus()), children: /* @__PURE__ */ jsx9("div", { className: "fe-page", children: /* @__PURE__ */ jsx9(EditorContent, { editor }) }) }),
913
+ showWordCount ? /* @__PURE__ */ jsxs8("div", { className: "fe-status", children: [
914
+ words,
915
+ " ",
916
+ words === 1 ? "word" : "words"
917
+ ] }) : null
918
+ ]
919
+ }
920
+ );
921
+ };
922
+ var RichTextEditor_default = RichTextEditor;
923
+ export {
924
+ DEFAULT_FONT_FAMILIES,
925
+ DEFAULT_FONT_SIZES,
926
+ DEFAULT_PDF_OPTIONS,
927
+ DEFAULT_TOOLBAR,
928
+ EMPTY_DOC,
929
+ INDENT_STEP_PX,
930
+ NODE_ID_ATTRIBUTE,
931
+ NODE_ID_TYPES,
932
+ NodeIds,
933
+ OUTPUT_FORMATS,
934
+ PARAGRAPH_SPACING,
935
+ PDF_TREE_VERSION,
936
+ ParagraphFormat,
937
+ RichTableCell,
938
+ RichTableHeader,
939
+ RichTextEditor_default as RichTextEditor,
940
+ STANDARD_FONTS,
941
+ TEXT_CASES,
942
+ buildEditorExtensions,
943
+ buildSchemaExtensions,
944
+ cleanHtml as cleanPastedHtml,
945
+ createEditorHandle,
946
+ RichTextEditor_default as default,
947
+ ensureNodeIds,
948
+ getEditorSchema,
949
+ getOutput,
950
+ htmlToJSON,
951
+ htmlToText,
952
+ isDocJson,
953
+ isPdfTree,
954
+ isWordHtml,
955
+ jsonToHTML,
956
+ jsonToPdfTree,
957
+ jsonToText,
958
+ normalizeHtmlInput,
959
+ replaceVariables,
960
+ replaceVariablesInString,
961
+ textToHtml,
962
+ toDocJson
963
+ };
964
+ //# sourceMappingURL=index.js.map