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
package/js/rte/rte.js CHANGED
@@ -1,7 +1,58 @@
1
+ import {Editor} from './src/client/editor.js';
2
+ import {Rte} from './src/core/core.js';
1
3
 
2
- import './src/fixes.js';
3
- import './src/behavior.js';
4
- import './src/rte.js';
5
- import './src/ui.js';
6
- import './src/ui.items.js';
4
+ export {rangeRect} from './src/browser/range-rect.js';
5
+ export {Commands} from './src/command/commands.js';
6
+ export {BlockStyles} from './src/command/block-style.js';
7
+ export {deleteBackward, deleteForward} from './src/command/delete.js';
8
+ export {Edit} from './src/command/edit.js';
9
+ export {elementAttributes, selectedElement} from './src/command/element.js';
10
+ export {enter, lineBreak} from './src/command/enter.js';
11
+ export {blank, blockEdge, emptyBlock, fill} from './src/command/block-boundary.js';
12
+ export {insertFragment} from './src/command/fragment.js';
13
+ export {insertNode} from './src/command/insert.js';
14
+ export {Lists} from './src/command/list.js';
15
+ export {Tables} from './src/command/table.js';
16
+ export {applyMark, removeMark, setMarks, toggleMark, valueMark} from './src/command/mark.js';
17
+ export {PendingMarks} from './src/command/pending-marks.js';
18
+ export {unstyleCommand} from './src/command/unstyle.js';
19
+ export {config, elementPresets, enabled, hostDefaults, inlineUi} from './src/config/config.js';
20
+ export {Rte} from './src/core/core.js';
21
+ export {History} from './src/history/history.js';
22
+ export {ExternalInput} from './src/input/external-input.js';
23
+ export {InputPipeline, inputRange, inputTrigger} from './src/input/input-pipeline.js';
24
+ export {Editor} from './src/client/editor.js';
25
+ export {externalInputs, importLevel} from './src/client/external-input.js';
26
+ export {history} from './src/client/history.js';
27
+ export {marks} from './src/client/marks.js';
28
+ export {structure} from './src/client/structure.js';
29
+ export {classMark} from './src/mark/standard.js';
30
+ export {
31
+ bold, boldHtml,
32
+ code, codeHtml,
33
+ italic, italicHtml,
34
+ link, linkHtml,
35
+ strike, strikeHtml,
36
+ underline, underlineHtml,
37
+ } from './src/mark/standard.js';
38
+ export {MarkAdapter} from './src/mark/dom-adapter.js';
39
+ export {Mark, MarkType, markSet} from './src/mark/mark.js';
40
+ export {ContentModel} from './src/model/content-model.js';
41
+ export {createHtmlModel, htmlModel} from './src/model/html/html-model.js';
42
+ export {RepairPlanner} from './src/normalize/repair/repair-planner.js';
43
+ export {RepairExecutor} from './src/normalize/execute/repair-executor.js';
44
+ export {Normalizer} from './src/normalize/normalizer/normalizer.js';
45
+ export {EditRange} from './src/selection/range/edit-range.js';
46
+ export {PointMap} from './src/selection/map/point-map.js';
47
+ export {SelectionSnapshot} from './src/selection/snapshot.js';
48
+ export {Point} from './src/selection/point/point.js';
49
+ export {NativeSanitizer} from './src/sanitize/native.js';
50
+ export {SanitizePolicy, sanitizeDefaults, sanitizePolicy} from './src/sanitize/policy.js';
51
+ export {Source} from './src/source/source.js';
52
+ export {Surface} from './src/surface/surface.js';
53
+ export {Transaction} from './src/transaction/transaction.js';
54
+ export {Toolbar} from './src/ui/toolbar.js';
55
+ export {Unstyle, defaultUnstyle, defaultUnstyleLevels} from './src/unstyle/unstyle.js';
7
56
 
