@openpresentation/opf-editor 0.27.0 → 0.28.0
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/dist/design-controls.js +3 -2
- package/dist/design-options.js +1 -1
- package/dist/json-editor.d.ts +33 -0
- package/dist/json-editor.js +83 -2
- package/dist/layout-adapter.js +96 -0
- package/dist/layout-canvas.js +122 -232
- package/dist/layout-editor-ui.js +46 -185
- package/dist/layout-inspector.js +36 -99
- package/dist/layout-layers.js +75 -92
- package/dist/layout-model.js +62 -103
- package/dist/react.d.ts +4 -0
- package/dist/react.js +9 -2
- package/dist/source-color-scheme.js +60 -0
- package/dist/source-pane.d.ts +85 -0
- package/dist/source-pane.js +151 -9
- package/dist/studio-kit/commands.d.ts +38 -0
- package/dist/studio-kit/commands.js +148 -0
- package/dist/studio-kit/dom.d.ts +36 -0
- package/dist/studio-kit/dom.js +162 -0
- package/dist/studio-kit/drag.d.ts +26 -0
- package/dist/studio-kit/drag.js +127 -0
- package/dist/studio-kit/gesture.d.ts +29 -0
- package/dist/studio-kit/gesture.js +87 -0
- package/dist/studio-kit/index.d.ts +29 -0
- package/dist/studio-kit/index.js +12 -0
- package/dist/studio-kit/inspector.d.ts +22 -0
- package/dist/studio-kit/inspector.js +119 -0
- package/dist/studio-kit/overlay.d.ts +29 -0
- package/dist/studio-kit/overlay.js +182 -0
- package/dist/studio-kit/selection.d.ts +30 -0
- package/dist/studio-kit/selection.js +106 -0
- package/dist/studio-kit/tree.d.ts +9 -0
- package/dist/studio-kit/tree.js +82 -0
- package/dist/studio-kit/viewport.d.ts +29 -0
- package/dist/studio-kit/viewport.js +153 -0
- package/dist/styles.css +28 -7
- package/dist/workspace.d.ts +26 -1
- package/dist/workspace.js +13 -0
- package/package.json +10 -10
package/dist/layout-canvas.js
CHANGED
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
// split, insert, add the heading regions, pan, and preview with sample content; drag a divider to resize, drag a region to reorder or move
|
|
3
3
|
// it into another split; a context menu and the keyboard do the same. The boxes are positioned from `layoutBoxes` (layout-tree.js), so
|
|
4
4
|
// the canvas keeps no tree of its own: every edit is a command on the shared model, and every pointer gesture is one history step.
|
|
5
|
+
// CE-18: the viewport, overlay, drag, divider and keymap behavior is the studio kit's (src/studio-kit/), over the layout adapter.
|
|
5
6
|
import { createLayoutModel } from "./layout-model.js";
|
|
7
|
+
import { createLayoutAdapter } from "./layout-adapter.js";
|
|
6
8
|
import { LAYOUT_TOOL_DEFS } from "./layout-palette.js";
|
|
7
9
|
import { sampleDeck } from "./layout-samples.js";
|
|
8
10
|
import { whenFontsReady } from "./font-gate.js";
|
|
11
|
+
import { isHeading, isPrefix, isSplit, listAt, nodeAt, pathKey, resizeSizes, samePath, setPairSizes } from "./layout-tree.js";
|
|
9
12
|
import {
|
|
10
|
-
|
|
11
|
-
} from "./layout-tree.js";
|
|
12
|
-
import {
|
|
13
|
-
SIZE_LABEL, buildWireframe, createDom, handleGlobalShortcut, handleSelectionShortcut, isTypingTarget, nextId, nodeMenuItems, openMenu, pct,
|
|
13
|
+
SIZE_LABEL, buildWireframe, createDom, handleGlobalShortcut, handleSelectionShortcut, isHandled, isTypingTarget, markHandled, nextId, nodeMenuItems, openMenu, pct,
|
|
14
14
|
} from "./layout-editor-ui.js";
|
|
15
|
+
import { createViewport } from "./studio-kit/viewport.js";
|
|
16
|
+
import { boxesAt, createKeyedLayer, createOverlay, dividerKeyAction, paintBadge, paintDivider, paintSelectable, ratioLabel, setBox } from "./studio-kit/overlay.js";
|
|
17
|
+
import { createDragController, dragGhost, trackPointer } from "./studio-kit/drag.js";
|
|
15
18
|
|
|
16
19
|
const ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
|
|
17
|
-
const DRAG_DISTANCE = 4;
|
|
18
20
|
const HEADING_ITEMS = [{ name: "title", label: "Title region" }, { name: "subtitle", label: "Subtitle region" }];
|
|
19
|
-
const inside = (box, p, slack = 0) => p.x >= box.x - slack && p.x <= box.x + box.w + slack && p.y >= box.y - slack && p.y <= box.y + box.h + slack;
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Mount the canvas in `container`. Options: `model` (shared state; one is created from the other options when omitted), `toSvg` and `fonts`
|
|
@@ -30,6 +31,7 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
30
31
|
const { el } = dom;
|
|
31
32
|
const ownModel = !options.model;
|
|
32
33
|
const model = options.model ?? createLayoutModel(options);
|
|
34
|
+
const adapter = createLayoutAdapter(model);
|
|
33
35
|
const id = nextId("opf-lx-canvas");
|
|
34
36
|
|
|
35
37
|
const root = el("div", "opf-lx-canvas opf-ui", { "data-density": model.density, "data-tool": model.tool });
|
|
@@ -42,13 +44,8 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
42
44
|
implicit.append(el("span", "opf-lx-implicit-label", { text: "title · implicit" }));
|
|
43
45
|
const layer = el("div", "opf-lx-layer");
|
|
44
46
|
const overlay = el("div", "opf-lx-overlay");
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const ratio = el("div", "opf-lx-ratio", { "aria-hidden": "true" });
|
|
48
|
-
cut.hidden = true;
|
|
49
|
-
drop.hidden = true;
|
|
50
|
-
ratio.hidden = true;
|
|
51
|
-
overlay.append(cut, drop, ratio);
|
|
47
|
+
// The kit overlay: the split tools' cut guide, the drop line and the ratio badge.
|
|
48
|
+
const marks = createOverlay(dom, overlay, { guide: "cut" });
|
|
52
49
|
content.append(implicit, layer, overlay);
|
|
53
50
|
frame.append(rendered, content);
|
|
54
51
|
stage.append(frame);
|
|
@@ -68,14 +65,9 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
68
65
|
root.append(banner, viewport, footer);
|
|
69
66
|
container.append(root);
|
|
70
67
|
|
|
71
|
-
const keyed =
|
|
72
|
-
let geo =
|
|
68
|
+
const keyed = createKeyedLayer(layer);
|
|
69
|
+
let geo = adapter.geometry();
|
|
73
70
|
let hover = null;
|
|
74
|
-
let zoom = 1;
|
|
75
|
-
let spaceHeld = false;
|
|
76
|
-
let drag = null;
|
|
77
|
-
let pending = null;
|
|
78
|
-
let pan = null;
|
|
79
71
|
let divider = null;
|
|
80
72
|
let menu = null;
|
|
81
73
|
let renameInput = null;
|
|
@@ -84,23 +76,28 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
84
76
|
let previewTimer = 0;
|
|
85
77
|
let lastPreview = "";
|
|
86
78
|
|
|
79
|
+
// --- the kit viewport (zoom and pan) and the drag controller ----------------------------------------------------------------
|
|
80
|
+
const viewportKit = createViewport(viewport, {
|
|
81
|
+
root, steps: ZOOMS, zoomVar: "--opf-lx-zoom", panEnabled: () => model.tool === "hand",
|
|
82
|
+
onZoom: (value) => { zoomLabel.textContent = `${Math.round(value * 100)}%`; },
|
|
83
|
+
});
|
|
84
|
+
const dragger = createDragController({
|
|
85
|
+
doc,
|
|
86
|
+
onStart: (press) => startDrag(press),
|
|
87
|
+
onStarted: () => { root.dataset.dragging = "true"; paintHover(); render(); },
|
|
88
|
+
onMove: (session, event) => moveDrag(session, event),
|
|
89
|
+
onEnd: (session) => endDrag(session),
|
|
90
|
+
onDrop: (session) => finishDrag(session),
|
|
91
|
+
onEscape: () => model.notify("Move cancelled."),
|
|
92
|
+
});
|
|
93
|
+
|
|
87
94
|
// --- geometry helpers -------------------------------------------------------------------------------------------------------
|
|
88
|
-
const unit = (event) =>
|
|
89
|
-
const box = content.getBoundingClientRect();
|
|
90
|
-
return { x: (event.clientX - box.left) / (box.width || 1), y: (event.clientY - box.top) / (box.height || 1) };
|
|
91
|
-
};
|
|
95
|
+
const unit = (event) => viewportKit.toUnit(event, content);
|
|
92
96
|
const boxOf = (path) => geo.nodes.find((box) => samePath(box.path, path));
|
|
93
|
-
const chainAt = (point, skip) => geo.nodes
|
|
97
|
+
const chainAt = (point, skip) => boxesAt(geo.nodes, point, { skip });
|
|
94
98
|
const nodeElement = (path) => keyed.get(`n:${pathKey(path)}`);
|
|
95
99
|
const isSelected = (path) => model.selection.some((entry) => samePath(entry, path));
|
|
96
100
|
|
|
97
|
-
function setBox(node, box, pad = 0) {
|
|
98
|
-
node.style.left = pct(box.x - pad);
|
|
99
|
-
node.style.top = pct(box.y - pad);
|
|
100
|
-
node.style.width = pct(box.w + 2 * pad);
|
|
101
|
-
node.style.height = pct(box.h + 2 * pad);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
101
|
// --- the descriptions a screen reader hears ---------------------------------------------------------------------------------
|
|
105
102
|
function describeNode(box) {
|
|
106
103
|
const { entry, path, direction } = box;
|
|
@@ -113,12 +110,6 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
// --- render ---------------------------------------------------------------------------------------------------------------
|
|
116
|
-
function ensure(key, create, seen) {
|
|
117
|
-
let node = keyed.get(key);
|
|
118
|
-
if (!node) { node = create(); keyed.set(key, node); }
|
|
119
|
-
seen.add(key);
|
|
120
|
-
return node;
|
|
121
|
-
}
|
|
122
113
|
function makeNode() {
|
|
123
114
|
const node = el("div", "opf-lx-node");
|
|
124
115
|
node.append(el("span", "opf-lx-label", { "aria-hidden": "true" }));
|
|
@@ -147,14 +138,14 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
147
138
|
function render() {
|
|
148
139
|
// Focus that is on a box (or the frame) follows the selection afterwards; a divider, slot, menu or field keeps its own.
|
|
149
140
|
const keepFocus = doc.activeElement === frame || Boolean(doc.activeElement?.classList?.contains("opf-lx-node"));
|
|
150
|
-
geo =
|
|
151
|
-
const
|
|
152
|
-
const
|
|
141
|
+
geo = adapter.geometry();
|
|
142
|
+
const pass = keyed.begin();
|
|
143
|
+
const drag = dragger.active;
|
|
153
144
|
const primary = model.primary;
|
|
154
145
|
const tabStop = primary && boxOf(primary) ? pathKey(primary) : geo.nodes[0] ? pathKey(geo.nodes[0].path) : null;
|
|
155
146
|
for (const box of geo.nodes) {
|
|
156
147
|
const key = `n:${pathKey(box.path)}`;
|
|
157
|
-
const node = ensure(key, makeNode
|
|
148
|
+
const node = pass.ensure(key, makeNode);
|
|
158
149
|
const split = isSplit(box.entry);
|
|
159
150
|
const issues = model.findingsFor(box.path);
|
|
160
151
|
node.dataset.path = pathKey(box.path);
|
|
@@ -164,21 +155,14 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
164
155
|
else { node.dataset.name = box.entry.name; delete node.dataset.direction; }
|
|
165
156
|
node.dataset.heading = !split && isHeading(box.entry) ? "true" : "false";
|
|
166
157
|
node.dataset.finding = issues.length ? issues[0].severity : model.findingsWithin(box.path).length ? "within" : "";
|
|
167
|
-
node.classList.toggle("is-selected", isSelected(box.path));
|
|
168
158
|
node.classList.toggle("is-dragging", Boolean(drag?.valid?.some((path) => isPrefix(path, box.path))));
|
|
169
|
-
node.
|
|
170
|
-
node.setAttribute("aria-label", describeNode(box));
|
|
171
|
-
if (split) node.removeAttribute("aria-pressed"); else node.setAttribute("aria-pressed", String(isSelected(box.path)));
|
|
172
|
-
node.tabIndex = pathKey(box.path) === tabStop ? 0 : -1;
|
|
173
|
-
setBox(node, box);
|
|
159
|
+
paintSelectable(node, { box, selected: isSelected(box.path), pressable: !split, label: describeNode(box), tabStop: pathKey(box.path) === tabStop });
|
|
174
160
|
// The name and size are a chip outside the box's top edge (a split's tag sits at the top right), so they never cover the content drawn
|
|
175
161
|
// inside. The words come from data attributes (CSS content), so the box's own accessible name is the one description a screen reader hears.
|
|
176
162
|
const label = node.firstElementChild;
|
|
177
163
|
label.dataset.text = split ? (box.entry.direction === "row" ? "row" : "column") : box.entry.name;
|
|
178
164
|
label.dataset.size = SIZE_LABEL(box.entry, box.direction);
|
|
179
|
-
|
|
180
|
-
if (issues.length && !badge) { badge = el("span", "opf-lx-badge", { "aria-hidden": "true" }); badge.append(dom.icon("CircleX", 14)); node.append(badge); }
|
|
181
|
-
if (badge) { if (issues.length) badge.title = issues.map((issue) => issue.message).join("\n"); else badge.remove(); }
|
|
165
|
+
paintBadge(dom, node, issues.map((issue) => issue.message));
|
|
182
166
|
let wire = node.querySelector(":scope > .opf-lx-wire");
|
|
183
167
|
const wanted = model.preview && !split;
|
|
184
168
|
const wireKey = wanted ? JSON.stringify([isHeading(box.entry), box.entry.accepts, box.entry.flow, box.entry.max]) : "";
|
|
@@ -189,43 +173,29 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
189
173
|
wire.dataset.key = wireKey;
|
|
190
174
|
node.append(wire);
|
|
191
175
|
}
|
|
192
|
-
order.push(node);
|
|
193
176
|
}
|
|
194
177
|
// Dividers: the gap between two siblings.
|
|
195
178
|
for (const info of geo.dividers) {
|
|
196
179
|
const key = `d:${pathKey(info.listPath)}:${info.index}`;
|
|
197
|
-
const node = ensure(key, () => makeDivider(key)
|
|
180
|
+
const node = pass.ensure(key, () => makeDivider(key));
|
|
198
181
|
node._info = info;
|
|
199
|
-
node.dataset.orientation = info.direction === "row" ? "vertical" : "horizontal";
|
|
200
182
|
node.dataset.depth = String(info.depth);
|
|
201
|
-
node.dataset.fixed = info.draggable ? "false" : "true";
|
|
202
183
|
node.dataset.active = divider?.key === key ? "true" : "false";
|
|
203
|
-
setBox(node, info);
|
|
204
|
-
node.setAttribute("role", "separator");
|
|
205
|
-
node.setAttribute("aria-orientation", info.direction === "row" ? "vertical" : "horizontal");
|
|
206
184
|
const first = [...info.listPath, info.index], second = [...info.listPath, info.index + 1];
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
node.removeAttribute("aria-disabled");
|
|
216
|
-
node.title = "Drag to resize. Double-click to make both sides equal.";
|
|
217
|
-
} else {
|
|
218
|
-
node.removeAttribute("tabindex");
|
|
219
|
-
for (const name of ["aria-label", "aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext", "title"]) node.removeAttribute(name);
|
|
220
|
-
node.setAttribute("aria-disabled", "true");
|
|
221
|
-
}
|
|
222
|
-
order.push(node);
|
|
185
|
+
paintDivider(node, {
|
|
186
|
+
box: info, orientation: info.direction === "row" ? "vertical" : "horizontal", draggable: info.draggable,
|
|
187
|
+
tabbable: model.tool === "select" || model.tool === "hand",
|
|
188
|
+
label: info.draggable ? `Resize ${model.describe(first)} and ${model.describe(second)}` : undefined,
|
|
189
|
+
valueNow: info.draggable ? Math.round(info.a / (info.a + info.b) * 100) : undefined,
|
|
190
|
+
valueText: info.draggable ? ratioLabel(info.a, info.b) : undefined,
|
|
191
|
+
title: "Drag to resize. Double-click to make both sides equal.",
|
|
192
|
+
});
|
|
223
193
|
}
|
|
224
194
|
// Insert slots, only for the Insert tool.
|
|
225
195
|
if (model.tool === "insert" && !model.readOnly) {
|
|
226
196
|
geo.slots.forEach((slot) => {
|
|
227
197
|
const key = `s:${pathKey(slot.listPath)}:${slot.index}`;
|
|
228
|
-
const node = ensure(key, makeSlot
|
|
198
|
+
const node = pass.ensure(key, makeSlot);
|
|
229
199
|
node._info = slot;
|
|
230
200
|
node.dataset.orientation = slot.direction === "row" ? "vertical" : "horizontal";
|
|
231
201
|
node.dataset.depth = String(slot.depth);
|
|
@@ -239,16 +209,9 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
239
209
|
node.style.top = slot.direction === "row" ? pct(thick.y) : `calc(${pct(thick.y)} - var(--opf-lx-slot) / 2)`;
|
|
240
210
|
node.style.width = slot.direction === "row" ? "var(--opf-lx-slot)" : pct(thick.w);
|
|
241
211
|
node.style.height = slot.direction === "row" ? pct(thick.h) : "var(--opf-lx-slot)";
|
|
242
|
-
order.push(node);
|
|
243
212
|
});
|
|
244
213
|
}
|
|
245
|
-
|
|
246
|
-
let cursor = null;
|
|
247
|
-
for (const node of order) {
|
|
248
|
-
const expected = cursor ? cursor.nextSibling : layer.firstChild;
|
|
249
|
-
if (node.parentNode !== layer || expected !== node) layer.insertBefore(node, expected);
|
|
250
|
-
cursor = node;
|
|
251
|
-
}
|
|
214
|
+
pass.done();
|
|
252
215
|
// The implicit title band and the layout-level findings.
|
|
253
216
|
implicit.hidden = !geo.implicitTitle;
|
|
254
217
|
if (geo.implicitTitle) setBox(implicit, geo.implicitTitle);
|
|
@@ -267,11 +230,9 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
267
230
|
schedulePreview();
|
|
268
231
|
if (keepFocus) syncFocus();
|
|
269
232
|
}
|
|
270
|
-
const trim = (value) => String(Math.round(value * 1000) / 1000);
|
|
271
233
|
|
|
272
234
|
function paintHover() {
|
|
273
|
-
|
|
274
|
-
if (hover && !drag && !divider) nodeElement(hover)?.setAttribute("data-hover", "true");
|
|
235
|
+
marks.hover(layer, hover && !dragger.active && !divider ? nodeElement(hover) : null);
|
|
275
236
|
}
|
|
276
237
|
function paintStatus() {
|
|
277
238
|
const notice = model.notice;
|
|
@@ -280,7 +241,7 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
280
241
|
status.dataset.live = notice && notice.kind !== "error" ? "true" : "false";
|
|
281
242
|
alert.textContent = notice?.kind === "error" ? notice.message : "";
|
|
282
243
|
alert.hidden = !alert.textContent;
|
|
283
|
-
zoomLabel.textContent = `${Math.round(zoom * 100)}%`;
|
|
244
|
+
zoomLabel.textContent = `${Math.round(viewportKit.zoom * 100)}%`;
|
|
284
245
|
}
|
|
285
246
|
/** Put DOM focus on the selected box (or the frame, when nothing is selected), after the structure changed under it. */
|
|
286
247
|
function syncFocus() {
|
|
@@ -303,24 +264,7 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
303
264
|
}
|
|
304
265
|
return deepest;
|
|
305
266
|
}
|
|
306
|
-
const panMode = () =>
|
|
307
|
-
|
|
308
|
-
viewport.addEventListener("pointerdown", (event) => {
|
|
309
|
-
if (!panMode() || (event.pointerType === "mouse" && event.button !== 0)) return;
|
|
310
|
-
event.preventDefault();
|
|
311
|
-
event.stopPropagation();
|
|
312
|
-
pan = { x: event.clientX, y: event.clientY, left: viewport.scrollLeft, top: viewport.scrollTop };
|
|
313
|
-
root.dataset.panning = "true";
|
|
314
|
-
viewport.setPointerCapture?.(event.pointerId);
|
|
315
|
-
}, true);
|
|
316
|
-
viewport.addEventListener("pointermove", (event) => {
|
|
317
|
-
if (!pan) return;
|
|
318
|
-
viewport.scrollLeft = pan.left - (event.clientX - pan.x);
|
|
319
|
-
viewport.scrollTop = pan.top - (event.clientY - pan.y);
|
|
320
|
-
});
|
|
321
|
-
const endPan = () => { pan = null; delete root.dataset.panning; };
|
|
322
|
-
viewport.addEventListener("pointerup", endPan);
|
|
323
|
-
viewport.addEventListener("pointercancel", endPan);
|
|
267
|
+
const panMode = () => viewportKit.panMode;
|
|
324
268
|
|
|
325
269
|
content.addEventListener("pointerdown", (event) => {
|
|
326
270
|
if (event.target.closest?.(".opf-lx-divider, .opf-lx-slot, .opf-lx-menu, .opf-lx-rename") || panMode()) return;
|
|
@@ -345,7 +289,7 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
345
289
|
focusPrimary();
|
|
346
290
|
if (model.readOnly) return;
|
|
347
291
|
const paths = isSelected(hit.path) ? model.selection.map((path) => [...path]) : [hit.path];
|
|
348
|
-
|
|
292
|
+
dragger.press(event, { paths });
|
|
349
293
|
content.setPointerCapture?.(event.pointerId);
|
|
350
294
|
});
|
|
351
295
|
// The margin of the slide, outside the content box: a click there selects the layout.
|
|
@@ -356,8 +300,7 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
356
300
|
frame.focus({ preventScroll: true });
|
|
357
301
|
});
|
|
358
302
|
content.addEventListener("pointermove", (event) => {
|
|
359
|
-
if (
|
|
360
|
-
if (drag) { moveDrag(event); return; }
|
|
303
|
+
if (dragger.move(event)) return;
|
|
361
304
|
if (event.pointerType === "touch") return;
|
|
362
305
|
// Hover: the node a click would select, and the guide of the split tools.
|
|
363
306
|
const point = unit(event);
|
|
@@ -369,11 +312,8 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
369
312
|
paintCut();
|
|
370
313
|
});
|
|
371
314
|
content.addEventListener("pointerleave", () => { hover = null; paintHover(); paintCut(); });
|
|
372
|
-
content.addEventListener("pointerup", (event) => {
|
|
373
|
-
|
|
374
|
-
pending = null;
|
|
375
|
-
});
|
|
376
|
-
content.addEventListener("pointercancel", () => { cancelDrag(); pending = null; });
|
|
315
|
+
content.addEventListener("pointerup", (event) => { dragger.release(event); });
|
|
316
|
+
content.addEventListener("pointercancel", () => { dragger.cancel(); });
|
|
377
317
|
content.addEventListener("dblclick", (event) => {
|
|
378
318
|
if (model.tool !== "select" || event.target.closest?.(".opf-lx-divider, .opf-lx-slot, .opf-lx-rename") || model.readOnly) return;
|
|
379
319
|
const region = [...chainAt(unit(event))].reverse().find((box) => !isSplit(box.entry));
|
|
@@ -382,11 +322,7 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
382
322
|
function paintCut() {
|
|
383
323
|
const tool = model.tool;
|
|
384
324
|
const box = (tool === "split-row" || tool === "split-column") && hover ? boxOf(hover) : null;
|
|
385
|
-
|
|
386
|
-
if (!box) return;
|
|
387
|
-
if (tool === "split-row") Object.assign(cut.style, { left: pct(box.x + box.w / 2), top: pct(box.y), width: "0", height: pct(box.h) });
|
|
388
|
-
else Object.assign(cut.style, { left: pct(box.x), top: pct(box.y + box.h / 2), width: pct(box.w), height: "0" });
|
|
389
|
-
cut.dataset.axis = tool === "split-row" ? "row" : "column";
|
|
325
|
+
if (box) marks.showGuide(box, tool === "split-row" ? "row" : "column"); else marks.hideGuide();
|
|
390
326
|
}
|
|
391
327
|
function focusPrimary() {
|
|
392
328
|
const primary = model.primary;
|
|
@@ -394,20 +330,15 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
394
330
|
(node ?? frame).focus({ preventScroll: true });
|
|
395
331
|
}
|
|
396
332
|
|
|
397
|
-
// --- drag a region to reorder it or move it into a split
|
|
398
|
-
function startDrag() {
|
|
399
|
-
if (
|
|
400
|
-
const paths =
|
|
401
|
-
if (!paths.length)
|
|
402
|
-
const ghost =
|
|
403
|
-
|
|
404
|
-
drag = { paths, valid: paths, ghost, drop: null };
|
|
405
|
-
root.dataset.dragging = "true";
|
|
406
|
-
doc.addEventListener("keydown", dragKey, true);
|
|
407
|
-
paintHover();
|
|
408
|
-
render();
|
|
333
|
+
// --- drag a region to reorder it or move it into a split (the kit's drag controller) ------------------------------------------
|
|
334
|
+
function startDrag(press) {
|
|
335
|
+
if (model.readOnly) return null;
|
|
336
|
+
const paths = press.payload.paths.filter((path) => nodeAt(model.record, path));
|
|
337
|
+
if (!paths.length) return null;
|
|
338
|
+
const ghost = dragGhost(dom, root, paths.length > 1 ? `${paths.length} regions` : model.describe(paths[0]));
|
|
339
|
+
return { paths, valid: paths, ghost, drop: null };
|
|
409
340
|
}
|
|
410
|
-
function dropAt(point) {
|
|
341
|
+
function dropAt(drag, point) {
|
|
411
342
|
const skip = (box) => drag.paths.some((path) => isPrefix(path, box.path));
|
|
412
343
|
const chain = chainAt(point, skip);
|
|
413
344
|
const hit = chain[chain.length - 1];
|
|
@@ -427,55 +358,35 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
427
358
|
index = hit.path[hit.path.length - 1] + (after ? 1 : 0);
|
|
428
359
|
}
|
|
429
360
|
const slot = geo.slots.find((entry) => samePath(entry.listPath, listPath) && entry.index === index);
|
|
430
|
-
|
|
431
|
-
let noop = false;
|
|
432
|
-
try {
|
|
433
|
-
const moved = moveNodes(model.record, drag.paths, listPath, index);
|
|
434
|
-
noop = JSON.stringify(moved.record) === JSON.stringify(model.record);
|
|
435
|
-
} catch (error) {
|
|
436
|
-
if (!(error instanceof LayoutOperationError)) throw error;
|
|
437
|
-
reason = error.message;
|
|
438
|
-
}
|
|
361
|
+
const { reason, noop } = adapter.trialMove(drag.paths, listPath, index);
|
|
439
362
|
return { listPath, index, slot, into, reason, noop };
|
|
440
363
|
}
|
|
441
|
-
function moveDrag(event) {
|
|
442
|
-
drag.ghost.
|
|
443
|
-
|
|
444
|
-
const found = dropAt(unit(event));
|
|
364
|
+
function moveDrag(drag, event) {
|
|
365
|
+
drag.ghost.moveTo(event);
|
|
366
|
+
const found = dropAt(drag, unit(event));
|
|
445
367
|
drag.drop = found;
|
|
446
|
-
|
|
447
|
-
if (!found?.slot) {
|
|
448
|
-
drop.hidden = false;
|
|
449
|
-
drop.dataset.orientation = found.slot.direction === "row" ? "vertical" : "horizontal";
|
|
450
|
-
drop.dataset.state = found.reason ? "refused" : found.noop ? "same" : "ok";
|
|
451
|
-
drop.title = found.reason;
|
|
368
|
+
marks.into(layer, null);
|
|
369
|
+
if (!found?.slot) { marks.hideDrop(); return; }
|
|
452
370
|
const slot = found.slot;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (found.into && !found.reason) nodeElement(found.into)?.setAttribute("data-into", "true");
|
|
371
|
+
marks.showDrop(slot, { orientation: slot.direction === "row" ? "vertical" : "horizontal", state: found.reason ? "refused" : found.noop ? "same" : "ok", reason: found.reason });
|
|
372
|
+
if (found.into && !found.reason) marks.into(layer, nodeElement(found.into));
|
|
456
373
|
}
|
|
457
|
-
function finishDrag() {
|
|
458
|
-
const outcome = drag
|
|
459
|
-
const paths = drag
|
|
460
|
-
endDrag();
|
|
374
|
+
function finishDrag(drag) {
|
|
375
|
+
const outcome = drag.drop;
|
|
376
|
+
const paths = drag.paths;
|
|
461
377
|
if (!outcome || !paths) return;
|
|
462
378
|
if (outcome.reason) { model.notify(outcome.reason, "error"); return; }
|
|
463
379
|
if (outcome.noop) return;
|
|
464
380
|
model.move(paths, outcome.listPath, outcome.index);
|
|
465
381
|
focusPrimary();
|
|
466
382
|
}
|
|
467
|
-
function endDrag() {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
drag = null;
|
|
471
|
-
pending = null;
|
|
472
|
-
drop.hidden = true;
|
|
383
|
+
function endDrag(drag) {
|
|
384
|
+
drag.ghost.remove();
|
|
385
|
+
marks.hideDrop();
|
|
473
386
|
delete root.dataset.dragging;
|
|
474
|
-
|
|
387
|
+
marks.into(layer, null);
|
|
475
388
|
render();
|
|
476
389
|
}
|
|
477
|
-
function cancelDrag() { if (drag) endDrag(); }
|
|
478
|
-
function dragKey(event) { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); cancelDrag(); model.notify("Move cancelled."); } }
|
|
479
390
|
|
|
480
391
|
// --- dividers ------------------------------------------------------------------------------------------------------------------
|
|
481
392
|
function startResize(event, node) {
|
|
@@ -483,52 +394,44 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
483
394
|
if (!info?.draggable || model.readOnly || panMode() || (event.pointerType === "mouse" && event.button !== 0)) return;
|
|
484
395
|
event.preventDefault();
|
|
485
396
|
event.stopPropagation();
|
|
486
|
-
|
|
397
|
+
// One gesture through the adapter: each move previews a draft record, the release commits one step, Escape or a cancel restores.
|
|
398
|
+
if (!adapter.begin("Resized")) return;
|
|
487
399
|
const startRecord = model.record;
|
|
488
400
|
divider = { key: node.dataset.key, info, startRecord, node, id: event.pointerId };
|
|
489
401
|
node.setPointerCapture?.(event.pointerId);
|
|
490
402
|
node.focus({ preventScroll: true });
|
|
491
403
|
node.dataset.active = "true";
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
divider = null;
|
|
514
|
-
if (commit) model.endGesture(label ? `Resized to ${label}` : "Resized"); else model.cancelGesture();
|
|
515
|
-
node.dataset.active = "false";
|
|
516
|
-
};
|
|
517
|
-
const onUp = () => finish(true);
|
|
518
|
-
const onCancel = () => finish(false);
|
|
519
|
-
const onKey = (key) => { if (key.key === "Escape") { key.preventDefault(); key.stopPropagation(); finish(false); } };
|
|
520
|
-
node.addEventListener("pointermove", onMove);
|
|
521
|
-
node.addEventListener("pointerup", onUp);
|
|
522
|
-
node.addEventListener("pointercancel", onCancel);
|
|
523
|
-
doc.addEventListener("keydown", onKey, true);
|
|
404
|
+
trackPointer(node, {
|
|
405
|
+
doc,
|
|
406
|
+
onMove(move) {
|
|
407
|
+
const p = unit(move);
|
|
408
|
+
const along = info.direction === "row" ? p.x : p.y;
|
|
409
|
+
const fraction = (along - info.pairStart) / info.pairLength;
|
|
410
|
+
const sizes = resizeSizes(startRecord, info.listPath, info.index, fraction, { free: move.shiftKey });
|
|
411
|
+
if (!sizes) return;
|
|
412
|
+
const next = setPairSizes(startRecord, info.listPath, info.index, sizes.a, sizes.b).record;
|
|
413
|
+
adapter.preview(next, `Resize ${sizes.label}`);
|
|
414
|
+
marks.showRatio(sizes.label, { x: info.x + info.w / 2, y: info.y + info.h / 2 });
|
|
415
|
+
divider.label = sizes.label;
|
|
416
|
+
},
|
|
417
|
+
onEnd(commit) {
|
|
418
|
+
marks.hideRatio();
|
|
419
|
+
const label = divider?.label;
|
|
420
|
+
divider = null;
|
|
421
|
+
if (commit) adapter.commit(label ? `Resized to ${label}` : "Resized"); else adapter.cancel();
|
|
422
|
+
node.dataset.active = "false";
|
|
423
|
+
},
|
|
424
|
+
});
|
|
524
425
|
}
|
|
525
426
|
function dividerKey(event, node) {
|
|
526
427
|
const info = node._info;
|
|
527
|
-
if (!info?.draggable
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
428
|
+
if (!info?.draggable) return;
|
|
429
|
+
const action = dividerKeyAction(event, info.direction === "row" ? "vertical" : "horizontal");
|
|
430
|
+
if (!action) return;
|
|
431
|
+
event.preventDefault();
|
|
432
|
+
event.stopPropagation();
|
|
433
|
+
if (action.type === "nudge") model.nudge(info.listPath, info.index, action.step, action.big);
|
|
434
|
+
else model.equalize(info.listPath, info.index);
|
|
532
435
|
}
|
|
533
436
|
|
|
534
437
|
// --- context menu ----------------------------------------------------------------------------------------------------------------
|
|
@@ -600,24 +503,11 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
600
503
|
});
|
|
601
504
|
}
|
|
602
505
|
|
|
603
|
-
// --- zoom
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
root.dataset.zoomed = String(zoom !== 1);
|
|
609
|
-
zoomLabel.textContent = `${Math.round(zoom * 100)}%`;
|
|
610
|
-
}
|
|
611
|
-
const stepZoom = (direction) => {
|
|
612
|
-
const at = ZOOMS.findIndex((value) => value >= zoom - 1e-9);
|
|
613
|
-
const index = direction > 0 ? (ZOOMS[at] > zoom + 1e-9 ? at : at + 1) : at - 1;
|
|
614
|
-
setZoom(ZOOMS[Math.max(0, Math.min(ZOOMS.length - 1, index))]);
|
|
615
|
-
};
|
|
616
|
-
zoomOut.addEventListener("click", () => stepZoom(-1));
|
|
617
|
-
zoomIn.addEventListener("click", () => stepZoom(1));
|
|
618
|
-
zoomFit.addEventListener("click", () => setZoom(1));
|
|
619
|
-
viewport.addEventListener("wheel", (event) => { if (event.ctrlKey || event.metaKey) { event.preventDefault(); stepZoom(event.deltaY < 0 ? 1 : -1); } }, { passive: false });
|
|
620
|
-
setZoom(1);
|
|
506
|
+
// --- zoom (the kit viewport: Ctrl or Cmd with the wheel steps it; Fit is 100%, the frame fitted to the canvas) -----------------------
|
|
507
|
+
const setZoom = (value) => { viewportKit.setZoom(value); };
|
|
508
|
+
zoomOut.addEventListener("click", () => viewportKit.step(-1));
|
|
509
|
+
zoomIn.addEventListener("click", () => viewportKit.step(1));
|
|
510
|
+
zoomFit.addEventListener("click", () => viewportKit.fit());
|
|
621
511
|
|
|
622
512
|
// --- preview with the host's renderer ---------------------------------------------------------------------------------------------
|
|
623
513
|
function schedulePreview() {
|
|
@@ -667,26 +557,25 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
667
557
|
content.addEventListener("mousedown", (event) => { if (!event.target.closest?.(".opf-lx-rename")) event.preventDefault(); });
|
|
668
558
|
content.addEventListener("focusin", (event) => {
|
|
669
559
|
const node = event.target.closest?.(".opf-lx-node");
|
|
670
|
-
if (node && node.dataset.path !== undefined && !pending && !
|
|
560
|
+
if (node && node.dataset.path !== undefined && !dragger.pending && !dragger.active) {
|
|
671
561
|
const path = node.dataset.path.split(".").map(Number);
|
|
672
562
|
if (!isSelected(path)) model.select([path]);
|
|
673
563
|
}
|
|
674
564
|
});
|
|
675
565
|
root.addEventListener("keydown", (event) => {
|
|
676
|
-
if (event.target === renameInput || event
|
|
566
|
+
if (event.target === renameInput || isHandled(event)) return;
|
|
677
567
|
if (event.key === " " && !isTypingTarget(event.target) && (event.target === frame || event.target.closest?.(".opf-lx-node, .opf-lx-viewport")) && !event.repeat) {
|
|
678
568
|
event.preventDefault();
|
|
679
|
-
|
|
680
|
-
root.dataset.space = "true";
|
|
569
|
+
viewportKit.holdSpace(true);
|
|
681
570
|
return;
|
|
682
571
|
}
|
|
683
572
|
if (handleGlobalShortcut(event, model)) return;
|
|
684
573
|
if (isTypingTarget(event.target)) return;
|
|
685
574
|
if (event.key === "Escape") {
|
|
686
575
|
event.preventDefault();
|
|
687
|
-
event
|
|
576
|
+
markHandled(event);
|
|
688
577
|
if (menu) { menu.close(); return; }
|
|
689
|
-
if (
|
|
578
|
+
if (dragger.active) { dragger.cancel(); return; }
|
|
690
579
|
if (model.tool !== "select") { model.setTool("select"); return; }
|
|
691
580
|
model.selectParent();
|
|
692
581
|
focusPrimary();
|
|
@@ -717,8 +606,8 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
717
606
|
openContextMenu({ x: box.left + (at ? (at.x + at.w / 2) * box.width : 40), y: box.top + (at ? (at.y + at.h / 2) * box.height : 40) }, node ?? frame);
|
|
718
607
|
}
|
|
719
608
|
});
|
|
720
|
-
root.addEventListener("keyup", (event) => { if (event.key === " "
|
|
721
|
-
root.addEventListener("focusout", () => {
|
|
609
|
+
root.addEventListener("keyup", (event) => { if (event.key === " ") viewportKit.holdSpace(false); });
|
|
610
|
+
root.addEventListener("focusout", () => { viewportKit.holdSpace(false); });
|
|
722
611
|
|
|
723
612
|
const unsubscribe = model.subscribe((event) => {
|
|
724
613
|
if (event.type === "notice") { paintStatus(); return; }
|
|
@@ -732,13 +621,14 @@ export function createLayoutCanvas(container, options = {}) {
|
|
|
732
621
|
model,
|
|
733
622
|
refresh: render,
|
|
734
623
|
setZoom,
|
|
735
|
-
getZoom: () => zoom,
|
|
624
|
+
getZoom: () => viewportKit.zoom,
|
|
736
625
|
focus() { focusPrimary(); },
|
|
737
626
|
destroy() {
|
|
738
627
|
unsubscribe();
|
|
739
628
|
win.clearTimeout(previewTimer);
|
|
740
629
|
previewToken++;
|
|
741
|
-
|
|
630
|
+
dragger.cancel();
|
|
631
|
+
viewportKit.destroy();
|
|
742
632
|
menu?.close({ restore: false });
|
|
743
633
|
headingMenu?.close({ restore: false });
|
|
744
634
|
root.remove();
|