u2ui 1.5.3 → 1.5.7
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/el/ico/ico.js +5 -5
- package/el/rte/README.md +53 -13
- package/el/rte/manifest.json +19 -4
- package/el/rte/rte.css +31 -6
- package/el/rte/rte.js +174 -44
- package/el/rte0/README.md +37 -0
- package/el/rte0/manifest.json +33 -0
- package/el/rte0/rte0.css +9 -0
- package/el/rte0/rte0.js +56 -0
- package/el/system/styler.js +2 -0
- package/el/tooltip/tooltip.css +2 -2
- package/js/Placer/Placer.js +2 -1
- package/js/rte/AGENTS.md +142 -0
- package/js/rte/PLAN.md +487 -0
- package/js/rte/README.md +319 -25
- package/js/rte/blocks.js +7 -0
- package/js/rte/breaks.js +6 -0
- package/js/rte/classes.js +7 -0
- package/js/rte/docs/usage.md +845 -0
- package/js/rte/images.js +7 -0
- package/js/rte/link.js +8 -0
- package/js/rte/playground/README.md +95 -0
- package/js/rte/playground/hosts.html +553 -0
- package/js/rte/playground/index.html +835 -0
- package/js/rte/rte.js +56 -5
- package/js/rte/run.out +17 -0
- package/js/rte/source.js +12 -0
- package/js/rte/src/browser/README.md +40 -0
- package/js/rte/src/browser/interactive.js +17 -0
- package/js/rte/src/browser/range-rect.js +82 -0
- package/js/rte/src/client/README.md +385 -0
- package/js/rte/src/client/blocks.js +38 -0
- package/js/rte/src/client/breaks.js +212 -0
- package/js/rte/src/client/classes.js +48 -0
- package/js/rte/src/client/editor.js +472 -0
- package/js/rte/src/client/external-input.js +47 -0
- package/js/rte/src/client/history.js +13 -0
- package/js/rte/src/client/images.js +285 -0
- package/js/rte/src/client/link.js +567 -0
- package/js/rte/src/client/marks.js +25 -0
- package/js/rte/src/client/source.js +182 -0
- package/js/rte/src/client/structure.js +27 -0
- package/js/rte/src/client/tables.js +165 -0
- package/js/rte/src/client/unstyle.js +32 -0
- package/js/rte/src/command/README.md +520 -0
- package/js/rte/src/command/block-boundary.js +43 -0
- package/js/rte/src/command/block-style.js +120 -0
- package/js/rte/src/command/commands.js +159 -0
- package/js/rte/src/command/delete.js +119 -0
- package/js/rte/src/command/edit.js +78 -0
- package/js/rte/src/command/element.js +45 -0
- package/js/rte/src/command/enter.js +175 -0
- package/js/rte/src/command/fragment.js +76 -0
- package/js/rte/src/command/insert.js +52 -0
- package/js/rte/src/command/list.js +284 -0
- package/js/rte/src/command/mark.js +583 -0
- package/js/rte/src/command/pending-marks.js +216 -0
- package/js/rte/src/command/table.js +207 -0
- package/js/rte/src/command/unstyle.js +248 -0
- package/js/rte/src/config/README.md +96 -0
- package/js/rte/src/config/config.js +134 -0
- package/js/rte/src/core/README.md +63 -0
- package/js/rte/src/core/core.js +247 -0
- package/js/rte/src/history/README.md +83 -0
- package/js/rte/src/history/history.js +283 -0
- package/js/rte/src/input/README.md +173 -0
- package/js/rte/src/input/external-input.js +130 -0
- package/js/rte/src/input/input-pipeline.js +459 -0
- package/js/rte/src/mark/README.md +219 -0
- package/js/rte/src/mark/dom-adapter.js +100 -0
- package/js/rte/src/mark/mark.js +133 -0
- package/js/rte/src/mark/standard.js +76 -0
- package/js/rte/src/model/README.md +71 -0
- package/js/rte/src/model/content-model.js +213 -0
- package/js/rte/src/model/html/README.md +51 -0
- package/js/rte/src/model/html/html-model.js +90 -0
- package/js/rte/src/normalize/execute/README.md +33 -0
- package/js/rte/src/normalize/execute/repair-executor.js +129 -0
- package/js/rte/src/normalize/normalizer/README.md +48 -0
- package/js/rte/src/normalize/normalizer/normalizer.js +141 -0
- package/js/rte/src/normalize/repair/README.md +70 -0
- package/js/rte/src/normalize/repair/repair-planner.js +208 -0
- package/js/rte/src/sanitize/README.md +108 -0
- package/js/rte/src/sanitize/native.js +43 -0
- package/js/rte/src/sanitize/policy.js +206 -0
- package/js/rte/src/selection/README.md +39 -0
- package/js/rte/src/selection/map/README.md +53 -0
- package/js/rte/src/selection/map/point-map.js +307 -0
- package/js/rte/src/selection/ownership/README.md +32 -0
- package/js/rte/src/selection/ownership/ownership.js +42 -0
- package/js/rte/src/selection/point/README.md +34 -0
- package/js/rte/src/selection/point/point.js +83 -0
- package/js/rte/src/selection/range/README.md +38 -0
- package/js/rte/src/selection/range/edit-range.js +223 -0
- package/js/rte/src/selection/snapshot.js +64 -0
- package/js/rte/src/source/README.md +64 -0
- package/js/rte/src/source/source.js +193 -0
- package/js/rte/src/surface/README.md +36 -0
- package/js/rte/src/surface/content.js +16 -0
- package/js/rte/src/surface/surface.js +87 -0
- package/js/rte/src/transaction/README.md +35 -0
- package/js/rte/src/transaction/transaction.js +56 -0
- package/js/rte/src/ui/README.md +271 -0
- package/js/rte/src/ui/caret.js +17 -0
- package/js/rte/src/ui/chrome.js +214 -0
- package/js/rte/src/ui/handles.js +179 -0
- package/js/rte/src/ui/highlight.js +86 -0
- package/js/rte/src/ui/place.js +25 -0
- package/js/rte/src/ui/toolbar.js +258 -0
- package/js/rte/src/unstyle/README.md +103 -0
- package/js/rte/src/unstyle/unstyle.js +140 -0
- package/js/rte/tables.js +7 -0
- package/js/rte/unstyle.js +8 -0
- package/js/rte0/README.md +37 -0
- package/js/rte0/rte.js +7 -0
- package/js/{rte → rte0}/src/behavior.js +0 -1
- package/js/{rte → rte0}/src/events.js +0 -1
- package/js/{rte → rte0}/src/range.js +0 -2
- package/js/{rte → rte0}/src/ui.items.js +0 -2
- package/js/{rte → rte0}/src/ui.js +0 -1
- package/package.json +1 -1
- package/u2/Element.js +2 -2
- package/u2/plan-scoped-refactoring.md +133 -303
- package/u2/projects.json +18 -2
- package/u2/tools/update.repos.json.js +1 -1
- package/el/qrcode/package.json +0 -22
- /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
- /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
- /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/main.css +0 -0
- /package/js/{rte → rte0}/old/old.rte.js +0 -0
- /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
- /package/js/{rte → rte0}/src/fixes.js +0 -0
- /package/js/{rte → rte0}/src/rte.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
- /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
- /package/js/{rte → rte0}/src/util/indentHtml.js +0 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
A command is one named editing action, such as inserting a paragraph, making
|
|
4
|
+
text bold, or creating a link. The same command can be triggered by the
|
|
5
|
+
keyboard, an Input Event, or a toolbar.
|
|
6
|
+
|
|
7
|
+
`commands.js` stores the commands available in one editor. `edit.js` gives a
|
|
8
|
+
running command the selected content and the tools needed to change it safely.
|
|
9
|
+
`block-boundary.js` defines shared editor-empty and exact block-edge semantics.
|
|
10
|
+
`enter.js` contains Enter and line break; `delete.js` owns backward block
|
|
11
|
+
merging. `mark.js` creates generic commands for applying, removing, querying,
|
|
12
|
+
and toggling text formatting; `pending-marks.js` extends them across the next
|
|
13
|
+
ordinary text input at a caret.
|
|
14
|
+
|
|
15
|
+
## Command contract
|
|
16
|
+
|
|
17
|
+
A command is a plain object. Only `run` is required:
|
|
18
|
+
|
|
19
|
+
- `run(edit)` performs the change and returns whatever the command reports.
|
|
20
|
+
- `enabled(edit)` decides availability without a transaction. The default is
|
|
21
|
+
"the surface owns a range". It must not mutate anything.
|
|
22
|
+
- `state(edit)` optionally derives a UI-facing state without a transaction. It
|
|
23
|
+
must not mutate anything.
|
|
24
|
+
- `inputTypes` lists the native `beforeinput` types this command replaces.
|
|
25
|
+
- `transaction: false` declares a view-only action. It still uses availability,
|
|
26
|
+
state, and `u2-rte-command`, but receives `edit.transaction === null`, opens
|
|
27
|
+
no editing transaction, emits no content change, and never triggers command
|
|
28
|
+
cleanup. Editing commands omit the flag.
|
|
29
|
+
|
|
30
|
+
Commands are policy, not framework: an application may replace `enter` with its
|
|
31
|
+
own object under the same name, or register commands the engine never ships.
|
|
32
|
+
|
|
33
|
+
## Registry
|
|
34
|
+
|
|
35
|
+
- `add()` indexes `inputTypes`; re-registering a name drops its stale entries.
|
|
36
|
+
- `input(inputType)` answers which command replaces a native input type.
|
|
37
|
+
- `enabled(name, detail)` never opens a transaction.
|
|
38
|
+
- `state(name, detail)` returns a stateful command's state independently of
|
|
39
|
+
`enabled()`; a command returns `null` when no meaningful state exists.
|
|
40
|
+
- `run(name, detail)` normally opens one transaction with `{trigger: 'command',
|
|
41
|
+
command, inputType}`, checks availability again against the state the
|
|
42
|
+
transaction restored, executes, and emits `u2-rte-command` with that
|
|
43
|
+
transaction. A view command runs synchronously without one and reports a
|
|
44
|
+
`null` transaction in the event.
|
|
45
|
+
- An unknown command is a programming error and throws; an unavailable one
|
|
46
|
+
returns `undefined` without touching the DOM.
|
|
47
|
+
- `detail` is passed to the `Edit`; `range` targets a specific range instead of
|
|
48
|
+
the current selection, `inputType` and `data` retain the native input cause
|
|
49
|
+
and text payload, and `value` carries the command-specific value. The registry
|
|
50
|
+
deliberately does not constrain that value; its owning command validates it.
|
|
51
|
+
|
|
52
|
+
## Edit
|
|
53
|
+
|
|
54
|
+
- `range` is the `EditRange` to act on: the explicit target range if one was
|
|
55
|
+
given, otherwise the current selection, and otherwise the selection the
|
|
56
|
+
surface has saved — a field of the editor's own chrome holds the caret while
|
|
57
|
+
it names what is still selected in the surface. Foreign ranges resolve to
|
|
58
|
+
`null`.
|
|
59
|
+
- `map` is a fresh `PointMap`. Mutations that go through it keep every tracked
|
|
60
|
+
point, so a command can compute its resulting caret before it exists.
|
|
61
|
+
- `select(start, end, backward)` sets the resulting selection; one point
|
|
62
|
+
collapses it.
|
|
63
|
+
- `transaction` is `null` while availability is checked and a live transaction
|
|
64
|
+
during `run()`.
|
|
65
|
+
- `inputType` is the native operation name, `data` is its inserted text payload,
|
|
66
|
+
`value` is an optional command-specific scalar or structure, and `fragment` is a prepared
|
|
67
|
+
`DocumentFragment` or `null`. Commands never need to recover payloads from
|
|
68
|
+
the DOM.
|
|
69
|
+
- `config`, `model`, `element`, and `document` expose the host context so
|
|
70
|
+
commands never reach into the surface internals.
|
|
71
|
+
- `Commands.model` narrows its base model through the surface's current
|
|
72
|
+
`--u2-rte-elements` policy. Availability and execution therefore make the
|
|
73
|
+
same structural decision as cleanup.
|
|
74
|
+
- `edit.model` resolves that narrowing on first use. Reading the host
|
|
75
|
+
configuration is the expensive part and most availability checks never
|
|
76
|
+
consult the model, so a toolbar refresh does not pay for every control that
|
|
77
|
+
does not care. `narrow(model, elements)` is the one place applying the
|
|
78
|
+
policy; the input pipeline and the source view use it too.
|
|
79
|
+
|
|
80
|
+
## Enter
|
|
81
|
+
|
|
82
|
+
`enter` replaces `insertParagraph`, `lineBreak` replaces `insertLineBreak`.
|
|
83
|
+
Both act on a collapsed caret only: a selection would have to be deleted first,
|
|
84
|
+
and a caret inside atomic content has no structure to split, so those cases keep
|
|
85
|
+
their native behavior.
|
|
86
|
+
|
|
87
|
+
`--u2-rte-enter` names what Enter splits. `break` inserts a line break,
|
|
88
|
+
`block` splits the nearest text block declared by the content model, falling
|
|
89
|
+
back to the `--u2-rte-block` element for application policies that have not
|
|
90
|
+
classified it. `item`, `row`, and `cell` split the nearest `li`, `tr`, or
|
|
91
|
+
`td`/`th`. The split runs only where the content model allows a second element
|
|
92
|
+
of that kind beside the first; otherwise Enter falls back to a line break. That
|
|
93
|
+
keeps one algorithm for every host:
|
|
94
|
+
|
|
95
|
+
List structure takes precedence over a generic editor's default block: a caret
|
|
96
|
+
inside an `li` creates another `li` even when the editing host is a `div` whose
|
|
97
|
+
ordinary block is `p`. An explicit `--u2-rte-enter: break` still requests a line
|
|
98
|
+
break instead.
|
|
99
|
+
|
|
100
|
+
| Host | Caret in | Result |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `div` | `<p>one\|two</p>` | two paragraphs |
|
|
103
|
+
| `div` | `<h1>one\|two</h1>` | two headings |
|
|
104
|
+
| `div` | `<h1>title\|</h1>` | the heading followed by the default paragraph |
|
|
105
|
+
| `div` | `<div class=layout><p>one\|two</p></div>` | two paragraphs inside the wrapper |
|
|
106
|
+
| `ul` | `<li><p>one\|two</p></li>` | two list items |
|
|
107
|
+
| `div` | `<ul><li>one\|two</li></ul>` | two list items inside the list |
|
|
108
|
+
| `div` | `<td>one\|two</td>` | a break inside the cell |
|
|
109
|
+
| `p` | `one\|two` | a break |
|
|
110
|
+
|
|
111
|
+
Splitting keeps the inline context on both sides, never duplicates an `id`, and
|
|
112
|
+
leaves a `<br>` in a block the split emptied — an empty block has no caret
|
|
113
|
+
position of its own. A break at the end of its block gets the same treatment for
|
|
114
|
+
the same reason. At the exact end of a non-default text block, the trailing
|
|
115
|
+
empty half becomes a fresh `--u2-rte-block` element instead. With the defaults,
|
|
116
|
+
Enter therefore continues a heading in its middle but leaves it for a new `p`
|
|
117
|
+
at its end. The replacement is used only when the content model accepts both it
|
|
118
|
+
beside the original block and every cloned inline-context child inside it.
|
|
119
|
+
|
|
120
|
+
In `block` or `item` mode, Enter in an empty item exits a list nested inside the
|
|
121
|
+
surface. A middle item splits the list around the new default block; an edge
|
|
122
|
+
item removes only its empty half. The original list node and its `id` stay with
|
|
123
|
+
the surviving content, and split ordered lists continue their original
|
|
124
|
+
numbering, including `start`, `reversed`, and item `value`. A list that is itself
|
|
125
|
+
the editing surface cannot be exited, because commands never create content
|
|
126
|
+
outside their surface.
|
|
127
|
+
|
|
128
|
+
## Collapsed deletion
|
|
129
|
+
|
|
130
|
+
`deleteBackward` replaces `deleteContentBackward` only at the leading boundary
|
|
131
|
+
of a content-model `mergeable` block. `deleteForward` symmetrically replaces
|
|
132
|
+
`deleteContentForward` at its trailing boundary. Ordinary character deletion,
|
|
133
|
+
non-collapsed selections, atomic content, and an outer edge remain native.
|
|
134
|
+
Both commands keep the left wrapper and move the right block's children into
|
|
135
|
+
it. Nested mergeable blocks naturally choose the nearest valid boundary; at
|
|
136
|
+
the start of the first paragraph in a list item, for example, the paragraph has
|
|
137
|
+
no preceding sibling so the surrounding item may merge instead.
|
|
138
|
+
|
|
139
|
+
Every DOM position inside an otherwise empty block represents its one visual
|
|
140
|
+
caret. A filler `<br>` is therefore not a special case: positions before and
|
|
141
|
+
after it produce the same merge. Before becoming available, either command
|
|
142
|
+
checks that both blocks are mergeable and the left block's content rule permits
|
|
143
|
+
every moved child. The default HTML policy marks text blocks and list items as
|
|
144
|
+
mergeable; applications can opt any custom block in or out through their model.
|
|
145
|
+
Whitespace and comments between two otherwise adjacent blocks are neutral and
|
|
146
|
+
removed as part of the same mapped merge; meaningful text is never skipped.
|
|
147
|
+
|
|
148
|
+
An atomic block on the other side of the boundary is not merged: it holds
|
|
149
|
+
nothing to join, so it is removed and the caret stays where it already was. A
|
|
150
|
+
horizontal rule between two paragraphs therefore disappears on the first press
|
|
151
|
+
and the paragraphs merge on the second, which is what makes an inserted rule
|
|
152
|
+
removable at all — the browser's own deletion leaves it in place.
|
|
153
|
+
|
|
154
|
+
## Range marks
|
|
155
|
+
|
|
156
|
+
`applyMark(adapter, value)`, `removeMark(adapter, value)`, and
|
|
157
|
+
`toggleMark(adapter, value)` change a non-collapsed selection. They split only
|
|
158
|
+
its text and mark boundaries, map the selection through each mutation, and
|
|
159
|
+
preserve forward or backward direction.
|
|
160
|
+
|
|
161
|
+
Their state is `true` when all selected editable text has the concrete mark,
|
|
162
|
+
`false` when none has it, and `'mixed'` when only part has it. Atomic content and
|
|
163
|
+
nested editors do not contribute. Toggle removes only the fully active case;
|
|
164
|
+
inactive and mixed selections receive the mark throughout.
|
|
165
|
+
|
|
166
|
+
At a caret, state is `true` when its actual DOM position is inside a matching
|
|
167
|
+
inline wrapper and `false` otherwise. A boundary immediately outside that
|
|
168
|
+
wrapper stays outside; an empty matching wrapper still provides active context.
|
|
169
|
+
Base mark commands stay disabled at a caret. `PendingMarks.toggle()` adds caret
|
|
170
|
+
availability and stores only the difference from that structural state.
|
|
171
|
+
|
|
172
|
+
Application policy stays in the `MarkAdapter`. When its `reuse` policy accepts
|
|
173
|
+
a fully selected phrasing element, applying a class decorates that element
|
|
174
|
+
directly. Partially selected or bare text uses the adapter's canonical wrapper.
|
|
175
|
+
Blocks, atomic content, the editing host, and nested editable hosts are never
|
|
176
|
+
reused or crossed. Removal clears only the requested mark. An attributeless
|
|
177
|
+
`span` is unwrapped automatically; an adapter may also request removal of its
|
|
178
|
+
semantic wrapper. Unrelated attributes then survive on a neutral `span`.
|
|
179
|
+
|
|
180
|
+
Applying also joins adjacent canonical wrappers from the same adapter. Thus
|
|
181
|
+
`<span class="x">first</span><span class="x">second</span>` becomes one wrapper.
|
|
182
|
+
Elements that merely carry the mark in addition to other semantics, such as
|
|
183
|
+
`<b class="x">`, or have unrelated attributes are not merged.
|
|
184
|
+
|
|
185
|
+
The same mapped cleanup removes redundant nested copies and reaches a fixed
|
|
186
|
+
point after a merge exposes new siblings. A complete mark set additionally
|
|
187
|
+
orders exact canonical single-child wrappers by mark rank before merging them.
|
|
188
|
+
It never rotates an augmented wrapper, crosses atomic or nested editable
|
|
189
|
+
content, or creates a relationship rejected by the content model.
|
|
190
|
+
|
|
191
|
+
Applying a different value of an excluding type first removes that mark from
|
|
192
|
+
the selected part. A partial red span therefore becomes red/blue/red instead of
|
|
193
|
+
nesting conflicting wrappers.
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
const x = new MarkType('x');
|
|
197
|
+
const xHtml = new MarkAdapter(x, {
|
|
198
|
+
selector: '.x',
|
|
199
|
+
tag: 'span',
|
|
200
|
+
reuse: true,
|
|
201
|
+
write: element => element.classList.add('x'),
|
|
202
|
+
clear: element => element.classList.remove('x'),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
commands.add('applyX', applyMark(xHtml));
|
|
206
|
+
commands.add('removeX', removeMark(xHtml));
|
|
207
|
+
commands.add('toggleX', toggleMark(xHtml));
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
For one exact multi-mark operation, `setMarks(adapters)` creates a value-bearing
|
|
211
|
+
command:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const marks = setMarks([boldHtml, colorHtml, linkHtml]);
|
|
215
|
+
commands.add('marks', marks);
|
|
216
|
+
commands.run('marks', {value: [bold.create(), color.create('blue')]});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The adapter list is the closed universe for that command. It requires one
|
|
220
|
+
removable adapter per type, removes configured marks absent from the target,
|
|
221
|
+
resolves target conflicts through `markSet()`, and applies the canonical result
|
|
222
|
+
in one mapped transaction. Equivalent nested runs settle into the same wrapper
|
|
223
|
+
order and merge where their canonical structure permits it. Formatting from
|
|
224
|
+
unlisted adapters is preserved.
|
|
225
|
+
`state()` returns the current canonical array, `'mixed'`, or `null`; at a caret
|
|
226
|
+
it reports structural context while the command remains disabled.
|
|
227
|
+
|
|
228
|
+
For caret input, bind one lightweight `PendingMarks` instance to the surface.
|
|
229
|
+
Its toggle delegates range selections to the normal command and stores a caret
|
|
230
|
+
override only when needed. Its `insertText` command replaces native input only
|
|
231
|
+
while that override is current:
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
const pending = new PendingMarks(surface);
|
|
235
|
+
commands.add('toggleX', pending.toggle(xHtml));
|
|
236
|
+
commands.add('insertText', pending.insertText);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Moving the selection invalidates the override through the surface's existing
|
|
240
|
+
selection snapshot; no selection listener or observer is installed. Ordinary
|
|
241
|
+
text is inserted explicitly once, after which the resulting DOM carries the
|
|
242
|
+
state and native input resumes. IME composition always stays native: a live
|
|
243
|
+
start point follows its mutations, and the final composed range receives the
|
|
244
|
+
pending overrides only after `compositionend`. Canceled composition retains the
|
|
245
|
+
override. Plain-text surfaces, atomic content, and nested editors are excluded.
|
|
246
|
+
|
|
247
|
+
`PendingMarks.dispose()` removes its composition listeners and clears transient
|
|
248
|
+
state. Surface disconnection does this automatically.
|
|
249
|
+
|
|
250
|
+
The standard HTML policies compose these pieces without special commands:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
import {boldHtml, italicHtml, linkHtml} from './rte.js';
|
|
254
|
+
|
|
255
|
+
commands.add('bold', pending.toggle(boldHtml));
|
|
256
|
+
commands.add('italic', pending.toggle(italicHtml));
|
|
257
|
+
commands.add('link', pending.toggle(linkHtml, {href: '/docs'}));
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Bold, italic, underline, strike, code, and link use the same range and pending
|
|
261
|
+
input paths as custom marks. Their adapters recognize semantic aliases and emit
|
|
262
|
+
one canonical tag. Link values use `{href, target?, rel?, title?}`; URL policy
|
|
263
|
+
belongs to the application supplying that command value.
|
|
264
|
+
|
|
265
|
+
## Value marks
|
|
266
|
+
|
|
267
|
+
`valueMark(adapter)` is one command for a mark whose value is content rather
|
|
268
|
+
than a fixed choice. `edit.value` carries it, and a null value removes whatever
|
|
269
|
+
mark of that type is there, so a single control creates, changes, and removes a
|
|
270
|
+
link without a command per URL:
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
commands.add('link', valueMark(linkHtml));
|
|
274
|
+
commands.run('link', {value: {href: '/docs', target: '_blank'}});
|
|
275
|
+
commands.run('link', {value: {href: '/other'}});
|
|
276
|
+
commands.run('link');
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`state(edit)` returns the mark's value, `'mixed'` when the range carries several
|
|
280
|
+
values or is only partly marked, and null when it carries none.
|
|
281
|
+
|
|
282
|
+
A caret carries no range to mark, but it does sit on one: at a caret inside a
|
|
283
|
+
mark the command acts on that whole mark. That is what makes editing an existing
|
|
284
|
+
link's value possible without selecting its text first, and it applies to any
|
|
285
|
+
value mark, not just links.
|
|
286
|
+
|
|
287
|
+
## Block styles
|
|
288
|
+
|
|
289
|
+
`BlockStyles` defines one closed group of mutually exclusive text-block
|
|
290
|
+
representations. Its commands replace only matching wrappers and preserve their
|
|
291
|
+
children, unrelated attributes, mapped selection boundaries, and forward or
|
|
292
|
+
backward direction:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
const styles = new BlockStyles([
|
|
296
|
+
{name: 'paragraph', label: 'Paragraph', selector: 'p', tag: 'p'},
|
|
297
|
+
{name: 'h1', label: 'Heading 1', selector: 'h1', tag: 'h1'},
|
|
298
|
+
{
|
|
299
|
+
name: 'lead',
|
|
300
|
+
label: 'Lead',
|
|
301
|
+
selector: 'p.lead',
|
|
302
|
+
tag: 'p',
|
|
303
|
+
write: element => element.classList.add('lead'),
|
|
304
|
+
clear: element => element.classList.remove('lead'),
|
|
305
|
+
},
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
commands.add('blockStyle', styles.command());
|
|
309
|
+
commands.run('blockStyle', {value: 'h1'});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The group is deliberately closed: only an element matching one of its
|
|
313
|
+
selectors is a styleable text block. A surrounding `div`, list item, table cell,
|
|
314
|
+
or layout section is not converted merely because the content model classifies
|
|
315
|
+
it as a block. Later matching definitions refine earlier ones, so `p.lead`
|
|
316
|
+
wins over the base `p` definition. Switching styles calls every definition's
|
|
317
|
+
optional `clear()` on a detached replacement and then the target's `write()`;
|
|
318
|
+
unrelated classes and attributes survive.
|
|
319
|
+
|
|
320
|
+
A command is enabled only where the target wrapper is allowed by the active
|
|
321
|
+
content model and can contain all existing children. Known HTML headings work
|
|
322
|
+
with the default model. A custom element needs an application content-model
|
|
323
|
+
rule if the fallback cannot prove its block content valid. State is the active
|
|
324
|
+
style name, `'mixed'`, or `null` when the selection contains no styleable block.
|
|
325
|
+
One value-bearing command avoids traversing the same selected blocks once for
|
|
326
|
+
every represented style.
|
|
327
|
+
|
|
328
|
+
## Lists
|
|
329
|
+
|
|
330
|
+
`Lists` defines one closed group of list container elements. Item elements are
|
|
331
|
+
never named: they come from the model's `defaultChild`, so the same commands
|
|
332
|
+
serve any configured list-like structure.
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
const lists = new Lists(['ul', 'ol']);
|
|
336
|
+
|
|
337
|
+
commands.add('bullets', lists.toggle('ul'));
|
|
338
|
+
commands.add('numbers', lists.toggle('ol'));
|
|
339
|
+
commands.add('indent', lists.indent);
|
|
340
|
+
commands.add('outdent', lists.outdent);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`toggle(tag)` has one boolean state and three behaviors, decided by what the
|
|
344
|
+
selection already is:
|
|
345
|
+
|
|
346
|
+
- Plain blocks become items of a new list. The host's configured default block
|
|
347
|
+
becomes the item itself, so a paragraph turns into `<li>text</li>`; every
|
|
348
|
+
other block keeps its own element inside one, so a heading turns into
|
|
349
|
+
`<li><h2>…</h2></li>`.
|
|
350
|
+
- Items of another kind convert their container, preserving its attributes.
|
|
351
|
+
- Items of the same kind are lifted back out. An item contributes its own
|
|
352
|
+
blocks when the target accepts all of them, and otherwise becomes the host's
|
|
353
|
+
configured text block.
|
|
354
|
+
|
|
355
|
+
Selecting part of a list splits it, so only the selected run changes. A
|
|
356
|
+
resulting list that meets a list of its own kind becomes one list, so applying
|
|
357
|
+
to a paragraph below an existing list extends that list instead of starting a
|
|
358
|
+
second one.
|
|
359
|
+
|
|
360
|
+
`indent` moves a run of items into a nested list inside the previous item,
|
|
361
|
+
reusing one that is already there. `outdent` raises a run into the list that
|
|
362
|
+
owns its parent item, splitting that item so content after the nested list
|
|
363
|
+
stays behind, and lifts the run out of the list entirely at the top level.
|
|
364
|
+
Both claim the `formatIndent` and `formatOutdent` input types.
|
|
365
|
+
|
|
366
|
+
Every step is checked against the content model first, so a host that does not
|
|
367
|
+
allow the container, the item, or the text block keeps its control disabled
|
|
368
|
+
instead of producing invalid structure.
|
|
369
|
+
|
|
370
|
+
## One selected element
|
|
371
|
+
|
|
372
|
+
`selectedElement(edit, match)` is the element a selection covers when it covers
|
|
373
|
+
exactly that element and nothing else. Clicking an image inside a
|
|
374
|
+
`contenteditable` produces such a selection, which is what makes an atomic
|
|
375
|
+
element addressable at all.
|
|
376
|
+
|
|
377
|
+
`elementAttributes(names, {match})` is a value command over a fixed set of that
|
|
378
|
+
element's attributes:
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
commands.add('imageSize', elementAttributes(['width', 'height'], {
|
|
382
|
+
match: element => element.matches('img'),
|
|
383
|
+
}));
|
|
384
|
+
commands.run('imageSize', {value: {width: 320, height: 200}});
|
|
385
|
+
commands.run('imageSize');
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
A missing or empty value removes its attribute, so one command both sets a size
|
|
389
|
+
and clears it — which is what "back to its own size" means for an image. The
|
|
390
|
+
element stays selected afterwards, so a UI acting on it stays up.
|
|
391
|
+
|
|
392
|
+
## Tables
|
|
393
|
+
|
|
394
|
+
`Tables` groups the structural table actions. It names no section, row, or cell
|
|
395
|
+
tag: they come from the model's `defaultChild`, one level apart, so a configured
|
|
396
|
+
table-like structure works the same way.
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
const tables = new Tables();
|
|
400
|
+
for (const [name, command] of Object.entries(tables.commands)) commands.add(name, command);
|
|
401
|
+
commands.run('insertTable', {value: {rows: 3, columns: 2}});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Everything but `insertTable` needs the caret inside a cell. A new row copies the
|
|
405
|
+
kinds of cell its neighbour has, so a header row grows into header cells; a new
|
|
406
|
+
column takes its kind from the cell it sits beside in each row. Deleting the
|
|
407
|
+
last row or the last column takes the table with it, and the caret always lands
|
|
408
|
+
where the edit happened.
|
|
409
|
+
|
|
410
|
+
A cell spanning several rows or columns makes an index stop naming a column, so
|
|
411
|
+
the actions that count on one report themselves unavailable rather than shifting
|
|
412
|
+
the wrong cells. Row actions still work across a `colspan`, because a row is
|
|
413
|
+
still a row.
|
|
414
|
+
|
|
415
|
+
## Inserted elements
|
|
416
|
+
|
|
417
|
+
`insertNode(create, inputTypes)` inserts one prepared element at the selection.
|
|
418
|
+
The content model decides where it belongs: the block is split only as far as
|
|
419
|
+
the nearest container that accepts the element, so a block-level rule separates
|
|
420
|
+
paragraphs while an inline element stays inside its text. An empty half left by
|
|
421
|
+
the split receives a filler break so it keeps a caret position.
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
commands.add('rule', insertNode(document => document.createElement('hr'),
|
|
425
|
+
['insertHorizontalRule']));
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
A non-collapsed selection is not deleted first: the element lands where the
|
|
429
|
+
selection starts and the selected content stays after it. Insertion may not
|
|
430
|
+
depend on a caret, or a toolbar shown only for a selection would never offer it.
|
|
431
|
+
The same holds for `insertTable`.
|
|
432
|
+
|
|
433
|
+
## Unstyle
|
|
434
|
+
|
|
435
|
+
`unstyleCommand(policy, {blocks})` adapts the presentation policy in
|
|
436
|
+
[`../unstyle/`](../unstyle/README.md) to an editor selection. A collapsed caret
|
|
437
|
+
reaches everything the surface holds: someone who presses with nothing selected
|
|
438
|
+
wants the content cleaned, not nothing to happen, and the caret is put back
|
|
439
|
+
afterwards rather than the whole document being left selected. It
|
|
440
|
+
reports the first level that can change the selection and applies exactly that
|
|
441
|
+
level, so repeated presses become deliberately stronger only after the previous
|
|
442
|
+
level is already a no-op.
|
|
443
|
+
|
|
444
|
+
The command appends one rung of its own to the policy's ladder. `blocks` reduces
|
|
445
|
+
every selected structure to the host's default block: each outermost block
|
|
446
|
+
becomes one default block per unit of content it holds, so lists, tables,
|
|
447
|
+
quotes, and headings all collapse the same way without a single tag being named.
|
|
448
|
+
Atomic blocks are content and stay. It lives here rather than in the policy
|
|
449
|
+
because it needs the content model and mapped mutation, and because the policy
|
|
450
|
+
also runs on detached paste fragments where there is no structure to reduce.
|
|
451
|
+
Pass `{blocks: false}` for an action that only ever removes presentation.
|
|
452
|
+
|
|
453
|
+
Together the rungs mean the action is unavailable only when the selection is
|
|
454
|
+
already plain text in default blocks — someone who presses it wants something to
|
|
455
|
+
happen, and until then there is always something left to remove.
|
|
456
|
+
|
|
457
|
+
Inline boundaries are split only where needed, every mutation uses the point
|
|
458
|
+
map, and forward/backward selection intent survives. A block attribute changes
|
|
459
|
+
only when the entire block content is selected; a partial text selection never
|
|
460
|
+
restyles unselected text through its shared block. Nested editables remain hard
|
|
461
|
+
boundaries. The command owns native `formatRemove` when installed.
|
|
462
|
+
|
|
463
|
+
This explicit action is not automatic normalization. Command-triggered
|
|
464
|
+
normalization may repair the result afterwards, but it does not choose the
|
|
465
|
+
Unstyle strength.
|
|
466
|
+
|
|
467
|
+
## Prepared fragments
|
|
468
|
+
|
|
469
|
+
`insertFragment` replaces the current selection with an already prepared
|
|
470
|
+
`DocumentFragment` and collapses the caret after it. It never accepts or parses
|
|
471
|
+
an HTML string. The caller owns the ordered external-input stages: security
|
|
472
|
+
sanitizing, optional Unstyle cleanup, then this command.
|
|
473
|
+
|
|
474
|
+
Text boundaries are split through the point map and selected roots are removed
|
|
475
|
+
as mapped operations. An empty fragment therefore acts as explicit selected
|
|
476
|
+
range deletion. For insertion, the command asks the content model whether all
|
|
477
|
+
fragment children fit at the caret. If not, it lifts the boundary, splitting
|
|
478
|
+
only a non-edge ancestor, until it finds a valid context. One algorithm handles
|
|
479
|
+
an inline mark inside a paragraph, a paragraph pasted into a paragraph, an `li`
|
|
480
|
+
inside a list, and a link beside an existing link. Exact edge insertion does
|
|
481
|
+
not create an empty wrapper. Nested editable hosts are never removed.
|
|
482
|
+
|
|
483
|
+
Fragments from a template or another document are accepted because native DOM
|
|
484
|
+
insertion safely adopts their nodes without changing identity. This command
|
|
485
|
+
does not prove that a fragment is safe; passing unsanitized DOM is a caller
|
|
486
|
+
error.
|
|
487
|
+
|
|
488
|
+
## Invariants
|
|
489
|
+
|
|
490
|
+
- A command may declare a `shortcut`: one or more chords separated by spaces,
|
|
491
|
+
each `[modifier+]*key` with `ctrl` matching Control or Command. The registry
|
|
492
|
+
indexes them like input types, so keys belong to commands rather than to a
|
|
493
|
+
toolbar and work whether or not a control is on screen.
|
|
494
|
+
- `run(name, {trigger: 'input'})` marks a run as ongoing input rather than a
|
|
495
|
+
discrete action, so history groups a live control's edits — a field typed
|
|
496
|
+
into, a slider dragged — instead of recording a step per change.
|
|
497
|
+
- Commands run inside exactly one transaction and report their dirty nodes.
|
|
498
|
+
- A command never crosses its editing host and never splits the host itself.
|
|
499
|
+
- The resulting selection is set by the command, not guessed by the caller.
|
|
500
|
+
- Cleanup is not a command concern; the input pipeline normalizes on
|
|
501
|
+
`u2-rte-command` when `--u2-rte-clean-on` includes `command`.
|
|
502
|
+
|
|
503
|
+
## TODO
|
|
504
|
+
|
|
505
|
+
- Delete a selection first, then split, instead of leaving it native.
|
|
506
|
+
- Delete non-collapsed ranges explicitly where native DOM results are not
|
|
507
|
+
interoperable.
|
|
508
|
+
- Create the host's default block when Enter is pressed in content that has not
|
|
509
|
+
been wrapped into one yet.
|
|
510
|
+
- Let content policy provide the line separator instead of assuming `<br>`.
|
|
511
|
+
- Merge compatible mark wrappers across nested equivalent structures.
|
|
512
|
+
- Decide whether changing a value mark should keep the attributes its adapter
|
|
513
|
+
does not name, instead of writing the given value exactly.
|
|
514
|
+
- Publish availability and active state as observable state for UI adapters.
|
|
515
|
+
- Decide whether block styles should support an application-defined fallback
|
|
516
|
+
for loose text before normalization has created a text block.
|
|
517
|
+
- Preserve `start` and `value` numbering when a split or lift divides an
|
|
518
|
+
ordered list, the way Enter's list exit already does.
|
|
519
|
+
- Decide whether lifting an item should keep a nested list at its own level or
|
|
520
|
+
raise it with its parent.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {isEditingBoundary} from '../selection/ownership/ownership.js';
|
|
2
|
+
|
|
3
|
+
// Editor emptiness differs from textContent emptiness: a filler br is empty,
|
|
4
|
+
// while atomic content and nested editing boundaries are meaningful.
|
|
5
|
+
export function emptyBlock(node, model, root = node) {
|
|
6
|
+
if (node.nodeType === Node.TEXT_NODE) return !node.data.trim();
|
|
7
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return true;
|
|
8
|
+
if (node.localName === 'br') return true;
|
|
9
|
+
if (node !== root && (isEditingBoundary(node) || model.atomic(node))) return false;
|
|
10
|
+
return [...node.childNodes].every(child => emptyBlock(child, model, root));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function blockEdge(unit, point, edge) {
|
|
14
|
+
if (edge !== 'start' && edge !== 'end') throw new TypeError('Block edge must be start or end');
|
|
15
|
+
let {node, offset} = point;
|
|
16
|
+
while (node !== unit) {
|
|
17
|
+
const length = node.nodeType === Node.TEXT_NODE ? node.length : node.childNodes.length;
|
|
18
|
+
if (offset !== (edge === 'start' ? 0 : length)) return false;
|
|
19
|
+
const parent = node.parentNode;
|
|
20
|
+
if (!parent) return false;
|
|
21
|
+
const index = [...parent.childNodes].indexOf(node);
|
|
22
|
+
offset = index + (edge === 'end' ? 1 : 0);
|
|
23
|
+
node = parent;
|
|
24
|
+
}
|
|
25
|
+
return offset === (edge === 'start' ? 0 : unit.childNodes.length);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Emptiness for structural editing: a filler br already occupies its block, so
|
|
29
|
+
// unlike `emptyBlock` it counts as content and must not be doubled.
|
|
30
|
+
export function blank(model, node) {
|
|
31
|
+
if (node.nodeType === Node.TEXT_NODE) return !node.data.trim();
|
|
32
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return true;
|
|
33
|
+
if (model.atomic(node) || node.textContent.trim()) return false;
|
|
34
|
+
return ![...node.querySelectorAll('*')].some(element => model.atomic(element));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// An empty block has no caret position of its own until it holds a break.
|
|
38
|
+
export function fill(edit, element) {
|
|
39
|
+
if (!blank(edit.model, element)) return null;
|
|
40
|
+
const filler = edit.document.createElement('br');
|
|
41
|
+
edit.map.insert(element, element.childNodes.length, filler);
|
|
42
|
+
return filler;
|
|
43
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {Point} from '../selection/point/point.js';
|
|
2
|
+
|
|
3
|
+
const TAG = /^[a-z][a-z\d-]*$/;
|
|
4
|
+
|
|
5
|
+
// A closed group of mutually exclusive text-block representations. The group
|
|
6
|
+
// defines which existing elements are styleable, so layout containers are not
|
|
7
|
+
// mistaken for paragraphs merely because the content model calls both blocks.
|
|
8
|
+
export class BlockStyles {
|
|
9
|
+
#styles;
|
|
10
|
+
#byName;
|
|
11
|
+
|
|
12
|
+
constructor(styles) {
|
|
13
|
+
if (!Array.isArray(styles) || !styles.length) throw new TypeError('Block styles require a non-empty array');
|
|
14
|
+
this.#styles = Object.freeze(styles.map(style));
|
|
15
|
+
this.#byName = new Map(this.#styles.map(style => [style.name, style]));
|
|
16
|
+
if (this.#byName.size !== this.#styles.length) throw new RangeError('Block style names must be unique');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get styles() { return [...this.#styles]; }
|
|
20
|
+
|
|
21
|
+
command() {
|
|
22
|
+
return {
|
|
23
|
+
enabled: edit => this.#enabled(edit),
|
|
24
|
+
state: edit => this.#state(edit),
|
|
25
|
+
run: edit => this.#apply(edit, this.#byName.get(edit.value)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#enabled(edit) {
|
|
30
|
+
const blocks = this.#blocks(edit);
|
|
31
|
+
const target = edit.value === null ? null : this.#byName.get(edit.value);
|
|
32
|
+
if (edit.value !== null && !target) return false;
|
|
33
|
+
return blocks.some(block => target
|
|
34
|
+
? this.#render(edit, block, target)
|
|
35
|
+
: this.#styles.some(style => this.#render(edit, block, style))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#blocks(edit) {
|
|
40
|
+
if (!edit?.range) return [];
|
|
41
|
+
return edit.range.blocks(element => element !== edit.element && this.#current(element));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#current(element) {
|
|
45
|
+
let current = null;
|
|
46
|
+
for (const style of this.#styles) if (matches(style, element)) current = style;
|
|
47
|
+
return current;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#state(edit) {
|
|
51
|
+
const blocks = this.#blocks(edit);
|
|
52
|
+
if (!blocks.length) return null;
|
|
53
|
+
let value = null;
|
|
54
|
+
for (const block of blocks) {
|
|
55
|
+
const name = this.#current(block).name;
|
|
56
|
+
if (value !== null && value !== name) return 'mixed';
|
|
57
|
+
value = name;
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#apply(edit, target) {
|
|
63
|
+
if (!target) return [];
|
|
64
|
+
const blocks = this.#blocks(edit);
|
|
65
|
+
if (!blocks.length) return [];
|
|
66
|
+
const start = new Point(edit.range.start.node, edit.range.start.offset, 'forward');
|
|
67
|
+
const end = new Point(edit.range.end.node, edit.range.end.offset, 'backward');
|
|
68
|
+
const backward = !!edit.surface.selection?.backward;
|
|
69
|
+
edit.map.add(start).add(end);
|
|
70
|
+
const changed = [];
|
|
71
|
+
for (const block of blocks) {
|
|
72
|
+
const replacement = this.#render(edit, block, target);
|
|
73
|
+
if (!replacement || block.cloneNode(false).isEqualNode(replacement)) continue;
|
|
74
|
+
const parent = block.parentNode;
|
|
75
|
+
edit.map.replaceWrapper(block, replacement);
|
|
76
|
+
edit.transaction.touch(replacement).touch(parent);
|
|
77
|
+
changed.push(replacement);
|
|
78
|
+
}
|
|
79
|
+
edit.select(edit.map.get(start), edit.map.get(end), backward);
|
|
80
|
+
return changed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#render(edit, block, target) {
|
|
84
|
+
const replacement = edit.document.createElement(target.tag);
|
|
85
|
+
for (const attribute of block.attributes) replacement.setAttribute(attribute.name, attribute.value);
|
|
86
|
+
for (const style of this.#styles) style.clear?.(replacement);
|
|
87
|
+
target.write?.(replacement);
|
|
88
|
+
if (!edit.model.allows(block.parentNode, replacement)) return null;
|
|
89
|
+
if ([...block.childNodes].some(child => !edit.model.allows(replacement, child))) return null;
|
|
90
|
+
return replacement;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function style(value) {
|
|
95
|
+
if (!value || typeof value !== 'object') throw new TypeError('A block style must be an object');
|
|
96
|
+
for (const property of ['name', 'label', 'selector', 'tag']) {
|
|
97
|
+
if (typeof value[property] !== 'string' || !value[property].trim()) {
|
|
98
|
+
throw new TypeError(`A block style requires ${property}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (value.write !== undefined && typeof value.write !== 'function') {
|
|
102
|
+
throw new TypeError('Block style write must be a function');
|
|
103
|
+
}
|
|
104
|
+
if (value.clear !== undefined && typeof value.clear !== 'function') {
|
|
105
|
+
throw new TypeError('Block style clear must be a function');
|
|
106
|
+
}
|
|
107
|
+
if (!TAG.test(value.tag.toLowerCase())) throw new TypeError('A block style tag must be a usable tag name');
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
name: value.name.trim(),
|
|
110
|
+
label: value.label,
|
|
111
|
+
selector: value.selector,
|
|
112
|
+
tag: value.tag.toLowerCase(),
|
|
113
|
+
write: value.write || null,
|
|
114
|
+
clear: value.clear || null,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function matches(style, element) {
|
|
119
|
+
return element.matches(style.selector);
|
|
120
|
+
}
|