57
+ export const rte = new Rte(document);
58
+ export const editor = new Editor(rte);
@@ -0,0 +1,12 @@
1
+ import {editor} from './rte.js';
2
+ import {sourceView} from './src/client/source.js';
3
+
4
+ export {sourceView} from './src/client/source.js';
5
+ export {Source} from './src/source/source.js';
6
+
7
+ // Syntax highlighting is an enhancement: the dialog wraps its text area in
8
+ // <u2-code> and works without it. This entry loads that element on first use,
9
+ // so the composable `sourceView()` stays free of any dependency.
10
+ export const source = sourceView({highlight: () => import('../../el/code/code.js')});
11
+
12
+ editor.add(source);
@@ -0,0 +1,40 @@
1
+ # Browser policies
2
+
3
+ Browser policies compensate for observable differences in native editing,
4
+ selection, and layout primitives. This directory is not a general `fixes.js`:
5
+ each policy has one named responsibility, no hidden global state, and a focused
6
+ regression suite. Semantic editing behavior remains in commands, normalization,
7
+ or UI rather than being mislabeled as a browser fix.
8
+
9
+ ## Range geometry
10
+
11
+ `range-rect.js` exports `rangeRect(range, {root})`. A native rectangle with a
12
+ rendered size or a non-zero position is returned unchanged. When a collapsed
13
+ range reports an entirely zero rectangle, the
14
+ function derives its caret edge from following content, preceding content, or
15
+ the nearest rendered ancestor inside `root`, in that order.
16
+
17
+ - The DOM and live selection are never mutated; no temporary marker is inserted.
18
+ - Text is measured with a temporary native range over one adjacent character.
19
+ - Element boundaries use descendant content first and the element box second.
20
+ - Returned fallbacks have zero width at the inferred caret edge and retain the
21
+ measured line or element height.
22
+ - `root` is optional, but when supplied it both validates ownership and bounds
23
+ ancestor fallback.
24
+ - The implementation uses observed geometry, not browser names or user-agent
25
+ branches.
26
+
27
+ ## Placement of future policies
28
+
29
+ A new file belongs here only when current engines expose different or unusable
30
+ native behavior below editor semantics. Content structure belongs to the model
31
+ and normalizer; key meaning belongs to commands/input; focus visibility belongs
32
+ to UI. Prefer feature detection and a narrow primitive that higher layers can
33
+ replace or omit.
34
+
35
+ ## TODO
36
+
37
+ - Verify collapsed text, filler, replaced-element, vertical-writing, zoom, and
38
+ Shadow DOM geometry in current Chromium, Firefox, and WebKit.
39
+ - Add policy composition only after two independent policies need shared
40
+ installation or teardown; do not introduce a global fix registry in advance.
@@ -0,0 +1,82 @@
1
+ // Native collapsed ranges may report an empty rectangle. Derive the same caret
2
+ // boundary from adjacent rendered content without mutating DOM or selection.
3
+ export function rangeRect(range, {root = null} = {}) {
4
+ if (!range?.getBoundingClientRect || !range?.cloneRange) throw new TypeError('Range geometry requires a native Range');
5
+ if (root !== null && (typeof root?.contains !== 'function'
6
+ || !contains(root, range.startContainer) || !contains(root, range.endContainer))) {
7
+ throw new RangeError('Range geometry root must contain the range');
8
+ }
9
+ const native = range.getBoundingClientRect();
10
+ if (!range.collapsed || located(native)) return native;
11
+ return after(range.startContainer, range.startOffset)
12
+ || before(range.startContainer, range.startOffset)
13
+ || ancestor(range.startContainer, root)
14
+ || native;
15
+ }
16
+
17
+ function after(container, offset) {
18
+ if (container.nodeType === Node.TEXT_NODE) {
19
+ return offset < container.length ? textEdge(container, offset, false) : null;
20
+ }
21
+ const child = container.childNodes[offset];
22
+ return child ? nodeEdge(child, false) : null;
23
+ }
24
+
25
+ function before(container, offset) {
26
+ if (container.nodeType === Node.TEXT_NODE) {
27
+ return offset > 0 ? textEdge(container, offset - 1, true) : null;
28
+ }
29
+ const child = container.childNodes[offset - 1];
30
+ return child ? nodeEdge(child, true) : null;
31
+ }
32
+
33
+ function nodeEdge(node, end) {
34
+ if (node.nodeType === Node.TEXT_NODE) {
35
+ if (!node.length) return null;
36
+ return textEdge(node, end ? node.length - 1 : 0, end);
37
+ }
38
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
39
+ const child = node.childNodes[end ? node.childNodes.length - 1 : 0];
40
+ const nested = child && nodeEdge(child, end);
41
+ if (nested) return nested;
42
+ const rect = node.getBoundingClientRect();
43
+ return located(rect) ? edge(rect, end) : null;
44
+ }
45
+
46
+ function textEdge(node, offset, end) {
47
+ const range = node.ownerDocument.createRange();
48
+ range.setStart(node, offset);
49
+ range.setEnd(node, offset + 1);
50
+ const rect = range.getBoundingClientRect();
51
+ return located(rect) ? edge(rect, end) : null;
52
+ }
53
+
54
+ function ancestor(node, root) {
55
+ let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
56
+ while (element) {
57
+ const rect = element.getBoundingClientRect();
58
+ if (located(rect)) return edge(rect, false);
59
+ if (element === root) break;
60
+ element = element.parentElement;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function edge(rect, end) {
66
+ const left = end ? rect.right : rect.left;
67
+ const DOMRect = rect.constructor;
68
+ if (typeof DOMRect?.fromRect === 'function') {
69
+ return DOMRect.fromRect({x: left, y: rect.top, width: 0, height: rect.height});
70
+ }
71
+ return Object.freeze({x: left, y: rect.top, top: rect.top, right: left,
72
+ bottom: rect.bottom, left, width: 0, height: rect.height});
73
+ }
74
+
75
+ function located(rect) {
76
+ return !!rect && (rect.left !== 0 || rect.top !== 0 || rect.right !== 0 || rect.bottom !== 0
77
+ || rect.width !== 0 || rect.height !== 0);
78
+ }
79
+
80
+ function contains(root, node) {
81
+ return node === root || root.contains(node);
82
+ }
@@ -0,0 +1,378 @@
1
+ # Convention editor client
2
+
3
+ This is the deliberately small batteries-included layer above the RTE engine.
4
+ It proves the desired one-import setup without making its provisional module and
5
+ default-UI decisions part of the core.
6
+
7
+ The public root `rte.js` creates one `Editor` for the default document core:
8
+
9
+ ```js
10
+ import './rte.js';
11
+ ```
12
+
13
+ ```css
14
+ [contenteditable] {
15
+ --u2-rte: true;
16
+ --u2-rte-toolbar: bold;
17
+ }
18
+ ```
19
+
20
+ No surface is discovered by observation. The core registers an opted-in host
21
+ when focus reaches it; the client then installs its standard command registry
22
+ and input pipeline. A default roaming toolbar is allocated only when the first
23
+ rich-text surface becomes active. `plaintext-only` hosts stay completely native.
24
+
25
+ The optional `classes.js` entry adds a content-class control. Its choices come
26
+ from the host's `--u2-rte-classes`, so its toolbar select is filled per surface
27
+ rather than at registration: a module control may declare `options` as a
28
+ function of the surface, and the client refills that select before the toolbar
29
+ is shown. The same declaration is what the sanitizer keeps and what
30
+ presentation cleanup leaves alone.
31
+
32
+ The optional `images.js` entry frames a selected image, with handles on its
33
+ trailing edges and its alt text in a field below it. Both come and go with the
34
+ selection and neither takes the focus by appearing, so naming an image is one
35
+ click away and nothing has to be opened.
36
+
37
+ The optional `link.js` entry adds a contextual link editor. Its `link` command
38
+ is an ordinary `valueMark`, so any other UI can drive the same create, change,
39
+ and remove path; the module supplies one form.
40
+
41
+ That form is where the caret is: it appears on its own when the caret enters a
42
+ link, anchored on the link rather than on the selection, and goes when the caret
43
+ leaves — the same rule the table and image handles follow. Appearing therefore
44
+ never takes the focus, because someone is typing. What is left for the toolbar
45
+ control is the one thing that is a decision: turning a selection into a link, and
46
+ at an existing link handing it the keyboard.
47
+
48
+ The form has no Apply and no Remove: what its fields say is what the link is, as
49
+ it is typed, and an emptied address says there is no link — the form stays on the
50
+ text, so the same words can be linked again.
51
+ Each edit runs the command marked as ongoing input, so history keeps a whole
52
+ address as one step rather than one per keystroke. Marking the link moves the
53
+ document selection into it and an engine follows that with focus, so the field
54
+ being typed into gets focus back with its caret intact — otherwise the second
55
+ character would land in the editor. What it edits is one link, decided when it
56
+ appeared — not wherever the caret has moved on to, or a pending edit would land
57
+ somewhere else the moment the caret did. Enter and Escape hand the caret back to
58
+ the text — after the link, not inside it, because whoever just made one wants to
59
+ keep writing without it. Neither dismisses the form: it goes because the caret
60
+ has left the link, and with every edit already applied one key has nothing left
61
+ to confirm and the other nothing to undo.
62
+
63
+ Beside the address sits a way to open it in a new tab, shown only while the
64
+ address is one the browser can follow: an application scheme — a page id, a
65
+ record reference — is a link the editor understands and the browser does not. The address is a plain text field on
66
+ purpose: native url validation rejects relative paths, fragments, and
67
+ application schemes and would silently block the form, while which protocols are
68
+ acceptable is the sanitizer's decision.
69
+
70
+ What an address means is the application's decision, and `linkEditor()` takes
71
+ two hooks for it. `normalize(value, surface)` receives the finished value
72
+ once, when the form is done, and may complete a bare domain, map an address into
73
+ a scheme of its own, or derive `rel` and `target` from it; what it returns is
74
+ what gets written and shown. Not per keystroke, which would rewrite a half-typed
75
+ address under the caret — and not when the field is left either: marking the link
76
+ takes the focus away and hands it back on every keystroke, so leaving a field is
77
+ not an event this form can wait for. `suggest(text, surface)` is asked, possibly
78
+ asynchronously, what a *new* link should point at, given the text it is being put
79
+ on; its answer is used only while the form is still open on the same link and
80
+ the address is still empty. It is offered rather than applied — it stands in the
81
+ field, selected so one keystroke replaces it, and becomes the link when the form
82
+ is done. Applying it at once would mark the link, and marking moves the selection
83
+ into it and the focus after that, throwing whoever asked for a link back into the
84
+ text before they had seen the answer.
85
+
86
+ `complete(text, surface)` is asked what addresses go with what is being typed and
87
+ answers with entries `{value, label}` — or `{value, html}` for a row with markup,
88
+ which the form writes through `setHTML()`, so the platform sanitizes it. The form
89
+ renders its own list rather than a native datalist, which cannot show markup: the
90
+ arrow keys move through it, Enter takes the current entry, Escape closes the list
91
+ before it closes the form, and a pointer does the same. One question is in flight
92
+ at a time, and a late answer to a word that has since changed is dropped.
93
+
94
+ Everything this client draws goes into one `Chrome`: its toolbar, the contextual
95
+ handles, the link form, the source dialog. A module receives it as `chrome` in
96
+ its setup context and puts its UI in `chrome.root`, so no module owns a layer, a
97
+ top-layer element, or a place in the application's DOM. The chrome is made on
98
+ first use, so an editor that never shows anything adds nothing to the document,
99
+ and disposing the client takes all of it away at once.
100
+ When the active surface sits in a modal dialog, open popover, or fullscreen
101
+ element, the same chrome follows it into that native top-layer boundary and
102
+ returns afterward. The client registers the chrome as retained core UI, so its
103
+ controls keep activation and direct top-layer hosts keep it out of Source and
104
+ History content.
105
+
106
+ The optional `images.js` entry frames a selected image with three handles on its
107
+ trailing edges: the bottom-right corner keeps the proportion, the right edge
108
+ changes only the width and the bottom edge only the height. An image sits in a
109
+ text flow with its top and start edges held in place, so dragging the other side
110
+ is the only direction that grows it where the eye expects — a handle on a
111
+ leading edge would move the picture rather than resize it. A drag moves the
112
+ frame only and the size is written once when it is released, so a resize is one
113
+ undo step rather than a trail of them. The module also selects an image that is
114
+ clicked, because engines disagree about whether pointing at one does, and
115
+ nothing is addressable until it is the selection. What is resizable is a
116
+ selector, so any atomic element can be made sizeable.
117
+
118
+ The optional `tables.js` entry puts its actions on the table itself. Row handles
119
+ run down the table's left edge and column handles along its top, each lined up
120
+ with the cell the caret is in: add before, delete, add after. A handle whose
121
+ command cannot run is disabled rather than hidden, so the row of three keeps its
122
+ shape. Only `insertTable` is a toolbar control, because it applies where no
123
+ table is.
124
+
125
+ The layer is one per editor, lives in the browser top layer, and is repositioned
126
+ from selection, change, input, scroll, and resize — never from a timer.
127
+
128
+ The optional `source.js` entry adds a modal HTML source view. Its dialog wraps
129
+ its text area in `<u2-code>`: where that element is defined it takes the area
130
+ over and highlights it, and where it is not the area renders on its own.
131
+ Highlighting is therefore an enhancement, never a dependency — `sourceView()`
132
+ alone imports nothing, and the root entry passes a `highlight` loader that pulls
133
+ `u2/el/code` before the first open. Closing without applying restores the caret
134
+ the view was opened on.
135
+
136
+ ## Prototype contract
137
+
138
+ `new Editor(core)` currently installs these defaults per rich-text surface:
139
+
140
+ - `enter` and `lineBreak` structural commands;
141
+ - `deleteBackward` and `deleteForward` for content-model-approved block joins
142
+ while ordinary character deletion stays native;
143
+ - one `PendingMarks` instance;
144
+ - `insertText` routing and post-composition formatting while a pending mark exists;
145
+ - one `History` instance with `undo` and `redo`, including Ctrl/Command+Z,
146
+ Ctrl/Command+Shift+Z, and Ctrl/Command+Y;
147
+ - the ready-made inline marks `bold`, `italic`, `underline`, `strike`, and
148
+ `code`, with Ctrl/Command+B, +I, and +U;
149
+ - the block structure commands `bullets`, `numbers`, `indent`, `outdent`, and
150
+ `rule`;
151
+ - one `InputPipeline` using the normal host configuration.
152
+
153
+ `commands(surface)` and `history(surface)` expose the per-surface registry and
154
+ history to an application; `controls` lists what the registered modules offer by
155
+ name, so a host can build its own list of what `--u2-rte-toolbar` may choose
156
+ from without guessing.
157
+
158
+ Those defaults arrive as three ordinary modules — `history`, `marks`, and
159
+ `structure`, one file each — registered before any surface exists. They use exactly the module
160
+ contract below and can be removed with `editor.delete('structure')`, so nothing
161
+ about them is privileged. Every control they contribute is gated by
162
+ `--u2-rte-toolbar`, so a host that lists only `bold` shows only Bold while the
163
+ commands and their keyboard shortcuts stay available.
164
+
165
+ `add(module)` installs an optional extension into already registered and future
166
+ rich-text surfaces. A module has one client-wide name and may provide a command
167
+ factory, lifecycle hooks, and declarative toolbar controls:
168
+
169
+ ```js
170
+ const highlight = {
171
+ name: 'highlight',
172
+ commands({pending}) {
173
+ return {highlight: pending.toggle(highlightHtml)};
174
+ },
175
+ toolbar: [{
176
+ command: 'highlight',
177
+ label: 'Highlight',
178
+ text: 'H',
179
+ state: true,
180
+ }],
181
+ };
182
+
183
+ editor.add(highlight);
184
+ ```
185
+
186
+ The factory returns a plain object whose keys are command names and whose values
187
+ follow the normal `Commands` contract. `surface` is the target surface,
188
+ `pending` is its shared `PendingMarks` instance, so formatting modules compose
189
+ with the one `insertText` route rather than installing competing pipelines, and
190
+ `history` is its `History`.
191
+ `toolbar` is optional; its controls require non-empty `command`, `label`, and
192
+ `text`, with optional `state` and `shortcut`.
193
+
194
+ A module may instead contribute a value-bound select. Its `name` is the CSS
195
+ toolbar token, `command` is one normal command, and every option maps a label to
196
+ an `edit.value`:
197
+
198
+ ```js
199
+ toolbar: [{
200
+ type: 'select',
201
+ name: 'block',
202
+ command: 'blockStyle',
203
+ label: 'Block style',
204
+ options: [
205
+ {value: 'paragraph', label: 'Paragraph'},
206
+ {value: 'h1', label: 'Heading 1'},
207
+ ],
208
+ }]
209
+ ```
210
+
211
+ The optional root `blocks.js` entry uses this contract for Paragraph, H1, H2,
212
+ and H3. `blockStyles(styles, options)` creates a replacement module from custom
213
+ definitions, including class-based styles such as `p.lead`. Importing
214
+ `blocks.js` also imports the convention editor and registers the default module.
215
+
216
+ Module names, command names, and routed `inputType` ownership are exclusive.
217
+ Adding the same module object twice is idempotent; a different module with the
218
+ same name or a command/input route already owned by the client throws. The
219
+ declaration and controls are snapshotted at registration, so later caller
220
+ mutation cannot change future surfaces. Installation into existing surfaces is
221
+ atomic. `delete()` accepts the registered module object or its name, removes its
222
+ commands and controls everywhere, and returns whether it existed. Changing
223
+ module topology clears transient pending caret marks so a removed formatter
224
+ cannot affect later input. A disposed client rejects new modules.
225
+
226
+ `setup({editor, core, root})` runs once when an extension is added.
227
+ `attach({editor, surface, pending, commands})` runs once for every current and
228
+ future rich-text surface, after that extension's commands are registered. Each
229
+ hook may return nothing or one object with `dispose()`. Surface disposal,
230
+ `delete()`, failed atomic installation, and editor disposal release those
231
+ objects in the reverse scope. There is deliberately no second `destroy()` or
232
+ cleanup-function convention.
233
+
234
+ This remains a narrow extension contract rather than a general plugin
235
+ framework: setup and attachment are synchronous, command/input ownership stays
236
+ exclusive, and an extension cannot reach private client state.
237
+ `commands(surface)` exposes that surface's registry or `null` for direct
238
+ inspection. `refresh()` re-reads the active surface's CSS and toolbar state.
239
+
240
+ The first active roaming surface creates one application-owned default toolbar
241
+ element and one small style element in the core root. `--u2-rte-toolbar` filters
242
+ its available controls through the normal `Toolbar` contract. Only `bold` is
243
+ shipped here yet; an unknown name such as `code` remains invisible rather than
244
+ pretending that its command exists. `--u2-rte-ui: none` prevents allocation
245
+ until another roaming surface becomes active. Leaving both the active surface
246
+ and toolbar hides it. Default placement uses the isolated `rangeRect()` browser
247
+ policy, so an empty native caret rectangle is derived from adjacent rendered
248
+ content without inserting a marker into the editor.
249
+ `--u2-rte-toolbar-when: selection` additionally keeps the toolbar hidden at a
250
+ caret and opens it only for a non-collapsed saved selection.
251
+ When Popover is available, the convention toolbar is a manual popover in the
252
+ browser top layer; its fixed range placement and focus-preserving behavior stay
253
+ the same inside a Document or ShadowRoot.
254
+
255
+ ### Visible line breaks
256
+
257
+ The optional root `breaks.js` entry is the first lifecycle extension:
258
+
259
+ ```js
260
+ import './breaks.js';
261
+ ```
262
+
263
+ ```css
264
+ [contenteditable] {
265
+ --u2-rte-show-breaks: true;
266
+ --u2-rte-toolbar: bold breaks;
267
+ }
268
+ ```
269
+
270
+ The CSS property chooses the initial view state when a surface connects.
271
+ Leaving `breaks` out of the toolbar list gives an always-on display with no
272
+ toggle. The `showBreaks` command runs outside an editing transaction, so it
273
+ emits a command event but no content-change event and never enters history.
274
+
275
+ The extension does not insert markers beside `<br>` nodes. One extension-owned
276
+ top-layer overlay contains presentation-only marks and is allocated only while
277
+ at least one surface shows them. A single root scroll listener, resize listener,
278
+ and `ResizeObserver` schedule all visible surfaces together; input and editor
279
+ changes schedule their own surface. Removing the extension restores its host
280
+ attributes, overlay, styles, observers, and listeners. Document and ShadowRoot
281
+ cores keep all of those resources in their own root. Nested editable surfaces
282
+ own their own `<br>` markers, so an outer surface never duplicates them.
283
+
284
+ ### Optional Unstyle
285
+
286
+ The optional root `unstyle.js` entry adds a selection-only `unstyle` action:
287
+
288
+ ```js
289
+ import './unstyle.js';
290
+ ```
291
+
292
+ ```css
293
+ [contenteditable] { --u2-rte-toolbar: bold unstyle; }
294
+ ```
295
+
296
+ Each click applies the first remaining presentation level. The button disables
297
+ when the selection has nothing covered by the policy. `unstyles(levels,
298
+ options)` creates a replacement module with application levels, command/control
299
+ names, label, and text. The underlying policy also cleans safely parsed
300
+ external fragments; the toolbar contains no paste-specific logic.
301
+
302
+ ### Optional rich external input
303
+
304
+ `externalInputs(options)` adapts the low-level `ExternalInput` boundary to the
305
+ convention client's normal extension lifecycle. It is needed only when an
306
+ application chooses to read and replace rich HTML before native insertion. A
307
+ sanitizer is mandatory and is never selected implicitly:
308
+
309
+ ```js
310
+ import {editor, externalInputs, NativeSanitizer} from '../../rte.js';
311
+
312
+ editor.add(externalInputs({sanitizer: new NativeSanitizer()}));
313
+ ```
314
+
315
+ The module registers mapped `insertFragment` and attaches one input boundary to
316
+ every current and future rich surface. Removing it disposes the listeners and
317
+ command everywhere. It contributes no toolbar control.
318
+
319
+ With the default Unstyle policy, an inherited CSS property chooses cumulative
320
+ presentation cleanup for each import:
321
+
322
+ ```css
323
+ [contenteditable] { --u2-rte-import-unstyle: styles; }
324
+ ```
325
+
326
+ The value may be `none`, `classes`, `styles`, `attributes`, or `formatting`.
327
+ Custom `Unstyle` policies may use their own level names. Pass `unstyle: null`
328
+ to disable this stage, or pass `through` as a fixed name or resolver function
329
+ to replace CSS resolution. The security sanitizer still always runs.
330
+
331
+ `rte.js` exports the factory but does not install it by default. Its ordinary
332
+ input pipeline leaves paste/drop insertion to the browser, removes classes and
333
+ inline styles from only the added elements, and then normalizes their structure.
334
+ That path needs no HTML parser. Applications importing arbitrary HTML strings
335
+ may still provide any safe adapter through the same `sanitize()` contract.
336
+
337
+ `dispose()` removes every installed input pipeline, listener, toolbar node, and
338
+ style node without disposing the supplied core. It is idempotent and is also
339
+ available through `[Symbol.dispose]()`; core disposal disposes the client. One
340
+ core accepts only one convention client at a time, preventing duplicate input
341
+ pipelines. Disposal releases that ownership so a new client can take over.
342
+
343
+ ## Invariants
344
+
345
+ - Importing the convention client adds only core listeners. It scans no DOM,
346
+ installs no observer, and creates no UI before a qualifying surface is active.
347
+ - Every mutable registry and pipeline belongs to one `Editor` instance and one
348
+ surface; different cores cannot share state.
349
+ - A module is installed once per rich surface, owns all command names and
350
+ controls it contributes, and is removed from all of them as one operation.
351
+ - Module installation either succeeds for every existing surface or leaves
352
+ every registry and input route unchanged.
353
+ - All editing still runs through public commands, transactions, point mapping,
354
+ normalization, and `Toolbar`. The client contains no second editor algorithm.
355
+ - The engine entry `rte.js` remains usable without importing this layer.
356
+
357
+ ## Browser considerations
358
+
359
+ Lazy registration uses the core's existing capture-phase focus route. The UI
360
+ uses a manual top-layer popover with fixed positioning around the saved range,
361
+ with non-mutating
362
+ collapsed-range geometry fallback, and intentionally does not claim final
363
+ viewport, zoom, touch, writing-mode, or virtual-keyboard policy.
364
+ The real-browser suite covers lazy focus setup, Enter routing, toolbar commands,
365
+ late module installation, future surfaces, rollback, removal, plain-text
366
+ exclusion, rich external-input composition, and complete teardown.
367
+
368
+ ## TODO exposed by the prototype
369
+
370
+ - Exercise lifecycle failure and coexistence further before adding dependency,
371
+ ordering, or asynchronous setup concepts.
372
+ - Decide whether the default UI becomes application DOM, a custom element, or
373
+ an optional UI module before stabilizing its styling hooks.
374
+ - Generalize placement for viewport edges, writing modes, zoom, touch handles,
375
+ and virtual keyboards.
376
+ - Decide separately whether `code` means inline `<code>` formatting or the old
377
+ editor's HTML-source UI; block styles now provide the first substantial
378
+ optional capability.
@@ -0,0 +1,38 @@
1
+ import {BlockStyles} from '../command/block-style.js';
2
+
3
+ export const defaultBlockStyles = Object.freeze([
4
+ Object.freeze({name: 'paragraph', label: 'Paragraph', selector: 'p', tag: 'p'}),
5
+ Object.freeze({name: 'h1', label: 'Heading 1', selector: 'h1', tag: 'h1'}),
6
+ Object.freeze({name: 'h2', label: 'Heading 2', selector: 'h2', tag: 'h2'}),
7
+ Object.freeze({name: 'h3', label: 'Heading 3', selector: 'h3', tag: 'h3'}),
8
+ ]);
9
+
10
+ export function blockStyles(styles = defaultBlockStyles, {
11
+ name = 'blocks',
12
+ control = 'block',
13
+ command = 'blockStyle',
14
+ label = 'Block style',
15
+ } = {}) {
16
+ for (const [property, value] of Object.entries({name, control, command, label})) {
17
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`Block module ${property} must be a non-empty string`);
18
+ }
19
+ const policy = new BlockStyles(styles);
20
+ const commands = Object.freeze({[command]: policy.command()});
21
+ const options = Object.freeze(policy.styles.map(style => Object.freeze({
22
+ value: style.name,
23
+ label: style.label,
24
+ })));
25
+ return Object.freeze({
26
+ name: name.trim(),
27
+ commands: () => commands,
28
+ toolbar: Object.freeze([Object.freeze({
29
+ type: 'select',
30
+ name: control.trim(),
31
+ command: command.trim(),
32
+ label,
33
+ options,
34
+ })]),
35
+ });
36
+ }
37
+
38
+ export const blocks = blockStyles();