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.
- package/el/ico/ico.js +5 -5
- package/el/rte/README.md +53 -13
- package/el/rte/manifest.json +19 -4
- package/el/rte/rte.css +31 -6
- package/el/rte/rte.js +174 -44
- package/el/rte0/README.md +37 -0
- package/el/rte0/manifest.json +33 -0
- package/el/rte0/rte0.css +9 -0
- package/el/rte0/rte0.js +56 -0
- package/el/system/styler.js +2 -0
- package/el/tooltip/tooltip.css +2 -2
- package/js/Placer/Placer.js +2 -1
- package/js/rte/AGENTS.md +142 -0
- package/js/rte/PLAN.md +488 -0
- package/js/rte/README.md +319 -25
- package/js/rte/blocks.js +7 -0
- package/js/rte/breaks.js +6 -0
- package/js/rte/classes.js +7 -0
- package/js/rte/docs/usage.md +845 -0
- package/js/rte/images.js +7 -0
- package/js/rte/link.js +8 -0
- package/js/rte/playground/README.md +95 -0
- package/js/rte/playground/hosts.html +518 -0
- package/js/rte/playground/index.html +824 -0
- package/js/rte/rte.js +56 -5
- package/js/rte/source.js +12 -0
- package/js/rte/src/browser/README.md +40 -0
- package/js/rte/src/browser/range-rect.js +82 -0
- package/js/rte/src/client/README.md +378 -0
- package/js/rte/src/client/blocks.js +38 -0
- package/js/rte/src/client/breaks.js +212 -0
- package/js/rte/src/client/classes.js +48 -0
- package/js/rte/src/client/editor.js +461 -0
- package/js/rte/src/client/external-input.js +47 -0
- package/js/rte/src/client/history.js +13 -0
- package/js/rte/src/client/images.js +275 -0
- package/js/rte/src/client/link.js +572 -0
- package/js/rte/src/client/marks.js +25 -0
- package/js/rte/src/client/source.js +182 -0
- package/js/rte/src/client/structure.js +27 -0
- package/js/rte/src/client/tables.js +162 -0
- package/js/rte/src/client/unstyle.js +32 -0
- package/js/rte/src/command/README.md +517 -0
- package/js/rte/src/command/block-boundary.js +43 -0
- package/js/rte/src/command/block-style.js +120 -0
- package/js/rte/src/command/commands.js +158 -0
- package/js/rte/src/command/delete.js +112 -0
- package/js/rte/src/command/edit.js +72 -0
- package/js/rte/src/command/element.js +45 -0
- package/js/rte/src/command/enter.js +175 -0
- package/js/rte/src/command/fragment.js +76 -0
- package/js/rte/src/command/insert.js +52 -0
- package/js/rte/src/command/list.js +284 -0
- package/js/rte/src/command/mark.js +583 -0
- package/js/rte/src/command/pending-marks.js +216 -0
- package/js/rte/src/command/table.js +207 -0
- package/js/rte/src/command/unstyle.js +248 -0
- package/js/rte/src/config/README.md +96 -0
- package/js/rte/src/config/config.js +127 -0
- package/js/rte/src/core/README.md +41 -0
- package/js/rte/src/core/core.js +156 -0
- package/js/rte/src/history/README.md +83 -0
- package/js/rte/src/history/history.js +283 -0
- package/js/rte/src/input/README.md +155 -0
- package/js/rte/src/input/external-input.js +130 -0
- package/js/rte/src/input/input-pipeline.js +414 -0
- package/js/rte/src/mark/README.md +219 -0
- package/js/rte/src/mark/dom-adapter.js +100 -0
- package/js/rte/src/mark/mark.js +133 -0
- package/js/rte/src/mark/standard.js +76 -0
- package/js/rte/src/model/README.md +71 -0
- package/js/rte/src/model/content-model.js +213 -0
- package/js/rte/src/model/html/README.md +51 -0
- package/js/rte/src/model/html/html-model.js +90 -0
- package/js/rte/src/normalize/execute/README.md +33 -0
- package/js/rte/src/normalize/execute/repair-executor.js +129 -0
- package/js/rte/src/normalize/normalizer/README.md +48 -0
- package/js/rte/src/normalize/normalizer/normalizer.js +141 -0
- package/js/rte/src/normalize/repair/README.md +70 -0
- package/js/rte/src/normalize/repair/repair-planner.js +208 -0
- package/js/rte/src/sanitize/README.md +105 -0
- package/js/rte/src/sanitize/native.js +43 -0
- package/js/rte/src/sanitize/policy.js +202 -0
- package/js/rte/src/selection/README.md +39 -0
- package/js/rte/src/selection/map/README.md +53 -0
- package/js/rte/src/selection/map/point-map.js +307 -0
- package/js/rte/src/selection/ownership/README.md +32 -0
- package/js/rte/src/selection/ownership/ownership.js +42 -0
- package/js/rte/src/selection/point/README.md +34 -0
- package/js/rte/src/selection/point/point.js +83 -0
- package/js/rte/src/selection/range/README.md +38 -0
- package/js/rte/src/selection/range/edit-range.js +223 -0
- package/js/rte/src/selection/snapshot.js +64 -0
- package/js/rte/src/source/README.md +64 -0
- package/js/rte/src/source/source.js +193 -0
- package/js/rte/src/surface/README.md +36 -0
- package/js/rte/src/surface/content.js +16 -0
- package/js/rte/src/surface/surface.js +87 -0
- package/js/rte/src/transaction/README.md +35 -0
- package/js/rte/src/transaction/transaction.js +56 -0
- package/js/rte/src/ui/README.md +255 -0
- package/js/rte/src/ui/chrome.js +214 -0
- package/js/rte/src/ui/handles.js +179 -0
- package/js/rte/src/ui/place.js +25 -0
- package/js/rte/src/ui/toolbar.js +283 -0
- package/js/rte/src/unstyle/README.md +103 -0
- package/js/rte/src/unstyle/unstyle.js +140 -0
- package/js/rte/tables.js +7 -0
- package/js/rte/unstyle.js +8 -0
- package/js/rte0/README.md +37 -0
- package/js/rte0/rte.js +7 -0
- package/js/{rte → rte0}/src/behavior.js +0 -1
- package/js/{rte → rte0}/src/events.js +0 -1
- package/js/{rte → rte0}/src/range.js +0 -2
- package/js/{rte → rte0}/src/ui.items.js +0 -2
- package/js/{rte → rte0}/src/ui.js +0 -1
- package/package.json +1 -1
- package/u2/Element.js +2 -2
- package/u2/plan-scoped-refactoring.md +133 -303
- package/u2/projects.json +18 -2
- package/u2/tools/update.repos.json.js +1 -1
- package/el/qrcode/package.json +0 -22
- /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
- /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
- /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/main.css +0 -0
- /package/js/{rte → rte0}/old/old.rte.js +0 -0
- /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
- /package/js/{rte → rte0}/src/fixes.js +0 -0
- /package/js/{rte → rte0}/src/rte.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
- /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
- /package/js/{rte → rte0}/src/util/indentHtml.js +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {Edit} from './edit.js';
|
|
2
|
+
import {applyMark, removeMark, toggleMark} from './mark.js';
|
|
3
|
+
import {editingHost, elementOf, isEditingBoundary, isPlainTextHost} from '../selection/ownership/ownership.js';
|
|
4
|
+
import {Mark} from '../mark/mark.js';
|
|
5
|
+
import {Point} from '../selection/point/point.js';
|
|
6
|
+
|
|
7
|
+
// Pending mark overrides for one surface. They remain valid only while the
|
|
8
|
+
// surface keeps the same captured selection, so moving the caret invalidates
|
|
9
|
+
// them without another selection listener.
|
|
10
|
+
export class PendingMarks {
|
|
11
|
+
#surface;
|
|
12
|
+
#selection = null;
|
|
13
|
+
#entries = new Map();
|
|
14
|
+
#insertText;
|
|
15
|
+
#composition = null;
|
|
16
|
+
#controller;
|
|
17
|
+
|
|
18
|
+
constructor(surface) {
|
|
19
|
+
if (surface?.element?.nodeType !== Node.ELEMENT_NODE || typeof surface?.capture !== 'function') {
|
|
20
|
+
throw new TypeError('Pending marks require an editor surface');
|
|
21
|
+
}
|
|
22
|
+
this.#surface = surface;
|
|
23
|
+
this.#insertText = {
|
|
24
|
+
inputTypes: ['insertText'],
|
|
25
|
+
enabled: edit => this.#current(edit) && typeof edit.data === 'string' && !!edit.data,
|
|
26
|
+
run: edit => this.#insert(edit),
|
|
27
|
+
};
|
|
28
|
+
const root = surface.element;
|
|
29
|
+
this.#controller = new root.ownerDocument.defaultView.AbortController();
|
|
30
|
+
const listen = {signal: this.#controller.signal};
|
|
31
|
+
root.addEventListener('compositionstart', this.#compositionStart, listen);
|
|
32
|
+
root.addEventListener('compositionend', this.#compositionEnd, listen);
|
|
33
|
+
surface.addEventListener('u2-rte-disconnect', () => this.dispose(), listen);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get surface() { return this.#surface; }
|
|
37
|
+
get insertText() { return this.#insertText; }
|
|
38
|
+
get connected() { return !this.#controller.signal.aborted; }
|
|
39
|
+
|
|
40
|
+
toggle(adapter, value) {
|
|
41
|
+
const hasValue = arguments.length > 1;
|
|
42
|
+
const toggle = hasValue ? toggleMark(adapter, value) : toggleMark(adapter);
|
|
43
|
+
const apply = hasValue ? applyMark(adapter, value) : applyMark(adapter);
|
|
44
|
+
const remove = hasValue ? removeMark(adapter, value) : removeMark(adapter);
|
|
45
|
+
const mark = value instanceof Mark ? value : adapter.type.create(hasValue ? value : true);
|
|
46
|
+
const key = {};
|
|
47
|
+
return {
|
|
48
|
+
enabled: edit => this.#owns(edit) && (!!edit.range && (edit.range.collapsed
|
|
49
|
+
? markable(edit, adapter, mark, toggle.state(edit) === true)
|
|
50
|
+
: toggle.enabled(edit))),
|
|
51
|
+
state: edit => this.#entry(edit, key)?.active ?? toggle.state(edit),
|
|
52
|
+
run: edit => {
|
|
53
|
+
this.#assert(edit);
|
|
54
|
+
if (!edit.range?.collapsed) {
|
|
55
|
+
this.clear();
|
|
56
|
+
return toggle.run(edit);
|
|
57
|
+
}
|
|
58
|
+
if (!markable(edit, adapter, mark, toggle.state(edit) === true)) return;
|
|
59
|
+
const base = toggle.state(edit);
|
|
60
|
+
const active = (this.#entry(edit, key)?.active ?? base) !== true;
|
|
61
|
+
this.#prepare(edit);
|
|
62
|
+
if (active === base) this.#entries.delete(key);
|
|
63
|
+
else this.#entries.set(key, {active, apply, remove, adapter, mark, model: edit.model});
|
|
64
|
+
return active;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear() {
|
|
70
|
+
this.#selection = null;
|
|
71
|
+
this.#entries.clear();
|
|
72
|
+
this.#composition = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
dispose() {
|
|
76
|
+
if (!this.connected) return;
|
|
77
|
+
this.#controller.abort();
|
|
78
|
+
this.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[Symbol.dispose]() {
|
|
82
|
+
this.dispose();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#insert(edit) {
|
|
86
|
+
this.#assert(edit);
|
|
87
|
+
const entries = [...this.#entries.values()];
|
|
88
|
+
const text = insert(edit, edit.data);
|
|
89
|
+
this.clear();
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const current = new Edit(this.#surface, edit.transaction, {
|
|
92
|
+
model: edit.model,
|
|
93
|
+
inputType: edit.inputType,
|
|
94
|
+
data: edit.data,
|
|
95
|
+
});
|
|
96
|
+
(entry.active ? entry.apply : entry.remove).run(current);
|
|
97
|
+
}
|
|
98
|
+
const selection = this.#surface.core.selection;
|
|
99
|
+
const range = selection.getRangeAt(0);
|
|
100
|
+
edit.select(Point.fromRange(range, 'end'));
|
|
101
|
+
return text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#prepare(edit) {
|
|
105
|
+
if (this.#selection !== this.#surface.selection) this.#entries.clear();
|
|
106
|
+
this.#selection = this.#surface.selection;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#current(edit) {
|
|
110
|
+
return this.#owns(edit)
|
|
111
|
+
&& edit.range?.collapsed
|
|
112
|
+
&& [...this.#entries.values()].every(entry => markable(edit, entry.adapter, entry.mark, !entry.active))
|
|
113
|
+
&& this.#selection === this.#surface.selection
|
|
114
|
+
&& this.#entries.size > 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#entry(edit, key) {
|
|
118
|
+
return this.#current(edit) ? this.#entries.get(key) : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#owns(edit) {
|
|
122
|
+
return this.connected && edit?.surface === this.#surface && !isPlainTextHost(this.#surface.element);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#assert(edit) {
|
|
126
|
+
if (!this.#owns(edit)) throw new RangeError('Pending marks belong to one rich-text surface');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#compositionStart = event => {
|
|
130
|
+
if (!this.#ownsEvent(event)) return;
|
|
131
|
+
const edit = new Edit(this.#surface);
|
|
132
|
+
const entries = [...this.#entries.values()];
|
|
133
|
+
if (!edit.range?.collapsed || this.#selection !== this.#surface.selection || !entries.length) return;
|
|
134
|
+
this.#composition = {
|
|
135
|
+
start: edit.range.start.withAffinity('backward'),
|
|
136
|
+
entries,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
#compositionEnd = event => {
|
|
141
|
+
if (!this.#ownsEvent(event) || !this.#composition) return;
|
|
142
|
+
const {start, entries} = this.#composition;
|
|
143
|
+
this.#composition = null;
|
|
144
|
+
const current = new Edit(this.#surface).range;
|
|
145
|
+
if (!current?.collapsed || !start.within(this.#surface.element)) {
|
|
146
|
+
this.clear();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const end = current.end.withAffinity('forward');
|
|
150
|
+
if (start.compare(end) === 0) {
|
|
151
|
+
this.#selection = this.#surface.capture();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (start.compare(end) > 0) {
|
|
155
|
+
this.clear();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const range = start.range();
|
|
159
|
+
range.setEnd(end.node, end.offset);
|
|
160
|
+
if (!range.toString()) {
|
|
161
|
+
this.clear();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.clear();
|
|
165
|
+
this.#surface.transact(transaction => {
|
|
166
|
+
let active = range;
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const edit = new Edit(this.#surface, transaction, {
|
|
169
|
+
model: entry.model,
|
|
170
|
+
range: active,
|
|
171
|
+
inputType: 'insertCompositionText',
|
|
172
|
+
data: typeof event.data === 'string' ? event.data : null,
|
|
173
|
+
});
|
|
174
|
+
(entry.active ? entry.apply : entry.remove).run(edit);
|
|
175
|
+
active = this.#surface.core.selection.getRangeAt(0).cloneRange();
|
|
176
|
+
}
|
|
177
|
+
const edit = new Edit(this.#surface, transaction, {range: active});
|
|
178
|
+
edit.select(edit.range.end);
|
|
179
|
+
}, {trigger: 'input', inputType: 'insertCompositionText', composition: true});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
#ownsEvent(event) {
|
|
183
|
+
const target = event.composedPath()[0];
|
|
184
|
+
return target === this.#surface.element || editingHost(target) === this.#surface.element;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function insert(edit, data) {
|
|
189
|
+
const start = edit.range.start.withAffinity('backward');
|
|
190
|
+
const end = edit.range.end.withAffinity('forward');
|
|
191
|
+
edit.map.add(start).add(end);
|
|
192
|
+
const node = start.node;
|
|
193
|
+
let text;
|
|
194
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
195
|
+
text = node;
|
|
196
|
+
edit.map.insertText(node, start.offset, data);
|
|
197
|
+
} else {
|
|
198
|
+
text = edit.document.createTextNode(data);
|
|
199
|
+
edit.map.insert(node, start.offset, text);
|
|
200
|
+
}
|
|
201
|
+
edit.transaction.touch(text.parentNode);
|
|
202
|
+
edit.select(edit.map.get(start), edit.map.get(end));
|
|
203
|
+
return text;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function markable(edit, adapter, mark, removing) {
|
|
207
|
+
if (!edit.range?.collapsed) return false;
|
|
208
|
+
for (let element = elementOf(edit.range.start.node); element && element !== edit.element; element = element.parentElement) {
|
|
209
|
+
if (isEditingBoundary(element) || edit.model.atomic(element)) return false;
|
|
210
|
+
}
|
|
211
|
+
if (removing) return true;
|
|
212
|
+
const node = edit.range.start.node;
|
|
213
|
+
const parent = node.nodeType === Node.TEXT_NODE ? node.parentNode : node;
|
|
214
|
+
return edit.model.allows(parent, adapter.render(mark, edit.document));
|
|
215
|
+
}
|
|
216
|
+
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {elementOf} from '../selection/ownership/ownership.js';
|
|
2
|
+
import {fill} from './block-boundary.js';
|
|
3
|
+
import {Point} from '../selection/point/point.js';
|
|
4
|
+
|
|
5
|
+
// Table structure commands.
|
|
6
|
+
//
|
|
7
|
+
// Sections, rows, and cells are never named: they come from the model's default
|
|
8
|
+
// child, so a configured table-like structure works the same way. Every change
|
|
9
|
+
// is an ordinary mapped mutation, which is what makes it one undo step and lets
|
|
10
|
+
// the caret land where the edit happened.
|
|
11
|
+
export class Tables {
|
|
12
|
+
#tag;
|
|
13
|
+
#commands;
|
|
14
|
+
|
|
15
|
+
constructor(tag = 'table') {
|
|
16
|
+
if (typeof tag !== 'string' || !tag.trim()) throw new TypeError('A table command group requires a tag name');
|
|
17
|
+
this.#tag = tag.trim().toLowerCase();
|
|
18
|
+
this.#commands = Object.freeze({
|
|
19
|
+
insertTable: {
|
|
20
|
+
enabled: edit => !!this.#target(edit),
|
|
21
|
+
run: edit => this.#insert(edit),
|
|
22
|
+
},
|
|
23
|
+
rowBefore: this.#rows('before'),
|
|
24
|
+
rowAfter: this.#rows('after'),
|
|
25
|
+
rowDelete: this.#rows(null),
|
|
26
|
+
columnBefore: this.#columns('before'),
|
|
27
|
+
columnAfter: this.#columns('after'),
|
|
28
|
+
columnDelete: this.#columns(null),
|
|
29
|
+
deleteTable: {
|
|
30
|
+
enabled: edit => !!this.#at(edit),
|
|
31
|
+
run: edit => this.#remove(edit, this.#at(edit)?.table),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get tag() { return this.#tag; }
|
|
37
|
+
get commands() { return this.#commands; }
|
|
38
|
+
|
|
39
|
+
// The tags one level apart, taken from the model rather than assumed.
|
|
40
|
+
#shape(edit) {
|
|
41
|
+
const section = edit.model.defaultChild(this.#tag);
|
|
42
|
+
const row = section && edit.model.defaultChild(section);
|
|
43
|
+
const cell = row && edit.model.defaultChild(row);
|
|
44
|
+
return cell ? {table: this.#tag, section, row, cell} : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The cell the caret sits in, with everything around it.
|
|
48
|
+
#at(edit) {
|
|
49
|
+
const shape = this.#shape(edit);
|
|
50
|
+
if (!shape || !edit.range) return null;
|
|
51
|
+
for (let element = elementOf(edit.range.start.node); element && element !== edit.element;
|
|
52
|
+
element = element.parentElement) {
|
|
53
|
+
if (element.parentElement?.localName !== shape.row) continue;
|
|
54
|
+
const cell = element;
|
|
55
|
+
const row = cell.parentElement;
|
|
56
|
+
const table = row.closest(shape.table);
|
|
57
|
+
if (!table || !edit.element.contains(table)) return null;
|
|
58
|
+
return {shape, cell, row, table, column: [...row.children].indexOf(cell)};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Where a new table may go: the nearest container that accepts one. A
|
|
64
|
+
// selection is not deleted first; the table lands where it starts.
|
|
65
|
+
#target(edit) {
|
|
66
|
+
const shape = this.#shape(edit);
|
|
67
|
+
const start = edit.range?.start;
|
|
68
|
+
if (!shape || !start || this.#at(edit)) return null;
|
|
69
|
+
const table = edit.document.createElement(shape.table);
|
|
70
|
+
for (let element = elementOf(start.node); element; element = element.parentElement) {
|
|
71
|
+
if (edit.model.atomic(element)) return null;
|
|
72
|
+
if (edit.model.allows(element, table)) return element;
|
|
73
|
+
if (element === edit.element) return null;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#rows(where) {
|
|
79
|
+
return {
|
|
80
|
+
enabled: edit => {
|
|
81
|
+
const found = this.#at(edit);
|
|
82
|
+
return !!found && !spans(found.table, 'rowSpan') && (!!where || rows(found.table).length > 0);
|
|
83
|
+
},
|
|
84
|
+
run: edit => where ? this.#addRow(edit, where) : this.#dropRow(edit),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#columns(where) {
|
|
89
|
+
return {
|
|
90
|
+
enabled: edit => {
|
|
91
|
+
const found = this.#at(edit);
|
|
92
|
+
return !!found && !spans(found.table, 'rowSpan') && !spans(found.table, 'colSpan');
|
|
93
|
+
},
|
|
94
|
+
run: edit => where ? this.#addColumn(edit, where) : this.#dropColumn(edit),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#insert(edit, {rows: height = 2, columns: width = 2} = edit.value || {}) {
|
|
99
|
+
const parent = this.#target(edit);
|
|
100
|
+
const shape = this.#shape(edit);
|
|
101
|
+
if (!parent) return null;
|
|
102
|
+
const table = build(edit, shape, Math.max(1, height), Math.max(1, width));
|
|
103
|
+
const start = edit.range.start;
|
|
104
|
+
const at = edit.map.split(parent, start.node, start.offset);
|
|
105
|
+
edit.map.insert(parent, at, table);
|
|
106
|
+
edit.transaction.touch(parent);
|
|
107
|
+
edit.select(Point.start(table.querySelector(shape.cell)));
|
|
108
|
+
return table;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#addRow(edit, where) {
|
|
112
|
+
const found = this.#at(edit);
|
|
113
|
+
if (!found) return null;
|
|
114
|
+
const row = edit.document.createElement(found.shape.row);
|
|
115
|
+
for (const cell of found.row.children) row.append(edit.document.createElement(cell.localName));
|
|
116
|
+
const section = found.row.parentElement;
|
|
117
|
+
edit.map.insert(section, [...section.childNodes].indexOf(found.row) + (where === 'after' ? 1 : 0), row);
|
|
118
|
+
for (const cell of row.children) fill(edit, cell);
|
|
119
|
+
edit.transaction.touch(section);
|
|
120
|
+
edit.select(Point.start(row.firstElementChild));
|
|
121
|
+
return row;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#dropRow(edit) {
|
|
125
|
+
const found = this.#at(edit);
|
|
126
|
+
if (!found) return null;
|
|
127
|
+
const remaining = rows(found.table).filter(row => row !== found.row);
|
|
128
|
+
if (!remaining.length) return this.#remove(edit, found.table);
|
|
129
|
+
const next = neighbour(rows(found.table), found.row);
|
|
130
|
+
const section = found.row.parentElement;
|
|
131
|
+
edit.map.remove(found.row);
|
|
132
|
+
if (!section.children.length && section !== found.table) edit.map.remove(section);
|
|
133
|
+
edit.transaction.touch(found.table);
|
|
134
|
+
edit.select(Point.start(next.children[Math.min(found.column, next.children.length - 1)]));
|
|
135
|
+
return found.row;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#addColumn(edit, where) {
|
|
139
|
+
const found = this.#at(edit);
|
|
140
|
+
if (!found) return null;
|
|
141
|
+
let landed = null;
|
|
142
|
+
for (const row of rows(found.table)) {
|
|
143
|
+
const at = Math.min(found.column + (where === 'after' ? 1 : 0), row.children.length);
|
|
144
|
+
const reference = row.children[at] || null;
|
|
145
|
+
const model = row.children[Math.min(found.column, row.children.length - 1)];
|
|
146
|
+
const cell = edit.document.createElement(model?.localName || found.shape.cell);
|
|
147
|
+
edit.map.insert(row, reference ? [...row.childNodes].indexOf(reference) : row.childNodes.length, cell);
|
|
148
|
+
fill(edit, cell);
|
|
149
|
+
if (row === found.row) landed = cell;
|
|
150
|
+
}
|
|
151
|
+
edit.transaction.touch(found.table);
|
|
152
|
+
if (landed) edit.select(Point.start(landed));
|
|
153
|
+
return landed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#dropColumn(edit) {
|
|
157
|
+
const found = this.#at(edit);
|
|
158
|
+
if (!found) return null;
|
|
159
|
+
if (found.row.children.length <= 1) return this.#remove(edit, found.table);
|
|
160
|
+
for (const row of rows(found.table)) {
|
|
161
|
+
const cell = row.children[found.column];
|
|
162
|
+
if (cell) edit.map.remove(cell);
|
|
163
|
+
}
|
|
164
|
+
edit.transaction.touch(found.table);
|
|
165
|
+
const cells = [...found.row.children];
|
|
166
|
+
edit.select(Point.start(cells[Math.min(found.column, cells.length - 1)]));
|
|
167
|
+
return found.table;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#remove(edit, table) {
|
|
171
|
+
if (!table) return null;
|
|
172
|
+
const parent = table.parentElement;
|
|
173
|
+
const at = [...parent.childNodes].indexOf(table);
|
|
174
|
+
edit.map.remove(table);
|
|
175
|
+
edit.transaction.touch(parent);
|
|
176
|
+
edit.select(new Point(parent, Math.min(at, parent.childNodes.length), 'forward'));
|
|
177
|
+
return table;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function build(edit, shape, height, width) {
|
|
182
|
+
const table = edit.document.createElement(shape.table);
|
|
183
|
+
const section = shape.section ? table.appendChild(edit.document.createElement(shape.section)) : table;
|
|
184
|
+
for (let y = 0; y < height; y++) {
|
|
185
|
+
const row = section.appendChild(edit.document.createElement(shape.row));
|
|
186
|
+
for (let x = 0; x < width; x++) {
|
|
187
|
+
const cell = row.appendChild(edit.document.createElement(shape.cell));
|
|
188
|
+
cell.append(edit.document.createElement('br'));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return table;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function rows(table) {
|
|
195
|
+
return [...table.rows || []];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Spanning cells make an index no longer name a column, so the commands that
|
|
199
|
+
// count on one stay unavailable rather than shifting the wrong cells.
|
|
200
|
+
function spans(table, property) {
|
|
201
|
+
return rows(table).some(row => [...row.children].some(cell => (cell[property] || 1) > 1));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function neighbour(list, item) {
|
|
205
|
+
const at = list.indexOf(item);
|
|
206
|
+
return list[at + 1] || list[at - 1];
|
|
207
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {Edit} from './edit.js';
|
|
2
|
+
import {EditRange} from '../selection/range/edit-range.js';
|
|
3
|
+
import {elementOf, isEditingBoundary} from '../selection/ownership/ownership.js';
|
|
4
|
+
import {Point} from '../selection/point/point.js';
|
|
5
|
+
import {Unstyle, declared, defaultUnstyle, keepFor, removable, strip as stripAttributes} from '../unstyle/unstyle.js';
|
|
6
|
+
import {indexOf} from '../selection/point/point.js';
|
|
7
|
+
|
|
8
|
+
// The structural rung is the ladder's end: presentation levels only ever strip
|
|
9
|
+
// attributes and unwrap inline elements, so without it a selection that is
|
|
10
|
+
// already plain inline content has nothing left to give. It needs the content
|
|
11
|
+
// model and mapped mutation, which is why it lives here rather than in the
|
|
12
|
+
// `Unstyle` policy that also runs on detached paste fragments.
|
|
13
|
+
const BLOCKS = Object.freeze({name: 'blocks'});
|
|
14
|
+
|
|
15
|
+
export function unstyleCommand(policy = defaultUnstyle, {blocks = true} = {}) {
|
|
16
|
+
if (!(policy instanceof Unstyle)) throw new TypeError('An unstyle command requires a policy');
|
|
17
|
+
const levels = blocks ? [...policy.levels, BLOCKS] : policy.levels;
|
|
18
|
+
return {
|
|
19
|
+
inputTypes: ['formatRemove'],
|
|
20
|
+
enabled: edit => !!next(edit, levels),
|
|
21
|
+
state: edit => next(edit, levels)?.name || null,
|
|
22
|
+
run: edit => run(edit, next(edit, levels)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function next(edit, levels) {
|
|
27
|
+
if (!edit.range) return null;
|
|
28
|
+
const scope = reach(edit);
|
|
29
|
+
if (!scope.range || scope.range.collapsed) return null;
|
|
30
|
+
for (const level of levels) {
|
|
31
|
+
const found = level === BLOCKS
|
|
32
|
+
? structural(scope).length
|
|
33
|
+
: targets(scope, level, scope.range, true).length;
|
|
34
|
+
if (found) return level;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function run(edit, level) {
|
|
40
|
+
if (!level) return;
|
|
41
|
+
// Someone who presses with nothing selected wants the content cleaned, not
|
|
42
|
+
// nothing to happen, so the caret is put back afterwards rather than the
|
|
43
|
+
// whole document being left selected.
|
|
44
|
+
const caret = edit.range?.collapsed
|
|
45
|
+
? new Point(edit.range.start.node, edit.range.start.offset, 'forward')
|
|
46
|
+
: null;
|
|
47
|
+
const scope = reach(edit);
|
|
48
|
+
if (caret) scope.map.add(caret);
|
|
49
|
+
const result = level === BLOCKS ? flatten(scope) : strip(scope, level);
|
|
50
|
+
if (caret) scope.select(scope.map.get(caret));
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A collapsed caret reaches everything the surface holds.
|
|
55
|
+
function reach(edit) {
|
|
56
|
+
if (!edit.range?.collapsed) return edit;
|
|
57
|
+
const range = edit.document.createRange();
|
|
58
|
+
range.selectNodeContents(edit.element);
|
|
59
|
+
return new Edit(edit.surface, edit.transaction, {model: edit.model, range, value: edit.value});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function strip(edit, level) {
|
|
63
|
+
const state = prepare(edit);
|
|
64
|
+
isolate(edit, level, state.end);
|
|
65
|
+
isolate(edit, level, state.start);
|
|
66
|
+
const range = mappedRange(edit, state);
|
|
67
|
+
const changed = targets(edit, level, range);
|
|
68
|
+
for (const element of changed.reverse()) clear(edit, level, element);
|
|
69
|
+
edit.select(edit.map.get(state.start), edit.map.get(state.end), state.backward);
|
|
70
|
+
return {level: level.name, changed};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reduces every selected structure to the host's default block. One pass is
|
|
74
|
+
// enough: each outermost block is replaced by one default block per unit of
|
|
75
|
+
// content it holds, so lists, tables, quotes, and headings all collapse the
|
|
76
|
+
// same way without naming a single tag.
|
|
77
|
+
function flatten(edit) {
|
|
78
|
+
const state = prepare(edit);
|
|
79
|
+
const changed = [];
|
|
80
|
+
for (const block of structural(edit).reverse()) changed.push(...reduce(edit, block));
|
|
81
|
+
edit.select(edit.map.get(state.start), edit.map.get(state.end), state.backward);
|
|
82
|
+
return {level: BLOCKS.name, changed};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function reduce(edit, block) {
|
|
86
|
+
const parent = block.parentElement;
|
|
87
|
+
const tag = edit.config.block;
|
|
88
|
+
let at = indexOf(block);
|
|
89
|
+
const created = [];
|
|
90
|
+
const emit = nodes => {
|
|
91
|
+
const target = edit.document.createElement(tag);
|
|
92
|
+
edit.map.insert(parent, at++, target);
|
|
93
|
+
for (const node of nodes) edit.map.move(node, target, target.childNodes.length);
|
|
94
|
+
created.push(target);
|
|
95
|
+
};
|
|
96
|
+
// Content of its own and each nested structure become separate blocks, so
|
|
97
|
+
// nothing is lost and nothing is merged that was not adjacent before.
|
|
98
|
+
const visit = element => {
|
|
99
|
+
let own = [];
|
|
100
|
+
for (const node of [...element.childNodes]) {
|
|
101
|
+
if (structure(edit, node)) {
|
|
102
|
+
if (own.length) emit(own);
|
|
103
|
+
own = [];
|
|
104
|
+
visit(node);
|
|
105
|
+
} else {
|
|
106
|
+
own.push(node);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (own.length) emit(own);
|
|
110
|
+
};
|
|
111
|
+
visit(block);
|
|
112
|
+
edit.map.remove(block);
|
|
113
|
+
edit.transaction.touch(parent);
|
|
114
|
+
return created;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The outermost blocks in the selection that still hold structure to remove.
|
|
118
|
+
function structural(edit) {
|
|
119
|
+
const tag = edit.config.block;
|
|
120
|
+
if (!tag || !edit.range || edit.range.collapsed) return [];
|
|
121
|
+
const found = new Set();
|
|
122
|
+
for (const block of edit.range.blocks(element => element !== edit.element && structure(edit, element))) {
|
|
123
|
+
let outermost = null;
|
|
124
|
+
for (let element = block; element && element !== edit.element; element = element.parentElement) {
|
|
125
|
+
if (structure(edit, element)) outermost = element;
|
|
126
|
+
}
|
|
127
|
+
if (outermost && reducible(edit, outermost, tag)) found.add(outermost);
|
|
128
|
+
}
|
|
129
|
+
return [...found];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// An atomic block is content, not structure: a rule or an image stays.
|
|
133
|
+
function structure(edit, node) {
|
|
134
|
+
return node.nodeType === Node.ELEMENT_NODE
|
|
135
|
+
&& !isEditingBoundary(node)
|
|
136
|
+
&& edit.model.block(node)
|
|
137
|
+
&& !edit.model.atomic(node);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function reducible(edit, block, tag) {
|
|
141
|
+
if (!edit.model.allows(block.parentElement, edit.document.createElement(tag))) return false;
|
|
142
|
+
return block.localName !== tag || [...block.children].some(child => structure(edit, child));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function targets(edit, level, range, preview = false) {
|
|
146
|
+
const result = [];
|
|
147
|
+
const visit = parent => {
|
|
148
|
+
for (const element of parent.children) {
|
|
149
|
+
if (isEditingBoundary(element) || !range.intersects(element)) continue;
|
|
150
|
+
if (!edit.model.atomic(element)) visit(element);
|
|
151
|
+
if (!applicable(edit, level, element)) continue;
|
|
152
|
+
if (covered(range, element) || preview && inline(edit, element)) result.push(element);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
visit(edit.element);
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function clear(edit, level, element) {
|
|
160
|
+
const parent = element.parentNode;
|
|
161
|
+
stripAttributes(level, element, kept(edit, level));
|
|
162
|
+
const unwrap = level.elements.includes(element.localName)
|
|
163
|
+
|| element.localName === 'span' && !element.attributes.length;
|
|
164
|
+
if (unwrap && inline(edit, element)) edit.map.unwrap(element);
|
|
165
|
+
edit.transaction.touch(unwrap ? parent : element).touch(parent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function prepare(edit) {
|
|
169
|
+
const start = new Point(edit.range.start.node, edit.range.start.offset, 'forward');
|
|
170
|
+
const end = new Point(edit.range.end.node, edit.range.end.offset, 'backward');
|
|
171
|
+
edit.map.add(start).add(end);
|
|
172
|
+
splitText(edit, end);
|
|
173
|
+
splitText(edit, start);
|
|
174
|
+
return {start, end, backward: !!edit.surface.selection?.backward};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function splitText(edit, point) {
|
|
178
|
+
const current = edit.map.get(point);
|
|
179
|
+
if (current.node.nodeType === Node.TEXT_NODE && current.offset > 0 && current.offset < current.node.length) {
|
|
180
|
+
edit.map.splitText(current.node, current.offset);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isolate(edit, level, point) {
|
|
185
|
+
const current = edit.map.get(point);
|
|
186
|
+
const wrapper = matchingWrapper(edit, level, current.node);
|
|
187
|
+
if (!wrapper || !inside(current, wrapper)) return;
|
|
188
|
+
edit.transaction.touch(wrapper.parentNode);
|
|
189
|
+
edit.map.split(wrapper.parentNode, current.node, current.offset);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function matchingWrapper(edit, level, node) {
|
|
193
|
+
let found = null;
|
|
194
|
+
for (let element = elementOf(node); element && element !== edit.element; element = element.parentElement) {
|
|
195
|
+
if (isEditingBoundary(element) || !inline(edit, element)) break;
|
|
196
|
+
if (applicable(edit, level, element)) found = element;
|
|
197
|
+
}
|
|
198
|
+
return found;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function applicable(edit, level, element) {
|
|
202
|
+
const keep = kept(edit, level);
|
|
203
|
+
return level.attributes.some(name => removable(name, element, keep))
|
|
204
|
+
|| level.elements.includes(element.localName) && !declared(element, keep) && inline(edit, element);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The host's content classes survive the foreign-presentation rungs, exactly as
|
|
208
|
+
// they survive paste cleanup; the rungs below that scope take them too.
|
|
209
|
+
function kept(edit, level) {
|
|
210
|
+
const names = edit.config.classes;
|
|
211
|
+
return keepFor(level, names.length ? new Set(names) : null);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function covered(range, element) {
|
|
215
|
+
if (!element.childNodes.length) return range.contains(element);
|
|
216
|
+
return [...element.childNodes].every(node => {
|
|
217
|
+
if (node.nodeType === Node.TEXT_NODE) return !node.data || range.contains(node);
|
|
218
|
+
if (node.nodeType !== Node.ELEMENT_NODE || !node.childNodes.length) return range.contains(node);
|
|
219
|
+
return covered(range, node);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function inline(edit, element) {
|
|
224
|
+
return element !== edit.element && !isEditingBoundary(element)
|
|
225
|
+
&& !edit.model.block(element) && !edit.model.atomic(element);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function mappedRange(edit, state) {
|
|
229
|
+
return EditRange.fromPoints(edit.map.get(state.start), edit.map.get(state.end), edit.element);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function inside(point, element) {
|
|
233
|
+
return point.within(element) && !atEdge(point, element, 'start') && !atEdge(point, element, 'end');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function atEdge(point, element, edge) {
|
|
237
|
+
let node = point.node;
|
|
238
|
+
let offset = point.offset;
|
|
239
|
+
while (node !== element) {
|
|
240
|
+
const length = typeof node.length === 'number' ? node.length : node.childNodes.length;
|
|
241
|
+
if (offset !== (edge === 'start' ? 0 : length)) return false;
|
|
242
|
+
const parent = node.parentNode;
|
|
243
|
+
offset = Array.prototype.indexOf.call(parent.childNodes, node) + (edge === 'end' ? 1 : 0);
|
|
244
|
+
node = parent;
|
|
245
|
+
}
|
|
246
|
+
return offset === (edge === 'start' ? 0 : element.childNodes.length);
|
|
247
|
+
}
|
|
248
|
+
|