lumina-slides 9.0.5 → 9.0.7
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 +63 -0
- package/dist/lumina-slides.js +21750 -19334
- package/dist/lumina-slides.umd.cjs +223 -223
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/LandingPage.vue +1 -1
- package/src/components/LuminaDeck.vue +237 -232
- package/src/components/base/LuminaElement.vue +2 -0
- package/src/components/layouts/LayoutFeatures.vue +125 -123
- package/src/components/layouts/LayoutFlex.vue +212 -212
- package/src/components/layouts/LayoutStatement.vue +5 -2
- package/src/components/layouts/LayoutSteps.vue +110 -108
- package/src/components/parts/FlexHtml.vue +65 -65
- package/src/components/parts/FlexImage.vue +81 -81
- package/src/components/site/SiteDocs.vue +3313 -3314
- package/src/components/site/SiteExamples.vue +66 -66
- package/src/components/studio/EditorLayoutChart.vue +18 -0
- package/src/components/studio/EditorLayoutCustom.vue +18 -0
- package/src/components/studio/EditorLayoutVideo.vue +18 -0
- package/src/components/studio/LuminaStudioEmbed.vue +68 -0
- package/src/components/studio/StudioEmbedRoot.vue +19 -0
- package/src/components/studio/StudioInspector.vue +1113 -7
- package/src/components/studio/StudioJsonEditor.vue +10 -3
- package/src/components/studio/StudioSettings.vue +658 -7
- package/src/components/studio/StudioToolbar.vue +26 -7
- package/src/composables/useElementState.ts +12 -1
- package/src/composables/useFlexLayout.ts +128 -128
- package/src/core/Lumina.ts +174 -113
- package/src/core/animationConfig.ts +10 -0
- package/src/core/elementController.ts +18 -0
- package/src/core/elementResolver.ts +4 -2
- package/src/core/schema.ts +503 -503
- package/src/core/store.ts +465 -465
- package/src/core/types.ts +26 -11
- package/src/index.ts +2 -2
- package/src/utils/prepareDeckForExport.ts +47 -0
- package/src/utils/templateInterpolation.ts +52 -52
- package/src/views/DeckView.vue +313 -313
package/src/core/store.ts
CHANGED
|
@@ -1,465 +1,465 @@
|
|
|
1
|
-
import { reactive, readonly, InjectionKey } from 'vue';
|
|
2
|
-
import type { Deck, LuminaOptions, ActionPayload, ElementState } from './types';
|
|
3
|
-
import { expandValue } from './compression';
|
|
4
|
-
import { normalizeAliases } from './schema';
|
|
5
|
-
import { setByPath, insertByPath, removeByPath, moveByPath, getByPath } from '../utils/deep';
|
|
6
|
-
import { getElementIds } from './elementResolver';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* LUMINA STORE FACTORY
|
|
10
|
-
* Refactored to support multiple instances via Dependency Injection.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export interface LuminaState {
|
|
14
|
-
deck: Deck | null;
|
|
15
|
-
currentIndex: number;
|
|
16
|
-
options: LuminaOptions;
|
|
17
|
-
isReady: boolean;
|
|
18
|
-
// Feature: Feedback Loop (Context Window)
|
|
19
|
-
actionHistory: ActionPayload[];
|
|
20
|
-
/**
|
|
21
|
-
* Per-element state for {@link ElementController}: visibility, opacity, transform, class, style.
|
|
22
|
-
* Keys = element ids (e.g. "s0-title", "s1-features-0"). Initialized from
|
|
23
|
-
* {@link DeckMeta.initialElementState} on load; updated by engine.element(id).show/hide/opacity/etc.
|
|
24
|
-
* loadDeck applies initialElementState before assigning deck so the first paint sees the correct state.
|
|
25
|
-
* @see loadDeck
|
|
26
|
-
*/
|
|
27
|
-
elementState: Record<string, ElementState>;
|
|
28
|
-
/**
|
|
29
|
-
* Key-value store for arbitrary user data. Access via engine.data or store.getUserData/setUserData.
|
|
30
|
-
* Persists across deck loads; not serialized.
|
|
31
|
-
*/
|
|
32
|
-
userData: Record<string, unknown>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type LuminaStore = ReturnType<typeof createStore>;
|
|
36
|
-
|
|
37
|
-
export const StoreKey: InjectionKey<LuminaStore> = Symbol('LuminaStore');
|
|
38
|
-
|
|
39
|
-
const DEFAULT_OPTIONS: LuminaOptions = {
|
|
40
|
-
loop: false,
|
|
41
|
-
navigation: true,
|
|
42
|
-
keyboard: true,
|
|
43
|
-
touch: true,
|
|
44
|
-
debug: false,
|
|
45
|
-
theme: 'default',
|
|
46
|
-
ui: {
|
|
47
|
-
visible: true,
|
|
48
|
-
showProgressBar: true,
|
|
49
|
-
showSlideCount: true,
|
|
50
|
-
showControls: true
|
|
51
|
-
},
|
|
52
|
-
keys: {
|
|
53
|
-
next: ['ArrowRight', ' ', 'Enter'],
|
|
54
|
-
prev: ['ArrowLeft', 'Backspace']
|
|
55
|
-
},
|
|
56
|
-
animation: {
|
|
57
|
-
enabled: true,
|
|
58
|
-
type: 'cascade',
|
|
59
|
-
durationIn: 1.0,
|
|
60
|
-
durationOut: 0.5,
|
|
61
|
-
stagger: 0.1,
|
|
62
|
-
ease: 'power3.out'
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Deeply expands compressed values in an object.
|
|
68
|
-
*/
|
|
69
|
-
function deepExpand(obj: any): any {
|
|
70
|
-
if (typeof obj === 'string') return expandValue(obj);
|
|
71
|
-
if (Array.isArray(obj)) return obj.map(deepExpand);
|
|
72
|
-
if (obj && typeof obj === 'object') {
|
|
73
|
-
const newObj: any = {};
|
|
74
|
-
for (const key in obj) {
|
|
75
|
-
newObj[key] = deepExpand(obj[key]);
|
|
76
|
-
}
|
|
77
|
-
return newObj;
|
|
78
|
-
}
|
|
79
|
-
return obj;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Simple deep merge for patching.
|
|
84
|
-
*/
|
|
85
|
-
function deepMerge(target: any, source: any) {
|
|
86
|
-
if (typeof target !== 'object' || target === null) return source;
|
|
87
|
-
if (typeof source !== 'object' || source === null) return source;
|
|
88
|
-
|
|
89
|
-
if (Array.isArray(source)) {
|
|
90
|
-
// Arrays are replaced, not merged, to avoid duplication issues in lists
|
|
91
|
-
return source.map(deepExpand);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const output = { ...target };
|
|
95
|
-
for (const key in source) {
|
|
96
|
-
if (source[key] === Object(source[key])) {
|
|
97
|
-
if (!(key in target)) {
|
|
98
|
-
Object.assign(output, { [key]: deepExpand(source[key]) });
|
|
99
|
-
} else {
|
|
100
|
-
output[key] = deepMerge(target[key], source[key]);
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
Object.assign(output, { [key]: deepExpand(source[key]) });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return output;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Creates a new isolated Lumina Store instance.
|
|
111
|
-
* Uses the Factory pattern to support multiple engine instances on the same page.
|
|
112
|
-
*
|
|
113
|
-
* @param initialOptions - Optional configuration overrides.
|
|
114
|
-
* @returns A store object containing the reactive state and methods.
|
|
115
|
-
*/
|
|
116
|
-
export function createStore(initialOptions: LuminaOptions = {}) {
|
|
117
|
-
const baseOptions = { ...DEFAULT_OPTIONS, ...initialOptions };
|
|
118
|
-
if (initialOptions && typeof initialOptions.keys === 'object') {
|
|
119
|
-
baseOptions.keys = { ...DEFAULT_OPTIONS.keys!, ...initialOptions.keys };
|
|
120
|
-
}
|
|
121
|
-
const state = reactive<LuminaState>({
|
|
122
|
-
deck: null,
|
|
123
|
-
currentIndex: 0,
|
|
124
|
-
options: baseOptions,
|
|
125
|
-
isReady: false,
|
|
126
|
-
actionHistory: [],
|
|
127
|
-
elementState: {},
|
|
128
|
-
userData: {}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// --- Getters ---
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Returns the complete data object for the currently active slide.
|
|
135
|
-
*/
|
|
136
|
-
const currentSlide = () => {
|
|
137
|
-
if (!state.deck || state.deck.slides.length === 0) return null;
|
|
138
|
-
return state.deck.slides[state.currentIndex] || null;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
/** Checks if there is a next slide available (respects loop). */
|
|
142
|
-
const hasNext = () => {
|
|
143
|
-
if (!state.deck) return false;
|
|
144
|
-
return state.options.loop
|
|
145
|
-
? true
|
|
146
|
-
: state.currentIndex < state.deck.slides.length - 1;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
/** Checks if there is a previous slide available (respects loop). */
|
|
150
|
-
const hasPrev = () => {
|
|
151
|
-
if (!state.deck) return false;
|
|
152
|
-
return state.options.loop
|
|
153
|
-
? true
|
|
154
|
-
: state.currentIndex > 0;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
/** Returns the current progress as a normalized value between 0 and 1. */
|
|
158
|
-
const progress = () => {
|
|
159
|
-
if (!state.deck || state.deck.slides.length <= 1) return 0;
|
|
160
|
-
return state.currentIndex / (state.deck.slides.length - 1);
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// --- Actions ---
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Updates the engine options at runtime.
|
|
167
|
-
* Merges provided options with existing ones.
|
|
168
|
-
*
|
|
169
|
-
* @param options - Partial options object to merge.
|
|
170
|
-
*/
|
|
171
|
-
function setOptions(options: LuminaOptions) {
|
|
172
|
-
state.options = { ...state.options, ...options };
|
|
173
|
-
|
|
174
|
-
// Theme Merging Logic
|
|
175
|
-
if (options.theme) {
|
|
176
|
-
if (typeof options.theme === 'string') {
|
|
177
|
-
// Completely replace if string preset
|
|
178
|
-
state.options.theme = options.theme;
|
|
179
|
-
} else {
|
|
180
|
-
// Allow fine-grained object overrides
|
|
181
|
-
state.options.theme = options.theme;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (options.ui) state.options.ui = { ...state.options.ui, ...options.ui };
|
|
186
|
-
if (options.keys) state.options.keys = { ...state.options.keys, ...options.keys };
|
|
187
|
-
if (options.animation) state.options.animation = { ...state.options.animation, ...options.animation };
|
|
188
|
-
if (options.elementControl) state.options.elementControl = { ...(state.options.elementControl || {}), ...options.elementControl };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Ensures all elements in flex layouts have unique dragKeys for vuedraggable.
|
|
193
|
-
*/
|
|
194
|
-
function ensureDragKeys(obj: any): any {
|
|
195
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
196
|
-
if (Array.isArray(obj)) return obj.map(ensureDragKeys);
|
|
197
|
-
|
|
198
|
-
const newObj = { ...obj };
|
|
199
|
-
if (newObj.type === 'flex' && Array.isArray(newObj.elements)) {
|
|
200
|
-
newObj.elements = newObj.elements.map((el: any) => ({
|
|
201
|
-
dragKey: el.dragKey || Date.now() + Math.random(),
|
|
202
|
-
...ensureDragKeys(el)
|
|
203
|
-
}));
|
|
204
|
-
} else if (newObj.type === 'content' && Array.isArray(newObj.elements)) {
|
|
205
|
-
newObj.elements = newObj.elements.map((el: any) => ({
|
|
206
|
-
dragKey: el.dragKey || Date.now() + Math.random() + 1,
|
|
207
|
-
...ensureDragKeys(el)
|
|
208
|
-
}));
|
|
209
|
-
} else {
|
|
210
|
-
for (const key in newObj) {
|
|
211
|
-
newObj[key] = ensureDragKeys(newObj[key]);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return newObj;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Loads a new deck into the engine and resets state.
|
|
219
|
-
*
|
|
220
|
-
* **Critical order (element control):** Apply `meta.initialElementState` to
|
|
221
|
-
* `state.elementState` *before* assigning `state.deck`. Assigning `state.deck` triggers
|
|
222
|
-
* Vue's re-render; on that first paint, LuminaElements and useTransition read
|
|
223
|
-
* `elementState` to decide initial opacity and whether to exclude `.reveal-*` containers.
|
|
224
|
-
* If `elementState` were applied after `state.deck`, on that first frame they would see
|
|
225
|
-
* empty or stale state (e.g. `visible: true` from a previous load), and elements that
|
|
226
|
-
* should start hidden would appear visible until the next tick.
|
|
227
|
-
*
|
|
228
|
-
* @param deck - Full deck: `{ meta: { initialElementState?, ... }, slides: [...] }`.
|
|
229
|
-
*/
|
|
230
|
-
function loadDeck(deck: Deck) {
|
|
231
|
-
if (!deck || !Array.isArray(deck.slides)) {
|
|
232
|
-
console.error('[LuminaStore] Invalid deck format');
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const normalizedDeck = normalizeAliases(deck);
|
|
236
|
-
const keyedDeck = ensureDragKeys(normalizedDeck);
|
|
237
|
-
|
|
238
|
-
const initial = keyedDeck.meta?.initialElementState;
|
|
239
|
-
const initialObj = initial && typeof initial === 'object' && !Array.isArray(initial) ? initial : {};
|
|
240
|
-
state.elementState = Object.fromEntries(
|
|
241
|
-
Object.entries(initialObj).map(([k, v]) => [k, { ...(v && typeof v === 'object' ? v : {}) }])
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
state.deck = deepExpand(keyedDeck);
|
|
245
|
-
state.currentIndex = 0;
|
|
246
|
-
state.isReady = true;
|
|
247
|
-
state.actionHistory = [];
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
const hideByDefault =
|
|
251
|
-
state.options.elementControl?.defaultVisible
|
|
252
|
-
keyedDeck.meta?.elementControl?.defaultVisible
|
|
253
|
-
if (hideByDefault) {
|
|
254
|
-
const inInitial = new Set(Object.keys(initialObj));
|
|
255
|
-
(state.deck?.slides ?? []).forEach((slide, i) => {
|
|
256
|
-
getElementIds(slide, i).forEach((id) => {
|
|
257
|
-
if (!inInitial.has(id)) {
|
|
258
|
-
const cur = state.elementState[id] || {};
|
|
259
|
-
state.elementState[id] = { ...cur, visible: false };
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Feature: Diff Updates
|
|
268
|
-
* Patches the current deck with partial data.
|
|
269
|
-
*/
|
|
270
|
-
function patchDeck(partial: Partial<Deck>) {
|
|
271
|
-
if (!state.deck) return;
|
|
272
|
-
const merged = deepMerge(state.deck, partial);
|
|
273
|
-
state.deck = ensureDragKeys(merged);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Records a user action for the Feedback Loop.
|
|
278
|
-
*/
|
|
279
|
-
function recordAction(action: ActionPayload) {
|
|
280
|
-
state.actionHistory.push(action);
|
|
281
|
-
if (state.actionHistory.length > 50) state.actionHistory.shift(); // Keep buffer small
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** Advances to the next slide if possible. */
|
|
285
|
-
function next() {
|
|
286
|
-
if (!hasNext()) return;
|
|
287
|
-
const count = state.deck?.slides.length || 0;
|
|
288
|
-
let newIndex = state.currentIndex;
|
|
289
|
-
|
|
290
|
-
if (state.options.loop) {
|
|
291
|
-
newIndex = (state.currentIndex + 1) % count;
|
|
292
|
-
} else {
|
|
293
|
-
newIndex++;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (newIndex !== state.currentIndex) {
|
|
297
|
-
state.currentIndex = newIndex;
|
|
298
|
-
// Emit event via bus (imported from events.ts, need to update imports)
|
|
299
|
-
// Ideally store shouldn't know about bus, but for now this is the direct fix
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Returns to the previous slide if possible. */
|
|
304
|
-
function prev() {
|
|
305
|
-
if (!hasPrev()) return;
|
|
306
|
-
const count = state.deck?.slides.length || 0;
|
|
307
|
-
let newIndex = state.currentIndex;
|
|
308
|
-
|
|
309
|
-
if (state.options.loop) {
|
|
310
|
-
newIndex = (state.currentIndex - 1 + count) % count;
|
|
311
|
-
} else {
|
|
312
|
-
newIndex--;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (newIndex !== state.currentIndex) {
|
|
316
|
-
state.currentIndex = newIndex;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Jumps immediately to a specific slide index.
|
|
322
|
-
* @param index - Zero-based index of the target slide.
|
|
323
|
-
*/
|
|
324
|
-
function goto(index: number) {
|
|
325
|
-
if (!state.deck) return;
|
|
326
|
-
if (index >= 0 && index < state.deck.slides.length) {
|
|
327
|
-
if (state.currentIndex !== index) {
|
|
328
|
-
state.currentIndex = index;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// --- Editor Actions (Immutable) ---
|
|
336
|
-
|
|
337
|
-
function updateNode(path: string, value: any) {
|
|
338
|
-
if (!state.deck) return;
|
|
339
|
-
const updated = setByPath(state.deck, path, value);
|
|
340
|
-
state.deck = ensureDragKeys(updated);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function addNode(arrayPath: string, item: any, index?: number) {
|
|
344
|
-
if (!state.deck) return;
|
|
345
|
-
const target = getByPath(state.deck, arrayPath);
|
|
346
|
-
// Default to end of array if index not provided
|
|
347
|
-
const actualIndex = (index !== undefined) ? index : (Array.isArray(target) ? target.length : 0);
|
|
348
|
-
// Ensure item has keys
|
|
349
|
-
const keyedItem = ensureDragKeys(item);
|
|
350
|
-
state.deck = insertByPath(state.deck, arrayPath, actualIndex, keyedItem);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function removeNode(arrayPath: string, index: number) {
|
|
354
|
-
if (!state.deck) return;
|
|
355
|
-
state.deck = removeByPath(state.deck, arrayPath, index);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function moveNode(arrayPath: string, from: number, to: number) {
|
|
359
|
-
if (!state.deck) return;
|
|
360
|
-
state.deck = moveByPath(state.deck, arrayPath, from, to);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Merges a partial {@link ElementState} into the stored state for an element. Used by
|
|
365
|
-
* {@link ElementController} (engine.element(id)). style and class are merged; visible, opacity, transform
|
|
366
|
-
* are replaced when present in `patch`.
|
|
367
|
-
*
|
|
368
|
-
* @param id - Element id (from resolveId, elemId, or explicit id in JSON).
|
|
369
|
-
* @param patch - Partial ElementState. Only provided keys are applied.
|
|
370
|
-
*/
|
|
371
|
-
function setElementState(id: string, patch: Partial<ElementState>) {
|
|
372
|
-
const prev = state.elementState[id] || {};
|
|
373
|
-
const next: ElementState = { ...prev };
|
|
374
|
-
if (patch.visible !== undefined) next.visible = patch.visible;
|
|
375
|
-
if (patch.opacity !== undefined) next.opacity = patch.opacity;
|
|
376
|
-
if (patch.transform !== undefined) next.transform = patch.transform;
|
|
377
|
-
if (patch.class !== undefined) next.class = patch.class;
|
|
378
|
-
if (patch.style !== undefined) next.style = { ...(prev.style || {}), ...patch.style };
|
|
379
|
-
state.elementState[id] = next;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Resets elementState for all ids of the given slide to their initial values.
|
|
384
|
-
* Used when navigating to a slide so that controlled entrance animations
|
|
385
|
-
* (e.g. engine.element(id).show()) can run again. Respects
|
|
386
|
-
* meta.initialElementState and meta.elementControl.defaultVisible (or
|
|
387
|
-
* options.elementControl.defaultVisible).
|
|
388
|
-
*
|
|
389
|
-
* @param slideIndex - Zero-based index of the slide to reset.
|
|
390
|
-
*/
|
|
391
|
-
function resetElementStateForSlide(slideIndex: number) {
|
|
392
|
-
const deck = state.deck;
|
|
393
|
-
if (!deck?.slides) return;
|
|
394
|
-
const slide = deck.slides[slideIndex];
|
|
395
|
-
if (!slide) return;
|
|
396
|
-
|
|
397
|
-
const initial = deck.meta?.initialElementState;
|
|
398
|
-
const initialObj = initial && typeof initial === 'object' && !Array.isArray(initial) ? initial : {};
|
|
399
|
-
const hideByDefault =
|
|
400
|
-
state.options.elementControl?.defaultVisible
|
|
401
|
-
deck.meta?.elementControl?.defaultVisible
|
|
402
|
-
const inInitial = new Set(Object.keys(initialObj));
|
|
403
|
-
|
|
404
|
-
getElementIds(slide, slideIndex).forEach((id) => {
|
|
405
|
-
if (inInitial.has(id)) {
|
|
406
|
-
const v = initialObj[id];
|
|
407
|
-
state.elementState[id] = { ...(v && typeof v === 'object' ? v : {}) };
|
|
408
|
-
} else if (hideByDefault) {
|
|
409
|
-
state.elementState[id] = { visible: false };
|
|
410
|
-
} else {
|
|
411
|
-
state.elementState[id] = {};
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// --- Key-Value Store (engine.data) ---
|
|
417
|
-
|
|
418
|
-
function getUserData(key: string): unknown {
|
|
419
|
-
return state.userData[key];
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function setUserData(key: string, value: unknown): void {
|
|
423
|
-
state.userData[key] = value;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function hasUserData(key: string): boolean {
|
|
427
|
-
return Object.prototype.hasOwnProperty.call(state.userData, key);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function deleteUserData(key: string): boolean {
|
|
431
|
-
if (!Object.prototype.hasOwnProperty.call(state.userData, key)) return false;
|
|
432
|
-
delete state.userData[key];
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function clearUserData(): void {
|
|
437
|
-
state.userData = {};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
state: readonly(state),
|
|
442
|
-
currentSlide,
|
|
443
|
-
hasNext,
|
|
444
|
-
hasPrev,
|
|
445
|
-
progress,
|
|
446
|
-
setOptions,
|
|
447
|
-
loadDeck,
|
|
448
|
-
patchDeck,
|
|
449
|
-
updateNode,
|
|
450
|
-
addNode,
|
|
451
|
-
removeNode,
|
|
452
|
-
moveNode,
|
|
453
|
-
setElementState,
|
|
454
|
-
resetElementStateForSlide,
|
|
455
|
-
recordAction,
|
|
456
|
-
next,
|
|
457
|
-
prev,
|
|
458
|
-
goto,
|
|
459
|
-
getUserData,
|
|
460
|
-
setUserData,
|
|
461
|
-
hasUserData,
|
|
462
|
-
deleteUserData,
|
|
463
|
-
clearUserData
|
|
464
|
-
};
|
|
465
|
-
}
|
|
1
|
+
import { reactive, readonly, InjectionKey } from 'vue';
|
|
2
|
+
import type { Deck, LuminaOptions, ActionPayload, ElementState } from './types';
|
|
3
|
+
import { expandValue } from './compression';
|
|
4
|
+
import { normalizeAliases } from './schema';
|
|
5
|
+
import { setByPath, insertByPath, removeByPath, moveByPath, getByPath } from '../utils/deep';
|
|
6
|
+
import { getElementIds } from './elementResolver';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* LUMINA STORE FACTORY
|
|
10
|
+
* Refactored to support multiple instances via Dependency Injection.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface LuminaState {
|
|
14
|
+
deck: Deck | null;
|
|
15
|
+
currentIndex: number;
|
|
16
|
+
options: LuminaOptions;
|
|
17
|
+
isReady: boolean;
|
|
18
|
+
// Feature: Feedback Loop (Context Window)
|
|
19
|
+
actionHistory: ActionPayload[];
|
|
20
|
+
/**
|
|
21
|
+
* Per-element state for {@link ElementController}: visibility, opacity, transform, class, style.
|
|
22
|
+
* Keys = element ids (e.g. "s0-title", "s1-features-0"). Initialized from
|
|
23
|
+
* {@link DeckMeta.initialElementState} on load; updated by engine.element(id).show/hide/opacity/etc.
|
|
24
|
+
* loadDeck applies initialElementState before assigning deck so the first paint sees the correct state.
|
|
25
|
+
* @see loadDeck
|
|
26
|
+
*/
|
|
27
|
+
elementState: Record<string, ElementState>;
|
|
28
|
+
/**
|
|
29
|
+
* Key-value store for arbitrary user data. Access via engine.data or store.getUserData/setUserData.
|
|
30
|
+
* Persists across deck loads; not serialized.
|
|
31
|
+
*/
|
|
32
|
+
userData: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type LuminaStore = ReturnType<typeof createStore>;
|
|
36
|
+
|
|
37
|
+
export const StoreKey: InjectionKey<LuminaStore> = Symbol('LuminaStore');
|
|
38
|
+
|
|
39
|
+
const DEFAULT_OPTIONS: LuminaOptions = {
|
|
40
|
+
loop: false,
|
|
41
|
+
navigation: true,
|
|
42
|
+
keyboard: true,
|
|
43
|
+
touch: true,
|
|
44
|
+
debug: false,
|
|
45
|
+
theme: 'default',
|
|
46
|
+
ui: {
|
|
47
|
+
visible: true,
|
|
48
|
+
showProgressBar: true,
|
|
49
|
+
showSlideCount: true,
|
|
50
|
+
showControls: true
|
|
51
|
+
},
|
|
52
|
+
keys: {
|
|
53
|
+
next: ['ArrowRight', ' ', 'Enter'],
|
|
54
|
+
prev: ['ArrowLeft', 'Backspace']
|
|
55
|
+
},
|
|
56
|
+
animation: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
type: 'cascade',
|
|
59
|
+
durationIn: 1.0,
|
|
60
|
+
durationOut: 0.5,
|
|
61
|
+
stagger: 0.1,
|
|
62
|
+
ease: 'power3.out'
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Deeply expands compressed values in an object.
|
|
68
|
+
*/
|
|
69
|
+
function deepExpand(obj: any): any {
|
|
70
|
+
if (typeof obj === 'string') return expandValue(obj);
|
|
71
|
+
if (Array.isArray(obj)) return obj.map(deepExpand);
|
|
72
|
+
if (obj && typeof obj === 'object') {
|
|
73
|
+
const newObj: any = {};
|
|
74
|
+
for (const key in obj) {
|
|
75
|
+
newObj[key] = deepExpand(obj[key]);
|
|
76
|
+
}
|
|
77
|
+
return newObj;
|
|
78
|
+
}
|
|
79
|
+
return obj;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Simple deep merge for patching.
|
|
84
|
+
*/
|
|
85
|
+
function deepMerge(target: any, source: any) {
|
|
86
|
+
if (typeof target !== 'object' || target === null) return source;
|
|
87
|
+
if (typeof source !== 'object' || source === null) return source;
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(source)) {
|
|
90
|
+
// Arrays are replaced, not merged, to avoid duplication issues in lists
|
|
91
|
+
return source.map(deepExpand);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const output = { ...target };
|
|
95
|
+
for (const key in source) {
|
|
96
|
+
if (source[key] === Object(source[key])) {
|
|
97
|
+
if (!(key in target)) {
|
|
98
|
+
Object.assign(output, { [key]: deepExpand(source[key]) });
|
|
99
|
+
} else {
|
|
100
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
Object.assign(output, { [key]: deepExpand(source[key]) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates a new isolated Lumina Store instance.
|
|
111
|
+
* Uses the Factory pattern to support multiple engine instances on the same page.
|
|
112
|
+
*
|
|
113
|
+
* @param initialOptions - Optional configuration overrides.
|
|
114
|
+
* @returns A store object containing the reactive state and methods.
|
|
115
|
+
*/
|
|
116
|
+
export function createStore(initialOptions: LuminaOptions = {}) {
|
|
117
|
+
const baseOptions = { ...DEFAULT_OPTIONS, ...initialOptions };
|
|
118
|
+
if (initialOptions && typeof initialOptions.keys === 'object') {
|
|
119
|
+
baseOptions.keys = { ...DEFAULT_OPTIONS.keys!, ...initialOptions.keys };
|
|
120
|
+
}
|
|
121
|
+
const state = reactive<LuminaState>({
|
|
122
|
+
deck: null,
|
|
123
|
+
currentIndex: 0,
|
|
124
|
+
options: baseOptions,
|
|
125
|
+
isReady: false,
|
|
126
|
+
actionHistory: [],
|
|
127
|
+
elementState: {},
|
|
128
|
+
userData: {}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// --- Getters ---
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns the complete data object for the currently active slide.
|
|
135
|
+
*/
|
|
136
|
+
const currentSlide = () => {
|
|
137
|
+
if (!state.deck || state.deck.slides.length === 0) return null;
|
|
138
|
+
return state.deck.slides[state.currentIndex] || null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** Checks if there is a next slide available (respects loop). */
|
|
142
|
+
const hasNext = () => {
|
|
143
|
+
if (!state.deck) return false;
|
|
144
|
+
return state.options.loop
|
|
145
|
+
? true
|
|
146
|
+
: state.currentIndex < state.deck.slides.length - 1;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** Checks if there is a previous slide available (respects loop). */
|
|
150
|
+
const hasPrev = () => {
|
|
151
|
+
if (!state.deck) return false;
|
|
152
|
+
return state.options.loop
|
|
153
|
+
? true
|
|
154
|
+
: state.currentIndex > 0;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Returns the current progress as a normalized value between 0 and 1. */
|
|
158
|
+
const progress = () => {
|
|
159
|
+
if (!state.deck || state.deck.slides.length <= 1) return 0;
|
|
160
|
+
return state.currentIndex / (state.deck.slides.length - 1);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// --- Actions ---
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Updates the engine options at runtime.
|
|
167
|
+
* Merges provided options with existing ones.
|
|
168
|
+
*
|
|
169
|
+
* @param options - Partial options object to merge.
|
|
170
|
+
*/
|
|
171
|
+
function setOptions(options: LuminaOptions) {
|
|
172
|
+
state.options = { ...state.options, ...options };
|
|
173
|
+
|
|
174
|
+
// Theme Merging Logic
|
|
175
|
+
if (options.theme) {
|
|
176
|
+
if (typeof options.theme === 'string') {
|
|
177
|
+
// Completely replace if string preset
|
|
178
|
+
state.options.theme = options.theme;
|
|
179
|
+
} else {
|
|
180
|
+
// Allow fine-grained object overrides
|
|
181
|
+
state.options.theme = options.theme;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (options.ui) state.options.ui = { ...state.options.ui, ...options.ui };
|
|
186
|
+
if (options.keys) state.options.keys = { ...state.options.keys, ...options.keys };
|
|
187
|
+
if (options.animation) state.options.animation = { ...state.options.animation, ...options.animation };
|
|
188
|
+
if (options.elementControl) state.options.elementControl = { ...(state.options.elementControl || {}), ...options.elementControl };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Ensures all elements in flex layouts have unique dragKeys for vuedraggable.
|
|
193
|
+
*/
|
|
194
|
+
function ensureDragKeys(obj: any): any {
|
|
195
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
196
|
+
if (Array.isArray(obj)) return obj.map(ensureDragKeys);
|
|
197
|
+
|
|
198
|
+
const newObj = { ...obj };
|
|
199
|
+
if (newObj.type === 'flex' && Array.isArray(newObj.elements)) {
|
|
200
|
+
newObj.elements = newObj.elements.map((el: any) => ({
|
|
201
|
+
dragKey: el.dragKey || Date.now() + Math.random(),
|
|
202
|
+
...ensureDragKeys(el)
|
|
203
|
+
}));
|
|
204
|
+
} else if (newObj.type === 'content' && Array.isArray(newObj.elements)) {
|
|
205
|
+
newObj.elements = newObj.elements.map((el: any) => ({
|
|
206
|
+
dragKey: el.dragKey || Date.now() + Math.random() + 1,
|
|
207
|
+
...ensureDragKeys(el)
|
|
208
|
+
}));
|
|
209
|
+
} else {
|
|
210
|
+
for (const key in newObj) {
|
|
211
|
+
newObj[key] = ensureDragKeys(newObj[key]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return newObj;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Loads a new deck into the engine and resets state.
|
|
219
|
+
*
|
|
220
|
+
* **Critical order (element control):** Apply `meta.initialElementState` to
|
|
221
|
+
* `state.elementState` *before* assigning `state.deck`. Assigning `state.deck` triggers
|
|
222
|
+
* Vue's re-render; on that first paint, LuminaElements and useTransition read
|
|
223
|
+
* `elementState` to decide initial opacity and whether to exclude `.reveal-*` containers.
|
|
224
|
+
* If `elementState` were applied after `state.deck`, on that first frame they would see
|
|
225
|
+
* empty or stale state (e.g. `visible: true` from a previous load), and elements that
|
|
226
|
+
* should start hidden would appear visible until the next tick.
|
|
227
|
+
*
|
|
228
|
+
* @param deck - Full deck: `{ meta: { initialElementState?, ... }, slides: [...] }`.
|
|
229
|
+
*/
|
|
230
|
+
function loadDeck(deck: Deck) {
|
|
231
|
+
if (!deck || !Array.isArray(deck.slides)) {
|
|
232
|
+
console.error('[LuminaStore] Invalid deck format');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const normalizedDeck = normalizeAliases(deck);
|
|
236
|
+
const keyedDeck = ensureDragKeys(normalizedDeck);
|
|
237
|
+
|
|
238
|
+
const initial = keyedDeck.meta?.initialElementState;
|
|
239
|
+
const initialObj = initial && typeof initial === 'object' && !Array.isArray(initial) ? initial : {};
|
|
240
|
+
state.elementState = Object.fromEntries(
|
|
241
|
+
Object.entries(initialObj).map(([k, v]) => [k, { ...(v && typeof v === 'object' ? v : {}) }])
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
state.deck = deepExpand(keyedDeck);
|
|
245
|
+
state.currentIndex = 0;
|
|
246
|
+
state.isReady = true;
|
|
247
|
+
state.actionHistory = [];
|
|
248
|
+
|
|
249
|
+
// Elements are hidden by default. Set elementControl.defaultVisible: true (options or meta) to show all.
|
|
250
|
+
const hideByDefault =
|
|
251
|
+
state.options.elementControl?.defaultVisible !== true &&
|
|
252
|
+
keyedDeck.meta?.elementControl?.defaultVisible !== true;
|
|
253
|
+
if (hideByDefault) {
|
|
254
|
+
const inInitial = new Set(Object.keys(initialObj));
|
|
255
|
+
(state.deck?.slides ?? []).forEach((slide, i) => {
|
|
256
|
+
getElementIds(slide, i).forEach((id) => {
|
|
257
|
+
if (!inInitial.has(id)) {
|
|
258
|
+
const cur = state.elementState[id] || {};
|
|
259
|
+
state.elementState[id] = { ...cur, visible: false };
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Feature: Diff Updates
|
|
268
|
+
* Patches the current deck with partial data.
|
|
269
|
+
*/
|
|
270
|
+
function patchDeck(partial: Partial<Deck>) {
|
|
271
|
+
if (!state.deck) return;
|
|
272
|
+
const merged = deepMerge(state.deck, partial);
|
|
273
|
+
state.deck = ensureDragKeys(merged);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Records a user action for the Feedback Loop.
|
|
278
|
+
*/
|
|
279
|
+
function recordAction(action: ActionPayload) {
|
|
280
|
+
state.actionHistory.push(action);
|
|
281
|
+
if (state.actionHistory.length > 50) state.actionHistory.shift(); // Keep buffer small
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Advances to the next slide if possible. */
|
|
285
|
+
function next() {
|
|
286
|
+
if (!hasNext()) return;
|
|
287
|
+
const count = state.deck?.slides.length || 0;
|
|
288
|
+
let newIndex = state.currentIndex;
|
|
289
|
+
|
|
290
|
+
if (state.options.loop) {
|
|
291
|
+
newIndex = (state.currentIndex + 1) % count;
|
|
292
|
+
} else {
|
|
293
|
+
newIndex++;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (newIndex !== state.currentIndex) {
|
|
297
|
+
state.currentIndex = newIndex;
|
|
298
|
+
// Emit event via bus (imported from events.ts, need to update imports)
|
|
299
|
+
// Ideally store shouldn't know about bus, but for now this is the direct fix
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Returns to the previous slide if possible. */
|
|
304
|
+
function prev() {
|
|
305
|
+
if (!hasPrev()) return;
|
|
306
|
+
const count = state.deck?.slides.length || 0;
|
|
307
|
+
let newIndex = state.currentIndex;
|
|
308
|
+
|
|
309
|
+
if (state.options.loop) {
|
|
310
|
+
newIndex = (state.currentIndex - 1 + count) % count;
|
|
311
|
+
} else {
|
|
312
|
+
newIndex--;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (newIndex !== state.currentIndex) {
|
|
316
|
+
state.currentIndex = newIndex;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Jumps immediately to a specific slide index.
|
|
322
|
+
* @param index - Zero-based index of the target slide.
|
|
323
|
+
*/
|
|
324
|
+
function goto(index: number) {
|
|
325
|
+
if (!state.deck) return;
|
|
326
|
+
if (index >= 0 && index < state.deck.slides.length) {
|
|
327
|
+
if (state.currentIndex !== index) {
|
|
328
|
+
state.currentIndex = index;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
// --- Editor Actions (Immutable) ---
|
|
336
|
+
|
|
337
|
+
function updateNode(path: string, value: any) {
|
|
338
|
+
if (!state.deck) return;
|
|
339
|
+
const updated = setByPath(state.deck, path, value);
|
|
340
|
+
state.deck = ensureDragKeys(updated);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function addNode(arrayPath: string, item: any, index?: number) {
|
|
344
|
+
if (!state.deck) return;
|
|
345
|
+
const target = getByPath(state.deck, arrayPath);
|
|
346
|
+
// Default to end of array if index not provided
|
|
347
|
+
const actualIndex = (index !== undefined) ? index : (Array.isArray(target) ? target.length : 0);
|
|
348
|
+
// Ensure item has keys
|
|
349
|
+
const keyedItem = ensureDragKeys(item);
|
|
350
|
+
state.deck = insertByPath(state.deck, arrayPath, actualIndex, keyedItem);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function removeNode(arrayPath: string, index: number) {
|
|
354
|
+
if (!state.deck) return;
|
|
355
|
+
state.deck = removeByPath(state.deck, arrayPath, index);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function moveNode(arrayPath: string, from: number, to: number) {
|
|
359
|
+
if (!state.deck) return;
|
|
360
|
+
state.deck = moveByPath(state.deck, arrayPath, from, to);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Merges a partial {@link ElementState} into the stored state for an element. Used by
|
|
365
|
+
* {@link ElementController} (engine.element(id)). style and class are merged; visible, opacity, transform
|
|
366
|
+
* are replaced when present in `patch`.
|
|
367
|
+
*
|
|
368
|
+
* @param id - Element id (from resolveId, elemId, or explicit id in JSON).
|
|
369
|
+
* @param patch - Partial ElementState. Only provided keys are applied.
|
|
370
|
+
*/
|
|
371
|
+
function setElementState(id: string, patch: Partial<ElementState>) {
|
|
372
|
+
const prev = state.elementState[id] || {};
|
|
373
|
+
const next: ElementState = { ...prev };
|
|
374
|
+
if (patch.visible !== undefined) next.visible = patch.visible;
|
|
375
|
+
if (patch.opacity !== undefined) next.opacity = patch.opacity;
|
|
376
|
+
if (patch.transform !== undefined) next.transform = patch.transform;
|
|
377
|
+
if (patch.class !== undefined) next.class = patch.class;
|
|
378
|
+
if (patch.style !== undefined) next.style = { ...(prev.style || {}), ...patch.style };
|
|
379
|
+
state.elementState[id] = next;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Resets elementState for all ids of the given slide to their initial values.
|
|
384
|
+
* Used when navigating to a slide so that controlled entrance animations
|
|
385
|
+
* (e.g. engine.element(id).show()) can run again. Respects
|
|
386
|
+
* meta.initialElementState and meta.elementControl.defaultVisible (or
|
|
387
|
+
* options.elementControl.defaultVisible).
|
|
388
|
+
*
|
|
389
|
+
* @param slideIndex - Zero-based index of the slide to reset.
|
|
390
|
+
*/
|
|
391
|
+
function resetElementStateForSlide(slideIndex: number) {
|
|
392
|
+
const deck = state.deck;
|
|
393
|
+
if (!deck?.slides) return;
|
|
394
|
+
const slide = deck.slides[slideIndex];
|
|
395
|
+
if (!slide) return;
|
|
396
|
+
|
|
397
|
+
const initial = deck.meta?.initialElementState;
|
|
398
|
+
const initialObj = initial && typeof initial === 'object' && !Array.isArray(initial) ? initial : {};
|
|
399
|
+
const hideByDefault =
|
|
400
|
+
state.options.elementControl?.defaultVisible !== true &&
|
|
401
|
+
deck.meta?.elementControl?.defaultVisible !== true;
|
|
402
|
+
const inInitial = new Set(Object.keys(initialObj));
|
|
403
|
+
|
|
404
|
+
getElementIds(slide, slideIndex).forEach((id) => {
|
|
405
|
+
if (inInitial.has(id)) {
|
|
406
|
+
const v = initialObj[id];
|
|
407
|
+
state.elementState[id] = { ...(v && typeof v === 'object' ? v : {}) };
|
|
408
|
+
} else if (hideByDefault) {
|
|
409
|
+
state.elementState[id] = { visible: false };
|
|
410
|
+
} else {
|
|
411
|
+
state.elementState[id] = {};
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// --- Key-Value Store (engine.data) ---
|
|
417
|
+
|
|
418
|
+
function getUserData(key: string): unknown {
|
|
419
|
+
return state.userData[key];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function setUserData(key: string, value: unknown): void {
|
|
423
|
+
state.userData[key] = value;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function hasUserData(key: string): boolean {
|
|
427
|
+
return Object.prototype.hasOwnProperty.call(state.userData, key);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function deleteUserData(key: string): boolean {
|
|
431
|
+
if (!Object.prototype.hasOwnProperty.call(state.userData, key)) return false;
|
|
432
|
+
delete state.userData[key];
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function clearUserData(): void {
|
|
437
|
+
state.userData = {};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
state: readonly(state),
|
|
442
|
+
currentSlide,
|
|
443
|
+
hasNext,
|
|
444
|
+
hasPrev,
|
|
445
|
+
progress,
|
|
446
|
+
setOptions,
|
|
447
|
+
loadDeck,
|
|
448
|
+
patchDeck,
|
|
449
|
+
updateNode,
|
|
450
|
+
addNode,
|
|
451
|
+
removeNode,
|
|
452
|
+
moveNode,
|
|
453
|
+
setElementState,
|
|
454
|
+
resetElementStateForSlide,
|
|
455
|
+
recordAction,
|
|
456
|
+
next,
|
|
457
|
+
prev,
|
|
458
|
+
goto,
|
|
459
|
+
getUserData,
|
|
460
|
+
setUserData,
|
|
461
|
+
hasUserData,
|
|
462
|
+
deleteUserData,
|
|
463
|
+
clearUserData
|
|
464
|
+
};
|
|
465
|
+
}
|