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,845 @@
|
|
|
1
|
+
# Using RTE
|
|
2
|
+
|
|
3
|
+
RTE is plain ESM with no build step and no dependencies. Choose the prototype
|
|
4
|
+
convention client for a minimal editor or `rte.js` for explicit composition.
|
|
5
|
+
|
|
6
|
+
## Minimal prototype
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import './rte.js';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```css
|
|
13
|
+
[contenteditable] {
|
|
14
|
+
--u2-rte: true;
|
|
15
|
+
--u2-rte-elements: @article;
|
|
16
|
+
--u2-rte-toolbar: undo redo bold italic bullets numbers rule;
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That one side-effect import lazily installs Enter, line break, structural
|
|
21
|
+
Backspace, normalization, pending text marks, undo and redo, the standard
|
|
22
|
+
inline marks, list and separator commands, and one shared roaming toolbar when
|
|
23
|
+
an opted-in rich-text host first receives focus. It does not scan the DOM or
|
|
24
|
+
create toolbar markup before then. `contenteditable="plaintext-only"` remains
|
|
25
|
+
native.
|
|
26
|
+
|
|
27
|
+
Ready control names are `undo`, `redo`, `bold`, `italic`, `underline`,
|
|
28
|
+
`strike`, `code`, `bullets`, `numbers`, `indent`, `outdent`, and `rule`, plus
|
|
29
|
+
`block`, `style`, `unstyle`, `breaks`, `link`, `source`,
|
|
30
|
+
`imageOriginal`, and `insertTable` from the optional root modules.
|
|
31
|
+
`--u2-rte-toolbar` chooses and orders them; the commands and their shortcuts
|
|
32
|
+
work whether or not a control is listed. List and separator controls consult
|
|
33
|
+
the content model, so `--u2-rte-elements` disables what a host does not allow.
|
|
34
|
+
|
|
35
|
+
Undo and redo work from Ctrl/Command+Z, Ctrl/Command+Shift+Z, and Ctrl/Command+Y
|
|
36
|
+
whether or not their controls are listed. History records every change of the
|
|
37
|
+
host's content, including edits an application makes itself, and groups
|
|
38
|
+
continuous typing into steps. See
|
|
39
|
+
[`../src/history/README.md`](../src/history/README.md).
|
|
40
|
+
|
|
41
|
+
## Keyboard shortcuts
|
|
42
|
+
|
|
43
|
+
A command declares its own key, so it works whether or not a control for it is
|
|
44
|
+
listed in `--u2-rte-toolbar`:
|
|
45
|
+
|
|
46
|
+
| keys | command |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `Ctrl+B` `Ctrl+I` `Ctrl+U` | bold, italic, underline |
|
|
49
|
+
| `Ctrl+Shift+X` `Ctrl+E` | strikethrough, code |
|
|
50
|
+
| `Ctrl+K` | link |
|
|
51
|
+
| `Ctrl+Shift+8` `Ctrl+Shift+7` | bulleted list, numbered list |
|
|
52
|
+
| `Tab` `Shift+Tab` | one list level in, one out |
|
|
53
|
+
| `Ctrl+\` | remove formatting |
|
|
54
|
+
| `Ctrl+H` | html source |
|
|
55
|
+
| `Ctrl+Z` `Ctrl+Y` `Ctrl+Shift+Z` | undo, redo |
|
|
56
|
+
|
|
57
|
+
`Ctrl` matches Command on Apple keyboards. A key only takes effect where its
|
|
58
|
+
command is available, so `Tab` outside a list still moves focus.
|
|
59
|
+
|
|
60
|
+
Custom commands declare theirs the same way, as one or more chords separated by
|
|
61
|
+
spaces:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
commands.add('highlight', {shortcut: 'ctrl+shift+h', run: edit => …});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A digit is matched by its physical key, so `ctrl+shift+8` is the same chord on
|
|
68
|
+
every keyboard layout.
|
|
69
|
+
|
|
70
|
+
## What a paste may bring in
|
|
71
|
+
|
|
72
|
+
`--u2-rte-elements` is what a host tolerates in content it already owns.
|
|
73
|
+
`--u2-rte-import-elements` is the narrower question of what may *arrive*, and it
|
|
74
|
+
defaults to the `@content` preset: headings, text, lists, tables, media, and the
|
|
75
|
+
text-level semantics that carry meaning — no layout, no embeds, no forms.
|
|
76
|
+
|
|
77
|
+
An element that preset does not carry, but whose meaning one of its own does, is
|
|
78
|
+
replaced rather than dropped. `<b>` becomes `<strong>` and `<i>` becomes `<em>`,
|
|
79
|
+
so the strict list costs no emphasis:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import {importAliases} from './src/input/input-pipeline.js';
|
|
83
|
+
new InputPipeline(surface, {aliases: {...importAliases, dfn: 'em'}});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Nothing rewrites `<b>` later on its own — the bold mark recognizes it, but only
|
|
87
|
+
running a mark command ever makes an element canonical.
|
|
88
|
+
|
|
89
|
+
## Removing formatting
|
|
90
|
+
|
|
91
|
+
`unstyle` walks a ladder that never runs out: foreign styles, presentational
|
|
92
|
+
attributes and classes, formatting wrappers, then the declared content classes,
|
|
93
|
+
the remaining semantic inline elements, and finally the structure itself. It is
|
|
94
|
+
unavailable only when the selection is already plain text in default blocks.
|
|
95
|
+
|
|
96
|
+
With nothing selected it reaches the whole content. Someone who presses with no
|
|
97
|
+
selection wants the document cleaned, not nothing to happen; the caret is put
|
|
98
|
+
back afterwards rather than the document being left selected.
|
|
99
|
+
|
|
100
|
+
## Content classes
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import './classes.js';
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```css
|
|
107
|
+
[contenteditable] {
|
|
108
|
+
--u2-rte-classes: lead, caption, brandColor;
|
|
109
|
+
--u2-rte-toolbar: style bold;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The style list offers exactly the declared names, per host. The group is one
|
|
114
|
+
mark type and therefore mutually exclusive; an application that needs
|
|
115
|
+
independent axes registers a second module with its own adapter.
|
|
116
|
+
|
|
117
|
+
That one declaration is the single source of truth for those names:
|
|
118
|
+
|
|
119
|
+
- the control offers them,
|
|
120
|
+
- the sanitizer keeps them and drops every other class from external HTML,
|
|
121
|
+
- remove-format and paste cleanup leave them alone, including the wrapper that
|
|
122
|
+
carries one, because a declared class is content rather than presentation.
|
|
123
|
+
|
|
124
|
+
A host that declares none keeps the control hidden and its class handling
|
|
125
|
+
unchanged.
|
|
126
|
+
|
|
127
|
+
## Links
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import './link.js';
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```css
|
|
134
|
+
[contenteditable] { --u2-rte-toolbar: bold link; }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The form is where the caret is: it appears on its own at a link and goes when the
|
|
138
|
+
caret leaves, the way the table and image handles do, so it never takes the focus
|
|
139
|
+
by appearing. `link` is therefore left with the one thing that is a decision:
|
|
140
|
+
turning a selection into a link — and, at a link, handing the keyboard to the
|
|
141
|
+
form. There is no Apply, the link is what the fields say as they are typed, and
|
|
142
|
+
removing one is emptying its address. Leaving puts the caret after the link, so
|
|
143
|
+
typing continues outside it. Beside the address sits a way to open it, whenever
|
|
144
|
+
the address is one a browser can follow. Relative paths, fragments, and application
|
|
145
|
+
schemes are accepted — the sanitizer decides which protocols survive, not the
|
|
146
|
+
form.
|
|
147
|
+
|
|
148
|
+
The command behind it is `valueMark(linkHtml)`, an ordinary command any UI can
|
|
149
|
+
drive:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
commands.run('link', {value: {href: '/docs', target: '_blank'}});
|
|
153
|
+
commands.run('link');
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Present or available
|
|
157
|
+
|
|
158
|
+
A control is **absent** when this editor does not offer it: its command is not
|
|
159
|
+
registered, `--u2-rte-toolbar` does not list it, or its choices are not
|
|
160
|
+
configured — a style select with no `--u2-rte-classes` has nothing to be. A
|
|
161
|
+
control that exists but cannot act on the current selection is **disabled**,
|
|
162
|
+
never hidden.
|
|
163
|
+
|
|
164
|
+
That keeps the toolbar's shape following the configuration rather than the
|
|
165
|
+
caret, so a control is always found in the same place.
|
|
166
|
+
|
|
167
|
+
```css
|
|
168
|
+
[contenteditable] { --u2-rte-toolbar-unavailable: hide; }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
trades that away for a toolbar that only ever shows what it can do. Worth it for
|
|
172
|
+
a compact toolbar; costly for a wide one, whose controls then move under the
|
|
173
|
+
pointer as the caret travels.
|
|
174
|
+
|
|
175
|
+
## The editor's own chrome
|
|
176
|
+
|
|
177
|
+
Everything the editor draws — the toolbar, the contextual handles, the link
|
|
178
|
+
form, the source dialog — lives in one shadow root of its own, marked
|
|
179
|
+
`[data-u2-rte-chrome=editor]`. The page's CSS cannot reach it and its styles
|
|
180
|
+
cannot leak out, which is what lets an editor survive being embedded in a
|
|
181
|
+
document that styles `button` for its own purposes.
|
|
182
|
+
|
|
183
|
+
That root is deliberately closed to styling: there is no `::part` surface,
|
|
184
|
+
because it would make the chrome's internal structure a public contract. An
|
|
185
|
+
application that wants different chrome builds its own on the commands — every
|
|
186
|
+
action is a plain command, and `Toolbar` binds markup you supply. Configuration
|
|
187
|
+
still crosses the boundary: custom properties inherit into it as usual.
|
|
188
|
+
|
|
189
|
+
## Images
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
import './images.js';
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```css
|
|
196
|
+
[contenteditable] { --u2-rte-toolbar: imageOriginal; }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Clicking an image selects it and frames it. Three handles sit on its trailing
|
|
200
|
+
edges, because the flow holds its top and start edges in place: the bottom-right
|
|
201
|
+
corner keeps the proportion, the right edge changes only the width and the
|
|
202
|
+
bottom edge only the height. `imageOriginal` clears the size again.
|
|
203
|
+
|
|
204
|
+
Below it stands its alt text, in one field, appearing and going with the frame
|
|
205
|
+
and never taking the focus by appearing. What an image says is part of the image,
|
|
206
|
+
and a form nobody opens is a form everybody fills in.
|
|
207
|
+
|
|
208
|
+
A resize writes `width` and `height` — the attributes the sanitize policy allows
|
|
209
|
+
on an image — and does so once, when the drag is released, so it is one undo
|
|
210
|
+
step. Changing one measurement alone stretches what the browser was given; an
|
|
211
|
+
application that would rather regenerate the file at that size reads the new
|
|
212
|
+
attributes from the ordinary `u2-rte-change` event and replaces the source.
|
|
213
|
+
|
|
214
|
+
`imageTools({selector, minimum})` makes any atomic element sizeable:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
editor.add(imageTools({selector: 'img, video, .widget'}));
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Tables
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import './tables.js';
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```css
|
|
227
|
+
[contenteditable] { --u2-rte-toolbar: insertTable; }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Putting the caret in a cell brings up handles on the table: rows down its left
|
|
231
|
+
edge, columns along its top, each lined up with that cell — add before, delete,
|
|
232
|
+
add after. Inserting a table stays a toolbar control because it applies where no
|
|
233
|
+
table is.
|
|
234
|
+
|
|
235
|
+
Every action is also an ordinary command, so any other UI can drive it:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
commands.run('insertTable', {value: {rows: 3, columns: 2}});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
A new row copies the kinds of cell its neighbour has, so a header row grows into
|
|
242
|
+
header cells. Deleting the last row or column takes the table with it. A cell
|
|
243
|
+
spanning several rows or columns makes the counted actions unavailable rather
|
|
244
|
+
than shifting the wrong cells.
|
|
245
|
+
|
|
246
|
+
## Editing the HTML directly
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
import './source.js';
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```css
|
|
253
|
+
[contenteditable] { --u2-rte-toolbar: bold source; }
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The `source` control opens the surface's HTML in a modal dialog, scrolled and
|
|
257
|
+
selected where the caret is. Applying parses the text through the configured
|
|
258
|
+
sanitizer — never through `innerHTML` — narrowed by `--u2-rte-elements`, and
|
|
259
|
+
lands as one undo step. Formatting only breaks lines where whitespace cannot be
|
|
260
|
+
significant, so reading and applying unchanged text is a round trip.
|
|
261
|
+
|
|
262
|
+
The text area is wrapped in `<u2-code>`, so the source is syntax highlighted
|
|
263
|
+
where that element is defined and a plain text area where it is not. This entry
|
|
264
|
+
loads it from `u2/el/code` on first use; nothing else in RTE depends on it.
|
|
265
|
+
|
|
266
|
+
`new Source(surface)` is the same responsibility without any UI: `read()`
|
|
267
|
+
returns `{html, start, end}` and `write(html)` replaces the content. The
|
|
268
|
+
composable `sourceView(options)` builds the dialog module without a highlighter
|
|
269
|
+
unless one is passed as `highlight`. See
|
|
270
|
+
[`../src/source/README.md`](../src/source/README.md).
|
|
271
|
+
|
|
272
|
+
This entry is intentionally a prototype. Unknown toolbar names remain hidden
|
|
273
|
+
until a module provides both a command and a control. Final default UI
|
|
274
|
+
ownership and complete viewport/writing-mode placement are still being designed
|
|
275
|
+
from this working path. See
|
|
276
|
+
[`../src/client/README.md`](../src/client/README.md) for its exact boundaries.
|
|
277
|
+
|
|
278
|
+
An optional command module can extend the imported singleton without scanning
|
|
279
|
+
or rebuilding existing editors:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
import {editor} from './rte.js';
|
|
283
|
+
import {formatModule} from './my-format-module.js';
|
|
284
|
+
|
|
285
|
+
editor.add(formatModule);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The module's command factory runs once for every existing and future rich-text
|
|
289
|
+
surface and may use that surface's shared pending-mark state. Optional
|
|
290
|
+
`setup()` and `attach()` hooks own editor-wide and per-surface resources; each
|
|
291
|
+
returns at most one object with `dispose()`. Removing the extension with
|
|
292
|
+
`editor.delete(formatModule)` removes its commands, controls, and resources
|
|
293
|
+
everywhere.
|
|
294
|
+
|
|
295
|
+
### Allowed content
|
|
296
|
+
|
|
297
|
+
The element policy is shared by cleanup and commands:
|
|
298
|
+
|
|
299
|
+
```css
|
|
300
|
+
.article-editor {
|
|
301
|
+
--u2-rte-elements: @article;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.short-answer {
|
|
305
|
+
--u2-rte-elements: p strong em a br;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Built-in `@basic`, `@article`, and `@document` presets are starting points.
|
|
310
|
+
Commands cannot create an element outside the resolved policy. Value controls
|
|
311
|
+
such as the block-style select hide individual targets that are not allowed at
|
|
312
|
+
the current selection; `--u2-rte-toolbar` still chooses and orders the remaining
|
|
313
|
+
controls.
|
|
314
|
+
|
|
315
|
+
This element list is only the structural half of an input policy. A sanitizer
|
|
316
|
+
must additionally decide permitted attributes, URL protocols, and values. For
|
|
317
|
+
example, allowing `a` structurally does not by itself make an arbitrary `href`
|
|
318
|
+
safe.
|
|
319
|
+
|
|
320
|
+
### Optional block styles
|
|
321
|
+
|
|
322
|
+
Add the ready Paragraph/H1/H2/H3 selector with one optional import:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
import './rte.js';
|
|
326
|
+
import './blocks.js';
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```css
|
|
330
|
+
[contenteditable] {
|
|
331
|
+
--u2-rte: true;
|
|
332
|
+
--u2-rte-toolbar: block bold;
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`blocks.js` imports `rte.js` itself, so the first import may be omitted when
|
|
337
|
+
block styles are always present. The separate form makes the base and optional
|
|
338
|
+
layers visible.
|
|
339
|
+
|
|
340
|
+
Applications replace the default module to add class-based or custom block
|
|
341
|
+
styles:
|
|
342
|
+
|
|
343
|
+
```js
|
|
344
|
+
import {editor} from './rte.js';
|
|
345
|
+
import {blockStyles, blocks, defaultBlockStyles} from './blocks.js';
|
|
346
|
+
|
|
347
|
+
editor.delete(blocks);
|
|
348
|
+
editor.add(blockStyles([...defaultBlockStyles, {
|
|
349
|
+
name: 'lead',
|
|
350
|
+
label: 'Lead',
|
|
351
|
+
selector: 'p.lead',
|
|
352
|
+
tag: 'p',
|
|
353
|
+
write: element => element.classList.add('lead'),
|
|
354
|
+
clear: element => element.classList.remove('lead'),
|
|
355
|
+
}]));
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Unrelated block attributes survive conversion. A selection spanning different
|
|
359
|
+
styles shows a mixed/empty value; choosing one applies it to every styleable
|
|
360
|
+
text block. Lists, layout containers, table structure, and nested editors are
|
|
361
|
+
not mistaken for paragraphs.
|
|
362
|
+
|
|
363
|
+
### Optional visible line breaks
|
|
364
|
+
|
|
365
|
+
`breaks.js` makes otherwise invisible `<br>` elements inspectable without
|
|
366
|
+
putting marker nodes into editable HTML:
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
import './breaks.js';
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```css
|
|
373
|
+
[contenteditable] {
|
|
374
|
+
--u2-rte: true;
|
|
375
|
+
--u2-rte-show-breaks: true;
|
|
376
|
+
--u2-rte-toolbar: bold breaks;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Remove `breaks` from `--u2-rte-toolbar` when line breaks should remain visible
|
|
381
|
+
without a toggle. Remove or set `--u2-rte-show-breaks: false` to start hidden.
|
|
382
|
+
The marker overlay belongs to the extension, uses the root's top layer where
|
|
383
|
+
Popover is available, and disappears completely when no surface displays it.
|
|
384
|
+
Toggling is view state: it emits no `u2-rte-change` and cannot enter history.
|
|
385
|
+
|
|
386
|
+
### Optional Unstyle
|
|
387
|
+
|
|
388
|
+
Add the staged remove-format action with one import:
|
|
389
|
+
|
|
390
|
+
```js
|
|
391
|
+
import './unstyle.js';
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```css
|
|
395
|
+
[contenteditable] {
|
|
396
|
+
--u2-rte: true;
|
|
397
|
+
--u2-rte-toolbar: bold unstyle;
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
It acts only on a non-collapsed selection. Repeated clicks remove the first
|
|
402
|
+
remaining configured level: classes, inline styles, presentation attributes,
|
|
403
|
+
then ordinary formatting wrappers. A partially selected inline wrapper is
|
|
404
|
+
split so surrounding content retains its formatting; a partially selected
|
|
405
|
+
block is left alone.
|
|
406
|
+
|
|
407
|
+
Native paste and drop reuse the same policy automatically. By default they
|
|
408
|
+
remove classes and inline styles only from elements added by that input, then
|
|
409
|
+
run structural normalization. Existing content around the insertion is not
|
|
410
|
+
unstyled. Set `--u2-rte-import-unstyle:none` to retain native presentation.
|
|
411
|
+
|
|
412
|
+
The policy is also reusable for foreign HTML after security sanitizing:
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
import {NativeSanitizer, defaultUnstyle} from './rte.js';
|
|
416
|
+
|
|
417
|
+
const fragment = new NativeSanitizer().sanitize(externalHtml);
|
|
418
|
+
defaultUnstyle.clean(fragment, {through: 'styles'});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The explicit rich paste/drop adapter composes these same stages:
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
import {
|
|
425
|
+
Commands, ExternalInput, NativeSanitizer, defaultUnstyle, insertFragment,
|
|
426
|
+
} from './rte.js';
|
|
427
|
+
|
|
428
|
+
const commands = new Commands(surface, {commands: {insertFragment}});
|
|
429
|
+
const external = new ExternalInput(surface, {
|
|
430
|
+
commands,
|
|
431
|
+
sanitizer: new NativeSanitizer(),
|
|
432
|
+
unstyle: defaultUnstyle,
|
|
433
|
+
through: ({inputType}) => inputType === 'insertFromPaste' ? 'styles' : 'classes',
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
It takes over only rich `text/html`; plain-text and quotation insertion remain
|
|
438
|
+
native. Keep `external` for as long as the surface is used and call
|
|
439
|
+
`external.dispose()` during manual teardown. Surface disconnection also
|
|
440
|
+
disposes it.
|
|
441
|
+
|
|
442
|
+
## Explicit engine setup
|
|
443
|
+
|
|
444
|
+
Import from [`../rte.js`](../rte.js) when the application owns module wiring.
|
|
445
|
+
The API follows three objects: a **core** per document, a **surface** per
|
|
446
|
+
editable element, and the modules installed on a surface. The core alone does
|
|
447
|
+
not install input handling, commands, or UI, so applications pay only for the
|
|
448
|
+
behavior they construct.
|
|
449
|
+
|
|
450
|
+
```css
|
|
451
|
+
.editor { --u2-rte: true; }
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
import {Commands, InputPipeline, Rte, enter, lineBreak} from './rte.js';
|
|
456
|
+
|
|
457
|
+
const core = new Rte(document);
|
|
458
|
+
|
|
459
|
+
core.addEventListener('u2-rte-add', ({detail}) => {
|
|
460
|
+
const commands = new Commands(detail.surface, {commands: {enter, lineBreak}});
|
|
461
|
+
new InputPipeline(detail.surface, {commands});
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
```html
|
|
466
|
+
<article class="editor" contenteditable>Hello world</article>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Focusing an element that opted in through `--u2-rte`
|
|
470
|
+
registers a surface, `u2-rte-add` installs the modules on it, and from then on:
|
|
471
|
+
|
|
472
|
+
- typing repairs the affected block (loose text becomes a paragraph, invalid
|
|
473
|
+
nesting is fixed) without touching the rest of the document;
|
|
474
|
+
- Enter and Shift+Enter are handled by the `enter` and `lineBreak` commands
|
|
475
|
+
instead of the browser;
|
|
476
|
+
- the caret keeps its logical position through every repair;
|
|
477
|
+
- disconnecting the surface tears the pipeline down again.
|
|
478
|
+
|
|
479
|
+
`rte.js` also exports a ready-made `rte` core for the current document if you do
|
|
480
|
+
not want to create one:
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
import {rte} from './rte.js';
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Registering surfaces explicitly
|
|
487
|
+
|
|
488
|
+
`--u2-rte` only drives lazy registration on focus. Register an element yourself
|
|
489
|
+
when it exists before it is focused, when you do not want a CSS opt-in, or when
|
|
490
|
+
you build the editor programmatically:
|
|
491
|
+
|
|
492
|
+
```js
|
|
493
|
+
const core = new Rte(document, {auto: false});
|
|
494
|
+
const surface = core.add(element); // idempotent, returns the same surface
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`add()` requires an explicit `contenteditable` element that belongs to the
|
|
498
|
+
core's root. One core can coordinate any number of surfaces; use a second core
|
|
499
|
+
only for a different document, an iframe, or a shadow root:
|
|
500
|
+
|
|
501
|
+
```js
|
|
502
|
+
const shadow = new Rte(host.shadowRoot);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Configuration
|
|
506
|
+
|
|
507
|
+
Behavior is configured through inherited CSS custom properties, so one
|
|
508
|
+
stylesheet can configure a family of editors while a single element overrides
|
|
509
|
+
what differs. `auto` means "use the semantic default for this tag".
|
|
510
|
+
|
|
511
|
+
| Property | Values | Default |
|
|
512
|
+
| --- | --- | --- |
|
|
513
|
+
| `--u2-rte` | any truthy token | disabled |
|
|
514
|
+
| `--u2-rte-block` | tag name, `none`, `auto` | `p`; structural children in lists/tables; none in inline, item, and cell hosts |
|
|
515
|
+
| `--u2-rte-enter` | `break`, `block`, `item`, `row`, `cell`, `auto` | derived from the host |
|
|
516
|
+
| `--u2-rte-cleanup` | `none`, `minimal`, `structural`, `canonical` | `structural` |
|
|
517
|
+
| `--u2-rte-clean-on` | any of `input paste drop command` | all four |
|
|
518
|
+
| `--u2-rte-elements` | tag list, `@basic`, `@article`, `@document`, `all` | `all` |
|
|
519
|
+
| `--u2-rte-ui` | `none`, `roaming`, `static` | `roaming` |
|
|
520
|
+
| `--u2-rte-ui-size` | length; set on the page, not the field | `14px` |
|
|
521
|
+
| `--u2-rte-inline-ui` | any of `table image link`, `none` | every one the loaded modules bring |
|
|
522
|
+
| `--u2-rte-classes` | content class names separated by spaces or commas | none |
|
|
523
|
+
| `--u2-rte-import-elements` | element names, `@basic`, `@content`, `@article`, `@document`, `all` | `@content` |
|
|
524
|
+
| `--u2-rte-import-sanitize` | `policy`, `none` | `policy` |
|
|
525
|
+
| `--u2-rte-toolbar` | control names separated by spaces or commas | every represented control |
|
|
526
|
+
| `--u2-rte-toolbar-unavailable` | `disable`, `hide` | `disable` |
|
|
527
|
+
| `--u2-rte-toolbar-when` | `always`, `selection` | `always` |
|
|
528
|
+
| `--u2-rte-show-breaks` | truthy or false-like token | hidden |
|
|
529
|
+
| `--u2-rte-import-unstyle` | `none` or an installed Unstyle level | `styles` |
|
|
530
|
+
|
|
531
|
+
The host element decides the defaults: a `<ul contenteditable>` creates list
|
|
532
|
+
items, while a directly editable `li`, `caption`, `th`, or `td` keeps its text
|
|
533
|
+
unwrapped and Enter inserts a line break. A `<p contenteditable>` behaves the
|
|
534
|
+
same way, while a `<div contenteditable>` uses paragraphs. Values are read on
|
|
535
|
+
demand, so a class change or a media query can change editing behavior without
|
|
536
|
+
re-registering.
|
|
537
|
+
|
|
538
|
+
```css
|
|
539
|
+
.notes { --u2-rte: true; --u2-rte-block: div; --u2-rte-clean-on: input command; }
|
|
540
|
+
.title { --u2-rte: true; --u2-rte-block: none; }
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
See [`../src/config/README.md`](../src/config/README.md) for the exact resolution
|
|
544
|
+
rules.
|
|
545
|
+
|
|
546
|
+
## Commands
|
|
547
|
+
|
|
548
|
+
A registry belongs to one surface. Commands are looked up by name, expose
|
|
549
|
+
availability without running, and execute in one transaction:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
if (commands.enabled('enter')) commands.run('enter');
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
`enabled()` is what a toolbar button binds to; the default answer is "the
|
|
556
|
+
surface owns a range". `run()` returns whatever the command reports, or
|
|
557
|
+
`undefined` if it was unavailable or the transaction was canceled. Running a
|
|
558
|
+
command the registry does not know is a programming error and throws.
|
|
559
|
+
|
|
560
|
+
Commands work from a toolbar even though clicking a button moves focus out of
|
|
561
|
+
the editor: the transaction restores the surface's saved selection when the live
|
|
562
|
+
one no longer belongs to it.
|
|
563
|
+
|
|
564
|
+
A command is a plain object, so an application can add its own or replace a
|
|
565
|
+
shipped one under the same name:
|
|
566
|
+
|
|
567
|
+
```js
|
|
568
|
+
import {Point} from './rte.js';
|
|
569
|
+
|
|
570
|
+
const horizontalRule = {
|
|
571
|
+
enabled: edit => !!edit.range?.collapsed,
|
|
572
|
+
run(edit) {
|
|
573
|
+
const {node, offset} = edit.range.start;
|
|
574
|
+
const index = edit.map.split(edit.element, node, offset);
|
|
575
|
+
const rule = edit.document.createElement('hr');
|
|
576
|
+
edit.map.insert(edit.element, index, rule);
|
|
577
|
+
edit.transaction.touch(edit.element);
|
|
578
|
+
edit.select(Point.after(rule));
|
|
579
|
+
return rule;
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
commands.add('horizontalRule', horizontalRule);
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The `edit` argument carries the range to act on, a point map, and the resulting
|
|
587
|
+
selection. Every mutation that goes through `edit.map` keeps tracked points
|
|
588
|
+
alive, which is why the command can select a position that only exists after the
|
|
589
|
+
change. Adding `inputTypes: ['insertHorizontalRule']` would also let the input
|
|
590
|
+
pipeline replace that native input type with this command.
|
|
591
|
+
[`../src/command/README.md`](../src/command/README.md) documents the full
|
|
592
|
+
contract.
|
|
593
|
+
|
|
594
|
+
### Applying a CSS class
|
|
595
|
+
|
|
596
|
+
Marks separate formatting meaning from its HTML representation. This adapter
|
|
597
|
+
uses a `span` for bare text but may reuse any fully selected inline element:
|
|
598
|
+
|
|
599
|
+
```js
|
|
600
|
+
import {MarkAdapter, MarkType, PendingMarks, applyMark, removeMark, setMarks} from './rte.js';
|
|
601
|
+
|
|
602
|
+
const x = new MarkType('x');
|
|
603
|
+
const xHtml = new MarkAdapter(x, {
|
|
604
|
+
selector: '.x',
|
|
605
|
+
tag: 'span',
|
|
606
|
+
reuse: true,
|
|
607
|
+
write: element => element.classList.add('x'),
|
|
608
|
+
clear: element => element.classList.remove('x'),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
commands.add('applyX', applyMark(xHtml));
|
|
612
|
+
commands.add('removeX', removeMark(xHtml));
|
|
613
|
+
const pending = new PendingMarks(surface);
|
|
614
|
+
commands.add('toggleX', pending.toggle(xHtml));
|
|
615
|
+
commands.add('insertText', pending.insertText);
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Applying `x` to selected bare text creates `<span class="x">`. A completely
|
|
619
|
+
selected `<b>` or `<a>` receives the class directly; partially selected content
|
|
620
|
+
gets the canonical `span`. Removing `x` preserves semantic elements and other
|
|
621
|
+
attributes. Only a `span` left without attributes is unwrapped. Applying also
|
|
622
|
+
joins adjacent canonical `<span class="x">` wrappers.
|
|
623
|
+
|
|
624
|
+
`commands.state('toggleX')` returns `true` when all selected editable text has
|
|
625
|
+
the mark, `false` when none has it, and `'mixed'` when only part has it. Toggle
|
|
626
|
+
removes an active mark and applies it to inactive or mixed selections. At a
|
|
627
|
+
caret, state reports whether the caret's DOM position is structurally inside
|
|
628
|
+
the mark. Toggling there changes the next ordinary text input or completed IME
|
|
629
|
+
composition, then returns to native input. Composition itself is never
|
|
630
|
+
prevented or rewritten. Moving the selection cancels that pending override
|
|
631
|
+
without an additional selection listener.
|
|
632
|
+
|
|
633
|
+
When an operation owns a complete formatting state, register one closed
|
|
634
|
+
adapter universe and pass its target marks as the command value:
|
|
635
|
+
|
|
636
|
+
```js
|
|
637
|
+
commands.add('marks', setMarks([boldHtml, colorHtml]));
|
|
638
|
+
commands.run('marks', {value: [bold.create(), color.create('blue')]});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
This removes configured marks absent from the target, resolves exclusions, and
|
|
642
|
+
applies the canonical set atomically. Exact canonical wrappers are ordered by
|
|
643
|
+
mark rank, redundant nesting is removed, and equivalent nested runs merge to a
|
|
644
|
+
fixed point. Additional attributes, atomic content, nested editors, and
|
|
645
|
+
unlisted adapters remain untouched. `commands.state('marks')` returns the
|
|
646
|
+
current mark array, `'mixed'`, or `null`.
|
|
647
|
+
|
|
648
|
+
### Standard HTML marks
|
|
649
|
+
|
|
650
|
+
Bold, italic, underline, strike, code, and link ship as ordinary mark types with
|
|
651
|
+
replaceable default HTML adapters:
|
|
652
|
+
|
|
653
|
+
```js
|
|
654
|
+
import {
|
|
655
|
+
boldHtml, codeHtml, italicHtml, linkHtml, strikeHtml, underlineHtml,
|
|
656
|
+
} from './rte.js';
|
|
657
|
+
|
|
658
|
+
commands.add('bold', pending.toggle(boldHtml));
|
|
659
|
+
commands.add('italic', pending.toggle(italicHtml));
|
|
660
|
+
commands.add('code', pending.toggle(codeHtml));
|
|
661
|
+
commands.add('link', pending.toggle(linkHtml, {href: '/docs'}));
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Use the same `PendingMarks` instance and register its `insertText` command only
|
|
665
|
+
once. Boolean adapters recognize `strong`/`b`, `em`/`i`, `u`, `s`/`strike`, and
|
|
666
|
+
`code`, then emit `strong`, `em`, `u`, `s`, and `code`. Links recognize
|
|
667
|
+
`a[href]` and use `{href, target?, rel?, title?}`. The adapter validates this
|
|
668
|
+
shape but deliberately leaves URL schemes to the application; pasted HTML goes
|
|
669
|
+
through the separate sanitizer policy. Removing a bare semantic element unwraps
|
|
670
|
+
it, while unrelated attributes survive on a neutral `span`.
|
|
671
|
+
|
|
672
|
+
## Roaming toolbar
|
|
673
|
+
|
|
674
|
+
`Toolbar` binds application-owned markup to the command registry of the active
|
|
675
|
+
surface. It creates no buttons, icons, styles, or editor commands:
|
|
676
|
+
|
|
677
|
+
```html
|
|
678
|
+
<div id="toolbar" aria-label="Formatting">
|
|
679
|
+
<button type="button" data-command="bold" data-state data-shortcut="b">Bold</button>
|
|
680
|
+
<button type="button" data-command="toggleX" data-state data-shortcut="x">Highlight</button>
|
|
681
|
+
<button type="button" data-command="removeX">Remove</button>
|
|
682
|
+
</div>
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
```js
|
|
686
|
+
import {Toolbar, rangeRect} from './rte.js';
|
|
687
|
+
|
|
688
|
+
const commandsBySurface = new WeakMap();
|
|
689
|
+
core.addEventListener('u2-rte-add', ({detail}) => {
|
|
690
|
+
const commands = createCommands(detail.surface);
|
|
691
|
+
commandsBySurface.set(detail.surface, commands);
|
|
692
|
+
new InputPipeline(detail.surface, {commands});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const toolbar = new Toolbar(core, document.querySelector('#toolbar'), {
|
|
696
|
+
commands: surface => commandsBySurface.get(surface),
|
|
697
|
+
place: (element, surface) => placeNear(element,
|
|
698
|
+
rangeRect(surface.selection.range(), {root: surface.element})),
|
|
699
|
+
});
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
The optional `place` callback owns geometry, keeping anchor positioning
|
|
703
|
+
replaceable. An application toolbar with `popover="manual"` is opened and
|
|
704
|
+
closed in the browser top layer by the binder; an ordinary element remains an
|
|
705
|
+
ordinary element. Toggle items opt into `aria-pressed` reflection with
|
|
706
|
+
`data-state`; action buttons omit it. `data-shortcut="x"` binds Ctrl+X or
|
|
707
|
+
Command+X while keyboard input belongs to the active surface. An inherited
|
|
708
|
+
`--u2-rte-toolbar: toggleX removeX` property selects the items for one editor.
|
|
709
|
+
`--u2-rte-inline-ui` decides which contextual UIs a field draws at its content —
|
|
710
|
+
the table handles, the image frame, the link form. It is a property of the field,
|
|
711
|
+
not of the module set: the same editor then serves a body of text with everything
|
|
712
|
+
and a bare teaser field with `none`, without a second instance. The commands stay
|
|
713
|
+
either way; only the chrome goes.
|
|
714
|
+
|
|
715
|
+
`--u2-rte-ui-size` scales everything the editor draws. It is the one exception to
|
|
716
|
+
the table above in that it is read where the chrome hangs — the page — rather
|
|
717
|
+
than on the field, and it is deliberately the only way in: every size inside the
|
|
718
|
+
chrome is `em` against it, so nothing the site does to its own root font moves
|
|
719
|
+
the editor's furniture.
|
|
720
|
+
|
|
721
|
+
`--u2-rte-ui: none` hides the roaming toolbar. Moving focus between the surface
|
|
722
|
+
and toolbar keeps it open; leaving both hides it. `rangeRect()` preserves a
|
|
723
|
+
usable native range rectangle and derives an empty collapsed one from adjacent
|
|
724
|
+
rendered content without mutating DOM or selection.
|
|
725
|
+
Set `--u2-rte-toolbar-when: selection` to show it only while the saved editor
|
|
726
|
+
selection is non-collapsed; the default also shows it at a caret.
|
|
727
|
+
|
|
728
|
+
## Events
|
|
729
|
+
|
|
730
|
+
Surface events are dispatched on the DOM element as bubbling, composed events
|
|
731
|
+
and on the `Surface` object. The element is notified first, so a listener there
|
|
732
|
+
observes an event before the modules reacting to it.
|
|
733
|
+
|
|
734
|
+
| Event | Target | Meaning |
|
|
735
|
+
| --- | --- | --- |
|
|
736
|
+
| `u2-rte-add`, `u2-rte-delete` | core | a surface was registered or removed |
|
|
737
|
+
| `u2-rte-activechange` | core | the active surface changed |
|
|
738
|
+
| `u2-rte-activate`, `u2-rte-deactivate` | surface | this surface became active |
|
|
739
|
+
| `u2-rte-selectionchange` | surface | a new selection snapshot was captured |
|
|
740
|
+
| `u2-rte-beforechange` | surface | cancelable, before a transaction runs |
|
|
741
|
+
| `u2-rte-command` | surface | a command executed; `transaction` is null for a view action |
|
|
742
|
+
| `u2-rte-normalize` | surface | cleanup ran, with its actions and unresolved issues |
|
|
743
|
+
| `u2-rte-change` | surface | the transaction committed |
|
|
744
|
+
| `u2-rte-error` | surface | a transaction or fail-closed external input failed; direct calls rethrow |
|
|
745
|
+
| `u2-rte-disconnect`, `u2-rte-dispose` | surface, core | teardown |
|
|
746
|
+
|
|
747
|
+
**Listen to `u2-rte-change` for "the content changed".** It arrives once per
|
|
748
|
+
transaction, after every command and cleanup step inside it. `u2-rte-command`
|
|
749
|
+
and `u2-rte-normalize` report steps within a transaction and are meant for
|
|
750
|
+
diagnostics and modules. View commands such as visible line breaks emit only
|
|
751
|
+
`u2-rte-command` because they do not change content.
|
|
752
|
+
|
|
753
|
+
```js
|
|
754
|
+
element.addEventListener('u2-rte-change', event => {
|
|
755
|
+
save(element.innerHTML, event.detail.transaction.dirty);
|
|
756
|
+
});
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
Canceling `u2-rte-beforechange` prevents the change:
|
|
760
|
+
|
|
761
|
+
```js
|
|
762
|
+
element.addEventListener('u2-rte-beforechange', event => {
|
|
763
|
+
if (readOnly) event.preventDefault();
|
|
764
|
+
});
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
## Content in and out
|
|
768
|
+
|
|
769
|
+
There is no serializer and no setter yet. Read `element.innerHTML` and write it
|
|
770
|
+
the same way, then normalize what you inserted:
|
|
771
|
+
|
|
772
|
+
```js
|
|
773
|
+
element.innerHTML = trustedMarkup;
|
|
774
|
+
pipeline.normalize('command');
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
`normalize()` returns the executed actions, the repairs it could not resolve,
|
|
778
|
+
and the point map it used; it does nothing when `--u2-rte-clean-on` excludes the
|
|
779
|
+
trigger.
|
|
780
|
+
|
|
781
|
+
RTE now has an immutable security policy and a native `Element.setHTML()`
|
|
782
|
+
adapter, but the input pipeline does not consume clipboard or drag payloads
|
|
783
|
+
yet. Structural normalization still is not sanitizing: external HTML must pass
|
|
784
|
+
through a supported security adapter before insertion. Firefox/WebKit need the
|
|
785
|
+
planned non-native adapter rather than an unsafe parsing fallback.
|
|
786
|
+
|
|
787
|
+
## Teardown
|
|
788
|
+
|
|
789
|
+
```js
|
|
790
|
+
core.delete(surface); // or surface.dispose()
|
|
791
|
+
core.dispose(); // removes listeners and disconnects every surface
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
`Rte`, `Surface`, and `InputPipeline` also implement `[Symbol.dispose]()` for
|
|
795
|
+
`using`. Disconnecting a surface disposes its input pipeline; a registry needs
|
|
796
|
+
no cleanup. After teardown the element is inert and keeps its content.
|
|
797
|
+
|
|
798
|
+
## Nested editors
|
|
799
|
+
|
|
800
|
+
An explicit nested `contenteditable` is a hard boundary. Events, selections,
|
|
801
|
+
traversal, normalization, and commands never cross it, and the inner element
|
|
802
|
+
becomes a surface of its own only if you register it:
|
|
803
|
+
|
|
804
|
+
```html
|
|
805
|
+
<div contenteditable>
|
|
806
|
+
outer text
|
|
807
|
+
<figcaption contenteditable>independent editor</figcaption>
|
|
808
|
+
</div>
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
## What is not implemented yet
|
|
812
|
+
|
|
813
|
+
Being explicit is cheaper than surprising you:
|
|
814
|
+
|
|
815
|
+
- **No undo/redo.** Editor mutations do not go through the browser's editing
|
|
816
|
+
commands, so the native undo stack no longer matches the document after the
|
|
817
|
+
first repair. A history module owns this and does not exist yet.
|
|
818
|
+
- **No ready-made link-entry UI yet.** The standard mark types and HTML adapters
|
|
819
|
+
are available, but the convention client intentionally exposes only Bold.
|
|
820
|
+
Applications decide how URLs are entered and validated before registering a
|
|
821
|
+
link command or control.
|
|
822
|
+
- **No complete deletion or general list commands.** Backspace and Delete at a
|
|
823
|
+
mergeable block boundary are explicit; ordinary character deletion stays
|
|
824
|
+
native. Selected-range deletion, list creation, indent, and outdent remain open.
|
|
825
|
+
Enter can split an item and exit an empty list item.
|
|
826
|
+
- **No contextual plain-text/quotation importer and no serializer.** Rich
|
|
827
|
+
paste/drop remains browser-native across engines and receives mapped
|
|
828
|
+
presentation plus structural cleanup afterwards. The optional pre-native
|
|
829
|
+
HTML-string path still requires an explicitly supplied safe sanitizer.
|
|
830
|
+
- **The convention toolbar is only a prototype.** `rte.js` provides one
|
|
831
|
+
styled Bold control plus optional block-style and visible-break extensions;
|
|
832
|
+
the standalone roaming binder still leaves markup, theme, placement, and
|
|
833
|
+
command sets to the application. Static bindings and menu state are open.
|
|
834
|
+
- **The verified 379-test baseline covers Chromium 152, Firefox 154, and
|
|
835
|
+
WebKitGTK 2.52.5.** It adds
|
|
836
|
+
later list,
|
|
837
|
+
mark, toolbar, normalization, convention-client, optional-module, block-style,
|
|
838
|
+
heading Enter, structural deletion, range geometry, element policy, extension
|
|
839
|
+
lifecycle, top-layer toolbar, visible-break, sanitizing-policy, selection-only
|
|
840
|
+
toolbar, Unstyle, mapped fragment replacement, post-native import cleanup,
|
|
841
|
+
and composition-aware pending marks.
|
|
842
|
+
Standard HTML mark coverage raises the current runner to 381 tests;
|
|
843
|
+
cross-browser verification of that revision is pending.
|
|
844
|
+
|
|
845
|
+
[`../PLAN.md`](../PLAN.md) tracks what lands next.
|