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,35 @@
1
+ # Transactions
2
+
3
+ A transaction groups one complete editor change, from the selection before it
4
+ starts to the selection and affected nodes after it finishes. For example, an
5
+ Enter command and the structural cleanup it triggers should be observed as one
6
+ change rather than unrelated DOM mutations.
7
+
8
+ `transaction.js` provides this synchronous boundary for commands, input
9
+ handling, normalization, events, and history.
10
+
11
+ ## Contract
12
+
13
+ - A transaction runs synchronously and at most once.
14
+ - A live selection inside the surface is captured before change hooks execute;
15
+ only a selection the surface does not own is replaced by its saved snapshot.
16
+ - `u2-rte-beforechange` is cancelable on both the surface object and DOM host.
17
+ - `touch(node)` records affected nodes and rejects nodes outside the surface.
18
+ - Nested operations share the surface's current transaction.
19
+ - A successful transaction captures its resulting selection and emits
20
+ `u2-rte-change`; a thrown error emits `u2-rte-error` and is rethrown.
21
+ - `selectionBefore` and `selectionAfter` expose read-only snapshots.
22
+ - Transactions report state but do not pretend to roll back arbitrary DOM
23
+ mutations. Undo is provided by the state-based history layer, which uses the
24
+ `trigger` option to decide what belongs to one step.
25
+
26
+ Synchronous execution is intentional: selection, `beforeinput`, browser undo,
27
+ and DOM mutation order become ambiguous across an asynchronous gap.
28
+
29
+ ## TODO
30
+
31
+ - Reduce touched nodes into minimal dirty scopes for normalization.
32
+ - Record reversible DOM operations so history can store diffs instead of
33
+ whole content states.
34
+ - Add transaction metadata compatible with Input Events `inputType` values.
35
+ - Define composition transaction grouping without delaying DOM ownership.
@@ -0,0 +1,56 @@
1
+ export class Transaction {
2
+ #surface;
3
+ #options;
4
+ #dirty = new Set();
5
+ #state = 'new';
6
+ #selectionBefore = null;
7
+ #selectionAfter = null;
8
+
9
+ constructor(surface, options = {}) {
10
+ this.#surface = surface;
11
+ this.#options = Object.freeze({...options});
12
+ }
13
+
14
+ get surface() { return this.#surface; }
15
+ get options() { return this.#options; }
16
+ get state() { return this.#state; }
17
+ get dirty() { return [...this.#dirty]; }
18
+ get selectionBefore() { return this.#selectionBefore; }
19
+ get selectionAfter() { return this.#selectionAfter; }
20
+
21
+ touch(node) {
22
+ if (node !== this.#surface.element && !this.#surface.element.contains(node)) {
23
+ throw new RangeError('Dirty nodes must belong to the transaction surface');
24
+ }
25
+ this.#dirty.add(node);
26
+ return this;
27
+ }
28
+
29
+ run(change) {
30
+ if (this.#state !== 'new') throw new DOMException('A transaction can run only once', 'InvalidStateError');
31
+ // A live selection inside the surface is newer than the saved one; only
32
+ // a selection the surface does not own is replaced by its snapshot.
33
+ if (!this.#surface.capture()) this.#surface.restore();
34
+ this.#selectionBefore = this.#surface.selection;
35
+ if (!this.#surface.emit('u2-rte-beforechange', {transaction: this}, {cancelable: true})) {
36
+ this.#state = 'canceled';
37
+ return;
38
+ }
39
+ this.#state = 'running';
40
+ try {
41
+ const result = change(this);
42
+ if (result?.then) throw new TypeError('Editor transactions must be synchronous');
43
+ this.#selectionAfter = this.#surface.capture();
44
+ if (!this.#selectionAfter && this.#selectionBefore?.restore()) {
45
+ this.#selectionAfter = this.#surface.capture();
46
+ }
47
+ this.#state = 'committed';
48
+ this.#surface.emit('u2-rte-change', {transaction: this});
49
+ return result;
50
+ } catch (error) {
51
+ this.#state = 'failed';
52
+ this.#surface.emit('u2-rte-error', {transaction: this, error});
53
+ throw error;
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,255 @@
1
+ # Toolbar UI
2
+
3
+ `toolbar.js` binds an application's toolbar markup to the command registry of
4
+ the currently active surface. It provides the roaming behavior; it does not
5
+ create buttons, inject styles, choose icons, or define editor commands.
6
+
7
+ The useful interaction model comes from the original `../rte0` toolbar: one
8
+ shared toolbar follows the active editor, its items reflect the current
9
+ selection, pointer interaction keeps the editor selection, and keyboard
10
+ shortcuts invoke the same actions. RTE replaces the old global item registry,
11
+ `execCommand()`, delayed focus repair, and automatic style injection with the
12
+ existing core, surface, and command contracts.
13
+
14
+ ## Contract
15
+
16
+ ```js
17
+ const toolbar = new Toolbar(core, element, {
18
+ commands: surface => commandsBySurface.get(surface),
19
+ place: (element, surface) => placeNearSelection(element, surface.selection),
20
+ });
21
+ ```
22
+
23
+ - `core` supplies the active surface and owns the single root-level lifecycle.
24
+ - `element` is application-owned markup. Descendants with `data-command` are
25
+ action/toggle items; `select[data-command-value]` binds one value-bearing
26
+ command. No icon system or CSS is required. If the element has
27
+ `popover="manual"`, visibility also opens and closes it in the browser top
28
+ layer without changing focus.
29
+ - `commands(surface)` resolves the registry belonging to that surface. Returning
30
+ `null` hides the toolbar, which permits lazy surface setup without coupling a
31
+ registry to `Surface`.
32
+ - `place(element, surface)` is optional presentation policy called after every
33
+ visible refresh. Positioning remains replaceable and outside command logic.
34
+ - `refresh()` reads command availability and state immediately and returns
35
+ whether the toolbar is visible.
36
+ - `dispose()` removes all core, surface, DOM, and shortcut listeners. Core
37
+ disposal also disposes the toolbar.
38
+
39
+ The toolbar listens only to active-surface changes, selection changes, committed
40
+ changes, disconnect, and root focus transitions. Switching surfaces aborts the
41
+ old surface listeners. It does not observe DOM or poll state.
42
+
43
+ ## Items
44
+
45
+ ```html
46
+ <div class="toolbar" aria-label="Text formatting">
47
+ <button type="button" data-command="bold" data-state data-shortcut="b">Bold</button>
48
+ <button type="button" data-command="italic" data-state data-shortcut="i">Italic</button>
49
+ </div>
50
+ ```
51
+
52
+ An item is visible only when the active registry contains its command. Its
53
+ `disabled`/`aria-disabled` state follows `commands.enabled()`. On a `data-state`
54
+ item, boolean and `'mixed'` command states become `aria-pressed`, so toggle
55
+ buttons need no separate adapter. Action buttons omit `data-state` and never
56
+ pretend to be toggles merely because their command can report state.
57
+ `data-control` may give a button a CSS-facing name different from its command,
58
+ just as it does for a value control. This lets `--u2-rte-toolbar: breaks` expose
59
+ a button whose command is `showBreaks`.
60
+
61
+ `data-shortcut="b"` means Ctrl+B or Command+B while keyboard input belongs to
62
+ the active surface. Shortcuts with Shift or Alt are deliberately not inferred;
63
+ future keymap policy can generalize that syntax without changing commands.
64
+
65
+ An editor may select the shared toolbar's available items with an inherited CSS
66
+ property:
67
+
68
+ ```css
69
+ .compact-editor { --u2-rte-toolbar: bold italic; }
70
+ ```
71
+
72
+ An absent or empty property exposes every registered item represented in the
73
+ markup. `--u2-rte-ui: none` hides this roaming toolbar. A future static binding
74
+ can consume the same item contract without changing the core or registry.
75
+
76
+ By default the toolbar is useful at both a caret and a selected range. An
77
+ editor that wants contextual formatting controls only while text is visibly
78
+ selected can opt in without constructing a different toolbar:
79
+
80
+ ```css
81
+ .selection-toolbar { --u2-rte-toolbar-when: selection; }
82
+ ```
83
+
84
+ `selection` means a valid, non-collapsed saved selection. A missing selection
85
+ or caret hides the toolbar. `always`, an absent value, and unknown future values
86
+ retain the default behavior.
87
+
88
+ ### Command values
89
+
90
+ A select can represent a mutually exclusive group of commands:
91
+
92
+ ```html
93
+ <select data-command-value="blockStyle" data-control="block" aria-label="Block style">
94
+ <option value="">Block style</option>
95
+ <option value="paragraph">Paragraph</option>
96
+ <option value="h1">Heading 1</option>
97
+ </select>
98
+ ```
99
+
100
+ `data-command-value` names one command, while `data-control` is the name exposed
101
+ to `--u2-rte-toolbar`. Option values become `edit.value`. The command's string
102
+ state selects the matching option; mixed, null, unknown, or conflicting state
103
+ selects the empty placeholder. A change restores the saved editor selection and
104
+ runs the command through the same transaction path as a button. Availability
105
+ is queried with each option's value, so unsupported values are hidden and
106
+ disabled; state is queried once per refresh. The adapter attaches no listener
107
+ per option or surface.
108
+
109
+ ## Invariants
110
+
111
+ - UI focus never deactivates the current surface or becomes editor selection.
112
+ - Focus moving between the active surface and its toolbar keeps the toolbar
113
+ open. Leaving both hides it; later selection/change notifications do not
114
+ reopen it until focus returns.
115
+ - Every action runs through `Commands`; UI code never mutates editable DOM.
116
+ - One toolbar can serve arbitrarily many surfaces and resolve different command
117
+ sets for each one.
118
+ - Markup, labels, localization, icons, layout, and placement are application
119
+ policy and add no engine-side resources when no toolbar is constructed.
120
+
121
+ ## Chrome
122
+
123
+ `Chrome` is one shadow root per editor for everything the editor draws: its
124
+ toolbar, its contextual handles, its forms and dialogs. It carries the top layer.
125
+
126
+ `part(key, css, tag)` gives a piece of chrome its node and its stylesheet under
127
+ one name: the key becomes the element's `id`, so the css is written `#link input`
128
+ and nothing is named twice. Inside a root shared by nothing else an id is as
129
+ unique as an attribute, and a second claim on the same key is an error rather
130
+ than a silent second element. `style(key, css)` registers a stylesheet alone,
131
+ once, for a piece that builds its own node.
132
+
133
+ What every piece shares is one skin, `.panel` — blur, rounding, shadow, border —
134
+ because to the eye the editor's chrome is one thing and not five. A piece brings
135
+ only its own layout.
136
+
137
+ Sizes inside are `em`, against a text size stated once on the host. Page-relative
138
+ units would let a site's root font resize the editor's furniture by accident, and
139
+ a variable plus `calc()` would only be `em` with arithmetic bolted on. The size
140
+ is still meant to be changed, just not by accident: `--u2-rte-ui-size` (default
141
+ `14px`) sets it, and reaches the chrome because `all: initial` does not touch
142
+ custom properties and the host inherits them from the page.
143
+
144
+ `follow(element)` keeps that one chrome inside the closest currently active
145
+ native top-layer boundary: a modal dialog, open popover, or fullscreen element.
146
+ This is a DOM relationship rather than a `z-index` trick, because modal
147
+ inertness and fullscreen rendering admit only descendants. Dialog `open`
148
+ mutations, popover `toggle`, and `fullscreenchange` update the mount; closing a
149
+ boundary returns the chrome to its original `Document` or `ShadowRoot`.
150
+ The host is an explicit `contenteditable=false` boundary. A convention client
151
+ also retains it in the core so UI focus and temporary direct-host mounting do
152
+ not become editor state or content.
153
+
154
+ An editor is chrome inside someone else's document and has to survive their
155
+ `button {}` rule. One encapsulated root means the page's CSS reaches none of it,
156
+ none of its styles leak out, and the application sees a single element rather
157
+ than one per piece of UI. Its host is `all: initial`, so the page's typography
158
+ does not shift it either; custom properties are excluded from `all` and still
159
+ inherit, so configuration is unaffected.
160
+
161
+ There is deliberately no `::part` surface. Exposing parts would make the
162
+ internal structure of the chrome a public contract, and it is not needed:
163
+ an application that wants different chrome builds its own on the commands, which
164
+ is the supported path and always was. `Toolbar` already binds application-owned
165
+ markup for exactly that reason.
166
+
167
+ ## Handles
168
+
169
+ `Handles` is a set of buttons placed around something, with an optional frame
170
+ drawn behind them. It owns no editor concept: a caller says where each handle
171
+ goes and what pressing one means, so anything with a rectangle can use it —
172
+ inside this engine or outside it.
173
+
174
+ ```js
175
+ const handles = new Handles(chrome.root, {
176
+ name: 'tables',
177
+ handles: [{name: 'rowAfter', label: 'Row below', text: '+'}],
178
+ action: name => …,
179
+ press: name => …,
180
+ });
181
+ handles.show().place('rowAfter', x, y).disable('rowAfter', false);
182
+ ```
183
+
184
+ Given a shadow root it places itself inside it and registers its stylesheet
185
+ there. Given a document it makes an encapsulated root of its own, so it works
186
+ just as well outside an editor.
187
+
188
+ Both its listeners are on the containing root rather than the host: an event
189
+ that is not composed never leaves the shadow tree it happened in. Pressing a
190
+ handle prevents its default, because pointing at editor chrome must never move
191
+ the selection it acts on.
192
+
193
+ The glyph inside a handle is centred with `text-box: trim-both cap alphabetic`,
194
+ which trims the font's leading so a bare `+` or `×` sits on its optical centre
195
+ rather than its metric one. Where that is unsupported the flex centring alone
196
+ still applies.
197
+
198
+ ## Present or available
199
+
200
+ Two different questions, answered two different ways:
201
+
202
+ - **Presence** says what this editor offers at all. A control is absent when its
203
+ command is not registered on the surface, when `--u2-rte-toolbar` does not
204
+ list it, or when its choices are not configured — a style select with no
205
+ `--u2-rte-classes` has nothing to be.
206
+ - **Availability** says what the current selection allows. A control that exists
207
+ but cannot act is disabled, never hidden.
208
+
209
+ Keeping the two apart is what makes a toolbar usable: its shape follows the
210
+ configuration and stays put while the caret moves, so a control is always found
211
+ in the same place. A toolbar whose buttons appeared and vanished with every
212
+ selection change would move its targets out from under the pointer.
213
+
214
+ `--u2-rte-toolbar-unavailable: hide` trades that away deliberately, for a
215
+ toolbar that only ever shows what it can do. It is the host's call, not the
216
+ default: a compact toolbar of two or three controls loses little by moving,
217
+ while a wide one loses a great deal.
218
+
219
+ ## Focus across the boundary
220
+
221
+ A `focusout` listener outside the toolbar's shadow tree sees its related target
222
+ retargeted to that tree's host. Focus moving from the surface into a control of
223
+ the toolbar's own therefore looks like focus leaving the editor, and dismissing
224
+ on it would take the toolbar away in the middle of the click that opened a
225
+ select. Ownership is decided against the tree's host as well as its contents.
226
+
227
+ ## Placement
228
+
229
+ `place(element, surface, {align, prefer, gap})` is the one placement policy for every
230
+ contextual UI. It anchors on the surface's saved selection through `rangeRect`,
231
+ keeps the element inside the viewport, and falls to the other side when the
232
+ preferred one does not fit. The roaming toolbar centres above the selection; the
233
+ link form aligns to its start and prefers below.
234
+
235
+ A select with no usable choice hides like a button whose command is unavailable:
236
+ a control that cannot be used is not shown.
237
+
238
+ Pointing anywhere at the toolbar never moves the editor's selection, including
239
+ at a control that currently has nothing to run — otherwise clicking a disabled
240
+ button would blur the surface and dismiss the toolbar. Fields keep their own
241
+ pointer behaviour so they can be opened and typed in.
242
+
243
+ Keys are not the toolbar's concern: a command declares its own shortcut and the
244
+ input pipeline resolves it, so a key works whether or not a control for it is on
245
+ screen. `data-shortcut` remains as the hint shown in a control's title.
246
+
247
+ ## TODO
248
+
249
+ - Add a static-surface binding using the same item state rules.
250
+ - Define richer shortcut descriptors and conflict resolution with application
251
+ keymaps.
252
+ - Bind menus and application-owned custom controls; command-valued selects are
253
+ implemented.
254
+ - Verify top-layer Popover focus, caret, viewport, writing-mode, and ShadowRoot
255
+ behavior in all target browsers; use Dialog only for modal extension UI.
@@ -0,0 +1,214 @@
1
+ // One shadow root per editor for everything the editor draws: its toolbar, its
2
+ // contextual handles, its forms and dialogs.
3
+ //
4
+ // An editor is chrome inside someone else's document. Sharing one encapsulated
5
+ // root means the page's own CSS cannot reach any of it, none of its styles can
6
+ // leak out, and the application sees a single element rather than one per piece
7
+ // of UI. Each piece owns its own layout; what they share is one skin, `.panel`,
8
+ // because to the eye the editor's chrome is one thing and not five.
9
+ export class Chrome {
10
+ #host;
11
+ #root;
12
+ #home;
13
+ #context;
14
+ #document;
15
+ #target = null;
16
+ #styles = new Set();
17
+ #controller;
18
+ #observer;
19
+ #drawing;
20
+ #connected = true;
21
+
22
+ constructor(root, {name = 'chrome'} = {}) {
23
+ const isDocument = root?.nodeType === Node.DOCUMENT_NODE;
24
+ const isShadow = root?.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
25
+ if (!isDocument && !isShadow) throw new TypeError('Editor chrome requires a document or shadow root');
26
+ const document = isDocument ? root : root.ownerDocument;
27
+ this.#context = root;
28
+ this.#document = document;
29
+ this.#host = document.createElement('div');
30
+ this.#host.dataset.u2RteChrome = name;
31
+ this.#host.contentEditable = 'false';
32
+ this.#root = this.#host.attachShadow({mode: 'open'});
33
+ const style = document.createElement('style');
34
+ // The host is inert ground: only what a piece of UI places inside it
35
+ // takes the pointer.
36
+ //
37
+ // Important, because a normal declaration in a shadow tree loses to the
38
+ // outer document for the host element: a page rule as ordinary as
39
+ // `[popover] { border: solid }` would otherwise draw a frame around the
40
+ // whole editor. Important declarations reverse that order, so this is
41
+ // what makes the host unreachable rather than merely encapsulated.
42
+ style.textContent = `
43
+ :host {
44
+ all: initial !important;
45
+ /* The chrome's own text size lives here, and everything inside measures
46
+ itself in em against it — one knob, no arithmetic. Page-relative units
47
+ would instead let a site's root font resize the editor's furniture by
48
+ accident. Important like the rest of the reset, because a rule in the
49
+ outer document outranks one from this shadow tree; the size stays settable
50
+ all the same, because "all" does not touch custom properties and the host
51
+ inherits them from the page. */
52
+ font: var(--u2-rte-ui-size, 14px)/1.2 system-ui, sans-serif !important;
53
+ inset: 0 !important;
54
+ pointer-events: none !important;
55
+ position: fixed !important;
56
+ }
57
+ /* A part states its own display, and an id beats any rule that would hide it —
58
+ so hiding says so outright, once, for everything drawn in here. */
59
+ [hidden] { display: none !important; }
60
+ .panel {
61
+ backdrop-filter: blur(.57em);
62
+ background: color-mix(in srgb, Canvas 92%, transparent);
63
+ border: 1px solid color-mix(in srgb, CanvasText 24%, transparent);
64
+ border-radius: .51em;
65
+ box-shadow: 0 .34em 1.14em #0006;
66
+ color: CanvasText;
67
+ inset: auto;
68
+ margin: 0;
69
+ padding: .23em;
70
+ pointer-events: auto;
71
+ position: fixed;
72
+ z-index: 2147483647;
73
+
74
+ button, input, select { color: inherit; font: inherit; }
75
+ button, select { background: transparent; border: 0; }
76
+ :disabled { opacity: .4; }
77
+ }`;
78
+ this.#root.append(style);
79
+ this.#home = isDocument ? document.body || document.documentElement : root;
80
+ this.#home.append(this.#host);
81
+ if (typeof this.#host.showPopover === 'function') {
82
+ this.#host.popover = 'manual';
83
+ this.#host.showPopover();
84
+ }
85
+ this.#controller = new document.defaultView.AbortController();
86
+ const listen = {capture: true, signal: this.#controller.signal};
87
+ for (const type of ['toggle', 'fullscreenchange']) {
88
+ document.addEventListener(type, this.#topLayerChange, listen);
89
+ if (!isDocument) root.addEventListener(type, this.#topLayerChange, listen);
90
+ }
91
+ // Dialogs expose their open state synchronously as an attribute, but
92
+ // current engines do not all dispatch their newer toggle event yet.
93
+ this.#observer = new document.defaultView.MutationObserver(this.#topLayerChange);
94
+ // Whatever the editor draws is the most recent thing the user triggered,
95
+ // so it belongs in front of anything the page put in the top layer before.
96
+ this.#drawing = new document.defaultView.MutationObserver(records => {
97
+ if (records.some(record => !record.target.hidden)) this.raise();
98
+ });
99
+ this.#drawing.observe(this.#root, {subtree: true, attributes: true, attributeFilter: ['hidden']});
100
+ }
101
+
102
+ get element() { return this.#host; }
103
+ get root() { return this.#root; }
104
+ get target() { return this.#target; }
105
+ get connected() { return this.#connected; }
106
+
107
+ // Follows the target only across native top-layer boundaries. In ordinary
108
+ // content the chrome stays in its original document or shadow root.
109
+ follow(target = null) {
110
+ if (target !== null && target?.nodeType !== Node.ELEMENT_NODE) {
111
+ throw new TypeError('Editor chrome can follow only an element or null');
112
+ }
113
+ if (target && (target.ownerDocument !== this.#document || target.getRootNode() !== this.#context)) {
114
+ throw new RangeError('Editor chrome target must belong to its root');
115
+ }
116
+ this.#target = target;
117
+ this.#observer.disconnect();
118
+ for (let element = target; element?.nodeType === Node.ELEMENT_NODE; element = composedParent(element)) {
119
+ this.#observer.observe(element, {attributes: true, attributeFilter: ['open']});
120
+ }
121
+ this.#place();
122
+ return this;
123
+ }
124
+
125
+ // Registers one stylesheet under a key, once, however often it is offered.
126
+ style(key, css) {
127
+ if (!this.#connected || this.#styles.has(key)) return this;
128
+ this.#styles.add(key);
129
+ const style = this.#document.createElement('style');
130
+ style.id = `${key}-style`;
131
+ style.textContent = css;
132
+ this.#root.append(style);
133
+ return this;
134
+ }
135
+
136
+ // One node and one stylesheet under one name. The key is the element's id, so
137
+ // a piece of chrome writes `#link input` and is never named twice; inside an
138
+ // encapsulated root shared by nothing else, that is as unique as it gets.
139
+ part(key, css = '', tag = 'div') {
140
+ if (this.#root.getElementById(key)) throw new RangeError(`Editor chrome already draws: ${key}`);
141
+ const element = this.#document.createElement(tag);
142
+ element.id = key;
143
+ if (css) this.style(key, css);
144
+ this.#root.append(element);
145
+ return element;
146
+ }
147
+
148
+ dispose() {
149
+ if (!this.#connected) return;
150
+ this.#controller.abort();
151
+ this.#observer.disconnect();
152
+ this.#drawing.disconnect();
153
+ this.#host.remove();
154
+ // Emptying the root as well: a detached shadow tree still answers
155
+ // queries, and a disposed editor must have nothing left to find.
156
+ this.#root.replaceChildren();
157
+ this.#styles.clear();
158
+ this.#target = null;
159
+ this.#connected = false;
160
+ }
161
+
162
+ [Symbol.dispose]() {
163
+ this.dispose();
164
+ }
165
+
166
+ // Back to the front of the top layer, which is ordered by when each member
167
+ // was shown. Two manual popovers that never close — an editor's chrome and a
168
+ // host application's own menu — would otherwise keep the order they happened
169
+ // to be created in, whatever is being used right now.
170
+ raise() {
171
+ if (!this.#connected || typeof this.#host.showPopover !== 'function') return false;
172
+ if (!this.#host.matches(':popover-open')) return this.#place();
173
+ this.#host.hidePopover();
174
+ this.#host.showPopover();
175
+ return true;
176
+ }
177
+
178
+ #place() {
179
+ if (!this.#connected) return false;
180
+ const parent = topLayer(this.#target) || this.#home;
181
+ if (parent === this.#host.parentNode) {
182
+ if (typeof this.#host.showPopover === 'function' && !this.#host.matches(':popover-open')) {
183
+ this.#host.showPopover();
184
+ }
185
+ return false;
186
+ }
187
+ if (this.#host.matches(':popover-open')) this.#host.hidePopover();
188
+ parent.append(this.#host);
189
+ if (typeof this.#host.showPopover === 'function') this.#host.showPopover();
190
+ return true;
191
+ }
192
+
193
+ #topLayerChange = () => this.#place();
194
+ }
195
+
196
+ // The closest boundary is the one that owns the target's current top-layer
197
+ // context. Walking through shadow hosts keeps the rule identical for document
198
+ // and ShadowRoot editors.
199
+ export function topLayer(target) {
200
+ for (let element = target; element?.nodeType === Node.ELEMENT_NODE; element = composedParent(element)) {
201
+ for (const selector of [':fullscreen', ':modal', ':popover-open']) {
202
+ try {
203
+ if (element.matches(selector)) return element;
204
+ } catch {
205
+ // Unsupported top-layer states simply cannot be active.
206
+ }
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+
212
+ function composedParent(element) {
213
+ return element.parentElement || element.getRootNode()?.host || null;
214
+ }