u2ui 1.5.3 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) 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 +487 -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 +553 -0
  24. package/js/rte/playground/index.html +835 -0
  25. package/js/rte/rte.js +56 -5
  26. package/js/rte/run.out +17 -0
  27. package/js/rte/source.js +12 -0
  28. package/js/rte/src/browser/README.md +40 -0
  29. package/js/rte/src/browser/interactive.js +17 -0
  30. package/js/rte/src/browser/range-rect.js +82 -0
  31. package/js/rte/src/client/README.md +385 -0
  32. package/js/rte/src/client/blocks.js +38 -0
  33. package/js/rte/src/client/breaks.js +212 -0
  34. package/js/rte/src/client/classes.js +48 -0
  35. package/js/rte/src/client/editor.js +472 -0
  36. package/js/rte/src/client/external-input.js +47 -0
  37. package/js/rte/src/client/history.js +13 -0
  38. package/js/rte/src/client/images.js +285 -0
  39. package/js/rte/src/client/link.js +567 -0
  40. package/js/rte/src/client/marks.js +25 -0
  41. package/js/rte/src/client/source.js +182 -0
  42. package/js/rte/src/client/structure.js +27 -0
  43. package/js/rte/src/client/tables.js +165 -0
  44. package/js/rte/src/client/unstyle.js +32 -0
  45. package/js/rte/src/command/README.md +520 -0
  46. package/js/rte/src/command/block-boundary.js +43 -0
  47. package/js/rte/src/command/block-style.js +120 -0
  48. package/js/rte/src/command/commands.js +159 -0
  49. package/js/rte/src/command/delete.js +119 -0
  50. package/js/rte/src/command/edit.js +78 -0
  51. package/js/rte/src/command/element.js +45 -0
  52. package/js/rte/src/command/enter.js +175 -0
  53. package/js/rte/src/command/fragment.js +76 -0
  54. package/js/rte/src/command/insert.js +52 -0
  55. package/js/rte/src/command/list.js +284 -0
  56. package/js/rte/src/command/mark.js +583 -0
  57. package/js/rte/src/command/pending-marks.js +216 -0
  58. package/js/rte/src/command/table.js +207 -0
  59. package/js/rte/src/command/unstyle.js +248 -0
  60. package/js/rte/src/config/README.md +96 -0
  61. package/js/rte/src/config/config.js +134 -0
  62. package/js/rte/src/core/README.md +63 -0
  63. package/js/rte/src/core/core.js +247 -0
  64. package/js/rte/src/history/README.md +83 -0
  65. package/js/rte/src/history/history.js +283 -0
  66. package/js/rte/src/input/README.md +173 -0
  67. package/js/rte/src/input/external-input.js +130 -0
  68. package/js/rte/src/input/input-pipeline.js +459 -0
  69. package/js/rte/src/mark/README.md +219 -0
  70. package/js/rte/src/mark/dom-adapter.js +100 -0
  71. package/js/rte/src/mark/mark.js +133 -0
  72. package/js/rte/src/mark/standard.js +76 -0
  73. package/js/rte/src/model/README.md +71 -0
  74. package/js/rte/src/model/content-model.js +213 -0
  75. package/js/rte/src/model/html/README.md +51 -0
  76. package/js/rte/src/model/html/html-model.js +90 -0
  77. package/js/rte/src/normalize/execute/README.md +33 -0
  78. package/js/rte/src/normalize/execute/repair-executor.js +129 -0
  79. package/js/rte/src/normalize/normalizer/README.md +48 -0
  80. package/js/rte/src/normalize/normalizer/normalizer.js +141 -0
  81. package/js/rte/src/normalize/repair/README.md +70 -0
  82. package/js/rte/src/normalize/repair/repair-planner.js +208 -0
  83. package/js/rte/src/sanitize/README.md +108 -0
  84. package/js/rte/src/sanitize/native.js +43 -0
  85. package/js/rte/src/sanitize/policy.js +206 -0
  86. package/js/rte/src/selection/README.md +39 -0
  87. package/js/rte/src/selection/map/README.md +53 -0
  88. package/js/rte/src/selection/map/point-map.js +307 -0
  89. package/js/rte/src/selection/ownership/README.md +32 -0
  90. package/js/rte/src/selection/ownership/ownership.js +42 -0
  91. package/js/rte/src/selection/point/README.md +34 -0
  92. package/js/rte/src/selection/point/point.js +83 -0
  93. package/js/rte/src/selection/range/README.md +38 -0
  94. package/js/rte/src/selection/range/edit-range.js +223 -0
  95. package/js/rte/src/selection/snapshot.js +64 -0
  96. package/js/rte/src/source/README.md +64 -0
  97. package/js/rte/src/source/source.js +193 -0
  98. package/js/rte/src/surface/README.md +36 -0
  99. package/js/rte/src/surface/content.js +16 -0
  100. package/js/rte/src/surface/surface.js +87 -0
  101. package/js/rte/src/transaction/README.md +35 -0
  102. package/js/rte/src/transaction/transaction.js +56 -0
  103. package/js/rte/src/ui/README.md +271 -0
  104. package/js/rte/src/ui/caret.js +17 -0
  105. package/js/rte/src/ui/chrome.js +214 -0
  106. package/js/rte/src/ui/handles.js +179 -0
  107. package/js/rte/src/ui/highlight.js +86 -0
  108. package/js/rte/src/ui/place.js +25 -0
  109. package/js/rte/src/ui/toolbar.js +258 -0
  110. package/js/rte/src/unstyle/README.md +103 -0
  111. package/js/rte/src/unstyle/unstyle.js +140 -0
  112. package/js/rte/tables.js +7 -0
  113. package/js/rte/unstyle.js +8 -0
  114. package/js/rte0/README.md +37 -0
  115. package/js/rte0/rte.js +7 -0
  116. package/js/{rte → rte0}/src/behavior.js +0 -1
  117. package/js/{rte → rte0}/src/events.js +0 -1
  118. package/js/{rte → rte0}/src/range.js +0 -2
  119. package/js/{rte → rte0}/src/ui.items.js +0 -2
  120. package/js/{rte → rte0}/src/ui.js +0 -1
  121. package/package.json +1 -1
  122. package/u2/Element.js +2 -2
  123. package/u2/plan-scoped-refactoring.md +133 -303
  124. package/u2/projects.json +18 -2
  125. package/u2/tools/update.repos.json.js +1 -1
  126. package/el/qrcode/package.json +0 -22
  127. /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
  128. /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
  129. /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
  130. /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
  131. /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
  132. /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
  133. /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
  134. /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
  135. /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
  136. /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
  137. /package/js/{rte → rte0}/old/Rte/main.css +0 -0
  138. /package/js/{rte → rte0}/old/old.rte.js +0 -0
  139. /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
  140. /package/js/{rte → rte0}/src/fixes.js +0 -0
  141. /package/js/{rte → rte0}/src/rte.js +0 -0
  142. /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
  143. /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
  144. /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
  145. /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
  146. /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);
