PageSourceSearch

https://www.databreachtoday.com/javascripts-responsive/media-transcript-navigation.js?s=1790241542.6257

js databreachtoday.com collected 2026-09-24 09:15:01 UTC 26,436 bytes, 759 lines download raw bytes

1/**** mediaMetadataNavigation main mechanism ****/
2/**
3 * AM
4 * Module that handles all the transcript navigation magic :)
5 *
6 * @param options
7 */
8var mediaMetadataNavigation = function (options) {
9
10
11
12    var observer = new IntersectionObserver(function(entries) {
13        // isIntersecting is true when element and viewport are overlapping
14        // isIntersecting is false when element and viewport don't overlap
15
16
17        var  section_id;
18        section_id = entries[0].target.getAttribute('cue-data-section-id');
19        console.log(section_id);
20
21        // entries[0].getAttribute('cue-data-section-id');
22
23        if(entries[0].isIntersecting === true) {
24            console.log('Element has just become visible in screen:'+section_id);
25            $scope.observers[section_id] = true;
26        }
27        else
28        {
29            console.log('Element is not visible'+section_id);
30            $scope.observers[section_id] = false;
31        }
32    }, { threshold: [0.8] });
33
34
35
36    var $scope = this;
37
38
39    $scope.observers = [];
40
41
42    /**
43     * Saving the options in a variable in order to be able to use them in child functions
44     */
45    $scope.options = options;
46
47    /**
48     * Player instance
49     */
50    $scope.player = options.player;
51
52    /**
53     * Provided container id
54     */
55    $scope.data_container_id = options.data_container_id;
56
57    /**
58     * Provided container which will contain the video transcript text
59     */
60    $scope.data_container = document.querySelector($scope.data_container_id);
61
62    /**
63     * Giving the possibility of defining a custom height of the transcript text container
64     */
65    $scope.data_container_height = options.data_container_height ? options.data_container_height : 400;
66
67    /**
68     * Giving the possibility of setting the transcripts video tracks language
69     */
70    $scope.language = options.language;
71
72    /**
73     * Get current player tracks
74     */
75    $scope.track = getTracks();
76
77    if(typeof $scope.track == 'undefined') { return; }
78
79    /**
80     * Contains current video track cues (transcript...)
81     */
82    $scope.cues = $scope.track.cues;
83
84    /**
85     * Holds the cue object that is currently played
86     */
87    $scope.activeCue = ($scope.track.activeCues.length > 0) ? $scope.track.activeCues[0] : $scope.cues[0];
88
89    /**
90     * Cues are separated in sections that have approximately the same height as the data container,
91     * so the current cue section will be the section that holds the current cue ($scope.activeCue)
92     */
93    $scope.current_cue_section = null;
94
95    /**
96     * $scope.autoscroll variable is used to monitor if the auto scroll mode (that is turned on by default) is on or off;
97     * if it's on, the scrolling is automatically made when current transcript text is not visible and
98     * when it's turned off, the user can manually scroll throughout the transcript without being brought
99     * back to current transcript section (the user can resume auto-scroll by using the Resume auto-scroll button)
100     */
101    $scope.autoscroll = 'on';
102
103    /**
104     * creating the Resume auto-scroll button
105     */
106    $scope.resume_autoscroll_element = createQuickElementWithClass('div', 'stop-autoscrolling-button', $scope.data_container);
107
108    /**
109     * adding text to auto scroll resume btn
110     */
111    $scope.resume_autoscroll_element.textContent = 'Resume auto-scroll';
112
113    /**
114     *
115     */
116    $scope.hide_transcript_container = true
117
118    /**** Video Navigation Functions ****/
119
120    /**
121     * Getting player tracks by provided language on instantiation, in order to extract the transcript
122     */
123    function getTracks() {
124        var textTracks = $scope.player.textTracks;
125
126        if(textTracks.length != 0) {
127            for (track in textTracks)
128                if (textTracks[track].language == $scope.language)
129                    return textTracks[track];
130        }
131
132        return;
133    }
134
135    /**
136     * Preparing stylesheets for plugin elements
137     */
138    function prepareStylesheets() {
139        // set the height of the data container
140        $scope.data_container.style.height = $scope.data_container_height + 'px';
141        $scope.data_container.style.overflowY = 'scroll';
142
143        // setting style for button that is meant to stop the autoscroll
144        $scope.resume_autoscroll_element.style.cssText = '' +
145            'background: rgba(0,0,0,0.5); ' +
146            'border-radius: 3px; ' +
147            'color: #ffffff; ' +
148            'cursor: pointer; ' +
149            'display: none; ' +
150            'padding: 10px 0; ' +
151            'position: absolute; ' +
152            'bottom: 0; ' +
153            'text-align: center; ' +
154            'width: ' + getComputedStyle($scope.data_container).width;
155    }
156
157    /**
158     * This function checks if we need to scroll to the next section or not, depending on
159     * the activation of auto-scrolling and if the current cue element's parrent (cue parent section),
160     * is not visible
161     */
162    function scrollIfNecessary(element)
163    {
164        if($scope.autoscroll == 'on')
165            if (!elementInViewport(element)) // if the element is not in current viewport
166            {
167                $scope.autoscrolling = true; // auto-scrolling is turned on
168                $scope.data_container.scrollTop = element.offsetTop-16; // scroll to section - padding
169            }
170    }
171
172    /**
173     * Update current section element with given value
174     */
175    function updateCurrentSection(element)
176    {
177        $scope.current_cue_section = element;
178    }
179
180    /**
181     * This function handles the highlighting by changing the necessary classes from the prev transcript text
182     * element and current transcript text element and it sets the new transcript in the scope for further usage
183     */
184    function handleActiveCuesClasses()
185    {
186        if(typeof $scope.activeCue != "undefined")
187        {
188            // get last active cue and remove active class
189            var old_cue_element = document.querySelector('[cue-id="' + btoa($scope.activeCue.startTime) + '"]');
190            old_cue_element.classList.remove('is-active');
191
192            // update the scope variable with current active cue, get the element and add the active class
193            $scope.activeCue = ($scope.track.activeCues.length > 0) ? $scope.track.activeCues[0] : $scope.activeCue;
194
195            // getting current cue element querying by attribute (cue-id) and add the active class
196            var current_cue_element = document.querySelector('[cue-id="' + btoa($scope.activeCue.startTime) + '"]');
197            current_cue_element.classList.add('is-active');
198
199            // update the current section in scope
200            updateCurrentSection(current_cue_element.parentElement);
201
202
203            // use this function so we can scroll to the next section if current transcript text is not visible
204            scrollIfNecessary(current_cue_element.parentElement);
205        }
206    }
207
208    /**
209     * Transcript container populating function
210     * Workflow:
211     *  - the first section is created
212     *  - update the current section element
213     *  - starting to parse the cues
214     *      - we only take key of 'number' type
215     *      - create an id for current loop cue and encode it properly
216     *      - getting the current cue id
217     *      - determining the active que by comparing the two from above
218     *      - create current loop cue span element that will hold the current cue transcript text
219     *      - getting the style of the current section in order to verify if the height gets equal or
220     *        lower that the main data container; if the height got to the created limit (main_container - 20) a new
221     *        section is created to hold the next transcript texts and s
221o on...
222     *      - creating the cue span with it's necessary props and then appending it to current section
223     *          - besides class, cue-id attr and text content we set an onclick event
224     *            that will change the player current time to the cue's start time
225     */
226    function populateCues() {
227        // creating the first section
228        var cue_data_section_div = createQuickElementWithClass('div', 'cue-data-section', $scope.data_container);
229        var i = 0;
230        var cue_index = 0;
231        var section_id = 0;
232        var speaker_change_index=0;
233
234
235
236        cue_data_section_div.setAttribute('cue-data-section-id', section_id);
237        section_id++;
238
239
240        //attach an intersection observer for each section
241        observer.observe(cue_data_section_div);
242        $scope.observers.push(false);
243
244
245        updateCurrentSection(cue_data_section_div);
246
247
248
249        // starting cue parse
250        for(cue in $scope.cues)
251        {
252            if (!isNaN(cue)) // we only manipulate element if the key is a number (besides cues apparently we have some functions in the cues array)
253            {
254
255
256                var cue_id = btoa($scope.cues[cue].startTime), // create a cue-id attr value for current loop cue, using btoa encoding
257                    current_cue_id = btoa($scope.activeCue.startTime), // get current active cue, cue-id
258                    is_active = (cue_id == current_cue_id) ? 'is-active' : '', // assign active class if current cue equals current loop cue
259                    cue_data_span = document.createElement('span'); // create a span element for current cue text
260
261
262
263                // getting current computed style in order to check the height
264                var computedStyle = getComputedStyle(cue_data_section_div);
265
266
267
268                // if the height of the current section has gotten to be bigger or equal to the data container height
269                // create new section
270                if(parseInt(computedStyle.height) >= $scope.data_container_height-20) {
271                    cue_data_section_div = createQuickElementWithClass('div', 'cue-data-section', $scope.data_container);
272
273
274                    cue_data_section_div.setAttribute('cue-data-section-id', section_id);
275                    section_id++;
276
277
278                    //attach an intersection observer for each section
279                    observer.observe(cue_data_section_div);
280                    $scope.observers.push(false);
281                }
282
283
284
285                //check speakers & compute other needed data - only if show_speakers is checked
286                if($scope.options.show_speakers)
287                {
288
289                    var current_speaker;
290                    var cues_current_speaker;
291                    var current_speaker_change = true;
292                    var current_speaker_timestamp;
293                    var speaker_ver;
294
295
296                    //detect speakers version - in first cue
297                    if(cue_index == 0){
298
299
300                        //check for first version of speakers system
301                        var parser = new DOMParser();
302                        var doc = parser.parseFromString($scope.cues[cue].text, 'text/html');
303                        if(doc.querySelector("v")) {
304                            current_speaker = doc.querySelector("v").getAttribute("speaker");
305                        }
306                        speaker_ver = 0;//rev
307
308
309                        //check for second version of speakers system
310                        if(typeof current_speaker == 'undefined'){
311                            current_speaker = $scope.cues[cue].text.split(':')[0];
312                            speaker_ver = 1;//otter
313
314                        }
315
316
317                        //check if the speaker is predefined
318                        if(typeof current_speaker != 'undefined'){
319
320                            if(!caption_speakers.includes(current_speaker)){
321                                $scope.options.show_speakers = false;
322                            }
323
324                        }
325
326                    }
327
328
329
330                    //detect each cue's speaker
331                    if($scope.options.show_speakers && cue_index > 0){
332
333                        if(speaker_ver){
334                            cues_current_speaker = $scope.cues[cue].text.split(':')[0];
335                        }
336                        else{
337                            var parser = new DOMParser();
338                            var doc = parser.parseFromString($scope.cues[cue].text, 'text/html');
339                            if(doc.querySelector("v")) {
340                                cues_current_speaker = doc.querySelector("v").getAttribute("speaker");
341                            }
342
343                        }
344
345
346                        //check if the speaker is defined
347                        if(typeof cues_current_speaker != 'undefined'){
348
349
350                            //check if the speaker is predefined
351                            if(!caption_speakers.includes(cues_current_speaker)){
352
353                                //not predefined - treat it as it's the previous speaker
354                                current_speaker = cues_current_speaker;
355                                current_speaker_change = false;
356
357                            }
358                            else{ //is predefined
359
360                                //check if speaker remain the same as in the previous cue
361                                if(cues_current_speaker == current_speaker){
362                                    current_speaker_change = false;
363                                }
364                                else{
365                                    current_speaker_change = true;
366                                }
367
368                                current_speaker = cues_current_speaker;
369                            }
370
371                        }
372                        else{ //speaker is not defined in the cue so it remains the same as the previous
373                            current_speaker = cues_current_speaker;
374                            current_speaker_change = false;
375                        }
376                    }
377
378
379
380                }
381
382
383
384                //create speakers section/header
385                if($scope.options.show_speakers){
386
387
388                    cue_data_section_div.setAttribute('style', 'padding: 0px 30px 0px 50px;');
389
390
391                    var current_speaker_color;
392
393
394                    //on speaker change - create new speaker header
395                    if(current_speaker_change)
396                    {
397
398                        current_speaker_timestamp = new Date($scope.cues[cue].startTime * 1000).toISOString().substr(11, 8);
399
400                        if(speaker_change_index % 2 == 0) { current_speaker_color = 'green'; } else { current_speaker_color = 'red'; }
401
402
403                        // append speakers span to current section
404                        var cue_speakers_div = document.createElement('div');
405                        cue_speakers_div.setAttribute('class', "cue-data-speaker");
406                        cue_speakers_div.setAttribute('style', "position: relative;left: -30px;padding-top:10px;");
407                        cue_speakers_div.innerHTML =  cue_speakers_div.innerHTML + '<span style="border-radius: 50%;padding: 5px;background: '+current_speaker_color+';/* border: 2px solid #666; */color: #fff;text-align: center;font: 14px Arial, sans-serif;font-weight: bold;width: 27px;height: 27px;display: inline-block;">'+current_speaker.split(/\s/).reduce((response,word)=> response+=word.slice(0,1),'')+'</span><span style="padding-left: 10px;font-weight: bold;">'+current_speaker+'</span><span style="padding-left: 10px;">'+current_speaker_timestamp+'</span>';
408                        cue_data_section_div.appendChild(cue_speakers_div);
409
410
411                            speaker_change_index++;
412                    }
413
414
415
416                    if(speaker_ver == 1){ //if speakers ver=1 remove first occurrence of the speaker from the cue text
417
418                        $scope.cues[cue].text = $scope.cues[cue].text.replace(current_speaker+':', '');
419
420                    }
421
422
423
424
425                }
426
427
428
429                //if no speakers and no show_speakers option, then keep the old styles/layout
430                cue_data_span.className = 'cue-data ' + is_active; // set class for current cue span
431
432
433                cue_data_span.setAttribute('cue-id', cue_id); // set cue-id attr
434                cue_data_span.setAttribute('cue-id2', $scope.cues[cue].id); // set cue-id attr
435
436
437
438                cue_data_span.innerHTML = $scope.cues[cue].text; // set current cue text
439
440
441                // add on click event for current cue span so it can be used for video navigation
442                cue_data_span.onclick = function () {
443                    $scope.player.currentTime = atob(this.getAttribute('cue-id')); // navigate to clicked cue
444                };
445
446
447                cue_data_section_div.appendChild(cue_data_span); // finally append span to current section
448
449
450
451                //add the speaker to the cue - old version
452                /*current_speaker = $("[cue-id2='"+$scope.cues[cue].id+"']").children("v").attr('speaker');
453                if( && $scope.options.show_speakers)
454                {
455                    cue_data_span.innerHTML = current_speaker + ': ' + cue_data_span.innerHTML;
456                }
457                */
458
459
460
461                //search highlight -> if we find a highlight then activate/go to the cue
462                if($scope.cues[cue].text.search('<b>') != -1 && i==0){
463                    cue_data_span.click();
464                    i++;
465                }
466
467
468                cue_index++;
469            }
470        }
471    }
472
473    /**
474     * Storing the scroll handler logic inside this function
475     * This function manages to make the difference between an automatic scroll and a user manual scroll
476     * If an automatic scroll is made, the rest of the script is skipped
477     * Else, auto-scrolling is stopped, te rest of the script is also stopped and if another scroll event will
478     * occur, then that will be a manual scroll made by the user
479     */
480    function scrollHandler() {
481        // $scope.autoscroll = 'off';
482        if($scope.autoscrolling)
483            if($scope.current_cue_section.offsetTop >= $scope.data_container.scrollTop)
484            // if($scope.data_container.scrollTop % $scope.current_cue_section.offsetHeight != 0)
485                return;
486            else
487            {
488                $scope.autoscrolling = false;
489                return;
490            }
491
492
493        if(!$scope.autoscrolling)
494        {
495            $scope.autoscroll = 'off';
496            $scope.resume_autoscroll_element.style.display = 'block';
497        }
498    }
499
500    /**** end Video Navigation Functions ****/
501
502    /**** Utility functions ****/
503
504    /**
505     * Utility class made for quick creation of html element and asigning it's parent if the
506     * 'parent' paramter is provided
507     */
508    function createQuickElementWithClass(type, css_class, parent) {
509        // creating the first section
510        var element = document.createElement(type);
511
512        // set the element class
513        element.className = css_class;
514        if(parent) // append element if parent is provided
515            parent.appendChild(element);
516
517        // return the element for further usage
518        return element;
519    }
520
521    /**
522     * Utility function used for verifying if given element parameter is in viewport
523     */
524    function elementInViewport(el) {
525
526
527        return $scope.observers[el.getAttribute('cue-data-section-id')];
528
529
530        /*
531        var top = el.offsetTop;
532        var left = el.offsetLeft;
533        var width = el.offsetWidth;
534        var height = el.offsetHeight;
535
536        while(el.offsetParent) {
537            el = el.offsetParent;
538            top += el.offsetTop;
539            left += el.offsetLeft;
540        }
541
542        return (
543            top >= window.pageYOffset &&
544            left >= window.pageXOffset &&
545            (top + height) <= (window.pageYOffset + window.innerHeight) &&
546            (left + width) <= (window.pageXOffset + window.innerWidth)
547        );
548        */
549    }
550
551    function resetTranscriptContainerStyle() {
552        if($scope.hide_transcript_container)
553        {
554            $scope.data_container.parentElement.style.position = 'relative'
555            $scope.data_container.parentElement.style.visibility = 'visible'
556            $scope.data_container.parentElement.style.display = 'none'
557        }
558    }
559    /**** end Utility functions ****/
560
561
562
563    /**** events ****/
564
565    /**
566     * Event automatically fired when the cue changes
567     */
568    $scope.track.oncuechange = function () {
569        handleActiveCuesClasses(); // handles the highlighting by changing the necessary classes
570    };
571
572    /**
573     * Event automatically fires on scroll
574     */
575    $scope.data_container.onscroll = function () {
576        scrollHandler(); // handle automatic and manual scrolling
577    };
578
579    /**
580     * Event fired on click in order to resume automatic scrolling
581     */
582    $scope.resume_autoscroll_element.onclick = function () {
583        var _self = this; // storing the current object in order to be able to use it in the set timeout function
584        $scope.data_container.scrollTop = $scope.current_cue_section.offsetTop-16; // scroll to current section-padding
585        $scope.hideElement(_self)
586        // setting a timeout section to resume the automatic scrolling, to be sure the scrolling event is over
587        setTimeout(function () {
588            $scope.autoscroll = 'on';
589        }, 1000);
590    };
591
592    /**** end events ****/
593
594    /**** on init running functions ****/
595
596    (function init() {
597        // prepare data container height
598        prepareStylesheets();
599
600        // populate cues in provided html data_container by selector
601        populateCues();
602
603        // reset container style
604        resetTranscriptContainerStyle();
605
606    })();
607
608    /**** end on init running functions ****/
609};
610/**** end mediaMetadataNavigation main mechanism ****/
611
612/**** Utility Extenders ****/
613/**
614 * AM
615 * Returns true if we can use jQuery
616 * Need this method in order to be able to use jquery effects if available, otherwise use plain javascript
617 *
618 * @returns {boolean}
619 */
620mediaMetadataNavigation.prototype.isJqueryAvailable = function() {
621    /**
622     * If using jQuery we can pass its identifier (usually it is $)
623     */
624    this.jquery_identifier = this.options.jquery_identifier || 'jQuery';
625
626    return Boolean(window[this.jquery_identifier])
627}
628
629/**
630 * AM
631 * Utility function extension that checks if a given element is visible
632 *
633 * @returns {boolean|boolean|jQuery}
634 */
635mediaMetadataNavigation.prototype.isElementVisible = function(element) {
636    if(this.isJqueryAvailable())
637        return jQuery(element).parent().is(':visible')
638    else
639        return element.style.display != 'none' && element.style.visibility != 'hidden'
640}
641
642/**
643 * Hides Element using jquery if it is available, otherwise hiding using javascript
644 *
645 * @param element
646 * @param delay
647 */
648mediaMetadataNavigation.prototype.hideElement = function(element, delay) {
649    delay = delay || 600
650    if(this.isJqueryAvailable())
651        jQuery(element).fadeOut(delay)
652    else
653        element.style.display = 'none';
654}
655/**** end Utility Extenders ****/
656
657/**** Functionality extenders ****/
658/**
659 * AM
660 * Extending the metadata navigation with toggling mechanism for the captions container
661 */
662mediaMetadataNavigation.prototype.withToggleButton = function () {
663    var $scope = this
664
665    /**
666     * Selector of the toggle html element
667     *
668     * @type {string|mediaMetadataNavigation.withToggleButton.options.toggle_button_selector}
669     */
670    $scope.toggle_button_selector = this.options.toggle_button_selector
671
672    /**
673     * The toggle HTML element
674     */
675    $scope.toggle_button_selector_element = document.querySelector($scope.toggle_button_selector)
676
677    if(!$scope.toggle_button_selector_element) // if toggler element is not found, exit script
678        return
679
680    /**
681     * After loading cues, we set the html associated to current container state (visible or not visible)
682     */
683    setToggleButtonHTML('open')
684
685    /**
686     * Plain Javascript functionality of toggling the parent of the cues container
687     */
688    function javascriptCaptionToggle() {
689        if($scope.data_container.parentElement.style.display == 'none')
690            $scope.data_container.parentElement.style.display = 'block'
691        else
692            $scope.data_container.parentElement.style.display = 'none'
693    }
694
695    /**
696     * jQuery functionality of toggling the parent of the cues container
697     */
698    function jQueryCaptionToggle() {
699        if($scope.isElementVisible($scope.data_container))
700            jQuery($scope.data_container_id).parent().slideUp(1000)
701        else
702            jQuery($scope.data_container_id).parent().slideDown(400)
703    }
704
705    /**
706     * This function first checks if html was passed to the toggling button and sets it depending on captions container state
707     * and then it runs the toggling mechanism
708     */
709    function toggleCaptionsContainer() {
710        handleToggleButtonHtml()
711        if($scope.isJqueryAvailable())
712            jQueryCaptionToggle()
713        else
714            javascriptCaptionToggle()
715    }
716
717    /**
718     * This function sets button html content(if set), depending on captions container state
719     */
720    function handleToggleButtonHtml() {
721        if($scope.isElementVisible($scope.data_container))
722            setToggleButtonHTML('open')
723        else
724            setToggleButtonHTML('close')
725    }
726
727    /**
728     * This function sets toggling button html by a given type
729     * allowed types:
730     *  - open
731     *  - close
732     * allowed options to be passed to the mediaMetadataNavigation
733     *  - toggle_button_open_html
734     *  - toggle_button_close_html
735     *
736     * @param type
737     */
738    function setToggleButtonHTML(type) {
739        $scope.toggle_button_selector_element.innerHTML = $scope.options['toggle_button_' + type + '_html'] || '<span>Show Transcript</span>'
740    }
741
742    /**
743     * Click event handler for the caption container, where the toggling mechanism is initialized
744     */
745    $scope.toggle_button_selector_element.onclick = function() {
746
747
748        if(typeof $scope.resume_autoscroll_element == 'undefined') {return;}
749
750        $scope.resume_autoscroll_element.click()
751
752        toggleCaptionsContainer()
753
754        if(getComputedStyle($scope.data_container).width != getComputedStyle($scope.resume_autoscroll_element).width)
755            $scope.resume_autoscroll_element.style.width = getComputedStyle($scope.data_container).width
756    }
757}
758/**** end Functionality extenders ****/
759

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.