u2ui 1.5.2 → 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 (144) 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 +16 -4
  119. package/u2/enhance.js +2 -1
  120. package/u2/plan-scoped-refactoring.md +133 -304
  121. package/u2/projects.json +18 -2
  122. package/u2/tools/update.repos.json.js +1 -1
  123. package/u2/utils.js +0 -16
  124. package/el/qrcode/package.json +0 -22
  125. /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
  126. /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
  127. /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
  128. /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
  129. /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
  130. /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
  131. /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
  132. /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
  133. /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
  134. /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
  135. /package/js/{rte → rte0}/old/Rte/main.css +0 -0
  136. /package/js/{rte → rte0}/old/old.rte.js +0 -0
  137. /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
  138. /package/js/{rte → rte0}/src/fixes.js +0 -0
  139. /package/js/{rte → rte0}/src/rte.js +0 -0
  140. /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
  141. /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
  142. /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
  143. /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
  144. /package/js/{rte → rte0}/src/util/indentHtml.js +0 -0
@@ -0,0 +1,824 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>RTE playground</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font: 13px/1.35 system-ui, sans-serif;
11
+ --border: color-mix(in srgb, CanvasText 24%, transparent);
12
+ --panel: color-mix(in srgb, CanvasText 5%, Canvas);
13
+ }
14
+
15
+ * { box-sizing: border-box; }
16
+ body { margin: 0; padding: .8rem; }
17
+ h1 { font-size: 1.25rem; margin-block: 0 .65rem; }
18
+
19
+ .controls {
20
+ display: flex;
21
+ flex-wrap: wrap;
22
+ gap: .5rem;
23
+ margin-block-end: .65rem;
24
+
25
+ & fieldset {
26
+ align-items: end;
27
+ border: 1px solid var(--border);
28
+ display: flex;
29
+ flex-wrap: wrap;
30
+ gap: .4rem;
31
+ margin: 0;
32
+ min-width: 0;
33
+ padding: .35rem .5rem .5rem;
34
+ }
35
+ & legend { padding-inline: .2rem; }
36
+ & label { display: grid; gap: .15rem; }
37
+ & button, & input, & select { font: inherit; min-height: 1.8rem; }
38
+ & button { padding-inline: .55rem; }
39
+ }
40
+
41
+ .grid {
42
+ display: grid;
43
+ gap: .65rem;
44
+ grid-template-columns: minmax(20rem, 1.2fr) minmax(18rem, 1fr);
45
+
46
+ @media (width < 800px) { grid-template-columns: 1fr; }
47
+ }
48
+
49
+ .panel {
50
+ background: var(--panel);
51
+ border: 1px solid var(--border);
52
+ border-radius: .35rem;
53
+ min-width: 0;
54
+ padding: .65rem;
55
+
56
+ & h2 { font-size: .9rem; margin-block: 0 .4rem; }
57
+ & textarea { inline-size: 100%; min-block-size: 6rem; resize: vertical; }
58
+ & pre, & textarea {
59
+ font: 11px/1.35 ui-monospace, monospace;
60
+ tab-size: 2;
61
+ }
62
+ & pre {
63
+ margin: 0;
64
+ max-block-size: 18rem;
65
+ overflow: auto;
66
+ white-space: pre-wrap;
67
+ }
68
+ }
69
+
70
+ #tree {
71
+ & [data-position="anchor"] { color: #16833b; }
72
+ & [data-position="focus"] { color: #1672c4; }
73
+ & [data-position] { font-weight: 900; }
74
+ }
75
+
76
+ #stage { background: Canvas; padding: .5rem; }
77
+ /* The configuration stands beside what it configures, so a change and its
78
+ effect are in view at once. */
79
+ .prototype {
80
+ align-items: start;
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ gap: .8rem;
84
+ }
85
+ .prototype > #editor-prototype {
86
+ flex: 1 1 22rem;
87
+ min-inline-size: 0;
88
+ }
89
+ #host-settings {
90
+ display: flex;
91
+ flex: 0 1 19rem;
92
+ flex-direction: column;
93
+ gap: .5rem;
94
+ }
95
+ #host-settings fieldset {
96
+ border: 1px solid var(--border);
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ gap: .1rem .7rem;
100
+ margin: 0;
101
+ }
102
+ #host-settings #host-properties {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: .35rem;
106
+ }
107
+ #host-settings #host-properties label {
108
+ display: flex;
109
+ flex-direction: column;
110
+ }
111
+ #host-settings :is(select, input[type=text]) { inline-size: 100%; }
112
+ #host-settings fieldset label { white-space: nowrap; }
113
+ #editor, #editor-prototype {
114
+ border: 2px solid color-mix(in srgb, Highlight 55%, var(--border));
115
+ min-block-size: 6rem;
116
+ outline: none;
117
+ padding: .5rem;
118
+
119
+ &:focus { border-color: Highlight; }
120
+ & [contenteditable] { outline: 1px dashed orange; }
121
+ /* The demo content classes need a visible effect to be inspectable. */
122
+ & table { border-collapse: collapse; }
123
+ & :is(td, th) { border: 1px solid color-mix(in srgb, CanvasText 30%, transparent); padding: .2rem .4rem; }
124
+ & .lead { font-size: 1.15em; }
125
+ & .caption { font-size: .85em; opacity: .7; }
126
+ & .brandColor { color: color-mix(in srgb, Highlight 70%, CanvasText); }
127
+ & .x {
128
+ background: color-mix(in srgb, #ffbf00 36%, transparent);
129
+ border-radius: .2em;
130
+ box-decoration-break: clone;
131
+ padding-inline: .12em;
132
+ }
133
+ }
134
+
135
+ #mark-toggle {
136
+ border-color: color-mix(in srgb, #ffbf00 70%, var(--border));
137
+
138
+ &[aria-pressed="true"] { background: color-mix(in srgb, #ffbf00 36%, ButtonFace); }
139
+ &[aria-pressed="mixed"] {
140
+ background: linear-gradient(135deg, color-mix(in srgb, #ffbf00 36%, ButtonFace) 50%, ButtonFace 50%);
141
+ }
142
+ }
143
+
144
+ #toolbar {
145
+ align-items: center;
146
+ backdrop-filter: blur(.5rem);
147
+ background: color-mix(in srgb, Canvas 88%, transparent);
148
+ border: 1px solid color-mix(in srgb, Highlight 55%, var(--border));
149
+ border-radius: .55rem;
150
+ box-shadow: 0 .35rem 1.2rem #0004;
151
+ display: flex;
152
+ gap: .2rem;
153
+ padding: .25rem;
154
+ position: fixed;
155
+ z-index: 10;
156
+
157
+ &[hidden] { display: none; }
158
+ & button {
159
+ align-items: center;
160
+ background: transparent;
161
+ border: 0;
162
+ border-radius: .35rem;
163
+ display: inline-flex;
164
+ font: inherit;
165
+ gap: .25rem;
166
+ min-block-size: 2rem;
167
+ padding-inline: .55rem;
168
+ }
169
+ & button:hover:not(:disabled) { background: color-mix(in srgb, Highlight 16%, transparent); }
170
+ & button:disabled { opacity: .38; }
171
+ & button[aria-pressed="true"] { background: color-mix(in srgb, #ffbf00 36%, transparent); }
172
+ & button[aria-pressed="mixed"] {
173
+ background: linear-gradient(135deg, color-mix(in srgb, #ffbf00 36%, transparent) 50%, transparent 50%);
174
+ }
175
+ & .symbol { font-size: 1.15em; font-weight: 700; }
176
+ }
177
+
178
+ #status { margin-block: .4rem 0; }
179
+ details { font-size: .9em; }
180
+ details > summary { cursor: pointer; }
181
+ .wide { grid-column: 1 / -1; }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <h1>RTE playground</h1>
186
+ <p><a href=hosts.html>Open the editing-host matrix</a></p>
187
+
188
+ <div class="controls">
189
+ <fieldset>
190
+ <legend>Scenario</legend>
191
+ <label>Example
192
+ <select id="scenario">
193
+ <option value="combined">Nested p/div regression</option>
194
+ <option value="inline">Inline root run</option>
195
+ <option value="generic">Generic text block</option>
196
+ <option value="blocks">Redundant block wrapper</option>
197
+ <option value="list">List with stray blocks</option>
198
+ <option value="lift">Meaningful invalid block</option>
199
+ <option value="nested">Nested editable boundary</option>
200
+ <option value="marks">CSS class across inline elements</option>
201
+ </select>
202
+ </label>
203
+ <button id="reset" type="button">Reset</button>
204
+ </fieldset>
205
+ <fieldset>
206
+ <legend>CSS policy</legend>
207
+ <label>Cleanup
208
+ <select id="level">
209
+ <option>none</option>
210
+ <option>minimal</option>
211
+ <option selected>structural</option>
212
+ <option>canonical</option>
213
+ </select>
214
+ </label>
215
+ <label>Block
216
+ <input id="block" value="auto" size="6">
217
+ </label>
218
+ <label>Clean on
219
+ <input id="clean-on" value="input paste drop command" size="20">
220
+ </label>
221
+ </fieldset>
222
+ <fieldset>
223
+ <legend>Normalize</legend>
224
+ <button id="analyze" type="button">Analyze</button>
225
+ <button id="step" type="button">One step</button>
226
+ <button id="normalize" type="button">All</button>
227
+ </fieldset>
228
+ <fieldset>
229
+ <legend>Input pipeline</legend>
230
+ <label>Input type
231
+ <select id="input-type">
232
+ <option>insertText</option>
233
+ <option>insertParagraph</option>
234
+ <option>insertLineBreak</option>
235
+ <option>deleteContentBackward</option>
236
+ <option>deleteContentForward</option>
237
+ <option>insertFromPaste</option>
238
+ <option>insertFromDrop</option>
239
+ </select>
240
+ </label>
241
+ <button id="dispatch-input" type="button">Dispatch</button>
242
+ </fieldset>
243
+ </div>
244
+
245
+ <div id="toolbar" aria-label="Explicit editor formatting">
246
+ <button id="bold-toggle" type="button" data-command="bold" data-state data-shortcut="b" aria-label="Bold">
247
+ <span class="symbol" aria-hidden="true">B</span>
248
+ </button>
249
+ <button id="mark-toggle" type="button" data-command="toggleX" data-state data-shortcut="x" aria-label="Toggle .x mark">
250
+ <span class="symbol" aria-hidden="true">x</span> Toggle
251
+ </button>
252
+ <button id="mark-apply" type="button" data-command="applyX" aria-label="Apply .x mark">
253
+ <span class="symbol" aria-hidden="true">+</span> Apply
254
+ </button>
255
+ <button id="mark-remove" type="button" data-command="removeX" aria-label="Remove .x mark">
256
+ <span class="symbol" aria-hidden="true">−</span> Remove
257
+ </button>
258
+ </div>
259
+
260
+ <div class="grid">
261
+ <section class="panel">
262
+ <h2>Editable DOM — explicit integration</h2>
263
+ <div id="stage"></div>
264
+ <p id="status" aria-live="polite"></p>
265
+ </section>
266
+
267
+ <section class="panel">
268
+ <h2>DOM tree and selection</h2>
269
+ <pre id="tree"></pre>
270
+ </section>
271
+
272
+ <section class="panel">
273
+ <h2>Serialized inner HTML</h2>
274
+ <textarea id="html" spellcheck="false"></textarea>
275
+ <button id="apply-html" type="button">Parse and apply HTML</button>
276
+ </section>
277
+
278
+ <section class="panel">
279
+ <h2>Plans and executed actions</h2>
280
+ <pre id="log">Load a scenario, then analyze or normalize it.</pre>
281
+ </section>
282
+
283
+ <section class="panel wide">
284
+ <h2>One-import editor prototype</h2>
285
+ <div class="prototype">
286
+ <div id="editor-prototype" contenteditable style="--u2-rte:true; --u2-rte-toolbar:undo redo block style bold italic underline strike code link bullets numbers outdent indent rule insertTable imageOriginal unstyle breaks source; --u2-rte-classes:lead, caption, brandColor; --u2-rte-show-breaks:true">
287
+ <p>Select text to format it, choose its block style, or press Enter inside this list:</p>
288
+ <ul><li>List item</li><li>Put the caret here and press the indent control</li></ul>
289
+ <p>Turn these paragraphs into a list, then lift them out again.</p>
290
+ <p>Select this text and link it, or put the caret in <a href="/existing">this link</a> to edit it.</p>
291
+ <p>Select a few words and give them a <span class=brandColor>content class</span> from the style list.</p>
292
+ <p><img src="data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='90'%3E%3Crect width='160' height='90' fill='%23bcd'/%3E%3C/svg%3E" width="160" height="90" alt="Click the image and drag a corner"></p>
293
+ <table><tbody><tr><th>Head</th><th>Head</th></tr><tr><td>Put the caret in a cell</td><td>and add or drop rows and columns</td></tr></tbody></table>
294
+ <p><span class="pasted" style="color:#b30"><strong>Select this paste-like formatting</strong></span> and click T× repeatedly.</p>
295
+ <p>The faint arrow after this line is a visible break.<br>It is not part of the editable HTML.</p>
296
+ </div>
297
+ <form id="host-settings" aria-label="Host configuration">
298
+ <fieldset id="toolbar-controls"><legend><code>--u2-rte-toolbar</code></legend></fieldset>
299
+ <div id="host-properties"></div>
300
+ </form>
301
+ </div>
302
+ <p><small id="history-state">History: focus the surface to start recording.</small></p>
303
+ <p><small>This surface is wired by <code>rte.js</code>, optional <code>blocks.js</code>/<code>classes.js</code>/<code>unstyle.js</code>/<code>breaks.js</code>/<code>link.js</code>/<code>tables.js</code>/<code>images.js</code>/<code>source.js</code>, and CSS variables.</small></p>
304
+ </section>
305
+
306
+ <details class="panel wide">
307
+ <summary>Notes</summary>
308
+ <p>
309
+ The first surface is the explicit integration: its commands and toolbar
310
+ markup are listed in this page for inspection, while their behavior
311
+ comes from the production modules. The lower prototype is the
312
+ convention integration and wires itself from one import plus CSS.
313
+ Invalid nested structures are built with DOM methods because the HTML
314
+ parser repairs some of them immediately. “Parse and apply HTML” is
315
+ deliberately available to compare parser behavior with RTE behavior.
316
+ Place a selection inside the editor before stepping to observe mapped
317
+ range boundaries. Dispatch sends a cancelable <code>beforeinput</code>
318
+ and, unless a command replaced it, the following <code>input</code>.
319
+ Input types without a command run cleanup policies only; they do not
320
+ imitate the browser's native DOM mutation, so edit or apply HTML first.
321
+ The prototype toolbar lists every ready control, so the order in
322
+ <code>--u2-rte-toolbar</code> is the order shown. Undo and redo also
323
+ work from Ctrl/Command+Z and +Y; the readout under the surface reports
324
+ how many entries the history holds and where it stands, which makes the
325
+ grouping visible: continuous typing coalesces, while each command,
326
+ paste, and drop is its own step. List controls consult the content
327
+ model, so they disable themselves in a host that does not allow them.
328
+ The <code>&lt;/&gt;</code> control opens the HTML source of this surface
329
+ in a modal dialog, positioned at the caret. Applying it parses the text
330
+ through the sanitizer, never through <code>innerHTML</code>, and lands
331
+ as one undo step. Its text area is wrapped in <code>&lt;u2-code&gt;</code>,
332
+ so the source is syntax highlighted once that element has loaded.
333
+ The link control opens a contextual form at the selection: a caret
334
+ inside a link edits that whole link, a selection creates one. Its
335
+ address is a plain text field, so relative paths, fragments, and
336
+ application schemes are accepted and the sanitizer decides what
337
+ survives.
338
+ Clicking the image selects it and frames it with corner handles. The
339
+ drag moves the frame only and the size is written once when it is
340
+ released, so a resize is one undo step rather than a trail of them; the
341
+ corners keep the proportion. Putting the caret in a table cell brings up handles on the table itself:
342
+ rows down its left edge, columns along its top, lined up with the cell
343
+ you are in. They name no tag — sections, rows and cells come from the
344
+ content model's default child — and a cell spanning several columns
345
+ makes the counted actions unavailable rather than shifting the wrong
346
+ cells. Inserting a table stays a toolbar action, because it applies
347
+ where no table is.
348
+ Remove formatting walks a ladder that never runs out: foreign styles,
349
+ presentational attributes and classes, then the declared content
350
+ classes, the remaining semantic inline elements, and finally the
351
+ structure itself — lists, tables, quotes and headings all reduce to
352
+ plain blocks. Every control's key works whether or not it is listed in
353
+ <code>--u2-rte-toolbar</code>; hover a control to see its shortcut.
354
+ The style list offers exactly the names in
355
+ <code>--u2-rte-classes</code>. That one declaration is also what the
356
+ sanitizer keeps from pasted HTML and what remove-format leaves alone, so
357
+ a class the host knows is never offered without being allowed, and never
358
+ cleaned away as foreign.
359
+ Bold recognizes both <code>&lt;strong&gt;</code> and <code>&lt;b&gt;</code>,
360
+ creates canonical <code>&lt;strong&gt;</code>, and unwraps either form on
361
+ removal. The class-mark buttons reuse a fully selected inline element;
362
+ otherwise they create a <code>&lt;span class="x"&gt;</code>.
363
+ </p>
364
+ </details>
365
+ </div>
366
+
367
+ <script type="module">
368
+ import {editor as conventionEditor} from '../rte.js';
369
+ import '../blocks.js';
370
+ import '../classes.js';
371
+ import '../link.js';
372
+ import '../images.js';
373
+ import '../tables.js';
374
+ import '../source.js';
375
+ import '../breaks.js';
376
+ import '../unstyle.js';
377
+ import {rangeRect} from '../src/browser/range-rect.js';
378
+ import {Commands} from '../src/command/commands.js';
379
+ import {deleteBackward, deleteForward} from '../src/command/delete.js';
380
+ import {enter, lineBreak} from '../src/command/enter.js';
381
+ import {applyMark, removeMark} from '../src/command/mark.js';
382
+ import {PendingMarks} from '../src/command/pending-marks.js';
383
+ import {config} from '../src/config/config.js';
384
+ import {Rte} from '../src/core/core.js';
385
+ import {InputPipeline} from '../src/input/input-pipeline.js';
386
+ import {boldHtml} from '../src/mark/standard.js';
387
+ import {MarkAdapter} from '../src/mark/dom-adapter.js';
388
+ import {MarkType} from '../src/mark/mark.js';
389
+ import {htmlModel} from '../src/model/html/html-model.js';
390
+ import {Normalizer} from '../src/normalize/normalizer/normalizer.js';
391
+ import {RepairPlanner} from '../src/normalize/repair/repair-planner.js';
392
+ import {EditRange} from '../src/selection/range/edit-range.js';
393
+ import {isEditingBoundary} from '../src/selection/ownership/ownership.js';
394
+ import {Point} from '../src/selection/point/point.js';
395
+ import {SelectionSnapshot} from '../src/selection/snapshot.js';
396
+ import {Toolbar} from '../src/ui/toolbar.js';
397
+
398
+ const controls = {
399
+ scenario: document.querySelector('#scenario'),
400
+ level: document.querySelector('#level'),
401
+ block: document.querySelector('#block'),
402
+ cleanOn: document.querySelector('#clean-on'),
403
+ analyze: document.querySelector('#analyze'),
404
+ step: document.querySelector('#step'),
405
+ normalize: document.querySelector('#normalize'),
406
+ inputType: document.querySelector('#input-type'),
407
+ dispatchInput: document.querySelector('#dispatch-input'),
408
+ toolbar: document.querySelector('#toolbar'),
409
+ reset: document.querySelector('#reset'),
410
+ html: document.querySelector('#html'),
411
+ applyHtml: document.querySelector('#apply-html'),
412
+ stage: document.querySelector('#stage'),
413
+ status: document.querySelector('#status'),
414
+ tree: document.querySelector('#tree'),
415
+ log: document.querySelector('#log'),
416
+ };
417
+ const core = new Rte(document, {auto: false});
418
+ const classMark = new MarkType('x');
419
+ const classHtml = new MarkAdapter(classMark, {
420
+ selector: '.x',
421
+ tag: 'span',
422
+ reuse: true,
423
+ write: element => element.classList.add('x'),
424
+ clear: element => element.classList.remove('x'),
425
+ });
426
+ const scenarios = {
427
+ combined: {
428
+ tag: 'div',
429
+ block: 'auto',
430
+ build(host) {
431
+ const paragraph = document.createElement('p');
432
+ const nested = document.createElement('div');
433
+ const following = document.createElement('div');
434
+ nested.textContent = 'test';
435
+ following.textContent = 'hallo';
436
+ paragraph.append(nested, ' abc');
437
+ host.append(paragraph, following);
438
+ },
439
+ },
440
+ inline: {tag: 'div', block: 'auto', html: 'one <strong>two</strong> three'},
441
+ generic: {tag: 'section', block: 'auto', html: '<div>hello <em>world</em></div>'},
442
+ blocks: {tag: 'section', block: 'auto', html: '<div><p>one</p><p>two</p></div>'},
443
+ list: {tag: 'ul', block: 'auto', html: '<p>one</p><p>two</p>'},
444
+ lift: {
445
+ tag: 'div',
446
+ block: 'auto',
447
+ build(host) {
448
+ const paragraph = document.createElement('p');
449
+ const section = document.createElement('section');
450
+ section.className = 'meaningful';
451
+ section.textContent = 'block';
452
+ paragraph.append('before ', section, ' after');
453
+ host.append(paragraph);
454
+ },
455
+ },
456
+ nested: {tag: 'div', block: 'auto', html: '<div contenteditable><div>nested</div></div><div>outer</div>'},
457
+ marks: {
458
+ tag: 'ul',
459
+ block: 'auto',
460
+ html: '<li>hello</li><li><b>dear</b> world</li>',
461
+ select(host) {
462
+ getSelection().setBaseAndExtent(host.children[0].firstChild, 2, host.querySelector('b').firstChild, 4);
463
+ },
464
+ },
465
+ };
466
+ let editor;
467
+ let observer;
468
+ let surface;
469
+ let commands;
470
+ let pipeline;
471
+ let command;
472
+ let commandMessage;
473
+ const toolbar = new Toolbar(core, controls.toolbar, {
474
+ commands: target => target === surface ? commands : null,
475
+ place: placeToolbar,
476
+ });
477
+
478
+ controls.scenario.addEventListener('change', load);
479
+ controls.reset.addEventListener('click', load);
480
+ controls.analyze.addEventListener('click', analyze);
481
+ controls.step.addEventListener('click', () => execute('step'));
482
+ controls.normalize.addEventListener('click', () => execute('normalize'));
483
+ controls.dispatchInput.addEventListener('click', dispatchInput);
484
+ for (const control of [controls.level, controls.block, controls.cleanOn]) {
485
+ control.addEventListener('input', () => editor && options());
486
+ }
487
+ controls.applyHtml.addEventListener('click', () => {
488
+ editor.innerHTML = controls.html.value;
489
+ show('HTML parsed by the browser.');
490
+ });
491
+ document.addEventListener('selectionchange', refreshTree);
492
+
493
+ function load() {
494
+ const scenario = scenarios[controls.scenario.value];
495
+ observer?.disconnect();
496
+ pipeline?.dispose();
497
+ surface?.dispose();
498
+ editor = document.createElement(scenario.tag);
499
+ editor.id = 'editor';
500
+ editor.contentEditable = 'true';
501
+ controls.block.value = scenario.block;
502
+ controls.stage.replaceChildren(editor);
503
+ if (scenario.build) scenario.build(editor);
504
+ else editor.innerHTML = scenario.html;
505
+ options();
506
+ surface = core.add(editor);
507
+ const pending = new PendingMarks(surface);
508
+ commands = new Commands(surface, {commands: {
509
+ deleteBackward,
510
+ deleteForward,
511
+ enter,
512
+ lineBreak,
513
+ insertText: pending.insertText,
514
+ bold: {...pending.toggle(boldHtml), shortcut: 'ctrl+b'},
515
+ toggleX: {...pending.toggle(classHtml), shortcut: 'ctrl+x'},
516
+ applyX: applyMark(classHtml),
517
+ removeX: removeMark(classHtml),
518
+ }});
519
+ pipeline = new InputPipeline(surface, {commands});
520
+ editor.addEventListener('u2-rte-normalize', inputNormalized);
521
+ editor.addEventListener('u2-rte-command', commandExecuted);
522
+ editor.addEventListener('u2-rte-change', changeExecuted);
523
+ observer = new MutationObserver(refresh);
524
+ observer.observe(editor, {subtree: true, childList: true, characterData: true, attributes: true});
525
+ scenario.select?.(editor);
526
+ if (scenario.select) core.sync();
527
+ toolbar.refresh();
528
+ controls.log.textContent = 'Scenario loaded.';
529
+ show(`Loaded ${controls.scenario.selectedOptions[0].text}.`);
530
+ }
531
+
532
+ function options() {
533
+ editor.style.setProperty('--u2-rte-block', controls.block.value.trim() || 'none');
534
+ editor.style.setProperty('--u2-rte-cleanup', controls.level.value);
535
+ editor.style.setProperty('--u2-rte-clean-on', controls.cleanOn.value.trim());
536
+ const settings = config(editor);
537
+ return {
538
+ block: settings.block,
539
+ level: settings.cleanup,
540
+ };
541
+ }
542
+
543
+ function execute(method) {
544
+ const selection = trackedSelection();
545
+ try {
546
+ const result = new Normalizer(editor, options())[method]({points: selection.points});
547
+ selection.restore(result.map);
548
+ logResult(result);
549
+ show(`${method === 'step' ? 'Step' : 'Normalization'}: ${result.actions.length} action(s), stable: ${result.stable}.`);
550
+ } catch (error) {
551
+ controls.log.textContent = error.stack || String(error);
552
+ show(`${error.name}: ${error.message}`);
553
+ }
554
+ }
555
+
556
+ function dispatchInput() {
557
+ options();
558
+ const inputType = controls.inputType.value;
559
+ command = null;
560
+ controls.status.textContent = `Dispatched ${inputType}; no cleanup ran for this trigger.`;
561
+ const before = new InputEvent('beforeinput', {bubbles: true, cancelable: true, inputType});
562
+ editor.dispatchEvent(before);
563
+ if (!before.defaultPrevented) editor.dispatchEvent(new InputEvent('input', {bubbles: true, inputType}));
564
+ refresh();
565
+ if (command) controls.status.textContent = `${inputType} ran command ${command}.`;
566
+ }
567
+
568
+ function commandExecuted(event) {
569
+ const name = command = event.detail.name;
570
+ const action = {applyX: 'Applied', removeX: 'Removed', toggleX: 'Toggled', bold: 'Toggled bold'}[name];
571
+ if (!action) return;
572
+ const changed = event.detail.result;
573
+ controls.log.textContent = `${name}: ${Array.isArray(changed) ? changed.length : 0} element(s) changed.`;
574
+ commandMessage = name === 'bold' ? `${action} on the selected text.` : `${action} .x on the selected text.`;
575
+ }
576
+
577
+ function changeExecuted() {
578
+ if (!commandMessage) return;
579
+ const message = commandMessage;
580
+ commandMessage = null;
581
+ show(message);
582
+ }
583
+
584
+ function placeToolbar(element, target) {
585
+ const range = target.selection?.range();
586
+ if (!range) return;
587
+ const anchor = rangeRect(range, {root: target.element});
588
+ const box = element.getBoundingClientRect();
589
+ const gap = 8;
590
+ const left = Math.max(gap, Math.min(innerWidth - box.width - gap, anchor.left + anchor.width / 2 - box.width / 2));
591
+ const above = anchor.top - box.height - gap;
592
+ const top = above >= gap ? above : Math.min(innerHeight - box.height - gap, anchor.bottom + gap);
593
+ element.style.left = `${left}px`;
594
+ element.style.top = `${top}px`;
595
+ }
596
+
597
+ function inputNormalized(event) {
598
+ logResult(event.detail.result);
599
+ show(`${event.detail.inputType || event.detail.trigger}: ${event.detail.result.actions.length} action(s), stable: ${event.detail.result.stable}.`);
600
+ }
601
+
602
+ function logResult(result) {
603
+ const actions = result.actions.map((action, index) => `${index + 1}. ${describeAction(action)}`);
604
+ const issues = result.issues.map(issue => `! reject ${path(issue.node)}`);
605
+ controls.log.textContent = [...actions, ...issues].join('\n') || 'No repair required.';
606
+ }
607
+
608
+ function analyze() {
609
+ const planner = new RepairPlanner(editor, options());
610
+ const rows = [];
611
+ for (const parent of parentElements(editor)) {
612
+ for (const child of parent.childNodes) {
613
+ const plan = planner.plan(parent, child);
614
+ if (plan.type === 'keep') continue;
615
+ rows.push(`${path(child)} → ${describeAction(plan)}`);
616
+ }
617
+ }
618
+ controls.log.textContent = rows.join('\n') || 'No repair required.';
619
+ show(`${rows.length} non-keep plan(s).`);
620
+ }
621
+
622
+ function parentElements(scope) {
623
+ const result = [];
624
+ const visit = element => {
625
+ if (element !== scope && (isEditingBoundary(element) || htmlModel.atomic(element))) return;
626
+ for (const child of element.children) visit(child);
627
+ result.push(element);
628
+ };
629
+ visit(scope);
630
+ return result;
631
+ }
632
+
633
+ function trackedSelection() {
634
+ const snapshot = SelectionSnapshot.capture(getSelection(), editor);
635
+ if (!snapshot) return {points: [], restore() {}};
636
+ const range = snapshot.range();
637
+ const start = Point.fromRange(range, 'start');
638
+ const points = range.collapsed ? [start] : [start, Point.fromRange(range, 'end')];
639
+ return {
640
+ points,
641
+ restore(map) {
642
+ try {
643
+ EditRange.fromPoints(map.get(points[0]), map.get(points.at(-1)), editor).select(getSelection(), snapshot.backward);
644
+ } catch (error) {
645
+ controls.log.append(`\n! Selection restore failed: ${error.message}`);
646
+ }
647
+ },
648
+ };
649
+ }
650
+
651
+ function refresh() {
652
+ controls.html.value = editor.innerHTML;
653
+ refreshTree();
654
+ }
655
+
656
+ function refreshTree() {
657
+ if (!editor) return;
658
+ const selection = getSelection();
659
+ const owned = selection.rangeCount && editor.contains(selection.anchorNode) && editor.contains(selection.focusNode);
660
+ const positions = owned ? [
661
+ {name: 'anchor', node: selection.anchorNode, offset: selection.anchorOffset},
662
+ {name: 'focus', node: selection.focusNode, offset: selection.focusOffset},
663
+ ] : [];
664
+ const output = document.createDocumentFragment();
665
+ tree(editor, positions, 0, output);
666
+ if (owned) {
667
+ line(output, '');
668
+ line(output, `anchor: ${path(selection.anchorNode)} @ ${selection.anchorOffset}`);
669
+ line(output, `focus: ${path(selection.focusNode)} @ ${selection.focusOffset}`);
670
+ }
671
+ controls.tree.replaceChildren(output);
672
+ }
673
+
674
+ function tree(node, positions, depth, output) {
675
+ const pad = ' '.repeat(depth);
676
+ if (node.nodeType === Node.TEXT_NODE) {
677
+ const points = positions.filter(point => point.node === node).sort((a, b) => a.offset - b.offset);
678
+ const parts = [`${pad}#text "`];
679
+ let offset = 0;
680
+ for (const point of points) {
681
+ parts.push(escaped(node.data.slice(offset, point.offset)), marker(point));
682
+ offset = point.offset;
683
+ }
684
+ parts.push(`${escaped(node.data.slice(offset))}"`);
685
+ line(output, ...parts);
686
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
687
+ const attrs = [...node.attributes].map(attribute => ` ${attribute.name}=${JSON.stringify(attribute.value)}`).join('');
688
+ line(output, `${pad}<${node.localName}${attrs}>`);
689
+ for (let offset = 0; offset <= node.childNodes.length; offset++) {
690
+ const points = positions.filter(point => point.node === node && point.offset === offset);
691
+ if (points.length) line(output, ' '.repeat(depth + 1), ...points.map(marker));
692
+ if (node.childNodes[offset]) tree(node.childNodes[offset], positions, depth + 1, output);
693
+ }
694
+ } else {
695
+ line(output, `${pad}${node.nodeName}`);
696
+ }
697
+ }
698
+
699
+ function marker({name, offset}) {
700
+ const element = document.createElement('span');
701
+ element.dataset.position = name;
702
+ element.title = `${name} @ ${offset}`;
703
+ element.textContent = '┃';
704
+ return element;
705
+ }
706
+
707
+ function escaped(value) {
708
+ return JSON.stringify(value).slice(1, -1);
709
+ }
710
+
711
+ function line(output, ...parts) {
712
+ if (output.childNodes.length) output.append('\n');
713
+ output.append(...parts);
714
+ }
715
+
716
+ function path(node) {
717
+ if (node === editor) return editor.localName;
718
+ const parts = [];
719
+ for (let current = node; current && current !== editor; current = current.parentNode) {
720
+ const parent = current.parentNode;
721
+ if (!parent) break;
722
+ const index = Array.prototype.indexOf.call(parent.childNodes, current);
723
+ parts.unshift(`${current.nodeType === Node.TEXT_NODE ? '#text' : current.localName}[${index}]`);
724
+ }
725
+ return `${editor.localName}/${parts.join('/')}`;
726
+ }
727
+
728
+ function describeAction(action) {
729
+ const details = [];
730
+ if (action.tag) details.push(`<${action.tag}>`);
731
+ if (action.count) details.push(`${action.count} nodes`);
732
+ if (action.breaks) details.push('preserve breaks');
733
+ if (action.target) details.push(`to ${path(action.target)}`);
734
+ return `${action.type}${details.length ? ` (${details.join(', ')})` : ''}`;
735
+ }
736
+
737
+ function show(message) {
738
+ controls.status.textContent = message;
739
+ refresh();
740
+ }
741
+
742
+ /* Everything a host configures, configurable here: the panel writes the
743
+ custom properties onto the surface and asks the client to refresh. */
744
+ const HOST_PROPERTIES = [
745
+ ['toolbar-when', ['always', 'selection']],
746
+ ['toolbar-unavailable', ['disable', 'hide']],
747
+ ['cleanup', ['structural', 'none', 'minimal', 'canonical']],
748
+ ['elements', ['all', '@basic', '@content', '@article', '@document']],
749
+ ['import-elements', ['@content', '@basic', '@article', '@document', 'all']],
750
+ ['import-unstyle', ['classes', 'none', 'styles', 'attributes', 'formatting', 'contentClasses', 'inline']],
751
+ ['import-sanitize', ['policy', 'none']],
752
+ ];
753
+
754
+ /* The convention prototype reports its own history so grouping is visible. */
755
+ const prototype = document.querySelector('#editor-prototype');
756
+ const historyState = document.querySelector('#history-state');
757
+
758
+ function showHistory() {
759
+ const surface = conventionEditor.core.get(prototype);
760
+ const history = surface && conventionEditor.history(surface);
761
+ if (!history) {
762
+ historyState.textContent = 'History: focus the surface to start recording.';
763
+ return;
764
+ }
765
+ const moves = [history.canUndo && 'undo', history.canRedo && 'redo'].filter(Boolean);
766
+ historyState.textContent = `History: ${history.length} entries, at ${history.index + 1}`
767
+ + `. Available: ${moves.join(' and ') || 'nothing yet'}.`;
768
+ }
769
+
770
+ prototype.addEventListener('u2-rte-history', showHistory);
771
+ prototype.addEventListener('u2-rte-selectionchange', showHistory);
772
+ prototype.addEventListener('focusin', showHistory);
773
+
774
+ buildSettings();
775
+
776
+ function buildSettings() {
777
+ const listed = new Set(prototype.style.getPropertyValue('--u2-rte-toolbar').split(/[\s,]+/).filter(Boolean));
778
+ const controls = document.querySelector('#toolbar-controls');
779
+ /* The names come from the modules themselves, so the panel cannot drift. */
780
+ for (const control of conventionEditor.controls) {
781
+ const label = document.createElement('label');
782
+ const box = document.createElement('input');
783
+ box.type = 'checkbox';
784
+ box.value = control.name;
785
+ box.checked = listed.has(control.name);
786
+ box.addEventListener('change', writeToolbar);
787
+ label.append(box, ` ${control.name}`);
788
+ label.title = `${control.label} (${control.module})`;
789
+ controls.append(label);
790
+ }
791
+ const properties = document.querySelector('#host-properties');
792
+ for (const [name, values] of HOST_PROPERTIES) {
793
+ const label = document.createElement('label');
794
+ const select = document.createElement('select');
795
+ for (const value of values) select.append(new Option(value, value));
796
+ select.value = prototype.style.getPropertyValue(`--u2-rte-${name}`).trim() || values[0];
797
+ select.addEventListener('change', () => write(name, select.value));
798
+ label.append(`--u2-rte-${name}`, select);
799
+ properties.append(label);
800
+ }
801
+ const classes = document.createElement('label');
802
+ const input = document.createElement('input');
803
+ input.type = 'text';
804
+ input.value = prototype.style.getPropertyValue('--u2-rte-classes').trim();
805
+ input.addEventListener('input', () => write('classes', input.value));
806
+ classes.append('--u2-rte-classes', input);
807
+ properties.append(classes);
808
+ }
809
+
810
+ function writeToolbar() {
811
+ const names = [...document.querySelectorAll('#toolbar-controls input:checked')].map(box => box.value);
812
+ write('toolbar', names.join(' '));
813
+ }
814
+
815
+ function write(name, value) {
816
+ prototype.style.setProperty(`--u2-rte-${name}`, value);
817
+ conventionEditor.refresh();
818
+ }
819
+
820
+ load();
821
+ showHistory();
822
+ </script>
823
+ </body>
824
+ </html>