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.
- 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 +487 -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 +553 -0
- package/js/rte/playground/index.html +835 -0
- package/js/rte/rte.js +56 -5
- package/js/rte/run.out +17 -0
- package/js/rte/source.js +12 -0
- package/js/rte/src/browser/README.md +40 -0
- package/js/rte/src/browser/interactive.js +17 -0
- package/js/rte/src/browser/range-rect.js +82 -0
- package/js/rte/src/client/README.md +385 -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 +472 -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 +285 -0
- package/js/rte/src/client/link.js +567 -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 +165 -0
- package/js/rte/src/client/unstyle.js +32 -0
- package/js/rte/src/command/README.md +520 -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 +159 -0
- package/js/rte/src/command/delete.js +119 -0
- package/js/rte/src/command/edit.js +78 -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 +134 -0
- package/js/rte/src/core/README.md +63 -0
- package/js/rte/src/core/core.js +247 -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 +173 -0
- package/js/rte/src/input/external-input.js +130 -0
- package/js/rte/src/input/input-pipeline.js +459 -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 +108 -0
- package/js/rte/src/sanitize/native.js +43 -0
- package/js/rte/src/sanitize/policy.js +206 -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 +271 -0
- package/js/rte/src/ui/caret.js +17 -0
- package/js/rte/src/ui/chrome.js +214 -0
- package/js/rte/src/ui/handles.js +179 -0
- package/js/rte/src/ui/highlight.js +86 -0
- package/js/rte/src/ui/place.js +25 -0
- package/js/rte/src/ui/toolbar.js +258 -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,285 @@
|
|
|
1
|
+
import {elementAttributes, selectedElement} from '../command/element.js';
|
|
2
|
+
import {inlineUi} from '../config/config.js';
|
|
3
|
+
import {caretAfter} from '../ui/caret.js';
|
|
4
|
+
import {Handles} from '../ui/handles.js';
|
|
5
|
+
import {place} from '../ui/place.js';
|
|
6
|
+
|
|
7
|
+
const STYLE = `
|
|
8
|
+
#image {
|
|
9
|
+
align-items: center;
|
|
10
|
+
display: flex;
|
|
11
|
+
gap: .57em;
|
|
12
|
+
padding: .34em;
|
|
13
|
+
|
|
14
|
+
input {
|
|
15
|
+
background: transparent;
|
|
16
|
+
border: 1px solid color-mix(in srgb, CanvasText 24%, transparent);
|
|
17
|
+
border-radius: .34em;
|
|
18
|
+
min-inline-size: 16em;
|
|
19
|
+
padding: .17em .34em;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// Only the trailing edges: an image sits in a text flow with its top and its
|
|
26
|
+
// start edge held in place, so dragging the other side is the only direction
|
|
27
|
+
// that grows it where the eye expects. The corner keeps the proportion; the two
|
|
28
|
+
// edges change one measurement alone.
|
|
29
|
+
const HANDLES = Object.freeze([
|
|
30
|
+
Object.freeze({name: 'se', label: 'Resize', x: 1, y: 1, axis: 'both', cursor: 'nwse-resize'}),
|
|
31
|
+
Object.freeze({name: 'e', label: 'Width', x: 1, y: 0.5, axis: 'x', cursor: 'ew-resize'}),
|
|
32
|
+
Object.freeze({name: 's', label: 'Height', x: 0.5, y: 1, axis: 'y', cursor: 'ns-resize'}),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Optional image module. A selected image gets a frame with corner handles; the
|
|
36
|
+
// drag resizes the frame only and the size is written once when it is released,
|
|
37
|
+
// so a resize is one undo step rather than a trail of them.
|
|
38
|
+
//
|
|
39
|
+
// It also gets its alt text, in a field below it: what an image says is part of
|
|
40
|
+
// the image, and a form nobody opens is a form everybody fills in.
|
|
41
|
+
//
|
|
42
|
+
// Size is expressed as `width` and `height` attributes because that is what the
|
|
43
|
+
// sanitize policy allows on an image. What is resizable is a selector, so an
|
|
44
|
+
// application can make any atomic element sizeable.
|
|
45
|
+
export function imageTools({selector = 'img', minimum = 16} = {}) {
|
|
46
|
+
const match = element => element.matches(selector);
|
|
47
|
+
const editors = new WeakMap();
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
name: 'images',
|
|
50
|
+
setup({editor, root, chrome}) {
|
|
51
|
+
const state = {
|
|
52
|
+
root,
|
|
53
|
+
chrome,
|
|
54
|
+
document: root.nodeType === Node.DOCUMENT_NODE ? root : root.ownerDocument,
|
|
55
|
+
views: new WeakMap(),
|
|
56
|
+
active: null,
|
|
57
|
+
drag: null,
|
|
58
|
+
minimum,
|
|
59
|
+
handles: null,
|
|
60
|
+
alt: null,
|
|
61
|
+
writing: false,
|
|
62
|
+
controller: null,
|
|
63
|
+
pending: 0,
|
|
64
|
+
};
|
|
65
|
+
editors.set(editor, state);
|
|
66
|
+
let connected = true;
|
|
67
|
+
return {dispose() {
|
|
68
|
+
if (!connected) return;
|
|
69
|
+
release(state);
|
|
70
|
+
editors.delete(editor);
|
|
71
|
+
connected = false;
|
|
72
|
+
}};
|
|
73
|
+
},
|
|
74
|
+
commands({editor, surface, commands}) {
|
|
75
|
+
const state = editors.get(editor);
|
|
76
|
+
if (!state) throw new DOMException('The image extension is not set up', 'InvalidStateError');
|
|
77
|
+
state.views.set(surface, {surface, commands});
|
|
78
|
+
const size = elementAttributes(['width', 'height'], {match});
|
|
79
|
+
return {
|
|
80
|
+
imageSize: size,
|
|
81
|
+
imageAlt: elementAttributes(['alt'], {match}),
|
|
82
|
+
// No value clears both attributes, so the image is its own size.
|
|
83
|
+
imageOriginal: {
|
|
84
|
+
enabled: edit => {
|
|
85
|
+
const element = selectedElement(edit, match);
|
|
86
|
+
return !!element && (element.hasAttribute('width') || element.hasAttribute('height'));
|
|
87
|
+
},
|
|
88
|
+
run: edit => size.run(edit),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
attach({editor, surface}) {
|
|
93
|
+
const state = editors.get(editor);
|
|
94
|
+
const view = state?.views.get(surface);
|
|
95
|
+
if (!view) throw new DOMException('The image surface is not set up', 'InvalidStateError');
|
|
96
|
+
const controller = new state.document.defaultView.AbortController();
|
|
97
|
+
const listen = {signal: controller.signal};
|
|
98
|
+
const sync = () => track(state, view, match);
|
|
99
|
+
surface.addEventListener('u2-rte-selectionchange', sync, listen);
|
|
100
|
+
surface.addEventListener('u2-rte-change', sync, listen);
|
|
101
|
+
// What a session's end takes away, its beginning brings back: coming
|
|
102
|
+
// back to the same selection is no selection change to hear about.
|
|
103
|
+
surface.addEventListener('u2-rte-activate', sync, listen);
|
|
104
|
+
surface.addEventListener('u2-rte-deactivate', () => close(state, surface), listen);
|
|
105
|
+
return {dispose() {
|
|
106
|
+
controller.abort();
|
|
107
|
+
close(state, surface);
|
|
108
|
+
state.views.delete(surface);
|
|
109
|
+
}};
|
|
110
|
+
},
|
|
111
|
+
toolbar: Object.freeze([Object.freeze({
|
|
112
|
+
command: 'imageOriginal', name: 'imageOriginal', label: 'Original size', text: '⤢',
|
|
113
|
+
})]),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const images = imageTools();
|
|
118
|
+
|
|
119
|
+
function track(state, view, match) {
|
|
120
|
+
// Not while the field is naming the image: what it says is being typed, and
|
|
121
|
+
// the attribute is only what has been typed so far.
|
|
122
|
+
if (state.writing) return;
|
|
123
|
+
if (!inlineUi(view.surface.config, 'image')) return close(state, view.surface);
|
|
124
|
+
const element = current(view, match);
|
|
125
|
+
if (!element) return close(state, view.surface);
|
|
126
|
+
build(state);
|
|
127
|
+
state.active = {view, element};
|
|
128
|
+
state.handles.show();
|
|
129
|
+
state.alt.hidden = false;
|
|
130
|
+
state.alt.firstElementChild.value = element.getAttribute('alt') || '';
|
|
131
|
+
schedule(state);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function current(view, match) {
|
|
135
|
+
const range = view.surface.selection?.range();
|
|
136
|
+
if (!range || range.collapsed) return null;
|
|
137
|
+
const contents = range.cloneContents();
|
|
138
|
+
if (contents.childNodes.length !== 1) return null;
|
|
139
|
+
const element = range.startContainer.childNodes[range.startOffset];
|
|
140
|
+
return element?.nodeType === Node.ELEMENT_NODE && match(element)
|
|
141
|
+
&& view.surface.element.contains(element) ? element : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function close(state, surface) {
|
|
145
|
+
if (surface && state.active && state.active.view.surface !== surface) return;
|
|
146
|
+
state.active = null;
|
|
147
|
+
state.drag = null;
|
|
148
|
+
state.handles?.show(false);
|
|
149
|
+
if (state.alt) state.alt.hidden = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Naming the image moves the selection back onto it, and an engine follows that
|
|
153
|
+
// with focus — so the field takes it back, caret and all, exactly as the link
|
|
154
|
+
// form has to.
|
|
155
|
+
function name(state, field) {
|
|
156
|
+
const active = state.active;
|
|
157
|
+
if (!active) return null;
|
|
158
|
+
const caret = [field.selectionStart, field.selectionEnd];
|
|
159
|
+
state.writing = true;
|
|
160
|
+
try {
|
|
161
|
+
active.view.commands.run('imageAlt', {value: {alt: field.value.trim()}, trigger: 'input'});
|
|
162
|
+
} finally {
|
|
163
|
+
state.writing = false;
|
|
164
|
+
}
|
|
165
|
+
field.focus();
|
|
166
|
+
field.setSelectionRange(...caret);
|
|
167
|
+
return active.element;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function leave(state) {
|
|
171
|
+
const active = state.active;
|
|
172
|
+
if (!active) return;
|
|
173
|
+
const {element} = active;
|
|
174
|
+
const surface = active.view.surface;
|
|
175
|
+
close(state);
|
|
176
|
+
if (surface.connected) caretAfter(surface, element);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function schedule(state) {
|
|
180
|
+
if (state.pending || !state.handles) return;
|
|
181
|
+
state.pending = state.document.defaultView.requestAnimationFrame(() => {
|
|
182
|
+
state.pending = 0;
|
|
183
|
+
position(state);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function position(state, box = null) {
|
|
188
|
+
const element = state.active?.element;
|
|
189
|
+
if (!element?.isConnected) return close(state);
|
|
190
|
+
const rect = box || element.getBoundingClientRect();
|
|
191
|
+
state.handles.frame(rect);
|
|
192
|
+
for (const handle of HANDLES) {
|
|
193
|
+
state.handles.place(handle.name, rect.left + rect.width * handle.x, rect.top + rect.height * handle.y);
|
|
194
|
+
}
|
|
195
|
+
// Below the image, clear of the handles sitting on its lower edge.
|
|
196
|
+
place(state.alt, state.active.view.surface, {align: 'start', prefer: 'below', gap: 16, on: rect});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function build(state) {
|
|
200
|
+
if (state.handles) return state.handles;
|
|
201
|
+
const form = state.chrome.part('image', STYLE, 'form');
|
|
202
|
+
form.className = 'panel';
|
|
203
|
+
form.hidden = true;
|
|
204
|
+
form.setAttribute('aria-label', 'Image');
|
|
205
|
+
const field = state.document.createElement('input');
|
|
206
|
+
field.type = 'text';
|
|
207
|
+
field.name = 'alt';
|
|
208
|
+
field.placeholder = 'Alt text';
|
|
209
|
+
field.setAttribute('aria-label', 'Alt text');
|
|
210
|
+
form.append(field);
|
|
211
|
+
// One field, so Enter submits implicitly — and with the name already
|
|
212
|
+
// written, there is nothing left to confirm: it just leaves.
|
|
213
|
+
form.addEventListener('submit', event => {
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
leave(state);
|
|
216
|
+
});
|
|
217
|
+
form.addEventListener('input', () => name(state, field));
|
|
218
|
+
state.alt = form;
|
|
219
|
+
state.handles = new Handles(state.chrome.root, {
|
|
220
|
+
name: 'images',
|
|
221
|
+
handles: HANDLES,
|
|
222
|
+
part: 'corner',
|
|
223
|
+
press: name => start(state, name),
|
|
224
|
+
});
|
|
225
|
+
const controller = new state.document.defaultView.AbortController();
|
|
226
|
+
const listen = {signal: controller.signal};
|
|
227
|
+
const view = state.document.defaultView;
|
|
228
|
+
view.addEventListener('pointermove', event => drag(state, event), listen);
|
|
229
|
+
view.addEventListener('pointerup', event => drop(state, event), listen);
|
|
230
|
+
view.addEventListener('resize', () => state.active && schedule(state), listen);
|
|
231
|
+
state.root.addEventListener('scroll', () => state.active && schedule(state),
|
|
232
|
+
{capture: true, passive: true, signal: controller.signal});
|
|
233
|
+
state.controller = controller;
|
|
234
|
+
return state.handles;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function start(state, name) {
|
|
238
|
+
const handle = HANDLES.find(item => item.name === name);
|
|
239
|
+
if (!handle || !state.active) return;
|
|
240
|
+
const rect = state.active.element.getBoundingClientRect();
|
|
241
|
+
// The top and start edges stay where the flow put them.
|
|
242
|
+
state.drag = {rect, axis: handle.axis, ratio: rect.height / rect.width || 1};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function drag(state, event) {
|
|
246
|
+
if (!state.drag) return;
|
|
247
|
+
position(state, measure(state.drag, state.minimum, event));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function drop(state, event) {
|
|
251
|
+
const drag = state.drag;
|
|
252
|
+
state.drag = null;
|
|
253
|
+
if (!drag || !state.active) return;
|
|
254
|
+
const size = measure(drag, state.minimum, event);
|
|
255
|
+
const {view, element} = state.active;
|
|
256
|
+
if (!view.surface.connected) return;
|
|
257
|
+
if (!view.surface.capture()) view.surface.restore();
|
|
258
|
+
view.commands.run('imageSize', {
|
|
259
|
+
value: {width: Math.round(size.width), height: Math.round(size.height)},
|
|
260
|
+
});
|
|
261
|
+
if (element.isConnected) schedule(state);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The corner keeps the picture's proportion; an edge changes one measurement
|
|
265
|
+
// alone, which is a different intention and says so by where it sits.
|
|
266
|
+
function measure({rect, axis, ratio}, minimum, event) {
|
|
267
|
+
const dragged = {
|
|
268
|
+
width: Math.max(minimum, event.clientX - rect.left),
|
|
269
|
+
height: Math.max(minimum, event.clientY - rect.top),
|
|
270
|
+
};
|
|
271
|
+
const width = axis === 'y' ? rect.width : dragged.width;
|
|
272
|
+
const height = axis === 'x' ? rect.height : axis === 'y' ? dragged.height : width * ratio;
|
|
273
|
+
return {width, height, left: rect.left, top: rect.top};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function release(state) {
|
|
277
|
+
state.controller?.abort();
|
|
278
|
+
if (state.pending) state.document.defaultView.cancelAnimationFrame(state.pending);
|
|
279
|
+
state.handles?.dispose();
|
|
280
|
+
state.active = null;
|
|
281
|
+
state.drag = null;
|
|
282
|
+
state.pending = 0;
|
|
283
|
+
state.handles = null;
|
|
284
|
+
state.controller = null;
|
|
285
|
+
}
|