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.