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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5
|
|
4
|
+
|
|
5
|
+
- Remove the always-on drag handle that appeared next to every line, list item, and table.
|
|
6
|
+
- Keep list-style and blockquote changes scoped to the selection instead of bleeding into unrelated parts of the document.
|
|
7
|
+
- Fix blockquote and code block corrupting each other, or losing content, when toggled in sequence without reselecting text in between.
|
|
8
|
+
- Preserve code blocks and blockquotes nested inside each other, including on list items, instead of one silently stripping the other.
|
|
9
|
+
- Fix the toolbar's active-state indicators (and the code block toggle itself) going stale on list items after removing an enclosing blockquote.
|
|
10
|
+
- Stop pasted or existing blockquotes/code blocks from losing their border and internal spacing when the source HTML carries conflicting inline styles.
|
|
11
|
+
|
|
3
12
|
## 0.3.4
|
|
4
13
|
|
|
5
14
|
- Make foreground and background colours deterministic across paragraphs, lists, and table-cell text selections.
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ pnpm add smartrte-react
|
|
|
41
41
|
## 🚀 Quick Start
|
|
42
42
|
|
|
43
43
|
### 🎮 Live Demo
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
Try the editor instantly in your browser:
|
|
46
46
|
|
|
47
47
|
- **[Live Demo](https://playground-k9l44ah7t-ayush1852017s-projects.vercel.app/)** (Deployed Version)
|
|
@@ -88,10 +88,6 @@ export default App;
|
|
|
88
88
|
| `media` | `boolean` | `true` | Enable/disable media/image functionality |
|
|
89
89
|
| `formula` | `boolean` | `true` | Enable/disable formula/LaTeX functionality |
|
|
90
90
|
| `mediaManager` | `MediaManagerAdapter` | `undefined` | Custom media manager for handling images |
|
|
91
|
-
| `fonts` | `{ name: string; value: string }[]` | Default web-safe fonts | Custom font list for the toolbar |
|
|
92
|
-
| `defaultFont` | `string` | `undefined` | Default font family for the editor content |
|
|
93
|
-
| `theme` | `"light" \| "dark"` | `"light"` | Built-in theme mode |
|
|
94
|
-
| `className` | `string` | `undefined` | Custom CSS class for theming via CSS variable overrides |
|
|
95
91
|
|
|
96
92
|
### Advanced Examples
|
|
97
93
|
|
|
@@ -318,87 +314,23 @@ const myMediaManager: MediaManagerAdapter = {
|
|
|
318
314
|
};
|
|
319
315
|
```
|
|
320
316
|
|
|
321
|
-
## 🎨
|
|
322
|
-
|
|
323
|
-
The editor uses CSS custom properties (CSS variables) for all colors, making it fully customizable. No hardcoded colors — everything can be themed.
|
|
324
|
-
|
|
325
|
-
### Quick Start: Dark Mode
|
|
326
|
-
|
|
327
|
-
```tsx
|
|
328
|
-
<ClassicEditor theme="dark" onChange={handleChange} />
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### Custom Themes via CSS
|
|
332
|
-
|
|
333
|
-
Apply a custom class and override any CSS variables:
|
|
334
|
-
|
|
335
|
-
```tsx
|
|
336
|
-
<ClassicEditor className="my-theme" onChange={handleChange} />
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
```css
|
|
340
|
-
.my-theme {
|
|
341
|
-
--srte-bg: #1a1a2e;
|
|
342
|
-
--srte-text: #eaeaea;
|
|
343
|
-
--srte-border: #3a3a5c;
|
|
344
|
-
/* Override only the variables you need */
|
|
345
|
-
}
|
|
346
|
-
```
|
|
317
|
+
## 🎨 Styling
|
|
347
318
|
|
|
348
|
-
|
|
319
|
+
The editor comes with built-in styles. You can customize the appearance by wrapping it in a container:
|
|
349
320
|
|
|
350
321
|
```tsx
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
322
|
+
<div style={{
|
|
323
|
+
border: '1px solid #ddd',
|
|
324
|
+
borderRadius: '8px',
|
|
325
|
+
overflow: 'hidden',
|
|
326
|
+
}}>
|
|
327
|
+
<ClassicEditor
|
|
328
|
+
value={content}
|
|
329
|
+
onChange={setContent}
|
|
330
|
+
/>
|
|
331
|
+
</div>
|
|
354
332
|
```
|
|
355
333
|
|
|
356
|
-
Or purely via CSS (without the `theme` prop):
|
|
357
|
-
|
|
358
|
-
```css
|
|
359
|
-
@media (prefers-color-scheme: dark) {
|
|
360
|
-
.srte-editor {
|
|
361
|
-
--srte-bg: #1e1e1e;
|
|
362
|
-
--srte-text: #e0e0e0;
|
|
363
|
-
--srte-border: #3a3a3a;
|
|
364
|
-
/* ... */
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
### Available CSS Custom Properties
|
|
370
|
-
|
|
371
|
-
| Variable | Default (Light) | Description |
|
|
372
|
-
|---|---|---|
|
|
373
|
-
| `--srte-bg` | `#ffffff` | Editor container & content background |
|
|
374
|
-
| `--srte-text` | `#111111` | Primary UI text color |
|
|
375
|
-
| `--srte-text-muted` | `#4b5563` | Secondary/subtle text |
|
|
376
|
-
| `--srte-border` | `#dddddd` | Standard border color |
|
|
377
|
-
| `--srte-border-light` | `#eeeeee` | Light separator borders |
|
|
378
|
-
| `--srte-toolbar-bg` | `#ffffff` | Toolbar background |
|
|
379
|
-
| `--srte-input-bg` | `#ffffff` | Button, input, select backgrounds |
|
|
380
|
-
| `--srte-input-text` | `#111111` | Button, input, select text |
|
|
381
|
-
| `--srte-input-border` | `#e5e7eb` | Button, input, select borders |
|
|
382
|
-
| `--srte-modal-backdrop` | `rgba(0,0,0,0.35)` | Modal overlay background |
|
|
383
|
-
| `--srte-modal-bg` | `#ffffff` | Modal/dialog background |
|
|
384
|
-
| `--srte-modal-text` | `#000000` | Modal text color |
|
|
385
|
-
| `--srte-menu-bg` | `#ffffff` | Context menu background |
|
|
386
|
-
| `--srte-menu-text` | `#111111` | Context menu text |
|
|
387
|
-
| `--srte-menu-shadow` | `0 8px 24px rgba(0,0,0,0.18)` | Menu box-shadow |
|
|
388
|
-
| `--srte-accent` | `#1e90ff` | Accent/selection color |
|
|
389
|
-
| `--srte-accent-bg` | `rgba(30,144,255,0.15)` | Accent background (translucent) |
|
|
390
|
-
| `--srte-danger` | `#dc2626` | Destructive action color |
|
|
391
|
-
| `--srte-primary` | `#2563eb` | Primary action button color |
|
|
392
|
-
| `--srte-surface-subtle` | `#f3f4f6` | Subtle surface (dropzone, preset buttons) |
|
|
393
|
-
| `--srte-on-primary` | `#ffffff` | Text on primary/danger buttons |
|
|
394
|
-
| `--srte-cancel-bg` | `#f3f4f6` | Cancel button background |
|
|
395
|
-
|
|
396
|
-
### Important Notes
|
|
397
|
-
|
|
398
|
-
- **User content colors are preserved** — colors set via the color picker (text/background) are inline styles on content elements and are not affected by theming.
|
|
399
|
-
- **Color picker swatches are not themed** — they display actual color values regardless of theme.
|
|
400
|
-
- **The theme only affects editor chrome** — toolbar, dialogs, menus, and container. User content remains unchanged.
|
|
401
|
-
|
|
402
334
|
## 🛠️ Development
|
|
403
335
|
|
|
404
336
|
### Prerequisites
|
|
@@ -694,14 +626,13 @@ See the list of [contributors](https://github.com/ayush1852017/smart-rte/contrib
|
|
|
694
626
|
|
|
695
627
|
## 🗺️ Roadmap
|
|
696
628
|
|
|
697
|
-
### Current Version (0.
|
|
629
|
+
### Current Version (0.1.x)
|
|
698
630
|
|
|
699
631
|
- ✅ Rich text editing
|
|
700
632
|
- ✅ Table support
|
|
701
633
|
- ✅ Formula support (LaTeX/KaTeX)
|
|
702
634
|
- ✅ Media management
|
|
703
635
|
- ✅ TypeScript support
|
|
704
|
-
- ✅ Dark mode & theming (CSS custom properties)
|
|
705
636
|
|
|
706
637
|
### Upcoming Features
|
|
707
638
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { inlineToolDeclarations, type Attrs, type SmartMark } from "smartrte-core";
|
|
2
|
+
export type CanonicalInlineToolId = typeof inlineToolDeclarations[number]["id"];
|
|
3
|
+
export type CanonicalInlineIntent = "apply" | "remove" | "toggle" | "setAttrs" | "clearAll" | "editLink";
|
|
4
|
+
export interface CanonicalInlineExecution {
|
|
5
|
+
readonly changed: boolean;
|
|
6
|
+
readonly operations: number;
|
|
7
|
+
readonly coverage: "all" | "partial" | "none";
|
|
8
|
+
}
|
|
9
|
+
export declare const executeCanonicalInlineTool: (root: HTMLElement, id: CanonicalInlineToolId, intent?: CanonicalInlineIntent, attrs?: Attrs) => CanonicalInlineExecution;
|
|
10
|
+
/** Consumes adapter stored marks for ordinary text input. Composition remains
|
|
11
|
+
* owned by the canonical surface; atom-aware composition is explicitly Phase 7. */
|
|
12
|
+
export declare const installCanonicalInlineStoredMarkInput: (root: HTMLElement) => (() => void);
|
|
13
|
+
export declare const canonicalInlineStoredMarks: (root: HTMLElement) => readonly SmartMark[];
|
|
14
|
+
export declare const hasCanonicalInlineStoredMarkOverride: (root: HTMLElement) => boolean;
|
|
15
|
+
export declare const describeCanonicalInlineCoverage: (root: HTMLElement) => Readonly<Record<string, "all" | "partial">>;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { applyMarkCommand, applyOperations, clearAllMarksCommand, createFoundationEditor, createNodeId, createScopeIndex, editLinkCommand, executeMarkTool, foundationSchema, inlineToolDeclarations, parseCanonicalListHtml, removeLinkCommand, removeMarkCommand, serializeCanonicalListHtml, toggleMarkCommand, } from "smartrte-core";
|
|
2
|
+
const storedMarks = new WeakMap();
|
|
3
|
+
const ownerSelector = "p,h1,h2,h3,h4,h5,h6";
|
|
4
|
+
const ownerId = (owner) => {
|
|
5
|
+
var _a;
|
|
6
|
+
(_a = owner.dataset).smartId || (_a.smartId = createNodeId());
|
|
7
|
+
return owner.dataset.smartId;
|
|
8
|
+
};
|
|
9
|
+
const contentWidth = (node) => {
|
|
10
|
+
if (node.nodeType === node.TEXT_NODE)
|
|
11
|
+
return node.nodeValue?.length || 0;
|
|
12
|
+
if (!(node instanceof HTMLElement))
|
|
13
|
+
return [...node.childNodes].reduce((sum, child) => sum + contentWidth(child), 0);
|
|
14
|
+
if (node.closest("[data-smart-ui]"))
|
|
15
|
+
return 0;
|
|
16
|
+
if (node.matches("br,[data-smart-atomic='true']"))
|
|
17
|
+
return 1;
|
|
18
|
+
return [...node.childNodes].reduce((sum, child) => sum + contentWidth(child), 0);
|
|
19
|
+
};
|
|
20
|
+
const pointInOwner = (root, node, offset) => {
|
|
21
|
+
const element = node instanceof HTMLElement ? node : node?.parentElement;
|
|
22
|
+
const owner = element?.closest(ownerSelector);
|
|
23
|
+
if (!owner || !root.contains(owner) || !node)
|
|
24
|
+
return null;
|
|
25
|
+
const range = root.ownerDocument.createRange();
|
|
26
|
+
range.selectNodeContents(owner);
|
|
27
|
+
try {
|
|
28
|
+
range.setEnd(node, offset);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { ownerId: ownerId(owner), offset: contentWidth(range.cloneContents()) };
|
|
34
|
+
};
|
|
35
|
+
const captureSelection = (root) => {
|
|
36
|
+
const native = root.ownerDocument.defaultView?.getSelection();
|
|
37
|
+
if (!native?.anchorNode || !native.focusNode)
|
|
38
|
+
return null;
|
|
39
|
+
const anchor = pointInOwner(root, native.anchorNode, native.anchorOffset);
|
|
40
|
+
const head = pointInOwner(root, native.focusNode, native.focusOffset);
|
|
41
|
+
return anchor && head ? { anchor, head } : null;
|
|
42
|
+
};
|
|
43
|
+
const domPointAt = (owner, requested) => {
|
|
44
|
+
let remaining = Math.max(0, requested);
|
|
45
|
+
const visit = (parent) => {
|
|
46
|
+
for (let index = 0; index < parent.childNodes.length; index += 1) {
|
|
47
|
+
const child = parent.childNodes[index];
|
|
48
|
+
if (child.nodeType === child.TEXT_NODE) {
|
|
49
|
+
const length = child.nodeValue?.length || 0;
|
|
50
|
+
if (remaining <= length)
|
|
51
|
+
return { node: child, offset: remaining };
|
|
52
|
+
remaining -= length;
|
|
53
|
+
}
|
|
54
|
+
else if (child instanceof HTMLElement && child.matches("br,[data-smart-atomic='true']")) {
|
|
55
|
+
if (remaining === 0)
|
|
56
|
+
return { node: parent, offset: index };
|
|
57
|
+
if (remaining === 1)
|
|
58
|
+
return { node: parent, offset: index + 1 };
|
|
59
|
+
remaining -= 1;
|
|
60
|
+
}
|
|
61
|
+
else if (!(child instanceof HTMLElement && child.closest("[data-smart-ui]"))) {
|
|
62
|
+
const found = visit(child);
|
|
63
|
+
if (found)
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
return visit(owner) || { node: owner, offset: owner.childNodes.length };
|
|
70
|
+
};
|
|
71
|
+
const restoreSelection = (root, snapshot) => {
|
|
72
|
+
const byId = (id) => Array.from(root.querySelectorAll("[data-smart-id]"))
|
|
73
|
+
.find((node) => node.matches(ownerSelector) && node.dataset.smartId === id) || null;
|
|
74
|
+
const anchorOwner = byId(snapshot.anchor.ownerId);
|
|
75
|
+
const headOwner = byId(snapshot.head.ownerId);
|
|
76
|
+
const native = root.ownerDocument.defaultView?.getSelection();
|
|
77
|
+
if (!anchorOwner || !headOwner || !native)
|
|
78
|
+
return;
|
|
79
|
+
const anchor = domPointAt(anchorOwner, snapshot.anchor.offset);
|
|
80
|
+
const head = domPointAt(headOwner, snapshot.head.offset);
|
|
81
|
+
native.setBaseAndExtent(anchor.node, anchor.offset, head.node, head.offset);
|
|
82
|
+
};
|
|
83
|
+
const selectedOwners = (root, snapshot) => {
|
|
84
|
+
const owners = Array.from(root.querySelectorAll(ownerSelector));
|
|
85
|
+
const indexes = [snapshot.anchor.ownerId, snapshot.head.ownerId].map((id) => owners.findIndex((owner) => ownerId(owner) === id));
|
|
86
|
+
if (indexes.some((index) => index < 0))
|
|
87
|
+
return [];
|
|
88
|
+
const [from, to] = [Math.min(...indexes), Math.max(...indexes)];
|
|
89
|
+
return owners.slice(from, to + 1);
|
|
90
|
+
};
|
|
91
|
+
const modelSelection = (document, snapshot) => {
|
|
92
|
+
const positions = createScopeIndex().positions(document, foundationSchema);
|
|
93
|
+
const point = (value) => {
|
|
94
|
+
const content = positions.contentRangeOf(value.ownerId);
|
|
95
|
+
return content ? { path: [...content.from.path], offset: value.offset } : null;
|
|
96
|
+
};
|
|
97
|
+
const anchor = point(snapshot.anchor);
|
|
98
|
+
const head = point(snapshot.head);
|
|
99
|
+
return anchor && head ? { type: "text", anchor, head } : null;
|
|
100
|
+
};
|
|
101
|
+
const parseOwners = (owners) => parseCanonicalListHtml(owners.map((owner) => owner.outerHTML).join(""));
|
|
102
|
+
const renderOwners = (root, owners, document, preserveEditedLink = false) => {
|
|
103
|
+
const template = root.ownerDocument.createElement("template");
|
|
104
|
+
template.innerHTML = serializeCanonicalListHtml(document, { fragment: true });
|
|
105
|
+
owners.forEach((owner) => {
|
|
106
|
+
const id = ownerId(owner);
|
|
107
|
+
const replacement = Array.from(template.content.querySelectorAll("[data-smart-id]"))
|
|
108
|
+
.find((candidate) => candidate.dataset.smartId === id);
|
|
109
|
+
if (replacement) {
|
|
110
|
+
const existingLinks = owner.querySelectorAll("a");
|
|
111
|
+
const replacementLinks = replacement.querySelectorAll("a");
|
|
112
|
+
if (preserveEditedLink && existingLinks.length === 1 && replacementLinks.length === 1
|
|
113
|
+
&& existingLinks[0].textContent === replacementLinks[0].textContent
|
|
114
|
+
&& owner.textContent === replacement.textContent) {
|
|
115
|
+
const next = replacementLinks[0];
|
|
116
|
+
existingLinks[0].setAttribute("href", next.getAttribute("href") || "");
|
|
117
|
+
if (next.target) {
|
|
118
|
+
existingLinks[0].target = next.target;
|
|
119
|
+
existingLinks[0].rel = "noopener noreferrer";
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
existingLinks[0].removeAttribute("target");
|
|
123
|
+
existingLinks[0].removeAttribute("rel");
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
owner.innerHTML = replacement.innerHTML;
|
|
128
|
+
owner.querySelectorAll("span[style]").forEach((span) => {
|
|
129
|
+
const color = span.style.color;
|
|
130
|
+
const background = span.style.backgroundColor;
|
|
131
|
+
const size = span.style.fontSize;
|
|
132
|
+
const family = span.style.fontFamily;
|
|
133
|
+
span.removeAttribute("style");
|
|
134
|
+
if (color)
|
|
135
|
+
span.style.color = color;
|
|
136
|
+
if (background)
|
|
137
|
+
span.style.backgroundColor = background;
|
|
138
|
+
if (size)
|
|
139
|
+
span.style.fontSize = size;
|
|
140
|
+
if (family)
|
|
141
|
+
span.style.fontFamily = family;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
const declarationFor = (id) => {
|
|
147
|
+
const declaration = inlineToolDeclarations.find((tool) => tool.id === id);
|
|
148
|
+
if (!declaration)
|
|
149
|
+
throw new Error(`Unknown inline tool "${id}".`);
|
|
150
|
+
return declaration;
|
|
151
|
+
};
|
|
152
|
+
export const executeCanonicalInlineTool = (root, id, intent = "toggle", attrs) => {
|
|
153
|
+
const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
|
|
154
|
+
const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
|
|
155
|
+
const cleanupTemporaryIds = () => ownerElements.forEach((owner) => {
|
|
156
|
+
if (!ownersWithPersistedIds.has(owner))
|
|
157
|
+
owner.removeAttribute("data-smart-id");
|
|
158
|
+
});
|
|
159
|
+
const snapshot = captureSelection(root);
|
|
160
|
+
if (!snapshot)
|
|
161
|
+
return { changed: false, operations: 0, coverage: "none" };
|
|
162
|
+
const owners = selectedOwners(root, snapshot);
|
|
163
|
+
if (!owners.length)
|
|
164
|
+
return { changed: false, operations: 0, coverage: "none" };
|
|
165
|
+
const document = parseOwners(owners);
|
|
166
|
+
const selection = modelSelection(document, snapshot);
|
|
167
|
+
if (!selection)
|
|
168
|
+
return { changed: false, operations: 0, coverage: "none" };
|
|
169
|
+
const declaration = declarationFor(id);
|
|
170
|
+
const index = createScopeIndex();
|
|
171
|
+
const scope = index.resolve(document, selection, { want: "inline-range" }, foundationSchema);
|
|
172
|
+
const description = index.resolve(document, selection, { want: "describe" }, foundationSchema);
|
|
173
|
+
const coverage = description.marks.some((entry) => entry.mark.type === declaration.markType && entry.coverage === "all")
|
|
174
|
+
? "all" : description.marks.some((entry) => entry.mark.type === declaration.markType) ? "partial" : "none";
|
|
175
|
+
if (scope.collapsed && !(declaration.markType === "link" && (intent === "remove" || intent === "editLink"))) {
|
|
176
|
+
const editor = createFoundationEditor({ document, selection, storedMarks: storedMarks.get(root) });
|
|
177
|
+
executeMarkTool(editor, declaration, intent, attrs);
|
|
178
|
+
storedMarks.set(root, [...(editor.storedMarks || [])]);
|
|
179
|
+
cleanupTemporaryIds();
|
|
180
|
+
return { changed: false, operations: 0, coverage };
|
|
181
|
+
}
|
|
182
|
+
const ctx = { schema: foundationSchema, positions: index.positions(document, foundationSchema) };
|
|
183
|
+
const operations = intent === "clearAll" ? clearAllMarksCommand(document, scope, {}, ctx)
|
|
184
|
+
: intent === "remove" && declaration.markType === "link" ? removeLinkCommand(document, scope, { markType: "link" }, ctx)
|
|
185
|
+
: intent === "editLink" && declaration.markType === "link" ? editLinkCommand(document, scope, attrs, ctx)
|
|
186
|
+
: intent === "remove" ? removeMarkCommand(document, scope, { markType: declaration.markType }, ctx)
|
|
187
|
+
: intent === "toggle" ? toggleMarkCommand(document, scope, { markType: declaration.markType, attrs, coverage }, ctx)
|
|
188
|
+
: applyMarkCommand(document, scope, { markType: declaration.markType, attrs }, ctx);
|
|
189
|
+
if (!operations.length)
|
|
190
|
+
return { changed: false, operations: 0, coverage };
|
|
191
|
+
const output = applyOperations(document, operations);
|
|
192
|
+
renderOwners(root, owners, output, intent === "editLink");
|
|
193
|
+
restoreSelection(root, snapshot);
|
|
194
|
+
cleanupTemporaryIds();
|
|
195
|
+
storedMarks.delete(root);
|
|
196
|
+
return { changed: true, operations: operations.length, coverage };
|
|
197
|
+
};
|
|
198
|
+
/** Consumes adapter stored marks for ordinary text input. Composition remains
|
|
199
|
+
* owned by the canonical surface; atom-aware composition is explicitly Phase 7. */
|
|
200
|
+
export const installCanonicalInlineStoredMarkInput = (root) => {
|
|
201
|
+
const beforeInput = (event) => {
|
|
202
|
+
if (!storedMarks.has(root) || event.inputType !== "insertText" || !event.data)
|
|
203
|
+
return;
|
|
204
|
+
const marks = storedMarks.get(root) || [];
|
|
205
|
+
const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
|
|
206
|
+
const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
|
|
207
|
+
const snapshot = captureSelection(root);
|
|
208
|
+
if (!snapshot || snapshot.anchor.ownerId !== snapshot.head.ownerId || snapshot.anchor.offset !== snapshot.head.offset)
|
|
209
|
+
return;
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
const owners = selectedOwners(root, snapshot);
|
|
212
|
+
const document = parseOwners(owners);
|
|
213
|
+
const selection = modelSelection(document, snapshot);
|
|
214
|
+
if (!selection)
|
|
215
|
+
return;
|
|
216
|
+
const editor = createFoundationEditor({ document, selection, storedMarks: marks });
|
|
217
|
+
if (marks.length)
|
|
218
|
+
editor.typeText(event.data, { timestamp: Date.now() });
|
|
219
|
+
else
|
|
220
|
+
editor.transact((builder) => {
|
|
221
|
+
builder.operations.push({ type: "insertText", pos: selection.head, text: event.data });
|
|
222
|
+
const next = { path: [...selection.head.path], offset: selection.head.offset + event.data.length };
|
|
223
|
+
builder.setSelection({ type: "text", anchor: next, head: next });
|
|
224
|
+
builder.setStoredMarks([]);
|
|
225
|
+
}, { source: "input", timestamp: Date.now(), addToHistory: true });
|
|
226
|
+
renderOwners(root, owners, editor.document);
|
|
227
|
+
const next = { anchor: { ...snapshot.anchor, offset: snapshot.anchor.offset + event.data.length }, head: { ...snapshot.head, offset: snapshot.head.offset + event.data.length } };
|
|
228
|
+
restoreSelection(root, next);
|
|
229
|
+
ownerElements.forEach((owner) => { if (!ownersWithPersistedIds.has(owner))
|
|
230
|
+
owner.removeAttribute("data-smart-id"); });
|
|
231
|
+
storedMarks.set(root, [...(editor.storedMarks || [])]);
|
|
232
|
+
};
|
|
233
|
+
const selectionChange = () => {
|
|
234
|
+
const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
|
|
235
|
+
const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
|
|
236
|
+
const snapshot = captureSelection(root);
|
|
237
|
+
if (!snapshot || snapshot.anchor.ownerId !== snapshot.head.ownerId || snapshot.anchor.offset !== snapshot.head.offset)
|
|
238
|
+
storedMarks.delete(root);
|
|
239
|
+
ownerElements.forEach((owner) => { if (!ownersWithPersistedIds.has(owner))
|
|
240
|
+
owner.removeAttribute("data-smart-id"); });
|
|
241
|
+
};
|
|
242
|
+
root.addEventListener("beforeinput", beforeInput);
|
|
243
|
+
root.ownerDocument.addEventListener("selectionchange", selectionChange);
|
|
244
|
+
return () => {
|
|
245
|
+
root.removeEventListener("beforeinput", beforeInput);
|
|
246
|
+
root.ownerDocument.removeEventListener("selectionchange", selectionChange);
|
|
247
|
+
storedMarks.delete(root);
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
export const canonicalInlineStoredMarks = (root) => storedMarks.get(root) || [];
|
|
251
|
+
export const hasCanonicalInlineStoredMarkOverride = (root) => storedMarks.has(root);
|
|
252
|
+
export const describeCanonicalInlineCoverage = (root) => {
|
|
253
|
+
if (storedMarks.has(root))
|
|
254
|
+
return Object.fromEntries((storedMarks.get(root) || []).map((mark) => [mark.type, "all"]));
|
|
255
|
+
const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
|
|
256
|
+
const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
|
|
257
|
+
const snapshot = captureSelection(root);
|
|
258
|
+
if (!snapshot)
|
|
259
|
+
return {};
|
|
260
|
+
const owners = selectedOwners(root, snapshot);
|
|
261
|
+
const document = parseOwners(owners);
|
|
262
|
+
const selection = modelSelection(document, snapshot);
|
|
263
|
+
const description = selection
|
|
264
|
+
? createScopeIndex().resolve(document, selection, { want: "describe" }, foundationSchema)
|
|
265
|
+
: null;
|
|
266
|
+
ownerElements.forEach((owner) => { if (!ownersWithPersistedIds.has(owner))
|
|
267
|
+
owner.removeAttribute("data-smart-id"); });
|
|
268
|
+
return Object.fromEntries((description?.marks || []).map((entry) => [entry.mark.type, entry.coverage]));
|
|
269
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type SmartSelection } from "smartrte-core";
|
|
2
|
+
export declare const executeCanonicalListDepth: (root: HTMLElement, items: readonly HTMLElement[], direction: "indent" | "outdent") => boolean;
|
|
3
|
+
export declare const executeCanonicalListMove: (root: HTMLElement, items: readonly HTMLElement[], direction: "up" | "down") => boolean;
|
|
4
|
+
export declare const executeCanonicalListStyle: (args: {
|
|
5
|
+
root: HTMLElement;
|
|
6
|
+
items: readonly HTMLElement[];
|
|
7
|
+
style?: string;
|
|
8
|
+
preset?: string;
|
|
9
|
+
checkable?: boolean;
|
|
10
|
+
}) => boolean;
|
|
11
|
+
export declare const executeCanonicalListCheck: (root: HTMLElement, item: HTMLElement, checked: boolean) => boolean;
|
|
12
|
+
export declare const executeCanonicalListToggle: (args: {
|
|
13
|
+
root: HTMLElement;
|
|
14
|
+
items: readonly HTMLElement[];
|
|
15
|
+
blocks: readonly HTMLElement[];
|
|
16
|
+
listTag: "ul" | "ol";
|
|
17
|
+
range?: Range;
|
|
18
|
+
style?: string;
|
|
19
|
+
preset?: string;
|
|
20
|
+
checkable?: boolean;
|
|
21
|
+
}) => boolean;
|
|
22
|
+
export interface CanonicalListBridgeResult {
|
|
23
|
+
readonly changed: boolean;
|
|
24
|
+
readonly selection: SmartSelection | null;
|
|
25
|
+
}
|
|
26
|
+
export declare const executeCanonicalListStructuralInput: (root: HTMLElement, input: "enter" | "backspace" | "delete") => boolean;
|