u2ui 1.5.3 → 1.5.4

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.
Files changed (142) hide show
  1. package/el/ico/ico.js +5 -5
  2. package/el/rte/README.md +53 -13
  3. package/el/rte/manifest.json +19 -4
  4. package/el/rte/rte.css +31 -6
  5. package/el/rte/rte.js +174 -44
  6. package/el/rte0/README.md +37 -0
  7. package/el/rte0/manifest.json +33 -0
  8. package/el/rte0/rte0.css +9 -0
  9. package/el/rte0/rte0.js +56 -0
  10. package/el/system/styler.js +2 -0
  11. package/el/tooltip/tooltip.css +2 -2
  12. package/js/Placer/Placer.js +2 -1
  13. package/js/rte/AGENTS.md +142 -0
  14. package/js/rte/PLAN.md +488 -0
  15. package/js/rte/README.md +319 -25
  16. package/js/rte/blocks.js +7 -0
  17. package/js/rte/breaks.js +6 -0
  18. package/js/rte/classes.js +7 -0
  19. package/js/rte/docs/usage.md +845 -0
  20. package/js/rte/images.js +7 -0
  21. package/js/rte/link.js +8 -0
  22. package/js/rte/playground/README.md +95 -0
  23. package/js/rte/playground/hosts.html +518 -0
  24. package/js/rte/playground/index.html +824 -0
  25. package/js/rte/rte.js +56 -5
  26. package/js/rte/source.js +12 -0
  27. package/js/rte/src/browser/README.md +40 -0
  28. package/js/rte/src/browser/range-rect.js +82 -0
  29. package/js/rte/src/client/README.md +378 -0
  30. package/js/rte/src/client/blocks.js +38 -0
  31. package/js/rte/src/client/breaks.js +212 -0
  32. package/js/rte/src/client/classes.js +48 -0
  33. package/js/rte/src/client/editor.js +461 -0
  34. package/js/rte/src/client/external-input.js +47 -0
  35. package/js/rte/src/client/history.js +13 -0
  36. package/js/rte/src/client/images.js +275 -0
  37. package/js/rte/src/client/link.js +572 -0
  38. package/js/rte/src/client/marks.js +25 -0
  39. package/js/rte/src/client/source.js +182 -0
  40. package/js/rte/src/client/structure.js +27 -0
  41. package/js/rte/src/client/tables.js +162 -0
  42. package/js/rte/src/client/unstyle.js +32 -0
  43. package/js/rte/src/command/README.md +517 -0
  44. package/js/rte/src/command/block-boundary.js +43 -0
  45. package/js/rte/src/command/block-style.js +120 -0
  46. package/js/rte/src/command/commands.js +158 -0
  47. package/js/rte/src/command/delete.js +112 -0
  48. package/js/rte/src/command/edit.js +72 -0
  49. package/js/rte/src/command/element.js +45 -0
  50. package/js/rte/src/command/enter.js +175 -0
  51. package/js/rte/src/command/fragment.js +76 -0
  52. package/js/rte/src/command/insert.js +52 -0
  53. package/js/rte/src/command/list.js +284 -0
  54. package/js/rte/src/command/mark.js +583 -0
  55. package/js/rte/src/command/pending-marks.js +216 -0
  56. package/js/rte/src/command/table.js +207 -0
  57. package/js/rte/src/command/unstyle.js +248 -0
  58. package/js/rte/src/config/README.md +96 -0
  59. package/js/rte/src/config/config.js +127 -0
  60. package/js/rte/src/core/README.md +41 -0
  61. package/js/rte/src/core/core.js +156 -0
  62. package/js/rte/src/history/README.md +83 -0
  63. package/js/rte/src/history/history.js +283 -0
  64. package/js/rte/src/input/README.md +155 -0
  65. package/js/rte/src/input/external-input.js +130 -0
  66. package/js/rte/src/input/input-pipeline.js +414 -0
  67. package/js/rte/src/mark/README.md +219 -0
  68. package/js/rte/src/mark/dom-adapter.js +100 -0
  69. package/js/rte/src/mark/mark.js +133 -0
  70. package/js/rte/src/mark/standard.js +76 -0
  71. package/js/rte/src/model/README.md +71 -0
  72. package/js/rte/src/model/content-model.js +213 -0
  73. package/js/rte/src/model/html/README.md +51 -0
  74. package/js/rte/src/model/html/html-model.js +90 -0
  75. package/js/rte/src/normalize/execute/README.md +33 -0
  76. package/js/rte/src/normalize/execute/repair-executor.js +129 -0
  77. package/js/rte/src/normalize/normalizer/README.md +48 -0
  78. package/js/rte/src/normalize/normalizer/normalizer.js +141 -0
  79. package/js/rte/src/normalize/repair/README.md +70 -0
  80. package/js/rte/src/normalize/repair/repair-planner.js +208 -0
  81. package/js/rte/src/sanitize/README.md +105 -0
  82. package/js/rte/src/sanitize/native.js +43 -0
  83. package/js/rte/src/sanitize/policy.js +202 -0
  84. package/js/rte/src/selection/README.md +39 -0
  85. package/js/rte/src/selection/map/README.md +53 -0
  86. package/js/rte/src/selection/map/point-map.js +307 -0
  87. package/js/rte/src/selection/ownership/README.md +32 -0
  88. package/js/rte/src/selection/ownership/ownership.js +42 -0
  89. package/js/rte/src/selection/point/README.md +34 -0
  90. package/js/rte/src/selection/point/point.js +83 -0
  91. package/js/rte/src/selection/range/README.md +38 -0
  92. package/js/rte/src/selection/range/edit-range.js +223 -0
  93. package/js/rte/src/selection/snapshot.js +64 -0
  94. package/js/rte/src/source/README.md +64 -0
  95. package/js/rte/src/source/source.js +193 -0
  96. package/js/rte/src/surface/README.md +36 -0
  97. package/js/rte/src/surface/content.js +16 -0
  98. package/js/rte/src/surface/surface.js +87 -0
  99. package/js/rte/src/transaction/README.md +35 -0
  100. package/js/rte/src/transaction/transaction.js +56 -0
  101. package/js/rte/src/ui/README.md +255 -0
  102. package/js/rte/src/ui/chrome.js +214 -0
  103. package/js/rte/src/ui/handles.js +179 -0
  104. package/js/rte/src/ui/place.js +25 -0
  105. package/js/rte/src/ui/toolbar.js +283 -0
  106. package/js/rte/src/unstyle/README.md +103 -0
  107. package/js/rte/src/unstyle/unstyle.js +140 -0
  108. package/js/rte/tables.js +7 -0
  109. package/js/rte/unstyle.js +8 -0
  110. package/js/rte0/README.md +37 -0
  111. package/js/rte0/rte.js +7 -0
  112. package/js/{rte → rte0}/src/behavior.js +0 -1
  113. package/js/{rte → rte0}/src/events.js +0 -1
  114. package/js/{rte → rte0}/src/range.js +0 -2
  115. package/js/{rte → rte0}/src/ui.items.js +0 -2
  116. package/js/{rte → rte0}/src/ui.js +0 -1
  117. package/package.json +1 -1
  118. package/u2/Element.js +2 -2
  119. package/u2/plan-scoped-refactoring.md +133 -303
  120. package/u2/projects.json +18 -2
  121. package/u2/tools/update.repos.json.js +1 -1
  122. package/el/qrcode/package.json +0 -22
  123. /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
  124. /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
  125. /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
  126. /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
  127. /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
  128. /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
  129. /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
  130. /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
  131. /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
  132. /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
  133. /package/js/{rte → rte0}/old/Rte/main.css +0 -0
  134. /package/js/{rte → rte0}/old/old.rte.js +0 -0
  135. /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
  136. /package/js/{rte → rte0}/src/fixes.js +0 -0
  137. /package/js/{rte → rte0}/src/rte.js +0 -0
  138. /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
  139. /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
  140. /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
  141. /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
  142. /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.