@openpresentation/opf-editor 0.28.0 → 0.29.1
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 +20 -6
- package/dist/canvas.d.ts +13 -1
- package/dist/canvas.js +39 -9
- package/dist/chart-options-panel.js +16 -16
- package/dist/codemirror-guard.js +69 -0
- package/dist/content-controls.js +127 -74
- package/dist/data-grid.js +15 -7
- package/dist/design-controls.d.ts +13 -0
- package/dist/design-controls.js +134 -124
- package/dist/exact-source.d.ts +30 -0
- package/dist/font-gate.js +20 -1
- package/dist/json-editor.js +8 -2
- package/dist/json-options.d.ts +2 -1
- package/dist/json-options.js +7 -6
- package/dist/layout-templates.d.ts +8 -3
- package/dist/layout-templates.js +30 -16
- package/dist/numbering-panel.js +2 -3
- package/dist/react.d.ts +12 -4
- package/dist/react.js +39 -5
- package/dist/slide-adapter.d.ts +49 -0
- package/dist/slide-adapter.js +289 -0
- package/dist/slide-edits.d.ts +46 -0
- package/dist/slide-edits.js +629 -0
- package/dist/slide-manager.js +2 -1
- package/dist/slide-scene.d.ts +96 -0
- package/dist/slide-scene.js +413 -0
- package/dist/source-checker.js +45 -6
- package/dist/source-completions.js +6 -4
- package/dist/source-pane.d.ts +18 -6
- package/dist/source-pane.js +45 -0
- package/dist/source-worker.js +8 -4
- package/dist/studio-canvas.d.ts +34 -0
- package/dist/studio-canvas.js +586 -0
- package/dist/studio-commands.d.ts +15 -0
- package/dist/studio-commands.js +155 -0
- package/dist/studio-design.d.ts +16 -0
- package/dist/studio-design.js +354 -0
- package/dist/studio-kit/dom.d.ts +16 -0
- package/dist/studio-kit/dom.js +42 -6
- package/dist/studio-layers.d.ts +12 -0
- package/dist/studio-layers.js +218 -0
- package/dist/studio.d.ts +31 -0
- package/dist/studio.js +1608 -0
- package/dist/styles.css +1546 -117
- package/dist/thumbnail-cache.d.ts +2 -0
- package/dist/thumbnail-cache.js +10 -1
- package/dist/workspace-measure.js +6 -4
- package/dist/workspace-parts.js +20 -0
- package/dist/workspace-shell.js +25 -15
- package/dist/workspace-theme.js +3 -0
- package/dist/workspace.d.ts +97 -9
- package/dist/workspace.js +325 -59
- package/package.json +29 -18
package/dist/studio-kit/dom.js
CHANGED
|
@@ -41,6 +41,21 @@ export const ICONS = {
|
|
|
41
41
|
Hash: '<line x1="4" x2="20" y1="9" y2="9"/><line x1="4" x2="20" y1="15" y2="15"/><line x1="10" x2="8" y1="3" y2="21"/><line x1="16" x2="14" y1="3" y2="21"/>',
|
|
42
42
|
Quote: '<path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/><path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/>',
|
|
43
43
|
GitCommitHorizontal: '<circle cx="12" cy="12" r="3"/><line x1="3" x2="9" y1="12" y2="12"/><line x1="15" x2="21" y1="12" y2="12"/>',
|
|
44
|
+
Video: '<path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"/><rect x="2" y="6" width="14" height="12" rx="2"/>',
|
|
45
|
+
Gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
|
|
46
|
+
CalendarRange: '<rect width="18" height="18" x="3" y="4" rx="2"/><path d="M16 2v4"/><path d="M3 10h18"/><path d="M8 2v4"/><path d="M17 14h-6"/><path d="M13 18H7"/><path d="M7 14h.01"/><path d="M17 18h.01"/>',
|
|
47
|
+
Type: '<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/>',
|
|
48
|
+
Menu: '<line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/>',
|
|
49
|
+
FileCode: '<path d="M10 12.5 8 15l2 2.5"/><path d="m14 12.5 2 2.5-2 2.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/>',
|
|
50
|
+
PanelLeft: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/>',
|
|
51
|
+
Presentation: '<path d="M2 3h20"/><path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/><path d="m7 21 5-5 5 5"/>',
|
|
52
|
+
Keyboard: '<path d="M10 8h.01"/><path d="M12 12h.01"/><path d="M14 8h.01"/><path d="M16 12h.01"/><path d="M18 8h.01"/><path d="M6 8h.01"/><path d="M7 16h10"/><path d="M8 12h.01"/><rect width="20" height="16" x="2" y="4" rx="2"/>',
|
|
53
|
+
EyeOff: '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>',
|
|
54
|
+
Ungroup: '<rect width="8" height="6" x="5" y="4" rx="1"/><rect width="8" height="6" x="11" y="14" rx="1"/>',
|
|
55
|
+
ArrowUp: '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>',
|
|
56
|
+
IndentIncrease: '<path d="M21 12H11"/><path d="M21 18H11"/><path d="M21 6H11"/><path d="m3 8 4 4-4 4"/>',
|
|
57
|
+
IndentDecrease: '<path d="M21 12H11"/><path d="M21 18H11"/><path d="M21 6H11"/><path d="m7 8-4 4 4 4"/>',
|
|
58
|
+
ArrowDown: '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>',
|
|
44
59
|
Group: '<path d="M3 7V5c0-1.1.9-2 2-2h2"/><path d="M17 3h2c1.1 0 2 .9 2 2v2"/><path d="M21 17v2c0 1.1-.9 2-2 2h-2"/><path d="M7 21H5c-1.1 0-2-.9-2-2v-2"/><rect width="7" height="5" x="7" y="7" rx="1"/><rect width="7" height="5" x="10" y="12" rx="1"/>',
|
|
45
60
|
};
|
|
46
61
|
|
|
@@ -114,20 +129,41 @@ export const pct = (fraction) => `${Math.round(fraction * 10000) / 100}%`;
|
|
|
114
129
|
export function openMenu(dom, host, { x, y, items, label, returnFocus, onClose, className = "" }) {
|
|
115
130
|
const p = dom.prefix;
|
|
116
131
|
const menu = dom.el("div", `${p}-menu ${className}`.trim(), { role: "menu", "aria-label": label });
|
|
117
|
-
|
|
132
|
+
// CE-20: items may carry an `icon` (a Lucide name), a `media` element (a layout diagram), a `description` (a second line), a `title` (why
|
|
133
|
+
// it is disabled), `pointerdown(event, item)` (an item that can also be dragged out of the menu) and a `group` name: consecutive items of
|
|
134
|
+
// one group sit in a `role="group"` under a visible heading. `{ separator: true }` draws a separator.
|
|
135
|
+
let parent = menu, groupName;
|
|
136
|
+
const buttons = [];
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
if (item.separator) { menu.append(dom.el("div", `${p}-menu-separator`, { role: "separator" })); parent = menu; groupName = undefined; continue; }
|
|
139
|
+
if (item.group !== groupName) {
|
|
140
|
+
groupName = item.group;
|
|
141
|
+
if (groupName) {
|
|
142
|
+
parent = dom.el("div", `${p}-menu-group`, { role: "group", "aria-label": groupName });
|
|
143
|
+
parent.append(dom.el("div", `${p}-menu-heading`, { "aria-hidden": "true", text: groupName }));
|
|
144
|
+
menu.append(parent);
|
|
145
|
+
} else parent = menu;
|
|
146
|
+
}
|
|
118
147
|
const role = item.role ?? "menuitem";
|
|
119
|
-
const node = dom.el("button", `${p}-menuitem`, { type: "button", role, tabindex: "-1", "aria-disabled": item.disabled ? "true" : undefined, "aria-checked": role === "menuitemcheckbox" ? String(Boolean(item.checked)) : undefined });
|
|
120
|
-
node.append(
|
|
148
|
+
const node = dom.el("button", `${p}-menuitem`, { type: "button", role, tabindex: "-1", title: item.title, "aria-disabled": item.disabled ? "true" : undefined, "aria-checked": role === "menuitemcheckbox" ? String(Boolean(item.checked)) : undefined, "aria-keyshortcuts": item.keyshortcuts });
|
|
149
|
+
if (item.media) node.append(item.media);
|
|
150
|
+
else if (item.icon) node.append(dom.icon(item.icon, 16));
|
|
151
|
+
if (item.description) {
|
|
152
|
+
const text = dom.el("span", `${p}-menu-text`);
|
|
153
|
+
text.append(dom.el("span", `${p}-menu-label`, { text: item.label }), dom.el("span", `${p}-menu-description`, { text: item.description }));
|
|
154
|
+
node.append(text);
|
|
155
|
+
} else node.append(dom.el("span", `${p}-menu-label`, { text: item.label }));
|
|
121
156
|
if (item.hint) node.append(dom.el("span", `${p}-menu-hint`, { text: item.hint }));
|
|
122
157
|
if (role === "menuitemcheckbox") node.prepend(dom.icon(item.checked ? "Check" : "Square", 14));
|
|
158
|
+
if (item.pointerdown) node.addEventListener("pointerdown", (event) => item.pointerdown(event, item, { close: () => close({ restore: false }) }));
|
|
123
159
|
node.addEventListener("click", () => {
|
|
124
160
|
if (item.disabled) return;
|
|
125
161
|
close();
|
|
126
162
|
item.onSelect?.();
|
|
127
163
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
164
|
+
parent.append(node);
|
|
165
|
+
buttons.push(node);
|
|
166
|
+
}
|
|
131
167
|
let closed = false;
|
|
132
168
|
const outside = (event) => { if (!menu.contains(event.target)) close({ restore: false }); };
|
|
133
169
|
function close({ restore = true } = {}) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** CE-20 (internal): the Studio's layers tree over the current slide's scene. */
|
|
2
|
+
import type { KitDom } from "./studio-kit/dom.js";
|
|
3
|
+
import type { SlideAdapter } from "./slide-adapter.js";
|
|
4
|
+
|
|
5
|
+
export interface StudioLayers {
|
|
6
|
+
element: HTMLElement;
|
|
7
|
+
render(): void;
|
|
8
|
+
paintSelection(options?: { focus?: boolean }): void;
|
|
9
|
+
focus(): void;
|
|
10
|
+
destroy(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function createStudioLayers(container: HTMLElement, options: { dom: KitDom; adapter: SlideAdapter; studio: any }): StudioLayers;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// CE-20: the Studio's layers panel, the current slide's scene as a tree (the WAI-ARIA tree pattern from the kit: `role="tree"`, a roving tab
|
|
2
|
+
// stop, `aria-level`, `aria-setsize`, `aria-posinset`, `aria-selected`, `aria-expanded`). A click selects, Shift+click selects a range of
|
|
3
|
+
// siblings, Cmd or Ctrl+click toggles one; the arrow keys move and expand or collapse; Enter goes to the canvas with that node selected; F2 edits
|
|
4
|
+
// a text node; a drag reorders or moves into a container with the canvas's trial and reason. One selection for every surface: the Studio
|
|
5
|
+
// owns it, this only draws it and asks for changes.
|
|
6
|
+
import { hitRow, rovingTabStop, rowDropMode, showTreeDrop, treeItem, treeKeyAction } from "./studio-kit/tree.js";
|
|
7
|
+
import { isTypingTarget } from "./studio-kit/dom.js";
|
|
8
|
+
|
|
9
|
+
const ICON_OF = { slide: "Square", heading: "Heading", region: "Columns2", group: "Group", implicit: "AlignLeft", part: "GitCommitHorizontal" };
|
|
10
|
+
const TYPE_ICON = { text: "AlignLeft", list: "List", image: "Image", video: "Video", chart: "ChartColumn", table: "Table", code: "Code", metric: "Hash", quote: "Quote", timeline: "CalendarRange" };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mount the layers tree in `container`. `adapter` is the slide adapter, `studio` the controller. Returns `{ element, render, paintSelection,
|
|
14
|
+
* focus, destroy }`.
|
|
15
|
+
*/
|
|
16
|
+
export function createStudioLayers(container, { dom, adapter, studio }) {
|
|
17
|
+
const doc = container.ownerDocument;
|
|
18
|
+
const { el } = dom;
|
|
19
|
+
const p = dom.prefix;
|
|
20
|
+
const scroller = el("div", `${p}-layers-scroll`);
|
|
21
|
+
const tree = el("ul", `${p}-tree`, { role: "tree", "aria-label": "Layers", "aria-multiselectable": "true" });
|
|
22
|
+
const mark = el("div", `${p}-tree-drop`, { "aria-hidden": "true" });
|
|
23
|
+
mark.hidden = true;
|
|
24
|
+
scroller.append(tree, mark);
|
|
25
|
+
container.append(scroller);
|
|
26
|
+
const collapsed = new Set();
|
|
27
|
+
let rows = [];
|
|
28
|
+
let focusKey = null;
|
|
29
|
+
let drag = null;
|
|
30
|
+
let lastScene = null;
|
|
31
|
+
|
|
32
|
+
const scene = () => adapter.scene();
|
|
33
|
+
const visibleChildren = (s, key) => s.children(key).filter((node) => node.kind !== "part");
|
|
34
|
+
|
|
35
|
+
function render() {
|
|
36
|
+
const s = scene();
|
|
37
|
+
lastScene = s;
|
|
38
|
+
const hadFocus = tree.contains(doc.activeElement);
|
|
39
|
+
const selected = new Set(studio.selectedKeys());
|
|
40
|
+
const items = [];
|
|
41
|
+
rows = [];
|
|
42
|
+
const add = (node, level, setsize, posinset) => {
|
|
43
|
+
const children = visibleChildren(s, node.key);
|
|
44
|
+
const expandable = children.length > 0;
|
|
45
|
+
const expanded = expandable && !collapsed.has(node.key);
|
|
46
|
+
const li = treeItem(dom, { level, selected: selected.has(node.key), expanded: expandable ? expanded : undefined, setsize: level > 1 ? setsize : undefined, posinset: level > 1 ? posinset : undefined, attributes: { "data-key": node.key, "data-kind": node.kind }, className: `${p}-row` });
|
|
47
|
+
const label = el("span", `${p}-row-label`);
|
|
48
|
+
label.style.setProperty("--opf-studio-level", String(level - 1));
|
|
49
|
+
if (expandable) {
|
|
50
|
+
const caret = el("span", `${p}-caret`, { "aria-hidden": "true" });
|
|
51
|
+
caret.append(dom.icon(expanded ? "ChevronDown" : "ChevronRight", 14));
|
|
52
|
+
caret.addEventListener("click", (event) => { event.stopPropagation(); toggle(node.key); });
|
|
53
|
+
label.append(caret);
|
|
54
|
+
} else label.append(el("span", `${p}-caret`, { "aria-hidden": "true" }));
|
|
55
|
+
label.append(dom.icon(node.kind === "block" || node.kind === "implicit" ? TYPE_ICON[node.type] ?? "Square" : ICON_OF[node.kind] ?? "Square", 14));
|
|
56
|
+
label.append(el("span", `${p}-row-text`, { text: node.kind === "slide" ? studio.slideLabel(node) : node.label }));
|
|
57
|
+
if (node.findings.length) label.append(el("span", `${p}-row-badge`, { text: String(node.findings.length), title: node.findings.map((finding) => finding.message).join("\n"), "aria-label": `${node.findings.length} finding${node.findings.length === 1 ? "" : "s"}` }));
|
|
58
|
+
if (node.hidden) { const hidden = el("span", `${p}-row-hidden`, { title: "Hidden slide" }); hidden.append(dom.icon("EyeOff", 14)); label.append(hidden); }
|
|
59
|
+
li.append(label);
|
|
60
|
+
items.push(li);
|
|
61
|
+
rows.push({ row: li, key: node.key, node, level, expandable, expanded });
|
|
62
|
+
if (expanded) {
|
|
63
|
+
const group = el("ul", `${p}-tree-group`, { role: "group" });
|
|
64
|
+
li.append(group);
|
|
65
|
+
const parentItems = items.length;
|
|
66
|
+
children.forEach((child, index) => add(child, level + 1, children.length, index + 1));
|
|
67
|
+
group.append(...items.splice(parentItems));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const slide = s.node(s.slideKey);
|
|
71
|
+
add(slide, 1, 1, 1);
|
|
72
|
+
// The Background pseudo-row: it selects the slide and shows the Design panel's background section.
|
|
73
|
+
const background = treeItem(dom, { level: 2, selected: false, setsize: visibleChildren(s, s.slideKey).length + 1, posinset: visibleChildren(s, s.slideKey).length + 1, attributes: { "data-key": "#background", "data-kind": "background" }, className: `${p}-row` });
|
|
74
|
+
const backgroundLabel = el("span", `${p}-row-label`);
|
|
75
|
+
backgroundLabel.style.setProperty("--opf-studio-level", "1");
|
|
76
|
+
backgroundLabel.append(el("span", `${p}-caret`, { "aria-hidden": "true" }), dom.icon("Image", 14), el("span", `${p}-row-text`, { text: "Background" }));
|
|
77
|
+
background.append(backgroundLabel);
|
|
78
|
+
rows.push({ row: background, key: "#background", node: null, level: 2, expandable: false, expanded: false });
|
|
79
|
+
const slideGroup = items[0]?.querySelector(":scope > ul");
|
|
80
|
+
if (slideGroup) slideGroup.append(background);
|
|
81
|
+
else { const group = el("ul", `${p}-tree-group`, { role: "group" }); group.append(background); items[0]?.append(group); }
|
|
82
|
+
tree.replaceChildren(...items);
|
|
83
|
+
paintSelection({ focus: hadFocus });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Mark the selection and the tab stop without rebuilding (`focus`: move focus to the stop). */
|
|
87
|
+
function paintSelection({ focus = false } = {}) {
|
|
88
|
+
if (scene() !== lastScene) { render(); return; }
|
|
89
|
+
const selected = new Set(studio.selectedKeys());
|
|
90
|
+
const primary = studio.primaryKey();
|
|
91
|
+
for (const entry of rows) entry.row.setAttribute("aria-selected", String(entry.key !== "#background" && selected.has(entry.key)));
|
|
92
|
+
const stop = (focus ? null : rows.find((entry) => entry.key === (focusKey && tree.contains(doc.activeElement) ? focusKey : primary))) ?? rows.find((entry) => entry.key === primary) ?? rows[0];
|
|
93
|
+
if (focus && stop) focusKey = stop.key;
|
|
94
|
+
rovingTabStop(rows.map((entry) => entry.row), stop?.row);
|
|
95
|
+
if (focus && stop) stop.row.focus({ preventScroll: false });
|
|
96
|
+
else if (stop && !tree.contains(doc.activeElement)) stop.row.scrollIntoView?.({ block: "nearest" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toggle(key) {
|
|
100
|
+
if (collapsed.has(key)) collapsed.delete(key); else collapsed.add(key);
|
|
101
|
+
focusKey = key;
|
|
102
|
+
render();
|
|
103
|
+
rows.find((entry) => entry.key === key)?.row.focus();
|
|
104
|
+
}
|
|
105
|
+
const rowOf = (target) => rows.find((entry) => entry.row === target.closest?.(`.${p}-row`));
|
|
106
|
+
|
|
107
|
+
function activate(entry, event) {
|
|
108
|
+
if (entry.key === "#background") { studio.select([scene().slideKey], { source: "layers" }); studio.showDesignSection("background"); return; }
|
|
109
|
+
if (event.shiftKey) studio.selectRange(entry.key, { source: "layers" });
|
|
110
|
+
else if (event.metaKey || event.ctrlKey) studio.selectAdd(entry.key, { source: "layers" });
|
|
111
|
+
else studio.select([entry.key], { source: "layers" });
|
|
112
|
+
}
|
|
113
|
+
tree.addEventListener("click", (event) => {
|
|
114
|
+
const entry = rowOf(event.target);
|
|
115
|
+
if (!entry || drag?.moved) return;
|
|
116
|
+
focusKey = entry.key;
|
|
117
|
+
activate(entry, event);
|
|
118
|
+
entry.row.focus({ preventScroll: true });
|
|
119
|
+
});
|
|
120
|
+
tree.addEventListener("focusin", (event) => { const entry = rowOf(event.target); if (entry) focusKey = entry.key; });
|
|
121
|
+
tree.addEventListener("keydown", (event) => {
|
|
122
|
+
if (isTypingTarget(event.target)) return;
|
|
123
|
+
const entry = rowOf(event.target);
|
|
124
|
+
if (!entry) return;
|
|
125
|
+
const visible = rows.map((item) => item.row);
|
|
126
|
+
const parentRow = entry.node?.parent ? rows.find((item) => item.key === entry.node.parent)?.row : entry.key === "#background" ? rows[0]?.row : null;
|
|
127
|
+
const firstChild = entry.expanded ? rows[rows.indexOf(entry) + 1]?.row : null;
|
|
128
|
+
if (event.key === "F2" && entry.node) { event.preventDefault(); studio.select([entry.key], { source: "layers" }); studio.beginTextEdit(entry.key); return; }
|
|
129
|
+
if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey) {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
event.stopPropagation();
|
|
132
|
+
activate(entry, {});
|
|
133
|
+
studio.focusCanvas();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if ((event.key === "ContextMenu" || (event.key === "F10" && event.shiftKey)) && entry.node) {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
if (!studio.selectedKeys().includes(entry.key)) studio.select([entry.key], { source: "layers" });
|
|
139
|
+
const box = entry.row.getBoundingClientRect();
|
|
140
|
+
studio.openContextMenu({ x: box.left + 24, y: box.bottom }, { returnFocus: entry.row });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const action = treeKeyAction(event, entry.row, visible, { expandable: entry.expandable, expanded: entry.expanded, collapsible: entry.expandable, parent: parentRow, firstChild, menu: false });
|
|
144
|
+
if (!action || action.type === "activate" || action.type === "menu") {
|
|
145
|
+
if (event.key === " ") { event.preventDefault(); activate(entry, event); }
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
event.stopPropagation();
|
|
150
|
+
if (action.type === "expand" || action.type === "collapse") { toggle(entry.key); return; }
|
|
151
|
+
const target = rows.find((item) => item.row === action.target);
|
|
152
|
+
if (!target) return;
|
|
153
|
+
focusKey = target.key;
|
|
154
|
+
rovingTabStop(visible, target.row);
|
|
155
|
+
target.row.focus();
|
|
156
|
+
if (target.key !== "#background" && !event.shiftKey) studio.select([target.key], { source: "layers" });
|
|
157
|
+
else if (event.shiftKey && target.key !== "#background") studio.selectRange(target.key, { source: "layers" });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// --- drag to reorder or move into a container -------------------------------------------------------------------------------------------
|
|
161
|
+
tree.addEventListener("pointerdown", (event) => {
|
|
162
|
+
const entry = rowOf(event.target);
|
|
163
|
+
if (!entry?.node?.can?.move || studio.readOnly || (event.pointerType === "mouse" && event.button !== 0)) return;
|
|
164
|
+
drag = { entry, x: event.clientX, y: event.clientY, id: event.pointerId, moved: false, drop: null };
|
|
165
|
+
});
|
|
166
|
+
const dropFor = (clientY) => {
|
|
167
|
+
const target = hitRow(rows.filter((item) => item.node), clientY);
|
|
168
|
+
if (!target) return null;
|
|
169
|
+
const box = target.row.querySelector(`.${p}-row-label`).getBoundingClientRect();
|
|
170
|
+
const node = target.node;
|
|
171
|
+
const container = node.kind === "slide" || node.kind === "group" || node.kind === "region";
|
|
172
|
+
const mode = rowDropMode((clientY - box.top) / Math.max(1, box.height), { root: node.kind === "slide", container });
|
|
173
|
+
const keys = studio.selectedKeys().includes(drag.entry.key) ? studio.selectedKeys() : [drag.entry.key];
|
|
174
|
+
let targetKey, index;
|
|
175
|
+
if (mode === "into") { targetKey = node.key; index = scene().contentChildren(node.key).length; }
|
|
176
|
+
else {
|
|
177
|
+
if (!node.parent) return null;
|
|
178
|
+
targetKey = node.parent;
|
|
179
|
+
const list = scene().contentChildren(node.parent);
|
|
180
|
+
index = list.findIndex((child) => child.key === node.key) + (mode === "after" ? 1 : 0);
|
|
181
|
+
if (index < 0) return null;
|
|
182
|
+
}
|
|
183
|
+
const trial = adapter.trialMove(keys, targetKey, index);
|
|
184
|
+
return { keys, target: targetKey, index, mode, box, ...trial };
|
|
185
|
+
};
|
|
186
|
+
tree.addEventListener("pointermove", (event) => {
|
|
187
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
188
|
+
if (!drag.moved && Math.hypot(event.clientX - drag.x, event.clientY - drag.y) < 4) return;
|
|
189
|
+
if (!drag.moved) { drag.moved = true; try { tree.setPointerCapture?.(event.pointerId); } catch { /* a pointer that cannot be captured (a synthetic one) */ } if (!studio.selectedKeys().includes(drag.entry.key)) studio.select([drag.entry.key], { source: "layers" }); }
|
|
190
|
+
drag.drop = dropFor(event.clientY);
|
|
191
|
+
showTreeDrop(mark, scroller, drag.drop ? { mode: drag.drop.mode, state: drag.drop.ok ? (drag.drop.noop ? "same" : "ok") : "refused", reason: drag.drop.reason, box: drag.drop.box } : null);
|
|
192
|
+
if (drag.drop && !drag.drop.ok) studio.status(drag.drop.reason);
|
|
193
|
+
});
|
|
194
|
+
const endDrag = (commit) => {
|
|
195
|
+
const session = drag;
|
|
196
|
+
drag = null;
|
|
197
|
+
showTreeDrop(mark, scroller, null);
|
|
198
|
+
if (!session?.moved || !commit) return;
|
|
199
|
+
// The click that follows a drag must not select the row under the pointer.
|
|
200
|
+
drag = { moved: true };
|
|
201
|
+
globalThis.setTimeout(() => { if (drag?.moved && !drag.entry) drag = null; }, 0);
|
|
202
|
+
const found = session.drop;
|
|
203
|
+
if (!found) return;
|
|
204
|
+
if (!found.ok) { studio.announce(found.reason); return; }
|
|
205
|
+
if (!found.noop) studio.move(found.keys, found.target, found.index);
|
|
206
|
+
};
|
|
207
|
+
tree.addEventListener("pointerup", () => endDrag(true));
|
|
208
|
+
tree.addEventListener("pointercancel", () => endDrag(false));
|
|
209
|
+
tree.addEventListener("keydown", (event) => { if (event.key === "Escape" && drag?.moved) { event.preventDefault(); event.stopPropagation(); endDrag(false); studio.announce("Move cancelled."); } }, true);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
element: scroller,
|
|
213
|
+
render,
|
|
214
|
+
paintSelection,
|
|
215
|
+
focus() { (rows.find((entry) => entry.row.tabIndex === 0) ?? rows[0])?.row.focus(); },
|
|
216
|
+
destroy() { scroller.remove(); },
|
|
217
|
+
};
|
|
218
|
+
}
|
package/dist/studio.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CE-20 (internal): the Studio's shell and controller. Hosts use `createOpfEditor(el, { layout: "studio" })` from
|
|
3
|
+
* `@openpresentation/opf-editor/workspace`; the workspace calls these.
|
|
4
|
+
*/
|
|
5
|
+
import type { KitDom } from "./studio-kit/dom.js";
|
|
6
|
+
import type { OpfCommand, OpfCommandInfo, OpfEditorSlots, OpfStudioOptions, OpfStudioSelectionItem } from "./workspace.js";
|
|
7
|
+
import type { SlideAdapter } from "./slide-adapter.js";
|
|
8
|
+
|
|
9
|
+
export interface StudioShell {
|
|
10
|
+
dom: KitDom;
|
|
11
|
+
[element: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildStudioShell(root: HTMLElement, options: { parts: Record<string, boolean>; layout: "studio" | "playground"; labels?: Record<string, string>; studio?: OpfStudioOptions; slots?: OpfEditorSlots }): { slots: OpfEditorSlots & Record<string, HTMLElement>; controls: { undo: HTMLButtonElement; redo: HTMLButtonElement }; shell: StudioShell };
|
|
14
|
+
export interface Studio {
|
|
15
|
+
readonly layout: "studio" | "playground";
|
|
16
|
+
readonly selection: readonly OpfStudioSelectionItem[];
|
|
17
|
+
readonly zoom: number;
|
|
18
|
+
readonly adapter: SlideAdapter;
|
|
19
|
+
readonly commands: { list(): OpfCommandInfo[]; run(id: string): boolean; register(command: OpfCommand): () => void };
|
|
20
|
+
setLayout(layout: "studio" | "playground"): boolean;
|
|
21
|
+
select(keys: string[], options?: { source?: string }): void;
|
|
22
|
+
selectTargets(keysOrPaths: string | string[]): boolean;
|
|
23
|
+
revealPath(path: string): boolean;
|
|
24
|
+
setZoom(zoom: number): number;
|
|
25
|
+
fit(mode?: "slide" | "width"): number;
|
|
26
|
+
openLayout(layoutId?: string, options?: { slide?: number }): boolean;
|
|
27
|
+
showTab(id: string, options?: { focus?: boolean }): boolean;
|
|
28
|
+
refresh(): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function createStudio(context: Record<string, any>): Studio;
|