@editframe/elements 0.19.4-beta.0 → 0.20.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/elements/ContextProxiesController.d.ts +40 -0
- package/dist/elements/ContextProxiesController.js +69 -0
- package/dist/elements/EFCaptions.d.ts +45 -6
- package/dist/elements/EFCaptions.js +220 -26
- package/dist/elements/EFImage.js +4 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
- package/dist/elements/EFMedia.js +25 -1
- package/dist/elements/EFSurface.browsertest.d.ts +0 -0
- package/dist/elements/EFSurface.d.ts +30 -0
- package/dist/elements/EFSurface.js +96 -0
- package/dist/elements/EFTemporal.js +7 -6
- package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
- package/dist/elements/EFThumbnailStrip.d.ts +86 -0
- package/dist/elements/EFThumbnailStrip.js +490 -0
- package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
- package/dist/elements/EFTimegroup.d.ts +6 -1
- package/dist/elements/EFTimegroup.js +46 -10
- package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
- package/dist/elements/updateAnimations.d.ts +5 -0
- package/dist/elements/updateAnimations.js +37 -13
- package/dist/getRenderInfo.js +1 -1
- package/dist/gui/ContextMixin.js +27 -14
- package/dist/gui/EFControls.browsertest.d.ts +0 -0
- package/dist/gui/EFControls.d.ts +38 -0
- package/dist/gui/EFControls.js +51 -0
- package/dist/gui/EFFilmstrip.d.ts +40 -1
- package/dist/gui/EFFilmstrip.js +240 -3
- package/dist/gui/EFPreview.js +2 -1
- package/dist/gui/EFScrubber.d.ts +6 -5
- package/dist/gui/EFScrubber.js +31 -21
- package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
- package/dist/gui/EFTimeDisplay.d.ts +2 -6
- package/dist/gui/EFTimeDisplay.js +13 -23
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/currentTimeContext.d.ts +3 -0
- package/dist/gui/currentTimeContext.js +3 -0
- package/dist/gui/durationContext.d.ts +3 -0
- package/dist/gui/durationContext.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/style.css +1 -1
- package/dist/transcoding/types/index.d.ts +11 -0
- package/dist/utils/LRUCache.d.ts +46 -0
- package/dist/utils/LRUCache.js +382 -1
- package/dist/utils/LRUCache.test.d.ts +1 -0
- package/package.json +2 -2
- package/src/elements/ContextProxiesController.ts +123 -0
- package/src/elements/EFCaptions.browsertest.ts +1820 -0
- package/src/elements/EFCaptions.ts +373 -36
- package/src/elements/EFImage.ts +4 -1
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
- package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
- package/src/elements/EFMedia.ts +38 -1
- package/src/elements/EFSurface.browsertest.ts +155 -0
- package/src/elements/EFSurface.ts +141 -0
- package/src/elements/EFTemporal.ts +14 -8
- package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
- package/src/elements/EFThumbnailStrip.ts +905 -0
- package/src/elements/EFTimegroup.browsertest.ts +56 -7
- package/src/elements/EFTimegroup.ts +70 -11
- package/src/elements/updateAnimations.browsertest.ts +333 -11
- package/src/elements/updateAnimations.ts +68 -19
- package/src/gui/ContextMixin.browsertest.ts +0 -25
- package/src/gui/ContextMixin.ts +44 -20
- package/src/gui/EFControls.browsertest.ts +175 -0
- package/src/gui/EFControls.ts +84 -0
- package/src/gui/EFFilmstrip.ts +323 -4
- package/src/gui/EFPreview.ts +2 -1
- package/src/gui/EFScrubber.ts +29 -25
- package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
- package/src/gui/EFTimeDisplay.ts +12 -40
- package/src/gui/currentTimeContext.ts +5 -0
- package/src/gui/durationContext.ts +3 -0
- package/src/transcoding/types/index.ts +13 -0
- package/src/utils/LRUCache.test.ts +272 -0
- package/src/utils/LRUCache.ts +543 -0
- package/types.json +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +0 -73
- package/src/transcoding/cache/CacheManager.ts +0 -208
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deepGetTemporalElements,
|
|
3
|
-
isEFTemporal,
|
|
4
3
|
type TemporalMixinInterface,
|
|
5
4
|
} from "./EFTemporal.ts";
|
|
6
5
|
|
|
@@ -8,7 +7,7 @@ import {
|
|
|
8
7
|
export type AnimatableElement = TemporalMixinInterface & HTMLElement;
|
|
9
8
|
|
|
10
9
|
// Constants
|
|
11
|
-
const ANIMATION_PRECISION_OFFSET = 0.
|
|
10
|
+
const ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold
|
|
12
11
|
const DEFAULT_ANIMATION_ITERATIONS = 1;
|
|
13
12
|
const PROGRESS_PROPERTY = "--ef-progress";
|
|
14
13
|
const DURATION_PROPERTY = "--ef-duration";
|
|
@@ -39,8 +38,40 @@ export const evaluateTemporalState = (
|
|
|
39
38
|
? 1
|
|
40
39
|
: Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));
|
|
41
40
|
|
|
41
|
+
// Root timegroups should remain visible at exact end time, but other elements use exclusive end for clean transitions
|
|
42
|
+
const isRootTimegroup =
|
|
43
|
+
element.tagName.toLowerCase() === TIMEGROUP_TAGNAME &&
|
|
44
|
+
!(element as any).parentTimegroup;
|
|
45
|
+
const useInclusiveEnd = isRootTimegroup;
|
|
46
|
+
|
|
42
47
|
const isVisible =
|
|
43
|
-
element.startTimeMs <= timelineTimeMs &&
|
|
48
|
+
element.startTimeMs <= timelineTimeMs &&
|
|
49
|
+
(useInclusiveEnd
|
|
50
|
+
? element.endTimeMs >= timelineTimeMs
|
|
51
|
+
: element.endTimeMs > timelineTimeMs);
|
|
52
|
+
|
|
53
|
+
return { progress, isVisible, timelineTimeMs };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Evaluates element visibility specifically for animation coordination
|
|
58
|
+
* Uses inclusive end boundaries to prevent animation jumps at exact boundaries
|
|
59
|
+
*/
|
|
60
|
+
export const evaluateTemporalStateForAnimation = (
|
|
61
|
+
element: AnimatableElement,
|
|
62
|
+
): TemporalState => {
|
|
63
|
+
// Get timeline time from root timegroup, or use element's own time if it IS a timegroup
|
|
64
|
+
const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;
|
|
65
|
+
|
|
66
|
+
const progress =
|
|
67
|
+
element.durationMs <= 0
|
|
68
|
+
? 1
|
|
69
|
+
: Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));
|
|
70
|
+
|
|
71
|
+
// For animation coordination, use inclusive end for ALL elements to prevent visual jumps
|
|
72
|
+
const isVisible =
|
|
73
|
+
element.startTimeMs <= timelineTimeMs &&
|
|
74
|
+
element.endTimeMs >= timelineTimeMs;
|
|
44
75
|
|
|
45
76
|
return { progress, isVisible, timelineTimeMs };
|
|
46
77
|
};
|
|
@@ -80,9 +111,11 @@ const updateVisualState = (
|
|
|
80
111
|
};
|
|
81
112
|
|
|
82
113
|
/**
|
|
83
|
-
* Coordinates animations
|
|
114
|
+
* Coordinates animations for a single element and its subtree, using the element as the time source
|
|
84
115
|
*/
|
|
85
|
-
const
|
|
116
|
+
const coordinateAnimationsForSingleElement = (
|
|
117
|
+
element: AnimatableElement,
|
|
118
|
+
): void => {
|
|
86
119
|
const animations = element.getAnimations({ subtree: true });
|
|
87
120
|
|
|
88
121
|
for (const animation of animations) {
|
|
@@ -96,17 +129,12 @@ const coordinateAnimations = (element: AnimatableElement): void => {
|
|
|
96
129
|
}
|
|
97
130
|
|
|
98
131
|
const target = effect.target;
|
|
99
|
-
if (!target
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const timeTarget = isEFTemporal(target)
|
|
104
|
-
? target
|
|
105
|
-
: target.closest(TIMEGROUP_TAGNAME);
|
|
106
|
-
if (!timeTarget) {
|
|
132
|
+
if (!target) {
|
|
107
133
|
continue;
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
// For animations in this element's subtree, always use this element as the time source
|
|
137
|
+
// This handles both animations directly on the temporal element and on its non-temporal children
|
|
110
138
|
const timing = effect.getTiming();
|
|
111
139
|
const duration = Number(timing.duration) || 0;
|
|
112
140
|
const delay = Number(timing.delay) || 0;
|
|
@@ -118,8 +146,8 @@ const coordinateAnimations = (element: AnimatableElement): void => {
|
|
|
118
146
|
continue;
|
|
119
147
|
}
|
|
120
148
|
|
|
121
|
-
//
|
|
122
|
-
const currentTime =
|
|
149
|
+
// Use the element itself as the time source (it's guaranteed to be temporal)
|
|
150
|
+
const currentTime = element.ownCurrentTimeMs ?? 0;
|
|
123
151
|
|
|
124
152
|
if (currentTime < delay) {
|
|
125
153
|
animation.currentTime = 0;
|
|
@@ -130,12 +158,22 @@ const coordinateAnimations = (element: AnimatableElement): void => {
|
|
|
130
158
|
const currentIteration = Math.floor(adjustedTime / duration);
|
|
131
159
|
const currentIterationTime = adjustedTime % duration;
|
|
132
160
|
|
|
161
|
+
// Calculate the total animation timeline length (delay + duration * iterations)
|
|
162
|
+
const totalAnimationLength = delay + duration * iterations;
|
|
163
|
+
|
|
164
|
+
// CRITICAL: Always keep currentTime below totalAnimationLength to prevent completion
|
|
165
|
+
const maxSafeCurrentTime =
|
|
166
|
+
totalAnimationLength - ANIMATION_PRECISION_OFFSET;
|
|
167
|
+
|
|
133
168
|
if (currentIteration >= iterations) {
|
|
134
|
-
|
|
169
|
+
// Animation would be complete - clamp to just before completion
|
|
170
|
+
animation.currentTime = maxSafeCurrentTime;
|
|
135
171
|
} else {
|
|
136
|
-
|
|
172
|
+
// Animation in progress - clamp to safe value within current iteration
|
|
173
|
+
const proposedCurrentTime =
|
|
137
174
|
Math.min(currentIterationTime, duration - ANIMATION_PRECISION_OFFSET) +
|
|
138
175
|
delay;
|
|
176
|
+
animation.currentTime = Math.min(proposedCurrentTime, maxSafeCurrentTime);
|
|
139
177
|
}
|
|
140
178
|
}
|
|
141
179
|
};
|
|
@@ -151,7 +189,18 @@ export const updateAnimations = (element: AnimatableElement): void => {
|
|
|
151
189
|
});
|
|
152
190
|
updateVisualState(element, temporalState);
|
|
153
191
|
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
// Coordinate animations - use animation-specific visibility to prevent jumps at exact boundaries
|
|
193
|
+
const animationState = evaluateTemporalStateForAnimation(element);
|
|
194
|
+
if (animationState.isVisible) {
|
|
195
|
+
coordinateAnimationsForSingleElement(element);
|
|
156
196
|
}
|
|
197
|
+
|
|
198
|
+
// Coordinate animations for child elements using animation-specific visibility
|
|
199
|
+
deepGetTemporalElements(element).forEach((temporalElement) => {
|
|
200
|
+
const childAnimationState =
|
|
201
|
+
evaluateTemporalStateForAnimation(temporalElement);
|
|
202
|
+
if (childAnimationState.isVisible) {
|
|
203
|
+
coordinateAnimationsForSingleElement(temporalElement);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
157
206
|
};
|
|
@@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
4
4
|
|
|
5
5
|
import { ContextMixin } from "./ContextMixin.js";
|
|
6
6
|
|
|
7
|
-
// Required to test timeupdate event, we need a duration, and timegroups are a quick way to do that
|
|
8
7
|
import "../elements/EFTimegroup.js";
|
|
9
8
|
|
|
10
9
|
@customElement("test-context")
|
|
@@ -529,30 +528,6 @@ describe("ContextMixin", () => {
|
|
|
529
528
|
});
|
|
530
529
|
});
|
|
531
530
|
|
|
532
|
-
test("Time update event when the currentTimeMs changed", async () => {
|
|
533
|
-
const timegroup = document.createElement("ef-timegroup");
|
|
534
|
-
timegroup.mode = "fixed";
|
|
535
|
-
timegroup.duration = "10s";
|
|
536
|
-
|
|
537
|
-
const preview = document.createElement("test-context");
|
|
538
|
-
preview.append(timegroup);
|
|
539
|
-
document.body.append(preview);
|
|
540
|
-
|
|
541
|
-
type CurrentTimeEvent = CustomEvent<{ currentTimeMs: number }>;
|
|
542
|
-
|
|
543
|
-
// Expect the timeupdate event to be dispatched
|
|
544
|
-
const timeupdatePromise = new Promise<CurrentTimeEvent>((resolve) => {
|
|
545
|
-
preview.addEventListener(
|
|
546
|
-
"timeupdate",
|
|
547
|
-
(event: Event) => resolve(event as CurrentTimeEvent),
|
|
548
|
-
{ once: true },
|
|
549
|
-
);
|
|
550
|
-
});
|
|
551
|
-
preview.currentTimeMs = 1000;
|
|
552
|
-
const event = await timeupdatePromise;
|
|
553
|
-
expect(event.detail.currentTimeMs).toBe(1000);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
531
|
describe("Reactivity", () => {
|
|
557
532
|
test("should update durationMs when child tree changes", async () => {
|
|
558
533
|
const element = document.createElement("test-context-reactivity");
|
package/src/gui/ContextMixin.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { property, state } from "lit/decorators.js";
|
|
|
4
4
|
import { EF_RENDERING } from "../EF_RENDERING.ts";
|
|
5
5
|
import type { EFTimegroup } from "../elements/EFTimegroup.js";
|
|
6
6
|
import { globalURLTokenDeduplicator } from "../transcoding/cache/URLTokenDeduplicator.js";
|
|
7
|
+
import { currentTimeContext } from "./currentTimeContext.js";
|
|
8
|
+
import { durationContext } from "./durationContext.js";
|
|
7
9
|
import {
|
|
8
10
|
type EFConfiguration,
|
|
9
11
|
efConfigurationContext,
|
|
@@ -74,15 +76,12 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
74
76
|
targetTimegroup: EFTimegroup | null = null;
|
|
75
77
|
|
|
76
78
|
// Add reactive properties that depend on the targetTimegroup
|
|
77
|
-
@
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
79
|
+
@provide({ context: durationContext })
|
|
80
|
+
@property({ type: Number })
|
|
81
|
+
durationMs = 0;
|
|
81
82
|
|
|
82
|
-
@
|
|
83
|
-
|
|
84
|
-
return this.targetTimegroup?.endTimeMs ?? 0;
|
|
85
|
-
}
|
|
83
|
+
@property({ type: Number })
|
|
84
|
+
endTimeMs = 0;
|
|
86
85
|
|
|
87
86
|
@provide({ context: fetchContext })
|
|
88
87
|
fetch = async (url: string, init: RequestInit = {}) => {
|
|
@@ -248,7 +247,8 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
248
247
|
@property({ type: Boolean })
|
|
249
248
|
rendering = false;
|
|
250
249
|
|
|
251
|
-
@
|
|
250
|
+
@provide({ context: currentTimeContext })
|
|
251
|
+
@property({ type: Number })
|
|
252
252
|
currentTimeMs = Number.NaN;
|
|
253
253
|
|
|
254
254
|
#FPS = 30;
|
|
@@ -273,11 +273,24 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
273
273
|
}
|
|
274
274
|
} else if (mutation.type === "attributes") {
|
|
275
275
|
// Watch for attribute changes that might affect duration
|
|
276
|
+
const durationAffectingAttributes = [
|
|
277
|
+
"duration",
|
|
278
|
+
"mode",
|
|
279
|
+
"trimstart",
|
|
280
|
+
"trimend",
|
|
281
|
+
"sourcein",
|
|
282
|
+
"sourceout",
|
|
283
|
+
];
|
|
284
|
+
|
|
276
285
|
if (
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
durationAffectingAttributes.includes(
|
|
287
|
+
mutation.attributeName || "",
|
|
288
|
+
) ||
|
|
279
289
|
(mutation.target instanceof Element &&
|
|
280
290
|
(mutation.target.tagName === "EF-TIMEGROUP" ||
|
|
291
|
+
mutation.target.tagName === "EF-VIDEO" ||
|
|
292
|
+
mutation.target.tagName === "EF-AUDIO" ||
|
|
293
|
+
mutation.target.tagName === "EF-CAPTIONS" ||
|
|
281
294
|
mutation.target.closest("ef-timegroup")))
|
|
282
295
|
) {
|
|
283
296
|
shouldUpdate = true;
|
|
@@ -289,6 +302,8 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
289
302
|
// Trigger an update to ensure reactive properties recalculate
|
|
290
303
|
// Use a microtask to ensure DOM updates are complete
|
|
291
304
|
queueMicrotask(() => {
|
|
305
|
+
// Recalculate duration and endTime when timegroup changes
|
|
306
|
+
this.updateDurationProperties();
|
|
292
307
|
this.requestUpdate();
|
|
293
308
|
// Also ensure the targetTimegroup updates its computed properties
|
|
294
309
|
if (this.targetTimegroup) {
|
|
@@ -298,11 +313,29 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
298
313
|
}
|
|
299
314
|
});
|
|
300
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Update duration properties when timegroup changes
|
|
318
|
+
*/
|
|
319
|
+
updateDurationProperties(): void {
|
|
320
|
+
const newDuration = this.targetTimegroup?.durationMs ?? 0;
|
|
321
|
+
const newEndTime = this.targetTimegroup?.endTimeMs ?? 0;
|
|
322
|
+
|
|
323
|
+
if (this.durationMs !== newDuration) {
|
|
324
|
+
this.durationMs = newDuration;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (this.endTimeMs !== newEndTime) {
|
|
328
|
+
this.endTimeMs = newEndTime;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
301
332
|
connectedCallback(): void {
|
|
302
333
|
super.connectedCallback();
|
|
303
334
|
|
|
304
335
|
// Initialize targetTimegroup
|
|
305
336
|
this.targetTimegroup = this.querySelector("ef-timegroup");
|
|
337
|
+
// Initialize duration properties
|
|
338
|
+
this.updateDurationProperties();
|
|
306
339
|
|
|
307
340
|
this.#timegroupObserver.observe(this, {
|
|
308
341
|
childList: true,
|
|
@@ -342,15 +375,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
342
375
|
return;
|
|
343
376
|
}
|
|
344
377
|
this.targetTimegroup.currentTimeMs = this.currentTimeMs;
|
|
345
|
-
this.dispatchEvent(
|
|
346
|
-
new CustomEvent("timeupdate", {
|
|
347
|
-
detail: {
|
|
348
|
-
currentTimeMs: this.currentTimeMs,
|
|
349
|
-
progress:
|
|
350
|
-
this.currentTimeMs / this.targetTimegroup.durationMs,
|
|
351
|
-
},
|
|
352
|
-
}),
|
|
353
|
-
);
|
|
354
378
|
}
|
|
355
379
|
}
|
|
356
380
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { html, LitElement } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
3
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
import "../elements/EFTimegroup.js";
|
|
6
|
+
import { EFTargetable } from "../elements/TargetController.js";
|
|
7
|
+
import { ContextMixin } from "./ContextMixin.js";
|
|
8
|
+
import "./EFControls.js";
|
|
9
|
+
import { EFControls } from "./EFControls.js";
|
|
10
|
+
import "./EFPreview.js";
|
|
11
|
+
|
|
12
|
+
@customElement("test-context")
|
|
13
|
+
class TestContext extends EFTargetable(ContextMixin(LitElement)) {
|
|
14
|
+
render() {
|
|
15
|
+
return html`<slot></slot>`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Clean up localStorage
|
|
21
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
22
|
+
const key = localStorage.key(i);
|
|
23
|
+
if (typeof key !== "string") continue;
|
|
24
|
+
localStorage.removeItem(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Clean up DOM
|
|
28
|
+
while (document.body.children.length) {
|
|
29
|
+
document.body.children[0]?.remove();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("EFControls", () => {
|
|
34
|
+
test("should be defined", () => {
|
|
35
|
+
expect(EFControls).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("can find and connect to target preview by ID", async () => {
|
|
39
|
+
// Create a preview with an ID
|
|
40
|
+
const preview = document.createElement("ef-preview") as any;
|
|
41
|
+
preview.id = "test-preview";
|
|
42
|
+
|
|
43
|
+
// Add a timegroup to the preview to give it duration
|
|
44
|
+
const timegroup = document.createElement("ef-timegroup");
|
|
45
|
+
timegroup.mode = "fixed";
|
|
46
|
+
timegroup.duration = "10s";
|
|
47
|
+
preview.appendChild(timegroup);
|
|
48
|
+
|
|
49
|
+
document.body.appendChild(preview);
|
|
50
|
+
|
|
51
|
+
// Create controls targeting the preview
|
|
52
|
+
const controls = document.createElement("ef-controls");
|
|
53
|
+
controls.target = "test-preview";
|
|
54
|
+
document.body.appendChild(controls);
|
|
55
|
+
|
|
56
|
+
// Wait for both elements to complete their updates
|
|
57
|
+
await preview.updateComplete;
|
|
58
|
+
await controls.updateComplete;
|
|
59
|
+
|
|
60
|
+
// The controls should have found and connected to the preview
|
|
61
|
+
expect(controls.targetElement).toBe(preview);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("handles missing target gracefully", async () => {
|
|
65
|
+
const controls = document.createElement("ef-controls");
|
|
66
|
+
controls.target = "nonexistent-preview";
|
|
67
|
+
document.body.appendChild(controls);
|
|
68
|
+
|
|
69
|
+
// Wait for the controller to attempt connection
|
|
70
|
+
await controls.updateComplete;
|
|
71
|
+
|
|
72
|
+
// Should have no target but not crash
|
|
73
|
+
expect(controls.targetElement).toBe(null);
|
|
74
|
+
// Note: EFControls with context proxying doesn't have playing property
|
|
75
|
+
// It only proxies context requests to the target element
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("updates when target is set after connection", async () => {
|
|
79
|
+
const controls = document.createElement("ef-controls") as EFControls;
|
|
80
|
+
document.body.appendChild(controls);
|
|
81
|
+
|
|
82
|
+
// Initially no target
|
|
83
|
+
expect(controls.targetElement).toBe(null);
|
|
84
|
+
|
|
85
|
+
// Create preview
|
|
86
|
+
const preview = document.createElement("test-context") as TestContext;
|
|
87
|
+
preview.id = "test-preview";
|
|
88
|
+
|
|
89
|
+
const timegroup = document.createElement("ef-timegroup");
|
|
90
|
+
timegroup.mode = "fixed";
|
|
91
|
+
timegroup.duration = "10s";
|
|
92
|
+
preview.appendChild(timegroup);
|
|
93
|
+
|
|
94
|
+
document.body.appendChild(preview);
|
|
95
|
+
|
|
96
|
+
// Set target after both are connected
|
|
97
|
+
controls.target = "test-preview";
|
|
98
|
+
|
|
99
|
+
// Wait for both elements to complete their updates
|
|
100
|
+
await preview.updateComplete;
|
|
101
|
+
await controls.updateComplete;
|
|
102
|
+
|
|
103
|
+
// The controls should have found and connected to the target
|
|
104
|
+
expect(controls.targetElement).toBe(preview);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("disconnects from target when removed", async () => {
|
|
108
|
+
const preview = document.createElement("test-context") as TestContext;
|
|
109
|
+
preview.id = "test-preview";
|
|
110
|
+
|
|
111
|
+
const timegroup = document.createElement("ef-timegroup");
|
|
112
|
+
timegroup.mode = "fixed";
|
|
113
|
+
timegroup.duration = "10s";
|
|
114
|
+
preview.appendChild(timegroup);
|
|
115
|
+
|
|
116
|
+
document.body.appendChild(preview);
|
|
117
|
+
|
|
118
|
+
const controls = document.createElement("ef-controls") as EFControls;
|
|
119
|
+
controls.target = "test-preview";
|
|
120
|
+
document.body.appendChild(controls);
|
|
121
|
+
|
|
122
|
+
// Wait for both elements to complete their updates
|
|
123
|
+
await preview.updateComplete;
|
|
124
|
+
await controls.updateComplete;
|
|
125
|
+
|
|
126
|
+
// Should be connected
|
|
127
|
+
expect(controls.targetElement).toBe(preview);
|
|
128
|
+
|
|
129
|
+
// Disconnect the controls
|
|
130
|
+
document.body.removeChild(controls);
|
|
131
|
+
|
|
132
|
+
// After disconnection, targetElement persists but should have no effect
|
|
133
|
+
// (TargetController only clears targetElement when target is removed, not when consumer disconnects)
|
|
134
|
+
expect(controls.targetElement).toBe(preview);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("works with child control elements - EFTogglePlay", async () => {
|
|
138
|
+
// Import the control element
|
|
139
|
+
await import("./EFTogglePlay.js");
|
|
140
|
+
|
|
141
|
+
const preview = document.createElement("test-context") as TestContext;
|
|
142
|
+
preview.id = "test-preview";
|
|
143
|
+
preview.playing = false;
|
|
144
|
+
|
|
145
|
+
const timegroup = document.createElement("ef-timegroup");
|
|
146
|
+
timegroup.mode = "fixed";
|
|
147
|
+
timegroup.duration = "10s";
|
|
148
|
+
preview.appendChild(timegroup);
|
|
149
|
+
|
|
150
|
+
document.body.appendChild(preview);
|
|
151
|
+
|
|
152
|
+
const controls = document.createElement("ef-controls") as EFControls;
|
|
153
|
+
controls.target = "test-preview";
|
|
154
|
+
|
|
155
|
+
const togglePlay = document.createElement("ef-toggle-play");
|
|
156
|
+
controls.appendChild(togglePlay);
|
|
157
|
+
|
|
158
|
+
document.body.appendChild(controls);
|
|
159
|
+
|
|
160
|
+
// Wait for all elements to complete their updates
|
|
161
|
+
await preview.updateComplete;
|
|
162
|
+
await controls.updateComplete;
|
|
163
|
+
await togglePlay.updateComplete;
|
|
164
|
+
|
|
165
|
+
// The toggle play should be connected to the controls' context (which syncs with preview)
|
|
166
|
+
expect((togglePlay as any).playing).toBe(false);
|
|
167
|
+
|
|
168
|
+
// Test that clicking the toggle affects the preview
|
|
169
|
+
preview.playing = true;
|
|
170
|
+
await preview.updateComplete;
|
|
171
|
+
await togglePlay.updateComplete;
|
|
172
|
+
|
|
173
|
+
expect((togglePlay as any).playing).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { css, html, LitElement } from "lit";
|
|
2
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
import { ContextProxyController } from "../elements/ContextProxiesController.js";
|
|
5
|
+
import { TargetController } from "../elements/TargetController.js";
|
|
6
|
+
import type { ContextMixinInterface } from "./ContextMixin.js";
|
|
7
|
+
import { targetTimegroupContext } from "./ContextMixin.js";
|
|
8
|
+
import { currentTimeContext } from "./currentTimeContext.js";
|
|
9
|
+
import { durationContext } from "./durationContext.js";
|
|
10
|
+
import { efConfigurationContext } from "./EFConfiguration.js";
|
|
11
|
+
import { efContext } from "./efContext.js";
|
|
12
|
+
import { fetchContext } from "./fetchContext.js";
|
|
13
|
+
import { focusContext } from "./focusContext.js";
|
|
14
|
+
import { focusedElementContext } from "./focusedElementContext.js";
|
|
15
|
+
import { loopContext, playingContext } from "./playingContext.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* EFControls provides a way to control an ef-preview element that is not a direct ancestor.
|
|
19
|
+
* It bridges the contexts from a target preview element to its children controls.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* ```html
|
|
23
|
+
* <ef-preview id="my-preview">...</ef-preview>
|
|
24
|
+
*
|
|
25
|
+
* <ef-controls target="my-preview">
|
|
26
|
+
* <ef-toggle-play>
|
|
27
|
+
* <button slot="play">Play</button>
|
|
28
|
+
* <button slot="pause">Pause</button>
|
|
29
|
+
* </ef-toggle-play>
|
|
30
|
+
* <ef-scrubber></ef-scrubber>
|
|
31
|
+
* <ef-time-display></ef-time-display>
|
|
32
|
+
* </ef-controls>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
@customElement("ef-controls")
|
|
36
|
+
export class EFControls extends LitElement {
|
|
37
|
+
static styles = css`
|
|
38
|
+
:host {
|
|
39
|
+
display: block;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The ID of the ef-preview element to control
|
|
45
|
+
*/
|
|
46
|
+
@property({ type: String })
|
|
47
|
+
target = "";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The target element (set by TargetController)
|
|
51
|
+
*/
|
|
52
|
+
@state()
|
|
53
|
+
targetElement: ContextMixinInterface | null = null;
|
|
54
|
+
|
|
55
|
+
// @ts-expect-error controller is intentionally not referenced directly
|
|
56
|
+
#targetController = new TargetController(this);
|
|
57
|
+
|
|
58
|
+
// @ts-expect-error controller is intentionally not referenced directly
|
|
59
|
+
#contextProxyController = new ContextProxyController(this, {
|
|
60
|
+
target: () => this.targetElement,
|
|
61
|
+
contexts: [
|
|
62
|
+
playingContext,
|
|
63
|
+
loopContext,
|
|
64
|
+
currentTimeContext,
|
|
65
|
+
durationContext,
|
|
66
|
+
targetTimegroupContext,
|
|
67
|
+
focusedElementContext,
|
|
68
|
+
efContext,
|
|
69
|
+
fetchContext,
|
|
70
|
+
focusContext,
|
|
71
|
+
efConfigurationContext,
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
render() {
|
|
76
|
+
return html`<slot></slot>`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
declare global {
|
|
81
|
+
interface HTMLElementTagNameMap {
|
|
82
|
+
"ef-controls": EFControls;
|
|
83
|
+
}
|
|
84
|
+
}
|