@simai-studio/ui 5.20261023.214 → 5.20261025.207
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/README.md +4 -4
- package/distr/component/button-group/css/button-group.min.css +1 -1
- package/distr/core/contracts/public-variables.v1.json +88 -0
- package/distr/core/contracts/third-party-notices.v1.json +1 -1
- package/distr/core/css/core.css +3 -2
- package/distr/core/js/composition/overlay.mjs +472 -24
- package/distr/core/js/composition/sortable-inserter.mjs +534 -0
- package/distr/core/js/core-loader.js +1 -1
- package/distr/core/js/core-loader.min.js +1 -1
- package/distr/core/js/core-rules.js +242 -185
- package/distr/core/js/core-rules.min.js +1 -1
- package/distr/core/js/open-surface-mark.js +88 -0
- package/distr/core/js/template-utilities.generated.json +3 -0
- package/distr/rule/js/rule.min.js +1 -1
- package/distr/rule/rule.json +47 -1
- package/distr/smart/color/css/color.min.css +1 -0
- package/distr/smart/color/js/color.min.js +36 -17
- package/distr/smart/color/smart.manifest.json +45 -4
- package/distr/smart/color/template/default.js +13 -2
- package/distr/smart/composition-overlay/css/composition-overlay.min.css +1 -1
- package/distr/smart/composition-overlay/js/composition-overlay.min.js +1 -1
- package/distr/smart/composition-overlay/smart.manifest.json +178 -11
- package/distr/smart/dropdown/css/dropdown.min.css +1 -0
- package/distr/smart/dropdown/js/dropdown.min.js +41 -21
- package/distr/smart/dropdown/smart.manifest.json +62 -3
- package/distr/smart/dropdown/template/default.js +5 -1
- package/distr/smart/field-renderers/js/field-renderers.min.js +54 -42
- package/distr/smart/image-field/css/image-field.min.css +1 -0
- package/distr/smart/image-field/js/image-field.min.js +62 -0
- package/distr/smart/image-field/smart.manifest.json +519 -0
- package/distr/smart/image-field/studio.fixture.json +15 -0
- package/distr/smart/image-field/studio.scenario.json +22 -0
- package/distr/smart/image-field/template/default.js +125 -0
- package/distr/smart/inputs/css/inputs.min.css +1 -0
- package/distr/smart/inputs/js/inputs.min.js +93 -75
- package/distr/smart/inputs/smart.manifest.json +62 -3
- package/distr/smart/inputs/template/default.js +8 -2
- package/distr/smart/sortable/css/sortable.min.css +1 -1
- package/distr/smart/sortable/js/sortable.min.js +1 -1
- package/distr/smart/sortable/smart.manifest.json +101 -14
- package/distr/smart/switch/css/switch.min.css +1 -0
- package/distr/smart/switch/js/switch.min.js +31 -13
- package/distr/smart/switch/smart.manifest.json +62 -3
- package/distr/smart/switch/template/default.js +2 -0
- package/distr/smart/textarea/css/textarea.min.css +1 -0
- package/distr/smart/textarea/js/textarea.min.js +51 -32
- package/distr/smart/textarea/smart.manifest.json +62 -3
- package/distr/smart/textarea/template/default.js +8 -1
- package/package-manifest.json +50 -39
- package/package.json +5 -4
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import { isPlainObject } from './canonical.mjs';
|
|
2
2
|
import { pageText } from '../page-language.js';
|
|
3
3
|
import { activeSortableSession, registerSortableDropTarget } from './sortable.mjs';
|
|
4
|
+
import { holdOpenSurfaceMark, isTopOpenSurface } from '../open-surface-mark.js';
|
|
4
5
|
|
|
5
6
|
// Editor canvas overlay. It draws hover and selection frames, a floating
|
|
6
7
|
// action toolbar and insertion points over a rendered Composition Document
|
|
7
8
|
// without touching the canvas DOM: the published HTML stays byte-identical.
|
|
8
9
|
// The host supplies node descriptions and actions and performs every change.
|
|
10
|
+
//
|
|
11
|
+
// Framed mode (attachFrame + setNodes) draws the same chrome over a same-origin
|
|
12
|
+
// iframe fed a flat node list: frame coordinates are mapped to the host page
|
|
13
|
+
// through the iframe's position, borders, padding, scale and the frame's own
|
|
14
|
+
// scroll, and a plain-string inline edit happens on the element inside the frame.
|
|
9
15
|
|
|
10
16
|
export const OVERLAY_EVENTS = Object.freeze({
|
|
11
17
|
select: 'sf-composition-overlay-select',
|
|
12
18
|
action: 'sf-composition-overlay-action',
|
|
13
19
|
insert: 'sf-composition-overlay-insert',
|
|
14
20
|
move: 'sf-composition-overlay-move',
|
|
21
|
+
edit: 'sf-composition-overlay-edit',
|
|
15
22
|
});
|
|
16
23
|
|
|
17
24
|
const MESSAGES = {
|
|
18
|
-
ru: { picked: 'Перемещение: {label}. Стрелки выбирают место, Enter — положить, Escape — отменить.', cancelled: 'Перемещение отменено.', tree: 'Структура страницы', toolbar: 'Действия', insert: 'Вставить', insertAt: 'Вставить в «{slot}», позиция {index}', position: '{index} из {total}', selected: 'Выбрано: {label}' },
|
|
19
|
-
en: { picked: 'Moving {label}. Arrows choose a place, Enter drops, Escape cancels.', cancelled: 'Move cancelled.', tree: 'Page structure', toolbar: 'Actions', insert: 'Insert', insertAt: 'Insert into “{slot}”, position {index}', position: '{index} of {total}', selected: 'Selected: {label}' },
|
|
25
|
+
ru: { picked: 'Перемещение: {label}. Стрелки выбирают место, Enter — положить, Escape — отменить.', cancelled: 'Перемещение отменено.', tree: 'Структура страницы', toolbar: 'Действия', insert: 'Вставить', insertAt: 'Вставить в «{slot}», позиция {index}', position: '{index} из {total}', selected: 'Выбрано: {label}', editing: 'Редактирование: {label}. Enter — сохранить, Escape — отменить.' },
|
|
26
|
+
en: { picked: 'Moving {label}. Arrows choose a place, Enter drops, Escape cancels.', cancelled: 'Move cancelled.', tree: 'Page structure', toolbar: 'Actions', insert: 'Insert', insertAt: 'Insert into “{slot}”, position {index}', position: '{index} of {total}', selected: 'Selected: {label}', editing: 'Editing {label}. Enter saves, Escape cancels.' },
|
|
20
27
|
};
|
|
21
28
|
|
|
22
29
|
const ACTION_ID = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
@@ -50,6 +57,114 @@ export function checkOverlayActions(actions) {
|
|
|
50
57
|
});
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
const NODE_KEYS = new Set(['id', 'kind', 'rect', 'element', 'label', 'parent', 'editable']);
|
|
61
|
+
const finite = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
62
|
+
// Elements of the frame belong to another realm, so `instanceof Element` is false for them.
|
|
63
|
+
const isElement = (value) => Boolean(value) && typeof value === 'object' && value.nodeType === 1 && typeof value.getBoundingClientRect === 'function';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks a framed canvas's flat node list: [{id, kind, rect? | element?, label?,
|
|
67
|
+
* parent?, editable?}]. rect is in frame-document coordinates {x, y, width,
|
|
68
|
+
* height}; element is a node inside the frame. Returns frozen entries.
|
|
69
|
+
*/
|
|
70
|
+
export function checkOverlayNodes(nodes) {
|
|
71
|
+
if (!Array.isArray(nodes)) throw new TypeError('Overlay nodes must be a list');
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const list = nodes.map((node) => {
|
|
74
|
+
if (!isPlainObject(node) || Object.keys(node).some((key) => !NODE_KEYS.has(key))) {
|
|
75
|
+
throw new TypeError('Overlay nodes have id, kind, rect or element, label, parent and editable only');
|
|
76
|
+
}
|
|
77
|
+
if (typeof node.id !== 'string' || !node.id || seen.has(node.id)) throw new TypeError('Overlay nodes have unique non-empty string ids');
|
|
78
|
+
if (typeof node.kind !== 'string' || !node.kind) throw new TypeError(`Overlay node ${node.id} names its kind`);
|
|
79
|
+
const { rect, element } = node;
|
|
80
|
+
if (rect !== undefined && (!isPlainObject(rect) || !['x', 'y', 'width', 'height'].every((key) => finite(rect[key])) || rect.width < 0 || rect.height < 0)) {
|
|
81
|
+
throw new TypeError(`Overlay node ${node.id} has a rect {x, y, width, height} of finite numbers`);
|
|
82
|
+
}
|
|
83
|
+
if (element !== undefined && !isElement(element)) throw new TypeError(`Overlay node ${node.id} has an element`);
|
|
84
|
+
if (rect === undefined && element === undefined) throw new TypeError(`Overlay node ${node.id} has a rect or an element`);
|
|
85
|
+
if (node.label !== undefined && typeof node.label !== 'string') throw new TypeError(`Overlay node ${node.id} has a string label`);
|
|
86
|
+
if (node.parent !== undefined && node.parent !== null && (typeof node.parent !== 'string' || node.parent === node.id)) {
|
|
87
|
+
throw new TypeError(`Overlay node ${node.id} names another node as its parent`);
|
|
88
|
+
}
|
|
89
|
+
if (node.editable !== undefined && typeof node.editable !== 'boolean') throw new TypeError(`Overlay node ${node.id} has a boolean editable`);
|
|
90
|
+
if (node.editable && element === undefined) throw new TypeError(`Overlay node ${node.id} is editable only with an element`);
|
|
91
|
+
seen.add(node.id);
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
id: node.id,
|
|
94
|
+
kind: node.kind,
|
|
95
|
+
rect: rect === undefined ? null : Object.freeze({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }),
|
|
96
|
+
element: element ?? null,
|
|
97
|
+
label: node.label ?? null,
|
|
98
|
+
parent: node.parent ?? null,
|
|
99
|
+
editable: node.editable === true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
const byId = new Map(list.map((node) => [node.id, node]));
|
|
103
|
+
for (const node of list) {
|
|
104
|
+
const visited = new Set([node.id]);
|
|
105
|
+
for (let parent = node.parent; parent !== null; parent = byId.get(parent).parent) {
|
|
106
|
+
if (!byId.has(parent)) throw new TypeError(`Overlay node ${node.id} names a parent that is not in the list`);
|
|
107
|
+
if (visited.has(parent)) throw new TypeError(`Overlay node ${node.id} is its own ancestor`);
|
|
108
|
+
visited.add(parent);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze(list);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Overlay entries for a checked flat list, in list order; the top level is parent null, slot "default". */
|
|
115
|
+
export function flatOverlayNodes(list) {
|
|
116
|
+
const entries = list.map((node) => ({ id: node.id, type: node.kind, kind: node.kind, label: node.label, parent: node.parent, slot: 'default', index: 0, level: 1, slots: {}, flat: node }));
|
|
117
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
118
|
+
const counts = new Map();
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const key = entry.parent ?? '';
|
|
121
|
+
entry.index = counts.get(key) || 0;
|
|
122
|
+
counts.set(key, entry.index + 1);
|
|
123
|
+
if (entry.parent) (byId.get(entry.parent).slots.default ||= []).push(entry.id);
|
|
124
|
+
for (let parent = entry.parent; parent; parent = byId.get(parent).parent) entry.level += 1;
|
|
125
|
+
}
|
|
126
|
+
return entries;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Where a frame's viewport lies on the host page. rect is the iframe's border box
|
|
131
|
+
* on the host (after any transform or zoom), layout its untransformed border box
|
|
132
|
+
* size (offsetWidth/offsetHeight), border and padding its own {top, right,
|
|
133
|
+
* bottom, left} in CSS px, inner the frame's viewport size in its own px and
|
|
134
|
+
* scroll the frame's scroll position. The scale is rect over layout; without a
|
|
135
|
+
* layout size it is taken from the parts, which the viewport's whole-pixel size
|
|
136
|
+
* makes less exact under zoom.
|
|
137
|
+
*/
|
|
138
|
+
export function frameViewport({ rect, layout = null, border, padding, inner, scroll = { x: 0, y: 0 } }) {
|
|
139
|
+
const outerWidth = layout?.width > 0 ? layout.width : border.left + padding.left + inner.width + padding.right + border.right;
|
|
140
|
+
const outerHeight = layout?.height > 0 ? layout.height : border.top + padding.top + inner.height + padding.bottom + border.bottom;
|
|
141
|
+
const scaleX = outerWidth > 0 && rect.width > 0 ? rect.width / outerWidth : 1;
|
|
142
|
+
const scaleY = outerHeight > 0 && rect.height > 0 ? rect.height / outerHeight : 1;
|
|
143
|
+
return {
|
|
144
|
+
left: rect.left + (border.left + padding.left) * scaleX,
|
|
145
|
+
top: rect.top + (border.top + padding.top) * scaleY,
|
|
146
|
+
width: inner.width * scaleX,
|
|
147
|
+
height: inner.height * scaleY,
|
|
148
|
+
scaleX,
|
|
149
|
+
scaleY,
|
|
150
|
+
scrollX: scroll.x,
|
|
151
|
+
scrollY: scroll.y,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A box in the frame's viewport coordinates {x, y, width, height}, mapped to the host page. */
|
|
156
|
+
export function frameRectToHost(box, view) {
|
|
157
|
+
const left = view.left + box.x * view.scaleX;
|
|
158
|
+
const top = view.top + box.y * view.scaleY;
|
|
159
|
+
const width = box.width * view.scaleX;
|
|
160
|
+
const height = box.height * view.scaleY;
|
|
161
|
+
return { x: left, y: top, left, top, width, height, right: left + width, bottom: top + height };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const plainText = (element) => (element.textContent || '').replace(/\s+/gu, ' ').trim();
|
|
165
|
+
const px = (value) => Number.parseFloat(value) || 0;
|
|
166
|
+
const intersect = (a, b) => ({ left: Math.max(a.left, b.left), top: Math.max(a.top, b.top), right: Math.min(a.right, b.right), bottom: Math.min(a.bottom, b.bottom) });
|
|
167
|
+
|
|
53
168
|
export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
54
169
|
if (!registry || typeof globalThis.HTMLElement !== 'function') return null;
|
|
55
170
|
const existing = registry.get('sf-composition-overlay');
|
|
@@ -74,6 +189,13 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
74
189
|
this.moving = null;
|
|
75
190
|
this.unregisterDrop = null;
|
|
76
191
|
this.dropHint = null;
|
|
192
|
+
this.iframe = null;
|
|
193
|
+
this.frameDoc = null;
|
|
194
|
+
this.frameConnection = null;
|
|
195
|
+
this.view = null;
|
|
196
|
+
this.elementIds = new Map();
|
|
197
|
+
this.rectNodes = [];
|
|
198
|
+
this.editing = null;
|
|
77
199
|
}
|
|
78
200
|
|
|
79
201
|
get messages() {
|
|
@@ -85,10 +207,14 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
85
207
|
connectedCallback() {
|
|
86
208
|
if (!this.layer) this.buildLayer();
|
|
87
209
|
const canvasId = this.getAttribute('for');
|
|
88
|
-
|
|
210
|
+
const frameId = this.getAttribute('frame');
|
|
211
|
+
if (frameId && !this.canvas && !this.iframe) {
|
|
212
|
+
const iframe = globalThis.document.getElementById(frameId);
|
|
213
|
+
if (iframe) this.attachFrame(iframe);
|
|
214
|
+
} else if (canvasId && !this.canvas && !this.iframe) {
|
|
89
215
|
const canvas = globalThis.document.getElementById(canvasId);
|
|
90
216
|
if (canvas) this.attach(canvas);
|
|
91
|
-
} else if (this.canvas) {
|
|
217
|
+
} else if (this.canvas || this.iframe) {
|
|
92
218
|
this.bind();
|
|
93
219
|
}
|
|
94
220
|
}
|
|
@@ -138,18 +264,40 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
138
264
|
if (!this.layer) this.buildLayer();
|
|
139
265
|
this.unbind();
|
|
140
266
|
this.canvas = canvas;
|
|
267
|
+
this.iframe = null;
|
|
268
|
+
this.removeAttribute('data-sf-overlay-mode');
|
|
269
|
+
this.layer.style.clipPath = '';
|
|
270
|
+
this.insertionLayer.style.clipPath = '';
|
|
271
|
+
if (this.isConnected) this.bind();
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Binds the overlay to a same-origin iframe canvas fed by setNodes; the frame's HTML is never modified by the overlay. */
|
|
276
|
+
attachFrame(iframe) {
|
|
277
|
+
if (!isElement(iframe) || iframe.localName !== 'iframe') throw new TypeError('Overlay frame must be an iframe element');
|
|
278
|
+
if (!this.layer) this.buildLayer();
|
|
279
|
+
this.unbind();
|
|
280
|
+
this.canvas = null;
|
|
281
|
+
this.iframe = iframe;
|
|
282
|
+
this.setAttribute('data-sf-overlay-mode', 'frame');
|
|
141
283
|
if (this.isConnected) this.bind();
|
|
142
284
|
return this;
|
|
143
285
|
}
|
|
144
286
|
|
|
145
287
|
bind() {
|
|
146
|
-
if (this.connection || !this.canvas) return;
|
|
288
|
+
if (this.connection || (!this.canvas && !this.iframe)) return;
|
|
147
289
|
this.connection = new AbortController();
|
|
148
290
|
const options = { signal: this.connection.signal };
|
|
149
291
|
const passive = { ...options, passive: true };
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
292
|
+
if (this.iframe) {
|
|
293
|
+
// A new letter (srcdoc, navigation) is a new document: listen to it again.
|
|
294
|
+
this.iframe.addEventListener('load', () => this.bindFrameDocument(), options);
|
|
295
|
+
this.iframe.addEventListener('pointerleave', () => this.setHover(null), passive);
|
|
296
|
+
} else {
|
|
297
|
+
this.canvas.addEventListener('pointermove', (event) => this.onPointerMove(event), passive);
|
|
298
|
+
this.canvas.addEventListener('pointerleave', () => this.setHover(null), passive);
|
|
299
|
+
this.canvas.addEventListener('click', (event) => this.onCanvasClick(event), { ...options, capture: true });
|
|
300
|
+
}
|
|
153
301
|
this.tree.addEventListener('keydown', (event) => this.onTreeKey(event), options);
|
|
154
302
|
this.tree.addEventListener('focus', () => {
|
|
155
303
|
this.setAttribute('data-sf-overlay-focus', '');
|
|
@@ -163,7 +311,7 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
163
311
|
globalThis.addEventListener('resize', () => this.schedule(), passive);
|
|
164
312
|
if (typeof globalThis.ResizeObserver === 'function') {
|
|
165
313
|
this.resizeObserver = new globalThis.ResizeObserver(() => this.schedule());
|
|
166
|
-
this.resizeObserver.observe(this.canvas);
|
|
314
|
+
this.resizeObserver.observe(this.canvas || this.iframe);
|
|
167
315
|
}
|
|
168
316
|
this.unregisterDrop = registerSortableDropTarget({
|
|
169
317
|
resolve: (x, y, dragSession) => (this.accepts(dragSession) ? this.resolveDrop(x, y, null) : null),
|
|
@@ -174,10 +322,56 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
174
322
|
this.selectedFrame.firstChild.addEventListener('pointerdown', (event) => this.onHandleDown(event), options);
|
|
175
323
|
this.setAttribute('data-sf-overlay', 'connected');
|
|
176
324
|
this.renderTree();
|
|
325
|
+
if (this.iframe) this.bindFrameDocument();
|
|
326
|
+
this.schedule();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Listens to the frame's current document and window; called on bind and on every frame load. */
|
|
330
|
+
bindFrameDocument() {
|
|
331
|
+
if (this.editing) this.finishEdit(false);
|
|
332
|
+
this.frameConnection?.abort();
|
|
333
|
+
this.frameConnection = null;
|
|
334
|
+
let doc = null;
|
|
335
|
+
try {
|
|
336
|
+
doc = this.iframe.contentDocument;
|
|
337
|
+
} catch {
|
|
338
|
+
doc = null;
|
|
339
|
+
}
|
|
340
|
+
const win = doc?.defaultView || null;
|
|
341
|
+
this.frameDoc = win ? doc : null;
|
|
342
|
+
if (this.resizeObserver) {
|
|
343
|
+
this.resizeObserver.disconnect();
|
|
344
|
+
this.resizeObserver.observe(this.iframe);
|
|
345
|
+
}
|
|
346
|
+
if (!this.frameDoc) {
|
|
347
|
+
// A cross-origin or not yet available frame: nothing is drawn over it.
|
|
348
|
+
this.setAttribute('data-sf-overlay-frame', 'unreachable');
|
|
349
|
+
this.schedule();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.removeAttribute('data-sf-overlay-frame');
|
|
353
|
+
this.frameConnection = new AbortController();
|
|
354
|
+
const options = { signal: this.frameConnection.signal };
|
|
355
|
+
const passive = { ...options, passive: true };
|
|
356
|
+
doc.addEventListener('pointermove', (event) => this.setHover(this.frameNodeAt(event)), passive);
|
|
357
|
+
doc.addEventListener('pointerout', (event) => { if (!event.relatedTarget) this.setHover(null); }, passive);
|
|
358
|
+
doc.addEventListener('click', (event) => this.onFrameClick(event), { ...options, capture: true });
|
|
359
|
+
doc.addEventListener('dblclick', (event) => this.onFrameDoubleClick(event), { ...options, capture: true });
|
|
360
|
+
doc.addEventListener('keydown', (event) => this.onFrameKey(event), { ...options, capture: true });
|
|
361
|
+
win.addEventListener('scroll', () => this.schedule(), { ...passive, capture: true });
|
|
362
|
+
win.addEventListener('resize', () => this.schedule(), passive);
|
|
363
|
+
if (this.resizeObserver) {
|
|
364
|
+
if (doc.documentElement) this.resizeObserver.observe(doc.documentElement);
|
|
365
|
+
if (doc.body) this.resizeObserver.observe(doc.body);
|
|
366
|
+
}
|
|
177
367
|
this.schedule();
|
|
178
368
|
}
|
|
179
369
|
|
|
180
370
|
unbind() {
|
|
371
|
+
this.finishEdit(false);
|
|
372
|
+
this.frameConnection?.abort();
|
|
373
|
+
this.frameConnection = null;
|
|
374
|
+
this.frameDoc = null;
|
|
181
375
|
this.pointerDrag?.abort();
|
|
182
376
|
this.pointerDrag = null;
|
|
183
377
|
this.unregisterDrop?.();
|
|
@@ -189,15 +383,40 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
189
383
|
this.resizeObserver = null;
|
|
190
384
|
if (this.frameRequest) globalThis.cancelAnimationFrame(this.frameRequest);
|
|
191
385
|
this.frameRequest = 0;
|
|
192
|
-
if (this.canvas) this.setAttribute('data-sf-overlay', 'disposed');
|
|
386
|
+
if (this.canvas || this.iframe) this.setAttribute('data-sf-overlay', 'disposed');
|
|
193
387
|
}
|
|
194
388
|
|
|
195
389
|
/** Supplies the Document structure and a describe(node) → {label, badges}. */
|
|
196
390
|
setDocument(document, { describe } = {}) {
|
|
391
|
+
this.elementIds = new Map();
|
|
392
|
+
this.rectNodes = [];
|
|
393
|
+
this.replaceNodes(overlayNodes(document), describe);
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Supplies a framed canvas's flat node list [{id, kind, rect? | element?,
|
|
399
|
+
* label?, parent?, editable?}] in selection order, and an optional
|
|
400
|
+
* describe(node) → {label, badges}; without it the label is node.label or its kind.
|
|
401
|
+
*/
|
|
402
|
+
setNodes(nodes, { describe } = {}) {
|
|
403
|
+
const list = checkOverlayNodes(nodes);
|
|
404
|
+
this.elementIds = new Map(list.filter((node) => node.element).map((node) => [node.element, node.id]));
|
|
405
|
+
const entries = flatOverlayNodes(list);
|
|
406
|
+
this.rectNodes = entries.filter((entry) => entry.flat.rect && !entry.flat.element);
|
|
407
|
+
if (this.editing) {
|
|
408
|
+
const next = list.find((node) => node.id === this.editing.id);
|
|
409
|
+
if (!next || next.element !== this.editing.element || !next.editable) this.finishEdit(false);
|
|
410
|
+
}
|
|
411
|
+
this.replaceNodes(entries, typeof describe === 'function' ? describe : (node) => ({ label: node.label || node.kind }));
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
replaceNodes(nodes, describe) {
|
|
197
416
|
const previous = this.selectedId ? this.byId.get(this.selectedId) : null;
|
|
198
417
|
const active = globalThis.document.activeElement;
|
|
199
418
|
const focused = Boolean(active) && (this.toolbar?.contains(active) || this.insertionLayer?.contains(active));
|
|
200
|
-
this.nodes =
|
|
419
|
+
this.nodes = nodes;
|
|
201
420
|
this.byId = new Map(this.nodes.map((node) => [node.id, node]));
|
|
202
421
|
if (typeof describe === 'function') this.describe = describe;
|
|
203
422
|
this.renderTree();
|
|
@@ -210,6 +429,11 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
210
429
|
this.select(focused ? fallback : null, { focus: focused });
|
|
211
430
|
}
|
|
212
431
|
this.schedule();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Re-measures on the next frame, after a change no observer reports (a transform on an ancestor of the canvas). */
|
|
435
|
+
refresh() {
|
|
436
|
+
this.schedule();
|
|
213
437
|
return this;
|
|
214
438
|
}
|
|
215
439
|
|
|
@@ -345,6 +569,7 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
345
569
|
|
|
346
570
|
// Moving a canvas node: pointer via the selected label, keyboard via the tree.
|
|
347
571
|
onHandleDown(event) {
|
|
572
|
+
if (this.iframe) return;
|
|
348
573
|
if (event.button !== 0 || !this.selectedId || !this.byId.get(this.selectedId)?.parent) return;
|
|
349
574
|
event.preventDefault();
|
|
350
575
|
const moving = this.selectedId;
|
|
@@ -434,6 +659,7 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
434
659
|
|
|
435
660
|
select(id, { focus = false, emit = true } = {}) {
|
|
436
661
|
if (id !== null && !this.byId.has(id)) return this;
|
|
662
|
+
if (this.editing && this.editing.id !== id) this.finishEdit(true);
|
|
437
663
|
if (this.selectedId === id) return this;
|
|
438
664
|
this.selectedId = id;
|
|
439
665
|
this.renderTreeSelection();
|
|
@@ -481,7 +707,7 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
481
707
|
|
|
482
708
|
onTreeKey(event) {
|
|
483
709
|
if (this.moving && this.onMoveKey(event)) return;
|
|
484
|
-
if (event.key === ' ' && this.selectedId) {
|
|
710
|
+
if (event.key === ' ' && this.selectedId && !this.iframe) {
|
|
485
711
|
event.preventDefault();
|
|
486
712
|
this.startKeyboardMove();
|
|
487
713
|
return;
|
|
@@ -495,10 +721,14 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
495
721
|
else if (event.key === 'End') next = this.nodes.at(-1);
|
|
496
722
|
else if (event.key === 'ArrowLeft' && current?.parent) next = this.byId.get(current.parent);
|
|
497
723
|
else if (event.key === 'ArrowRight' && current) next = this.nodes.find((node) => node.parent === current.id) || null;
|
|
498
|
-
else if (event.key === 'Escape' && this.selectedId !== null) {
|
|
724
|
+
else if (event.key === 'Escape' && this.selectedId !== null && (!this.iframe || isTopOpenSurface(this))) {
|
|
499
725
|
event.preventDefault();
|
|
500
726
|
this.select(null);
|
|
501
727
|
return;
|
|
728
|
+
} else if (event.key === 'Enter' && this.iframe && current?.flat?.editable) {
|
|
729
|
+
event.preventDefault();
|
|
730
|
+
this.edit(current.id);
|
|
731
|
+
return;
|
|
502
732
|
} else if (event.key === 'Enter' && this.toolbar.querySelector('button')) {
|
|
503
733
|
event.preventDefault();
|
|
504
734
|
this.toolbar.querySelector('button').focus();
|
|
@@ -590,13 +820,29 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
590
820
|
});
|
|
591
821
|
}
|
|
592
822
|
|
|
823
|
+
/** A node's box on the host page: its element's rect, or in framed mode its frame box mapped out of the frame. */
|
|
824
|
+
nodeBox(id) {
|
|
825
|
+
const node = typeof id === 'string' ? this.byId.get(id) : null;
|
|
826
|
+
if (!node) return null;
|
|
827
|
+
if (!node.flat) return this.element(id)?.getBoundingClientRect() ?? null;
|
|
828
|
+
const view = this.view;
|
|
829
|
+
if (!view || !this.frameDoc) return null;
|
|
830
|
+
const { element, rect } = node.flat;
|
|
831
|
+
if (element) {
|
|
832
|
+
if (!element.isConnected || element.ownerDocument !== this.frameDoc) return null;
|
|
833
|
+
const box = element.getBoundingClientRect();
|
|
834
|
+
if (!box.width && !box.height) return null;
|
|
835
|
+
return frameRectToHost({ x: box.left, y: box.top, width: box.width, height: box.height }, view);
|
|
836
|
+
}
|
|
837
|
+
return frameRectToHost({ x: rect.x - view.scrollX, y: rect.y - view.scrollY, width: rect.width, height: rect.height }, view);
|
|
838
|
+
}
|
|
839
|
+
|
|
593
840
|
place(frame, id) {
|
|
594
|
-
const
|
|
595
|
-
if (!
|
|
841
|
+
const box = id ? this.nodeBox(id) : null;
|
|
842
|
+
if (!box) {
|
|
596
843
|
frame.hidden = true;
|
|
597
844
|
return null;
|
|
598
845
|
}
|
|
599
|
-
const box = element.getBoundingClientRect();
|
|
600
846
|
frame.hidden = false;
|
|
601
847
|
frame.style.insetInlineStart = '';
|
|
602
848
|
Object.assign(frame.style, { left: `${box.left}px`, top: `${box.top}px`, width: `${box.width}px`, height: `${box.height}px` });
|
|
@@ -606,9 +852,18 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
606
852
|
}
|
|
607
853
|
|
|
608
854
|
position() {
|
|
609
|
-
if (!this.canvas) return;
|
|
855
|
+
if (!this.canvas && !this.iframe) return;
|
|
856
|
+
if (this.iframe) {
|
|
857
|
+
this.view = this.frameMetrics();
|
|
858
|
+
this.applyClip(this.view?.clip ?? null);
|
|
859
|
+
}
|
|
610
860
|
this.place(this.hoverFrame, this.hoverId && this.hoverId !== this.selectedId ? this.hoverId : null);
|
|
611
861
|
const box = this.place(this.selectedFrame, this.selectedId);
|
|
862
|
+
if (this.iframe) {
|
|
863
|
+
this.positionFrameToolbar(box);
|
|
864
|
+
this.positionInsertions();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
612
867
|
// No toolbar without a drawn selection, e.g. while the host re-renders,
|
|
613
868
|
// unless it holds keyboard focus.
|
|
614
869
|
if (box) this.toolbar.hidden = this.actions.length === 0;
|
|
@@ -627,13 +882,13 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
627
882
|
|
|
628
883
|
positionInsertions() {
|
|
629
884
|
const selected = this.selectedId ? this.byId.get(this.selectedId) : null;
|
|
630
|
-
const points = [];
|
|
631
|
-
if (selected?.parent) {
|
|
885
|
+
const points = selected?.flat ? this.flatInsertionPoints(selected) : [];
|
|
886
|
+
if (selected?.parent && !selected.flat) {
|
|
632
887
|
const parent = this.byId.get(selected.parent);
|
|
633
888
|
const siblings = parent.slots[selected.slot] || [];
|
|
634
889
|
for (let index = 0; index <= siblings.length; index += 1) points.push({ parent: parent.id, slot: selected.slot, index, before: siblings[index], after: siblings[index - 1] });
|
|
635
890
|
}
|
|
636
|
-
if (selected) {
|
|
891
|
+
if (selected && !selected.flat) {
|
|
637
892
|
for (const [slot, children] of Object.entries(selected.slots)) points.push({ parent: selected.id, slot, index: children.length, after: children.at(-1), end: true });
|
|
638
893
|
}
|
|
639
894
|
const target = this.insertionTarget;
|
|
@@ -641,8 +896,8 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
641
896
|
const existing = new Map([...this.insertionLayer.children].map((button) => [button.dataset.key, button]));
|
|
642
897
|
const keep = new Set();
|
|
643
898
|
for (const point of points) {
|
|
644
|
-
const
|
|
645
|
-
if (!
|
|
899
|
+
const box = this.nodeBox(point.before) || this.nodeBox(point.after) || this.nodeBox(point.parent);
|
|
900
|
+
if (!box) continue;
|
|
646
901
|
const key = `${point.parent}|${point.slot}|${point.index}`;
|
|
647
902
|
keep.add(key);
|
|
648
903
|
let button = existing.get(key);
|
|
@@ -661,7 +916,6 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
661
916
|
button.append(mark);
|
|
662
917
|
this.insertionLayer.append(button);
|
|
663
918
|
}
|
|
664
|
-
const box = anchor.getBoundingClientRect();
|
|
665
919
|
const top = point.before && !point.end ? box.top : box.bottom;
|
|
666
920
|
const marked = Boolean(target && target.parent_id === point.parent && target.slot === point.slot && target.index === point.index);
|
|
667
921
|
button.setAttribute('aria-pressed', marked ? 'true' : 'false');
|
|
@@ -670,10 +924,204 @@ export function defineCompositionOverlay(registry = globalThis.customElements) {
|
|
|
670
924
|
}
|
|
671
925
|
for (const [key, button] of existing) if (!keep.has(key)) button.remove();
|
|
672
926
|
}
|
|
927
|
+
|
|
928
|
+
// Framed mode: insertion places among the selected node's siblings (the top
|
|
929
|
+
// level is parent_id ""), and at the end of its own children; setDropFilter hides places.
|
|
930
|
+
flatInsertionPoints(selected) {
|
|
931
|
+
const siblings = this.nodes.filter((node) => node.parent === selected.parent).map((node) => node.id);
|
|
932
|
+
const parent = selected.parent ?? '';
|
|
933
|
+
const points = [];
|
|
934
|
+
for (let index = 0; index <= siblings.length; index += 1) points.push({ parent, slot: 'default', index, before: siblings[index], after: siblings[index - 1] });
|
|
935
|
+
const children = selected.slots.default || [];
|
|
936
|
+
if (children.length) points.push({ parent: selected.id, slot: 'default', index: children.length, after: children.at(-1), end: true });
|
|
937
|
+
return points.filter((point) => this.dropFilter({ parent_id: point.parent, slot: point.slot, index: point.index }, null));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** The frame's viewport on the host page and the part of it that is visible (clip). */
|
|
941
|
+
frameMetrics() {
|
|
942
|
+
const iframe = this.iframe;
|
|
943
|
+
const win = this.frameDoc?.defaultView;
|
|
944
|
+
if (!iframe?.isConnected || !win) return null;
|
|
945
|
+
const rect = iframe.getBoundingClientRect();
|
|
946
|
+
if (!rect.width && !rect.height) return null;
|
|
947
|
+
const style = globalThis.getComputedStyle(iframe);
|
|
948
|
+
const sides = (name, suffix = '') => ({ top: px(style[`${name}Top${suffix}`]), right: px(style[`${name}Right${suffix}`]), bottom: px(style[`${name}Bottom${suffix}`]), left: px(style[`${name}Left${suffix}`]) });
|
|
949
|
+
const view = frameViewport({ rect, layout: { width: iframe.offsetWidth, height: iframe.offsetHeight }, border: sides('border', 'Width'), padding: sides('padding'), inner: { width: win.innerWidth, height: win.innerHeight }, scroll: { x: win.scrollX, y: win.scrollY } });
|
|
950
|
+
let clip = { left: view.left, top: view.top, right: view.left + view.width, bottom: view.top + view.height };
|
|
951
|
+
// Scrolling ancestors hide what lies outside them, and so does the window.
|
|
952
|
+
const root = globalThis.document.documentElement;
|
|
953
|
+
for (let element = iframe.parentElement; element && element !== root && element !== globalThis.document.body; element = element.parentElement) {
|
|
954
|
+
const own = globalThis.getComputedStyle(element);
|
|
955
|
+
if (own.overflowX === 'visible' && own.overflowY === 'visible') continue;
|
|
956
|
+
const box = element.getBoundingClientRect();
|
|
957
|
+
const left = box.left + element.clientLeft;
|
|
958
|
+
const top = box.top + element.clientTop;
|
|
959
|
+
clip = intersect(clip, { left, top, right: left + element.clientWidth, bottom: top + element.clientHeight });
|
|
960
|
+
}
|
|
961
|
+
view.clip = intersect(clip, { left: 0, top: 0, right: root.clientWidth, bottom: root.clientHeight });
|
|
962
|
+
return view;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
applyClip(clip) {
|
|
966
|
+
const layer = this.layer.getBoundingClientRect();
|
|
967
|
+
const value = !clip || clip.right <= clip.left || clip.bottom <= clip.top
|
|
968
|
+
? 'inset(50%)'
|
|
969
|
+
: `inset(${Math.max(0, clip.top - layer.top)}px ${Math.max(0, layer.right - clip.right)}px ${Math.max(0, layer.bottom - clip.bottom)}px ${Math.max(0, clip.left - layer.left)}px)`;
|
|
970
|
+
this.layer.style.clipPath = value;
|
|
971
|
+
this.insertionLayer.style.clipPath = value;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
positionFrameToolbar(box) {
|
|
975
|
+
const clip = this.view?.clip;
|
|
976
|
+
const visible = Boolean(box && clip && box.right > clip.left && box.left < clip.right && box.bottom > clip.top && box.top < clip.bottom);
|
|
977
|
+
if (visible) this.toolbar.hidden = this.actions.length === 0;
|
|
978
|
+
else if (!this.toolbar.contains(globalThis.document.activeElement)) this.toolbar.hidden = true;
|
|
979
|
+
if (!visible || this.toolbar.hidden) return;
|
|
980
|
+
const bar = this.toolbar.getBoundingClientRect();
|
|
981
|
+
// Above the node, else below it, and always inside the visible part of the frame.
|
|
982
|
+
const above = box.top - bar.height - 4;
|
|
983
|
+
let top = above >= clip.top ? above : box.bottom + 4;
|
|
984
|
+
top = Math.max(clip.top, Math.min(top, clip.bottom - bar.height));
|
|
985
|
+
const rtl = globalThis.getComputedStyle(this).direction === 'rtl';
|
|
986
|
+
const start = rtl ? box.right - bar.width : box.left;
|
|
987
|
+
const left = Math.max(0, Math.min(start, globalThis.document.documentElement.clientWidth - bar.width));
|
|
988
|
+
Object.assign(this.toolbar.style, { top: `${top}px`, left: `${left}px` });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** The innermost node under a pointer event in the frame: the smallest element or rect node that holds it. */
|
|
992
|
+
frameNodeAt(event) {
|
|
993
|
+
let best = null;
|
|
994
|
+
let area = Infinity;
|
|
995
|
+
const consider = (id, width, height) => {
|
|
996
|
+
if (width * height < area) {
|
|
997
|
+
best = id;
|
|
998
|
+
area = width * height;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
for (let element = isElement(event.target) ? event.target : event.target?.parentElement; element; element = element.parentElement) {
|
|
1002
|
+
const id = this.elementIds.get(element);
|
|
1003
|
+
if (id === undefined || !this.byId.has(id)) continue;
|
|
1004
|
+
const box = element.getBoundingClientRect();
|
|
1005
|
+
consider(id, box.width, box.height);
|
|
1006
|
+
}
|
|
1007
|
+
const win = this.frameDoc?.defaultView;
|
|
1008
|
+
if (win && this.rectNodes.length && finite(event.clientX)) {
|
|
1009
|
+
const x = event.clientX + win.scrollX;
|
|
1010
|
+
const y = event.clientY + win.scrollY;
|
|
1011
|
+
for (const { id, flat: { rect } } of this.rectNodes) {
|
|
1012
|
+
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) consider(id, rect.width, rect.height);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return best;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
onFrameClick(event) {
|
|
1019
|
+
if (this.editing && this.editing.element.contains(event.target)) return;
|
|
1020
|
+
// In the editor a click selects; it never follows links or submits. A
|
|
1021
|
+
// click on no node goes back to no selection (the letter itself).
|
|
1022
|
+
event.preventDefault();
|
|
1023
|
+
const id = this.frameNodeAt(event);
|
|
1024
|
+
if (id) event.stopPropagation();
|
|
1025
|
+
this.select(id);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
onFrameDoubleClick(event) {
|
|
1029
|
+
if (this.editing) return;
|
|
1030
|
+
const id = this.frameNodeAt(event);
|
|
1031
|
+
if (!id || !this.byId.get(id).flat?.editable) return;
|
|
1032
|
+
event.preventDefault();
|
|
1033
|
+
this.edit(id);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
onFrameKey(event) {
|
|
1037
|
+
if (this.editing) {
|
|
1038
|
+
if (event.isComposing) return;
|
|
1039
|
+
if (event.key === 'Enter' || event.key === 'Escape') {
|
|
1040
|
+
event.preventDefault();
|
|
1041
|
+
event.stopPropagation();
|
|
1042
|
+
this.finishEdit(event.key === 'Enter');
|
|
1043
|
+
}
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (!event.defaultPrevented) this.onTreeKey(event);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** Starts plain-string editing of an editable node inside the frame; returns whether it started. */
|
|
1050
|
+
edit(id) {
|
|
1051
|
+
const node = typeof id === 'string' ? this.byId.get(id) : null;
|
|
1052
|
+
const element = node?.flat?.editable ? node.flat.element : null;
|
|
1053
|
+
if (!element || !this.frameDoc || !element.isConnected || element.ownerDocument !== this.frameDoc) return false;
|
|
1054
|
+
if (this.editing?.id === id) return true;
|
|
1055
|
+
if (this.editing) this.finishEdit(true);
|
|
1056
|
+
const returnFocus = globalThis.document.activeElement === this.tree;
|
|
1057
|
+
this.select(id);
|
|
1058
|
+
const doc = element.ownerDocument;
|
|
1059
|
+
const attribute = element.getAttribute('contenteditable');
|
|
1060
|
+
element.setAttribute('contenteditable', 'plaintext-only');
|
|
1061
|
+
// Where plaintext-only is unknown, rich editing is narrowed to plain text by hand.
|
|
1062
|
+
const plain = element.contentEditable === 'plaintext-only';
|
|
1063
|
+
if (!plain) element.setAttribute('contenteditable', 'true');
|
|
1064
|
+
const connection = new AbortController();
|
|
1065
|
+
const options = { signal: connection.signal };
|
|
1066
|
+
element.addEventListener('blur', () => this.finishEdit(true), options);
|
|
1067
|
+
element.addEventListener('input', () => this.schedule(), options);
|
|
1068
|
+
element.addEventListener('beforeinput', (event) => {
|
|
1069
|
+
const type = event.inputType || '';
|
|
1070
|
+
if (type === 'insertParagraph' || type === 'insertLineBreak' || (!plain && type.startsWith('format'))) event.preventDefault();
|
|
1071
|
+
}, options);
|
|
1072
|
+
if (!plain) {
|
|
1073
|
+
element.addEventListener('paste', (event) => {
|
|
1074
|
+
event.preventDefault();
|
|
1075
|
+
doc.execCommand('insertText', false, (event.clipboardData?.getData('text/plain') || '').replace(/\s+/gu, ' '));
|
|
1076
|
+
}, options);
|
|
1077
|
+
}
|
|
1078
|
+
this.editing = { id, element, saved: element.cloneNode(true), previous: plainText(element), attribute, connection, returnFocus };
|
|
1079
|
+
holdOpenSurfaceMark(this, true);
|
|
1080
|
+
this.setAttribute('data-sf-overlay-editing', '');
|
|
1081
|
+
element.focus({ preventScroll: true });
|
|
1082
|
+
const selection = doc.defaultView?.getSelection();
|
|
1083
|
+
if (selection) {
|
|
1084
|
+
const range = doc.createRange();
|
|
1085
|
+
range.selectNodeContents(element);
|
|
1086
|
+
selection.removeAllRanges();
|
|
1087
|
+
selection.addRange(range);
|
|
1088
|
+
}
|
|
1089
|
+
this.live.textContent = format(this.messages.editing, { label: this.info(node).label });
|
|
1090
|
+
this.schedule();
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Ends the inline edit: commit emits sf-composition-overlay-edit (cancelable) when the text changed; cancel restores it. */
|
|
1095
|
+
finishEdit(commit) {
|
|
1096
|
+
const editing = this.editing;
|
|
1097
|
+
if (!editing) return;
|
|
1098
|
+
this.editing = null;
|
|
1099
|
+
editing.connection.abort();
|
|
1100
|
+
holdOpenSurfaceMark(this, false);
|
|
1101
|
+
this.removeAttribute('data-sf-overlay-editing');
|
|
1102
|
+
const { element } = editing;
|
|
1103
|
+
if (editing.attribute === null) element.removeAttribute('contenteditable');
|
|
1104
|
+
else element.setAttribute('contenteditable', editing.attribute);
|
|
1105
|
+
const restore = () => {
|
|
1106
|
+
if (element.isConnected) element.replaceChildren(...editing.saved.childNodes);
|
|
1107
|
+
};
|
|
1108
|
+
const value = plainText(element);
|
|
1109
|
+
if (!commit) restore();
|
|
1110
|
+
else if (value !== editing.previous) {
|
|
1111
|
+
const event = new globalThis.CustomEvent(OVERLAY_EVENTS.edit, {
|
|
1112
|
+
bubbles: true, composed: true, cancelable: true, detail: Object.freeze({ node_id: editing.id, value, previous: editing.previous }),
|
|
1113
|
+
});
|
|
1114
|
+
this.dispatchEvent(event);
|
|
1115
|
+
// The host refused the value: the letter shows what it showed before.
|
|
1116
|
+
if (event.defaultPrevented) restore();
|
|
1117
|
+
}
|
|
1118
|
+
if (editing.returnFocus && this.isConnected) this.tree.focus({ preventScroll: true });
|
|
1119
|
+
this.schedule();
|
|
1120
|
+
}
|
|
673
1121
|
}
|
|
674
1122
|
|
|
675
1123
|
registry.define('sf-composition-overlay', SfCompositionOverlay);
|
|
676
1124
|
return SfCompositionOverlay;
|
|
677
1125
|
}
|
|
678
1126
|
|
|
679
|
-
export default { OVERLAY_EVENTS, checkOverlayActions, defineCompositionOverlay, overlayNodes };
|
|
1127
|
+
export default { OVERLAY_EVENTS, checkOverlayActions, checkOverlayNodes, defineCompositionOverlay, flatOverlayNodes, frameRectToHost, frameViewport, overlayNodes };
|