PageSourceSearch

https://www.lek.com/themes/custom/lekdaisy/components/vimeo_video/vimeo_video.js?tm6zme

js lek.com collected 2026-10-01 09:02:58 UTC 10,546 bytes, 355 lines download raw bytes

1/* global Vimeo, Blob, URL, alert */
2(function (Drupal) {
3  'use strict';
4
5  const instances = new Map();
6
7  function formatTime(seconds) {
8    if (seconds == null || isNaN(seconds)) {
9      return '0:00';
10    }
11
12    const mins = Math.floor(seconds / 60);
13    const secs = Math.floor(seconds % 60);
14
15    return `${mins}:${secs.toString().padStart(2, '0')}`;
16  }
17
18  function renderTranscript(instance) {
19    const { transcriptEl, cues } = instance;
20
21    if (cues.length === 0) {
22      transcriptEl.innerHTML = '<p class="transcript-placeholder">No transcript available.</p>';
23      return;
24    }
25
26    transcriptEl.innerHTML = cues.map((cue, index) => `
27      <div class="transcript-cue" data-index="${index}" data-start="${cue.start}">
28        <span class="time">${formatTime(cue.start)}</span>
29        <span class="text">${cue.text}</span>
30      </div>
31    `).join('');
32
33    transcriptEl.querySelectorAll('.transcript-cue').forEach(el => {
34      el.addEventListener('click', () => {
35        const time = parseFloat(el.dataset.start);
36        instance.player.setCurrentTime(time);
37        instance.player.play();
38      });
39    });
40  }
41
42  function highlightCurrentCue(instance, currentTime) {
43    const { transcriptEl, autoScrollEl, cues } = instance;
44    const cueElements = transcriptEl.querySelectorAll('.transcript-cue');
45    let activeElement = null;
46    let activeIndex = -1;
47
48    for (let i = 0; i < cues.length; i++) {
49      const cue = cues[i];
50      const nextCue = cues[i + 1];
51      const isActive = currentTime >= cue.start && (!nextCue || currentTime < nextCue.start);
52
53      if (isActive) {
54        activeIndex = i;
55        break;
56      }
57    }
58
59    cueElements.forEach((el, i) => {
60      if (i === activeIndex) {
61        el.classList.add('active');
62        activeElement = el;
63      }
64      else {
65        el.classList.remove('active');
66      }
67    });
68
69    if (activeElement && autoScrollEl && autoScrollEl.checked) {
70      const containerHeight = transcriptEl.clientHeight;
71      const elementTop = activeElement.offsetTop;
72      const elementHeight = activeElement.clientHeight;
73      const scrollPosition = elementTop - (containerHeight / 2) + (elementHeight / 2);
74      transcriptEl.scrollTo({ top: scrollPosition, behavior: 'smooth' });
75    }
76  }
77
78  /**
79   * Get the base URL for API calls.
80   *
81   * @return {string} Base URL (empty for Drupal, full URL for Storybook).
82   */
83  function getApiBaseUrl() {
84    if (typeof drupalSettings !== 'undefined' && drupalSettings.path?.baseUrl) {
85      return drupalSettings.path.baseUrl.replace(/\/$/, '');
86    }
87    if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
88      return 'https://lek-redux.ddev.site';
89    }
90    return '';
91  }
92
93  /**
94   * Fetch embed HTML from backend API.
95   *
96   * @param {string} videoId - The Vimeo video ID.
97   * @return {Promise<object|null>} Embed data or null on error.
98   */
99  async function fetchEmbed(videoId) {
100    try {
101      const baseUrl = getApiBaseUrl();
102      const response = await fetch(`${baseUrl}/api/vimeo/${videoId}/embed`, { credentials: 'omit' });
103      const data = await response.json();
104
105      if (data.error) {
106        console.error('Embed fetch error:', data.error);
107        return null;
108      }
109
110      return data;
111    }
112    catch (error) {
113      console.error('Failed to fetch embed:', error);
114      return null;
115    }
116  }
117
118  /**
119   * Fetch transcript from backend API.
120   *
121   * @param {object} instance - The video instance.
122   * @param {string} [language] - Optional language code (e.g., 'en', 'fr').
123   */
124  async function fetchTranscript(instance, language) {
125    const { videoId, transcriptEl } = instance;
126
127    // Avoid redundant fetches once a transcript is loaded, unless a
128    // different caption language was explicitly selected.
129    if (instance.transcriptLoaded && (!language || language === instance.language)) {
130      return;
131    }
132
133    transcriptEl.innerHTML = '<p class="transcript-placeholder">Loading transcript...</p>';
134
135    try {
136      const baseUrl = getApiBaseUrl();
137      let url = `${baseUrl}/api/vimeo/${videoId}/transcript`;
138      if (language) {
139        url += `?lang=${encodeURIComponent(language)}`;
140      }
141      const response = await fetch(url, { credentials: 'omit' });
142      const data = await response.json();
143
144      if (data.error) {
145        transcriptEl.innerHTML = '<p class="transcript-placeholder">Could not load transcript.</p>';
146        return;
147      }
148
149      instance.transcriptLoaded = true;
150
151      if (!data.cues || data.cues.length === 0) {
152        transcriptEl.innerHTML = '<p class="transcript-placeholder">No transcript available for this video.</p>';
153        return;
154      }
155
156      instance.cues = data.cues;
157      instance.language = data.language;
158      renderTranscript(instance);
159    }
160    catch (error) {
161      console.error('Failed to fetch transcript:', error);
162      transcriptEl.innerHTML = '<p class="transcript-placeholder">Transcript not available.</p>';
163    }
164  }
165
166  async function initPlayer(instance) {
167    const { container, videoId } = instance;
168
169    const playerEl = container.querySelector('.player');
170    if (!playerEl) {
171      return;
172    }
173
174    instance.cues = [];
175
176    const embedData = await fetchEmbed(videoId);
177    if (!embedData?.embed) {
178      return;
179    }
180
181    playerEl.innerHTML = embedData.embed;
182
183    if (instance.transcriptContainer) {
184      instance.transcriptContainer.hidden = !embedData.hasTranscript;
185    }
186
187    const iframe = playerEl.querySelector('iframe');
188    if (!iframe) {
189      return;
190    }
191
192    instance.player = new Vimeo.Player(iframe);
193
194    instance.player.on('timeupdate', data => {
195      highlightCurrentCue(instance, data.seconds);
196    });
197
198    instance.player.on('error', error => {
199      console.error('Vimeo player error:', error.message);
200    });
201
202    instance.player.on('texttrackchange', data => {
203      if (data.kind === 'captions' || data.kind === 'subtitles') {
204        if (instance.transcriptContainer) {
205          instance.transcriptContainer.hidden = false;
206        }
207        fetchTranscript(instance, data.language);
208      }
209    });
210  }
211
212  function downloadTranscript(instance) {
213    if (!instance || instance.cues.length === 0) {
214      alert('No transcript available to download.');
215      return;
216    }
217
218    const content = instance.cues
219      .map(cue => `[${formatTime(cue.start)}] ${cue.text}`)
220      .join('\n');
221
222    const blob = new Blob([content], { type: 'text/plain' });
223    const url = URL.createObjectURL(blob);
224    const a = document.createElement('a');
225    a.href = url;
226    a.download = `transcript-${instance.videoId}.txt`;
227    a.click();
228    URL.revokeObjectURL(url);
229  }
230
231  Drupal.behaviors.vimeoVideo = {
232    attach: function (context) {
233      const containers = context.querySelectorAll('.player-container[data-video-id]');
234
235      containers.forEach(container => {
236        if (container.dataset.vimeoInitialized) {
237          return;
238        }
239
240        container.dataset.vimeoInitialized = 'true';
241
242        const videoId = container.dataset.videoId;
243        const wrapper = container.closest('.vimeo-video');
244        const transcriptEl = wrapper?.querySelector('.transcript-content');
245        const autoScrollEl = wrapper?.querySelector('.autoscroll input[type="checkbox"]');
246
247        const transcriptContainer = wrapper?.querySelector('.transcript-container');
248
249        const instance = {
250          videoId,
251          container,
252          wrapper,
253          transcriptEl,
254          autoScrollEl,
255          transcriptContainer,
256          player: null,
257          cues: [],
258          language: null,
259          transcriptLoaded: false,
260        };
261
262        instances.set(videoId, instance);
263        initPlayer(instance);
264      });
265    },
266  };
267
268  Drupal.behaviors.vimeoTranscriptAccordion = {
269    attach: function (context) {
270      const toggleButtons = context.querySelectorAll('.transcript-toggle');
271
272      toggleButtons.forEach(button => {
273        if (button.dataset.accordionInitialized) {
274          return;
275        }
276        button.dataset.accordionInitialized = 'true';
277
278        const container = button.closest('.transcript-container');
279        const content = container?.querySelector('.accordion-content');
280        const chevron = button.querySelector('.transcript-chevron');
281
282        if (!content) {
283          return;
284        }
285
286        button.addEventListener('click', async () => {
287          const isExpanded = button.getAttribute('aria-expanded') === 'true';
288
289          button.setAttribute('aria-expanded', !isExpanded);
290
291          // Toggle inert attribute to prevent focus on hidden content
292          if (isExpanded) {
293            content.setAttribute('inert', '');
294          }
295          else {
296            content.removeAttribute('inert');
297          }
298
299          if (chevron) {
300            chevron.classList.toggle('rotate-180', isExpanded);
301          }
302
303          if (!isExpanded) {
304            const wrapper = button.closest('.vimeo-video');
305            const playerContainer = wrapper?.querySelector('.player-container[data-video-id]');
306            const instance = instances.get(playerContainer?.dataset.videoId);
307
308            content.style.maxHeight = `${content.scrollHeight}px`;
309
310            if (instance && !instance.transcriptLoaded) {
311              await fetchTranscript(instance);
312              // Recalculate now that the transcript content has replaced
313              // the loading placeholder, so the accordion isn't clipped.
314              if (button.getAttribute('aria-expanded') === 'true') {
315                content.style.maxHeight = `${content.scrollHeight}px`;
316              }
317            }
318          }
319          else {
320            content.style.maxHeight = '0';
321          }
322        });
323      });
324    },
325  };
326
327  Drupal.behaviors.vimeoTranscriptDownload = {
328    attach: function (context) {
329      const downloadButtons = context.querySelectorAll('.transcript-download');
330
331      downloadButtons.forEach(button => {
332        if (button.dataset.downloadInitialized) {
333          return;
334        }
335        button.dataset.downloadInitialized = 'true';
336
337        button.addEventListener('click', () => {
338          const wrapper = button.closest('.vimeo-video');
339          if (!wrapper) {
340            return;
341          }
342
343          const container = wrapper.querySelector('.player-container[data-vi
343deo-id]');
344          if (!container) {
345            return;
346          }
347
348          const instance = instances.get(container.dataset.videoId);
349          downloadTranscript(instance);
350        });
351      });
352    },
353  };
354
355})(Drupal);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.