@editframe/elements 0.42.0 → 0.42.2

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.
@@ -176,15 +176,12 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
176
176
  if (fingerprint === this.#lastPropagatedAnimation) return;
177
177
  this.#lastPropagatedAnimation = fingerprint;
178
178
  const hasAnimation = animationName && animationName !== "none";
179
- const isLineMode = this.split === "line";
180
- for (const segment of segments) if (hasAnimation) {
181
- const isWhitespace = /^\s+$/.test(segment.segmentText || "");
182
- if (!isLineMode && !isWhitespace) segment.setAttribute("data-animated", "");
183
- for (const prop of animationPropsToPropagate) segment.style.setProperty(prop, computed.getPropertyValue(prop));
184
- } else {
185
- segment.removeAttribute("data-animated");
186
- for (const prop of animationPropsToPropagate) segment.style.removeProperty(prop);
179
+ for (const segment of segments) if (hasAnimation) for (const prop of animationPropsToPropagate) {
180
+ let value = computed.getPropertyValue(prop);
181
+ if (prop === "animation-fill-mode" && value === "none") value = "backwards";
182
+ segment.style.setProperty(prop, value);
187
183
  }
184
+ else for (const prop of animationPropsToPropagate) segment.style.removeProperty(prop);
188
185
  }
189
186
  getTextContent() {
190
187
  let text = "";
@@ -264,6 +261,7 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
264
261
  segment.segmentEndMs = durationMs;
265
262
  segment.staggerOffsetMs = staggerOffset ?? 0;
266
263
  if (this.split === "line") segment.setAttribute("data-line-segment", "true");
264
+ if (/^\s+$/.test(segmentText)) segment.setAttribute("data-whitespace", "");
267
265
  segment.setAttribute("data-segment-created", "true");
268
266
  if (this.split === "char" && wordBoundaries) {
269
267
  const originalCharIndex = textStartOffset + charIndex;
@@ -296,6 +294,7 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
296
294
  segment.segmentEndMs = durationMs;
297
295
  segment.staggerOffsetMs = staggerOffset ?? 0;
298
296
  if (this.split === "line") segment.setAttribute("data-line-segment", "true");
297
+ if (/^\s+$/.test(segmentText)) segment.setAttribute("data-whitespace", "");
299
298
  segment.setAttribute("data-segment-created", "true");
300
299
  if (this.split === "char" && wordBoundaries) {
301
300
  const originalCharIndex = textStartOffset + charIndex;
@@ -335,6 +334,10 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
335
334
  if (templateToPreserve) this.appendChild(templateToPreserve);
336
335
  this.#lastPropagatedAnimation = "";
337
336
  this.propagateAnimationToSegments();
337
+ if (useTemplate) for (const segment of this.segments) {
338
+ const computedFill = window.getComputedStyle(segment).getPropertyValue("animation-fill-mode");
339
+ if (computedFill === "none" || computedFill === "") segment.style.setProperty("animation-fill-mode", "backwards");
340
+ }
338
341
  const segmentElements = this.segments;
339
342
  Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {
340
343
  updateAnimations(this.rootTimegroup || this);
@@ -1 +1 @@
1
- {"version":3,"file":"EFText.js","names":["EFText","textNodes: ChildNode[]","#segmentsInitialized","segments","#lastPropagatedAnimation","wordBoundaries: Map<number, number> | null","currentWordIndex: number | null","currentWordSpan: HTMLSpanElement | null","staggerOffset: number | undefined","wordIndexForStagger: number","result: string[]"],"sources":["../../src/elements/EFText.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { durationConverter } from \"./durationConverter.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\n\nimport { evaluateEasing } from \"./easingUtils.js\";\nimport type { EFTextSegment } from \"./EFTextSegment.js\";\nimport { updateAnimations } from \"./updateAnimations.js\";\nimport type { AnimatableElement } from \"./updateAnimations.js\";\n\nexport type SplitMode = \"line\" | \"word\" | \"char\";\n\n// Animation properties to propagate from ef-text to its segments.\n// animation-delay is intentionally excluded -- the stagger system manages delay per segment.\nconst animationPropsToPropagate = [\n \"animation-name\",\n \"animation-duration\",\n \"animation-timing-function\",\n \"animation-fill-mode\",\n \"animation-iteration-count\",\n \"animation-direction\",\n] as const;\n\n@customElement(\"ef-text\")\nexport class EFText extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline;\n }\n :host([split=\"line\"]) {\n display: inline-block;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n split: SplitMode = \"word\";\n\n private validateSplit(value: string): SplitMode {\n if (value === \"line\" || value === \"word\" || value === \"char\") {\n return value as SplitMode;\n }\n console.warn(\n `Invalid split value \"${value}\". Must be \"line\", \"word\", or \"char\". Defaulting to \"word\".`,\n );\n return \"word\";\n }\n\n @property({\n type: Number,\n attribute: \"stagger\",\n converter: durationConverter,\n })\n staggerMs?: number;\n\n private validateStagger(value: number | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value < 0) {\n console.warn(`Invalid stagger value ${value}ms. Must be >= 0. Using 0.`);\n return 0;\n }\n return value;\n }\n\n @property({ type: String, reflect: true })\n easing = \"linear\";\n\n private mutationObserver?: MutationObserver;\n private animationObserver?: MutationObserver;\n private lastTextContent = \"\";\n private _textContent: string | null = null; // null means not initialized, \"\" means explicitly empty\n private _templateElement: HTMLTemplateElement | null = null;\n private _segmentsReadyResolvers: Array<() => void> = [];\n #segmentsInitialized = false;\n #lastPropagatedAnimation = \"\";\n\n render() {\n return html`<slot></slot>`;\n }\n\n // Store text content so we can use it even after DOM is cleared\n set textContent(value: string | null) {\n const newValue = value || \"\";\n // Only update if value actually changed\n if (this._textContent !== newValue) {\n this._textContent = newValue;\n\n // Find template element if not already stored\n if (!this._templateElement && this.isConnected) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Clear any existing text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n // Add new text node if value is not empty\n if (newValue) {\n const textNode = document.createTextNode(newValue);\n this.appendChild(textNode);\n }\n if (this.isConnected) {\n this.emitContentChange(\"content\");\n this.splitText();\n }\n }\n }\n\n get textContent(): string {\n // If _textContent is null, it hasn't been initialized - read from DOM\n if (this._textContent === null) {\n return this.getTextContent();\n }\n // Otherwise use stored value (even if empty string)\n return this._textContent;\n }\n\n /**\n * Get all ef-text-segment elements directly\n * @public\n */\n get segments(): EFTextSegment[] {\n return Array.from(\n this.querySelectorAll(\"ef-text-segment[data-segment-created]\"),\n ) as EFTextSegment[];\n }\n\n /**\n * Returns a promise that resolves when segments are ready (created and connected)\n * Use this to wait for segments after setting textContent or other properties\n * @public\n */\n async whenSegmentsReady(): Promise<EFTextSegment[]> {\n // Wait for text element to be updated first\n await this.updateComplete;\n\n // If no text content, segments will be empty - return immediately\n // Use same logic as splitText to read text content\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return [];\n }\n\n // If segments already initialized and exist, return immediately (no RAF waits)\n if (this.#segmentsInitialized && this.segments.length > 0) {\n await Promise.all(this.segments.map((seg) => seg.updateComplete));\n return this.segments;\n }\n\n // Check if segments are already in DOM (synchronous check - no RAF)\n let segments = this.segments;\n if (segments.length > 0) {\n // Segments exist, just wait for their Lit updates (no RAF)\n await Promise.all(segments.map((seg) => seg.updateComplete));\n this.#segmentsInitialized = true;\n return segments;\n }\n\n // Segments don't exist yet - use the promise-based mechanism (no RAF polling)\n // This waits for splitText() to complete and resolve the promise\n return new Promise<EFTextSegment[]>((resolve, reject) => {\n const timeout = setTimeout(() => {\n // Remove our resolver if we timeout\n const index = this._segmentsReadyResolvers.indexOf(resolveWithSegments);\n if (index > -1) {\n this._segmentsReadyResolvers.splice(index, 1);\n }\n reject(new Error(\"Timeout waiting for text segments to be created\"));\n }, 5000); // 5 second timeout\n\n const resolveWithSegments = () => {\n clearTimeout(timeout);\n // Wait for segment Lit updates\n const segments = this.segments;\n Promise.all(segments.map((seg) => seg.updateComplete)).then(() => {\n this.#segmentsInitialized = true;\n resolve(segments);\n });\n };\n\n this._segmentsReadyResolvers.push(resolveWithSegments);\n\n // Trigger splitText if it hasn't run yet\n // This handles the case where segments haven't been created at all\n if (segments.length === 0 && this.isConnected) {\n this.splitText();\n }\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Find and store template element before any modifications\n this._templateElement = this.querySelector(\"template\");\n\n // Initialize _textContent from DOM if not already set (for declarative usage)\n if (this._textContent === null) {\n this._textContent = this.getTextContent();\n this.lastTextContent = this._textContent;\n }\n\n // Use RAF to ensure DOM is fully ready before splitting text\n // Callers that need segments immediately (e.g., render clones) should await whenSegmentsReady()\n requestAnimationFrame(() => {\n this.setupMutationObserver();\n this.setupAnimationObserver();\n this.splitText();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.mutationObserver?.disconnect();\n this.animationObserver?.disconnect();\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n if (\n changedProperties.has(\"split\") ||\n changedProperties.has(\"staggerMs\") ||\n changedProperties.has(\"easing\") ||\n changedProperties.has(\"durationMs\")\n ) {\n this.emitContentChange(\"content\");\n this.splitText();\n }\n }\n\n private setupMutationObserver() {\n this.mutationObserver = new MutationObserver(() => {\n // Only react to changes that aren't from our own segment creation\n const currentText = this._textContent || this.getTextContent();\n if (currentText !== this.lastTextContent) {\n this._textContent = currentText;\n this.lastTextContent = currentText;\n this.splitText();\n }\n });\n\n this.mutationObserver.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n private setupAnimationObserver() {\n this.animationObserver = new MutationObserver(() => {\n this.propagateAnimationToSegments();\n });\n\n this.animationObserver.observe(this, {\n attributes: true,\n attributeFilter: [\"class\", \"style\"],\n });\n }\n\n private propagateAnimationToSegments() {\n const segments = this.segments;\n if (segments.length === 0) return;\n\n const computed = window.getComputedStyle(this);\n const animationName = computed.animationName;\n\n // Build a fingerprint to avoid redundant work\n const fingerprint = animationPropsToPropagate\n .map((prop) => computed.getPropertyValue(prop))\n .join(\"|\");\n\n if (fingerprint === this.#lastPropagatedAnimation) return;\n this.#lastPropagatedAnimation = fingerprint;\n\n const hasAnimation = animationName && animationName !== \"none\";\n const isLineMode = this.split === \"line\";\n\n for (const segment of segments) {\n if (hasAnimation) {\n // Mark non-whitespace segments so shadow DOM rule promotes them to inline-block\n // for transform support. Using an attribute (not inline style) so it survives the\n // visibility system's removeProperty(\"display\") calls.\n // Whitespace-only segments must stay inline — inline-block creates a new block\n // formatting context that collapses the space to zero width.\n const isWhitespace = /^\\s+$/.test(segment.segmentText || \"\");\n if (!isLineMode && !isWhitespace) {\n segment.setAttribute(\"data-animated\", \"\");\n }\n for (const prop of animationPropsToPropagate) {\n segment.style.setProperty(prop, computed.getPropertyValue(prop));\n }\n } else {\n segment.removeAttribute(\"data-animated\");\n for (const prop of animationPropsToPropagate) {\n segment.style.removeProperty(prop);\n }\n }\n }\n }\n\n private getTextContent(): string {\n // Get text content, handling both text nodes and HTML content\n let text = \"\";\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent || \"\";\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Skip ef-text-segment elements (they're created by us)\n if (element.tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n // Skip template elements (they're templates, not content)\n if (element.tagName === \"TEMPLATE\") {\n continue;\n }\n text += element.textContent || \"\";\n }\n }\n return text;\n }\n\n private splitText() {\n // Validate split mode\n const validatedSplit = this.validateSplit(this.split);\n if (validatedSplit !== this.split) {\n this.split = validatedSplit;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Validate stagger\n const validatedStagger = this.validateStagger(this.staggerMs);\n if (validatedStagger !== this.staggerMs) {\n this.staggerMs = validatedStagger;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Read text content - use stored _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n const trimmedText = text.trim();\n const textStartOffset = text.indexOf(trimmedText);\n\n // GUARD: Check if segments are already correct before clearing/recreating\n // This prevents redundant splits from RAF callbacks, updated(), etc.\n if (\n this.#segmentsInitialized &&\n this.segments.length > 0 &&\n this.lastTextContent === text\n ) {\n return;\n }\n\n if (!text || trimmedText.length === 0) {\n // Clear segments if no text\n const existingSegments = Array.from(\n this.querySelectorAll(\"ef-text-segment\"),\n );\n for (const segment of existingSegments) {\n segment.remove();\n }\n // Clear text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n this.lastTextContent = \"\";\n // Resolve any waiting promises\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n // Reset initialization flag when clearing segments\n this.#segmentsInitialized = false;\n return;\n }\n\n // Reset initialization flag when we're about to create new segments\n this.#segmentsInitialized = false;\n\n const segments = this.splitTextIntoSegments(text);\n const durationMs = this.durationMs || 1000; // Default 1 second if no duration\n\n // For character mode, detect word boundaries to wrap characters within words\n let wordBoundaries: Map<number, number> | null = null;\n if (this.split === \"char\") {\n wordBoundaries = this.detectWordBoundaries(text);\n }\n\n // Clear ALL child nodes (text nodes and segments) by replacing innerHTML\n // This ensures we don't have any leftover text nodes\n const fragment = document.createDocumentFragment();\n\n // Find template element if not already stored\n if (!this._templateElement) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Get template content structure\n // If template exists, clone it; otherwise create default ef-text-segment\n const templateContent = this._templateElement?.content;\n const templateSegments = templateContent\n ? Array.from(templateContent.querySelectorAll(\"ef-text-segment\"))\n : [];\n\n // If no template segments found, we'll create a default one\n const useTemplate = templateSegments.length > 0;\n const segmentsPerTextSegment = useTemplate ? templateSegments.length : 1;\n\n // For character mode with word wrapping, track current word and wrap segments\n let currentWordIndex: number | null = null;\n let currentWordSpan: HTMLSpanElement | null = null;\n let charIndex = 0; // Track position in original text for character mode\n\n // For word splitting, count only word segments (not whitespace) for stagger calculation\n const wordOnlySegments =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg))\n : segments;\n const wordSegmentCount = wordOnlySegments.length;\n\n // Track word index as we iterate (for word mode with duplicate words)\n // This ensures each occurrence of duplicate words gets a unique stagger index\n let wordStaggerIndex = 0;\n\n // Create new segments in a fragment first\n segments.forEach((segmentText, textIndex) => {\n // Calculate stagger offset if stagger is set\n let staggerOffset: number | undefined;\n if (this.staggerMs !== undefined) {\n // For word splitting, whitespace segments should inherit stagger from preceding word\n const isWhitespace = /^\\s+$/.test(segmentText);\n let wordIndexForStagger: number;\n\n if (this.split === \"word\" && isWhitespace) {\n // Whitespace inherits from the preceding word's index\n // Use the word stagger index (which is the index of the word before this whitespace)\n wordIndexForStagger = Math.max(0, wordStaggerIndex - 1);\n } else if (this.split === \"word\") {\n // For word mode, use the current word stagger index (incremented for each word encountered)\n // This ensures duplicate words get unique indices based on their position\n wordIndexForStagger = wordStaggerIndex;\n wordStaggerIndex++;\n } else {\n // For char/line mode, use the actual position in segments array\n wordIndexForStagger = textIndex;\n }\n\n // Apply easing to the stagger offset\n // Normalize index to 0-1 range (0 for first segment, 1 for last segment)\n const normalizedProgress =\n wordSegmentCount > 1\n ? wordIndexForStagger / (wordSegmentCount - 1)\n : 0;\n\n // Apply easing function to get eased progress\n const easedProgress = evaluateEasing(this.easing, normalizedProgress);\n\n // Calculate total stagger duration (last segment gets full stagger)\n const totalStaggerDuration = (wordSegmentCount - 1) * this.staggerMs;\n\n // Apply eased progress to total stagger duration\n staggerOffset = easedProgress * totalStaggerDuration;\n }\n\n if (useTemplate && templateContent) {\n // Clone template content for each text segment\n // This allows multiple ef-text-segment elements per character/word/line\n const clonedContent = templateContent.cloneNode(\n true,\n ) as DocumentFragment;\n const clonedSegments = Array.from(\n clonedContent.querySelectorAll(\"ef-text-segment\"),\n ) as EFTextSegment[];\n\n clonedSegments.forEach((segment, templateIndex) => {\n // Set properties - Lit will process these when element is connected\n segment.segmentText = segmentText;\n // Calculate segment index accounting for multiple segments per text segment\n segment.segmentIndex =\n textIndex * segmentsPerTextSegment + templateIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode with templates, also wrap in word spans\n if (this.split === \"char\" && wordBoundaries) {\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n if (wordIndex !== currentWordIndex) {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.style.whiteSpace = \"nowrap\";\n }\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n charIndex += segmentText.length;\n } else {\n fragment.appendChild(segment);\n }\n });\n } else {\n // No template - create default ef-text-segment\n const segment = document.createElement(\n \"ef-text-segment\",\n ) as EFTextSegment;\n\n segment.segmentText = segmentText;\n segment.segmentIndex = textIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode, wrap segments within words to prevent line breaks\n if (this.split === \"char\" && wordBoundaries) {\n // Map character index in trimmed text to original text position\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n // Check if we're starting a new word\n if (wordIndex !== currentWordIndex) {\n // Close previous word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n // Start new word span\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.style.whiteSpace = \"nowrap\";\n }\n // Append segment to current word span\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n // Not part of a word (whitespace/punctuation) - append directly\n // Close current word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n // Update character index for next iteration (in trimmed text)\n charIndex += segmentText.length;\n } else {\n // Not character mode or no word boundaries - append directly\n fragment.appendChild(segment);\n }\n }\n });\n\n // Close any remaining word span\n if (this.split === \"char\" && currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n\n // Ensure segments are connected to DOM before checking for animations\n // Append fragment first, then trigger updates\n\n // Replace all children with the fragment (this clears text nodes and old segments)\n // But preserve the template element if it exists\n const templateToPreserve = this._templateElement;\n while (this.firstChild) {\n const child = this.firstChild;\n // Don't remove the template element\n if (child === templateToPreserve) {\n // Skip template, but we need to move it after the fragment\n // So we'll remove it temporarily and re-add it after\n this.removeChild(child);\n continue;\n }\n this.removeChild(child);\n }\n this.appendChild(fragment);\n // Re-add template element if it existed\n if (templateToPreserve) {\n this.appendChild(templateToPreserve);\n }\n\n // Propagate animation properties from this element to newly created segments\n this.#lastPropagatedAnimation = \"\";\n this.propagateAnimationToSegments();\n\n // Segments will pause their own animations in connectedCallback\n // Lit will automatically update them when they're connected to the DOM\n // Ensure segments are updated after being connected\n const segmentElements = this.segments;\n Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {\n const rootTimegroup = this.rootTimegroup || this;\n updateAnimations(rootTimegroup as AnimatableElement);\n });\n\n this.lastTextContent = text;\n this._textContent = text;\n\n // Mark segments as initialized to prevent redundant splits\n this.#segmentsInitialized = true;\n\n // Resolve any waiting promises after segments are connected (synchronous)\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n }\n\n private detectWordBoundaries(text: string): Map<number, number> {\n // Create a map from character index to word index\n // Characters within the same word will have the same word index\n const boundaries = new Map<number, number>();\n const trimmedText = text.trim();\n if (!trimmedText) {\n return boundaries;\n }\n\n // Use Intl.Segmenter to detect word boundaries\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n\n // Find the offset of trimmedText within the original text\n const textStart = text.indexOf(trimmedText);\n\n let wordIndex = 0;\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Map all character positions in this word to the same word index\n for (let i = 0; i < seg.segment.length; i++) {\n const charPos = textStart + seg.index + i;\n boundaries.set(charPos, wordIndex);\n }\n wordIndex++;\n }\n }\n\n return boundaries;\n }\n\n private splitTextIntoSegments(text: string): string[] {\n // Trim text before segmenting to remove leading/trailing whitespace\n const trimmedText = text.trim();\n if (!trimmedText) {\n return [];\n }\n\n switch (this.split) {\n case \"line\": {\n // Split on newlines and trim each line\n const lines = trimmedText.split(/\\r?\\n/);\n return lines\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n }\n case \"word\": {\n // Use Intl.Segmenter for locale-aware word segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n const result: string[] = [];\n\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Word-like segment - add it\n result.push(seg.segment);\n } else if (/^\\s+$/.test(seg.segment)) {\n // Whitespace segment - add it as-is\n result.push(seg.segment);\n } else {\n // Punctuation segment - attach to preceding word if it exists\n if (result.length > 0) {\n const lastItem = result[result.length - 1];\n if (lastItem && !/^\\s+$/.test(lastItem)) {\n result[result.length - 1] = lastItem + seg.segment;\n } else {\n result.push(seg.segment);\n }\n } else {\n result.push(seg.segment);\n }\n }\n }\n\n return result;\n }\n case \"char\": {\n // Use Intl.Segmenter for grapheme-aware character segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"grapheme\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n return segments.map((seg) => seg.segment);\n }\n default:\n return [trimmedText];\n }\n }\n\n get intrinsicDurationMs(): number | undefined {\n // If explicit duration is set, use it\n if (this.hasExplicitDuration) {\n return undefined; // Let explicit duration take precedence\n }\n\n // If we have a parent timegroup that dictates duration (fixed) or inherits it (fit),\n // we should inherit from it instead of using our intrinsic duration.\n // For 'sequence' and 'contain' modes, the parent relies on our intrinsic duration,\n // so we must provide it.\n if (this.parentTimegroup) {\n const mode = this.parentTimegroup.mode;\n if (mode === \"fixed\" || mode === \"fit\") {\n return undefined;\n }\n }\n\n // Otherwise, calculate from content\n // Use _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return 0;\n }\n\n // Use the same splitting logic as splitTextIntoSegments for consistency\n const segments = this.splitTextIntoSegments(text);\n // For word splitting, only count word segments (not whitespace) for intrinsic duration\n const segmentCount =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg)).length || 1\n : segments.length || 1;\n\n return segmentCount * 1000;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text\": EFText;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EFText.js","names":["EFText","textNodes: ChildNode[]","#segmentsInitialized","segments","#lastPropagatedAnimation","wordBoundaries: Map<number, number> | null","currentWordIndex: number | null","currentWordSpan: HTMLSpanElement | null","staggerOffset: number | undefined","wordIndexForStagger: number","result: string[]"],"sources":["../../src/elements/EFText.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { durationConverter } from \"./durationConverter.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\n\nimport { evaluateEasing } from \"./easingUtils.js\";\nimport type { EFTextSegment } from \"./EFTextSegment.js\";\nimport { updateAnimations } from \"./updateAnimations.js\";\nimport type { AnimatableElement } from \"./updateAnimations.js\";\n\nexport type SplitMode = \"line\" | \"word\" | \"char\";\n\n// Animation properties to propagate from ef-text to its segments.\n// animation-delay is intentionally excluded -- the stagger system manages delay per segment.\nconst animationPropsToPropagate = [\n \"animation-name\",\n \"animation-duration\",\n \"animation-timing-function\",\n \"animation-fill-mode\",\n \"animation-iteration-count\",\n \"animation-direction\",\n] as const;\n\n@customElement(\"ef-text\")\nexport class EFText extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline;\n }\n :host([split=\"line\"]) {\n display: inline-block;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n split: SplitMode = \"word\";\n\n private validateSplit(value: string): SplitMode {\n if (value === \"line\" || value === \"word\" || value === \"char\") {\n return value as SplitMode;\n }\n console.warn(\n `Invalid split value \"${value}\". Must be \"line\", \"word\", or \"char\". Defaulting to \"word\".`,\n );\n return \"word\";\n }\n\n @property({\n type: Number,\n attribute: \"stagger\",\n converter: durationConverter,\n })\n staggerMs?: number;\n\n private validateStagger(value: number | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value < 0) {\n console.warn(`Invalid stagger value ${value}ms. Must be >= 0. Using 0.`);\n return 0;\n }\n return value;\n }\n\n @property({ type: String, reflect: true })\n easing = \"linear\";\n\n private mutationObserver?: MutationObserver;\n private animationObserver?: MutationObserver;\n private lastTextContent = \"\";\n private _textContent: string | null = null; // null means not initialized, \"\" means explicitly empty\n private _templateElement: HTMLTemplateElement | null = null;\n private _segmentsReadyResolvers: Array<() => void> = [];\n #segmentsInitialized = false;\n #lastPropagatedAnimation = \"\";\n\n render() {\n return html`<slot></slot>`;\n }\n\n // Store text content so we can use it even after DOM is cleared\n set textContent(value: string | null) {\n const newValue = value || \"\";\n // Only update if value actually changed\n if (this._textContent !== newValue) {\n this._textContent = newValue;\n\n // Find template element if not already stored\n if (!this._templateElement && this.isConnected) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Clear any existing text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n // Add new text node if value is not empty\n if (newValue) {\n const textNode = document.createTextNode(newValue);\n this.appendChild(textNode);\n }\n if (this.isConnected) {\n this.emitContentChange(\"content\");\n this.splitText();\n }\n }\n }\n\n get textContent(): string {\n // If _textContent is null, it hasn't been initialized - read from DOM\n if (this._textContent === null) {\n return this.getTextContent();\n }\n // Otherwise use stored value (even if empty string)\n return this._textContent;\n }\n\n /**\n * Get all ef-text-segment elements directly\n * @public\n */\n get segments(): EFTextSegment[] {\n return Array.from(\n this.querySelectorAll(\"ef-text-segment[data-segment-created]\"),\n ) as EFTextSegment[];\n }\n\n /**\n * Returns a promise that resolves when segments are ready (created and connected)\n * Use this to wait for segments after setting textContent or other properties\n * @public\n */\n async whenSegmentsReady(): Promise<EFTextSegment[]> {\n // Wait for text element to be updated first\n await this.updateComplete;\n\n // If no text content, segments will be empty - return immediately\n // Use same logic as splitText to read text content\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return [];\n }\n\n // If segments already initialized and exist, return immediately (no RAF waits)\n if (this.#segmentsInitialized && this.segments.length > 0) {\n await Promise.all(this.segments.map((seg) => seg.updateComplete));\n return this.segments;\n }\n\n // Check if segments are already in DOM (synchronous check - no RAF)\n let segments = this.segments;\n if (segments.length > 0) {\n // Segments exist, just wait for their Lit updates (no RAF)\n await Promise.all(segments.map((seg) => seg.updateComplete));\n this.#segmentsInitialized = true;\n return segments;\n }\n\n // Segments don't exist yet - use the promise-based mechanism (no RAF polling)\n // This waits for splitText() to complete and resolve the promise\n return new Promise<EFTextSegment[]>((resolve, reject) => {\n const timeout = setTimeout(() => {\n // Remove our resolver if we timeout\n const index = this._segmentsReadyResolvers.indexOf(resolveWithSegments);\n if (index > -1) {\n this._segmentsReadyResolvers.splice(index, 1);\n }\n reject(new Error(\"Timeout waiting for text segments to be created\"));\n }, 5000); // 5 second timeout\n\n const resolveWithSegments = () => {\n clearTimeout(timeout);\n // Wait for segment Lit updates\n const segments = this.segments;\n Promise.all(segments.map((seg) => seg.updateComplete)).then(() => {\n this.#segmentsInitialized = true;\n resolve(segments);\n });\n };\n\n this._segmentsReadyResolvers.push(resolveWithSegments);\n\n // Trigger splitText if it hasn't run yet\n // This handles the case where segments haven't been created at all\n if (segments.length === 0 && this.isConnected) {\n this.splitText();\n }\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Find and store template element before any modifications\n this._templateElement = this.querySelector(\"template\");\n\n // Initialize _textContent from DOM if not already set (for declarative usage)\n if (this._textContent === null) {\n this._textContent = this.getTextContent();\n this.lastTextContent = this._textContent;\n }\n\n // Use RAF to ensure DOM is fully ready before splitting text\n // Callers that need segments immediately (e.g., render clones) should await whenSegmentsReady()\n requestAnimationFrame(() => {\n this.setupMutationObserver();\n this.setupAnimationObserver();\n this.splitText();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.mutationObserver?.disconnect();\n this.animationObserver?.disconnect();\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n if (\n changedProperties.has(\"split\") ||\n changedProperties.has(\"staggerMs\") ||\n changedProperties.has(\"easing\") ||\n changedProperties.has(\"durationMs\")\n ) {\n this.emitContentChange(\"content\");\n this.splitText();\n }\n }\n\n private setupMutationObserver() {\n this.mutationObserver = new MutationObserver(() => {\n // Only react to changes that aren't from our own segment creation\n const currentText = this._textContent || this.getTextContent();\n if (currentText !== this.lastTextContent) {\n this._textContent = currentText;\n this.lastTextContent = currentText;\n this.splitText();\n }\n });\n\n this.mutationObserver.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n private setupAnimationObserver() {\n this.animationObserver = new MutationObserver(() => {\n this.propagateAnimationToSegments();\n });\n\n this.animationObserver.observe(this, {\n attributes: true,\n attributeFilter: [\"class\", \"style\"],\n });\n }\n\n private propagateAnimationToSegments() {\n const segments = this.segments;\n if (segments.length === 0) return;\n\n const computed = window.getComputedStyle(this);\n const animationName = computed.animationName;\n\n // Build a fingerprint to avoid redundant work\n const fingerprint = animationPropsToPropagate\n .map((prop) => computed.getPropertyValue(prop))\n .join(\"|\");\n\n if (fingerprint === this.#lastPropagatedAnimation) return;\n this.#lastPropagatedAnimation = fingerprint;\n\n const hasAnimation = animationName && animationName !== \"none\";\n\n for (const segment of segments) {\n if (hasAnimation) {\n for (const prop of animationPropsToPropagate) {\n let value = computed.getPropertyValue(prop);\n if (prop === \"animation-fill-mode\" && value === \"none\") {\n value = \"backwards\";\n }\n segment.style.setProperty(prop, value);\n }\n } else {\n for (const prop of animationPropsToPropagate) {\n segment.style.removeProperty(prop);\n }\n }\n }\n }\n\n private getTextContent(): string {\n // Get text content, handling both text nodes and HTML content\n let text = \"\";\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent || \"\";\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Skip ef-text-segment elements (they're created by us)\n if (element.tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n // Skip template elements (they're templates, not content)\n if (element.tagName === \"TEMPLATE\") {\n continue;\n }\n text += element.textContent || \"\";\n }\n }\n return text;\n }\n\n private splitText() {\n // Validate split mode\n const validatedSplit = this.validateSplit(this.split);\n if (validatedSplit !== this.split) {\n this.split = validatedSplit;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Validate stagger\n const validatedStagger = this.validateStagger(this.staggerMs);\n if (validatedStagger !== this.staggerMs) {\n this.staggerMs = validatedStagger;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Read text content - use stored _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n const trimmedText = text.trim();\n const textStartOffset = text.indexOf(trimmedText);\n\n // GUARD: Check if segments are already correct before clearing/recreating\n // This prevents redundant splits from RAF callbacks, updated(), etc.\n if (\n this.#segmentsInitialized &&\n this.segments.length > 0 &&\n this.lastTextContent === text\n ) {\n return;\n }\n\n if (!text || trimmedText.length === 0) {\n // Clear segments if no text\n const existingSegments = Array.from(\n this.querySelectorAll(\"ef-text-segment\"),\n );\n for (const segment of existingSegments) {\n segment.remove();\n }\n // Clear text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n this.lastTextContent = \"\";\n // Resolve any waiting promises\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n // Reset initialization flag when clearing segments\n this.#segmentsInitialized = false;\n return;\n }\n\n // Reset initialization flag when we're about to create new segments\n this.#segmentsInitialized = false;\n\n const segments = this.splitTextIntoSegments(text);\n const durationMs = this.durationMs || 1000; // Default 1 second if no duration\n\n // For character mode, detect word boundaries to wrap characters within words\n let wordBoundaries: Map<number, number> | null = null;\n if (this.split === \"char\") {\n wordBoundaries = this.detectWordBoundaries(text);\n }\n\n // Clear ALL child nodes (text nodes and segments) by replacing innerHTML\n // This ensures we don't have any leftover text nodes\n const fragment = document.createDocumentFragment();\n\n // Find template element if not already stored\n if (!this._templateElement) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Get template content structure\n // If template exists, clone it; otherwise create default ef-text-segment\n const templateContent = this._templateElement?.content;\n const templateSegments = templateContent\n ? Array.from(templateContent.querySelectorAll(\"ef-text-segment\"))\n : [];\n\n // If no template segments found, we'll create a default one\n const useTemplate = templateSegments.length > 0;\n const segmentsPerTextSegment = useTemplate ? templateSegments.length : 1;\n\n // For character mode with word wrapping, track current word and wrap segments\n let currentWordIndex: number | null = null;\n let currentWordSpan: HTMLSpanElement | null = null;\n let charIndex = 0; // Track position in original text for character mode\n\n // For word splitting, count only word segments (not whitespace) for stagger calculation\n const wordOnlySegments =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg))\n : segments;\n const wordSegmentCount = wordOnlySegments.length;\n\n // Track word index as we iterate (for word mode with duplicate words)\n // This ensures each occurrence of duplicate words gets a unique stagger index\n let wordStaggerIndex = 0;\n\n // Create new segments in a fragment first\n segments.forEach((segmentText, textIndex) => {\n // Calculate stagger offset if stagger is set\n let staggerOffset: number | undefined;\n if (this.staggerMs !== undefined) {\n // For word splitting, whitespace segments should inherit stagger from preceding word\n const isWhitespace = /^\\s+$/.test(segmentText);\n let wordIndexForStagger: number;\n\n if (this.split === \"word\" && isWhitespace) {\n // Whitespace inherits from the preceding word's index\n // Use the word stagger index (which is the index of the word before this whitespace)\n wordIndexForStagger = Math.max(0, wordStaggerIndex - 1);\n } else if (this.split === \"word\") {\n // For word mode, use the current word stagger index (incremented for each word encountered)\n // This ensures duplicate words get unique indices based on their position\n wordIndexForStagger = wordStaggerIndex;\n wordStaggerIndex++;\n } else {\n // For char/line mode, use the actual position in segments array\n wordIndexForStagger = textIndex;\n }\n\n // Apply easing to the stagger offset\n // Normalize index to 0-1 range (0 for first segment, 1 for last segment)\n const normalizedProgress =\n wordSegmentCount > 1\n ? wordIndexForStagger / (wordSegmentCount - 1)\n : 0;\n\n // Apply easing function to get eased progress\n const easedProgress = evaluateEasing(this.easing, normalizedProgress);\n\n // Calculate total stagger duration (last segment gets full stagger)\n const totalStaggerDuration = (wordSegmentCount - 1) * this.staggerMs;\n\n // Apply eased progress to total stagger duration\n staggerOffset = easedProgress * totalStaggerDuration;\n }\n\n if (useTemplate && templateContent) {\n // Clone template content for each text segment\n // This allows multiple ef-text-segment elements per character/word/line\n const clonedContent = templateContent.cloneNode(\n true,\n ) as DocumentFragment;\n const clonedSegments = Array.from(\n clonedContent.querySelectorAll(\"ef-text-segment\"),\n ) as EFTextSegment[];\n\n clonedSegments.forEach((segment, templateIndex) => {\n // Set properties - Lit will process these when element is connected\n segment.segmentText = segmentText;\n // Calculate segment index accounting for multiple segments per text segment\n segment.segmentIndex =\n textIndex * segmentsPerTextSegment + templateIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n if (/^\\s+$/.test(segmentText)) {\n segment.setAttribute(\"data-whitespace\", \"\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode with templates, also wrap in word spans\n if (this.split === \"char\" && wordBoundaries) {\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n if (wordIndex !== currentWordIndex) {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.style.whiteSpace = \"nowrap\";\n }\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n charIndex += segmentText.length;\n } else {\n fragment.appendChild(segment);\n }\n });\n } else {\n // No template - create default ef-text-segment\n const segment = document.createElement(\n \"ef-text-segment\",\n ) as EFTextSegment;\n\n segment.segmentText = segmentText;\n segment.segmentIndex = textIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n if (/^\\s+$/.test(segmentText)) {\n segment.setAttribute(\"data-whitespace\", \"\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode, wrap segments within words to prevent line breaks\n if (this.split === \"char\" && wordBoundaries) {\n // Map character index in trimmed text to original text position\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n // Check if we're starting a new word\n if (wordIndex !== currentWordIndex) {\n // Close previous word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n // Start new word span\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.style.whiteSpace = \"nowrap\";\n }\n // Append segment to current word span\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n // Not part of a word (whitespace/punctuation) - append directly\n // Close current word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n // Update character index for next iteration (in trimmed text)\n charIndex += segmentText.length;\n } else {\n // Not character mode or no word boundaries - append directly\n fragment.appendChild(segment);\n }\n }\n });\n\n // Close any remaining word span\n if (this.split === \"char\" && currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n\n // Ensure segments are connected to DOM before checking for animations\n // Append fragment first, then trigger updates\n\n // Replace all children with the fragment (this clears text nodes and old segments)\n // But preserve the template element if it exists\n const templateToPreserve = this._templateElement;\n while (this.firstChild) {\n const child = this.firstChild;\n // Don't remove the template element\n if (child === templateToPreserve) {\n // Skip template, but we need to move it after the fragment\n // So we'll remove it temporarily and re-add it after\n this.removeChild(child);\n continue;\n }\n this.removeChild(child);\n }\n this.appendChild(fragment);\n // Re-add template element if it existed\n if (templateToPreserve) {\n this.appendChild(templateToPreserve);\n }\n\n // Propagate animation properties from this element to newly created segments\n this.#lastPropagatedAnimation = \"\";\n this.propagateAnimationToSegments();\n\n // For template-path segments: their animation comes from a class, not from ef-text.\n // propagateAnimationToSegments() is a no-op when ef-text has no animation, but its\n // else-branch removes all animation inline styles — so we apply the fill-mode default\n // after propagation to avoid being cleared.\n if (useTemplate) {\n for (const segment of this.segments) {\n const computedFill = window\n .getComputedStyle(segment)\n .getPropertyValue(\"animation-fill-mode\");\n if (computedFill === \"none\" || computedFill === \"\") {\n segment.style.setProperty(\"animation-fill-mode\", \"backwards\");\n }\n }\n }\n\n // Segments will pause their own animations in connectedCallback\n // Lit will automatically update them when they're connected to the DOM\n // Ensure segments are updated after being connected\n const segmentElements = this.segments;\n Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {\n const rootTimegroup = this.rootTimegroup || this;\n updateAnimations(rootTimegroup as AnimatableElement);\n });\n\n this.lastTextContent = text;\n this._textContent = text;\n\n // Mark segments as initialized to prevent redundant splits\n this.#segmentsInitialized = true;\n\n // Resolve any waiting promises after segments are connected (synchronous)\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n }\n\n private detectWordBoundaries(text: string): Map<number, number> {\n // Create a map from character index to word index\n // Characters within the same word will have the same word index\n const boundaries = new Map<number, number>();\n const trimmedText = text.trim();\n if (!trimmedText) {\n return boundaries;\n }\n\n // Use Intl.Segmenter to detect word boundaries\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n\n // Find the offset of trimmedText within the original text\n const textStart = text.indexOf(trimmedText);\n\n let wordIndex = 0;\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Map all character positions in this word to the same word index\n for (let i = 0; i < seg.segment.length; i++) {\n const charPos = textStart + seg.index + i;\n boundaries.set(charPos, wordIndex);\n }\n wordIndex++;\n }\n }\n\n return boundaries;\n }\n\n private splitTextIntoSegments(text: string): string[] {\n // Trim text before segmenting to remove leading/trailing whitespace\n const trimmedText = text.trim();\n if (!trimmedText) {\n return [];\n }\n\n switch (this.split) {\n case \"line\": {\n // Split on newlines and trim each line\n const lines = trimmedText.split(/\\r?\\n/);\n return lines\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n }\n case \"word\": {\n // Use Intl.Segmenter for locale-aware word segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n const result: string[] = [];\n\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Word-like segment - add it\n result.push(seg.segment);\n } else if (/^\\s+$/.test(seg.segment)) {\n // Whitespace segment - add it as-is\n result.push(seg.segment);\n } else {\n // Punctuation segment - attach to preceding word if it exists\n if (result.length > 0) {\n const lastItem = result[result.length - 1];\n if (lastItem && !/^\\s+$/.test(lastItem)) {\n result[result.length - 1] = lastItem + seg.segment;\n } else {\n result.push(seg.segment);\n }\n } else {\n result.push(seg.segment);\n }\n }\n }\n\n return result;\n }\n case \"char\": {\n // Use Intl.Segmenter for grapheme-aware character segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"grapheme\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n return segments.map((seg) => seg.segment);\n }\n default:\n return [trimmedText];\n }\n }\n\n get intrinsicDurationMs(): number | undefined {\n // If explicit duration is set, use it\n if (this.hasExplicitDuration) {\n return undefined; // Let explicit duration take precedence\n }\n\n // If we have a parent timegroup that dictates duration (fixed) or inherits it (fit),\n // we should inherit from it instead of using our intrinsic duration.\n // For 'sequence' and 'contain' modes, the parent relies on our intrinsic duration,\n // so we must provide it.\n if (this.parentTimegroup) {\n const mode = this.parentTimegroup.mode;\n if (mode === \"fixed\" || mode === \"fit\") {\n return undefined;\n }\n }\n\n // Otherwise, calculate from content\n // Use _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return 0;\n }\n\n // Use the same splitting logic as splitTextIntoSegments for consistency\n const segments = this.splitTextIntoSegments(text);\n // For word splitting, only count word segments (not whitespace) for intrinsic duration\n const segmentCount =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg)).length || 1\n : segments.length || 1;\n\n return segmentCount * 1000;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text\": EFText;\n }\n}\n"],"mappings":"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"}
@@ -17,11 +17,11 @@ let EFTextSegment = class EFTextSegment$1 extends EFTemporal(LitElement) {
17
17
  static {
18
18
  this.styles = [css`
19
19
  :host {
20
- display: inline;
21
- }
22
- :host([data-animated]) {
23
20
  display: inline-block;
24
21
  }
22
+ :host([data-whitespace]) {
23
+ display: inline;
24
+ }
25
25
  :host([data-line-segment]) {
26
26
  display: block;
27
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EFTextSegment.js","names":["EFTextSegment"],"sources":["../../src/elements/EFTextSegment.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { EFTemporal } from \"./EFTemporal.ts\";\nimport { EFText } from \"./EFText.js\";\n\n// Global registry for animation stylesheets shared across all text segments\nconst globalAnimationSheets = new Map<string, CSSStyleSheet>();\n\n@customElement(\"ef-text-segment\")\nexport class EFTextSegment extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline;\n }\n :host([data-animated]) {\n display: inline-block;\n }\n :host([data-line-segment]) {\n display: block;\n }\n :host([hidden]) {\n opacity: 0;\n pointer-events: none;\n }\n `,\n ];\n\n /**\n * Registers animation styles that should be shared across all text segments.\n * This is the correct way to inject animation styles for segments - not via innerHTML.\n *\n * @param id Unique identifier for this stylesheet (e.g., \"my-animations\")\n * @param cssText The CSS rules to inject\n *\n * @example\n * EFTextSegment.registerAnimations(\"bounceIn\", `\n * @keyframes bounceIn {\n * from { transform: scale(0); }\n * to { transform: scale(1); }\n * }\n * .bounce-in {\n * animation: bounceIn 0.5s ease-out;\n * }\n * `);\n */\n static registerAnimations(id: string, cssText: string): void {\n if (globalAnimationSheets.has(id)) {\n // Already registered\n return;\n }\n\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(cssText);\n globalAnimationSheets.set(id, sheet);\n\n // Apply to all existing instances\n document.querySelectorAll(\"ef-text-segment\").forEach((segment) => {\n if (segment.shadowRoot) {\n const adoptedSheets = segment.shadowRoot.adoptedStyleSheets;\n if (!adoptedSheets.includes(sheet)) {\n segment.shadowRoot.adoptedStyleSheets = [...adoptedSheets, sheet];\n }\n }\n });\n }\n\n /**\n * Unregisters previously registered animation styles.\n *\n * @param id The identifier of the stylesheet to remove\n */\n static unregisterAnimations(id: string): void {\n const sheet = globalAnimationSheets.get(id);\n if (!sheet) {\n return;\n }\n\n globalAnimationSheets.delete(id);\n\n // Remove from all existing instances\n document.querySelectorAll(\"ef-text-segment\").forEach((segment) => {\n if (segment.shadowRoot) {\n segment.shadowRoot.adoptedStyleSheets =\n segment.shadowRoot.adoptedStyleSheets.filter((s) => s !== sheet);\n }\n });\n }\n\n render() {\n // Set CSS variables in render() to ensure they're always set\n // This is necessary because Lit may clear inline styles during updates\n this.setCSSVariables();\n return html`${this.segmentText}`;\n }\n\n private setCSSVariables(): void {\n // Set deterministic --ef-seed value based on segment index\n const seed = (this.segmentIndex * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-seed\", seedValue.toString());\n\n // Set stagger offset CSS variable\n // staggerOffsetMs is always set (defaults to 0), so we can always set the CSS variable\n const offsetMs = this.staggerOffsetMs ?? 0;\n this.style.setProperty(\"--ef-stagger-offset\", `${offsetMs}ms`);\n\n // Set index CSS variable\n this.style.setProperty(\"--ef-index\", this.segmentIndex.toString());\n }\n\n protected firstUpdated(): void {\n this.setCSSVariables();\n }\n\n protected updated(): void {\n this.setCSSVariables();\n }\n\n @property({ type: String, attribute: false })\n segmentText = \"\";\n\n @property({ type: Number, attribute: false })\n segmentIndex = 0;\n\n @property({ type: Number, attribute: false })\n staggerOffsetMs?: number;\n\n @property({ type: Number, attribute: false })\n segmentStartMs = 0;\n\n @property({ type: Number, attribute: false })\n segmentEndMs = 0;\n\n @property({ type: Boolean, reflect: true })\n hidden = false;\n\n get startTimeMs() {\n const parentText = this.closest(\"ef-text\") as EFText;\n const parentStartTime = parentText?.startTimeMs || 0;\n return parentStartTime + (this.segmentStartMs || 0);\n }\n\n get endTimeMs() {\n // Derive from parent's live durationMs rather than the snapshot stored in segmentEndMs.\n // This ensures segments track changes when the parent's duration updates\n // (e.g., a contain-mode timegroup whose duration changes after a video loads).\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.startTimeMs + parentText.durationMs;\n }\n return this.segmentEndMs || 0;\n }\n\n get durationMs(): number {\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.durationMs;\n }\n return this.segmentEndMs - this.segmentStartMs;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text-segment\": EFTextSegment;\n }\n}\n"],"mappings":";;;;;;AAMA,MAAM,wCAAwB,IAAI,KAA4B;AAGvD,0BAAMA,wBAAsB,WAAW,WAAW,CAAC;;;qBA+G1C;sBAGC;wBAME;sBAGF;gBAGN;;;gBA7HO,CACd,GAAG;;;;;;;;;;;;;;MAeJ;;;;;;;;;;;;;;;;;;;;CAoBD,OAAO,mBAAmB,IAAY,SAAuB;AAC3D,MAAI,sBAAsB,IAAI,GAAG,CAE/B;EAGF,MAAM,QAAQ,IAAI,eAAe;AACjC,QAAM,YAAY,QAAQ;AAC1B,wBAAsB,IAAI,IAAI,MAAM;AAGpC,WAAS,iBAAiB,kBAAkB,CAAC,SAAS,YAAY;AAChE,OAAI,QAAQ,YAAY;IACtB,MAAM,gBAAgB,QAAQ,WAAW;AACzC,QAAI,CAAC,cAAc,SAAS,MAAM,CAChC,SAAQ,WAAW,qBAAqB,CAAC,GAAG,eAAe,MAAM;;IAGrE;;;;;;;CAQJ,OAAO,qBAAqB,IAAkB;EAC5C,MAAM,QAAQ,sBAAsB,IAAI,GAAG;AAC3C,MAAI,CAAC,MACH;AAGF,wBAAsB,OAAO,GAAG;AAGhC,WAAS,iBAAiB,kBAAkB,CAAC,SAAS,YAAY;AAChE,OAAI,QAAQ,WACV,SAAQ,WAAW,qBACjB,QAAQ,WAAW,mBAAmB,QAAQ,MAAM,MAAM,MAAM;IAEpE;;CAGJ,SAAS;AAGP,OAAK,iBAAiB;AACtB,SAAO,IAAI,GAAG,KAAK;;CAGrB,AAAQ,kBAAwB;EAG9B,MAAM,YADQ,KAAK,eAAe,OAAQ,MACjB;AACzB,OAAK,MAAM,YAAY,aAAa,UAAU,UAAU,CAAC;EAIzD,MAAM,WAAW,KAAK,mBAAmB;AACzC,OAAK,MAAM,YAAY,uBAAuB,GAAG,SAAS,IAAI;AAG9D,OAAK,MAAM,YAAY,cAAc,KAAK,aAAa,UAAU,CAAC;;CAGpE,AAAU,eAAqB;AAC7B,OAAK,iBAAiB;;CAGxB,AAAU,UAAgB;AACxB,OAAK,iBAAiB;;CAqBxB,IAAI,cAAc;AAGhB,UAFmB,KAAK,QAAQ,UAAU,EACN,eAAe,MACzB,KAAK,kBAAkB;;CAGnD,IAAI,YAAY;EAId,MAAM,aAAa,KAAK,QAAQ,UAAU;AAC1C,MAAI,WACF,QAAO,WAAW,cAAc,WAAW;AAE7C,SAAO,KAAK,gBAAgB;;CAG9B,IAAI,aAAqB;EACvB,MAAM,aAAa,KAAK,QAAQ,UAAU;AAC1C,MAAI,WACF,QAAO,WAAW;AAEpB,SAAO,KAAK,eAAe,KAAK;;;YAxCjC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAS,SAAS;CAAM,CAAC;4BA9H5C,cAAc,kBAAkB"}
1
+ {"version":3,"file":"EFTextSegment.js","names":["EFTextSegment"],"sources":["../../src/elements/EFTextSegment.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { EFTemporal } from \"./EFTemporal.ts\";\nimport { EFText } from \"./EFText.js\";\n\n// Global registry for animation stylesheets shared across all text segments\nconst globalAnimationSheets = new Map<string, CSSStyleSheet>();\n\n@customElement(\"ef-text-segment\")\nexport class EFTextSegment extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline-block;\n }\n :host([data-whitespace]) {\n display: inline;\n }\n :host([data-line-segment]) {\n display: block;\n }\n :host([hidden]) {\n opacity: 0;\n pointer-events: none;\n }\n `,\n ];\n\n /**\n * Registers animation styles that should be shared across all text segments.\n * This is the correct way to inject animation styles for segments - not via innerHTML.\n *\n * @param id Unique identifier for this stylesheet (e.g., \"my-animations\")\n * @param cssText The CSS rules to inject\n *\n * @example\n * EFTextSegment.registerAnimations(\"bounceIn\", `\n * @keyframes bounceIn {\n * from { transform: scale(0); }\n * to { transform: scale(1); }\n * }\n * .bounce-in {\n * animation: bounceIn 0.5s ease-out;\n * }\n * `);\n */\n static registerAnimations(id: string, cssText: string): void {\n if (globalAnimationSheets.has(id)) {\n // Already registered\n return;\n }\n\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(cssText);\n globalAnimationSheets.set(id, sheet);\n\n // Apply to all existing instances\n document.querySelectorAll(\"ef-text-segment\").forEach((segment) => {\n if (segment.shadowRoot) {\n const adoptedSheets = segment.shadowRoot.adoptedStyleSheets;\n if (!adoptedSheets.includes(sheet)) {\n segment.shadowRoot.adoptedStyleSheets = [...adoptedSheets, sheet];\n }\n }\n });\n }\n\n /**\n * Unregisters previously registered animation styles.\n *\n * @param id The identifier of the stylesheet to remove\n */\n static unregisterAnimations(id: string): void {\n const sheet = globalAnimationSheets.get(id);\n if (!sheet) {\n return;\n }\n\n globalAnimationSheets.delete(id);\n\n // Remove from all existing instances\n document.querySelectorAll(\"ef-text-segment\").forEach((segment) => {\n if (segment.shadowRoot) {\n segment.shadowRoot.adoptedStyleSheets =\n segment.shadowRoot.adoptedStyleSheets.filter((s) => s !== sheet);\n }\n });\n }\n\n render() {\n // Set CSS variables in render() to ensure they're always set\n // This is necessary because Lit may clear inline styles during updates\n this.setCSSVariables();\n return html`${this.segmentText}`;\n }\n\n private setCSSVariables(): void {\n // Set deterministic --ef-seed value based on segment index\n const seed = (this.segmentIndex * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-seed\", seedValue.toString());\n\n // Set stagger offset CSS variable\n // staggerOffsetMs is always set (defaults to 0), so we can always set the CSS variable\n const offsetMs = this.staggerOffsetMs ?? 0;\n this.style.setProperty(\"--ef-stagger-offset\", `${offsetMs}ms`);\n\n // Set index CSS variable\n this.style.setProperty(\"--ef-index\", this.segmentIndex.toString());\n }\n\n protected firstUpdated(): void {\n this.setCSSVariables();\n }\n\n protected updated(): void {\n this.setCSSVariables();\n }\n\n @property({ type: String, attribute: false })\n segmentText = \"\";\n\n @property({ type: Number, attribute: false })\n segmentIndex = 0;\n\n @property({ type: Number, attribute: false })\n staggerOffsetMs?: number;\n\n @property({ type: Number, attribute: false })\n segmentStartMs = 0;\n\n @property({ type: Number, attribute: false })\n segmentEndMs = 0;\n\n @property({ type: Boolean, reflect: true })\n hidden = false;\n\n get startTimeMs() {\n const parentText = this.closest(\"ef-text\") as EFText;\n const parentStartTime = parentText?.startTimeMs || 0;\n return parentStartTime + (this.segmentStartMs || 0);\n }\n\n get endTimeMs() {\n // Derive from parent's live durationMs rather than the snapshot stored in segmentEndMs.\n // This ensures segments track changes when the parent's duration updates\n // (e.g., a contain-mode timegroup whose duration changes after a video loads).\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.startTimeMs + parentText.durationMs;\n }\n return this.segmentEndMs || 0;\n }\n\n get durationMs(): number {\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.durationMs;\n }\n return this.segmentEndMs - this.segmentStartMs;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text-segment\": EFTextSegment;\n }\n}\n"],"mappings":";;;;;;AAMA,MAAM,wCAAwB,IAAI,KAA4B;AAGvD,0BAAMA,wBAAsB,WAAW,WAAW,CAAC;;;qBA+G1C;sBAGC;wBAME;sBAGF;gBAGN;;;gBA7HO,CACd,GAAG;;;;;;;;;;;;;;MAeJ;;;;;;;;;;;;;;;;;;;;CAoBD,OAAO,mBAAmB,IAAY,SAAuB;AAC3D,MAAI,sBAAsB,IAAI,GAAG,CAE/B;EAGF,MAAM,QAAQ,IAAI,eAAe;AACjC,QAAM,YAAY,QAAQ;AAC1B,wBAAsB,IAAI,IAAI,MAAM;AAGpC,WAAS,iBAAiB,kBAAkB,CAAC,SAAS,YAAY;AAChE,OAAI,QAAQ,YAAY;IACtB,MAAM,gBAAgB,QAAQ,WAAW;AACzC,QAAI,CAAC,cAAc,SAAS,MAAM,CAChC,SAAQ,WAAW,qBAAqB,CAAC,GAAG,eAAe,MAAM;;IAGrE;;;;;;;CAQJ,OAAO,qBAAqB,IAAkB;EAC5C,MAAM,QAAQ,sBAAsB,IAAI,GAAG;AAC3C,MAAI,CAAC,MACH;AAGF,wBAAsB,OAAO,GAAG;AAGhC,WAAS,iBAAiB,kBAAkB,CAAC,SAAS,YAAY;AAChE,OAAI,QAAQ,WACV,SAAQ,WAAW,qBACjB,QAAQ,WAAW,mBAAmB,QAAQ,MAAM,MAAM,MAAM;IAEpE;;CAGJ,SAAS;AAGP,OAAK,iBAAiB;AACtB,SAAO,IAAI,GAAG,KAAK;;CAGrB,AAAQ,kBAAwB;EAG9B,MAAM,YADQ,KAAK,eAAe,OAAQ,MACjB;AACzB,OAAK,MAAM,YAAY,aAAa,UAAU,UAAU,CAAC;EAIzD,MAAM,WAAW,KAAK,mBAAmB;AACzC,OAAK,MAAM,YAAY,uBAAuB,GAAG,SAAS,IAAI;AAG9D,OAAK,MAAM,YAAY,cAAc,KAAK,aAAa,UAAU,CAAC;;CAGpE,AAAU,eAAqB;AAC7B,OAAK,iBAAiB;;CAGxB,AAAU,UAAgB;AACxB,OAAK,iBAAiB;;CAqBxB,IAAI,cAAc;AAGhB,UAFmB,KAAK,QAAQ,UAAU,EACN,eAAe,MACzB,KAAK,kBAAkB;;CAGnD,IAAI,YAAY;EAId,MAAM,aAAa,KAAK,QAAQ,UAAU;AAC1C,MAAI,WACF,QAAO,WAAW,cAAc,WAAW;AAE7C,SAAO,KAAK,gBAAgB;;CAG9B,IAAI,aAAqB;EACvB,MAAM,aAAa,KAAK,QAAQ,UAAU;AAC1C,MAAI,WACF,QAAO,WAAW;AAEpB,SAAO,KAAK,eAAe,KAAK;;;YAxCjC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAS,SAAS;CAAM,CAAC;4BA9H5C,cAAc,kBAAkB"}
@@ -759,7 +759,9 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
759
759
  didBecomeRoot() {
760
760
  super.didBecomeRoot();
761
761
  this.#setupPlaybackListener();
762
- if (this.playbackController) fetch("https://editframe.com/api/v1/telemetry", {
762
+ const hostname = typeof window !== "undefined" ? window.location.hostname : "";
763
+ const isEditframeDomain = hostname === "editframe.com" || hostname.endsWith(".editframe.com");
764
+ if (this.playbackController && typeof process !== "undefined" && process.env.EF_TELEMETRY_ENABLED === "true" && !isEditframeDomain) fetch("https://editframe.com/api/v1/telemetry", {
763
765
  method: "POST",
764
766
  headers: { "Content-Type": "application/json" },
765
767
  body: JSON.stringify({ event_type: "load" }),