@editframe/elements 0.45.2 → 0.45.4
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/DelayedLoadingState.js.map +1 -1
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.js +3 -3
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.js.map +1 -1
- package/dist/canvas/api/CanvasAPI.js.map +1 -1
- package/dist/canvas/getElementBounds.js.map +1 -1
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js +25 -23
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/canvas/selection/SelectionModel.js.map +1 -1
- package/dist/canvas/selection/selectionContext.js.map +1 -1
- package/dist/elements/ContainerInfo.js.map +1 -1
- package/dist/elements/CrossUpdateController.js.map +1 -1
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.js +1 -1
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
- package/dist/elements/EFMedia/TimingModel.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/GlobalInputCache.js.map +1 -1
- package/dist/elements/EFMedia/shared/PrecisionUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.js +9 -8
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +7 -8
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +4 -4
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.js.map +1 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SampleBuffer.js.map +1 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.js.map +1 -1
- package/dist/elements/durationConverter.js.map +1 -1
- package/dist/elements/easingUtils.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js.map +1 -1
- package/dist/elements/updateAnimations.js +1 -1
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +4 -4
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayItem.js.map +1 -1
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.js.map +1 -1
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFPreview.js.map +1 -1
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTogglePlay.js.map +1 -1
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +4 -4
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin2.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/currentTimeContext.js.map +1 -1
- package/dist/gui/efContext.js.map +1 -1
- package/dist/gui/fetchContext.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -1
- package/dist/gui/panZoomTransformContext.js.map +1 -1
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.js +0 -1
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/transformCalculations.js.map +1 -1
- package/dist/gui/transformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.d.ts +4 -4
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/BridgeSpanExporter.js.map +1 -1
- package/dist/otel/setupBrowserTracing.js.map +1 -1
- package/dist/otel/tracingHelpers.js.map +1 -1
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js.map +1 -1
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +2 -44
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +2 -2
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +2 -2
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js.map +1 -1
- package/dist/preview/rendering/loadImage.js.map +1 -1
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/utils/LRUCache.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#isEditframeDomain","#apiHost","#targetTemporal","#subscribedController","#controllerSubscribed","#onControllerUpdate","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#collectUndefinedEFTags","#retryTemporalDiscovery","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport {\n type EFConfiguration,\n efConfigurationContext,\n} from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\nimport { shouldSignUrl } from \"./shouldSignUrl.js\";\n\nexport const targetTemporalContext =\n createContext<TemporalMixinInterface | null>(Symbol(\"target-temporal\"));\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n contextMixinSymbol in value.constructor\n );\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (\n element: Element,\n ): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n #subscribedController: any = null;\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (\n this.#targetTemporal === value &&\n value?.playbackController === this.#subscribedController &&\n this.#controllerSubscribed\n )\n return;\n\n // Unsubscribe from old controller updates\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n if (init.body) {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n }\n\n if (\n !EF_RENDERING() &&\n this.signingURL &&\n shouldSignUrl(url, window.location.origin)\n ) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n init.headers ||= {};\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n // Only include credentials for same-origin requests where session cookies\n // are relevant. For cross-origin requests without a signing URL, credentials\n // cause CORS failures when the server responds with Access-Control-Allow-Origin: *\n if (!shouldSignUrl(url, window.location.origin)) {\n init.credentials = \"include\";\n }\n\n if (this.#isEditframeDomain(url)) {\n console.warn(\n `[Editframe] Request to ${new URL(url).hostname} has no signing URL configured. ` +\n `Ensure <ef-configuration signing-url=\"...\"> is an ancestor of your <ef-preview> or <ef-workbench>.`,\n );\n }\n }\n\n try {\n const fetchPromise = fetch(url, init);\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n // For AbortErrors, re-throw directly without modification\n // DOMException properties like 'name' are read-only\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n\n console.error(\n \"ContextMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor =\n error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties (except for DOMException which has read-only properties)\n if (error instanceof Error && !(error instanceof DOMException)) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\n \"ContextMixin fetch error (synchronous)\",\n url,\n error,\n window.location.href,\n );\n throw error;\n }\n };\n\n #isEditframeDomain(url: string): boolean {\n try {\n const hostname = new URL(url).hostname;\n return (\n hostname === \"editframe.dev\" ||\n hostname === \"editframe.com\" ||\n hostname.endsWith(\".editframe.dev\") ||\n hostname.endsWith(\".editframe.com\")\n );\n } catch {\n return false;\n }\n }\n\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return (\n this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN\n );\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n const undefinedEFTags = new Set<string>();\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (\n mutation.target instanceof Element &&\n isEFTemporal(mutation.target)\n ) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n\n // Collect ef-* tags from added nodes that haven't upgraded yet.\n // When React hydrates or TimelineRoot renders, the custom element\n // may be inserted before its class is defined, so isEFTemporal()\n // returns false. We need to retry after the element upgrades.\n if (!this.targetTemporal) {\n for (const node of mutation.addedNodes) {\n if (node instanceof Element) {\n this.#collectUndefinedEFTags(node, undefinedEFTags);\n }\n }\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(\n mutation.attributeName || \"\",\n ) ||\n (mutation.target instanceof Element &&\n isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (undefinedEFTags.size > 0) {\n this.#retryTemporalDiscovery(undefinedEFTags);\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Recursively collect ef-* tag names from an element tree that\n * have not yet been registered as custom elements.\n */\n #collectUndefinedEFTags(el: Element, tags: Set<string>): void {\n const tag = el.tagName.toLowerCase();\n if (tag.startsWith(\"ef-\") && !customElements.get(tag)) {\n tags.add(tag);\n }\n for (const child of el.children) {\n this.#collectUndefinedEFTags(child, tags);\n }\n }\n\n /**\n * Wait for unregistered ef-* custom elements to upgrade, then\n * retry findRootTemporal(). Mirrors the whenDefined pattern in play().\n */\n async #retryTemporalDiscovery(tags: Set<string>): Promise<void> {\n await Promise.all(\n [...tags].map((tag) => customElements.whenDefined(tag).catch(() => {})),\n );\n\n if (this.targetTemporal) return; // already found by another path\n\n const found = this.findRootTemporal();\n if (found) {\n this.targetTemporal = found;\n await (found as any).updateComplete;\n this.updateDurationProperties();\n this.requestUpdate();\n }\n }\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available or changes\n const currentController = this.#targetTemporal?.playbackController;\n if (\n currentController &&\n (!this.#controllerSubscribed ||\n this.#subscribedController !== currentController)\n ) {\n // Unsubscribe from old controller if it changed\n if (\n this.#subscribedController &&\n this.#subscribedController !== currentController\n ) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n }\n currentController.addListener(this.#onControllerUpdate);\n this.#controllerSubscribed = true;\n this.#subscribedController = currentController;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n currentController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) =>\n customElements.whenDefined(tag).catch(() => {}),\n ),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n try {\n const audioContext = new AudioContext({ latencyHint: \"playback\" });\n audioContext.resume();\n this.targetTemporal.playbackController.setPendingAudioContext(\n audioContext,\n );\n } catch (error) {\n console.warn(\n \"Failed to create/resume AudioContext synchronously:\",\n error,\n );\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#isEditframeDomain","#apiHost","#targetTemporal","#subscribedController","#controllerSubscribed","#onControllerUpdate","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#collectUndefinedEFTags","#retryTemporalDiscovery","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport { isEFTemporal, type TemporalMixinInterface } from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { type EFConfiguration, efConfigurationContext } from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\nimport { shouldSignUrl } from \"./shouldSignUrl.js\";\n\nexport const targetTemporalContext = createContext<TemporalMixinInterface | null>(\n Symbol(\"target-temporal\"),\n);\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return typeof value === \"object\" && value !== null && contextMixinSymbol in value.constructor;\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (element: Element): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n #subscribedController: any = null;\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (\n this.#targetTemporal === value &&\n value?.playbackController === this.#subscribedController &&\n this.#controllerSubscribed\n )\n return;\n\n // Unsubscribe from old controller updates\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n if (init.body) {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n }\n\n if (!EF_RENDERING() && this.signingURL && shouldSignUrl(url, window.location.origin)) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n init.headers ||= {};\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n // Only include credentials for same-origin requests where session cookies\n // are relevant. For cross-origin requests without a signing URL, credentials\n // cause CORS failures when the server responds with Access-Control-Allow-Origin: *\n if (!shouldSignUrl(url, window.location.origin)) {\n init.credentials = \"include\";\n }\n\n if (this.#isEditframeDomain(url)) {\n console.warn(\n `[Editframe] Request to ${new URL(url).hostname} has no signing URL configured. ` +\n `Ensure <ef-configuration signing-url=\"...\"> is an ancestor of your <ef-preview> or <ef-workbench>.`,\n );\n }\n }\n\n try {\n const fetchPromise = fetch(url, init);\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n // For AbortErrors, re-throw directly without modification\n // DOMException properties like 'name' are read-only\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n\n console.error(\"ContextMixin fetch error\", url, error, window.location.href);\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor = error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties (except for DOMException which has read-only properties)\n if (error instanceof Error && !(error instanceof DOMException)) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\"ContextMixin fetch error (synchronous)\", url, error, window.location.href);\n throw error;\n }\n };\n\n #isEditframeDomain(url: string): boolean {\n try {\n const hostname = new URL(url).hostname;\n return (\n hostname === \"editframe.dev\" ||\n hostname === \"editframe.com\" ||\n hostname.endsWith(\".editframe.dev\") ||\n hostname.endsWith(\".editframe.com\")\n );\n } catch {\n return false;\n }\n }\n\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN;\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n const undefinedEFTags = new Set<string>();\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (mutation.target instanceof Element && isEFTemporal(mutation.target)) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n\n // Collect ef-* tags from added nodes that haven't upgraded yet.\n // When React hydrates or TimelineRoot renders, the custom element\n // may be inserted before its class is defined, so isEFTemporal()\n // returns false. We need to retry after the element upgrades.\n if (!this.targetTemporal) {\n for (const node of mutation.addedNodes) {\n if (node instanceof Element) {\n this.#collectUndefinedEFTags(node, undefinedEFTags);\n }\n }\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(mutation.attributeName || \"\") ||\n (mutation.target instanceof Element && isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (undefinedEFTags.size > 0) {\n this.#retryTemporalDiscovery(undefinedEFTags);\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Recursively collect ef-* tag names from an element tree that\n * have not yet been registered as custom elements.\n */\n #collectUndefinedEFTags(el: Element, tags: Set<string>): void {\n const tag = el.tagName.toLowerCase();\n if (tag.startsWith(\"ef-\") && !customElements.get(tag)) {\n tags.add(tag);\n }\n for (const child of el.children) {\n this.#collectUndefinedEFTags(child, tags);\n }\n }\n\n /**\n * Wait for unregistered ef-* custom elements to upgrade, then\n * retry findRootTemporal(). Mirrors the whenDefined pattern in play().\n */\n async #retryTemporalDiscovery(tags: Set<string>): Promise<void> {\n await Promise.all([...tags].map((tag) => customElements.whenDefined(tag).catch(() => {})));\n\n if (this.targetTemporal) return; // already found by another path\n\n const found = this.findRootTemporal();\n if (found) {\n this.targetTemporal = found;\n await (found as any).updateComplete;\n this.updateDurationProperties();\n this.requestUpdate();\n }\n }\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available or changes\n const currentController = this.#targetTemporal?.playbackController;\n if (\n currentController &&\n (!this.#controllerSubscribed || this.#subscribedController !== currentController)\n ) {\n // Unsubscribe from old controller if it changed\n if (this.#subscribedController && this.#subscribedController !== currentController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n }\n currentController.addListener(this.#onControllerUpdate);\n this.#controllerSubscribed = true;\n this.#subscribedController = currentController;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n currentController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) => customElements.whenDefined(tag).catch(() => {})),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n try {\n const audioContext = new AudioContext({ latencyHint: \"playback\" });\n audioContext.resume();\n this.targetTemporal.playbackController.setPendingAudioContext(audioContext);\n } catch (error) {\n console.warn(\"Failed to create/resume AudioContext synchronously:\", error);\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Controllable.js","names":["durationPollInterval: ReturnType<typeof setInterval> | null"],"sources":["../../src/gui/Controllable.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Controllable.js","names":["durationPollInterval: ReturnType<typeof setInterval> | null"],"sources":["../../src/gui/Controllable.ts"],"sourcesContent":["// oxlint-disable-next-line no-unused-vars -- used as base class in declare class below\nimport type { LitElement } from \"lit\";\n\nimport { isEFTemporal, type TemporalMixinInterface } from \"../elements/EFTemporal.js\";\nimport { type ContextMixinInterface, isContextMixin } from \"./ContextMixin.js\";\nimport type { PlaybackControllerUpdateEvent } from \"./PlaybackController.js\";\n\nexport declare class ControllableInterface extends LitElement {\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n durationMs: number;\n play(): void | Promise<void>;\n pause(): void;\n}\n\nexport function isControllable(value: any): value is ControllableInterface {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n\n if (isContextMixin(value)) {\n return true;\n }\n\n if (isEFTemporal(value)) {\n const temporal = value as TemporalMixinInterface;\n return temporal.playbackController !== undefined;\n }\n\n return false;\n}\n\nexport type ControllableElement =\n | ContextMixinInterface\n | (TemporalMixinInterface & {\n playbackController: NonNullable<TemporalMixinInterface[\"playbackController\"]>;\n });\n\n// ============================================================================\n// Core Concept: Controllable Target Type\n// ============================================================================\n// A controllable target is either a context-providing wrapper (EFPreview)\n// OR a direct temporal element with its own playback controller.\n// This enumeration makes the mental model explicit.\n// ============================================================================\n\nexport type ControllableTargetType = \"context-provider\" | \"direct-temporal\" | \"none\";\n\n/**\n * Determines the type of controllable target for subscription purposes.\n *\n * - \"context-provider\": Target is a ContextMixin (like EFPreview) that provides contexts\n * - \"direct-temporal\": Target is a root temporal element with its own playbackController\n * - \"none\": Target is not controllable (null, undefined, or nested temporal)\n */\nexport function determineTargetType(target: unknown): ControllableTargetType {\n if (!target) return \"none\";\n\n if (isContextMixin(target)) {\n return \"context-provider\";\n }\n\n if (isEFTemporal(target)) {\n const temporal = target as TemporalMixinInterface;\n // Only root temporal elements have playbackController\n // Nested elements delegate to their root\n if (temporal.playbackController) {\n return \"direct-temporal\";\n }\n }\n\n return \"none\";\n}\n\n// ============================================================================\n// Subscription Interface\n// ============================================================================\n// Abstracts the mechanism of subscribing to playback state updates.\n// Different target types use different mechanisms (context vs direct listener).\n// ============================================================================\n\nexport interface SubscriptionCallbacks {\n onPlayingChange(value: boolean): void;\n onLoopChange(value: boolean): void;\n onCurrentTimeMsChange(value: number): void;\n onDurationMsChange(value: number): void;\n onTargetTemporalChange(value: TemporalMixinInterface | null): void;\n onFocusedElementChange?(value: HTMLElement | undefined): void;\n}\n\nexport interface ControllableSubscription {\n unsubscribe(): void;\n}\n\n/**\n * Creates a subscription to a direct temporal element's playback controller.\n * Used when EFControls targets a temporal element directly (not wrapped in EFPreview).\n */\nexport function createDirectTemporalSubscription(\n target: TemporalMixinInterface & HTMLElement,\n callbacks: SubscriptionCallbacks,\n): ControllableSubscription {\n const controller = target.playbackController!;\n\n // Initial sync - propagate current state immediately\n callbacks.onPlayingChange(controller.playing);\n callbacks.onLoopChange(controller.loop);\n callbacks.onCurrentTimeMsChange(controller.currentTimeMs);\n callbacks.onDurationMsChange(target.durationMs);\n callbacks.onTargetTemporalChange(target);\n\n // Subscribe to playback controller updates\n const listener = (event: PlaybackControllerUpdateEvent) => {\n switch (event.property) {\n case \"playing\":\n callbacks.onPlayingChange(event.value as boolean);\n break;\n case \"loop\":\n callbacks.onLoopChange(event.value as boolean);\n break;\n case \"currentTimeMs\":\n callbacks.onCurrentTimeMsChange(event.value as number);\n break;\n }\n };\n controller.addListener(listener);\n\n // Watch for duration changes via MutationObserver on duration-affecting attributes\n const durationObserver = new MutationObserver(() => {\n callbacks.onDurationMsChange(target.durationMs);\n });\n durationObserver.observe(target, {\n attributes: true,\n attributeFilter: [\"duration\", \"trimstart\", \"trimend\", \"sourcein\", \"sourceout\"],\n subtree: true,\n });\n\n // For media elements (ef-video, ef-audio), also watch for intrinsic duration changes\n // The intrinsicDurationMs comes from mediaEngineTask which loads asynchronously\n let lastKnownDuration = target.durationMs;\n let durationPollInterval: ReturnType<typeof setInterval> | null = null;\n\n // If duration is currently 0, poll until it becomes available\n // This handles the case where media hasn't loaded yet\n if (lastKnownDuration === 0) {\n durationPollInterval = setInterval(() => {\n const currentDuration = target.durationMs;\n if (currentDuration !== lastKnownDuration) {\n lastKnownDuration = currentDuration;\n callbacks.onDurationMsChange(currentDuration);\n // Once we have a non-zero duration, stop polling\n if (currentDuration > 0 && durationPollInterval) {\n clearInterval(durationPollInterval);\n durationPollInterval = null;\n }\n }\n }, 100); // Check every 100ms\n }\n\n return {\n unsubscribe: () => {\n controller.removeListener(listener);\n durationObserver.disconnect();\n if (durationPollInterval) {\n clearInterval(durationPollInterval);\n }\n },\n };\n}\n"],"mappings":";;;;AAgBA,SAAgB,eAAe,OAA4C;AACzE,KAAI,CAAC,SAAS,OAAO,UAAU,SAC7B,QAAO;AAGT,KAAI,eAAe,MAAM,CACvB,QAAO;AAGT,KAAI,aAAa,MAAM,CAErB,QADiB,MACD,uBAAuB;AAGzC,QAAO;;;;;;;;;AA0BT,SAAgB,oBAAoB,QAAyC;AAC3E,KAAI,CAAC,OAAQ,QAAO;AAEpB,KAAI,eAAe,OAAO,CACxB,QAAO;AAGT,KAAI,aAAa,OAAO,EAItB;MAHiB,OAGJ,mBACX,QAAO;;AAIX,QAAO;;;;;;AA2BT,SAAgB,iCACd,QACA,WAC0B;CAC1B,MAAM,aAAa,OAAO;AAG1B,WAAU,gBAAgB,WAAW,QAAQ;AAC7C,WAAU,aAAa,WAAW,KAAK;AACvC,WAAU,sBAAsB,WAAW,cAAc;AACzD,WAAU,mBAAmB,OAAO,WAAW;AAC/C,WAAU,uBAAuB,OAAO;CAGxC,MAAM,YAAY,UAAyC;AACzD,UAAQ,MAAM,UAAd;GACE,KAAK;AACH,cAAU,gBAAgB,MAAM,MAAiB;AACjD;GACF,KAAK;AACH,cAAU,aAAa,MAAM,MAAiB;AAC9C;GACF,KAAK;AACH,cAAU,sBAAsB,MAAM,MAAgB;AACtD;;;AAGN,YAAW,YAAY,SAAS;CAGhC,MAAM,mBAAmB,IAAI,uBAAuB;AAClD,YAAU,mBAAmB,OAAO,WAAW;GAC/C;AACF,kBAAiB,QAAQ,QAAQ;EAC/B,YAAY;EACZ,iBAAiB;GAAC;GAAY;GAAa;GAAW;GAAY;GAAY;EAC9E,SAAS;EACV,CAAC;CAIF,IAAI,oBAAoB,OAAO;CAC/B,IAAIA,uBAA8D;AAIlE,KAAI,sBAAsB,EACxB,wBAAuB,kBAAkB;EACvC,MAAM,kBAAkB,OAAO;AAC/B,MAAI,oBAAoB,mBAAmB;AACzC,uBAAoB;AACpB,aAAU,mBAAmB,gBAAgB;AAE7C,OAAI,kBAAkB,KAAK,sBAAsB;AAC/C,kBAAc,qBAAqB;AACnC,2BAAuB;;;IAG1B,IAAI;AAGT,QAAO,EACL,mBAAmB;AACjB,aAAW,eAAe,SAAS;AACnC,mBAAiB,YAAY;AAC7B,MAAI,qBACF,eAAc,qBAAqB;IAGxC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit21 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html20 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFActiveRootTemporal.d.ts
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ import * as lit_html21 from "lit-html";
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
declare class EFActiveRootTemporal extends LitElement {
|
|
17
|
-
static styles:
|
|
17
|
+
static styles: lit21.CSSResult;
|
|
18
18
|
/**
|
|
19
19
|
* Canvas element ID or selector to bind to.
|
|
20
20
|
* If not specified, will search for the nearest ef-canvas ancestor.
|
|
@@ -38,7 +38,7 @@ declare class EFActiveRootTemporal extends LitElement {
|
|
|
38
38
|
* Remove event listener.
|
|
39
39
|
*/
|
|
40
40
|
private removeListener;
|
|
41
|
-
render():
|
|
41
|
+
render(): lit_html20.TemplateResult<1>;
|
|
42
42
|
}
|
|
43
43
|
declare global {
|
|
44
44
|
interface HTMLElementTagNameMap {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFActiveRootTemporal.js","names":["EFActiveRootTemporal"],"sources":["../../src/gui/EFActiveRootTemporal.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport type { EFCanvas } from \"../canvas/EFCanvas.js\";\nimport type { TemporalMixinInterface } from \"../elements/EFTemporal.js\";\n\n/**\n * Displays the ID of the active root temporal element from a canvas.\n * Automatically updates when selection changes.\n *\n * @example\n * ```html\n * <ef-active-root-temporal canvas=\"canvas\"></ef-active-root-temporal>\n * ```\n */\n@customElement(\"ef-active-root-temporal\")\nexport class EFActiveRootTemporal extends LitElement {\n static styles = css`\n :host {\n display: inline-block;\n }\n `;\n\n /**\n * Canvas element ID or selector to bind to.\n * If not specified, will search for the nearest ef-canvas ancestor.\n */\n @property({ type: String })\n canvas = \"\";\n\n @state()\n private activeRootTemporal: (TemporalMixinInterface & HTMLElement) | null
|
|
1
|
+
{"version":3,"file":"EFActiveRootTemporal.js","names":["EFActiveRootTemporal"],"sources":["../../src/gui/EFActiveRootTemporal.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport type { EFCanvas } from \"../canvas/EFCanvas.js\";\nimport type { TemporalMixinInterface } from \"../elements/EFTemporal.js\";\n\n/**\n * Displays the ID of the active root temporal element from a canvas.\n * Automatically updates when selection changes.\n *\n * @example\n * ```html\n * <ef-active-root-temporal canvas=\"canvas\"></ef-active-root-temporal>\n * ```\n */\n@customElement(\"ef-active-root-temporal\")\nexport class EFActiveRootTemporal extends LitElement {\n static styles = css`\n :host {\n display: inline-block;\n }\n `;\n\n /**\n * Canvas element ID or selector to bind to.\n * If not specified, will search for the nearest ef-canvas ancestor.\n */\n @property({ type: String })\n canvas = \"\";\n\n @state()\n private activeRootTemporal: (TemporalMixinInterface & HTMLElement) | null = null;\n\n private canvasElement: EFCanvas | null = null;\n private activeroottemporalchangeHandler?: () => void;\n\n connectedCallback(): void {\n super.connectedCallback();\n this.findCanvas();\n this.setupListener();\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeListener();\n }\n\n protected updated(changedProperties: Map<string | number | symbol, unknown>): void {\n if (changedProperties.has(\"canvas\")) {\n this.findCanvas();\n this.setupListener();\n }\n }\n\n /**\n * Find the canvas element to bind to.\n */\n private findCanvas(): void {\n // If canvas attribute is set, use it\n if (this.canvas) {\n const byId = document.getElementById(this.canvas);\n if (byId && byId.tagName === \"EF-CANVAS\") {\n this.canvasElement = byId as EFCanvas;\n return;\n }\n\n // Try as selector\n try {\n const bySelector = document.querySelector(this.canvas) as EFCanvas | null;\n if (bySelector && bySelector.tagName === \"EF-CANVAS\") {\n this.canvasElement = bySelector;\n return;\n }\n } catch {\n // Invalid selector, ignore\n }\n }\n\n // Fall back to nearest ancestor\n const ancestor = this.closest(\"ef-canvas\") as EFCanvas | null;\n if (ancestor) {\n this.canvasElement = ancestor;\n return;\n }\n\n this.canvasElement = null;\n }\n\n /**\n * Setup listener for activeroottemporalchange events.\n */\n private setupListener(): void {\n this.removeListener();\n\n if (!this.canvasElement) {\n this.activeRootTemporal = null;\n return;\n }\n\n // Get initial value\n const canvasEl = this.canvasElement as any;\n this.activeRootTemporal = canvasEl.activeRootTemporal || null;\n\n // Listen for changes\n this.activeroottemporalchangeHandler = () => {\n const canvasEl = this.canvasElement as any;\n this.activeRootTemporal = canvasEl.activeRootTemporal || null;\n };\n\n this.canvasElement.addEventListener(\n \"activeroottemporalchange\",\n this.activeroottemporalchangeHandler,\n );\n }\n\n /**\n * Remove event listener.\n */\n private removeListener(): void {\n if (this.canvasElement && this.activeroottemporalchangeHandler) {\n this.canvasElement.removeEventListener(\n \"activeroottemporalchange\",\n this.activeroottemporalchangeHandler,\n );\n this.activeroottemporalchangeHandler = undefined;\n }\n }\n\n render() {\n const displayText = this.activeRootTemporal?.id || this.textContent || \"None\";\n return html`<span>${displayText}</span>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-active-root-temporal\": EFActiveRootTemporal;\n }\n}\n"],"mappings":";;;;;AAeO,iCAAMA,+BAA6B,WAAW;;;gBAY1C;4BAGmE;uBAEnC;;;gBAhBzB,GAAG;;;;;;CAmBnB,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,OAAK,YAAY;AACjB,OAAK,eAAe;;CAGtB,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,OAAK,gBAAgB;;CAGvB,AAAU,QAAQ,mBAAiE;AACjF,MAAI,kBAAkB,IAAI,SAAS,EAAE;AACnC,QAAK,YAAY;AACjB,QAAK,eAAe;;;;;;CAOxB,AAAQ,aAAmB;AAEzB,MAAI,KAAK,QAAQ;GACf,MAAM,OAAO,SAAS,eAAe,KAAK,OAAO;AACjD,OAAI,QAAQ,KAAK,YAAY,aAAa;AACxC,SAAK,gBAAgB;AACrB;;AAIF,OAAI;IACF,MAAM,aAAa,SAAS,cAAc,KAAK,OAAO;AACtD,QAAI,cAAc,WAAW,YAAY,aAAa;AACpD,UAAK,gBAAgB;AACrB;;WAEI;;EAMV,MAAM,WAAW,KAAK,QAAQ,YAAY;AAC1C,MAAI,UAAU;AACZ,QAAK,gBAAgB;AACrB;;AAGF,OAAK,gBAAgB;;;;;CAMvB,AAAQ,gBAAsB;AAC5B,OAAK,gBAAgB;AAErB,MAAI,CAAC,KAAK,eAAe;AACvB,QAAK,qBAAqB;AAC1B;;AAKF,OAAK,qBADY,KAAK,cACa,sBAAsB;AAGzD,OAAK,wCAAwC;AAE3C,QAAK,qBADY,KAAK,cACa,sBAAsB;;AAG3D,OAAK,cAAc,iBACjB,4BACA,KAAK,gCACN;;;;;CAMH,AAAQ,iBAAuB;AAC7B,MAAI,KAAK,iBAAiB,KAAK,iCAAiC;AAC9D,QAAK,cAAc,oBACjB,4BACA,KAAK,gCACN;AACD,QAAK,kCAAkC;;;CAI3C,SAAS;AAEP,SAAO,IAAI,SADS,KAAK,oBAAoB,MAAM,KAAK,eAAe,OACvC;;;YAvGjC,SAAS,EAAE,MAAM,QAAQ,CAAC;YAG1B,OAAO;mCAfT,cAAc,0BAA0B"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit7 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html7 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFConfiguration.d.ts
|
|
6
6
|
declare class EFConfiguration extends LitElement {
|
|
7
|
-
static styles:
|
|
7
|
+
static styles: lit7.CSSResult[];
|
|
8
8
|
efConfiguration: this;
|
|
9
9
|
apiHost?: string;
|
|
10
10
|
signingURL: string;
|
|
11
|
-
render():
|
|
11
|
+
render(): lit_html7.TemplateResult<1>;
|
|
12
12
|
}
|
|
13
13
|
declare global {
|
|
14
14
|
interface HTMLElementTagNameMap {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFControls.js","names":["EFControls","#subscribeAbortController","#contextUnsubscribeMap","#directTemporalSubscription","#subscribeToContextProvider","#subscribeToDirectTemporal","#waitForTemporalToInitialize","#unsubscribe","#subscribe","#resubscribe"],"sources":["../../src/gui/EFControls.ts"],"sourcesContent":["import { type Context, createContext, provide } from \"@lit/context\";\nimport { css, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { attachContextRoot } from \"../attachContextRoot.js\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { TargetController } from \"../elements/TargetController.js\";\nimport { targetTemporalContext } from \"./ContextMixin.js\";\nimport {\n type ControllableInterface,\n type ControllableSubscription,\n createDirectTemporalSubscription,\n determineTargetType,\n} from \"./Controllable.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { efContext } from \"./efContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\nattachContextRoot();\n\nclass ContextRequestEvent extends Event {\n context: Context<any, any>;\n contextTarget: Element;\n callback: (proxy: EFControls, value: any) => void;\n subscribe: boolean;\n\n constructor(\n context: Context<any, any>,\n contextTarget: Element,\n callback: (proxy: EFControls, value: any) => void,\n subscribe: boolean,\n ) {\n super(\"context-request\", { bubbles: true, composed: true });\n this.context = context;\n this.contextTarget = contextTarget;\n this.callback = callback;\n this.subscribe = subscribe ?? false;\n }\n}\n\nconst proxiedContexts = [\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: boolean) => (proxy.playing = value),\n playingContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: boolean) => (proxy.loop = value),\n loopContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: number) => (proxy.currentTimeMs = value),\n currentTimeContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: number) => (proxy.durationMs = value),\n durationContext,\n ],\n [\n (proxy: EFControls, value: TemporalMixinInterface) =>\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy.targetTemporal = value),\n targetTemporalContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: HTMLElement) => (proxy.focusedElement = value),\n focusedElementContext,\n ],\n [\n (proxy: EFControls, value: HTMLElement) =>\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy.focusContext.focusedElement = value),\n focusContext,\n ],\n] as const;\n\nexport const testContext = createContext<string | null>(\"test\");\n\n/**\n * EFControls provides a way to control an ef-preview element that is not a direct ancestor.\n * It bridges the contexts from a target preview element to its children controls.\n *\n * Usage:\n * ```html\n * <ef-preview id=\"my-preview\">...</ef-preview>\n *\n * <ef-controls target=\"my-preview\">\n * <ef-toggle-play>\n * <button slot=\"play\">Play</button>\n * <button slot=\"pause\">Pause</button>\n * </ef-toggle-play>\n * <ef-scrubber></ef-scrubber>\n * <ef-time-display></ef-time-display>\n * </ef-controls>\n * ```\n */\n@customElement(\"ef-controls\")\nexport class EFControls extends LitElement {\n static styles = css`\n :host {\n display: block;\n }\n `;\n\n createRenderRoot() {\n return this;\n }\n\n /**\n * The ID of the ef-preview element to control\n */\n @property({ type: String })\n target = \"\";\n\n /**\n * The target element (set by TargetController)\n */\n @provide({ context: efContext })\n @state()\n targetElement: ControllableInterface | null = null;\n\n @provide({ context: playingContext })\n @state()\n playing = false;\n\n @provide({ context: loopContext })\n @state()\n loop = false;\n\n @provide({ context: currentTimeContext })\n @state()\n currentTimeMs = 0;\n\n @provide({ context: durationContext })\n @state()\n durationMs = 0;\n\n @provide({ context: targetTemporalContext })\n @state()\n targetTemporal: TemporalMixinInterface | null = null;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n // @ts-expect-error controller is intentionally not referenced directly\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for side effects\n #targetController = new TargetController(this);\n\n // Subscription tracking for different target types\n #contextUnsubscribeMap = new Map<Context<any, any>, () => void>();\n #directTemporalSubscription: ControllableSubscription | null = null;\n\n #unsubscribe() {\n // Abort any pending async subscription attempts\n this.#subscribeAbortController?.abort();\n this.#subscribeAbortController = null;\n\n // Unsubscribe from context-provider subscriptions\n for (const unsubscribe of this.#contextUnsubscribeMap.values()) {\n unsubscribe();\n }\n this.#contextUnsubscribeMap.clear();\n\n // Unsubscribe from direct-temporal subscription\n if (this.#directTemporalSubscription) {\n this.#directTemporalSubscription.unsubscribe();\n this.#directTemporalSubscription = null;\n }\n\n // Note: We don't reset state here because:\n // 1. During resubscribe, the new subscription will immediately provide values\n // 2. During disconnect, the element is being removed anyway\n // Resetting state would cause race conditions with context consumers\n }\n\n #subscribeAbortController: AbortController | null = null;\n\n #subscribe() {\n if (!this.targetElement) return;\n\n // Cancel any pending async subscription\n this.#subscribeAbortController?.abort();\n this.#subscribeAbortController = new AbortController();\n\n const targetType = determineTargetType(this.targetElement);\n\n switch (targetType) {\n case \"context-provider\":\n this.#subscribeToContextProvider();\n break;\n case \"direct-temporal\":\n this.#subscribeToDirectTemporal();\n break;\n case \"none\":\n // Target might be a temporal that hasn't initialized yet\n // Try to wait for it to become controllable\n this.#waitForTemporalToInitialize();\n break;\n }\n }\n\n /**\n * Wait for a temporal element to initialize its playbackController.\n * This handles the case where we target a temporal element before it has\n * completed initialization and created its playbackController.\n *\n * Note: playbackController is created in EFTemporal's connectedCallback via\n * `this.updateComplete.then(() => this.didBecomeRoot())`, so we need to wait\n * past updateComplete for the .then() callback to run.\n */\n async #waitForTemporalToInitialize() {\n if (!this.targetElement || !isEFTemporal(this.targetElement)) return;\n\n const temporal = this.targetElement as TemporalMixinInterface & HTMLElement;\n const signal = this.#subscribeAbortController?.signal;\n\n // Wait for the element to finish its update cycle\n await temporal.updateComplete;\n if (signal?.aborted) return;\n\n // playbackController is created in a .then() callback after updateComplete,\n // so we need to wait for that microtask to complete\n if (!temporal.playbackController) {\n await new Promise((resolve) => requestAnimationFrame(resolve));\n if (signal?.aborted) return;\n await temporal.updateComplete;\n if (signal?.aborted) return;\n }\n\n // Check again if it now has a playbackController\n if (temporal.playbackController) {\n this.#subscribeToDirectTemporal();\n }\n }\n\n /**\n * Subscribe to a context-providing target (like EFPreview).\n * Uses Lit Context dispatch mechanism.\n */\n #subscribeToContextProvider() {\n if (!this.targetElement) return;\n\n for (const [callback, context] of proxiedContexts) {\n const event = new ContextRequestEvent(\n context,\n this,\n (value, unsubscribe) => {\n callback(this, value as never);\n this.#contextUnsubscribeMap.set(context, unsubscribe);\n },\n true,\n );\n this.targetElement.dispatchEvent(event);\n }\n }\n\n /**\n * Subscribe to a direct temporal element's playback controller.\n * Used when targeting ef-timegroup, ef-video, ef-audio directly without ef-preview wrapper.\n */\n async #subscribeToDirectTemporal() {\n if (!this.targetElement || !isEFTemporal(this.targetElement)) return;\n\n const temporal = this.targetElement as TemporalMixinInterface & HTMLElement;\n const signal = this.#subscribeAbortController?.signal;\n\n // Wait for the temporal element to complete initialization\n // This ensures playbackController exists\n await temporal.updateComplete;\n if (signal?.aborted) return;\n\n // If playbackController still doesn't exist, the element might need another update cycle\n if (!temporal.playbackController) {\n // Wait one more frame for didBecomeRoot to create the controller\n await new Promise((resolve) => requestAnimationFrame(resolve));\n if (signal?.aborted) return;\n await temporal.updateComplete;\n if (signal?.aborted) return;\n }\n\n if (!temporal.playbackController) {\n // Still no controller - this element is likely a nested temporal\n return;\n }\n\n this.#directTemporalSubscription = createDirectTemporalSubscription(\n temporal,\n {\n onPlayingChange: (value) => {\n this.playing = value;\n },\n onLoopChange: (value) => {\n this.loop = value;\n },\n onCurrentTimeMsChange: (value) => {\n this.currentTimeMs = value;\n },\n onDurationMsChange: (value) => {\n this.durationMs = value;\n },\n onTargetTemporalChange: (value) => {\n this.targetTemporal = value;\n },\n onFocusedElementChange: (value) => {\n this.focusedElement = value;\n },\n },\n );\n }\n\n #resubscribe() {\n this.#unsubscribe();\n this.#subscribe();\n }\n updated(changedProperties: PropertyValueMap<this>) {\n super.updated(changedProperties);\n if (changedProperties.has(\"targetElement\")) {\n this.#resubscribe();\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.#unsubscribe();\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-controls\": EFControls;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFControls.js","names":["EFControls","#subscribeAbortController","#contextUnsubscribeMap","#directTemporalSubscription","#subscribeToContextProvider","#subscribeToDirectTemporal","#waitForTemporalToInitialize","#unsubscribe","#subscribe","#resubscribe"],"sources":["../../src/gui/EFControls.ts"],"sourcesContent":["import { type Context, createContext, provide } from \"@lit/context\";\nimport { css, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { attachContextRoot } from \"../attachContextRoot.js\";\nimport { isEFTemporal, type TemporalMixinInterface } from \"../elements/EFTemporal.js\";\nimport { TargetController } from \"../elements/TargetController.js\";\nimport { targetTemporalContext } from \"./ContextMixin.js\";\nimport {\n type ControllableInterface,\n type ControllableSubscription,\n createDirectTemporalSubscription,\n determineTargetType,\n} from \"./Controllable.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { efContext } from \"./efContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\nattachContextRoot();\n\nclass ContextRequestEvent extends Event {\n context: Context<any, any>;\n contextTarget: Element;\n callback: (proxy: EFControls, value: any) => void;\n subscribe: boolean;\n\n constructor(\n context: Context<any, any>,\n contextTarget: Element,\n callback: (proxy: EFControls, value: any) => void,\n subscribe: boolean,\n ) {\n super(\"context-request\", { bubbles: true, composed: true });\n this.context = context;\n this.contextTarget = contextTarget;\n this.callback = callback;\n this.subscribe = subscribe ?? false;\n }\n}\n\nconst proxiedContexts = [\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: boolean) => (proxy.playing = value),\n playingContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: boolean) => (proxy.loop = value),\n loopContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: number) => (proxy.currentTimeMs = value),\n currentTimeContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: number) => (proxy.durationMs = value),\n durationContext,\n ],\n [\n (proxy: EFControls, value: TemporalMixinInterface) =>\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy.targetTemporal = value),\n targetTemporalContext,\n ],\n [\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy: EFControls, value: HTMLElement) => (proxy.focusedElement = value),\n focusedElementContext,\n ],\n [\n (proxy: EFControls, value: HTMLElement) =>\n // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback\n (proxy.focusContext.focusedElement = value),\n focusContext,\n ],\n] as const;\n\nexport const testContext = createContext<string | null>(\"test\");\n\n/**\n * EFControls provides a way to control an ef-preview element that is not a direct ancestor.\n * It bridges the contexts from a target preview element to its children controls.\n *\n * Usage:\n * ```html\n * <ef-preview id=\"my-preview\">...</ef-preview>\n *\n * <ef-controls target=\"my-preview\">\n * <ef-toggle-play>\n * <button slot=\"play\">Play</button>\n * <button slot=\"pause\">Pause</button>\n * </ef-toggle-play>\n * <ef-scrubber></ef-scrubber>\n * <ef-time-display></ef-time-display>\n * </ef-controls>\n * ```\n */\n@customElement(\"ef-controls\")\nexport class EFControls extends LitElement {\n static styles = css`\n :host {\n display: block;\n }\n `;\n\n createRenderRoot() {\n return this;\n }\n\n /**\n * The ID of the ef-preview element to control\n */\n @property({ type: String })\n target = \"\";\n\n /**\n * The target element (set by TargetController)\n */\n @provide({ context: efContext })\n @state()\n targetElement: ControllableInterface | null = null;\n\n @provide({ context: playingContext })\n @state()\n playing = false;\n\n @provide({ context: loopContext })\n @state()\n loop = false;\n\n @provide({ context: currentTimeContext })\n @state()\n currentTimeMs = 0;\n\n @provide({ context: durationContext })\n @state()\n durationMs = 0;\n\n @provide({ context: targetTemporalContext })\n @state()\n targetTemporal: TemporalMixinInterface | null = null;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n // @ts-expect-error controller is intentionally not referenced directly\n // oxlint-disable-next-line no-unused-private-class-members -- retained for constructor side effects\n #targetController = new TargetController(this);\n\n // Subscription tracking for different target types\n #contextUnsubscribeMap = new Map<Context<any, any>, () => void>();\n #directTemporalSubscription: ControllableSubscription | null = null;\n\n #unsubscribe() {\n // Abort any pending async subscription attempts\n this.#subscribeAbortController?.abort();\n this.#subscribeAbortController = null;\n\n // Unsubscribe from context-provider subscriptions\n for (const unsubscribe of this.#contextUnsubscribeMap.values()) {\n unsubscribe();\n }\n this.#contextUnsubscribeMap.clear();\n\n // Unsubscribe from direct-temporal subscription\n if (this.#directTemporalSubscription) {\n this.#directTemporalSubscription.unsubscribe();\n this.#directTemporalSubscription = null;\n }\n\n // Note: We don't reset state here because:\n // 1. During resubscribe, the new subscription will immediately provide values\n // 2. During disconnect, the element is being removed anyway\n // Resetting state would cause race conditions with context consumers\n }\n\n #subscribeAbortController: AbortController | null = null;\n\n #subscribe() {\n if (!this.targetElement) return;\n\n // Cancel any pending async subscription\n this.#subscribeAbortController?.abort();\n this.#subscribeAbortController = new AbortController();\n\n const targetType = determineTargetType(this.targetElement);\n\n switch (targetType) {\n case \"context-provider\":\n this.#subscribeToContextProvider();\n break;\n case \"direct-temporal\":\n this.#subscribeToDirectTemporal();\n break;\n case \"none\":\n // Target might be a temporal that hasn't initialized yet\n // Try to wait for it to become controllable\n this.#waitForTemporalToInitialize();\n break;\n }\n }\n\n /**\n * Wait for a temporal element to initialize its playbackController.\n * This handles the case where we target a temporal element before it has\n * completed initialization and created its playbackController.\n *\n * Note: playbackController is created in EFTemporal's connectedCallback via\n * `this.updateComplete.then(() => this.didBecomeRoot())`, so we need to wait\n * past updateComplete for the .then() callback to run.\n */\n async #waitForTemporalToInitialize() {\n if (!this.targetElement || !isEFTemporal(this.targetElement)) return;\n\n const temporal = this.targetElement as TemporalMixinInterface & HTMLElement;\n const signal = this.#subscribeAbortController?.signal;\n\n // Wait for the element to finish its update cycle\n await temporal.updateComplete;\n if (signal?.aborted) return;\n\n // playbackController is created in a .then() callback after updateComplete,\n // so we need to wait for that microtask to complete\n if (!temporal.playbackController) {\n await new Promise((resolve) => requestAnimationFrame(resolve));\n if (signal?.aborted) return;\n await temporal.updateComplete;\n if (signal?.aborted) return;\n }\n\n // Check again if it now has a playbackController\n if (temporal.playbackController) {\n this.#subscribeToDirectTemporal();\n }\n }\n\n /**\n * Subscribe to a context-providing target (like EFPreview).\n * Uses Lit Context dispatch mechanism.\n */\n #subscribeToContextProvider() {\n if (!this.targetElement) return;\n\n for (const [callback, context] of proxiedContexts) {\n const event = new ContextRequestEvent(\n context,\n this,\n (value, unsubscribe) => {\n callback(this, value as never);\n this.#contextUnsubscribeMap.set(context, unsubscribe);\n },\n true,\n );\n this.targetElement.dispatchEvent(event);\n }\n }\n\n /**\n * Subscribe to a direct temporal element's playback controller.\n * Used when targeting ef-timegroup, ef-video, ef-audio directly without ef-preview wrapper.\n */\n async #subscribeToDirectTemporal() {\n if (!this.targetElement || !isEFTemporal(this.targetElement)) return;\n\n const temporal = this.targetElement as TemporalMixinInterface & HTMLElement;\n const signal = this.#subscribeAbortController?.signal;\n\n // Wait for the temporal element to complete initialization\n // This ensures playbackController exists\n await temporal.updateComplete;\n if (signal?.aborted) return;\n\n // If playbackController still doesn't exist, the element might need another update cycle\n if (!temporal.playbackController) {\n // Wait one more frame for didBecomeRoot to create the controller\n await new Promise((resolve) => requestAnimationFrame(resolve));\n if (signal?.aborted) return;\n await temporal.updateComplete;\n if (signal?.aborted) return;\n }\n\n if (!temporal.playbackController) {\n // Still no controller - this element is likely a nested temporal\n return;\n }\n\n this.#directTemporalSubscription = createDirectTemporalSubscription(temporal, {\n onPlayingChange: (value) => {\n this.playing = value;\n },\n onLoopChange: (value) => {\n this.loop = value;\n },\n onCurrentTimeMsChange: (value) => {\n this.currentTimeMs = value;\n },\n onDurationMsChange: (value) => {\n this.durationMs = value;\n },\n onTargetTemporalChange: (value) => {\n this.targetTemporal = value;\n },\n onFocusedElementChange: (value) => {\n this.focusedElement = value;\n },\n });\n }\n\n #resubscribe() {\n this.#unsubscribe();\n this.#subscribe();\n }\n updated(changedProperties: PropertyValueMap<this>) {\n super.updated(changedProperties);\n if (changedProperties.has(\"targetElement\")) {\n this.#resubscribe();\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.#unsubscribe();\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-controls\": EFControls;\n }\n}\n"],"mappings":"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"}
|
package/dist/gui/EFDial.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit22 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html21 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFDial.d.ts
|
|
6
6
|
interface DialChangeDetail {
|
|
@@ -13,12 +13,12 @@ declare class EFDial extends LitElement {
|
|
|
13
13
|
private isDragging;
|
|
14
14
|
private dragStartAngle;
|
|
15
15
|
private dragStartValue;
|
|
16
|
-
static styles:
|
|
16
|
+
static styles: lit22.CSSResult;
|
|
17
17
|
private getAngleFromPoint;
|
|
18
18
|
private handlePointerDown;
|
|
19
19
|
private handlePointerMove;
|
|
20
20
|
private handlePointerUp;
|
|
21
|
-
render():
|
|
21
|
+
render(): lit_html21.TemplateResult<1>;
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
export { DialChangeDetail, EFDial };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { TemporalMixinInterface } from "../elements/EFTemporal.js";
|
|
2
2
|
import "./timeline/EFTimeline.js";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit10 from "lit";
|
|
4
4
|
import { LitElement } from "lit";
|
|
5
|
-
import * as
|
|
5
|
+
import * as lit_html10 from "lit-html";
|
|
6
6
|
import * as lit_html_directives_ref0 from "lit-html/directives/ref";
|
|
7
7
|
|
|
8
8
|
//#region src/gui/EFFilmstrip.d.ts
|
|
9
9
|
declare const EFFilmstrip_base: typeof LitElement;
|
|
10
10
|
declare class EFFilmstrip extends EFFilmstrip_base {
|
|
11
11
|
#private;
|
|
12
|
-
static styles:
|
|
12
|
+
static styles: lit10.CSSResult[];
|
|
13
13
|
target: string;
|
|
14
14
|
pixelsPerMs: number;
|
|
15
15
|
hidePlayhead: boolean;
|
|
@@ -24,7 +24,7 @@ declare class EFFilmstrip extends EFFilmstrip_base {
|
|
|
24
24
|
timelineRef: lit_html_directives_ref0.Ref<HTMLElement>;
|
|
25
25
|
connectedCallback(): void;
|
|
26
26
|
protected willUpdate(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
27
|
-
render():
|
|
27
|
+
render(): lit_html10.TemplateResult<1>;
|
|
28
28
|
}
|
|
29
29
|
declare global {
|
|
30
30
|
interface HTMLElementTagNameMap {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFFilmstrip.js","names":["EFFilmstrip","#targetController","#lastTargetTemporal"],"sources":["../../src/gui/EFFilmstrip.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\n\nimport {
|
|
1
|
+
{"version":3,"file":"EFFilmstrip.js","names":["EFFilmstrip","#targetController","#lastTargetTemporal"],"sources":["../../src/gui/EFFilmstrip.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\n\nimport { isEFTemporal, type TemporalMixinInterface } from \"../elements/EFTemporal.js\";\nimport { TargetController } from \"../elements/TargetController.js\";\nimport { targetTemporalContext } from \"./ContextMixin.ts\";\nimport { TWMixin } from \"./TWMixin.js\";\nimport \"./timeline/EFTimeline.js\";\n\n@customElement(\"ef-filmstrip\")\nexport class EFFilmstrip extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n `,\n ];\n\n @property({ type: String })\n target = \"\";\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.04;\n\n @property({ type: Boolean, attribute: \"hide-playhead\" })\n hidePlayhead = false;\n\n @property({ type: Boolean, attribute: \"disable-internal-scroll\" })\n disableInternalScroll = false;\n\n @property({ type: String })\n hide = \"\";\n\n @property({ type: String })\n show = \"\";\n\n @state()\n targetElement: Element | null = null;\n\n #targetController?: TargetController;\n #lastTargetTemporal?: TemporalMixinInterface | null;\n\n @consume({ context: targetTemporalContext, subscribe: true })\n @state()\n private _contextProvidedTemporal?: TemporalMixinInterface | null;\n\n get targetTemporal(): TemporalMixinInterface | null {\n const fromTarget =\n this.targetElement && isEFTemporal(this.targetElement)\n ? (this.targetElement as TemporalMixinInterface & HTMLElement)\n : null;\n const fromContext = this._contextProvidedTemporal;\n\n if (fromTarget && fromContext && fromTarget !== fromContext) {\n console.warn(\n \"EFFilmstrip: Both target attribute and parent context found. Using target attribute.\",\n { target: this.target, fromTarget, fromContext },\n );\n }\n\n return fromTarget ?? fromContext ?? null;\n }\n\n get hideSelectors(): string[] | undefined {\n if (!this.hide) return undefined;\n return this.hide\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n\n get showSelectors(): string[] | undefined {\n if (!this.show) return undefined;\n return this.show\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n\n timelineRef = createRef<HTMLElement>();\n\n connectedCallback(): void {\n super.connectedCallback();\n if (this.target) {\n this.#targetController = new TargetController(this);\n }\n }\n\n protected willUpdate(changedProperties: Map<string | number | symbol, unknown>) {\n if (changedProperties.has(\"target\")) {\n if (this.target && !this.#targetController) {\n this.#targetController = new TargetController(this);\n }\n }\n\n const currentTargetTemporal = this.targetTemporal;\n if (this.#lastTargetTemporal !== currentTargetTemporal) {\n this.#lastTargetTemporal = currentTargetTemporal;\n\n // The inner ef-timeline lives in our shadow root and can't resolve\n // targets from the document registry. Force it to re-render when\n // our own target resolution succeeds or changes.\n const timeline = this.timelineRef.value as any;\n if (timeline) {\n timeline.requestUpdate();\n }\n }\n\n super.willUpdate(changedProperties);\n }\n\n render() {\n const targetId = this.targetTemporal\n ? (this.targetTemporal as unknown as HTMLElement).id || this.target\n : this.target;\n\n return html`\n <ef-timeline\n ${ref(this.timelineRef)}\n target=${targetId}\n control-target=${targetId}\n pixels-per-ms=${this.pixelsPerMs}\n .showPlayhead=${!this.hidePlayhead}\n .showControls=${true}\n hide=${this.hide}\n show=${this.show}\n ></ef-timeline>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-filmstrip\": EFFilmstrip;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAYO,wBAAMA,sBAAoB,QAAQ,WAAW,CAAC;;;gBAY1C;qBAGK;sBAGC;+BAGS;cAGjB;cAGA;uBAGyB;qBA0ClB,WAAwB;;;gBAvEtB,CACd,GAAG;;;;;;MAOJ;;CAuBD;CACA;CAMA,IAAI,iBAAgD;EAClD,MAAM,aACJ,KAAK,iBAAiB,aAAa,KAAK,cAAc,GACjD,KAAK,gBACN;EACN,MAAM,cAAc,KAAK;AAEzB,MAAI,cAAc,eAAe,eAAe,YAC9C,SAAQ,KACN,wFACA;GAAE,QAAQ,KAAK;GAAQ;GAAY;GAAa,CACjD;AAGH,SAAO,cAAc,eAAe;;CAGtC,IAAI,gBAAsC;AACxC,MAAI,CAAC,KAAK,KAAM,QAAO;AACvB,SAAO,KAAK,KACT,MAAM,IAAI,CACV,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,EAAE;;CAGhC,IAAI,gBAAsC;AACxC,MAAI,CAAC,KAAK,KAAM,QAAO;AACvB,SAAO,KAAK,KACT,MAAM,IAAI,CACV,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,EAAE;;CAKhC,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,MAAI,KAAK,OACP,OAAKC,mBAAoB,IAAI,iBAAiB,KAAK;;CAIvD,AAAU,WAAW,mBAA2D;AAC9E,MAAI,kBAAkB,IAAI,SAAS,EACjC;OAAI,KAAK,UAAU,CAAC,MAAKA,iBACvB,OAAKA,mBAAoB,IAAI,iBAAiB,KAAK;;EAIvD,MAAM,wBAAwB,KAAK;AACnC,MAAI,MAAKC,uBAAwB,uBAAuB;AACtD,SAAKA,qBAAsB;GAK3B,MAAM,WAAW,KAAK,YAAY;AAClC,OAAI,SACF,UAAS,eAAe;;AAI5B,QAAM,WAAW,kBAAkB;;CAGrC,SAAS;EACP,MAAM,WAAW,KAAK,iBACjB,KAAK,eAA0C,MAAM,KAAK,SAC3D,KAAK;AAET,SAAO,IAAI;;UAEL,IAAI,KAAK,YAAY,CAAC;iBACf,SAAS;yBACD,SAAS;wBACV,KAAK,YAAY;wBACjB,CAAC,KAAK,aAAa;wBACnB,KAAK;eACd,KAAK,KAAK;eACV,KAAK,KAAK;;;;;YA3GtB,SAAS,EAAE,MAAM,QAAQ,CAAC;YAG1B,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAiB,CAAC;YAGtD,SAAS;CAAE,MAAM;CAAS,WAAW;CAAiB,CAAC;YAGvD,SAAS;CAAE,MAAM;CAAS,WAAW;CAA2B,CAAC;YAGjE,SAAS,EAAE,MAAM,QAAQ,CAAC;YAG1B,SAAS,EAAE,MAAM,QAAQ,CAAC;YAG1B,OAAO;YAMP,QAAQ;CAAE,SAAS;CAAuB,WAAW;CAAM,CAAC,EAC5D,OAAO;0BArCT,cAAc,eAAe"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFFitScale.js","names":["EFFitScale","results: HTMLElement[]","children","allContentElements: HTMLElement[]"],"sources":["../../src/gui/EFFitScale.ts"],"sourcesContent":["import { LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef } from \"lit/directives/ref.js\";\n\n/* ━━ Pure scale calculation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface ScaleInput {\n containerWidth: number;\n containerHeight: number;\n contentWidth: number;\n contentHeight: number;\n}\n\nexport interface ScaleOutput {\n scale: number;\n translateX: number;\n translateY: number;\n}\n\n/**\n * Compute the scale factor and centering translation needed to fit\n * content of a given size into a container while preserving aspect ratio.\n *\n * Returns `null` when any dimension is zero or negative (cannot compute).\n */\nexport function computeFitScale(input: ScaleInput): ScaleOutput | null {\n const { containerWidth, containerHeight, contentWidth, contentHeight } =\n input;\n\n if (\n containerWidth <= 0 ||\n containerHeight <= 0 ||\n contentWidth <= 0 ||\n contentHeight <= 0\n ) {\n return null;\n }\n\n const containerRatio = containerWidth / containerHeight;\n const contentRatio = contentWidth / contentHeight;\n\n const scale =\n containerRatio > contentRatio\n ? containerHeight / contentHeight\n : containerWidth / contentWidth;\n\n const scaledWidth = contentWidth * scale;\n const scaledHeight = contentHeight * scale;\n const translateX = (containerWidth - scaledWidth) / 2;\n const translateY = (containerHeight - scaledHeight) / 2;\n\n return { scale, translateX, translateY };\n}\n\n/* ━━ Component ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n@customElement(\"ef-fit-scale\")\nexport class EFFitScale extends LitElement {\n containerRef = createRef<HTMLDivElement>();\n contentRef = createRef<HTMLSlotElement>();\n\n createRenderRoot() {\n Object.assign(this.style, {\n display: \"grid\",\n width: \"100%\",\n height: \"100%\",\n gridTemplateColumns: \"100%\",\n gridTemplateRows: \"100%\",\n overflow: \"hidden\",\n boxSizing: \"border-box\",\n contain: \"layout paint style\",\n position: \"relative\",\n });\n this.id = `${this.uniqueId}`;\n return this;\n }\n\n uniqueId = Math.random().toString(36).substring(2, 15);\n\n @property({ type: Boolean })\n paused = false;\n\n updated(changedProperties: Map<string, unknown>): void {\n if (changedProperties.has(\"paused\") && !this.paused) {\n // When unpaused, immediately recalculate scale\n this.updateScale();\n }\n }\n\n private containerResizeObserver?: ResizeObserver;\n private contentResizeObserver?: ResizeObserver;\n private childMutationObserver?: MutationObserver;\n private observedContentChild: HTMLElement | null = null;\n private hasWarnedZeroDimensions = false;\n\n get contentChild() {\n if (!this.children.length) return null;\n\n const isNonContentElement = (element: Element): boolean => {\n const tagName = element.tagName.toLowerCase();\n const nonContentTags = [\n \"style\",\n \"script\",\n \"meta\",\n \"link\",\n \"title\",\n \"noscript\",\n ];\n if (nonContentTags.includes(tagName)) return true;\n\n try {\n const display = window.getComputedStyle(element).display;\n return display === \"none\" || display === \"contents\";\n } catch {\n return false;\n }\n };\n\n const findAllContentElements = (element: Element): HTMLElement[] => {\n const results: HTMLElement[] = [];\n\n if (element instanceof HTMLSlotElement) {\n const assigned = element.assignedElements()[0];\n if (assigned) {\n results.push(...findAllContentElements(assigned));\n }\n return results;\n }\n\n if (!isNonContentElement(element)) {\n results.push(element as HTMLElement);\n }\n\n const children = Array.from(element.children);\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (child) {\n results.push(...findAllContentElements(child));\n }\n }\n\n return results;\n };\n\n const children = Array.from(this.children);\n const allContentElements: HTMLElement[] = [];\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (child) {\n allContentElements.push(...findAllContentElements(child));\n }\n }\n\n if (allContentElements.length === 0) return null;\n\n return allContentElements[0] ?? null;\n }\n\n get scaleInfo() {\n if (!this.contentChild) {\n return {\n scale: 1,\n containerWidth: 0,\n containerHeight: 0,\n contentWidth: 0,\n contentHeight: 0,\n };\n }\n\n const containerWidth = this.clientWidth;\n const containerHeight = this.clientHeight;\n\n // Try to get natural dimensions from media elements (ef-video, ef-image)\n let contentWidth = 0;\n let contentHeight = 0;\n\n if (typeof (this.contentChild as any).getNaturalDimensions === \"function\") {\n const naturalDimensions = (\n this.contentChild as any\n ).getNaturalDimensions() as { width: number; height: number } | null;\n if (\n naturalDimensions &&\n naturalDimensions.width > 0 &&\n naturalDimensions.height > 0\n ) {\n contentWidth = naturalDimensions.width;\n contentHeight = naturalDimensions.height;\n\n // ESSENTIAL: For ef-video, set canvas to explicit pixel dimensions to break 100% circular dependency\n // Canvas default CSS is width:100%, height:100% which would make ef-video collapse to 0x0\n // when ef-video is set to width:auto, height:auto by ElementRenderer\n if (this.contentChild.tagName === \"EF-VIDEO\") {\n const canvas = (this.contentChild as any).canvasElement;\n if (canvas) {\n canvas.style.setProperty(\"width\", `${contentWidth}px`, \"important\");\n canvas.style.setProperty(\n \"height\",\n `${contentHeight}px`,\n \"important\",\n );\n }\n }\n } else {\n // Natural dimensions not available yet, fall back to client dimensions\n contentWidth = this.contentChild.clientWidth;\n contentHeight = this.contentChild.clientHeight;\n }\n } else {\n // For other elements, use clientWidth/Height\n contentWidth = this.contentChild.clientWidth;\n contentHeight = this.contentChild.clientHeight;\n }\n\n if (contentWidth === 0 || contentHeight === 0) {\n return {\n scale: 1,\n containerWidth,\n containerHeight,\n contentWidth: 0,\n contentHeight: 0,\n };\n }\n\n const result = computeFitScale({\n containerWidth,\n containerHeight,\n contentWidth,\n contentHeight,\n });\n\n return {\n scale: result?.scale ?? 1,\n containerWidth,\n containerHeight,\n contentWidth,\n contentHeight,\n };\n }\n\n scaleLastSetOn: HTMLElement | null = null;\n\n private updateScale = (): void => {\n if (!this.isConnected || this.paused) return;\n\n const { containerWidth, containerHeight, contentWidth, contentHeight } =\n this.scaleInfo;\n\n // Warn on zero container dimensions\n if (containerWidth === 0 || containerHeight === 0) {\n if (!this.hasWarnedZeroDimensions) {\n this.hasWarnedZeroDimensions = true;\n console.warn(\n `[ef-fit-scale] Container has zero dimensions (${containerWidth}×${containerHeight}). ` +\n `Content will be invisible. Ensure all ancestors have resolved height.`,\n this,\n );\n }\n return;\n }\n\n // Reset warning flag when dimensions become valid\n this.hasWarnedZeroDimensions = false;\n\n if (this.contentChild && contentWidth > 0 && contentHeight > 0) {\n const result = computeFitScale({\n containerWidth,\n containerHeight,\n contentWidth,\n contentHeight,\n });\n\n if (!result) return;\n\n // In the rare event that the content child is changed, we need to remove the scale\n // because we don't want to have a scale on the old content child that is somewhere else in the DOM\n if (this.scaleLastSetOn !== this.contentChild) {\n this.removeScale();\n }\n // Use toFixed to avoid floating point precision issues\n // this will update every frame with sub-pixel changes if we don't pin it down\n Object.assign(this.contentChild.style, {\n width: `${contentWidth}px`,\n height: `${contentHeight}px`,\n transform: `translate(${result.translateX.toFixed(4)}px, ${result.translateY.toFixed(4)}px) scale(${result.scale.toFixed(4)})`,\n transformOrigin: \"top left\",\n });\n this.scaleLastSetOn = this.contentChild;\n }\n };\n\n private observeContentChild(): void {\n const child = this.contentChild;\n if (child === this.observedContentChild) return;\n\n // Stop observing old child\n this.contentResizeObserver?.disconnect();\n\n // Observe new child\n if (child) {\n this.contentResizeObserver = new ResizeObserver(() => {\n this.updateScale();\n });\n this.contentResizeObserver.observe(child);\n }\n this.observedContentChild = child;\n }\n\n removeScale = () => {\n if (this.scaleLastSetOn) {\n Object.assign(this.scaleLastSetOn.style, {\n width: \"\",\n height: \"\",\n transform: \"\",\n transformOrigin: \"\",\n });\n this.scaleLastSetOn = null;\n }\n };\n\n connectedCallback(): void {\n super.connectedCallback();\n\n this.hasWarnedZeroDimensions = false;\n\n // Observe self for container size changes\n this.containerResizeObserver = new ResizeObserver(() => {\n this.updateScale();\n });\n this.containerResizeObserver.observe(this);\n\n // Observe child list for content child changes\n this.childMutationObserver = new MutationObserver(() => {\n this.observeContentChild();\n this.updateScale();\n });\n this.childMutationObserver.observe(this, { childList: true });\n\n // Initial content child observation\n this.observeContentChild();\n\n // Initial scale calculation (deferred to allow layout to settle)\n requestAnimationFrame(() => this.updateScale());\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeScale();\n this.containerResizeObserver?.disconnect();\n this.contentResizeObserver?.disconnect();\n this.childMutationObserver?.disconnect();\n this.observedContentChild = null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-fit-scale\": EFFitScale;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFFitScale.js","names":["EFFitScale","results: HTMLElement[]","children","allContentElements: HTMLElement[]"],"sources":["../../src/gui/EFFitScale.ts"],"sourcesContent":["import { LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef } from \"lit/directives/ref.js\";\n\n/* ━━ Pure scale calculation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface ScaleInput {\n containerWidth: number;\n containerHeight: number;\n contentWidth: number;\n contentHeight: number;\n}\n\nexport interface ScaleOutput {\n scale: number;\n translateX: number;\n translateY: number;\n}\n\n/**\n * Compute the scale factor and centering translation needed to fit\n * content of a given size into a container while preserving aspect ratio.\n *\n * Returns `null` when any dimension is zero or negative (cannot compute).\n */\nexport function computeFitScale(input: ScaleInput): ScaleOutput | null {\n const { containerWidth, containerHeight, contentWidth, contentHeight } = input;\n\n if (containerWidth <= 0 || containerHeight <= 0 || contentWidth <= 0 || contentHeight <= 0) {\n return null;\n }\n\n const containerRatio = containerWidth / containerHeight;\n const contentRatio = contentWidth / contentHeight;\n\n const scale =\n containerRatio > contentRatio ? containerHeight / contentHeight : containerWidth / contentWidth;\n\n const scaledWidth = contentWidth * scale;\n const scaledHeight = contentHeight * scale;\n const translateX = (containerWidth - scaledWidth) / 2;\n const translateY = (containerHeight - scaledHeight) / 2;\n\n return { scale, translateX, translateY };\n}\n\n/* ━━ Component ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n@customElement(\"ef-fit-scale\")\nexport class EFFitScale extends LitElement {\n containerRef = createRef<HTMLDivElement>();\n contentRef = createRef<HTMLSlotElement>();\n\n createRenderRoot() {\n Object.assign(this.style, {\n display: \"grid\",\n width: \"100%\",\n height: \"100%\",\n gridTemplateColumns: \"100%\",\n gridTemplateRows: \"100%\",\n overflow: \"hidden\",\n boxSizing: \"border-box\",\n contain: \"layout paint style\",\n position: \"relative\",\n });\n this.id = `${this.uniqueId}`;\n return this;\n }\n\n uniqueId = Math.random().toString(36).substring(2, 15);\n\n @property({ type: Boolean })\n paused = false;\n\n updated(changedProperties: Map<string, unknown>): void {\n if (changedProperties.has(\"paused\") && !this.paused) {\n // When unpaused, immediately recalculate scale\n this.updateScale();\n }\n }\n\n private containerResizeObserver?: ResizeObserver;\n private contentResizeObserver?: ResizeObserver;\n private childMutationObserver?: MutationObserver;\n private observedContentChild: HTMLElement | null = null;\n private hasWarnedZeroDimensions = false;\n\n get contentChild() {\n if (!this.children.length) return null;\n\n const isNonContentElement = (element: Element): boolean => {\n const tagName = element.tagName.toLowerCase();\n const nonContentTags = [\"style\", \"script\", \"meta\", \"link\", \"title\", \"noscript\"];\n if (nonContentTags.includes(tagName)) return true;\n\n try {\n const display = window.getComputedStyle(element).display;\n return display === \"none\" || display === \"contents\";\n } catch {\n return false;\n }\n };\n\n const findAllContentElements = (element: Element): HTMLElement[] => {\n const results: HTMLElement[] = [];\n\n if (element instanceof HTMLSlotElement) {\n const assigned = element.assignedElements()[0];\n if (assigned) {\n results.push(...findAllContentElements(assigned));\n }\n return results;\n }\n\n if (!isNonContentElement(element)) {\n results.push(element as HTMLElement);\n }\n\n const children = Array.from(element.children);\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (child) {\n results.push(...findAllContentElements(child));\n }\n }\n\n return results;\n };\n\n const children = Array.from(this.children);\n const allContentElements: HTMLElement[] = [];\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (child) {\n allContentElements.push(...findAllContentElements(child));\n }\n }\n\n if (allContentElements.length === 0) return null;\n\n return allContentElements[0] ?? null;\n }\n\n get scaleInfo() {\n if (!this.contentChild) {\n return {\n scale: 1,\n containerWidth: 0,\n containerHeight: 0,\n contentWidth: 0,\n contentHeight: 0,\n };\n }\n\n const containerWidth = this.clientWidth;\n const containerHeight = this.clientHeight;\n\n // Try to get natural dimensions from media elements (ef-video, ef-image)\n let contentWidth = 0;\n let contentHeight = 0;\n\n if (typeof (this.contentChild as any).getNaturalDimensions === \"function\") {\n const naturalDimensions = (this.contentChild as any).getNaturalDimensions() as {\n width: number;\n height: number;\n } | null;\n if (naturalDimensions && naturalDimensions.width > 0 && naturalDimensions.height > 0) {\n contentWidth = naturalDimensions.width;\n contentHeight = naturalDimensions.height;\n\n // ESSENTIAL: For ef-video, set canvas to explicit pixel dimensions to break 100% circular dependency\n // Canvas default CSS is width:100%, height:100% which would make ef-video collapse to 0x0\n // when ef-video is set to width:auto, height:auto by ElementRenderer\n if (this.contentChild.tagName === \"EF-VIDEO\") {\n const canvas = (this.contentChild as any).canvasElement;\n if (canvas) {\n canvas.style.setProperty(\"width\", `${contentWidth}px`, \"important\");\n canvas.style.setProperty(\"height\", `${contentHeight}px`, \"important\");\n }\n }\n } else {\n // Natural dimensions not available yet, fall back to client dimensions\n contentWidth = this.contentChild.clientWidth;\n contentHeight = this.contentChild.clientHeight;\n }\n } else {\n // For other elements, use clientWidth/Height\n contentWidth = this.contentChild.clientWidth;\n contentHeight = this.contentChild.clientHeight;\n }\n\n if (contentWidth === 0 || contentHeight === 0) {\n return {\n scale: 1,\n containerWidth,\n containerHeight,\n contentWidth: 0,\n contentHeight: 0,\n };\n }\n\n const result = computeFitScale({\n containerWidth,\n containerHeight,\n contentWidth,\n contentHeight,\n });\n\n return {\n scale: result?.scale ?? 1,\n containerWidth,\n containerHeight,\n contentWidth,\n contentHeight,\n };\n }\n\n scaleLastSetOn: HTMLElement | null = null;\n\n private updateScale = (): void => {\n if (!this.isConnected || this.paused) return;\n\n const { containerWidth, containerHeight, contentWidth, contentHeight } = this.scaleInfo;\n\n // Warn on zero container dimensions\n if (containerWidth === 0 || containerHeight === 0) {\n if (!this.hasWarnedZeroDimensions) {\n this.hasWarnedZeroDimensions = true;\n console.warn(\n `[ef-fit-scale] Container has zero dimensions (${containerWidth}×${containerHeight}). ` +\n `Content will be invisible. Ensure all ancestors have resolved height.`,\n this,\n );\n }\n return;\n }\n\n // Reset warning flag when dimensions become valid\n this.hasWarnedZeroDimensions = false;\n\n if (this.contentChild && contentWidth > 0 && contentHeight > 0) {\n const result = computeFitScale({\n containerWidth,\n containerHeight,\n contentWidth,\n contentHeight,\n });\n\n if (!result) return;\n\n // In the rare event that the content child is changed, we need to remove the scale\n // because we don't want to have a scale on the old content child that is somewhere else in the DOM\n if (this.scaleLastSetOn !== this.contentChild) {\n this.removeScale();\n }\n // Use toFixed to avoid floating point precision issues\n // this will update every frame with sub-pixel changes if we don't pin it down\n Object.assign(this.contentChild.style, {\n width: `${contentWidth}px`,\n height: `${contentHeight}px`,\n transform: `translate(${result.translateX.toFixed(4)}px, ${result.translateY.toFixed(4)}px) scale(${result.scale.toFixed(4)})`,\n transformOrigin: \"top left\",\n });\n this.scaleLastSetOn = this.contentChild;\n }\n };\n\n private observeContentChild(): void {\n const child = this.contentChild;\n if (child === this.observedContentChild) return;\n\n // Stop observing old child\n this.contentResizeObserver?.disconnect();\n\n // Observe new child\n if (child) {\n this.contentResizeObserver = new ResizeObserver(() => {\n this.updateScale();\n });\n this.contentResizeObserver.observe(child);\n }\n this.observedContentChild = child;\n }\n\n removeScale = () => {\n if (this.scaleLastSetOn) {\n Object.assign(this.scaleLastSetOn.style, {\n width: \"\",\n height: \"\",\n transform: \"\",\n transformOrigin: \"\",\n });\n this.scaleLastSetOn = null;\n }\n };\n\n connectedCallback(): void {\n super.connectedCallback();\n\n this.hasWarnedZeroDimensions = false;\n\n // Observe self for container size changes\n this.containerResizeObserver = new ResizeObserver(() => {\n this.updateScale();\n });\n this.containerResizeObserver.observe(this);\n\n // Observe child list for content child changes\n this.childMutationObserver = new MutationObserver(() => {\n this.observeContentChild();\n this.updateScale();\n });\n this.childMutationObserver.observe(this, { childList: true });\n\n // Initial content child observation\n this.observeContentChild();\n\n // Initial scale calculation (deferred to allow layout to settle)\n requestAnimationFrame(() => this.updateScale());\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeScale();\n this.containerResizeObserver?.disconnect();\n this.contentResizeObserver?.disconnect();\n this.childMutationObserver?.disconnect();\n this.observedContentChild = null;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-fit-scale\": EFFitScale;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit32 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html30 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFOverlayItem.d.ts
|
|
6
6
|
/**
|
|
@@ -23,7 +23,7 @@ interface OverlayItemPosition {
|
|
|
23
23
|
* ensures transforms are applied before positions are read.
|
|
24
24
|
*/
|
|
25
25
|
declare class EFOverlayItem extends LitElement {
|
|
26
|
-
static styles:
|
|
26
|
+
static styles: lit32.CSSResult[];
|
|
27
27
|
elementId?: string;
|
|
28
28
|
target?: HTMLElement | string;
|
|
29
29
|
private currentPosition;
|
|
@@ -36,7 +36,7 @@ declare class EFOverlayItem extends LitElement {
|
|
|
36
36
|
updatePosition(): void;
|
|
37
37
|
connectedCallback(): void;
|
|
38
38
|
disconnectedCallback(): void;
|
|
39
|
-
render():
|
|
39
|
+
render(): lit_html30.TemplateResult<1>;
|
|
40
40
|
}
|
|
41
41
|
declare global {
|
|
42
42
|
interface HTMLElementTagNameMap {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFOverlayItem.js","names":["EFOverlayItem"],"sources":["../../src/gui/EFOverlayItem.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport type { EFOverlayLayer } from \"./EFOverlayLayer.js\";\nimport { parseRotationFromTransform } from \"./transformCalculations.js\";\n\n/**\n * Position changed event detail.\n */\nexport interface OverlayItemPosition {\n x: number;\n y: number;\n width: number;\n height: number;\n rotation: number;\n}\n\n/**\n * Individual overlay item that tracks a target element.\n * Must be a direct child of ef-overlay-layer.\n *\n * NEW ARCHITECTURE: This component is now PASSIVE - it does not run its own RAF loop.\n * Instead, it registers with its parent EFOverlayLayer, which manages position updates\n * for all children in a synchronized RAF loop. This eliminates race conditions and\n * ensures transforms are applied before positions are read.\n */\n@customElement(\"ef-overlay-item\")\nexport class EFOverlayItem extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n pointer-events: auto;\n transform-origin: center;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"element-id\" })\n elementId?: string;\n\n @property({ attribute: false })\n target?: HTMLElement | string;\n\n private currentPosition: OverlayItemPosition | null = null;\n\n private resolveTarget(): HTMLElement | null {\n if (this.elementId) {\n let element = document.querySelector(
|
|
1
|
+
{"version":3,"file":"EFOverlayItem.js","names":["EFOverlayItem"],"sources":["../../src/gui/EFOverlayItem.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport type { EFOverlayLayer } from \"./EFOverlayLayer.js\";\nimport { parseRotationFromTransform } from \"./transformCalculations.js\";\n\n/**\n * Position changed event detail.\n */\nexport interface OverlayItemPosition {\n x: number;\n y: number;\n width: number;\n height: number;\n rotation: number;\n}\n\n/**\n * Individual overlay item that tracks a target element.\n * Must be a direct child of ef-overlay-layer.\n *\n * NEW ARCHITECTURE: This component is now PASSIVE - it does not run its own RAF loop.\n * Instead, it registers with its parent EFOverlayLayer, which manages position updates\n * for all children in a synchronized RAF loop. This eliminates race conditions and\n * ensures transforms are applied before positions are read.\n */\n@customElement(\"ef-overlay-item\")\nexport class EFOverlayItem extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n pointer-events: auto;\n transform-origin: center;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"element-id\" })\n elementId?: string;\n\n @property({ attribute: false })\n target?: HTMLElement | string;\n\n private currentPosition: OverlayItemPosition | null = null;\n\n private resolveTarget(): HTMLElement | null {\n if (this.elementId) {\n let element = document.querySelector(`[data-element-id=\"${this.elementId}\"]`) as HTMLElement;\n\n if (!element) {\n element = document.querySelector(`[data-timegroup-id=\"${this.elementId}\"]`) as HTMLElement;\n }\n\n return element || null;\n }\n\n if (!this.target) return null;\n\n if (typeof this.target === \"string\") {\n return (\n (document.querySelector(this.target) as HTMLElement) || document.getElementById(this.target)\n );\n }\n\n // For direct HTMLElement references, verify the element is still in the DOM\n if (!this.target.isConnected) {\n return null;\n }\n\n return this.target;\n }\n\n /**\n * Update position based on target element.\n * Called by parent EFOverlayLayer in its synchronized RAF loop.\n * PUBLIC API - called by parent layer.\n */\n updatePosition() {\n const targetElement = this.resolveTarget();\n if (!targetElement) {\n // Target not found - hide the overlay item\n if (this.style.display !== \"none\") {\n this.style.display = \"none\";\n }\n return;\n }\n\n // Target found - ensure we're visible\n if (this.style.display === \"none\") {\n this.style.display = \"\";\n }\n\n // Use parent element as overlay layer (like old system)\n const overlayLayer = this.parentElement;\n if (!overlayLayer) {\n return;\n }\n\n const overlayLayerRect = overlayLayer.getBoundingClientRect();\n const targetRect = targetElement.getBoundingClientRect();\n\n if (targetRect.width === 0 && targetRect.height === 0) return;\n\n const computedStyle = window.getComputedStyle(targetElement);\n const rotation = parseRotationFromTransform(computedStyle.transform);\n\n const overlayPosition = {\n x: targetRect.left - overlayLayerRect.left,\n y: targetRect.top - overlayLayerRect.top,\n width: targetRect.width,\n height: targetRect.height,\n rotation,\n };\n\n if (overlayPosition.width <= 0 || overlayPosition.height <= 0) return;\n\n const positionChanged =\n !this.currentPosition ||\n Math.abs(this.currentPosition.x - overlayPosition.x) > 0.01 ||\n Math.abs(this.currentPosition.y - overlayPosition.y) > 0.01 ||\n Math.abs(this.currentPosition.width - overlayPosition.width) > 0.01 ||\n Math.abs(this.currentPosition.height - overlayPosition.height) > 0.01 ||\n Math.abs(this.currentPosition.rotation - overlayPosition.rotation) > 0.01;\n\n if (positionChanged) {\n this.currentPosition = overlayPosition;\n\n this.style.left = `${overlayPosition.x}px`;\n this.style.top = `${overlayPosition.y}px`;\n this.style.width = `${overlayPosition.width}px`;\n this.style.height = `${overlayPosition.height}px`;\n\n if (overlayPosition.rotation !== 0) {\n this.style.transform = `rotate(${overlayPosition.rotation}deg)`;\n } else {\n this.style.transform = \"\";\n }\n\n this.dispatchEvent(\n new CustomEvent<OverlayItemPosition>(\"position-changed\", {\n detail: { ...overlayPosition },\n bubbles: true,\n composed: true,\n }),\n );\n }\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Register with parent overlay layer for coordinated updates\n const parent = this.parentElement as EFOverlayLayer | null;\n if (parent && typeof (parent as any).registerOverlayItem === \"function\") {\n (parent as any).registerOverlayItem(this);\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n // Unregister from parent overlay layer\n const parent = this.parentElement as EFOverlayLayer | null;\n if (parent && typeof (parent as any).unregisterOverlayItem === \"function\") {\n (parent as any).unregisterOverlayItem(this);\n }\n }\n\n render() {\n return html`<slot></slot>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-overlay-item\": EFOverlayItem;\n }\n}\n"],"mappings":";;;;;;AA0BO,0BAAMA,wBAAsB,WAAW;;;yBAkBU;;;gBAjBtC,CACd,GAAG;;;;;;;MAQJ;;CAUD,AAAQ,gBAAoC;AAC1C,MAAI,KAAK,WAAW;GAClB,IAAI,UAAU,SAAS,cAAc,qBAAqB,KAAK,UAAU,IAAI;AAE7E,OAAI,CAAC,QACH,WAAU,SAAS,cAAc,uBAAuB,KAAK,UAAU,IAAI;AAG7E,UAAO,WAAW;;AAGpB,MAAI,CAAC,KAAK,OAAQ,QAAO;AAEzB,MAAI,OAAO,KAAK,WAAW,SACzB,QACG,SAAS,cAAc,KAAK,OAAO,IAAoB,SAAS,eAAe,KAAK,OAAO;AAKhG,MAAI,CAAC,KAAK,OAAO,YACf,QAAO;AAGT,SAAO,KAAK;;;;;;;CAQd,iBAAiB;EACf,MAAM,gBAAgB,KAAK,eAAe;AAC1C,MAAI,CAAC,eAAe;AAElB,OAAI,KAAK,MAAM,YAAY,OACzB,MAAK,MAAM,UAAU;AAEvB;;AAIF,MAAI,KAAK,MAAM,YAAY,OACzB,MAAK,MAAM,UAAU;EAIvB,MAAM,eAAe,KAAK;AAC1B,MAAI,CAAC,aACH;EAGF,MAAM,mBAAmB,aAAa,uBAAuB;EAC7D,MAAM,aAAa,cAAc,uBAAuB;AAExD,MAAI,WAAW,UAAU,KAAK,WAAW,WAAW,EAAG;EAGvD,MAAM,WAAW,2BADK,OAAO,iBAAiB,cAAc,CACF,UAAU;EAEpE,MAAM,kBAAkB;GACtB,GAAG,WAAW,OAAO,iBAAiB;GACtC,GAAG,WAAW,MAAM,iBAAiB;GACrC,OAAO,WAAW;GAClB,QAAQ,WAAW;GACnB;GACD;AAED,MAAI,gBAAgB,SAAS,KAAK,gBAAgB,UAAU,EAAG;AAU/D,MAPE,CAAC,KAAK,mBACN,KAAK,IAAI,KAAK,gBAAgB,IAAI,gBAAgB,EAAE,GAAG,OACvD,KAAK,IAAI,KAAK,gBAAgB,IAAI,gBAAgB,EAAE,GAAG,OACvD,KAAK,IAAI,KAAK,gBAAgB,QAAQ,gBAAgB,MAAM,GAAG,OAC/D,KAAK,IAAI,KAAK,gBAAgB,SAAS,gBAAgB,OAAO,GAAG,OACjE,KAAK,IAAI,KAAK,gBAAgB,WAAW,gBAAgB,SAAS,GAAG,KAElD;AACnB,QAAK,kBAAkB;AAEvB,QAAK,MAAM,OAAO,GAAG,gBAAgB,EAAE;AACvC,QAAK,MAAM,MAAM,GAAG,gBAAgB,EAAE;AACtC,QAAK,MAAM,QAAQ,GAAG,gBAAgB,MAAM;AAC5C,QAAK,MAAM,SAAS,GAAG,gBAAgB,OAAO;AAE9C,OAAI,gBAAgB,aAAa,EAC/B,MAAK,MAAM,YAAY,UAAU,gBAAgB,SAAS;OAE1D,MAAK,MAAM,YAAY;AAGzB,QAAK,cACH,IAAI,YAAiC,oBAAoB;IACvD,QAAQ,EAAE,GAAG,iBAAiB;IAC9B,SAAS;IACT,UAAU;IACX,CAAC,CACH;;;CAIL,oBAAoB;AAClB,QAAM,mBAAmB;EAEzB,MAAM,SAAS,KAAK;AACpB,MAAI,UAAU,OAAQ,OAAe,wBAAwB,WAC3D,CAAC,OAAe,oBAAoB,KAAK;;CAI7C,uBAAuB;AACrB,QAAM,sBAAsB;EAE5B,MAAM,SAAS,KAAK;AACpB,MAAI,UAAU,OAAQ,OAAe,0BAA0B,WAC7D,CAAC,OAAe,sBAAsB,KAAK;;CAI/C,SAAS;AACP,SAAO,IAAI;;;YAlIZ,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAc,CAAC;YAGnD,SAAS,EAAE,WAAW,OAAO,CAAC;4BAhBhC,cAAc,kBAAkB"}
|