package/js/rte/run.out ADDED
@@ -0,0 +1,17 @@
1
+ node:internal/modules/cjs/loader:1433
2
+ throw err;
3
+ ^
4
+
5
+ Error: Cannot find module '/var/www/workplace/u2/js/rte/run.mjs'
6
+ at Function._resolveFilename (node:internal/modules/cjs/loader:1430:15)
7
+ at defaultResolveImpl (node:internal/modules/cjs/loader:1040:19)
8
+ at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1045:22)
9
+ at Function._load (node:internal/modules/cjs/loader:1216:25)
10
+ at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
11
+ at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
12
+ at node:internal/main/run_main_module:36:49 {
13
+ code: 'MODULE_NOT_FOUND',
14
+ requireStack: []
15
+ }
16
+
17
+ Node.js v22.23.2
@@ -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,17 @@
1
+ // A link, a button, or a label around an editing host — `<a><span
2
+ // contenteditable>` — is a host shape engines answer with the element instead of
3
+ // the text: the link is dragged rather than giving the text a caret and followed
4
+ // when the press ends, and the label hands the press to the control it names.
5
+ // All of it belongs to what surrounds the text, not to someone editing it.
6
+ //
7
+ // Only what surrounds the host is asked for: a link the content itself holds is
8
+ // content, and what a press does to that one is the editor's own business.
9
+ export function interactiveAround(host) {
10
+ return host?.closest('a[href], button, label') || null;
11
+ }
12
+
13
+ // And a button answers Space and Enter by activating itself, whatever it holds:
14
+ // the text inside never sees those keys, and no `beforeinput` arrives at all.
15
+ export function activatingAround(host) {
16
+ return host?.closest('button') || null;
17
+ }
@@ -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,385 @@
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. The name is written as it is typed, so
36
+ Enter has nothing to confirm: it hands the caret back to the text, after the
37
+ image.
38
+
39
+ The optional `link.js` entry adds a contextual link editor. Its `link` command
40
+ is an ordinary `valueMark`, so any other UI can drive the same create, change,
41
+ and remove path; the module supplies one form.
42
+
43
+ That form is where the caret is: it appears on its own when the caret enters a
44
+ link, anchored on the link rather than on the selection, and goes when the caret
45
+ leaves — the same rule the table and image handles follow. Appearing therefore
46
+ never takes the focus, because someone is typing. What is left for the toolbar
47
+ control is the one thing that is a decision: turning a selection into a link, and
48
+ at an existing link handing it the keyboard.
49
+
50
+ The form has no Apply and no Remove: what its fields say is what the link is, as
51
+ it is typed, and an emptied address says there is no link — the form stays on the
52
+ text, so the same words can be linked again.
53
+ Each edit runs the command marked as ongoing input, so history keeps a whole
54
+ address as one step rather than one per keystroke. Marking the link moves the
55
+ document selection into it and an engine follows that with focus, so the field
56
+ being typed into gets focus back with its caret intact — otherwise the second
57
+ character would land in the editor. What it edits is one link, decided when it
58
+ appeared — not wherever the caret has moved on to, or a pending edit would land
59
+ somewhere else the moment the caret did. Enter and Escape hand the caret back to
60
+ the text — after the link, not inside it, because whoever just made one wants to
61
+ keep writing without it. Neither dismisses the form: it goes because the caret
62
+ has left the link, and with every edit already applied one key has nothing left
63
+ to confirm and the other nothing to undo.
64
+
65
+ Beside the address sits a way to open it in a new tab, shown only while the
66
+ address is one the browser can follow: an application scheme — a page id, a
67
+ record reference — is a link the editor understands and the browser does not. The address is a plain text field on
68
+ purpose: native url validation rejects relative paths, fragments, and
69
+ application schemes and would silently block the form, while which protocols are
70
+ acceptable is the sanitizer's decision.
71
+
72
+ What an address means is the application's decision, and `linkEditor()` takes
73
+ two hooks for it. `normalize(value, surface)` receives the finished value
74
+ once, when the form is done, and may complete a bare domain, map an address into
75
+ a scheme of its own, or derive `rel` and `target` from it; what it returns is
76
+ what gets written and shown. Not per keystroke, which would rewrite a half-typed
77
+ address under the caret — and not when the field is left either: marking the link
78
+ takes the focus away and hands it back on every keystroke, so leaving a field is
79
+ not an event this form can wait for. `suggest(text, surface)` is asked, possibly
80
+ asynchronously, what a *new* link should point at, given the text it is being put
81
+ on; its answer is used only while the form is still open on the same link and
82
+ the address is still empty. It is offered rather than applied — it stands in the
83
+ field, selected so one keystroke replaces it, and becomes the link when the form
84
+ is done. Applying it at once would mark the link, and marking moves the selection
85
+ into it and the focus after that, throwing whoever asked for a link back into the
86
+ text before they had seen the answer.
87
+
88
+ `complete(text, surface)` is asked what addresses go with what is being typed and
89
+ answers with entries `{value, label}` — or `{value, html}` for a row with markup,
90
+ which the form writes through `setHTML()`, so the platform sanitizes it; where an
91
+ engine does not ship that yet, the entry shows its address instead. The form
92
+ renders its own list rather than a native datalist, which cannot show markup: the
93
+ arrow keys move through it, Enter takes the current entry, Escape closes the list
94
+ before it closes the form, and a pointer does the same. One question is in flight
95
+ at a time, and a late answer to a word that has since changed is dropped.
96
+
97
+ Contextual UI goes with the editing session and comes back with it: a module
98
+ draws on `u2-rte-activate` as well as on selection and content changes, because
99
+ returning to the field a caret was already in is no selection change and would
100
+ otherwise leave its handles or its form behind.
101
+
102
+ Everything this client draws goes into one `Chrome`: its toolbar, the contextual
103
+ handles, the link form, the source dialog. A module receives it as `chrome` in
104
+ its setup context and puts its UI in `chrome.root`, so no module owns a layer, a
105
+ top-layer element, or a place in the application's DOM. The chrome is made on
106
+ first use, so an editor that never shows anything adds nothing to the document,
107
+ and disposing the client takes all of it away at once.
108
+ When the active surface sits in a modal dialog, open popover, or fullscreen
109
+ element, the same chrome follows it into that native top-layer boundary and
110
+ returns afterward. The client registers the chrome as retained core UI, so its
111
+ controls keep activation and direct top-layer hosts keep it out of Source and
112
+ History content.
113
+
114
+ The optional `images.js` entry frames a selected image with three handles on its
115
+ trailing edges: the bottom-right corner keeps the proportion, the right edge
116
+ changes only the width and the bottom edge only the height. An image sits in a
117
+ text flow with its top and start edges held in place, so dragging the other side
118
+ is the only direction that grows it where the eye expects — a handle on a
119
+ leading edge would move the picture rather than resize it. A drag moves the
120
+ frame only and the size is written once when it is released, so a resize is one
121
+ undo step rather than a trail of them. Selecting the image that was clicked is
122
+ not its job: the input pipeline selects any atomic element a click lands on.
123
+ What is resizable is a selector, so any atomic element can be made sizeable.
124
+
125
+ The optional `tables.js` entry puts its actions on the table itself. Row handles
126
+ run down the table's left edge and column handles along its top, each lined up
127
+ with the cell the caret is in: add before, delete, add after. A handle whose
128
+ command cannot run is disabled rather than hidden, so the row of three keeps its
129
+ shape. Only `insertTable` is a toolbar control, because it applies where no
130
+ table is.
131
+
132
+ The layer is one per editor, lives in the browser top layer, and is repositioned
133
+ from selection, change, input, scroll, and resize — never from a timer.
134
+
135
+ The optional `source.js` entry adds a modal HTML source view. Its dialog wraps
136
+ its text area in `<u2-code>`: where that element is defined it takes the area
137
+ over and highlights it, and where it is not the area renders on its own.
138
+ Highlighting is therefore an enhancement, never a dependency — `sourceView()`
139
+ alone imports nothing, and the root entry passes a `highlight` loader that pulls
140
+ `u2/el/code` before the first open. Closing without applying restores the caret
141
+ the view was opened on.
142
+
143
+ ## Prototype contract
144
+
145
+ `new Editor(core)` currently installs these defaults per rich-text surface:
146
+
147
+ - `enter` and `lineBreak` structural commands;
148
+ - `deleteBackward` and `deleteForward` for content-model-approved block joins
149
+ while ordinary character deletion stays native;
150
+ - one `PendingMarks` instance;
151
+ - `insertText` routing and post-composition formatting while a pending mark exists;
152
+ - one `History` instance with `undo` and `redo`, including Ctrl/Command+Z,
153
+ Ctrl/Command+Shift+Z, and Ctrl/Command+Y;
154
+ - the ready-made inline marks `bold`, `italic`, `underline`, `strike`, and
155
+ `code`, with Ctrl/Command+B, +I, and +U;
156
+ - the block structure commands `bullets`, `numbers`, `indent`, `outdent`, and
157
+ `rule`;
158
+ - one `InputPipeline` using the normal host configuration.
159
+
160
+ `commands(surface)` and `history(surface)` expose the per-surface registry and
161
+ history to an application; `controls` lists what the registered modules offer by
162
+ name, so a host can build its own list of what `--u2-rte-toolbar` may choose
163
+ from without guessing.
164
+
165
+ Those defaults arrive as three ordinary modules — `history`, `marks`, and
166
+ `structure`, one file each — registered before any surface exists. They use exactly the module
167
+ contract below and can be removed with `editor.delete('structure')`, so nothing
168
+ about them is privileged. Every control they contribute is gated by
169
+ `--u2-rte-toolbar`, so a host that lists only `bold` shows only Bold while the
170
+ commands and their keyboard shortcuts stay available.
171
+
172
+ `add(module)` installs an optional extension into already registered and future
173
+ rich-text surfaces. A module has one client-wide name and may provide a command
174
+ factory, lifecycle hooks, and declarative toolbar controls:
175
+
176
+ ```js
177
+ const highlight = {
178
+ name: 'highlight',
179
+ commands({pending}) {
180
+ return {highlight: pending.toggle(highlightHtml)};
181
+ },
182
+ toolbar: [{
183
+ command: 'highlight',
184
+ label: 'Highlight',
185
+ text: 'H',
186
+ state: true,
187
+ }],
188
+ };
189
+
190
+ editor.add(highlight);
191
+ ```
192
+
193
+ The factory returns a plain object whose keys are command names and whose values
194
+ follow the normal `Commands` contract. `surface` is the target surface,
195
+ `pending` is its shared `PendingMarks` instance, so formatting modules compose
196
+ with the one `insertText` route rather than installing competing pipelines, and
197
+ `history` is its `History`.
198
+ `toolbar` is optional; its controls require non-empty `command`, `label`, and
199
+ `text`, with optional `state` and `shortcut`.
200
+
201
+ A module may instead contribute a value-bound select. Its `name` is the CSS
202
+ toolbar token, `command` is one normal command, and every option maps a label to
203
+ an `edit.value`:
204
+
205
+ ```js
206
+ toolbar: [{
207
+ type: 'select',
208
+ name: 'block',
209
+ command: 'blockStyle',
210
+ label: 'Block style',
211
+ options: [
212
+ {value: 'paragraph', label: 'Paragraph'},
213
+ {value: 'h1', label: 'Heading 1'},
214
+ ],
215
+ }]
216
+ ```
217
+
218
+ The optional root `blocks.js` entry uses this contract for Paragraph, H1, H2,
219
+ and H3. `blockStyles(styles, options)` creates a replacement module from custom
220
+ definitions, including class-based styles such as `p.lead`. Importing
221
+ `blocks.js` also imports the convention editor and registers the default module.
222
+
223
+ Module names, command names, and routed `inputType` ownership are exclusive.
224
+ Adding the same module object twice is idempotent; a different module with the
225
+ same name or a command/input route already owned by the client throws. The
226
+ declaration and controls are snapshotted at registration, so later caller
227
+ mutation cannot change future surfaces. Installation into existing surfaces is
228
+ atomic. `delete()` accepts the registered module object or its name, removes its
229
+ commands and controls everywhere, and returns whether it existed. Changing
230
+ module topology clears transient pending caret marks so a removed formatter
231
+ cannot affect later input. A disposed client rejects new modules.
232
+
233
+ `setup({editor, core, root})` runs once when an extension is added.
234
+ `attach({editor, surface, pending, commands})` runs once for every current and
235
+ future rich-text surface, after that extension's commands are registered. Each
236
+ hook may return nothing or one object with `dispose()`. Surface disposal,
237
+ `delete()`, failed atomic installation, and editor disposal release those
238
+ objects in the reverse scope. There is deliberately no second `destroy()` or
239
+ cleanup-function convention.
240
+
241
+ This remains a narrow extension contract rather than a general plugin
242
+ framework: setup and attachment are synchronous, command/input ownership stays
243
+ exclusive, and an extension cannot reach private client state.
244
+ `commands(surface)` exposes that surface's registry or `null` for direct
245
+ inspection. `refresh()` re-reads the active surface's CSS and toolbar state.
246
+
247
+ The first active roaming surface creates one application-owned default toolbar
248
+ element and one small style element in the core root. `--u2-rte-toolbar` filters
249
+ its available controls through the normal `Toolbar` contract. Only `bold` is
250
+ shipped here yet; an unknown name such as `code` remains invisible rather than
251
+ pretending that its command exists. `--u2-rte-ui: none` prevents allocation
252
+ until another roaming surface becomes active. Leaving both the active surface
253
+ and toolbar hides it. Default placement uses the isolated `rangeRect()` browser
254
+ policy, so an empty native caret rectangle is derived from adjacent rendered
255
+ content without inserting a marker into the editor.
256
+ `--u2-rte-toolbar-when: selection` additionally keeps the toolbar hidden at a
257
+ caret and opens it only for a non-collapsed saved selection.
258
+ When Popover is available, the convention toolbar is a manual popover in the
259
+ browser top layer; its fixed range placement and focus-preserving behavior stay
260
+ the same inside a Document or ShadowRoot.
261
+
262
+ ### Visible line breaks
263
+
264
+ The optional root `breaks.js` entry is the first lifecycle extension:
265
+
266
+ ```js
267
+ import './breaks.js';
268
+ ```
269
+
270
+ ```css
271
+ [contenteditable] {
272
+ --u2-rte-show-breaks: true;
273
+ --u2-rte-toolbar: bold breaks;
274
+ }
275
+ ```
276
+
277
+ The CSS property chooses the initial view state when a surface connects.
278
+ Leaving `breaks` out of the toolbar list gives an always-on display with no
279
+ toggle. The `showBreaks` command runs outside an editing transaction, so it
280
+ emits a command event but no content-change event and never enters history.
281
+
282
+ The extension does not insert markers beside `<br>` nodes. One extension-owned
283
+ top-layer overlay contains presentation-only marks and is allocated only while
284
+ at least one surface shows them. A single root scroll listener, resize listener,
285
+ and `ResizeObserver` schedule all visible surfaces together; input and editor
286
+ changes schedule their own surface. Removing the extension restores its host
287
+ attributes, overlay, styles, observers, and listeners. Document and ShadowRoot
288
+ cores keep all of those resources in their own root. Nested editable surfaces
289
+ own their own `<br>` markers, so an outer surface never duplicates them.
290
+
291
+ ### Optional Unstyle
292
+
293
+ The optional root `unstyle.js` entry adds a selection-only `unstyle` action:
294
+
295
+ ```js
296
+ import './unstyle.js';
297
+ ```
298
+
299
+ ```css
300
+ [contenteditable] { --u2-rte-toolbar: bold unstyle; }
301
+ ```
302
+
303
+ Each click applies the first remaining presentation level. The button disables
304
+ when the selection has nothing covered by the policy. `unstyles(levels,
305
+ options)` creates a replacement module with application levels, command/control
306
+ names, label, and text. The underlying policy also cleans safely parsed
307
+ external fragments; the toolbar contains no paste-specific logic.
308
+
309
+ ### Optional rich external input
310
+
311
+ `externalInputs(options)` adapts the low-level `ExternalInput` boundary to the
312
+ convention client's normal extension lifecycle. It is needed only when an
313
+ application chooses to read and replace rich HTML before native insertion. A
314
+ sanitizer is mandatory and is never selected implicitly:
315
+
316
+ ```js
317
+ import {editor, externalInputs, NativeSanitizer} from '../../rte.js';
318
+
319
+ editor.add(externalInputs({sanitizer: new NativeSanitizer()}));
320
+ ```
321
+
322
+ The module registers mapped `insertFragment` and attaches one input boundary to
323
+ every current and future rich surface. Removing it disposes the listeners and
324
+ command everywhere. It contributes no toolbar control.
325
+
326
+ With the default Unstyle policy, an inherited CSS property chooses cumulative
327
+ presentation cleanup for each import:
328
+
329
+ ```css
330
+ [contenteditable] { --u2-rte-import-unstyle: styles; }
331
+ ```
332
+
333
+ The value may be `none`, `classes`, `styles`, `attributes`, or `formatting`.
334
+ Custom `Unstyle` policies may use their own level names. Pass `unstyle: null`
335
+ to disable this stage, or pass `through` as a fixed name or resolver function
336
+ to replace CSS resolution. The security sanitizer still always runs.
337
+
338
+ `rte.js` exports the factory but does not install it by default. Its ordinary
339
+ input pipeline leaves paste/drop insertion to the browser, removes classes and
340
+ inline styles from only the added elements, and then normalizes their structure.
341
+ That path needs no HTML parser. Applications importing arbitrary HTML strings
342
+ may still provide any safe adapter through the same `sanitize()` contract.
343
+
344
+ `dispose()` removes every installed input pipeline, listener, toolbar node, and
345
+ style node without disposing the supplied core. It is idempotent and is also
346
+ available through `[Symbol.dispose]()`; core disposal disposes the client. One
347
+ core accepts only one convention client at a time, preventing duplicate input
348
+ pipelines. Disposal releases that ownership so a new client can take over.
349
+
350
+ ## Invariants
351
+
352
+ - Importing the convention client adds only core listeners. It scans no DOM,
353
+ installs no observer, and creates no UI before a qualifying surface is active.
354
+ - Every mutable registry and pipeline belongs to one `Editor` instance and one
355
+ surface; different cores cannot share state.
356
+ - A module is installed once per rich surface, owns all command names and
357
+ controls it contributes, and is removed from all of them as one operation.
358
+ - Module installation either succeeds for every existing surface or leaves
359
+ every registry and input route unchanged.
360
+ - All editing still runs through public commands, transactions, point mapping,
361
+ normalization, and `Toolbar`. The client contains no second editor algorithm.
362
+ - The engine entry `rte.js` remains usable without importing this layer.
363
+
364
+ ## Browser considerations
365
+
366
+ Lazy registration uses the core's existing capture-phase focus route. The UI
367
+ uses a manual top-layer popover with fixed positioning around the saved range,
368
+ with non-mutating
369
+ collapsed-range geometry fallback, and intentionally does not claim final
370
+ viewport, zoom, touch, writing-mode, or virtual-keyboard policy.
371
+ The real-browser suite covers lazy focus setup, Enter routing, toolbar commands,
372
+ late module installation, future surfaces, rollback, removal, plain-text
373
+ exclusion, rich external-input composition, and complete teardown.
374
+
375
+ ## TODO exposed by the prototype
376
+
377
+ - Exercise lifecycle failure and coexistence further before adding dependency,
378
+ ordering, or asynchronous setup concepts.
379
+ - Decide whether the default UI becomes application DOM, a custom element, or
380
+ an optional UI module before stabilizing its styling hooks.
381
+ - Generalize placement for viewport edges, writing modes, zoom, touch handles,
382
+ and virtual keyboards.
383
+ - Decide separately whether `code` means inline `<code>` formatting or the old
384
+ editor's HTML-source UI; block styles now provide the first substantial
385
+ 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();