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
@@ -0,0 +1,159 @@
1
+ import {configuredElements} from '../config/config.js';
2
+ import {Edit, narrow} from './edit.js';
3
+ import {htmlModel} from '../model/html/html-model.js';
4
+
5
+ // Named editor commands for one surface. A command is a plain object with
6
+ // `run(edit)`, optional `enabled(edit)` and `state(edit)`, the native
7
+ // `inputTypes` it replaces, and an optional keyboard `shortcut`. Availability,
8
+ // state, execution, input routing, and keys stay separate so a UI, a keyboard
9
+ // module, and the input pipeline can use the same registry.
10
+ //
11
+ // Shortcuts belong here rather than to a toolbar so a key works whether or not
12
+ // a control for it is on screen.
13
+ export class Commands {
14
+ #surface;
15
+ #model;
16
+ #commands = new Map();
17
+ #inputs = new Map();
18
+ #keys = new Map();
19
+
20
+ constructor(surface, {model = htmlModel, commands = {}} = {}) {
21
+ if (typeof surface?.transact !== 'function') throw new TypeError('A command registry requires an editor surface');
22
+ if (typeof model?.allows !== 'function' || typeof model?.allowed !== 'function') {
23
+ throw new TypeError('A command registry requires a content model');
24
+ }
25
+ this.#surface = surface;
26
+ this.#model = model;
27
+ for (const [name, command] of Object.entries(commands)) this.add(name, command);
28
+ }
29
+
30
+ get surface() { return this.#surface; }
31
+ get model() {
32
+ return narrow(this.#model, configuredElements(this.#surface.element));
33
+ }
34
+ get names() { return [...this.#commands.keys()]; }
35
+
36
+ add(name, command) {
37
+ if (typeof name !== 'string' || !name.trim()) throw new TypeError('A command name must be a non-empty string');
38
+ if (typeof command?.run !== 'function') throw new TypeError('A command must implement run()');
39
+ if (command.enabled !== undefined && typeof command.enabled !== 'function') {
40
+ throw new TypeError('Command availability must be a function');
41
+ }
42
+ if (command.state !== undefined && typeof command.state !== 'function') {
43
+ throw new TypeError('Command state must be a function');
44
+ }
45
+ if (command.transaction !== undefined && typeof command.transaction !== 'boolean') {
46
+ throw new TypeError('Command transaction flag must be boolean');
47
+ }
48
+ const keys = chords(command.shortcut);
49
+ this.delete(name);
50
+ this.#commands.set(name, command);
51
+ for (const inputType of command.inputTypes || []) this.#inputs.set(inputType, name);
52
+ for (const key of keys) this.#keys.set(key, name);
53
+ return this;
54
+ }
55
+
56
+ delete(name) {
57
+ if (!this.#commands.has(name)) return false;
58
+ for (const [inputType, owner] of this.#inputs) if (owner === name) this.#inputs.delete(inputType);
59
+ for (const [key, owner] of this.#keys) if (owner === name) this.#keys.delete(key);
60
+ return this.#commands.delete(name);
61
+ }
62
+
63
+ get(name) {
64
+ return this.#commands.get(name) || null;
65
+ }
66
+
67
+ has(name) {
68
+ return this.#commands.has(name);
69
+ }
70
+
71
+ input(inputType) {
72
+ return this.#inputs.get(inputType) || null;
73
+ }
74
+
75
+ // The command a keystroke denotes, or null.
76
+ shortcut(event) {
77
+ return this.#keys.get(chord(event)) || null;
78
+ }
79
+
80
+ get keys() {
81
+ return new Map(this.#keys);
82
+ }
83
+
84
+ enabled(name, detail) {
85
+ const command = this.#commands.get(name);
86
+ return !!command && this.#allows(command, this.#edit(null, detail));
87
+ }
88
+
89
+ state(name, detail) {
90
+ const command = this.#commands.get(name);
91
+ if (!command?.state) return null;
92
+ return command.state(this.#edit(null, detail));
93
+ }
94
+
95
+ run(name, detail = {}) {
96
+ const command = this.#commands.get(name);
97
+ if (!command) throw new RangeError(`Unknown command: ${name}`);
98
+ const edit = this.#edit(null, detail);
99
+ if (!this.#allows(command, edit)) return;
100
+ if (command.transaction === false) {
101
+ const result = command.run(edit);
102
+ this.#surface.emit('u2-rte-command', {name, inputType: edit.inputType, transaction: null, result});
103
+ return result;
104
+ }
105
+ return this.#surface.transact(transaction => {
106
+ const edit = this.#edit(transaction, detail);
107
+ // The transaction restores the saved selection, so availability is
108
+ // decided again against the state the command actually sees.
109
+ if (!this.#allows(command, edit)) return;
110
+ const result = command.run(edit);
111
+ this.#surface.emit('u2-rte-command', {name, inputType: edit.inputType, transaction: edit.transaction, result});
112
+ return result;
113
+ // A caller may mark a run as ongoing input — a field edited live, a
114
+ // slider dragged — so history groups it instead of recording a step per
115
+ // keystroke.
116
+ }, {trigger: detail.trigger === 'input' ? 'input' : 'command', command: name, inputType: detail.inputType || ''});
117
+ }
118
+
119
+ // The base model is handed over unnarrowed: `Edit` resolves it only if the
120
+ // command actually consults it.
121
+ #edit(transaction, detail) {
122
+ return new Edit(this.#surface, transaction, {model: this.#model, ...detail});
123
+ }
124
+
125
+ #allows(command, edit) {
126
+ return command.enabled ? !!command.enabled(edit) : !!edit.range;
127
+ }
128
+ }
129
+
130
+ const MODIFIERS = ['ctrl', 'alt', 'shift'];
131
+
132
+ // A shortcut is one or more chords separated by spaces, each written as
133
+ // `[modifier+]*key`. `ctrl` matches Control or Command, so one declaration
134
+ // serves both platforms.
135
+ function chords(value) {
136
+ if (value === undefined || value === null) return [];
137
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError('A command shortcut must be a non-empty string');
138
+ return value.trim().toLowerCase().split(/\s+/).map(item => {
139
+ const parts = item.split('+');
140
+ const key = parts.pop();
141
+ if (!key || parts.some(part => !MODIFIERS.includes(part)) || new Set(parts).size !== parts.length) {
142
+ throw new TypeError(`Invalid command shortcut: ${item}`);
143
+ }
144
+ return [...MODIFIERS.filter(name => parts.includes(name)), key].join('+');
145
+ });
146
+ }
147
+
148
+ // The chord a keyboard event denotes. A digit is read from its physical key, so
149
+ // a shifted digit is the same shortcut on every keyboard layout, while letters
150
+ // keep their layout meaning.
151
+ function chord(event) {
152
+ const digit = /^Digit(\d)$/.exec(event.code || '');
153
+ const parts = [];
154
+ if (event.ctrlKey || event.metaKey) parts.push('ctrl');
155
+ if (event.altKey) parts.push('alt');
156
+ if (event.shiftKey) parts.push('shift');
157
+ parts.push(digit ? digit[1] : String(event.key).toLowerCase());
158
+ return parts.join('+');
159
+ }
@@ -0,0 +1,119 @@
1
+ import {elementOf} from '../selection/ownership/ownership.js';
2
+ import {Point} from '../selection/point/point.js';
3
+ import {blockEdge, emptyBlock} from './block-boundary.js';
4
+
5
+ // Collapsed deletion owns two boundaries: the one between compatible mergeable
6
+ // blocks, and the one beside an atomic block, which has no content to merge and
7
+ // is removed instead. Character and range deletion remain native.
8
+ export const deleteBackward = deletion('backward', 'deleteContentBackward');
9
+ export const deleteForward = deletion('forward', 'deleteContentForward');
10
+
11
+ function deletion(direction, inputType) {
12
+ return {
13
+ inputTypes: [inputType],
14
+ enabled: edit => !!boundary(edit, direction),
15
+ run(edit) {
16
+ const found = boundary(edit, direction);
17
+ if (!found) return undefined;
18
+ return found.atomic
19
+ ? discard(edit, found.atomic, found.between)
20
+ : merge(edit, found.left, found.right, found.between);
21
+ },
22
+ };
23
+ }
24
+
25
+ // An atomic block is indivisible and holds nothing to join: deleting across its
26
+ // boundary removes it and leaves the caret where it already was.
27
+ function discard(edit, node, between) {
28
+ const parent = node.parentElement;
29
+ const start = edit.range.start;
30
+ const caret = new Point(start.node, start.offset, 'forward');
31
+ edit.map.add(caret);
32
+ for (const skipped of between) edit.map.remove(skipped);
33
+ edit.map.remove(node);
34
+ edit.transaction.touch(parent);
35
+ edit.select(edit.map.get(caret));
36
+ return node;
37
+ }
38
+
39
+ function merge(edit, left, right, between) {
40
+ const parent = left.parentElement;
41
+ const leftEmpty = emptyBlock(left, edit.model);
42
+ const rightEmpty = emptyBlock(right, edit.model);
43
+ for (const node of between) edit.map.remove(node);
44
+ if (rightEmpty) {
45
+ edit.map.remove(right);
46
+ edit.transaction.touch(parent);
47
+ edit.select(leftEmpty
48
+ ? new Point(left, 0, 'forward')
49
+ : new Point(left, left.childNodes.length, 'backward'));
50
+ return left;
51
+ }
52
+
53
+ if (leftEmpty) while (left.firstChild) edit.map.remove(left.firstChild);
54
+ const caret = new Point(left, left.childNodes.length, 'backward');
55
+ edit.map.add(caret);
56
+ while (right.firstChild) edit.map.move(right.firstChild, left, left.childNodes.length);
57
+ edit.map.remove(right);
58
+ edit.transaction.touch(left);
59
+ edit.transaction.touch(parent);
60
+ edit.select(edit.map.get(caret));
61
+ return left;
62
+ }
63
+
64
+ function boundary(edit, direction) {
65
+ const point = edit.range?.collapsed && edit.range.start;
66
+ if (!point) return null;
67
+ // An engine can leave the caret inside an element that has no inside —
68
+ // clicking a rule does that in some. The position is at the element, not in
69
+ // it, and deleting there is what takes the element away.
70
+ const hollow = elementOf(point.node) === point.node && !point.node.childNodes.length
71
+ && point.node !== edit.element && edit.model.atomic(point.node);
72
+ if (hollow) return {atomic: point.node, between: []};
73
+ if (insideAtomic(edit, point.node)) return null;
74
+ const edge = direction === 'backward' ? 'start' : 'end';
75
+ for (let current = elementOf(point.node); current && current !== edit.element; current = current.parentElement) {
76
+ if (!edit.model.mergeable(current) || !atEdge(current, point, edge, edit.model)) continue;
77
+ const adjacent = neighbor(current, direction, edit.model);
78
+ if (atomicBlock(edit.model, adjacent.node)) return {atomic: adjacent.node, between: adjacent.between};
79
+ const [left, right] = direction === 'backward' ? [adjacent.node, current] : [current, adjacent.node];
80
+ if (compatible(edit.model, left, right)) return {left, right, between: adjacent.between};
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function neighbor(current, direction, model) {
86
+ const property = direction === 'backward' ? 'previousSibling' : 'nextSibling';
87
+ const between = [];
88
+ let node = current[property];
89
+ while (node && node.nodeType !== Node.ELEMENT_NODE && emptyBlock(node, model)) {
90
+ between.push(node);
91
+ node = node[property];
92
+ }
93
+ return {node, between};
94
+ }
95
+
96
+ function atomicBlock(model, node) {
97
+ return node?.nodeType === Node.ELEMENT_NODE && model.block(node) && model.atomic(node);
98
+ }
99
+
100
+ function compatible(model, left, right) {
101
+ if (left?.nodeType !== Node.ELEMENT_NODE || right?.nodeType !== Node.ELEMENT_NODE
102
+ || !model.mergeable(left) || !model.mergeable(right)) return false;
103
+ if (emptyBlock(right, model)) return true;
104
+ return [...right.childNodes].every(child => model.allows(left, child));
105
+ }
106
+
107
+ function atEdge(unit, point, edge, model) {
108
+ // Every DOM position inside an empty block denotes its one visual caret.
109
+ // A filler br is therefore not a special boundary of its own.
110
+ return emptyBlock(unit, model) || blockEdge(unit, point, edge);
111
+ }
112
+
113
+ function insideAtomic(edit, node) {
114
+ for (let element = elementOf(node); element && element !== edit.element; element = element.parentElement) {
115
+ if (edit.model.atomic(element)) return true;
116
+ }
117
+ return false;
118
+ }
119
+
@@ -0,0 +1,78 @@
1
+ import {configuredElements} from '../config/config.js';
2
+ import {htmlModel} from '../model/html/html-model.js';
3
+ import {EditRange} from '../selection/range/edit-range.js';
4
+ import {PointMap} from '../selection/map/point-map.js';
5
+
6
+ // One command execution: the affected range, the point map every mutation goes
7
+ // through, and the resulting selection. Commands own their algorithm, not this
8
+ // plumbing.
9
+ export class Edit {
10
+ #surface;
11
+ #transaction;
12
+ #model;
13
+ #narrowed = null;
14
+ #inputType;
15
+ #data;
16
+ #value;
17
+ #fragment;
18
+ #range;
19
+ #map = new PointMap();
20
+
21
+ constructor(surface, transaction = null, {
22
+ model = htmlModel,
23
+ range = null,
24
+ inputType = '',
25
+ data = null,
26
+ value = null,
27
+ fragment = null,
28
+ } = {}) {
29
+ if (surface?.element?.nodeType !== Node.ELEMENT_NODE) throw new TypeError('An edit requires an editor surface');
30
+ if (data !== null && typeof data !== 'string') throw new TypeError('Edit input data must be a string or null');
31
+ if (fragment !== null && fragment?.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
32
+ throw new TypeError('Edit fragment must be a DocumentFragment or null');
33
+ }
34
+ this.#surface = surface;
35
+ this.#transaction = transaction;
36
+ this.#model = model;
37
+ this.#inputType = inputType;
38
+ this.#data = data;
39
+ this.#value = value;
40
+ this.#fragment = fragment;
41
+ // The same rule a transaction applies: a live selection inside the
42
+ // surface is newer than the saved one, and a selection outside it says
43
+ // nothing about what is being edited — a field of the editor's own
44
+ // chrome holds the caret while it names what is still selected here.
45
+ this.#range = range
46
+ ? EditRange.fromRange(range, surface.element)
47
+ : EditRange.fromSelection(surface.core.selection, surface.element)
48
+ || (surface.selection ? EditRange.fromRange(surface.selection.range(), surface.element) : null);
49
+ }
50
+
51
+ get surface() { return this.#surface; }
52
+ get element() { return this.#surface.element; }
53
+ get config() { return this.#surface.config; }
54
+ get transaction() { return this.#transaction; }
55
+ // Narrowing the model reads the host configuration, and most availability
56
+ // checks never consult it. Resolving it on first use keeps a toolbar
57
+ // refresh from paying for every control that does not care.
58
+ get model() {
59
+ this.#narrowed ??= narrow(this.#model, configuredElements(this.#surface.element));
60
+ return this.#narrowed;
61
+ }
62
+ get map() { return this.#map; }
63
+ get inputType() { return this.#inputType; }
64
+ get data() { return this.#data; }
65
+ get value() { return this.#value; }
66
+ get fragment() { return this.#fragment; }
67
+ get range() { return this.#range; }
68
+ get document() { return this.#surface.element.ownerDocument; }
69
+
70
+ select(start, end = start, backward = false) {
71
+ const range = EditRange.fromPoints(start, end, this.element);
72
+ return range.select(this.#surface.core.selection, backward) ? range : null;
73
+ }
74
+ }
75
+
76
+ export function narrow(model, elements) {
77
+ return elements === null || typeof model.withElements !== 'function' ? model : model.withElements(elements);
78
+ }
@@ -0,0 +1,45 @@
1
+ import {Point} from '../selection/point/point.js';
2
+
3
+ // The one element a selection covers, when it covers exactly that element and
4
+ // nothing else. Clicking an image inside a contenteditable produces such a
5
+ // selection, which is what makes an atomic element addressable at all.
6
+ export function selectedElement(edit, match = null) {
7
+ if (!edit.range || edit.range.collapsed) return null;
8
+ const nodes = edit.range.roots();
9
+ const element = nodes.length === 1 && nodes[0].nodeType === Node.ELEMENT_NODE ? nodes[0] : null;
10
+ if (!element || !edit.element.contains(element)) return null;
11
+ return !match || match(element, edit) ? element : null;
12
+ }
13
+
14
+ // A value command over a fixed set of that element's attributes. A missing or
15
+ // empty value removes its attribute, which is what "back to its own size" means
16
+ // for an image, so one command both sets and clears.
17
+ export function elementAttributes(names, {match = null} = {}) {
18
+ if (!Array.isArray(names) || !names.length) throw new TypeError('An attribute command requires attribute names');
19
+ const attributes = names.map(name => String(name).toLowerCase());
20
+ return {
21
+ enabled: edit => !!selectedElement(edit, match),
22
+ state(edit) {
23
+ const element = selectedElement(edit, match);
24
+ if (!element) return null;
25
+ const value = {};
26
+ for (const name of attributes) {
27
+ if (element.hasAttribute(name)) value[name] = element.getAttribute(name);
28
+ }
29
+ return value;
30
+ },
31
+ run(edit) {
32
+ const element = selectedElement(edit, match);
33
+ if (!element) return null;
34
+ for (const name of attributes) {
35
+ const value = edit.value?.[name];
36
+ if (value === undefined || value === null || value === '') element.removeAttribute(name);
37
+ else element.setAttribute(name, String(value));
38
+ }
39
+ edit.transaction.touch(element);
40
+ // The element stays selected, so the UI acting on it stays up.
41
+ edit.select(Point.before(element), Point.after(element));
42
+ return element;
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,175 @@
1
+ import {elementOf} from '../selection/ownership/ownership.js';
2
+ import {Point} from '../selection/point/point.js';
3
+ import {blank, blockEdge, emptyBlock, fill} from './block-boundary.js';
4
+
5
+ // The host policy names the element Enter splits; `block` follows the
6
+ // configured default block, the structural values name their own unit.
7
+ const UNITS = Object.freeze({item: ['li'], row: ['tr'], cell: ['td', 'th']});
8
+ const LISTS = Object.freeze(['ul', 'ol', 'menu']);
9
+
10
+ export const enter = {
11
+ inputTypes: ['insertParagraph'],
12
+ enabled: editable,
13
+ run(edit) {
14
+ const exited = exitList(edit);
15
+ if (exited) return exited;
16
+ const unit = structure(edit);
17
+ return unit ? split(edit, unit) : insertBreak(edit);
18
+ },
19
+ };
20
+
21
+ export const lineBreak = {
22
+ inputTypes: ['insertLineBreak'],
23
+ enabled: editable,
24
+ run: edit => insertBreak(edit),
25
+ };
26
+
27
+ // Both commands replace one caret with new structure. A selection would have to
28
+ // be deleted first, and atomic content is indivisible, so the browser keeps its
29
+ // native behavior in those cases.
30
+ function editable(edit) {
31
+ if (!edit.range?.collapsed) return false;
32
+ for (let element = elementOf(edit.range.start.node); element && element !== edit.element; element = element.parentElement) {
33
+ if (edit.model.atomic(element)) return false;
34
+ }
35
+ return true;
36
+ }
37
+
38
+ function structure(edit) {
39
+ const {enter: mode, block: tag} = edit.config;
40
+ if (mode === 'break') return null;
41
+ if (mode === 'block') {
42
+ const item = ancestor(edit, ['li']);
43
+ if (item && LISTS.includes(item.parentElement?.localName)) return item;
44
+ const textBlock = ancestor(edit, element => edit.model.textBlock(element));
45
+ if (textBlock) return textBlock;
46
+ }
47
+ return ancestor(edit, UNITS[mode] || (tag ? [tag] : []));
48
+ }
49
+
50
+ function ancestor(edit, match) {
51
+ if (Array.isArray(match)) {
52
+ const tags = match;
53
+ match = element => tags.includes(element.localName);
54
+ }
55
+ for (let element = elementOf(edit.range.start.node); element && element !== edit.element; element = element.parentElement) {
56
+ if (match(element) && edit.model.allows(element.parentElement, element)) return element;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function exitList(edit) {
62
+ const {enter: mode, block: tag} = edit.config;
63
+ if (!tag || mode !== 'block' && mode !== 'item') return null;
64
+ let item = elementOf(edit.range.start.node);
65
+ while (item && item !== edit.element && item.localName !== 'li') item = item.parentElement;
66
+ if (!item || item === edit.element || !emptyBlock(item, edit.model)) return null;
67
+ const list = item.parentElement;
68
+ if (!LISTS.includes(list?.localName) || list === edit.element) return null;
69
+ const parent = list.parentElement;
70
+ const block = edit.document.createElement(tag);
71
+ if (!edit.model.allows(parent, block)) return null;
72
+
73
+ const start = continuation(list, item);
74
+ const offset = [...list.childNodes].indexOf(item);
75
+ edit.map.remove(item);
76
+ const at = edit.map.split(parent, list, offset);
77
+ const tail = parent.childNodes[at];
78
+ let trailing = tail;
79
+ edit.map.insert(parent, at, block);
80
+ edit.map.insert(block, 0, edit.document.createElement('br'));
81
+ if (!list.children.length) {
82
+ if (tail.children.length) {
83
+ while (tail.firstChild) edit.map.move(tail.firstChild, list, list.childNodes.length);
84
+ edit.map.remove(tail);
85
+ edit.map.move(list, parent, [...parent.childNodes].indexOf(block) + 1);
86
+ trailing = list;
87
+ } else {
88
+ edit.map.remove(list);
89
+ edit.map.remove(tail);
90
+ trailing = null;
91
+ }
92
+ } else if (!tail.children.length) {
93
+ edit.map.remove(tail);
94
+ trailing = null;
95
+ }
96
+ if (start !== null && trailing) trailing.setAttribute('start', start);
97
+ edit.transaction.touch(parent);
98
+ edit.select(new Point(block, 0, 'forward'));
99
+ return block;
100
+ }
101
+
102
+ function continuation(list, item) {
103
+ if (list.localName !== 'ol') return null;
104
+ const items = [...list.children].filter(child => child.localName === 'li');
105
+ const target = items.indexOf(item) + 1;
106
+ if (!target || target >= items.length || items[target].hasAttribute('value')) return null;
107
+ let value = list.hasAttribute('start') ? list.start : list.reversed ? items.length : 1;
108
+ const step = list.reversed ? -1 : 1;
109
+ for (let index = 0; index < target; index++) {
110
+ if (items[index].hasAttribute('value')) value = items[index].value;
111
+ value += step;
112
+ }
113
+ return String(value);
114
+ }
115
+
116
+ function split(edit, unit) {
117
+ const start = edit.range.start;
118
+ const defaultBlock = continuationBlock(edit, unit, start);
119
+ const caret = new Point(start.node, start.offset, 'forward');
120
+ edit.map.add(caret);
121
+ const parent = unit.parentElement;
122
+ let tail = parent.childNodes[edit.map.split(parent, start.node, start.offset)];
123
+ if (defaultBlock && [...tail.childNodes].every(child => edit.model.allows(defaultBlock, child))) {
124
+ tail = edit.map.replaceWrapper(tail, defaultBlock);
125
+ }
126
+ const position = edit.map.get(caret);
127
+ fill(edit, unit);
128
+ fill(edit, tail);
129
+ edit.transaction.touch(unit);
130
+ edit.transaction.touch(tail);
131
+ edit.select(position);
132
+ return tail;
133
+ }
134
+
135
+ function continuationBlock(edit, unit, point) {
136
+ const tag = edit.config.block;
137
+ if (!tag || !edit.model.textBlock(unit) || unit.localName === tag || !blockEdge(unit, point, 'end')) return null;
138
+ const block = edit.document.createElement(tag);
139
+ if (!edit.model.allows(unit.parentElement, block)) return null;
140
+ return block;
141
+ }
142
+
143
+ function insertBreak(edit) {
144
+ const start = edit.range.start;
145
+ const caret = new Point(start.node, start.offset, 'forward');
146
+ edit.map.add(caret);
147
+ const parent = elementOf(start.node);
148
+ const index = edit.map.split(parent, start.node, start.offset);
149
+ const separator = edit.document.createElement('br');
150
+ edit.map.insert(parent, index, separator);
151
+ const position = edit.map.get(caret);
152
+ // A break at the end of its block needs a second one to become visible.
153
+ if (!following(edit, separator)) edit.map.insert(parent, index + 1, edit.document.createElement('br'));
154
+ edit.transaction.touch(parent);
155
+ edit.select(position);
156
+ return separator;
157
+ }
158
+
159
+ function following(edit, node) {
160
+ const limit = block(edit, node);
161
+ for (let current = node; current && current !== limit; current = current.parentElement) {
162
+ for (let sibling = current.nextSibling; sibling; sibling = sibling.nextSibling) {
163
+ if (!blank(edit.model, sibling)) return true;
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+
169
+ function block(edit, node) {
170
+ for (let element = node.parentElement; element && element !== edit.element; element = element.parentElement) {
171
+ if (edit.model.block(element)) return element;
172
+ }
173
+ return edit.element;
174
+ }
175
+
@@ -0,0 +1,76 @@
1
+ import {EditRange} from '../selection/range/edit-range.js';
2
+ import {Point, indexOf} from '../selection/point/point.js';
3
+
4
+ // Inserts only an already prepared DOM fragment. Parsing, security sanitizing,
5
+ // presentation cleanup, and clipboard access remain outside this command.
6
+ export const insertFragment = {
7
+ enabled: edit => !!edit.range && !!edit.fragment
8
+ && (!!edit.fragment.childNodes.length || !edit.range.collapsed),
9
+ run(edit) {
10
+ const state = prepare(edit);
11
+ if (!edit.range.collapsed) removeSelection(edit, state);
12
+ const nodes = [...edit.fragment.childNodes];
13
+ const end = new Point(edit.fragment, nodes.length, 'forward');
14
+ edit.map.add(end);
15
+ if (nodes.length) {
16
+ const point = edit.map.get(state.start);
17
+ const target = insertionTarget(edit, point, nodes);
18
+ edit.map.insertFragment(target.parent, target.offset, edit.fragment);
19
+ edit.transaction.touch(target.parent);
20
+ for (const node of nodes) edit.transaction.touch(node);
21
+ }
22
+ edit.select(nodes.length ? edit.map.get(end) : edit.map.get(state.start));
23
+ return nodes;
24
+ },
25
+ };
26
+
27
+ function prepare(edit) {
28
+ const start = new Point(edit.range.start.node, edit.range.start.offset, 'forward');
29
+ const end = new Point(edit.range.end.node, edit.range.end.offset, 'backward');
30
+ edit.map.add(start).add(end);
31
+ splitText(edit, end);
32
+ splitText(edit, start);
33
+ return {start, end};
34
+ }
35
+
36
+ function splitText(edit, point) {
37
+ const current = edit.map.get(point);
38
+ if (current.node.nodeType === Node.TEXT_NODE && current.offset > 0 && current.offset < current.node.length) {
39
+ edit.map.splitText(current.node, current.offset);
40
+ }
41
+ }
42
+
43
+ function removeSelection(edit, state) {
44
+ const range = EditRange.fromPoints(edit.map.get(state.start), edit.map.get(state.end), edit.element);
45
+ for (const node of range.roots()) {
46
+ const parent = node.parentNode;
47
+ edit.map.remove(node);
48
+ edit.transaction.touch(parent);
49
+ }
50
+ }
51
+
52
+ function insertionTarget(edit, point, nodes) {
53
+ let {parent, offset} = childBoundary(point);
54
+ while (parent !== edit.element && !nodes.every(node => edit.model.allows(parent, node))) {
55
+ const grand = parent.parentNode;
56
+ const index = indexOf(parent);
57
+ if (offset === 0) {
58
+ offset = index;
59
+ } else if (offset === parent.childNodes.length) {
60
+ offset = index + 1;
61
+ } else {
62
+ offset = edit.map.split(grand, parent, offset);
63
+ edit.transaction.touch(parent).touch(grand);
64
+ }
65
+ parent = grand;
66
+ }
67
+ return {parent, offset};
68
+ }
69
+
70
+ function childBoundary(point) {
71
+ if (point.node.nodeType !== Node.TEXT_NODE) return {parent: point.node, offset: point.offset};
72
+ const parent = point.node.parentNode;
73
+ const index = indexOf(point.node);
74
+ return {parent, offset: index + (point.offset === point.node.length ? 1 : 0)};
75
+ }
76
+