@velumo/cli 0.1.0-beta.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/dist/browser-overview.d.ts +72 -0
- package/dist/browser-overview.d.ts.map +1 -0
- package/dist/browser-overview.js +431 -0
- package/dist/browser-overview.js.map +1 -0
- package/dist/browser-presenter.d.ts +63 -0
- package/dist/browser-presenter.d.ts.map +1 -0
- package/dist/browser-presenter.js +304 -0
- package/dist/browser-presenter.js.map +1 -0
- package/dist/browser-runtime.d.ts +43 -0
- package/dist/browser-runtime.d.ts.map +1 -0
- package/dist/browser-runtime.js +293 -0
- package/dist/browser-runtime.js.map +1 -0
- package/dist/browser-spatial.d.ts +42 -0
- package/dist/browser-spatial.d.ts.map +1 -0
- package/dist/browser-spatial.js +207 -0
- package/dist/browser-spatial.js.map +1 -0
- package/dist/browser-sync.d.ts +37 -0
- package/dist/browser-sync.d.ts.map +1 -0
- package/dist/browser-sync.js +77 -0
- package/dist/browser-sync.js.map +1 -0
- package/dist/browser-timer.d.ts +9 -0
- package/dist/browser-timer.d.ts.map +1 -0
- package/dist/browser-timer.js +13 -0
- package/dist/browser-timer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +902 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +42 -0
- package/src/browser-overview.ts +641 -0
- package/src/browser-presenter.ts +602 -0
- package/src/browser-runtime.ts +493 -0
- package/src/browser-spatial.ts +292 -0
- package/src/browser-sync.ts +151 -0
- package/src/browser-timer.ts +24 -0
- package/src/index.ts +1273 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntimeRegistry,
|
|
3
|
+
installPlugins,
|
|
4
|
+
parseRuntimeHash,
|
|
5
|
+
RuntimeController,
|
|
6
|
+
serializeRuntimeHash,
|
|
7
|
+
type VelumoPlugin,
|
|
8
|
+
type RuntimeControllerOptions,
|
|
9
|
+
type RuntimeRegistry,
|
|
10
|
+
} from "@velumo/runtime";
|
|
11
|
+
import {
|
|
12
|
+
DomRenderer,
|
|
13
|
+
mountSlideThumbnail,
|
|
14
|
+
type DomRendererOptions,
|
|
15
|
+
} from "@velumo/renderer-dom";
|
|
16
|
+
import type { Manifest } from "@velumo/core";
|
|
17
|
+
import {
|
|
18
|
+
createPresenterSync,
|
|
19
|
+
type PresenterCommand,
|
|
20
|
+
type PresenterSyncChannel,
|
|
21
|
+
} from "./browser-sync.js";
|
|
22
|
+
import {
|
|
23
|
+
setupOverviewMode,
|
|
24
|
+
type OverviewMountOptions,
|
|
25
|
+
type OverviewRootLike,
|
|
26
|
+
type OverviewWindowLike,
|
|
27
|
+
} from "./browser-overview.js";
|
|
28
|
+
import {
|
|
29
|
+
createSpatialSession,
|
|
30
|
+
type SpatialSession,
|
|
31
|
+
} from "./browser-spatial.js";
|
|
32
|
+
|
|
33
|
+
interface ManifestResponse {
|
|
34
|
+
readonly ok: boolean;
|
|
35
|
+
readonly status: number;
|
|
36
|
+
json(): Promise<Manifest>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type BrowserRoot = DomRendererOptions["root"];
|
|
40
|
+
type BrowserRenderer = Pick<DomRenderer, "mount"> &
|
|
41
|
+
Partial<Pick<DomRenderer, "render" | "getRoot">>;
|
|
42
|
+
type RuntimeFactory = (
|
|
43
|
+
manifest: Manifest,
|
|
44
|
+
options?: RuntimeControllerOptions,
|
|
45
|
+
) => RuntimeController;
|
|
46
|
+
type RendererFactory = (options: DomRendererOptions) => BrowserRenderer;
|
|
47
|
+
|
|
48
|
+
interface BrowserKeyboardEvent {
|
|
49
|
+
readonly key: string;
|
|
50
|
+
readonly target: unknown;
|
|
51
|
+
preventDefault(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface BrowserWindowLike {
|
|
55
|
+
readonly BroadcastChannel?: new (name: string) => PresenterSyncChannel;
|
|
56
|
+
readonly crypto?: { randomUUID?(): string };
|
|
57
|
+
readonly location: {
|
|
58
|
+
hash: string;
|
|
59
|
+
};
|
|
60
|
+
addEventListener(
|
|
61
|
+
type: "keydown",
|
|
62
|
+
handler: (event: BrowserKeyboardEvent) => void,
|
|
63
|
+
): void;
|
|
64
|
+
removeEventListener(
|
|
65
|
+
type: "keydown",
|
|
66
|
+
handler: (event: BrowserKeyboardEvent) => void,
|
|
67
|
+
): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface BrowserRuntimeGlobals {
|
|
71
|
+
readonly BroadcastChannel?: new (name: string) => PresenterSyncChannel;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BrowserRuntimeMountOptions {
|
|
75
|
+
readonly root: BrowserRoot | null | undefined;
|
|
76
|
+
readonly manifestUrl: string;
|
|
77
|
+
readonly keyboard?: boolean;
|
|
78
|
+
readonly urlState?: boolean;
|
|
79
|
+
readonly window?: BrowserWindowLike;
|
|
80
|
+
readonly createRuntime?: RuntimeFactory;
|
|
81
|
+
readonly createRenderer?: RendererFactory;
|
|
82
|
+
readonly plugins?: readonly VelumoPlugin<unknown, unknown, unknown>[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface BrowserRuntimeMountResult {
|
|
86
|
+
readonly manifest: Manifest;
|
|
87
|
+
readonly runtime: RuntimeController;
|
|
88
|
+
readonly renderer: BrowserRenderer;
|
|
89
|
+
readonly registry?: RuntimeRegistry<unknown, unknown, unknown>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function mountVelumoBrowserRuntime(
|
|
93
|
+
options: BrowserRuntimeMountOptions,
|
|
94
|
+
): Promise<BrowserRuntimeMountResult | undefined> {
|
|
95
|
+
if (options.root === null || options.root === undefined) {
|
|
96
|
+
throw new Error("Velumo root element not found");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const root = options.root;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const response = (await fetch(options.manifestUrl)) as ManifestResponse;
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`manifest request failed with status ${response.status}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const manifest = await response.json();
|
|
109
|
+
const browserWindow = options.window ?? getGlobalWindow();
|
|
110
|
+
const runtimeOptions =
|
|
111
|
+
options.urlState === false || browserWindow === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: parseRuntimeHash(browserWindow.location.hash);
|
|
114
|
+
const runtime =
|
|
115
|
+
options.createRuntime?.(manifest, runtimeOptions) ??
|
|
116
|
+
new RuntimeController(manifest, runtimeOptions);
|
|
117
|
+
|
|
118
|
+
// Ward 033 contract alignment: install plugins (if provided) into a fresh
|
|
119
|
+
// runtime registry, then thread the registry into the DOM renderer so its
|
|
120
|
+
// custom-layer-renderer dispatch can resolve fx:* / custom layer types
|
|
121
|
+
// against the plugin-registered renderers. Type-erased handoff at this
|
|
122
|
+
// boundary: RuntimeRegistry<unknown,unknown,unknown> is structurally
|
|
123
|
+
// satisfied by DomRendererOptions["registry"].
|
|
124
|
+
const installedPlugins = options.plugins ?? [];
|
|
125
|
+
const registry =
|
|
126
|
+
installedPlugins.length > 0
|
|
127
|
+
? createRuntimeRegistry<unknown, unknown, unknown>()
|
|
128
|
+
: undefined;
|
|
129
|
+
if (registry !== undefined) {
|
|
130
|
+
installPlugins({ registry, manifest, plugins: installedPlugins });
|
|
131
|
+
}
|
|
132
|
+
const rendererOptions: DomRendererOptions = {
|
|
133
|
+
root,
|
|
134
|
+
manifest,
|
|
135
|
+
runtime,
|
|
136
|
+
registry: registry as DomRendererOptions["registry"],
|
|
137
|
+
};
|
|
138
|
+
const renderer =
|
|
139
|
+
options.createRenderer?.(rendererOptions) ??
|
|
140
|
+
new DomRenderer(rendererOptions);
|
|
141
|
+
|
|
142
|
+
renderer.mount();
|
|
143
|
+
const urlState = options.urlState !== false;
|
|
144
|
+
setupBrowserControls({
|
|
145
|
+
keyboard: options.keyboard !== false,
|
|
146
|
+
urlState,
|
|
147
|
+
window: browserWindow,
|
|
148
|
+
manifest,
|
|
149
|
+
runtime,
|
|
150
|
+
renderer,
|
|
151
|
+
});
|
|
152
|
+
armSpatialForCurrentSlide(
|
|
153
|
+
{
|
|
154
|
+
urlState,
|
|
155
|
+
window: browserWindow,
|
|
156
|
+
manifest,
|
|
157
|
+
runtime,
|
|
158
|
+
renderer,
|
|
159
|
+
},
|
|
160
|
+
"forward",
|
|
161
|
+
);
|
|
162
|
+
setupPresenterAudienceSync({
|
|
163
|
+
urlState,
|
|
164
|
+
window: browserWindow,
|
|
165
|
+
manifest,
|
|
166
|
+
runtime,
|
|
167
|
+
renderer,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (browserWindow !== undefined) {
|
|
171
|
+
setupOverviewMode({
|
|
172
|
+
root: root as unknown as OverviewRootLike,
|
|
173
|
+
runtime,
|
|
174
|
+
manifest,
|
|
175
|
+
window: browserWindow as unknown as OverviewWindowLike,
|
|
176
|
+
// Thread the plugin registry into overview thumbnails so plugin-
|
|
177
|
+
// registered layer renderers (e.g. fx:*) render real content, not
|
|
178
|
+
// fallback layer-type text.
|
|
179
|
+
mountThumbnail: ((opts: {
|
|
180
|
+
root: unknown;
|
|
181
|
+
manifest: unknown;
|
|
182
|
+
slideId: string;
|
|
183
|
+
}) =>
|
|
184
|
+
mountSlideThumbnail({
|
|
185
|
+
root: opts.root as Parameters<
|
|
186
|
+
typeof mountSlideThumbnail
|
|
187
|
+
>[0]["root"],
|
|
188
|
+
manifest: opts.manifest as Parameters<
|
|
189
|
+
typeof mountSlideThumbnail
|
|
190
|
+
>[0]["manifest"],
|
|
191
|
+
slideId: opts.slideId,
|
|
192
|
+
registry: registry as DomRendererOptions["registry"],
|
|
193
|
+
})) as unknown as OverviewMountOptions["mountThumbnail"],
|
|
194
|
+
onAfterJump: () => {
|
|
195
|
+
renderer.render?.();
|
|
196
|
+
armSpatialForCurrentSlide(
|
|
197
|
+
{
|
|
198
|
+
urlState,
|
|
199
|
+
window: browserWindow,
|
|
200
|
+
manifest,
|
|
201
|
+
runtime,
|
|
202
|
+
renderer,
|
|
203
|
+
},
|
|
204
|
+
"forward",
|
|
205
|
+
);
|
|
206
|
+
if (urlState) {
|
|
207
|
+
browserWindow.location.hash = serializeRuntimeHash(
|
|
208
|
+
runtime.getState(),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { manifest, runtime, renderer, registry };
|
|
216
|
+
} catch (error) {
|
|
217
|
+
renderMountError(root, error);
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderMountError(root: BrowserRoot, error: unknown): void {
|
|
223
|
+
const container = root.ownerDocument.createElement("section");
|
|
224
|
+
container.setAttribute("data-velumo-error", "mount");
|
|
225
|
+
|
|
226
|
+
const title = root.ownerDocument.createElement("h1");
|
|
227
|
+
title.textContent = "Velumo mount error";
|
|
228
|
+
|
|
229
|
+
const message = root.ownerDocument.createElement("p");
|
|
230
|
+
message.textContent =
|
|
231
|
+
error instanceof Error ? error.message : "Unknown browser runtime error";
|
|
232
|
+
|
|
233
|
+
container.append(title, message);
|
|
234
|
+
root.replaceChildren(container);
|
|
235
|
+
root.setAttribute("data-velumo-error", "mount");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface BrowserControlsOptions {
|
|
239
|
+
readonly keyboard: boolean;
|
|
240
|
+
readonly urlState: boolean;
|
|
241
|
+
readonly window: BrowserWindowLike | undefined;
|
|
242
|
+
readonly manifest: Manifest;
|
|
243
|
+
readonly runtime: RuntimeController;
|
|
244
|
+
readonly renderer: BrowserRenderer;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type BrowserCommandOptions = Omit<BrowserControlsOptions, "keyboard">;
|
|
248
|
+
|
|
249
|
+
function getGlobalWindow(): BrowserWindowLike | undefined {
|
|
250
|
+
return (globalThis as { window?: BrowserWindowLike }).window;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let activeSpatialSession: SpatialSession | undefined;
|
|
254
|
+
|
|
255
|
+
function currentSlideEntry(options: BrowserCommandOptions) {
|
|
256
|
+
const state = options.runtime.getState();
|
|
257
|
+
return (
|
|
258
|
+
options.manifest.slides.find((slide) => slide.id === state.slideId) ??
|
|
259
|
+
options.manifest.slides[0]
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function armSpatialForCurrentSlide(
|
|
264
|
+
options: BrowserCommandOptions,
|
|
265
|
+
direction: "forward" | "backward",
|
|
266
|
+
): void {
|
|
267
|
+
activeSpatialSession?.dispose();
|
|
268
|
+
activeSpatialSession = undefined;
|
|
269
|
+
if (options.window === undefined) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const root = options.renderer.getRoot?.();
|
|
273
|
+
if (root === undefined) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
activeSpatialSession = createSpatialSession({
|
|
277
|
+
root: root as unknown as Parameters<typeof createSpatialSession>[0]["root"],
|
|
278
|
+
slide: currentSlideEntry(options),
|
|
279
|
+
window: options.window as unknown as Parameters<
|
|
280
|
+
typeof createSpatialSession
|
|
281
|
+
>[0]["window"],
|
|
282
|
+
direction,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function setupBrowserControls(options: BrowserControlsOptions): void {
|
|
287
|
+
if (!options.keyboard || options.window === undefined) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const browserWindow = options.window;
|
|
292
|
+
|
|
293
|
+
browserWindow.addEventListener("keydown", (event) => {
|
|
294
|
+
if (isEditableTarget(event.target)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const result = navigateWithKey(event.key, options);
|
|
299
|
+
|
|
300
|
+
if (result === undefined) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
|
|
306
|
+
if (result === "spatial") {
|
|
307
|
+
updateBrowserHash(options);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (result === "runtime-forward") {
|
|
312
|
+
applyRuntimeChange(options, "forward");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (result === "runtime-backward") {
|
|
317
|
+
applyRuntimeChange(options, "backward");
|
|
318
|
+
}
|
|
319
|
+
// "none" → preventDefault already called; nothing else
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function setupPresenterAudienceSync(options: BrowserCommandOptions): void {
|
|
324
|
+
if (options.window === undefined) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const channel = createPresenterChannel(options.window);
|
|
329
|
+
|
|
330
|
+
if (channel === undefined) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const sync = createPresenterSync({
|
|
335
|
+
channel,
|
|
336
|
+
instanceId: createPresenterInstanceId(options.window),
|
|
337
|
+
role: "audience",
|
|
338
|
+
onCommand(command) {
|
|
339
|
+
applyPresenterCommand(command, options);
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
sync.publishState(options.runtime.getState());
|
|
344
|
+
options.runtime.on("state:change", (event) => {
|
|
345
|
+
sync.publishState(event.state);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function applyPresenterCommand(
|
|
350
|
+
command: PresenterCommand,
|
|
351
|
+
options: BrowserCommandOptions,
|
|
352
|
+
): void {
|
|
353
|
+
const changed = runPresenterCommand(command, options.runtime);
|
|
354
|
+
if (!changed) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
applyRuntimeChange(options, command === "previous" ? "backward" : "forward");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function applyRuntimeChange(
|
|
361
|
+
options: BrowserCommandOptions,
|
|
362
|
+
direction: "forward" | "backward",
|
|
363
|
+
): void {
|
|
364
|
+
activeSpatialSession?.dispose();
|
|
365
|
+
activeSpatialSession = undefined;
|
|
366
|
+
options.renderer.render?.();
|
|
367
|
+
updateBrowserHash(options);
|
|
368
|
+
armSpatialForCurrentSlide(options, direction);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function updateBrowserHash(options: BrowserCommandOptions): void {
|
|
372
|
+
if (options.urlState && options.window !== undefined) {
|
|
373
|
+
options.window.location.hash = serializeRuntimeHash(
|
|
374
|
+
options.runtime.getState(),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function runPresenterCommand(
|
|
380
|
+
command: PresenterCommand,
|
|
381
|
+
runtime: RuntimeController,
|
|
382
|
+
): boolean {
|
|
383
|
+
switch (command) {
|
|
384
|
+
case "next":
|
|
385
|
+
return runtime.next();
|
|
386
|
+
case "previous":
|
|
387
|
+
return runtime.previous();
|
|
388
|
+
case "resetTimer":
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function createPresenterChannel(
|
|
394
|
+
browserWindow: BrowserWindowLike,
|
|
395
|
+
): PresenterSyncChannel | undefined {
|
|
396
|
+
const BroadcastChannelCtor =
|
|
397
|
+
browserWindow.BroadcastChannel ??
|
|
398
|
+
(globalThis as BrowserRuntimeGlobals).BroadcastChannel;
|
|
399
|
+
if (BroadcastChannelCtor === undefined) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const channel = new BroadcastChannelCtor("velumo-presenter");
|
|
404
|
+
channel.unref?.();
|
|
405
|
+
return channel;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function createPresenterInstanceId(browserWindow: BrowserWindowLike): string {
|
|
409
|
+
return (
|
|
410
|
+
browserWindow.crypto?.randomUUID?.() ??
|
|
411
|
+
`audience-${Math.random().toString(36).slice(2)}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
type NavigationResult =
|
|
416
|
+
| "spatial"
|
|
417
|
+
| "runtime-forward"
|
|
418
|
+
| "runtime-backward"
|
|
419
|
+
| "none"
|
|
420
|
+
| undefined;
|
|
421
|
+
|
|
422
|
+
function navigateWithKey(
|
|
423
|
+
key: string,
|
|
424
|
+
options: BrowserControlsOptions,
|
|
425
|
+
): NavigationResult {
|
|
426
|
+
switch (key) {
|
|
427
|
+
case " ":
|
|
428
|
+
case "Space":
|
|
429
|
+
case "Spacebar":
|
|
430
|
+
case "ArrowRight":
|
|
431
|
+
case "PageDown":
|
|
432
|
+
case "Enter": {
|
|
433
|
+
if (
|
|
434
|
+
activeSpatialSession !== undefined &&
|
|
435
|
+
activeSpatialSession.advance()
|
|
436
|
+
) {
|
|
437
|
+
return "spatial";
|
|
438
|
+
}
|
|
439
|
+
return options.runtime.next() ? "runtime-forward" : "none";
|
|
440
|
+
}
|
|
441
|
+
case "ArrowLeft":
|
|
442
|
+
case "PageUp":
|
|
443
|
+
case "Backspace": {
|
|
444
|
+
if (activeSpatialSession !== undefined && activeSpatialSession.back()) {
|
|
445
|
+
return "spatial";
|
|
446
|
+
}
|
|
447
|
+
return options.runtime.previous() ? "runtime-backward" : "none";
|
|
448
|
+
}
|
|
449
|
+
case "Home": {
|
|
450
|
+
const firstSlide = options.manifest.slides[0];
|
|
451
|
+
if (firstSlide === undefined) {
|
|
452
|
+
return "none";
|
|
453
|
+
}
|
|
454
|
+
return options.runtime.jumpToSlide(firstSlide.id)
|
|
455
|
+
? "runtime-forward"
|
|
456
|
+
: "none";
|
|
457
|
+
}
|
|
458
|
+
case "End": {
|
|
459
|
+
const lastSlide = options.manifest.slides.at(-1);
|
|
460
|
+
if (lastSlide === undefined) {
|
|
461
|
+
return "none";
|
|
462
|
+
}
|
|
463
|
+
return options.runtime.jumpToSlide(lastSlide.id)
|
|
464
|
+
? "runtime-forward"
|
|
465
|
+
: "none";
|
|
466
|
+
}
|
|
467
|
+
default:
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function isEditableTarget(target: unknown): boolean {
|
|
473
|
+
if (target === null || typeof target !== "object") {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const candidate = target as {
|
|
478
|
+
readonly isContentEditable?: boolean;
|
|
479
|
+
readonly tagName?: string;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (candidate.isContentEditable === true) {
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const tagName = candidate.tagName?.toLowerCase();
|
|
487
|
+
return (
|
|
488
|
+
tagName === "input" ||
|
|
489
|
+
tagName === "textarea" ||
|
|
490
|
+
tagName === "select" ||
|
|
491
|
+
tagName === "button"
|
|
492
|
+
);
|
|
493
|
+
}
|