smartrte-react 0.3.4 → 0.3.5
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/CHANGELOG.md +9 -0
- package/README.md +14 -83
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +15 -0
- package/dist/adapters/canonicalInlineCommandBridge.js +269 -0
- package/dist/adapters/canonicalListCommandBridge.d.ts +26 -0
- package/dist/adapters/canonicalListCommandBridge.js +430 -0
- package/dist/adapters/documentFormats.d.ts +26 -0
- package/dist/adapters/documentFormats.js +49 -0
- package/dist/adapters/docxFormat.d.ts +5 -0
- package/dist/adapters/docxFormat.js +272 -0
- package/dist/adapters/domBlockCommandBridge.d.ts +19 -0
- package/dist/adapters/domBlockCommandBridge.js +79 -0
- package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
- package/dist/adapters/domChecklistCommandBridge.js +33 -0
- package/dist/adapters/domCommandBridge.d.ts +19 -0
- package/dist/adapters/domCommandBridge.js +33 -0
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineAtomCommandBridge.js +101 -0
- package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineImageCommandBridge.js +72 -0
- package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
- package/dist/adapters/domMoveCommandBridge.js +54 -0
- package/dist/adapters/domSelectionBridge.d.ts +3 -1
- package/dist/adapters/domSelectionBridge.js +96 -44
- package/dist/adapters/domSmartDocument.js +158 -16
- package/dist/adapters/domTableCommandBridge.d.ts +35 -0
- package/dist/adapters/domTableCommandBridge.js +201 -0
- package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
- package/dist/adapters/legacyListShadowComparator.js +316 -0
- package/dist/adapters/pdfFormat.d.ts +23 -0
- package/dist/adapters/pdfFormat.js +204 -0
- package/dist/adapters/portableDocxAtoms.d.ts +4 -0
- package/dist/adapters/portableDocxAtoms.js +58 -0
- package/dist/adapters/styledDocxFormat.d.ts +8 -0
- package/dist/adapters/styledDocxFormat.js +190 -0
- package/dist/builtInFormatDefinitions.d.ts +5 -0
- package/dist/builtInFormatDefinitions.js +82 -0
- package/dist/canonicalAuthorityFlag.d.ts +32 -0
- package/dist/canonicalAuthorityFlag.js +49 -0
- package/dist/canonicalClipboardRuntime.d.ts +15 -0
- package/dist/canonicalClipboardRuntime.js +76 -0
- package/dist/canonicalEditorRuntime.d.ts +77 -0
- package/dist/canonicalEditorRuntime.js +274 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +25 -0
- package/dist/components/CanonicalAuthorityEditor.js +518 -0
- package/dist/components/ClassicEditor.d.ts +13 -3
- package/dist/components/ClassicEditor.js +1226 -1114
- package/dist/components/ClassicEditorAuthority.d.ts +38 -0
- package/dist/components/ClassicEditorAuthority.js +49 -0
- package/dist/components/MediaPicker.d.ts +10 -0
- package/dist/components/MediaPicker.js +16 -0
- package/dist/editorController.d.ts +78 -0
- package/dist/editorController.js +298 -0
- package/dist/formatFidelity.d.ts +14 -0
- package/dist/formatFidelity.js +107 -0
- package/dist/formatRuntime.d.ts +50 -0
- package/dist/formatRuntime.js +20 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +10 -0
- package/dist/mediaProvider.d.ts +39 -0
- package/dist/mediaProvider.js +1 -0
- package/dist/pluginRuntime.d.ts +68 -0
- package/dist/pluginRuntime.js +78 -0
- package/dist/standalone/editor.js +722 -495
- package/dist/test-harness/atomShadowComparator.d.ts +14 -0
- package/dist/test-harness/atomShadowComparator.js +63 -0
- package/dist/test-harness/blockShadowComparator.d.ts +30 -0
- package/dist/test-harness/blockShadowComparator.js +140 -0
- package/dist/test-harness/clipboardShadowComparator.d.ts +12 -0
- package/dist/test-harness/clipboardShadowComparator.js +46 -0
- package/dist/test-harness/inlineShadowComparator.d.ts +32 -0
- package/dist/test-harness/inlineShadowComparator.js +152 -0
- package/dist/test-harness/legacyAtomEngine.d.ts +10 -0
- package/dist/test-harness/legacyAtomEngine.js +15 -0
- package/dist/test-harness/legacyBlockEngine.d.ts +31 -0
- package/dist/test-harness/legacyBlockEngine.js +31 -0
- package/dist/test-harness/legacyClipboardEngine.d.ts +14 -0
- package/dist/test-harness/legacyClipboardEngine.js +67 -0
- package/dist/test-harness/legacyInlineEngine.d.ts +19 -0
- package/dist/test-harness/legacyInlineEngine.js +49 -0
- package/dist/test-harness/legacyTableEngine.d.ts +12 -0
- package/dist/test-harness/legacyTableEngine.js +25 -0
- package/dist/test-harness/tableShadowComparator.d.ts +29 -0
- package/dist/test-harness/tableShadowComparator.js +101 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +28 -0
- package/package.json +1 -1
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
+
import { atomDeclarations, continueListNumbering, createList, createNodeId, deleteAtom, executeMarkTool, indentBlockCommand, indentList, inlineToolDeclarations, isTextNode, insertAtom, insertTableColumnCommand, insertTableCommand, insertTableRowCommand, mergeTableCellsCommand, moveListItems, moveTableColumnCommand, moveTableRowCommand, parseCanonicalListHtml, parseCanonicalListMarkdown, removeTableColumnCommand, removeTableCommand, removeTableRowCommand, resizeAtom, runAtomUpload, restartListNumbering, serializeCanonicalListHtml, serializeCanonicalListMarkdown, setListChecked, setListPreset, setListStyle, setTableHeaderCommand, setBlockAttributes, setBlockTypeCommand, moveBlockCommand, outdentBlockCommand, splitTableCellCommand, unwrapBlocks, wrapBlocks, unwrapList, updateAtom, outdentList, } from "smartrte-core/foundation";
|
|
4
|
+
import { SMART_LIST_PRESETS } from "smartrte-core";
|
|
5
|
+
import { ensureStyleSheet } from "../theme.js";
|
|
6
|
+
import { DefaultMediaPicker } from "./MediaPicker.js";
|
|
7
|
+
import { serializeSmartDocument, smartDocumentFromHtml } from "../adapters/domSmartDocument.js";
|
|
8
|
+
import { exportDocxDocument } from "../adapters/docxFormat.js";
|
|
9
|
+
import { importStyledDocxDocument } from "../adapters/styledDocxFormat.js";
|
|
10
|
+
import { importPdfDocument, printSmartDocumentAsPdf } from "../adapters/pdfFormat.js";
|
|
11
|
+
import { CanonicalEditorRuntime, } from "../canonicalEditorRuntime.js";
|
|
12
|
+
const labels = {
|
|
13
|
+
bold: "Bold", italic: "Italic", underline: "Underline", strike: "Strikethrough", strikethrough: "Strikethrough",
|
|
14
|
+
inlineCode: "Inline code", superscript: "Superscript", subscript: "Subscript",
|
|
15
|
+
textColor: "Text colour", backgroundColor: "Background colour",
|
|
16
|
+
fontSize: "Font size", fontFamily: "Font family",
|
|
17
|
+
};
|
|
18
|
+
const listSelectionParts = (scope) => {
|
|
19
|
+
if (scope.kind === "list-selection")
|
|
20
|
+
return [scope];
|
|
21
|
+
if (scope.kind !== "mixed")
|
|
22
|
+
return [];
|
|
23
|
+
return scope.parts.flatMap(listSelectionParts);
|
|
24
|
+
};
|
|
25
|
+
const findNode = (root, id) => {
|
|
26
|
+
if (isTextNode(root))
|
|
27
|
+
return null;
|
|
28
|
+
if (root.id === id)
|
|
29
|
+
return root;
|
|
30
|
+
for (const child of root.children || []) {
|
|
31
|
+
const found = findNode(child, id);
|
|
32
|
+
if (found)
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
const downloadText = (ownerDocument, name, type, value) => {
|
|
38
|
+
const url = URL.createObjectURL(new Blob([value], { type }));
|
|
39
|
+
const anchor = ownerDocument.createElement("a");
|
|
40
|
+
anchor.href = url;
|
|
41
|
+
anchor.download = name;
|
|
42
|
+
anchor.click();
|
|
43
|
+
URL.revokeObjectURL(url);
|
|
44
|
+
};
|
|
45
|
+
const downloadBlob = (ownerDocument, name, blob) => {
|
|
46
|
+
const url = URL.createObjectURL(blob);
|
|
47
|
+
const anchor = ownerDocument.createElement("a");
|
|
48
|
+
anchor.href = url;
|
|
49
|
+
anchor.download = name;
|
|
50
|
+
anchor.click();
|
|
51
|
+
URL.revokeObjectURL(url);
|
|
52
|
+
};
|
|
53
|
+
export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEditor({ defaultValue, onChange, onHtmlChange, onClipboardDiagnostic, mediaProvider, mediaPicker: MediaPicker = DefaultMediaPicker, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, className, onRuntime, }, forwardedRef) {
|
|
54
|
+
ensureStyleSheet();
|
|
55
|
+
const rootRef = useRef(null);
|
|
56
|
+
const importRef = useRef(null);
|
|
57
|
+
const runtimeRef = useRef();
|
|
58
|
+
const pendingMediaUploads = useRef(new Set());
|
|
59
|
+
const [mediaKind, setMediaKind] = useState(null);
|
|
60
|
+
if (!runtimeRef.current)
|
|
61
|
+
runtimeRef.current = new CanonicalEditorRuntime({ initialValue: defaultValue, onChange, onHtmlChange, onClipboardDiagnostic });
|
|
62
|
+
const runtime = runtimeRef.current;
|
|
63
|
+
const [, setEditorTick] = useState(0);
|
|
64
|
+
runtime.setCallbacks(onChange, onHtmlChange);
|
|
65
|
+
useImperativeHandle(forwardedRef, () => runtime, [runtime]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const root = rootRef.current;
|
|
68
|
+
if (!root)
|
|
69
|
+
return;
|
|
70
|
+
runtime.mount(root);
|
|
71
|
+
onRuntime?.(runtime);
|
|
72
|
+
return () => {
|
|
73
|
+
pendingMediaUploads.current.forEach((controller) => controller.abort());
|
|
74
|
+
pendingMediaUploads.current.clear();
|
|
75
|
+
runtime.unmount();
|
|
76
|
+
};
|
|
77
|
+
}, [runtime, onRuntime]);
|
|
78
|
+
useEffect(() => runtime.editor.subscribe(() => setEditorTick((value) => value + 1)), [runtime]);
|
|
79
|
+
const transactBlock = (operations) => {
|
|
80
|
+
runtime.executeOperations(operations);
|
|
81
|
+
};
|
|
82
|
+
const blockScope = () => runtime.editor.resolveScope({ want: "block-range" });
|
|
83
|
+
const blockContext = () => ({ schema: runtime.editor.schema, positions: runtime.editor.positions });
|
|
84
|
+
const listScope = () => runtime.editor.resolveScope({ want: "list-selection" });
|
|
85
|
+
const tableScope = () => runtime.editor.resolveScope({ want: "table-grid" });
|
|
86
|
+
const atomScope = () => runtime.editor.resolveScope({ want: "atomic-node" });
|
|
87
|
+
const ids = (count) => Array.from({ length: count }, () => createNodeId());
|
|
88
|
+
// Style/preset changes are a decision about the whole list the user is
|
|
89
|
+
// working in, not just the nested list segment nearest the cursor —
|
|
90
|
+
// unlike indent/outdent/move, which deliberately stay scoped to the
|
|
91
|
+
// nearest list and must not use this.
|
|
92
|
+
const outermostListId = (listId) => {
|
|
93
|
+
let current = listId;
|
|
94
|
+
for (;;) {
|
|
95
|
+
const resolved = runtime.editor.positions.positionOf(current);
|
|
96
|
+
if (!resolved || resolved.parent.type !== "list_item")
|
|
97
|
+
return current;
|
|
98
|
+
const itemResolved = runtime.editor.positions.positionOf(resolved.parent.id);
|
|
99
|
+
if (!itemResolved || itemResolved.parent.type !== "list")
|
|
100
|
+
return current;
|
|
101
|
+
current = itemResolved.parent.id;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const currentListScope = listScope();
|
|
105
|
+
const currentListParts = listSelectionParts(currentListScope);
|
|
106
|
+
const currentListStates = currentListParts.map((part) => {
|
|
107
|
+
const list = findNode(runtime.editor.document, part.listId);
|
|
108
|
+
const selectedIds = new Set(part.items.map((item) => item.itemId));
|
|
109
|
+
const indexes = list?.children?.flatMap((child, index) => !isTextNode(child) && selectedIds.has(child.id) ? [index] : []) || [];
|
|
110
|
+
return { part, list, indexes };
|
|
111
|
+
});
|
|
112
|
+
// A mixed scope is intentionally supported by list commands: list parts are
|
|
113
|
+
// transformed and plain-block parts are ignored. Keep the toolbar state in
|
|
114
|
+
// sync with that same policy instead of treating mixed as universally inert.
|
|
115
|
+
const currentList = currentListStates.length === 1 ? currentListStates[0].list : null;
|
|
116
|
+
const rootList = currentListScope.kind === "list-selection"
|
|
117
|
+
? findNode(runtime.editor.document, outermostListId(currentListScope.listId))
|
|
118
|
+
: null;
|
|
119
|
+
const currentTableScope = tableScope();
|
|
120
|
+
const currentAtomScope = atomScope();
|
|
121
|
+
const currentListItems = currentListStates.length === 1 ? currentListStates[0].part.items : [];
|
|
122
|
+
const orderedList = Boolean(currentList && /^(?:decimal|lower-|upper-|ordered)/.test(String(currentList.attrs?.style || currentList.attrs?.preset || "")));
|
|
123
|
+
const canIndent = currentListStates.some(({ indexes }) => indexes.length > 0 && Math.min(...indexes) > 0);
|
|
124
|
+
const canMoveUp = currentListStates.some(({ indexes }) => indexes.length > 0 && Math.min(...indexes) > 0);
|
|
125
|
+
const canMoveDown = currentListStates.some(({ indexes, list }) => indexes.length > 0
|
|
126
|
+
&& Math.max(...indexes) < (list?.children?.length || 0) - 1);
|
|
127
|
+
const tableSelected = currentTableScope.kind === "table-grid";
|
|
128
|
+
const atomSelected = currentAtomScope.kind === "atomic-node";
|
|
129
|
+
const blockTypeAt = (position) => {
|
|
130
|
+
let node = runtime.editor.document;
|
|
131
|
+
for (let depth = 0; depth <= position.path.length; depth += 1) {
|
|
132
|
+
if (!isTextNode(node) && node.type !== "doc" && runtime.editor.schema.nodes[node.type]?.group === "block") {
|
|
133
|
+
if (node.type === "code_block")
|
|
134
|
+
return "code_block";
|
|
135
|
+
if (node.type === "heading")
|
|
136
|
+
return `heading-${Number(node.attrs?.level || 1)}`;
|
|
137
|
+
return "paragraph";
|
|
138
|
+
}
|
|
139
|
+
if (depth === position.path.length || isTextNode(node))
|
|
140
|
+
break;
|
|
141
|
+
const child = node.children?.[position.path[depth]];
|
|
142
|
+
if (!child)
|
|
143
|
+
break;
|
|
144
|
+
node = child;
|
|
145
|
+
}
|
|
146
|
+
return "paragraph";
|
|
147
|
+
};
|
|
148
|
+
const currentBlockType = blockTypeAt(runtime.editor.selection.head);
|
|
149
|
+
const currentListPreset = currentListParts.length === 1 && typeof rootList?.attrs?.preset === "string"
|
|
150
|
+
? rootList.attrs.preset
|
|
151
|
+
: "";
|
|
152
|
+
const toggleCheckedItems = () => {
|
|
153
|
+
if (currentListScope.kind !== "list-selection" || currentList?.attrs?.checkable !== true)
|
|
154
|
+
return;
|
|
155
|
+
const selectedIds = new Set(currentListScope.items.map((item) => item.itemId));
|
|
156
|
+
const selectedNodes = (currentList.children || []).filter((node) => !isTextNode(node) && selectedIds.has(node.id));
|
|
157
|
+
const checked = !selectedNodes.length || !selectedNodes.every((node) => node.attrs?.checked === true);
|
|
158
|
+
runtime.executeOperations(setListChecked(runtime.editor.document, currentListScope, { checked }, blockContext()), { preserveSelectionById: true });
|
|
159
|
+
};
|
|
160
|
+
const restartNumbering = () => {
|
|
161
|
+
if (currentListScope.kind !== "list-selection" || !orderedList)
|
|
162
|
+
return;
|
|
163
|
+
const value = window.prompt("Restart numbering at", String(currentList?.attrs?.start || 1));
|
|
164
|
+
const start = Number(value);
|
|
165
|
+
if (!Number.isInteger(start) || start < 1)
|
|
166
|
+
return;
|
|
167
|
+
runtime.executeOperations(restartListNumbering(runtime.editor.document, currentListScope, { start }, blockContext()), { preserveSelectionById: true });
|
|
168
|
+
};
|
|
169
|
+
// Shared by the toggle buttons' click handler and their aria-pressed state,
|
|
170
|
+
// so "this button looks active" and "clicking it again removes the list"
|
|
171
|
+
// can never drift apart. Checked against the outermost list (see
|
|
172
|
+
// outermostListId) so a deeply nested cursor still reports the true
|
|
173
|
+
// whole-list state, matching what applying a new type would change.
|
|
174
|
+
const listStyleActive = (style, checkable = false) => rootList?.attrs?.style === style && Boolean(rootList.attrs?.checkable) === checkable;
|
|
175
|
+
const toggleList = (style, checkable = false) => {
|
|
176
|
+
const selectedList = listScope();
|
|
177
|
+
const context = blockContext();
|
|
178
|
+
if (selectedList.kind === "list-selection") {
|
|
179
|
+
const rootId = outermostListId(selectedList.listId);
|
|
180
|
+
const list = findNode(runtime.editor.document, rootId);
|
|
181
|
+
const sameStyle = list?.attrs?.style === style && Boolean(list.attrs?.checkable) === checkable;
|
|
182
|
+
const operations = sameStyle
|
|
183
|
+
// Toggling an already-active style off is deliberately scoped to just
|
|
184
|
+
// the current item (selectedList), not the whole list.
|
|
185
|
+
? unwrapList(runtime.editor.document, selectedList, { splitListIds: ids(4) }, context)
|
|
186
|
+
// Applying a genuinely different style/type is a whole-list decision
|
|
187
|
+
// regardless of how deep the cursor is nested.
|
|
188
|
+
: setListStyle(runtime.editor.document, { ...selectedList, listId: rootId }, { style, checkable }, context);
|
|
189
|
+
runtime.executeOperations(operations, { preserveSelectionById: true });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const selectedBlocks = blockScope();
|
|
193
|
+
const count = selectedBlocks.kind === "block-range" ? selectedBlocks.blockIds.length : 1;
|
|
194
|
+
// Nesting reconstruction (see createList) can need up to one extra list
|
|
195
|
+
// per block, on top of one list per originally-flat contiguous group —
|
|
196
|
+
// over-provision generously rather than compute the exact worst case.
|
|
197
|
+
runtime.executeOperations(createList(runtime.editor.document, selectedBlocks, {
|
|
198
|
+
listIds: ids(Math.max(1, count * 2)), itemIds: ids(Math.max(1, count)), style, checkable,
|
|
199
|
+
}, context), { preserveSelectionById: true });
|
|
200
|
+
};
|
|
201
|
+
const runList = (action) => {
|
|
202
|
+
const scope = listScope();
|
|
203
|
+
const context = blockContext();
|
|
204
|
+
const operations = action === "indent" ? indentList(runtime.editor.document, scope, { nestedListIds: ids(8) }, context)
|
|
205
|
+
: action === "outdent" ? outdentList(runtime.editor.document, scope, { splitListIds: ids(8) }, context)
|
|
206
|
+
: moveListItems(runtime.editor.document, scope, { direction: action }, context);
|
|
207
|
+
runtime.executeOperations(operations, { preserveSelectionById: true });
|
|
208
|
+
};
|
|
209
|
+
const applyAttributedMark = (id) => {
|
|
210
|
+
const declaration = inlineToolDeclarations.find((tool) => tool.id === id);
|
|
211
|
+
if (!declaration)
|
|
212
|
+
return;
|
|
213
|
+
const attrs = id === "textColor" || id === "backgroundColor"
|
|
214
|
+
? (() => {
|
|
215
|
+
const value = window.prompt(id === "textColor" ? "Text colour" : "Background colour", "#000000");
|
|
216
|
+
return value ? { value } : undefined;
|
|
217
|
+
})()
|
|
218
|
+
: id === "fontSize"
|
|
219
|
+
? (() => {
|
|
220
|
+
const value = window.prompt("Font size (px)", "16");
|
|
221
|
+
const valuePx = value === null ? undefined : Number(value);
|
|
222
|
+
return Number.isFinite(valuePx) ? { valuePx } : undefined;
|
|
223
|
+
})()
|
|
224
|
+
: id === "fontFamily"
|
|
225
|
+
? (() => {
|
|
226
|
+
const value = window.prompt("Font family", "system-ui");
|
|
227
|
+
return value ? { value } : undefined;
|
|
228
|
+
})()
|
|
229
|
+
: undefined;
|
|
230
|
+
if ((id === "textColor" || id === "backgroundColor" || id === "fontSize" || id === "fontFamily") && !attrs)
|
|
231
|
+
return;
|
|
232
|
+
try {
|
|
233
|
+
executeMarkTool(runtime.editor, declaration, "apply", attrs);
|
|
234
|
+
runtime.focus();
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
window.alert(error instanceof Error ? error.message : "Invalid formatting value.");
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const toggleBlockquote = () => {
|
|
241
|
+
const scope = blockScope();
|
|
242
|
+
const resolved = runtime.editor.resolve({ pos: runtime.editor.selection.head });
|
|
243
|
+
const ancestor = [...resolved.ancestors].reverse().find((node) => node.type === "blockquote");
|
|
244
|
+
const context = blockContext();
|
|
245
|
+
if (ancestor) {
|
|
246
|
+
const quoteScope = {
|
|
247
|
+
kind: "block-range",
|
|
248
|
+
blockIds: [ancestor.id],
|
|
249
|
+
promotedFromPartial: false,
|
|
250
|
+
commonParentId: null,
|
|
251
|
+
range: { from: resolved.pos, to: resolved.pos },
|
|
252
|
+
isolatingAncestorId: null,
|
|
253
|
+
clamped: false,
|
|
254
|
+
};
|
|
255
|
+
runtime.executeOperations(unwrapBlocks(runtime.editor.document, quoteScope, { type: "blockquote" }, context), { preserveSelectionById: true });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (scope.kind !== "block-range" || !scope.blockIds.length)
|
|
259
|
+
return;
|
|
260
|
+
runtime.executeOperations(wrapBlocks(runtime.editor.document, scope, {
|
|
261
|
+
type: "blockquote", wrapperIds: ids(scope.blockIds.length),
|
|
262
|
+
}, context), { preserveSelectionById: true });
|
|
263
|
+
};
|
|
264
|
+
const runBlock = (action) => {
|
|
265
|
+
const scope = blockScope();
|
|
266
|
+
const context = blockContext();
|
|
267
|
+
const operations = action === "up" || action === "down"
|
|
268
|
+
? moveBlockCommand(runtime.editor.document, scope, { direction: action }, context)
|
|
269
|
+
: action === "indent"
|
|
270
|
+
? indentBlockCommand(runtime.editor.document, scope, {}, context)
|
|
271
|
+
: outdentBlockCommand(runtime.editor.document, scope, {}, context);
|
|
272
|
+
runtime.executeOperations(operations, { preserveSelectionById: true });
|
|
273
|
+
};
|
|
274
|
+
const insertTable = () => {
|
|
275
|
+
const paragraphIds = ids(4);
|
|
276
|
+
const selected = blockScope();
|
|
277
|
+
const firstId = selected.kind === "block-range" ? selected.blockIds[0] : undefined;
|
|
278
|
+
const target = firstId ? runtime.editor.positions.positionOf(firstId) : null;
|
|
279
|
+
const operations = insertTableCommand(runtime.editor.document, selected, {
|
|
280
|
+
rows: 2, columns: 2, placement: "after",
|
|
281
|
+
ids: { tableId: createNodeId(), rowIds: ids(2), cellIds: ids(4), paragraphIds },
|
|
282
|
+
}, blockContext());
|
|
283
|
+
// Keep an editable block after a table inserted at the end of its
|
|
284
|
+
// container. Without this, the browser has no legal caret position below
|
|
285
|
+
// the table and clicking/arrowing past it appears to do nothing.
|
|
286
|
+
if (target && target.parent.children && target.pos.offset === target.parent.children.length - 1) {
|
|
287
|
+
operations.push({
|
|
288
|
+
type: "insertNode",
|
|
289
|
+
pos: { path: [...target.pos.path], offset: target.pos.offset + 2 },
|
|
290
|
+
node: { type: "paragraph", id: createNodeId(), children: [] },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
runtime.executeOperations(operations, { selectionOwnerId: paragraphIds[0] });
|
|
294
|
+
};
|
|
295
|
+
const runTable = (action) => {
|
|
296
|
+
const scope = tableScope();
|
|
297
|
+
const context = blockContext();
|
|
298
|
+
const many = ids(128);
|
|
299
|
+
const operations = action === "remove" ? removeTableCommand(runtime.editor.document, scope, {}, context)
|
|
300
|
+
: action === "row+" ? insertTableRowCommand(runtime.editor.document, scope, { position: "after", rowId: createNodeId(), cellIds: many, paragraphIds: ids(128) }, context)
|
|
301
|
+
: action === "row-" ? removeTableRowCommand(runtime.editor.document, scope, {}, context)
|
|
302
|
+
: action === "column+" ? insertTableColumnCommand(runtime.editor.document, scope, { position: "after", cellIds: many, paragraphIds: ids(128) }, context)
|
|
303
|
+
: action === "column-" ? removeTableColumnCommand(runtime.editor.document, scope, {}, context)
|
|
304
|
+
: action === "merge" ? mergeTableCellsCommand(runtime.editor.document, scope, {}, context)
|
|
305
|
+
: action === "split" ? splitTableCellCommand(runtime.editor.document, scope, { cellIds: many, paragraphIds: ids(128) }, context)
|
|
306
|
+
: action === "header" ? setTableHeaderCommand(runtime.editor.document, scope, { target: "row" }, context)
|
|
307
|
+
: action === "row-up" || action === "row-down" ? moveTableRowCommand(runtime.editor.document, scope, { direction: action === "row-up" ? "up" : "down" }, context)
|
|
308
|
+
: moveTableColumnCommand(runtime.editor.document, scope, { direction: action === "column-left" ? "left" : "right" }, context);
|
|
309
|
+
runtime.executeOperations(operations, { preserveSelectionById: true });
|
|
310
|
+
};
|
|
311
|
+
const insertInlineFormula = () => {
|
|
312
|
+
const source = window.prompt("Formula (LaTeX)", "E=mc^2");
|
|
313
|
+
if (!source)
|
|
314
|
+
return;
|
|
315
|
+
const declaration = atomDeclarations.find((entry) => entry.type === "formula");
|
|
316
|
+
const resolved = runtime.editor.resolve({ pos: runtime.editor.selection.head });
|
|
317
|
+
runtime.executeOperations(insertAtom(runtime.editor.document, atomScope(), {
|
|
318
|
+
declaration, nodeId: createNodeId(), ownerId: resolved.nodeId, offset: runtime.editor.selection.head.offset,
|
|
319
|
+
attrs: { source, notation: "latex" },
|
|
320
|
+
}, blockContext()));
|
|
321
|
+
};
|
|
322
|
+
const insertBlockAtom = (type, attrs, nodeId) => {
|
|
323
|
+
const declaration = atomDeclarations.find((entry) => entry.type === type);
|
|
324
|
+
const selection = runtime.editor.selection;
|
|
325
|
+
let parentId;
|
|
326
|
+
let index;
|
|
327
|
+
const resolved = runtime.editor.resolve({ pos: selection.head });
|
|
328
|
+
if (selection.type === "cell") {
|
|
329
|
+
// A cell range resolves at the active cell's content boundary. Block
|
|
330
|
+
// atoms belong inside that cell, never as invalid siblings of the row.
|
|
331
|
+
parentId = resolved.nodeId;
|
|
332
|
+
index = resolved.pos.offset;
|
|
333
|
+
}
|
|
334
|
+
else if (resolved.kind === "structural" && resolved.pos.path.length === selection.anchor.path.length
|
|
335
|
+
&& resolved.pos.path.every((part, pathIndex) => part === selection.anchor.path[pathIndex])) {
|
|
336
|
+
// A browser may expose a clicked block atom as a structural text range
|
|
337
|
+
// (the atom occupies one unit). Keep insertion in that same parent.
|
|
338
|
+
parentId = resolved.parent.id;
|
|
339
|
+
index = Math.max(selection.anchor.offset, selection.head.offset);
|
|
340
|
+
}
|
|
341
|
+
else if (selection.type === "node") {
|
|
342
|
+
parentId = resolved.parent.id;
|
|
343
|
+
index = resolved.pos.offset;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const location = runtime.editor.positions.positionOf(resolved.nodeId);
|
|
347
|
+
parentId = location?.parent.id;
|
|
348
|
+
index = location ? location.pos.offset + 1 : undefined;
|
|
349
|
+
}
|
|
350
|
+
if (!parentId || index === undefined)
|
|
351
|
+
return false;
|
|
352
|
+
const operations = insertAtom(runtime.editor.document, atomScope(), {
|
|
353
|
+
declaration, nodeId, parentId, index, attrs,
|
|
354
|
+
}, blockContext());
|
|
355
|
+
if (!operations.length)
|
|
356
|
+
return false;
|
|
357
|
+
// Block atoms cannot contain a caret. Keep an editable paragraph after a
|
|
358
|
+
// media node inserted at the end of its container (document root or table
|
|
359
|
+
// cell), and place the caret there so the next keystroke has a legal owner.
|
|
360
|
+
const parent = findNode(runtime.editor.document, parentId);
|
|
361
|
+
const contentRange = runtime.editor.positions.contentRangeOf(parentId);
|
|
362
|
+
let selectionOwnerId;
|
|
363
|
+
if (parent?.children && index >= parent.children.length && contentRange) {
|
|
364
|
+
selectionOwnerId = createNodeId();
|
|
365
|
+
operations.push({
|
|
366
|
+
type: "insertNode",
|
|
367
|
+
pos: { path: [...contentRange.from.path], offset: index + 1 },
|
|
368
|
+
node: { type: "paragraph", id: selectionOwnerId, children: [] },
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
runtime.executeOperations(operations, selectionOwnerId ? { selectionOwnerId, selectionOffset: 0 } : {});
|
|
372
|
+
return true;
|
|
373
|
+
};
|
|
374
|
+
const insertMediaFile = async (kind, file) => {
|
|
375
|
+
setMediaKind(null);
|
|
376
|
+
if (!mediaProvider)
|
|
377
|
+
return;
|
|
378
|
+
const type = kind === "image" ? "block_image" : kind;
|
|
379
|
+
const nodeId = createNodeId();
|
|
380
|
+
const preview = URL.createObjectURL(file);
|
|
381
|
+
const attrs = kind === "image"
|
|
382
|
+
? { src: preview, alt: file.name || "Image", decorative: false, status: "pending", uploadId: nodeId }
|
|
383
|
+
: { src: preview, status: "pending", uploadId: nodeId };
|
|
384
|
+
if (!insertBlockAtom(type, attrs, nodeId)) {
|
|
385
|
+
URL.revokeObjectURL(preview);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const controller = new AbortController();
|
|
389
|
+
pendingMediaUploads.current.add(controller);
|
|
390
|
+
try {
|
|
391
|
+
await runAtomUpload(runtime.editor, nodeId, async () => {
|
|
392
|
+
const result = await mediaProvider.upload(file, { signal: controller.signal });
|
|
393
|
+
return { src: result.url, id: result.id };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
pendingMediaUploads.current.delete(controller);
|
|
398
|
+
URL.revokeObjectURL(preview);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const editSelectedAtom = (resizeBy) => {
|
|
402
|
+
const scope = atomScope();
|
|
403
|
+
if (scope.kind !== "atomic-node")
|
|
404
|
+
return;
|
|
405
|
+
const node = findNode(runtime.editor.document, scope.nodeId);
|
|
406
|
+
if (!node)
|
|
407
|
+
return;
|
|
408
|
+
const operations = resizeBy === undefined
|
|
409
|
+
? updateAtom(runtime.editor.document, scope, node.type.includes("formula")
|
|
410
|
+
? { attrs: { source: window.prompt("Formula source", String(node.attrs?.source || "")) || node.attrs?.source } }
|
|
411
|
+
: { attrs: { alt: window.prompt("Alt text", String(node.attrs?.alt || "")) ?? node.attrs?.alt } }, blockContext())
|
|
412
|
+
: resizeAtom(runtime.editor.document, scope, {
|
|
413
|
+
width: Math.max(16, Number(node.attrs?.width || 160) + resizeBy),
|
|
414
|
+
height: Math.max(16, Number(node.attrs?.height || 90) + resizeBy),
|
|
415
|
+
}, blockContext());
|
|
416
|
+
runtime.executeOperations(operations, resizeBy === undefined ? {} : { historyGroup: `resize-${scope.nodeId}` });
|
|
417
|
+
};
|
|
418
|
+
const replaceCanonicalDocument = (document) => {
|
|
419
|
+
runtime.replaceValue({ schemaVersion: runtime.editor.schema.version, revision: runtime.getRevision() + 1, document });
|
|
420
|
+
runtime.focus();
|
|
421
|
+
};
|
|
422
|
+
const legacyDocument = () => {
|
|
423
|
+
const ownerDocument = rootRef.current?.ownerDocument;
|
|
424
|
+
if (!ownerDocument)
|
|
425
|
+
return null;
|
|
426
|
+
return smartDocumentFromHtml(serializeCanonicalListHtml(runtime.editor.document, { clean: true }), ownerDocument);
|
|
427
|
+
};
|
|
428
|
+
const runImport = async (file) => {
|
|
429
|
+
if (/\.docx$/i.test(file.name) || file.type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
|
|
430
|
+
const ownerDocument = rootRef.current?.ownerDocument;
|
|
431
|
+
if (!ownerDocument)
|
|
432
|
+
return;
|
|
433
|
+
const result = await importStyledDocxDocument(await file.arrayBuffer(), ownerDocument);
|
|
434
|
+
replaceCanonicalDocument(parseCanonicalListHtml(result.layoutHtml || serializeSmartDocument(result.document)));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (/\.pdf$/i.test(file.name) || file.type === "application/pdf") {
|
|
438
|
+
const ownerDocument = rootRef.current?.ownerDocument;
|
|
439
|
+
if (!ownerDocument)
|
|
440
|
+
return;
|
|
441
|
+
const result = await importPdfDocument(await file.arrayBuffer(), ownerDocument);
|
|
442
|
+
replaceCanonicalDocument(parseCanonicalListHtml(result.layoutHtml));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const text = await file.text();
|
|
446
|
+
const document = /\.md(?:own)?$/i.test(file.name) ? parseCanonicalListMarkdown(text) : parseCanonicalListHtml(text);
|
|
447
|
+
replaceCanonicalDocument(document);
|
|
448
|
+
};
|
|
449
|
+
const runExport = (format) => {
|
|
450
|
+
const envelope = runtime.getValue();
|
|
451
|
+
if (format === "native")
|
|
452
|
+
return downloadText(rootRef.current.ownerDocument, "smart-rte.json", "application/json", JSON.stringify(envelope, null, 2));
|
|
453
|
+
if (format === "markdown")
|
|
454
|
+
return downloadText(rootRef.current.ownerDocument, "smart-rte.md", "text/markdown", serializeCanonicalListMarkdown(envelope.document));
|
|
455
|
+
return downloadText(rootRef.current.ownerDocument, "smart-rte.html", "text/html", serializeCanonicalListHtml(envelope.document, { clean: true }));
|
|
456
|
+
};
|
|
457
|
+
const runDocxExport = async () => {
|
|
458
|
+
const document = legacyDocument();
|
|
459
|
+
if (!document || !rootRef.current)
|
|
460
|
+
return;
|
|
461
|
+
const blob = await exportDocxDocument(document);
|
|
462
|
+
downloadBlob(rootRef.current.ownerDocument, "smart-rte.docx", blob);
|
|
463
|
+
};
|
|
464
|
+
const runPdfExport = () => {
|
|
465
|
+
const document = legacyDocument();
|
|
466
|
+
const view = rootRef.current?.ownerDocument.defaultView;
|
|
467
|
+
if (document && view)
|
|
468
|
+
printSmartDocumentAsPdf(document, view);
|
|
469
|
+
};
|
|
470
|
+
return _jsxs("section", { className: `srte-root srte-editor srte-canonical-authority${className ? ` ${className}` : ""}`, "data-smart-authority": "canonical", children: [_jsxs("div", { className: "srte-toolbar", role: "toolbar", "aria-label": "Formatting toolbar", children: [inlineToolDeclarations.filter((tool) => labels[tool.id]).map((tool) => _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": labels[tool.id], title: labels[tool.id], disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
471
|
+
if (["textColor", "backgroundColor", "fontSize", "fontFamily"].includes(tool.id))
|
|
472
|
+
applyAttributedMark(tool.id);
|
|
473
|
+
else {
|
|
474
|
+
executeMarkTool(runtime.editor, tool, "toggle");
|
|
475
|
+
runtime.focus();
|
|
476
|
+
}
|
|
477
|
+
}, children: labels[tool.id] }, tool.id)), _jsxs("select", { "aria-label": "Block type", value: currentBlockType, disabled: readOnly, onChange: (event) => transactBlock(setBlockTypeCommand(runtime.editor.document, blockScope(), {
|
|
478
|
+
type: event.target.value === "paragraph" ? "paragraph" : event.target.value === "code_block" ? "code_block" : "heading",
|
|
479
|
+
attrs: event.target.value.startsWith("heading-") ? { level: Number(event.target.value.slice(8)) } : {},
|
|
480
|
+
}, blockContext())), children: [_jsx("option", { value: "paragraph", children: "Paragraph" }), Array.from({ length: 6 }, (_, index) => _jsxs("option", { value: `heading-${index + 1}`, children: ["Heading ", index + 1] }, index + 1)), _jsx("option", { value: "code_block", children: "Code block" })] }), ["left", "center", "right", "justify"].map((align) => _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": `Align ${align}`, disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align } }, blockContext())), children: align }, align)), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Blockquote", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: toggleBlockquote, children: "Blockquote" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Move block up", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => runBlock("up"), children: "Block \u2191" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Move block down", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => runBlock("down"), children: "Block \u2193" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Indent block", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => runBlock("indent"), children: "Block indent" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Outdent block", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => runBlock("outdent"), children: "Block outdent" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Insert or edit link", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
481
|
+
const href = window.prompt("Link URL", "https://");
|
|
482
|
+
const tool = inlineToolDeclarations.find((entry) => entry.id === "link");
|
|
483
|
+
const description = runtime.editor.resolveScope({ want: "describe" });
|
|
484
|
+
const editingExisting = description.marks.some((entry) => entry.mark.type === "link");
|
|
485
|
+
if (href && tool) {
|
|
486
|
+
executeMarkTool(runtime.editor, tool, editingExisting ? "editLink" : "apply", { href });
|
|
487
|
+
runtime.focus();
|
|
488
|
+
}
|
|
489
|
+
}, children: "Link" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Remove link", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
490
|
+
const tool = inlineToolDeclarations.find((entry) => entry.id === "link");
|
|
491
|
+
if (tool) {
|
|
492
|
+
executeMarkTool(runtime.editor, tool, "remove");
|
|
493
|
+
runtime.focus();
|
|
494
|
+
}
|
|
495
|
+
}, children: "Unlink" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Bulleted list", "aria-pressed": listStyleActive("disc"), disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => toggleList("disc"), children: "Bullets" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Numbered list", "aria-pressed": listStyleActive("decimal"), disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => toggleList("decimal"), children: "Numbering" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Checklist", "aria-pressed": listStyleActive("disc", true), disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => toggleList("disc", true), children: "Checklist" }), _jsxs("select", { "aria-label": "List preset", title: "List type / preset", disabled: readOnly || currentListParts.length !== 1, value: currentListPreset, onChange: (event) => {
|
|
496
|
+
const preset = event.target.value;
|
|
497
|
+
if (!preset || currentListParts.length !== 1)
|
|
498
|
+
return;
|
|
499
|
+
// A preset choice applies to the whole list, regardless of how deep
|
|
500
|
+
// the cursor is nested — see outermostListId.
|
|
501
|
+
const rootId = outermostListId(currentListParts[0].listId);
|
|
502
|
+
runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
|
|
503
|
+
}, children: [_jsx("option", { value: "", children: "List preset" }), SMART_LIST_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.kind === "bullet" ? `Bullet · ${preset.label}` : `Number · ${preset.label}` }, preset.id))] }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Check selected items", "aria-pressed": currentListScope.kind === "list-selection" && currentListScope.items.every((item) => findNode(runtime.editor.document, item.itemId)?.attrs?.checked === true), disabled: readOnly || currentList?.attrs?.checkable !== true, onMouseDown: (event) => event.preventDefault(), onClick: toggleCheckedItems, children: "Check" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Indent list item", disabled: readOnly || !canIndent, onMouseDown: (event) => event.preventDefault(), onClick: () => runList("indent"), children: "Indent" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Outdent list item", disabled: readOnly || currentListParts.length === 0, onMouseDown: (event) => event.preventDefault(), onClick: () => runList("outdent"), children: "Outdent" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Move item up", disabled: readOnly || !canMoveUp, onMouseDown: (event) => event.preventDefault(), onClick: () => runList("up"), children: "Item \u2191" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Move item down", disabled: readOnly || !canMoveDown, onMouseDown: (event) => event.preventDefault(), onClick: () => runList("down"), children: "Item \u2193" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Restart numbering", disabled: readOnly || !orderedList, onMouseDown: (event) => event.preventDefault(), onClick: restartNumbering, children: "Restart" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Continue numbering", disabled: readOnly || !orderedList, onMouseDown: (event) => event.preventDefault(), onClick: () => runtime.executeOperations(continueListNumbering(runtime.editor.document, currentListScope, {}, blockContext()), { preserveSelectionById: true }), children: "Continue" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Insert table", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: insertTable, children: "Table 2\u00D72" }), [["row+", "Add row"], ["row-", "Remove row"], ["column+", "Add column"], ["column-", "Remove column"], ["merge", "Merge cells"], ["split", "Split cell"], ["header", "Header row"], ["row-up", "Move row up"], ["row-down", "Move row down"], ["column-left", "Move column left"], ["column-right", "Move column right"], ["remove", "Remove table"]].map(([action, label]) => _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": label, disabled: readOnly || !tableSelected
|
|
504
|
+
|| action === "merge" && currentTableScope.cellIds.length < 2
|
|
505
|
+
|| action === "row-up" && currentTableScope.rect.top === 0
|
|
506
|
+
|| action === "column-left" && currentTableScope.rect.left === 0, onMouseDown: (event) => event.preventDefault(), onClick: () => runTable(action), children: label }, action)), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Insert image", disabled: readOnly || !mediaProvider, onMouseDown: (event) => event.preventDefault(), onClick: () => setMediaKind("image"), children: "Image" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Insert video", disabled: readOnly || !mediaProvider, onMouseDown: (event) => event.preventDefault(), onClick: () => setMediaKind("video"), children: "Video" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Insert audio", disabled: readOnly || !mediaProvider, onMouseDown: (event) => event.preventDefault(), onClick: () => setMediaKind("audio"), children: "Audio" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Insert formula", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: insertInlineFormula, children: "Formula" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Edit selected atom", disabled: readOnly || !atomSelected, onMouseDown: (event) => event.preventDefault(), onClick: () => editSelectedAtom(), children: "Edit media" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Grow selected atom", disabled: readOnly || !atomSelected, onMouseDown: (event) => event.preventDefault(), onClick: () => editSelectedAtom(20), children: "Resize +" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Shrink selected atom", disabled: readOnly || !atomSelected, onMouseDown: (event) => event.preventDefault(), onClick: () => editSelectedAtom(-20), children: "Resize \u2212" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Delete selected atom", disabled: readOnly || !atomSelected, onMouseDown: (event) => event.preventDefault(), onClick: () => runtime.executeOperations(deleteAtom(runtime.editor.document, atomScope(), {}, blockContext())), children: "Delete media" }), _jsx("input", { ref: importRef, type: "file", accept: ".html,.htm,.md,.markdown,.docx,.pdf,text/html,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf", hidden: true, onChange: (event) => {
|
|
507
|
+
const file = event.currentTarget.files?.[0];
|
|
508
|
+
if (file)
|
|
509
|
+
void runImport(file);
|
|
510
|
+
event.currentTarget.value = "";
|
|
511
|
+
} }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Import document", disabled: readOnly, onClick: () => importRef.current?.click(), children: "Import" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Export HTML", onClick: () => runExport("html"), children: "Export HTML" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Export Markdown", onClick: () => runExport("markdown"), children: "Export Markdown" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Export DOCX", onClick: () => void runDocxExport(), children: "Export DOCX" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Export PDF", onClick: runPdfExport, children: "Export PDF" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Export native document", onClick: () => runExport("native"), children: "Export Native" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Undo", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => { runtime.editor.undo(); runtime.focus(); }, children: "Undo" }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Redo", disabled: readOnly, onMouseDown: (event) => event.preventDefault(), onClick: () => { runtime.editor.redo(); runtime.focus(); }, children: "Redo" })] }), mediaKind && mediaProvider && _jsx(MediaPicker, { kind: mediaKind, onPick: (file) => void insertMediaFile(mediaKind, file), onCancel: () => setMediaKind(null) }), _jsx("div", { ref: rootRef, className: "srte-editor", "data-placeholder": placeholder, contentEditable: !readOnly, suppressContentEditableWarning: true, role: "textbox", "aria-label": "Smart RTE editing surface", "aria-multiline": "true", onClick: (event) => {
|
|
512
|
+
const anchor = event.target?.closest("a[href]");
|
|
513
|
+
if (!anchor || !(event.metaKey || event.ctrlKey))
|
|
514
|
+
return;
|
|
515
|
+
event.preventDefault();
|
|
516
|
+
window.open(anchor.href, "_blank", "noopener,noreferrer");
|
|
517
|
+
}, style: { minHeight, maxHeight, overflow: "auto" } })] });
|
|
518
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { MediaManagerAdapter } from "./MediaManager.js";
|
|
2
|
+
import { type CoreFeatureConfig } from 'smartrte-core';
|
|
2
3
|
import { SrteTheme } from '../theme.js';
|
|
3
|
-
type
|
|
4
|
+
import { type ReactEditorPlugin } from '../pluginRuntime.js';
|
|
5
|
+
import { type EditorFormatConfig, type EditorFormatDefinition } from '../formatRuntime.js';
|
|
6
|
+
export type ClassicEditorProps = {
|
|
4
7
|
value?: string;
|
|
5
8
|
onChange?: (html: string) => void;
|
|
6
9
|
placeholder?: string;
|
|
@@ -10,6 +13,14 @@ type ClassicEditorProps = {
|
|
|
10
13
|
table?: boolean;
|
|
11
14
|
media?: boolean;
|
|
12
15
|
formula?: boolean;
|
|
16
|
+
/** Filters the standard plugin preset. Supersedes legacy table/media/formula toggles. */
|
|
17
|
+
features?: CoreFeatureConfig;
|
|
18
|
+
/** Exact core plugin set. When provided, `features` and legacy feature toggles are ignored. */
|
|
19
|
+
plugins?: readonly ReactEditorPlugin[];
|
|
20
|
+
/** Independently enables or disables HTML, Markdown, DOCX, and PDF import/export. */
|
|
21
|
+
formats?: EditorFormatConfig;
|
|
22
|
+
/** Exact format definitions. Built-in ids may be replaced and proprietary formats may be added. */
|
|
23
|
+
formatDefinitions?: readonly EditorFormatDefinition[];
|
|
13
24
|
mediaManager?: MediaManagerAdapter;
|
|
14
25
|
/**
|
|
15
26
|
* Optional custom list of fonts to display in the toolbar.
|
|
@@ -62,5 +73,4 @@ type ClassicEditorProps = {
|
|
|
62
73
|
*/
|
|
63
74
|
className?: string;
|
|
64
75
|
};
|
|
65
|
-
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, preserveFontFamily, preserveColors, preserveDocxStyles, theme, showFontSize, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
66
|
-
export {};
|
|
76
|
+
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table: legacyTable, media: legacyMedia, formula: legacyFormula, features, plugins, formats, formatDefinitions, mediaManager, fonts, defaultFont, preserveFontFamily, preserveColors, preserveDocxStyles, theme, showFontSize, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|