smartrte-react 0.3.3 → 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 +16 -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 +1309 -1159
- 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 -6
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createSmartEditor, getNodeAtPath, mediaPlugin, } from "smartrte-core";
|
|
2
|
+
import { selectionFromDom } from "./domSelectionBridge.js";
|
|
3
|
+
import { smartDocumentFromEditorRoot } from "./domSmartDocument.js";
|
|
4
|
+
import { inlineAtomPathFromDom } from "./domInlineAtomCommandBridge.js";
|
|
5
|
+
/**
|
|
6
|
+
* Executes inline image insertion through core and projects only the resulting
|
|
7
|
+
* atom into the existing DOM range. Unrelated DOM and metadata stay untouched.
|
|
8
|
+
*/
|
|
9
|
+
export const executeDomInlineImageCommand = (root, input, domSelection = root.ownerDocument.defaultView?.getSelection() || null) => {
|
|
10
|
+
if (!domSelection?.rangeCount || !domSelection.isCollapsed)
|
|
11
|
+
return null;
|
|
12
|
+
const modelSelection = selectionFromDom(root, domSelection);
|
|
13
|
+
if (!modelSelection)
|
|
14
|
+
return null;
|
|
15
|
+
const { document } = smartDocumentFromEditorRoot(root);
|
|
16
|
+
const editor = createSmartEditor({
|
|
17
|
+
state: { document, selection: modelSelection },
|
|
18
|
+
plugins: [mediaPlugin],
|
|
19
|
+
});
|
|
20
|
+
if (!editor.execute("image.insert-inline", input))
|
|
21
|
+
return null;
|
|
22
|
+
const selectionAfter = editor.state.selection;
|
|
23
|
+
if (selectionAfter.type !== "text")
|
|
24
|
+
return null;
|
|
25
|
+
const imagePath = [
|
|
26
|
+
...selectionAfter.anchor.path.slice(0, -1),
|
|
27
|
+
selectionAfter.anchor.path[selectionAfter.anchor.path.length - 1] - 1,
|
|
28
|
+
];
|
|
29
|
+
const atom = getNodeAtPath(editor.state.document, imagePath);
|
|
30
|
+
if (atom?.type !== "inlineImage")
|
|
31
|
+
return null;
|
|
32
|
+
const range = domSelection.getRangeAt(0);
|
|
33
|
+
const image = root.ownerDocument.createElement("img");
|
|
34
|
+
image.src = atom.src;
|
|
35
|
+
image.alt = atom.alt || "";
|
|
36
|
+
if (atom.title)
|
|
37
|
+
image.title = atom.title;
|
|
38
|
+
if (atom.width)
|
|
39
|
+
image.width = atom.width;
|
|
40
|
+
if (atom.height)
|
|
41
|
+
image.height = atom.height;
|
|
42
|
+
image.dataset.srteInline = "true";
|
|
43
|
+
range.insertNode(image);
|
|
44
|
+
range.setStartAfter(image);
|
|
45
|
+
range.collapse(true);
|
|
46
|
+
domSelection.removeAllRanges();
|
|
47
|
+
domSelection.addRange(range);
|
|
48
|
+
return image;
|
|
49
|
+
};
|
|
50
|
+
/** Validates an inline image attribute update through core, then mutates it in place. */
|
|
51
|
+
export const executeDomInlineImageUpdate = (root, image, input) => {
|
|
52
|
+
const path = inlineAtomPathFromDom(root, image);
|
|
53
|
+
if (!path)
|
|
54
|
+
return false;
|
|
55
|
+
const selection = selectionFromDom(root, root.ownerDocument.defaultView?.getSelection() || null) || { type: "node", path };
|
|
56
|
+
const { document } = smartDocumentFromEditorRoot(root);
|
|
57
|
+
const editor = createSmartEditor({
|
|
58
|
+
state: { document, selection },
|
|
59
|
+
plugins: [mediaPlugin],
|
|
60
|
+
});
|
|
61
|
+
if (!editor.execute("image.update-inline", { path, ...input }))
|
|
62
|
+
return false;
|
|
63
|
+
if (input.alt !== undefined)
|
|
64
|
+
image.alt = input.alt;
|
|
65
|
+
if (input.title !== undefined)
|
|
66
|
+
image.title = input.title;
|
|
67
|
+
if (input.width !== undefined)
|
|
68
|
+
image.width = input.width;
|
|
69
|
+
if (input.height !== undefined)
|
|
70
|
+
image.height = input.height;
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type DomMoveDirection = "up" | "down" | "left" | "right";
|
|
2
|
+
/**
|
|
3
|
+
* Executes model move/indent commands while preserving the existing DOM nodes.
|
|
4
|
+
* Null means the DOM shape is outside this adapter; false means a valid command
|
|
5
|
+
* was disabled at a boundary.
|
|
6
|
+
*/
|
|
7
|
+
export declare const executeDomMoveCommand: (blocks: readonly HTMLElement[], direction: DomMoveDirection, beforeMutation?: () => void) => HTMLElement[] | false | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createSmartEditor, movePlugin } from "smartrte-core";
|
|
2
|
+
import { smartDocumentFromHtml } from "./domSmartDocument.js";
|
|
3
|
+
/**
|
|
4
|
+
* Executes model move/indent commands while preserving the existing DOM nodes.
|
|
5
|
+
* Null means the DOM shape is outside this adapter; false means a valid command
|
|
6
|
+
* was disabled at a boundary.
|
|
7
|
+
*/
|
|
8
|
+
export const executeDomMoveCommand = (blocks, direction, beforeMutation) => {
|
|
9
|
+
if (!blocks.length || !blocks[0].parentElement)
|
|
10
|
+
return null;
|
|
11
|
+
const parent = blocks[0].parentElement;
|
|
12
|
+
if (!blocks.every((block) => block.parentElement === parent))
|
|
13
|
+
return null;
|
|
14
|
+
const siblings = Array.from(parent.children);
|
|
15
|
+
const indexes = blocks.map((block) => siblings.indexOf(block));
|
|
16
|
+
if (indexes.some((index, position) => index < 0 || (position > 0 && index !== indexes[position - 1] + 1))) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const document = smartDocumentFromHtml(siblings.map((sibling) => sibling.outerHTML).join(""), parent.ownerDocument);
|
|
20
|
+
if (document.children.length !== siblings.length)
|
|
21
|
+
return null;
|
|
22
|
+
const editor = createSmartEditor({
|
|
23
|
+
state: { document, selection: { type: "all" } },
|
|
24
|
+
plugins: [movePlugin],
|
|
25
|
+
});
|
|
26
|
+
const commandId = direction === "up" || direction === "down" ? "block.move" : "block.indent";
|
|
27
|
+
const input = {
|
|
28
|
+
parentPath: [],
|
|
29
|
+
blockIndexes: indexes,
|
|
30
|
+
direction: direction === "left" ? "outdent" : direction === "right" ? "indent" : direction,
|
|
31
|
+
};
|
|
32
|
+
if (!editor.execute(commandId, input))
|
|
33
|
+
return false;
|
|
34
|
+
beforeMutation?.();
|
|
35
|
+
if (direction === "left" || direction === "right") {
|
|
36
|
+
indexes.forEach((index, position) => {
|
|
37
|
+
const indent = editor.state.document.children[index].indent || 0;
|
|
38
|
+
blocks[position].style.marginLeft = indent ? `${indent * 24}px` : "";
|
|
39
|
+
if (!blocks[position].getAttribute("style"))
|
|
40
|
+
blocks[position].removeAttribute("style");
|
|
41
|
+
});
|
|
42
|
+
return [...blocks];
|
|
43
|
+
}
|
|
44
|
+
if (direction === "up") {
|
|
45
|
+
const previous = siblings[indexes[0] - 1];
|
|
46
|
+
blocks.forEach((block) => parent.insertBefore(block, previous));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const next = siblings[indexes[indexes.length - 1] + 1];
|
|
50
|
+
const reference = next.nextSibling;
|
|
51
|
+
blocks.forEach((block) => parent.insertBefore(block, reference));
|
|
52
|
+
}
|
|
53
|
+
return [...blocks];
|
|
54
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { SmartSelection } from "smartrte-core";
|
|
1
|
+
import type { Path, SmartSelection } from "smartrte-core";
|
|
2
2
|
export declare const isEditorOnlyElement: (node: Element) => boolean;
|
|
3
|
+
/** Returns the semantic model path for a DOM element at the adapter boundary. */
|
|
4
|
+
export declare const pathForDomElement: (element: Element, editor: HTMLElement) => Path | null;
|
|
3
5
|
/** Converts browser selection into a core selection without exposing editor UI nodes. */
|
|
4
6
|
export declare const selectionFromDom: (editor: HTMLElement, selection: Selection | null) => SmartSelection | null;
|
|
5
7
|
/** Restores a text selection from core paths after the editor DOM is rebuilt. */
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const LEAF_TAGS = new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6"]);
|
|
2
2
|
const CONTAINER_TAGS = new Set(["blockquote", "ul", "ol", "li", "table", "tr", "td", "th"]);
|
|
3
3
|
export const isEditorOnlyElement = (node) => node.getAttribute("data-table-wrapper") === "true" ||
|
|
4
|
-
|
|
4
|
+
node.hasAttribute("data-srte-selection-marker") ||
|
|
5
|
+
node.hasAttribute("data-srte-resize-overlay") ||
|
|
6
|
+
node.hasAttribute("data-srte-drag-handle") ||
|
|
7
|
+
node.hasAttribute("data-srte-check") ||
|
|
5
8
|
node.matches(".srte-table-resize-handle, .srte-table-resize-overlay, .srte-drag-handle");
|
|
6
9
|
const isSemanticElement = (node) => LEAF_TAGS.has(node.tagName.toLowerCase()) || CONTAINER_TAGS.has(node.tagName.toLowerCase());
|
|
7
10
|
const hasSemanticChild = (node) => Array.from(node.children).some((child) => !isEditorOnlyElement(child) && isSemanticElement(child));
|
|
@@ -59,38 +62,51 @@ const pathForElement = (element, editor) => {
|
|
|
59
62
|
};
|
|
60
63
|
return findPath(editor, []);
|
|
61
64
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
/** Returns the semantic model path for a DOM element at the adapter boundary. */
|
|
66
|
+
export const pathForDomElement = (element, editor) => pathForElement(element, editor);
|
|
67
|
+
const isInlineAtom = (element) => element.tagName.toLowerCase() === "img" || element.hasAttribute("data-formula");
|
|
68
|
+
const inlineUnitsIn = (root) => {
|
|
69
|
+
const units = [];
|
|
70
|
+
const visit = (node) => {
|
|
71
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
72
|
+
if (node.textContent)
|
|
73
|
+
units.push({ type: "text", node: node });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!(node instanceof Element) || isEditorOnlyElement(node))
|
|
77
|
+
return;
|
|
78
|
+
if (isInlineAtom(node)) {
|
|
79
|
+
units.push({ type: "atom", node });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
Array.from(node.childNodes).forEach(visit);
|
|
83
|
+
};
|
|
84
|
+
Array.from(root.childNodes).forEach(visit);
|
|
85
|
+
return units;
|
|
81
86
|
};
|
|
82
|
-
const
|
|
83
|
-
|
|
87
|
+
const pointRelativeTo = (container, offset, unit) => {
|
|
88
|
+
const point = container.ownerDocument?.createRange();
|
|
89
|
+
const unitRange = container.ownerDocument?.createRange();
|
|
90
|
+
if (!point || !unitRange)
|
|
84
91
|
return null;
|
|
85
|
-
const range = leaf.ownerDocument.createRange();
|
|
86
|
-
range.selectNodeContents(leaf);
|
|
87
92
|
try {
|
|
88
|
-
|
|
93
|
+
point.setStart(container, offset);
|
|
94
|
+
point.collapse(true);
|
|
95
|
+
if (unit.type === "text")
|
|
96
|
+
unitRange.selectNodeContents(unit.node);
|
|
97
|
+
else
|
|
98
|
+
unitRange.selectNode(unit.node);
|
|
99
|
+
const fromStart = point.compareBoundaryPoints(Range.START_TO_START, unitRange);
|
|
100
|
+
const fromEnd = point.compareBoundaryPoints(Range.START_TO_END, unitRange);
|
|
101
|
+
if (fromStart <= 0)
|
|
102
|
+
return "before";
|
|
103
|
+
if (fromEnd >= 0)
|
|
104
|
+
return "after";
|
|
105
|
+
return "inside";
|
|
89
106
|
}
|
|
90
107
|
catch {
|
|
91
108
|
return null;
|
|
92
109
|
}
|
|
93
|
-
return range.toString().length;
|
|
94
110
|
};
|
|
95
111
|
const pointForDomPoint = (editor, container, offset) => {
|
|
96
112
|
const leaf = closestLeaf(container, editor);
|
|
@@ -99,25 +115,47 @@ const pointForDomPoint = (editor, container, offset) => {
|
|
|
99
115
|
const leafPath = pathForElement(leaf, editor);
|
|
100
116
|
if (!leafPath)
|
|
101
117
|
return null;
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
118
|
+
const units = inlineUnitsIn(leaf);
|
|
119
|
+
if (units.length === 0)
|
|
104
120
|
return { path: [...leafPath, 0], offset: 0 };
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
const directTextIndex = units.findIndex((unit) => unit.type === "text" && unit.node === container);
|
|
122
|
+
if (directTextIndex >= 0) {
|
|
123
|
+
const text = units[directTextIndex];
|
|
124
|
+
return {
|
|
125
|
+
path: [...leafPath, directTextIndex],
|
|
126
|
+
offset: Math.max(0, Math.min(offset, text.node.data.length)),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
let previousText = null;
|
|
130
|
+
for (let index = 0; index < units.length; index += 1) {
|
|
131
|
+
const unit = units[index];
|
|
132
|
+
const relative = pointRelativeTo(container, offset, unit);
|
|
133
|
+
if (relative === "before") {
|
|
134
|
+
if (unit.type === "text")
|
|
135
|
+
return { path: [...leafPath, index], offset: 0 };
|
|
136
|
+
const nextTextIndex = units.slice(index + 1).findIndex((candidate) => candidate.type === "text");
|
|
137
|
+
if (nextTextIndex >= 0)
|
|
138
|
+
return { path: [...leafPath, index + 1 + nextTextIndex], offset: 0 };
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (unit.type === "text")
|
|
142
|
+
previousText = { index, node: unit.node };
|
|
143
|
+
}
|
|
144
|
+
if (previousText) {
|
|
145
|
+
return { path: [...leafPath, previousText.index], offset: previousText.node.data.length };
|
|
114
146
|
}
|
|
115
|
-
|
|
116
|
-
return { path: [...leafPath, last], offset: textNodes[last].data.length };
|
|
147
|
+
return null;
|
|
117
148
|
};
|
|
118
149
|
const nodeSelectionFromRange = (editor, range) => {
|
|
119
|
-
const nodes = Array.from(editor.querySelectorAll("img, [data-srte-node-selection='true']"));
|
|
120
|
-
const selected = nodes.find((node) =>
|
|
150
|
+
const nodes = Array.from(editor.querySelectorAll("img, [data-formula], [data-srte-node-selection='true']"));
|
|
151
|
+
const selected = nodes.find((node) => {
|
|
152
|
+
if (range.collapsed)
|
|
153
|
+
return node === range.startContainer || node.contains(range.startContainer);
|
|
154
|
+
const nodeRange = editor.ownerDocument.createRange();
|
|
155
|
+
nodeRange.selectNode(node);
|
|
156
|
+
return range.compareBoundaryPoints(Range.START_TO_START, nodeRange) === 0 &&
|
|
157
|
+
range.compareBoundaryPoints(Range.END_TO_END, nodeRange) === 0;
|
|
158
|
+
});
|
|
121
159
|
if (!selected)
|
|
122
160
|
return null;
|
|
123
161
|
const path = pathForElement(selected, editor);
|
|
@@ -155,13 +193,27 @@ const domPointForSmartPoint = (editor, point) => {
|
|
|
155
193
|
const leaf = elementAtPath(editor, point.path.slice(0, -1));
|
|
156
194
|
if (!leaf)
|
|
157
195
|
return null;
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
196
|
+
const unit = inlineUnitsIn(leaf)[point.path[point.path.length - 1]];
|
|
197
|
+
if (unit?.type !== "text" || point.offset < 0 || point.offset > unit.node.data.length)
|
|
160
198
|
return null;
|
|
161
|
-
return { node:
|
|
199
|
+
return { node: unit.node, offset: point.offset };
|
|
162
200
|
};
|
|
163
201
|
/** Restores a text selection from core paths after the editor DOM is rebuilt. */
|
|
164
202
|
export const restoreSelectionToDom = (editor, smartSelection) => {
|
|
203
|
+
if (smartSelection.type === "node") {
|
|
204
|
+
const element = elementAtPath(editor, smartSelection.path);
|
|
205
|
+
if (!element)
|
|
206
|
+
return false;
|
|
207
|
+
const range = editor.ownerDocument.createRange();
|
|
208
|
+
range.selectNodeContents(element);
|
|
209
|
+
range.collapse(false);
|
|
210
|
+
const selection = editor.ownerDocument.defaultView?.getSelection();
|
|
211
|
+
if (!selection)
|
|
212
|
+
return false;
|
|
213
|
+
selection.removeAllRanges();
|
|
214
|
+
selection.addRange(range);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
165
217
|
if (smartSelection.type !== "text")
|
|
166
218
|
return false;
|
|
167
219
|
const anchor = domPointForSmartPoint(editor, smartSelection.anchor);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeCompatibilityHtml, sanitizeLinkAttrs, } from "smartrte-core";
|
|
2
|
+
import { isSmartListPreset } from "smartrte-core";
|
|
2
3
|
import { isEditorOnlyElement } from "./domSelectionBridge.js";
|
|
3
|
-
const blockTags = new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "table"]);
|
|
4
|
+
const blockTags = new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "table", "img", "video", "audio"]);
|
|
4
5
|
const unwrap = (element) => {
|
|
5
6
|
const parent = element.parentNode;
|
|
6
7
|
if (!parent)
|
|
@@ -40,6 +41,9 @@ const marksFor = (element, inherited) => {
|
|
|
40
41
|
marks = addMark(marks, { type: "subscript" });
|
|
41
42
|
if (tag === "code")
|
|
42
43
|
marks = addMark(marks, { type: "code" });
|
|
44
|
+
if (element.hasAttribute("data-formula")) {
|
|
45
|
+
marks = addMark(marks, { type: "formula", value: element.getAttribute("data-formula") || "" });
|
|
46
|
+
}
|
|
43
47
|
if (tag === "a") {
|
|
44
48
|
const safeLink = sanitizeLinkAttrs({
|
|
45
49
|
href: element.getAttribute("href") || "",
|
|
@@ -51,6 +55,7 @@ const marksFor = (element, inherited) => {
|
|
|
51
55
|
const color = element.getAttribute("color") || element.style.color;
|
|
52
56
|
const backgroundColor = element.style.backgroundColor;
|
|
53
57
|
const fontSize = element.style.fontSize;
|
|
58
|
+
const fontFamily = element.style.fontFamily;
|
|
54
59
|
if (color)
|
|
55
60
|
marks = addMark(marks, { type: "textColor", value: color });
|
|
56
61
|
if (backgroundColor)
|
|
@@ -64,6 +69,8 @@ const marksFor = (element, inherited) => {
|
|
|
64
69
|
marks = addMark(marks, { type: "fontSize", valuePx });
|
|
65
70
|
}
|
|
66
71
|
}
|
|
72
|
+
if (fontFamily)
|
|
73
|
+
marks = addMark(marks, { type: "fontFamily", value: fontFamily });
|
|
67
74
|
return marks;
|
|
68
75
|
};
|
|
69
76
|
const inlineNodes = (nodes, inherited = []) => {
|
|
@@ -76,6 +83,27 @@ const inlineNodes = (nodes, inherited = []) => {
|
|
|
76
83
|
}
|
|
77
84
|
if (!(node instanceof Element))
|
|
78
85
|
return;
|
|
86
|
+
if (node.hasAttribute("data-formula")) {
|
|
87
|
+
result.push({
|
|
88
|
+
type: "formula",
|
|
89
|
+
value: node.getAttribute("data-formula") || "",
|
|
90
|
+
...(node.textContent && node.textContent !== node.getAttribute("data-formula")
|
|
91
|
+
? { displayText: node.textContent }
|
|
92
|
+
: {}),
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (node.tagName.toLowerCase() === "img") {
|
|
97
|
+
result.push({
|
|
98
|
+
type: "inlineImage",
|
|
99
|
+
src: node.getAttribute("src") || "",
|
|
100
|
+
alt: node.getAttribute("alt") || undefined,
|
|
101
|
+
title: node.getAttribute("title") || undefined,
|
|
102
|
+
width: Number(node.getAttribute("width")) || undefined,
|
|
103
|
+
height: Number(node.getAttribute("height")) || undefined,
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
79
107
|
if (node.tagName.toLowerCase() === "br") {
|
|
80
108
|
result.push({ type: "text", text: "\n", marks: inherited.length ? inherited : undefined });
|
|
81
109
|
return;
|
|
@@ -103,9 +131,19 @@ const alignmentFor = (element) => {
|
|
|
103
131
|
const value = (element.style.textAlign || element.getAttribute("align") || "").toLowerCase();
|
|
104
132
|
return value === "center" || value === "right" || value === "justify" ? value : undefined;
|
|
105
133
|
};
|
|
134
|
+
const indentFor = (element) => {
|
|
135
|
+
const margin = Number.parseFloat(element.style.marginLeft || "0");
|
|
136
|
+
return Number.isFinite(margin) && margin > 0 ? Math.min(Math.round(margin / 24), 10) : undefined;
|
|
137
|
+
};
|
|
106
138
|
const parseBlocks = (parent) => directBlockChildren(parent).flatMap(parseBlock);
|
|
107
139
|
const parseList = (element) => {
|
|
108
|
-
const
|
|
140
|
+
const requestedStyle = element.style.listStyleType;
|
|
141
|
+
const supportedStyles = new Set([
|
|
142
|
+
"disc", "circle", "square", "decimal", "decimal-leading-zero", "lower-alpha", "upper-alpha",
|
|
143
|
+
"lower-roman", "upper-roman",
|
|
144
|
+
]);
|
|
145
|
+
const fallbackStyle = element.tagName.toLowerCase() === "ol" ? "decimal" : "disc";
|
|
146
|
+
const style = (supportedStyles.has(requestedStyle) ? requestedStyle : fallbackStyle);
|
|
109
147
|
const items = Array.from(element.children)
|
|
110
148
|
.filter((child) => child.tagName.toLowerCase() === "li")
|
|
111
149
|
.map((item) => {
|
|
@@ -113,9 +151,28 @@ const parseList = (element) => {
|
|
|
113
151
|
const nested = Array.from(item.children)
|
|
114
152
|
.filter((child) => ["ul", "ol", "blockquote", "pre", "table", "p", "h1", "h2", "h3", "h4", "h5", "h6"].includes(child.tagName.toLowerCase()))
|
|
115
153
|
.flatMap(parseBlock);
|
|
116
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
type: "listItem",
|
|
156
|
+
alignment: alignmentFor(item),
|
|
157
|
+
...(item.hasAttribute("data-checked") || item.hasAttribute("data-srte-checked")
|
|
158
|
+
? { checked: (item.getAttribute("data-checked") || item.getAttribute("data-srte-checked")) === "true" }
|
|
159
|
+
: {}),
|
|
160
|
+
children: [...(content ? [content] : []), ...nested],
|
|
161
|
+
};
|
|
117
162
|
});
|
|
118
|
-
|
|
163
|
+
const checklist = element.getAttribute("data-srte-checklist") === "true";
|
|
164
|
+
const requestedPreset = element.getAttribute("data-srte-list-preset");
|
|
165
|
+
return {
|
|
166
|
+
type: "list",
|
|
167
|
+
indent: indentFor(element),
|
|
168
|
+
style,
|
|
169
|
+
...(isSmartListPreset(requestedPreset) ? { preset: requestedPreset } : {}),
|
|
170
|
+
...(checklist ? { checklist: true } : {}),
|
|
171
|
+
...(checklist && element.getAttribute("data-srte-checklist-strike") === "true"
|
|
172
|
+
? { strikeCompleted: true }
|
|
173
|
+
: {}),
|
|
174
|
+
children: items,
|
|
175
|
+
};
|
|
119
176
|
};
|
|
120
177
|
const tableRows = (table) => {
|
|
121
178
|
const rows = [];
|
|
@@ -128,16 +185,38 @@ const tableRows = (table) => {
|
|
|
128
185
|
});
|
|
129
186
|
return rows;
|
|
130
187
|
};
|
|
188
|
+
const borderFor = (element) => {
|
|
189
|
+
const style = element.style;
|
|
190
|
+
if (style.border)
|
|
191
|
+
return style.border;
|
|
192
|
+
const declared = element.getAttribute("style")?.match(/(?:^|;)\s*border\s*:\s*([^;]+)/i)?.[1]?.trim();
|
|
193
|
+
if (declared)
|
|
194
|
+
return declared;
|
|
195
|
+
if (style.borderTopStyle === "none" &&
|
|
196
|
+
style.borderRightStyle === "none" &&
|
|
197
|
+
style.borderBottomStyle === "none" &&
|
|
198
|
+
style.borderLeftStyle === "none")
|
|
199
|
+
return "none";
|
|
200
|
+
return undefined;
|
|
201
|
+
};
|
|
131
202
|
const parseTable = (element) => ({
|
|
132
203
|
type: "table",
|
|
204
|
+
indent: indentFor(element),
|
|
205
|
+
columnWidths: Array.from(element.querySelectorAll(":scope > colgroup > col"))
|
|
206
|
+
.map((column) => Number.parseFloat(column.style.width || column.getAttribute("width") || ""))
|
|
207
|
+
.filter((width) => Number.isFinite(width) && width > 0),
|
|
133
208
|
children: tableRows(element).map((row) => ({
|
|
134
209
|
type: "tableRow",
|
|
210
|
+
heightPx: Number.parseFloat(row.style.height || "") || undefined,
|
|
135
211
|
children: Array.from(row.children)
|
|
136
212
|
.filter((cell) => ["td", "th"].includes(cell.tagName.toLowerCase()))
|
|
137
213
|
.map((cell) => ({
|
|
138
214
|
type: cell.tagName.toLowerCase() === "th" ? "tableHeaderCell" : "tableCell",
|
|
139
215
|
colspan: Number(cell.getAttribute("colspan")) || undefined,
|
|
140
216
|
rowspan: Number(cell.getAttribute("rowspan")) || undefined,
|
|
217
|
+
backgroundColor: cell.style.backgroundColor || undefined,
|
|
218
|
+
textColor: cell.style.color || undefined,
|
|
219
|
+
border: borderFor(cell),
|
|
141
220
|
children: parseBlocks(cell).length ? parseBlocks(cell) : [{ type: "paragraph", alignment: alignmentFor(cell), children: inlineNodes(cell.childNodes) }],
|
|
142
221
|
})),
|
|
143
222
|
})),
|
|
@@ -145,15 +224,34 @@ const parseTable = (element) => ({
|
|
|
145
224
|
const parseBlock = (element) => {
|
|
146
225
|
const tag = element.tagName.toLowerCase();
|
|
147
226
|
if (tag === "p")
|
|
148
|
-
return [{ type: "paragraph", alignment: alignmentFor(element), children: inlineNodes(element.childNodes) }];
|
|
227
|
+
return [{ type: "paragraph", alignment: alignmentFor(element), indent: indentFor(element), children: inlineNodes(element.childNodes) }];
|
|
149
228
|
if (/^h[1-6]$/.test(tag))
|
|
150
|
-
return [{ type: "heading", level: Number(tag.slice(1)), alignment: alignmentFor(element), children: inlineNodes(element.childNodes) }];
|
|
229
|
+
return [{ type: "heading", level: Number(tag.slice(1)), alignment: alignmentFor(element), indent: indentFor(element), children: inlineNodes(element.childNodes) }];
|
|
151
230
|
if (tag === "ul" || tag === "ol")
|
|
152
231
|
return [parseList(element)];
|
|
153
232
|
if (tag === "blockquote")
|
|
154
|
-
return [{ type: "blockquote", alignment: alignmentFor(element), children: parseBlocks(element) }];
|
|
233
|
+
return [{ type: "blockquote", alignment: alignmentFor(element), indent: indentFor(element), children: parseBlocks(element) }];
|
|
155
234
|
if (tag === "pre")
|
|
156
|
-
return [{ type: "codeBlock", alignment: alignmentFor(element), text: element.textContent || "", language: element.querySelector("code")?.className.replace(/^language-/, "") || undefined }];
|
|
235
|
+
return [{ type: "codeBlock", alignment: alignmentFor(element), indent: indentFor(element), text: element.textContent || "", language: element.querySelector("code")?.className.replace(/^language-/, "") || undefined }];
|
|
236
|
+
if (tag === "img")
|
|
237
|
+
return [{
|
|
238
|
+
type: "image",
|
|
239
|
+
indent: indentFor(element),
|
|
240
|
+
src: element.getAttribute("src") || "",
|
|
241
|
+
alt: element.getAttribute("alt") || undefined,
|
|
242
|
+
title: element.getAttribute("title") || undefined,
|
|
243
|
+
width: Number(element.getAttribute("width")) || undefined,
|
|
244
|
+
height: Number(element.getAttribute("height")) || undefined,
|
|
245
|
+
}];
|
|
246
|
+
if (tag === "video" || tag === "audio")
|
|
247
|
+
return [{
|
|
248
|
+
type: "media",
|
|
249
|
+
indent: indentFor(element),
|
|
250
|
+
src: element.getAttribute("src") || element.querySelector("source")?.getAttribute("src") || "",
|
|
251
|
+
mediaType: tag,
|
|
252
|
+
title: element.getAttribute("title") || undefined,
|
|
253
|
+
mimeType: element.getAttribute("type") || element.querySelector("source")?.getAttribute("type") || undefined,
|
|
254
|
+
}];
|
|
157
255
|
if (tag === "table")
|
|
158
256
|
return [parseTable(element)];
|
|
159
257
|
return [];
|
|
@@ -191,26 +289,70 @@ const serializeText = (node) => {
|
|
|
191
289
|
html = `<span style="background-color:${escapeHtml(mark.value)}">${html}</span>`;
|
|
192
290
|
if (mark.type === "fontSize")
|
|
193
291
|
html = `<span style="font-size:${mark.valuePx}px">${html}</span>`;
|
|
292
|
+
if (mark.type === "fontFamily")
|
|
293
|
+
html = `<span style="font-family:${escapeHtml(mark.value)}">${html}</span>`;
|
|
294
|
+
if (mark.type === "formula")
|
|
295
|
+
html = `<span data-formula="${escapeHtml(mark.value)}">${html}</span>`;
|
|
194
296
|
if (mark.type === "link")
|
|
195
297
|
html = `<a href="${escapeHtml(mark.href)}"${mark.target ? ` target="${escapeHtml(mark.target)}"` : ""}>${html}</a>`;
|
|
196
298
|
});
|
|
197
299
|
return html;
|
|
198
300
|
};
|
|
199
|
-
const
|
|
301
|
+
const serializeInline = (node) => {
|
|
302
|
+
if (node.type === "text")
|
|
303
|
+
return serializeText(node);
|
|
304
|
+
if (node.type === "formula") {
|
|
305
|
+
return `<span data-formula="${escapeHtml(node.value)}">${escapeHtml(node.displayText ?? node.value)}</span>`;
|
|
306
|
+
}
|
|
307
|
+
return `<img data-srte-inline="true" src="${escapeHtml(node.src)}"${node.alt ? ` alt="${escapeHtml(node.alt)}"` : ""}${node.title ? ` title="${escapeHtml(node.title)}"` : ""}${node.width ? ` width="${node.width}"` : ""}${node.height ? ` height="${node.height}"` : ""}>`;
|
|
308
|
+
};
|
|
309
|
+
const blockStyleAttribute = (block, extra) => {
|
|
310
|
+
const declarations = [
|
|
311
|
+
block.alignment ? `text-align:${block.alignment}` : "",
|
|
312
|
+
block.indent ? `margin-left:${block.indent * 24}px` : "",
|
|
313
|
+
extra || "",
|
|
314
|
+
].filter(Boolean);
|
|
315
|
+
return declarations.length ? ` style="${declarations.join(";")}"` : "";
|
|
316
|
+
};
|
|
317
|
+
const alignmentAttribute = (alignment) => blockStyleAttribute({ alignment });
|
|
200
318
|
const serializeBlock = (block) => {
|
|
201
319
|
if (block.type === "paragraph")
|
|
202
|
-
return `<p${
|
|
320
|
+
return `<p${blockStyleAttribute(block)}>${block.children.map(serializeInline).join("")}</p>`;
|
|
203
321
|
if (block.type === "heading")
|
|
204
|
-
return `<h${block.level}${
|
|
322
|
+
return `<h${block.level}${blockStyleAttribute(block)}>${block.children.map(serializeInline).join("")}</h${block.level}>`;
|
|
205
323
|
if (block.type === "blockquote")
|
|
206
|
-
return `<blockquote${
|
|
324
|
+
return `<blockquote${blockStyleAttribute(block)}>${block.children.map(serializeBlock).join("")}</blockquote>`;
|
|
207
325
|
if (block.type === "codeBlock")
|
|
208
|
-
return `<pre${
|
|
326
|
+
return `<pre${blockStyleAttribute(block)}><code${block.language ? ` class="language-${escapeHtml(block.language)}"` : ""}>${escapeHtml(block.text)}</code></pre>`;
|
|
327
|
+
if (block.type === "image")
|
|
328
|
+
return `<img${blockStyleAttribute(block)} src="${escapeHtml(block.src)}"${block.alt ? ` alt="${escapeHtml(block.alt)}"` : ""}${block.title ? ` title="${escapeHtml(block.title)}"` : ""}${block.width ? ` width="${block.width}"` : ""}${block.height ? ` height="${block.height}"` : ""}>`;
|
|
329
|
+
if (block.type === "media") {
|
|
330
|
+
const tag = block.mediaType === "audio" ? "audio" : "video";
|
|
331
|
+
return `<${tag}${blockStyleAttribute(block)} controls src="${escapeHtml(block.src)}"${block.title ? ` title="${escapeHtml(block.title)}"` : ""}${block.mimeType ? ` type="${escapeHtml(block.mimeType)}"` : ""}></${tag}>`;
|
|
332
|
+
}
|
|
209
333
|
if (block.type === "list") {
|
|
210
|
-
const
|
|
211
|
-
|
|
334
|
+
const ordered = ["decimal", "decimal-leading-zero", "lower-alpha", "upper-alpha", "lower-roman", "upper-roman"].includes(block.style);
|
|
335
|
+
const tag = ordered ? "ol" : "ul";
|
|
336
|
+
const style = blockStyleAttribute(block, `list-style-type:${block.style}`);
|
|
337
|
+
const checklist = block.checklist
|
|
338
|
+
? ` data-srte-checklist="true" data-srte-checklist-strike="${block.strikeCompleted ? "true" : "false"}"`
|
|
339
|
+
: "";
|
|
340
|
+
const preset = block.preset ? ` data-srte-list-preset="${block.preset}"` : "";
|
|
341
|
+
return `<${tag}${style}${preset}${checklist}>${block.children.map((item) => `<li${item.checked !== undefined ? ` data-srte-checked="${item.checked ? "true" : "false"}"` : ""}${alignmentAttribute(item.alignment)}>${item.children.map(serializeBlock).join("")}</li>`).join("")}</${tag}>`;
|
|
212
342
|
}
|
|
213
|
-
|
|
343
|
+
const columns = block.columnWidths?.length
|
|
344
|
+
? `<colgroup>${block.columnWidths.map((width) => `<col style="width:${width}px">`).join("")}</colgroup>`
|
|
345
|
+
: "";
|
|
346
|
+
return `<table${blockStyleAttribute(block)}>${columns}<tbody>${block.children.map((row) => `<tr${row.heightPx ? ` style="height:${row.heightPx}px"` : ""}>${row.children.map((cell) => {
|
|
347
|
+
const tag = cell.type === "tableHeaderCell" ? "th" : "td";
|
|
348
|
+
const spans = `${cell.colspan && cell.colspan > 1 ? ` colspan="${cell.colspan}"` : ""}${cell.rowspan && cell.rowspan > 1 ? ` rowspan="${cell.rowspan}"` : ""}`;
|
|
349
|
+
const style = [
|
|
350
|
+
cell.backgroundColor ? `background-color:${escapeHtml(cell.backgroundColor)}` : "",
|
|
351
|
+
cell.textColor ? `color:${escapeHtml(cell.textColor)}` : "",
|
|
352
|
+
cell.border ? `border:${escapeHtml(cell.border)}` : "",
|
|
353
|
+
].filter(Boolean);
|
|
354
|
+
return `<${tag}${spans}${style.length ? ` style="${style.join(";")}"` : ""}>${cell.children.map(serializeBlock).join("")}</${tag}>`;
|
|
355
|
+
}).join("")}</tr>`).join("")}</tbody></table>`;
|
|
214
356
|
};
|
|
215
357
|
/** Serializes the shadow-only model. It is not used for persisted editor HTML. */
|
|
216
358
|
export const serializeSmartDocument = (document) => document.children.map(serializeBlock).join("");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type MergeTableCellsInput, type SetTableCellStyleInput, type SetTableColumnWidthInput, type SetTableRowHeightInput, type TableHeaderInput, type SplitTableCellInput, type TableColumnInput, type TableRowInput, type ToggleTableCellBorderInput } from "smartrte-core";
|
|
2
|
+
export type DomTableCommand = {
|
|
3
|
+
id: "table.row.add" | "table.row.remove";
|
|
4
|
+
input: Omit<TableRowInput, "tablePath">;
|
|
5
|
+
} | {
|
|
6
|
+
id: "table.column.add" | "table.column.remove";
|
|
7
|
+
input: Omit<TableColumnInput, "tablePath">;
|
|
8
|
+
} | {
|
|
9
|
+
id: "table.cell.merge";
|
|
10
|
+
input: Omit<MergeTableCellsInput, "tablePath">;
|
|
11
|
+
} | {
|
|
12
|
+
id: "table.cell.split";
|
|
13
|
+
input: Omit<SplitTableCellInput, "tablePath">;
|
|
14
|
+
} | {
|
|
15
|
+
id: "table.header.cell.toggle" | "table.header.row.toggle" | "table.header.column.toggle";
|
|
16
|
+
input: Omit<TableHeaderInput, "tablePath">;
|
|
17
|
+
} | {
|
|
18
|
+
id: "table.cell.style.set";
|
|
19
|
+
input: Omit<SetTableCellStyleInput, "tablePath">;
|
|
20
|
+
} | {
|
|
21
|
+
id: "table.column.width.set";
|
|
22
|
+
input: Omit<SetTableColumnWidthInput, "tablePath">;
|
|
23
|
+
} | {
|
|
24
|
+
id: "table.row.height.set";
|
|
25
|
+
input: Omit<SetTableRowHeightInput, "tablePath">;
|
|
26
|
+
} | {
|
|
27
|
+
id: "table.cell.border.toggle";
|
|
28
|
+
input: Omit<ToggleTableCellBorderInput, "tablePath">;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Executes a core table command while replacing only the affected table.
|
|
32
|
+
* Visual table/cell attributes are transferred by logical grid position.
|
|
33
|
+
*/
|
|
34
|
+
export declare const executeDomTableCommand: (table: HTMLTableElement, command: DomTableCommand) => HTMLTableElement | null;
|
|
35
|
+
export declare const executeDomTableRemoval: (table: HTMLTableElement) => boolean;
|