@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/content-controls.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Content controls (RR-26): the DOM controls for the content actions, mounted by `createDesignControls` in its
|
|
2
|
-
// "Selected content" and "Slide structure" sections.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// "Selected content" and "Slide structure" sections. The structural actions of a block are one row of icon buttons
|
|
3
|
+
// (each named, with a tooltip); the others are native buttons or selects with a short label. An action runs a dry run
|
|
4
|
+
// through the same prepare function it uses, so before it is applied its tooltip names what it would not carry over,
|
|
5
|
+
// and when it does not apply it is shown disabled (aria-disabled, so it stays focusable) with the reason as its tooltip
|
|
6
|
+
// and accessible description. Each commits one undoable change through the session. Nothing here owns document state:
|
|
7
|
+
// `sync` re-reads the session after every change, Undo and Redo included.
|
|
6
8
|
import { BLOCK_KIND_LABELS, blockConversionTargets, convertBlock, metricGroupForSelection } from "./block-convert.js";
|
|
7
9
|
import {
|
|
8
10
|
listItemIndexForSelection,
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
shiftListItems,
|
|
22
24
|
ungroupBlock,
|
|
23
25
|
} from "./content-actions.js";
|
|
26
|
+
import { ICONS } from "./studio-kit/dom.js";
|
|
24
27
|
|
|
25
28
|
/** Region layouts offered for a slide with this many blocks (each block gets its own region, none overlap). */
|
|
26
29
|
export const REGION_LAYOUTS = Object.freeze({
|
|
@@ -51,34 +54,67 @@ const DESIGN_IMAGE_LABELS = { background: "background image", watermark: "waterm
|
|
|
51
54
|
// An image block's own framing fields (FA-22) do not stop it being "a block that holds only an image".
|
|
52
55
|
const IMAGE_BLOCK_FIELDS = new Set(["id", "extensions", "type", "fit", "focus", "aspectRatio", "shape", "cornerRadius", "border", "opacity", "recolor", "overlay", "placement"]);
|
|
53
56
|
const messageOf = (error) => error?.issues?.[0]?.message ?? error?.message ?? String(error);
|
|
54
|
-
const lossText = (prepared) => (prepared.loss
|
|
57
|
+
const lossText = (prepared) => (prepared.loss?.length ? `Not carried over: ${prepared.loss.join(", ")}.` : "");
|
|
58
|
+
const SVG = "http://www.w3.org/2000/svg";
|
|
59
|
+
|
|
60
|
+
/** An inline Lucide icon (aria-hidden) from the studio kit's set. */
|
|
61
|
+
export function iconNode(doc, name, size = 16) {
|
|
62
|
+
const svg = doc.createElementNS(SVG, "svg");
|
|
63
|
+
for (const [key, value] of Object.entries({ viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", focusable: "false", class: "opf-dc-icon" })) svg.setAttribute(key, String(value));
|
|
64
|
+
svg.innerHTML = ICONS[name] ?? "";
|
|
65
|
+
return svg;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The first sentence of a refusal, for a tooltip: "The first line is longer than 24 characters." */
|
|
69
|
+
export function shortReason(reason) {
|
|
70
|
+
if (!reason) return "";
|
|
71
|
+
const first = String(reason).split(/(?<=[.!?])\s/)[0];
|
|
72
|
+
return first.replace(/, which [^.]*\./, ".");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Show a button as available or not. A disabled action keeps its focus (aria-disabled) so the reason in its tooltip is reachable from the
|
|
77
|
+
* keyboard; the tooltip is the label (with its shortcut) when it is available, and the label and the reason when it is not.
|
|
78
|
+
*/
|
|
79
|
+
export function setActionState(button, { label, enabled, reason, shortcut, loss }) {
|
|
80
|
+
if (enabled) button.removeAttribute("aria-disabled");
|
|
81
|
+
else button.setAttribute("aria-disabled", "true");
|
|
82
|
+
const tip = enabled ? `${label}${shortcut ? ` (${shortcut})` : ""}${loss ? `. ${loss}` : ""}` : `${label}: ${shortReason(reason) || "not available here."}`;
|
|
83
|
+
button.title = tip;
|
|
84
|
+
}
|
|
55
85
|
|
|
56
86
|
/**
|
|
57
|
-
* Build the content controls. `ctx` supplies the panel's helpers: `editor`, `h` (element builder), `selectField`,
|
|
58
|
-
* `run(action, done)` (commit, report, restore on failure), `nextId`, `getSlide()
|
|
59
|
-
*
|
|
87
|
+
* Build the content controls. `ctx` supplies the panel's helpers: `editor`, `doc`, `h` (element builder), `selectField`,
|
|
88
|
+
* `run(action, done)` (commit, report, restore on failure), `nextId`, `getSlide()`, `reselect(path)` and, optionally,
|
|
89
|
+
* `blockActions` (`{ before, after }`: host actions for the arrange toolbar, each `{ role, label, icon, state(), run() }`).
|
|
90
|
+
* Returns the nodes to place and `sync(state)`, where `state` is `{ blockPath, groupPath, content, selectedPath, slideIndex }`
|
|
91
|
+
* (`groupPath` is a selected group, which has no content of its own).
|
|
60
92
|
*/
|
|
61
93
|
export function createContentControls(ctx) {
|
|
62
94
|
const { editor, h, selectField, run, nextId, getSlide, reselect } = ctx;
|
|
63
95
|
const updaters = [];
|
|
64
96
|
const ctxState = { current: {} };
|
|
65
97
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
/**
|
|
99
|
+
* A button for one content action: an icon button in a toolbar when `icon` is given, otherwise a text button. Before it is pressed its
|
|
100
|
+
* tooltip names what the change would not carry over; it is disabled, with the reason as its tooltip, when it does not apply.
|
|
101
|
+
*/
|
|
102
|
+
function action(label, { prepare, apply, done, selectAfter, role, icon }) {
|
|
103
|
+
const button = icon
|
|
104
|
+
? h("button", { type: "button", class: "opf-dc-tool", "aria-label": label, "data-role": role }, iconNode(ctx.doc, icon))
|
|
105
|
+
: h("button", { type: "button", class: "secondary", "data-role": role }, label);
|
|
71
106
|
button.addEventListener("click", () => {
|
|
107
|
+
if (button.getAttribute("aria-disabled") === "true") return;
|
|
72
108
|
run(
|
|
73
109
|
() => {
|
|
74
110
|
const change = apply();
|
|
75
111
|
if (selectAfter) reselect(selectAfter(change));
|
|
76
112
|
return change;
|
|
77
113
|
},
|
|
78
|
-
(change) => `${typeof done === "function" ? done(change) : done} ${lossText(change)}`,
|
|
114
|
+
(change) => `${typeof done === "function" ? done(change) : done}${lossText(change) ? ` ${lossText(change)}` : ""}`,
|
|
79
115
|
);
|
|
80
116
|
});
|
|
81
|
-
const wrap = h("div", { class: "opf-dc-action" }, button
|
|
117
|
+
const wrap = icon ? button : h("div", { class: "opf-dc-action" }, button);
|
|
82
118
|
let current = {};
|
|
83
119
|
const update = (state) => {
|
|
84
120
|
current = state;
|
|
@@ -86,19 +122,28 @@ export function createContentControls(ctx) {
|
|
|
86
122
|
try {
|
|
87
123
|
prepared = prepare(state);
|
|
88
124
|
} catch (error) {
|
|
89
|
-
button
|
|
90
|
-
note.textContent = messageOf(error);
|
|
125
|
+
setActionState(button, { label, enabled: false, reason: messageOf(error) });
|
|
91
126
|
return;
|
|
92
127
|
}
|
|
93
|
-
button.
|
|
94
|
-
note.textContent = prepared.changed ? lossText(prepared) : (prepared.reason ?? "");
|
|
128
|
+
setActionState(button, { label, enabled: prepared.changed, reason: prepared.reason, loss: prepared.changed ? lossText(prepared) : "" });
|
|
95
129
|
};
|
|
96
|
-
return { wrap, update, state: () => current };
|
|
130
|
+
return { wrap, button, update, state: () => current };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A host action in the toolbar (the Studio's move, duplicate and delete): `{ role, label, icon, state() → { enabled, reason, shortcut }, run() }`. */
|
|
134
|
+
function hostAction(entry) {
|
|
135
|
+
const button = h("button", { type: "button", class: "opf-dc-tool", "aria-label": entry.label, "data-role": entry.role }, iconNode(ctx.doc, entry.icon));
|
|
136
|
+
button.addEventListener("click", () => {
|
|
137
|
+
if (button.getAttribute("aria-disabled") === "true") return;
|
|
138
|
+
entry.run();
|
|
139
|
+
});
|
|
140
|
+
return { wrap: button, update: () => setActionState(button, { label: entry.label, ...entry.state() }) };
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
// --- list levels ---------------------------------------------------------------------------------
|
|
100
144
|
|
|
101
|
-
const
|
|
145
|
+
const listTools = h("div", { class: "opf-dc-toolbar", role: "group", "aria-label": "List level" });
|
|
146
|
+
const list = h("div", { class: "opf-dc-row", "data-role": "list-levels" }, h("span", { class: "opf-dc-row-label", "aria-hidden": "true" }, "List level"), listTools);
|
|
102
147
|
{
|
|
103
148
|
const index = (state) => listItemIndexForSelection(state.selectedPath, state.blockPath);
|
|
104
149
|
const prepare = (delta) => (state) => {
|
|
@@ -110,9 +155,9 @@ export function createContentControls(ctx) {
|
|
|
110
155
|
const state = ctxState.current;
|
|
111
156
|
return shiftListItems(editor, state.blockPath, [index(state)], delta);
|
|
112
157
|
};
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
158
|
+
const outdent = action("Outdent item", { role: "outdent", icon: "IndentDecrease", prepare: prepare(-1), apply: apply(-1), done: "Item moved out." });
|
|
159
|
+
const indent = action("Indent item", { role: "indent", icon: "IndentIncrease", prepare: prepare(1), apply: apply(1), done: "Item moved in." });
|
|
160
|
+
listTools.append(outdent.wrap, indent.wrap);
|
|
116
161
|
updaters.push((state) => {
|
|
117
162
|
list.hidden = state.content?.kind !== "list";
|
|
118
163
|
if (!list.hidden) {
|
|
@@ -125,14 +170,13 @@ export function createContentControls(ctx) {
|
|
|
125
170
|
// --- a group of metrics --------------------------------------------------------------------------
|
|
126
171
|
|
|
127
172
|
const metrics = selectField("metric-group", "Group of metrics", {
|
|
128
|
-
empty: "Convert
|
|
129
|
-
help: "Every block in this group is a metric. Converting keeps their labels, values, units and deltas.",
|
|
173
|
+
empty: "Convert to…",
|
|
130
174
|
onChange: (value) => {
|
|
131
175
|
if (!value) return;
|
|
132
176
|
const path = ctxState.current.metricGroupPath;
|
|
133
177
|
run(
|
|
134
178
|
() => convertBlock(editor, path, value),
|
|
135
|
-
(change) => `Converted the group to ${BLOCK_KIND_LABELS[value]?.toLowerCase() ?? value}
|
|
179
|
+
(change) => `Converted the group to ${BLOCK_KIND_LABELS[value]?.toLowerCase() ?? value}.${lossText(change) ? ` ${lossText(change)}` : ""}`,
|
|
136
180
|
);
|
|
137
181
|
reselect(path);
|
|
138
182
|
},
|
|
@@ -144,9 +188,9 @@ export function createContentControls(ctx) {
|
|
|
144
188
|
metrics.setOptions(
|
|
145
189
|
targets.map((target) => ({
|
|
146
190
|
value: target.kind,
|
|
147
|
-
label: target.available ? `${target.label}${target.lossless ? "" : ` (loses ${target.loss.join(", ")})`}` :
|
|
191
|
+
label: target.available ? `${target.label}${target.lossless ? "" : ` (loses ${target.loss.join(", ")})`}` : target.label,
|
|
148
192
|
disabled: !target.available,
|
|
149
|
-
title: target.reason,
|
|
193
|
+
title: target.available ? undefined : shortReason(target.reason),
|
|
150
194
|
})),
|
|
151
195
|
);
|
|
152
196
|
metrics.set("");
|
|
@@ -154,9 +198,25 @@ export function createContentControls(ctx) {
|
|
|
154
198
|
|
|
155
199
|
// --- arrange the selected block -------------------------------------------------------------------
|
|
156
200
|
|
|
157
|
-
|
|
201
|
+
// One row of icon buttons: the host's actions before (the Studio's Move up and Move down), Group with the next block and Ungroup, then the
|
|
202
|
+
// host's actions after (Duplicate, Delete). `target` is the selected block, or the selected group (which has no content to convert).
|
|
203
|
+
const arrange = h("div", { class: "opf-dc-toolbar", role: "group", "aria-label": "Arrange", "data-role": "arrange" });
|
|
204
|
+
const imageUse = selectField("image-destination", "Use image as", {
|
|
205
|
+
empty: "Choose…",
|
|
206
|
+
onChange: (value) => {
|
|
207
|
+
const destination = IMAGE_DESTINATIONS.find((entry) => entry.value === value);
|
|
208
|
+
if (!destination) return;
|
|
209
|
+
const state = ctxState.current;
|
|
210
|
+
run(
|
|
211
|
+
() => moveImageToDesign(editor, state.blockPath, destination.target, destination.options),
|
|
212
|
+
(change) => `Image is now the ${destination.label.toLowerCase()}.${lossText(change) ? ` ${lossText(change)}` : ""}`,
|
|
213
|
+
);
|
|
214
|
+
reselect(`slides.${state.slideIndex}`);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
158
217
|
{
|
|
159
|
-
const
|
|
218
|
+
const target = (state) => state.blockPath ?? state.groupPath;
|
|
219
|
+
const parts = (state) => target(state)?.split(".") ?? [];
|
|
160
220
|
const blockIndex = (state) => {
|
|
161
221
|
const found = parts(state);
|
|
162
222
|
return found.at(-2) === "blocks" && /^\d+$/.test(found.at(-1)) ? Number(found.at(-1)) : undefined;
|
|
@@ -169,9 +229,10 @@ export function createContentControls(ctx) {
|
|
|
169
229
|
};
|
|
170
230
|
const group = action("Group with the next block", {
|
|
171
231
|
role: "group-next",
|
|
232
|
+
icon: "Group",
|
|
172
233
|
prepare: (state) => {
|
|
173
234
|
const indices = next(state);
|
|
174
|
-
if (!indices) return { changed: false, loss: [], reason: "There is no block after this one
|
|
235
|
+
if (!indices) return { changed: false, loss: [], reason: "There is no block after this one." };
|
|
175
236
|
return prepareGroupBlocks(editor.presentation, containerPath(state), indices, { validate: false });
|
|
176
237
|
},
|
|
177
238
|
apply: () => {
|
|
@@ -181,56 +242,49 @@ export function createContentControls(ctx) {
|
|
|
181
242
|
selectAfter: (change) => change.path,
|
|
182
243
|
done: "Blocks grouped.",
|
|
183
244
|
});
|
|
184
|
-
|
|
245
|
+
// The group to dissolve: the selected group itself, or the group the selected block is in.
|
|
246
|
+
const groupToUndo = (state) => {
|
|
247
|
+
if (state.groupPath && !state.blockPath) return state.groupPath;
|
|
185
248
|
const found = parts(state);
|
|
186
249
|
return found.length >= 6 && found.at(-4) === "blocks" ? found.slice(0, -2).join(".") : undefined;
|
|
187
250
|
};
|
|
188
|
-
const ungroup = action("Ungroup
|
|
251
|
+
const ungroup = action("Ungroup", {
|
|
189
252
|
role: "ungroup",
|
|
253
|
+
icon: "Ungroup",
|
|
190
254
|
prepare: (state) => {
|
|
191
|
-
const path =
|
|
255
|
+
const path = groupToUndo(state);
|
|
192
256
|
if (!path) return { changed: false, loss: [], reason: "This block is not inside a group." };
|
|
193
257
|
return prepareUngroupBlock(editor.presentation, path, { validate: false });
|
|
194
258
|
},
|
|
195
|
-
apply: () => ungroupBlock(editor,
|
|
259
|
+
apply: () => ungroupBlock(editor, groupToUndo(ctxState.current)),
|
|
196
260
|
selectAfter: (change) => change.path,
|
|
197
261
|
done: "Group dissolved.",
|
|
198
262
|
});
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!destination) return;
|
|
205
|
-
const state = ctxState.current;
|
|
206
|
-
run(
|
|
207
|
-
() => moveImageToDesign(editor, state.blockPath, destination.target, destination.options),
|
|
208
|
-
(change) => `Image is now the ${destination.label.toLowerCase()}. ${lossText(change)}`,
|
|
209
|
-
);
|
|
210
|
-
reselect(`slides.${state.slideIndex}`);
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
arrange.append(group.wrap, ungroup.wrap, image.wrap);
|
|
263
|
+
const host = ctx.blockActions ?? {};
|
|
264
|
+
const before = (host.before ?? []).map(hostAction);
|
|
265
|
+
const after = (host.after ?? []).map(hostAction);
|
|
266
|
+
const separator = () => h("span", { class: "opf-dc-toolbar-separator", "aria-hidden": "true" });
|
|
267
|
+
arrange.append(...before.map((entry) => entry.wrap), ...(before.length ? [separator()] : []), group.wrap, ungroup.wrap, ...(after.length ? [separator()] : []), ...after.map((entry) => entry.wrap));
|
|
214
268
|
updaters.push((state) => {
|
|
215
|
-
arrange.hidden = !state.content;
|
|
269
|
+
arrange.hidden = !state.content && !state.groupPath;
|
|
216
270
|
if (arrange.hidden) return;
|
|
217
|
-
group.update(state);
|
|
218
|
-
|
|
271
|
+
for (const entry of [...before, group, ungroup, ...after]) entry.update(state);
|
|
272
|
+
});
|
|
273
|
+
updaters.push((state) => {
|
|
219
274
|
const isImage = state.imageBlock === true;
|
|
220
|
-
|
|
221
|
-
if (isImage)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
275
|
+
imageUse.wrap.hidden = !isImage;
|
|
276
|
+
if (!isImage) return;
|
|
277
|
+
imageUse.setOptions(
|
|
278
|
+
IMAGE_DESTINATIONS.map((entry) => {
|
|
279
|
+
try {
|
|
280
|
+
const prepared = prepareImageToDesign(editor.presentation, state.blockPath, entry.target, { ...entry.options, validate: false });
|
|
281
|
+
return { value: entry.value, label: `${entry.label}${prepared.loss.length ? ` (loses ${prepared.loss.join(", ")})` : ""}` };
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return { value: entry.value, label: entry.label, disabled: true, title: shortReason(messageOf(error)) };
|
|
284
|
+
}
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
imageUse.set("");
|
|
234
288
|
});
|
|
235
289
|
}
|
|
236
290
|
|
|
@@ -238,14 +292,13 @@ export function createContentControls(ctx) {
|
|
|
238
292
|
|
|
239
293
|
const slideBox = h("div", { class: "opf-dc-actions-block", "data-role": "slide-structure" });
|
|
240
294
|
{
|
|
241
|
-
const layouts = selectField("region-layout", "Place
|
|
295
|
+
const layouts = selectField("region-layout", "Place blocks in regions", {
|
|
242
296
|
empty: "Choose a layout…",
|
|
243
|
-
help: "Each block gets its own part of the slide. Undo returns them to one list.",
|
|
244
297
|
onChange: (value) => {
|
|
245
298
|
const slideIndex = getSlide();
|
|
246
299
|
const choice = REGION_LAYOUTS[blockCount(slideIndex)]?.find((entry) => entry.id === value);
|
|
247
300
|
if (!choice) return;
|
|
248
|
-
run(() => placeBlocksInRegions(editor, slideIndex, choice.regions), (change) => `Blocks placed: ${choice.label.toLowerCase()}
|
|
301
|
+
run(() => placeBlocksInRegions(editor, slideIndex, choice.regions), (change) => `Blocks placed: ${choice.label.toLowerCase()}.${lossText(change) ? ` ${lossText(change)}` : ""}`);
|
|
249
302
|
reselect(`slides.${slideIndex}`);
|
|
250
303
|
},
|
|
251
304
|
});
|
|
@@ -278,7 +331,7 @@ export function createContentControls(ctx) {
|
|
|
278
331
|
const prepared = prepareBlocksToRegions(editor.presentation, state.slideIndex, choice.regions, { validate: false });
|
|
279
332
|
return { value: choice.id, label: `${choice.label}${prepared.loss.length ? ` (loses ${prepared.loss.join(", ")})` : ""}` };
|
|
280
333
|
} catch (error) {
|
|
281
|
-
return { value: choice.id, label:
|
|
334
|
+
return { value: choice.id, label: choice.label, disabled: true, title: shortReason(messageOf(error)) };
|
|
282
335
|
}
|
|
283
336
|
}),
|
|
284
337
|
);
|
|
@@ -303,8 +356,8 @@ export function createContentControls(ctx) {
|
|
|
303
356
|
}
|
|
304
357
|
|
|
305
358
|
return {
|
|
306
|
-
/** Nodes for the "Selected content" section: list levels, the group-of-metrics conversion and the
|
|
307
|
-
nodes: { list, metrics: metrics.wrap,
|
|
359
|
+
/** Nodes for the "Selected content" section: the arrange toolbar, list levels, the group-of-metrics conversion and the image destination. */
|
|
360
|
+
nodes: { arrange, list, metrics: metrics.wrap, image: imageUse.wrap },
|
|
308
361
|
/** The node for the "Slide structure" section. */
|
|
309
362
|
slideNode: slideBox,
|
|
310
363
|
/** Update every control from the session and the current selection. */
|
package/dist/data-grid.js
CHANGED
|
@@ -1120,6 +1120,19 @@ export function createDataGrid(container, options = {}) {
|
|
|
1120
1120
|
const td = event.target.closest?.("td[data-line]");
|
|
1121
1121
|
return td ? { u: Number(td.dataset.line), c: Number(td.dataset.column) } : undefined;
|
|
1122
1122
|
};
|
|
1123
|
+
// A press focuses a cell (or the table) at once. A host that was editing text elsewhere (the slide canvas) saves it when the grid takes
|
|
1124
|
+
// the focus, and may move the focus back as it does (the canvas focuses the target it saved, from its field's blur), so the grid takes it
|
|
1125
|
+
// once more. Both happen inside the press: a key pressed straight after the click must reach the grid, and a timer would run after it
|
|
1126
|
+
// (browsers dispatch pending input before timers). The timer stays for a host that moves the focus later.
|
|
1127
|
+
const focusFromPress = (at) => {
|
|
1128
|
+
const take = () => (cellNode(at.u, at.c) ?? table).focus?.({ preventScroll: true });
|
|
1129
|
+
const lost = () => !destroyed && !editing && grid && !root.contains(doc.activeElement);
|
|
1130
|
+
take();
|
|
1131
|
+
if (lost()) take();
|
|
1132
|
+
setTimeout(() => {
|
|
1133
|
+
if (lost()) take();
|
|
1134
|
+
}, 0);
|
|
1135
|
+
};
|
|
1123
1136
|
table.addEventListener("mousedown", (event) => {
|
|
1124
1137
|
if (!grid || event.button !== 0) return;
|
|
1125
1138
|
const ruler = event.target.closest?.("th[data-ruler]");
|
|
@@ -1139,7 +1152,7 @@ export function createDataGrid(container, options = {}) {
|
|
|
1139
1152
|
if (event.shiftKey) selection.anchor = { u: selection.anchor.u, c: 0 };
|
|
1140
1153
|
}
|
|
1141
1154
|
paint();
|
|
1142
|
-
(
|
|
1155
|
+
focusFromPress({ ...selection.focus });
|
|
1143
1156
|
return;
|
|
1144
1157
|
}
|
|
1145
1158
|
const hit = cellFromEvent(event);
|
|
@@ -1154,12 +1167,7 @@ export function createDataGrid(container, options = {}) {
|
|
|
1154
1167
|
dragging = true;
|
|
1155
1168
|
invalid = new Map();
|
|
1156
1169
|
setSelection(event.shiftKey ? selection.anchor : hit, hit, { scroll: false });
|
|
1157
|
-
|
|
1158
|
-
const wanted = { ...selection.focus };
|
|
1159
|
-
setTimeout(() => {
|
|
1160
|
-
if (destroyed || editing || !grid || root.contains(doc.activeElement)) return;
|
|
1161
|
-
cellNode(wanted.u, wanted.c)?.focus({ preventScroll: true });
|
|
1162
|
-
}, 0);
|
|
1170
|
+
focusFromPress({ ...selection.focus });
|
|
1163
1171
|
});
|
|
1164
1172
|
table.addEventListener("mouseover", (event) => {
|
|
1165
1173
|
if (!dragging) return;
|
|
@@ -37,6 +37,19 @@ export interface DesignControlsOptions {
|
|
|
37
37
|
onAddAsset?: (image: { name: string; mediaType: string; bytes: Uint8Array; size: number; alt?: string; file: unknown }) => string | { src: string } | Promise<string | { src: string }>;
|
|
38
38
|
/** Called with every status or error message the panel shows in its live regions. */
|
|
39
39
|
onStatus?: (message: string, info: { error: boolean }) => void;
|
|
40
|
+
/** Show the selected block's path as a copyable chip under the selection section's Advanced (default true). A host with its own Advanced turns it off. */
|
|
41
|
+
showPath?: boolean;
|
|
42
|
+
/** Host actions for the selected block's arrange toolbar: `before` Group with the next block and Ungroup (Move up, Move down), `after` them (Duplicate, Delete). */
|
|
43
|
+
blockActions?: { before?: readonly DesignBlockAction[]; after?: readonly DesignBlockAction[] };
|
|
44
|
+
}
|
|
45
|
+
/** One icon button of the arrange toolbar, supplied by the host. Its tooltip is the label and shortcut, or the label and the reason when it is disabled. */
|
|
46
|
+
export interface DesignBlockAction {
|
|
47
|
+
role: string;
|
|
48
|
+
label: string;
|
|
49
|
+
/** An icon name of the studio kit's set (`ArrowUp`, `ArrowDown`, `Copy`, `Trash2`, ...). */
|
|
50
|
+
icon: string;
|
|
51
|
+
state(): { enabled: boolean; reason?: string; shortcut?: string };
|
|
52
|
+
run(): void;
|
|
40
53
|
}
|
|
41
54
|
export interface DesignControls {
|
|
42
55
|
element: HTMLElement;
|