wai-website-theme 1.2 → 1.3

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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/_includes/backtotop.html +1 -1
  3. data/_includes/excol.html +1 -1
  4. data/_includes/feedback-box.html +13 -10
  5. data/_includes/footer.html +20 -6
  6. data/_includes/header.html +73 -35
  7. data/_includes/inpl.html +6 -0
  8. data/_includes/lang.html +7 -0
  9. data/_includes/link.html +92 -0
  10. data/_includes/menuitem.html +5 -3
  11. data/_includes/multilang-list-policy-links.html +1 -1
  12. data/_includes/navlist.html +1 -1
  13. data/_includes/prevnext.html +5 -4
  14. data/_includes/secondarynav.html +34 -6
  15. data/_includes/t.html +33 -0
  16. data/_includes/translation-note-msg.html +45 -0
  17. data/_includes/video-player.html +50 -9
  18. data/_layouts/default.html +32 -9
  19. data/_layouts/home.html +29 -5
  20. data/_layouts/news.html +27 -6
  21. data/_layouts/policy.html +27 -6
  22. data/_layouts/sidenav.html +27 -6
  23. data/_layouts/sidenavsidebar.html +27 -6
  24. data/assets/ableplayer/.gitattributes +0 -0
  25. data/assets/ableplayer/.gitignore +3 -1
  26. data/assets/ableplayer/Gruntfile.js +3 -1
  27. data/assets/ableplayer/LICENSE +0 -0
  28. data/assets/ableplayer/README.md +214 -170
  29. data/assets/ableplayer/_config.yml +1 -0
  30. data/assets/ableplayer/build/ableplayer.dist.js +2637 -744
  31. data/assets/ableplayer/build/ableplayer.js +2637 -744
  32. data/assets/ableplayer/build/ableplayer.min.css +2 -2
  33. data/assets/ableplayer/build/ableplayer.min.js +9 -7
  34. data/assets/ableplayer/button-icons/able-icons.svg +0 -0
  35. data/assets/ableplayer/button-icons/black/rabbit.png +0 -0
  36. data/assets/ableplayer/button-icons/black/turtle.png +0 -0
  37. data/assets/ableplayer/button-icons/white/rabbit.png +0 -0
  38. data/assets/ableplayer/button-icons/white/turtle.png +0 -0
  39. data/assets/ableplayer/images/wingrip.png +0 -0
  40. data/assets/ableplayer/package-lock.json +705 -0
  41. data/assets/ableplayer/package.json +11 -2
  42. data/assets/ableplayer/scripts/JQuery.doWhen.js +0 -0
  43. data/assets/ableplayer/scripts/ableplayer-base.js +129 -29
  44. data/assets/ableplayer/scripts/browser.js +0 -0
  45. data/assets/ableplayer/scripts/buildplayer.js +342 -262
  46. data/assets/ableplayer/scripts/caption.js +19 -0
  47. data/assets/ableplayer/scripts/chapters.js +21 -0
  48. data/assets/ableplayer/scripts/control.js +139 -56
  49. data/assets/ableplayer/scripts/description.js +0 -0
  50. data/assets/ableplayer/scripts/dialog.js +13 -13
  51. data/assets/ableplayer/scripts/dragdrop.js +102 -109
  52. data/assets/ableplayer/scripts/event.js +186 -83
  53. data/assets/ableplayer/scripts/initialize.js +261 -71
  54. data/assets/ableplayer/scripts/langs.js +4 -0
  55. data/assets/ableplayer/scripts/metadata.js +0 -0
  56. data/assets/ableplayer/scripts/misc.js +76 -7
  57. data/assets/ableplayer/scripts/preference.js +2 -2
  58. data/assets/ableplayer/scripts/search.js +10 -7
  59. data/assets/ableplayer/scripts/sign.js +0 -0
  60. data/assets/ableplayer/scripts/slider.js +35 -34
  61. data/assets/ableplayer/scripts/track.js +38 -22
  62. data/assets/ableplayer/scripts/transcript.js +15 -6
  63. data/assets/ableplayer/scripts/translation.js +29 -20
  64. data/assets/ableplayer/scripts/ttml2webvtt.js +87 -0
  65. data/assets/ableplayer/scripts/volume.js +16 -15
  66. data/assets/ableplayer/scripts/vts.js +1093 -0
  67. data/assets/ableplayer/scripts/webvtt.js +0 -0
  68. data/assets/ableplayer/scripts/youtube.js +16 -5
  69. data/assets/ableplayer/styles/ableplayer.css +125 -22
  70. data/assets/ableplayer/thirdparty/js.cookie.js +0 -0
  71. data/assets/ableplayer/thirdparty/modernizr.custom.js +0 -0
  72. data/assets/ableplayer/translations/ca.js +311 -1
  73. data/assets/ableplayer/translations/de.js +1 -1
  74. data/assets/ableplayer/translations/en.js +6 -0
  75. data/assets/ableplayer/translations/es.js +6 -0
  76. data/assets/ableplayer/translations/fr.js +6 -0
  77. data/assets/ableplayer/translations/he.js +311 -0
  78. data/assets/ableplayer/translations/it.js +7 -1
  79. data/assets/ableplayer/translations/ja.js +6 -0
  80. data/assets/ableplayer/translations/nb.js +311 -0
  81. data/assets/ableplayer/translations/nl.js +6 -0
  82. data/assets/ableplayer/translations/zh-tw.js +311 -0
  83. data/assets/css/style.css +1 -1
  84. data/assets/css/style.css.map +1 -1
  85. data/assets/fonts/{anonymouspro-bold.woff → anonymouspro/anonymouspro-bold.woff} +0 -0
  86. data/assets/fonts/{anonymouspro-bold.woff2 → anonymouspro/anonymouspro-bold.woff2} +0 -0
  87. data/assets/fonts/{anonymouspro-bolditalic.woff → anonymouspro/anonymouspro-bolditalic.woff} +0 -0
  88. data/assets/fonts/{anonymouspro-bolditalic.woff2 → anonymouspro/anonymouspro-bolditalic.woff2} +0 -0
  89. data/assets/fonts/{anonymouspro-italic.woff → anonymouspro/anonymouspro-italic.woff} +0 -0
  90. data/assets/fonts/{anonymouspro-italic.woff2 → anonymouspro/anonymouspro-italic.woff2} +0 -0
  91. data/assets/fonts/{anonymouspro-regular.woff → anonymouspro/anonymouspro-regular.woff} +0 -0
  92. data/assets/fonts/{anonymouspro-regular.woff2 → anonymouspro/anonymouspro-regular.woff2} +0 -0
  93. data/assets/fonts/notonaskh/bold-minimal.woff +0 -0
  94. data/assets/fonts/notonaskh/bold-minimal.woff2 +0 -0
  95. data/assets/fonts/notonaskh/bold.woff +0 -0
  96. data/assets/fonts/notonaskh/bold.woff2 +0 -0
  97. data/assets/fonts/notonaskh/regular-minimal.woff +0 -0
  98. data/assets/fonts/notonaskh/regular-minimal.woff2 +0 -0
  99. data/assets/fonts/notonaskh/regular.woff +0 -0
  100. data/assets/fonts/notonaskh/regular.woff2 +0 -0
  101. data/assets/fonts/{notosans-bold-subset.woff → notosans/notosans-bold-subset.woff} +0 -0
  102. data/assets/fonts/{notosans-bold-subset.woff2 → notosans/notosans-bold-subset.woff2} +0 -0
  103. data/assets/fonts/{notosans-bold.woff → notosans/notosans-bold.woff} +0 -0
  104. data/assets/fonts/{notosans-bold.woff2 → notosans/notosans-bold.woff2} +0 -0
  105. data/assets/fonts/{notosans-bolditalic-subset.woff → notosans/notosans-bolditalic-subset.woff} +0 -0
  106. data/assets/fonts/{notosans-bolditalic-subset.woff2 → notosans/notosans-bolditalic-subset.woff2} +0 -0
  107. data/assets/fonts/{notosans-bolditalic.woff → notosans/notosans-bolditalic.woff} +0 -0
  108. data/assets/fonts/{notosans-bolditalic.woff2 → notosans/notosans-bolditalic.woff2} +0 -0
  109. data/assets/fonts/{notosans-italic-subset.woff → notosans/notosans-italic-subset.woff} +0 -0
  110. data/assets/fonts/{notosans-italic-subset.woff2 → notosans/notosans-italic-subset.woff2} +0 -0
  111. data/assets/fonts/{notosans-italic.woff → notosans/notosans-italic.woff} +0 -0
  112. data/assets/fonts/{notosans-italic.woff2 → notosans/notosans-italic.woff2} +0 -0
  113. data/assets/fonts/{notosans-regular-subset.woff → notosans/notosans-regular-subset.woff} +0 -0
  114. data/assets/fonts/{notosans-regular-subset.woff2 → notosans/notosans-regular-subset.woff2} +0 -0
  115. data/assets/fonts/{notosans-regular.woff → notosans/notosans-regular.woff} +0 -0
  116. data/assets/fonts/{notosans-regular.woff2 → notosans/notosans-regular.woff2} +0 -0
  117. data/assets/fonts/notosansmono/notosansmono-semicondensed.woff +0 -0
  118. data/assets/fonts/notosansmono/notosansmono-semicondensed.woff2 +0 -0
  119. data/assets/fonts/notosansmono/notosansmono-semicondensedbold.woff +0 -0
  120. data/assets/fonts/notosansmono/notosansmono-semicondensedbold.woff2 +0 -0
  121. data/assets/images/icons.svg +24 -0
  122. data/assets/scripts/main.js +10 -3
  123. metadata +66 -33
  124. data/_data/lang.json +0 -730
  125. data/_data/techniques.yml +0 -180
  126. data/_data/wcag.yml +0 -125
  127. data/_includes/.DS_Store +0 -0
@@ -35,7 +35,6 @@
35
35
 
36
36
  languageSelectWrapper.append($('<label for="transcript-language-select">' + this.tt.language + ': </label>'), this.$transcriptLanguageSelect);
37
37
  this.$transcriptToolbar.append(languageSelectWrapper);
38
-
39
38
  this.$transcriptArea.append(this.$transcriptToolbar, this.$transcriptDiv);
40
39
 
41
40
  // If client has provided separate transcript location, put it there.
@@ -70,7 +69,7 @@
70
69
  thisObj.handleTranscriptLockToggle(thisObj.$autoScrollTranscriptCheckbox.prop('checked'));
71
70
  });
72
71
 
73
- this.$transcriptDiv.bind('mousewheel DOMMouseScroll click scroll', function (event) {
72
+ this.$transcriptDiv.on('mousewheel DOMMouseScroll click scroll', function (e) {
74
73
  // Propagation is stopped in transcript click handler, so clicks are on the scrollbar
75
74
  // or outside of a clickable span.
76
75
  if (!thisObj.scrollingTranscript) {
@@ -82,10 +81,10 @@
82
81
 
83
82
  if (typeof this.$transcriptLanguageSelect !== 'undefined') {
84
83
 
85
- this.$transcriptLanguageSelect.on('click mousedown',function (event) {
84
+ this.$transcriptLanguageSelect.on('click mousedown',function (e) {
86
85
  // execute default behavior
87
86
  // prevent propagation of mouse event to toolbar or window
88
- event.stopPropagation();
87
+ e.stopPropagation();
89
88
  });
90
89
 
91
90
  this.$transcriptLanguageSelect.on('change',function () {
@@ -212,11 +211,21 @@
212
211
  // Pressing Enter on an element that is not natively clickable does NOT trigger click()
213
212
  // Keydown events are handled elsehwere, both globally (ableplayer-base.js) and locally (event.js)
214
213
  if (this.$transcriptArea.length > 0) {
215
- this.$transcriptArea.find('span.able-transcript-seekpoint').click(function(event) {
214
+ this.$transcriptArea.find('span.able-transcript-seekpoint').click(function(e) {
215
+ thisObj.seekTrigger = 'transcript';
216
216
  var spanStart = parseFloat($(this).attr('data-start'));
217
217
  // Add a tiny amount so that we're inside the span.
218
218
  spanStart += .01;
219
- thisObj.seekTo(spanStart);
219
+ // Each click within the transcript triggers two click events (not sure why)
220
+ // this.seekingFromTranscript is a stopgab to prevent two calls to SeekTo()
221
+ if (!thisObj.seekingFromTranscript) {
222
+ thisObj.seekingFromTranscript = true;
223
+ thisObj.seekTo(spanStart);
224
+ }
225
+ else {
226
+ // don't seek a second time, but do reset var
227
+ thisObj.seekingFromTranscript = false;
228
+ }
220
229
  });
221
230
  }
222
231
  };
@@ -2,42 +2,51 @@
2
2
  AblePlayer.prototype.getSupportedLangs = function() {
3
3
  // returns an array of languages for which AblePlayer has translation tables
4
4
  // Removing 'nl' as of 2.3.54, pending updates
5
- var langs = ['ca','de','en','es','fr','it','ja'];
5
+ var langs = ['ca','de','en','es','fr','he','it','ja','nb','zh-tw'];
6
6
  return langs;
7
7
  };
8
8
 
9
9
  AblePlayer.prototype.getTranslationText = function() {
10
10
  // determine language, then get labels and prompts from corresponding translation var
11
11
  var deferred, thisObj, lang, thisObj, msg, translationFile;
12
-
13
12
  deferred = $.Deferred();
14
13
 
15
14
  thisObj = this;
16
15
 
16
+ // get language of the web page, if specified
17
+ if ($('body').attr('lang')) {
18
+ lang = $('body').attr('lang');
19
+ }
20
+ else if ($('html').attr('lang')) {
21
+ lang = $('html').attr('lang');
22
+ }
23
+ else {
24
+ lang = null;
25
+ }
26
+
17
27
  // override this.lang to language of the web page, if known and supported
18
28
  // otherwise this.lang will continue using default
19
29
  if (!this.forceLang) {
20
- if ($('body').attr('lang')) {
21
- lang = $('body').attr('lang');
22
- }
23
- else if ($('html').attr('lang')) {
24
- lang = $('html').attr('lang');
25
- }
26
- if (lang !== this.lang) {
27
- msg = 'Language of web page (' + lang +') ';
28
- if ($.inArray(lang,this.getSupportedLangs()) !== -1) {
29
- // this is a supported lang
30
- msg += ' has a translation table available.';
31
- this.lang = lang;
32
- }
33
- else {
34
- msg += ' is not currently supported. Using default language (' + this.lang + ')';
35
- }
36
- if (this.debug) {
37
- console.log(msg);
30
+ if (lang) {
31
+ if (lang !== this.lang) {
32
+ msg = 'Language of web page (' + lang +') ';
33
+ if ($.inArray(lang,this.getSupportedLangs()) !== -1) {
34
+ // this is a supported lang
35
+ msg += ' has a translation table available.';
36
+ this.lang = lang;
37
+ }
38
+ else {
39
+ msg += ' is not currently supported. Using default language (' + this.lang + ')';
40
+ }
41
+ if (this.debug) {
42
+ console.log(msg);
43
+ }
38
44
  }
39
45
  }
40
46
  }
47
+ if (!this.searchLang) {
48
+ this.searchLang = this.lang;
49
+ }
41
50
  translationFile = this.rootPath + 'translations/' + this.lang + '.js';
42
51
  this.importTranslationFile(translationFile).then(function(result) {
43
52
  thisObj.tt = eval(thisObj.lang);
@@ -0,0 +1,87 @@
1
+ (function($) {
2
+ AblePlayer.prototype.computeEndTime = function(startTime, durationTime) {
3
+ var SECONDS = 0;
4
+ var MINUTES = 1;
5
+ var HOURS = 2;
6
+
7
+ var startParts = startTime
8
+ .split(':')
9
+ .reverse()
10
+ .map(function(value) {
11
+ return parseFloat(value);
12
+ });
13
+
14
+ var durationParts = durationTime
15
+ .split(':')
16
+ .reverse()
17
+ .map(function(value) {
18
+ return parseFloat(value);
19
+ });
20
+
21
+ var endTime = startParts
22
+ .reduce(function(acc, val, index) {
23
+ var sum = val + durationParts[index];
24
+
25
+ if (index === SECONDS) {
26
+ if (sum > 60) {
27
+ durationParts[index + 1] += 1;
28
+ sum -= 60;
29
+ }
30
+
31
+ sum = sum.toFixed(3);
32
+ }
33
+
34
+ if (index === MINUTES) {
35
+ if (sum > 60) {
36
+ durationParts[index + 1] += 1;
37
+ sum -= 60;
38
+ }
39
+ }
40
+
41
+ if (sum < 10) {
42
+ sum = '0' + sum;
43
+ }
44
+
45
+ acc.push(sum);
46
+
47
+ return acc;
48
+ }, [])
49
+ .reverse()
50
+ .join(':');
51
+
52
+ return endTime;
53
+ };
54
+
55
+ AblePlayer.prototype.ttml2webvtt = function(contents) {
56
+ var thisObj = this;
57
+
58
+ var xml = thisObj.convert.xml2json(contents, {
59
+ ignoreComment: true,
60
+ alwaysChildren: true,
61
+ compact: true,
62
+ spaces: 2
63
+ });
64
+
65
+ var vttHeader = 'WEBVTT\n\n\n';
66
+ var captions = JSON.parse(xml).tt.body.div.p;
67
+
68
+ var vttCaptions = captions.reduce(function(acc, value, index) {
69
+ var text = value._text;
70
+ var isArray = Array.isArray(text);
71
+ var attributes = value._attributes;
72
+ var endTime = thisObj.computeEndTime(attributes.begin, attributes.dur);
73
+
74
+ var caption =
75
+ thisObj.computeEndTime(attributes.begin, '00:00:0') +
76
+ ' --> ' +
77
+ thisObj.computeEndTime(attributes.begin, attributes.dur) +
78
+ '\n' +
79
+ (isArray ? text.join('\n') : text) +
80
+ '\n\n';
81
+
82
+ return acc + caption;
83
+ }, vttHeader);
84
+
85
+ return vttCaptions;
86
+ };
87
+ })(jQuery);
@@ -22,11 +22,11 @@
22
22
  'id': volumeSliderId,
23
23
  'class': 'able-volume-slider',
24
24
  'aria-hidden': 'true'
25
- });
25
+ }).hide();
26
26
  this.$volumeSliderTooltip = $('<div>',{
27
27
  'class': 'able-tooltip',
28
28
  'role': 'tooltip'
29
- });
29
+ }).hide();
30
30
  this.$volumeSliderTrack = $('<div>',{
31
31
  'class': 'able-volume-track'
32
32
  });
@@ -63,42 +63,42 @@
63
63
  this.refreshVolumeSlider(this.volume);
64
64
 
65
65
  // add event listeners
66
- this.$volumeSliderHead.on('mousedown',function (event) {
67
- event.preventDefault(); // prevent text selection (implications?)
66
+ this.$volumeSliderHead.on('mousedown',function (e) {
67
+ e.preventDefault(); // prevent text selection (implications?)
68
68
  thisObj.draggingVolume = true;
69
69
  thisObj.volumeHeadPositionTop = $(this).offset().top;
70
70
  });
71
71
 
72
72
  // prevent dragging after mouseup as mouseup not detected over iframe (YouTube)
73
- this.$mediaContainer.on('mouseover',function (event) {
73
+ this.$mediaContainer.on('mouseover',function (e) {
74
74
  if(thisObj.player == 'youtube'){
75
75
  thisObj.draggingVolume = false;
76
76
  }
77
77
  });
78
78
 
79
- $(document).on('mouseup',function (event) {
79
+ $(document).on('mouseup',function (e) {
80
80
  thisObj.draggingVolume = false;
81
81
  });
82
82
 
83
- $(document).on('mousemove',function (event) {
83
+ $(document).on('mousemove',function (e) {
84
84
  if (thisObj.draggingVolume) {
85
- x = event.pageX;
86
- y = event.pageY;
85
+ x = e.pageX;
86
+ y = e.pageY;
87
87
  thisObj.moveVolumeHead(y);
88
88
  }
89
89
  });
90
90
 
91
- this.$volumeSliderHead.on('keydown',function (event) {
91
+ this.$volumeSliderHead.on('keydown',function (e) {
92
92
  // Left arrow or down arrow
93
- if (event.which === 37 || event.which === 40) {
93
+ if (e.which === 37 || e.which === 40) {
94
94
  thisObj.handleVolume('down');
95
95
  }
96
96
  // Right arrow or up arrow
97
- else if (event.which === 39 || event.which === 38) {
97
+ else if (e.which === 39 || e.which === 38) {
98
98
  thisObj.handleVolume('up');
99
99
  }
100
100
  // Escape key or Enter key
101
- else if (event.which === 27 || event.which === 13) {
101
+ else if (e.which === 27 || e.which === 13) {
102
102
  // close popup
103
103
  if (thisObj.$volumeSlider.is(':visible')) {
104
104
  thisObj.hideVolumePopup();
@@ -110,7 +110,7 @@
110
110
  else {
111
111
  return;
112
112
  }
113
- event.preventDefault();
113
+ e.preventDefault();
114
114
  });
115
115
  };
116
116
 
@@ -268,6 +268,7 @@
268
268
  this.closePopups();
269
269
  this.$tooltipDiv.hide();
270
270
  this.$volumeSlider.show().attr('aria-hidden','false');
271
+ this.$volumeButton.attr('aria-expanded','true');
271
272
  this.$volumeSliderHead.attr('tabindex','0').focus();
272
273
  };
273
274
 
@@ -275,7 +276,7 @@
275
276
 
276
277
  this.$volumeSlider.hide().attr('aria-hidden','true');
277
278
  this.$volumeSliderHead.attr('tabindex','-1');
278
- this.$volumeButton.focus();
279
+ this.$volumeButton.attr('aria-expanded','false').focus();
279
280
  };
280
281
 
281
282
  AblePlayer.prototype.isMuted = function () {
@@ -0,0 +1,1093 @@
1
+ /* Video Transcript Sorter (VTS)
2
+ * Used to synchronize time stamps from WebVTT resources
3
+ * so they appear in the proper sequence within an auto-generated interactive transcript
4
+ */
5
+
6
+ (function ($) {
7
+ AblePlayer.prototype.injectVTS = function() {
8
+
9
+ // To add a transcript sorter to a web page:
10
+ // Add <div id="able-vts"></div> to the web page
11
+
12
+ // Define all variables
13
+ var thisObj, tracks, $heading;
14
+ var $instructions, $p1, $p2, $ul, $li1, $li2, $li3;
15
+ var $fieldset, $legend, i, $radioDiv, radioId, $label, $radio;
16
+ var $saveButton, $savedTable;
17
+
18
+ thisObj = this;
19
+
20
+ if ($('#able-vts').length) {
21
+ // Page includes a container for a VTS instance
22
+
23
+ // Are they qualifying tracks?
24
+ if (this.vtsTracks.length) {
25
+ // Yes - there are!
26
+
27
+ // Build an array of unique languages
28
+ this.langs = [];
29
+ this.getAllLangs(this.vtsTracks);
30
+
31
+ // Set the default VTS language
32
+ this.vtsLang = this.lang;
33
+
34
+ // Inject a heading
35
+ $heading = $('<h2>').text('Video Transcript Sorter'); // TODO: Localize; intelligently assign proper heading level
36
+ $('#able-vts').append($heading);
37
+
38
+ // Inject an empty div for writing messages
39
+ this.$vtsAlert = $('<div>',{
40
+ 'id': 'able-vts-alert',
41
+ 'aria-live': 'polite',
42
+ 'aria-atomic': 'true'
43
+ })
44
+ $('#able-vts').append(this.$vtsAlert);
45
+
46
+ // Inject instructions (TODO: Localize)
47
+ $instructions = $('<div>',{
48
+ 'id': 'able-vts-instructions'
49
+ });
50
+ $p1 = $('<p>').text('Use the Video Transcript Sorter to perform any of the following tasks:');
51
+ $ul = $('<ul>');
52
+ $li1 = $('<li>').text('Reorder chapters, descriptions, captions, and/or subtitles so they appear in the proper sequence in Able Player\'s auto-generated transcript.');
53
+ $li2 = $('<li>').text('Modify content or start/end times (all are directly editable within the table).');
54
+ $li3 = $('<li>').text('Insert new content, such as chapters or descriptions.');
55
+ $p2 = $('<p>').text('When finished editing, click the "Save Changes" button. This will auto-generate new content for all relevant timed text files (chapters, descriptions, captions, and/or subtitles), which can be copied and pasted into separate WebVTT files for use by Able Player.');
56
+ $ul.append($li1,$li2,$li3);
57
+ $instructions.append($p1,$ul,$p2);
58
+ $('#able-vts').append($instructions);
59
+
60
+ // Inject a fieldset with radio buttons for each language
61
+ $fieldset = $('<fieldset>');
62
+ $legend = $('<legend>').text('Select a language'); // TODO: Localize this
63
+ $fieldset.append($legend)
64
+ for (i in this.langs) {
65
+ radioId = 'vts-lang-radio-' + this.langs[i];
66
+ $radioDiv = $('<div>',{
67
+ // uncomment the following if label is native name
68
+ // 'lang': this.langs[i]
69
+ });
70
+ $radio = $('<input>', {
71
+ 'type': 'radio',
72
+ 'name': 'vts-lang',
73
+ 'id': radioId,
74
+ 'value': this.langs[i]
75
+ }).on('click',function() {
76
+ thisObj.vtsLang = $(this).val();
77
+ thisObj.showVtsAlert('Loading ' + thisObj.getLanguageName(thisObj.vtsLang) + ' tracks');
78
+ thisObj.injectVtsTable('update',thisObj.vtsLang);
79
+ });
80
+ if (this.langs[i] == this.lang) {
81
+ // this is the default language.
82
+ $radio.prop('checked',true);
83
+ }
84
+ $label = $('<label>', {
85
+ 'for': radioId
86
+ // Two options for label:
87
+ // getLanguageNativeName() - returns native name; if using this be sure to add lang attr to <div> (see above)
88
+ // getLanguageName() - returns name in English; doesn't require lang attr on <label>
89
+ }).text(this.getLanguageName(this.langs[i]));
90
+ $radioDiv.append($radio,$label);
91
+ $fieldset.append($radioDiv);
92
+ }
93
+ $('#able-vts').append($fieldset);
94
+
95
+ // Inject a 'Save Changes' button
96
+ $saveButton = $('<button>',{
97
+ 'type': 'button',
98
+ 'id': 'able-vts-save',
99
+ 'value': 'save'
100
+ }).text('Save Changes'); // TODO: Localize this
101
+ $('#able-vts').append($saveButton);
102
+
103
+ // Inject a table with one row for each cue in the default language
104
+ this.injectVtsTable('add',this.vtsLang);
105
+
106
+ // TODO: Add drag/drop functionality for mousers
107
+
108
+ // Add event listeners for contenteditable cells
109
+ var kindOptions, beforeEditing, editedCell, editedContent, i, closestKind;
110
+ kindOptions = ['captions','chapters','descriptions','subtitles'];
111
+ $('td[contenteditable="true"]').on('focus',function() {
112
+ beforeEditing = $(this).text();
113
+ }).on('blur',function() {
114
+ if (beforeEditing != $(this).text()) {
115
+ editedCell = $(this).index();
116
+ editedContent = $(this).text();
117
+ if (editedCell === 1) {
118
+ // do some simple spelling auto-correct
119
+ if ($.inArray(editedContent,kindOptions) === -1) {
120
+ // whatever user typed is not a valid kind
121
+ // assume they correctly typed the first character
122
+ if (editedContent.substr(0,1) === 's') {
123
+ $(this).text('subtitles');
124
+ }
125
+ else if (editedContent.substr(0,1) === 'd') {
126
+ $(this).text('descriptions');
127
+ }
128
+ else if (editedContent.substr(0,2) === 'ch') {
129
+ $(this).text('chapters');
130
+ }
131
+ else {
132
+ // whatever else they types, assume 'captions'
133
+ $(this).text('captions');
134
+ }
135
+ }
136
+ }
137
+ else if (editedCell === 2 || editedCell === 3) {
138
+ // start or end time
139
+ // ensure proper formatting (with 3 decimal places)
140
+ $(this).text(thisObj.formatTimestamp(editedContent));
141
+ }
142
+ }
143
+ }).on('keydown',function(e) {
144
+ // don't allow keystrokes to trigger Able Player (or other) functions
145
+ // while user is editing
146
+ e.stopPropagation();
147
+ });
148
+
149
+ // handle click on the Save button
150
+
151
+ // handle click on the Save button
152
+ $('#able-vts-save').on('click',function(e) {
153
+ e.stopPropagation();
154
+ if ($(this).attr('value') == 'save') {
155
+ // replace table with WebVTT output in textarea fields (for copying/pasting)
156
+ $(this).attr('value','cancel').text('Return to Editor'); // TODO: Localize this
157
+ $savedTable = $('#able-vts table');
158
+ $('#able-vts-instructions').hide();
159
+ $('#able-vts > fieldset').hide();
160
+ $('#able-vts table').remove();
161
+ $('#able-vts-icon-credit').remove();
162
+ thisObj.parseVtsOutput($savedTable);
163
+ }
164
+ else {
165
+ // cancel saving, and restore the table using edited content
166
+ $(this).attr('value','save').text('Save Changes'); // TODO: Localize this
167
+ $('#able-vts-output').remove();
168
+ $('#able-vts-instructions').show();
169
+ $('#able-vts > fieldset').show();
170
+ $('#able-vts').append($savedTable);
171
+ $('#able-vts').append(thisObj.getIconCredit());
172
+ thisObj.showVtsAlert('Cancelling saving. Any edits you made have been restored in the VTS table.'); // TODO: Localize this
173
+ }
174
+ });
175
+ }
176
+ }
177
+ };
178
+
179
+ AblePlayer.prototype.setupVtsTracks = function(kind, lang, label, src, contents) {
180
+
181
+ // Called from tracks.js
182
+
183
+ var srcFile, vtsCues;
184
+
185
+ srcFile = this.getFilenameFromPath(src);
186
+ vtsCues = this.parseVtsTracks(contents);
187
+
188
+ this.vtsTracks.push({
189
+ 'kind': kind,
190
+ 'language': lang,
191
+ 'label': label,
192
+ 'srcFile': srcFile,
193
+ 'cues': vtsCues
194
+ });
195
+ };
196
+
197
+ AblePlayer.prototype.getFilenameFromPath = function(path) {
198
+
199
+ var lastSlash;
200
+
201
+ lastSlash = path.lastIndexOf('/');
202
+ if (lastSlash === -1) {
203
+ // there are no slashes in path.
204
+ return path;
205
+ }
206
+ else {
207
+ return path.substr(lastSlash+1);
208
+ }
209
+ };
210
+
211
+ AblePlayer.prototype.getFilenameFromTracks = function(kind,lang) {
212
+
213
+ for (var i=0; i<this.vtsTracks.length; i++) {
214
+ if (this.vtsTracks[i].kind === kind && this.vtsTracks[i].language === lang) {
215
+ // this is a matching track
216
+ // srcFile has already been converted to filename from path before saving to vtsTracks
217
+ return this.vtsTracks[i].srcFile;
218
+ }
219
+ }
220
+ // no matching track found
221
+ return false;
222
+ };
223
+
224
+ AblePlayer.prototype.parseVtsTracks = function(contents) {
225
+
226
+ var rows, timeParts, cues, i, j, thisRow, nextRow, content, blankRow;
227
+ rows = contents.split("\n");
228
+ cues = [];
229
+ i = 0;
230
+ while (i < rows.length) {
231
+ thisRow = rows[i];
232
+ if (thisRow.indexOf(' --> ') !== -1) {
233
+ // this is probably a time row
234
+ timeParts = thisRow.trim().split(' ');
235
+ if (this.isValidTimestamp(timeParts[0]) && this.isValidTimestamp(timeParts[2])) {
236
+ // both timestamps are valid. This is definitely a time row
237
+ content = '';
238
+ j = i+1;
239
+ blankRow = false;
240
+ while (j < rows.length && !blankRow) {
241
+ nextRow = rows[j].trim();
242
+ if (nextRow.length > 0) {
243
+ if (content.length > 0) {
244
+ // add back the EOL between rows of content
245
+ content += "\n" + nextRow;
246
+ }
247
+ else {
248
+ // this is the first row of content. No need for an EOL
249
+ content += nextRow;
250
+ }
251
+ }
252
+ else {
253
+ blankRow = true;
254
+ }
255
+ j++;
256
+ }
257
+ cues.push({
258
+ 'start': timeParts[0],
259
+ 'end': timeParts[2],
260
+ 'content': content
261
+ });
262
+ i = j; //skip ahead
263
+ }
264
+ }
265
+ else {
266
+ i++;
267
+ }
268
+ }
269
+ return cues;
270
+ };
271
+
272
+ AblePlayer.prototype.isValidTimestamp = function(timestamp) {
273
+
274
+ // return true if timestamp contains only numbers or expected punctuation
275
+ if (/^[0-9:,.]*$/.test(timestamp)) {
276
+ return true;
277
+ }
278
+ else {
279
+ return false;
280
+ }
281
+ };
282
+
283
+ AblePlayer.prototype.formatTimestamp = function(timestamp) {
284
+
285
+ // timestamp is a string in the form "HH:MM:SS.xxx"
286
+ // Take some simple steps to ensure edited timestamp values still adhere to expected format
287
+
288
+ var firstPart, lastPart;
289
+
290
+ var firstPart = timestamp.substr(0,timestamp.lastIndexOf('.')+1);
291
+ var lastPart = timestamp.substr(timestamp.lastIndexOf('.')+1);
292
+
293
+ // TODO: Be sure each component within firstPart has only exactly two digits
294
+ // Probably can't justify doing this automatically
295
+ // If users enters '5' for minutes, that could be either '05' or '50'
296
+ // This should trigger an error and prompt the user to correct the value before proceeding
297
+
298
+ // Be sure lastPart has exactly three digits
299
+ if (lastPart.length > 3) {
300
+ // chop off any extra digits
301
+ lastPart = lastPart.substr(0,3);
302
+ }
303
+ else if (lastPart.length < 3) {
304
+ // add trailing zeros
305
+ while (lastPart.length < 3) {
306
+ lastPart += '0';
307
+ }
308
+ }
309
+ return firstPart + lastPart;
310
+ };
311
+
312
+
313
+ AblePlayer.prototype.injectVtsTable = function(action,lang) {
314
+
315
+ // action is either 'add' (for a new table) or 'update' (if user has selected a new lang)
316
+
317
+ var $table, headers, i, $tr, $th, $td, rows, rowNum, rowId;
318
+
319
+ if (action === 'update') {
320
+ // remove existing table
321
+ $('#able-vts table').remove();
322
+ $('#able-vts-icon-credit').remove();
323
+ }
324
+
325
+ $table = $('<table>',{
326
+ 'lang': lang
327
+ });
328
+ $tr = $('<tr>',{
329
+ 'lang': 'en' // TEMP, until header row is localized
330
+ });
331
+ headers = ['Row #','Kind','Start','End','Content','Actions']; // TODO: Localize this
332
+ for (i=0; i < headers.length; i++) {
333
+ $th = $('<th>', {
334
+ 'scope': 'col'
335
+ }).text(headers[i]);
336
+ if (headers[i] === 'Actions') {
337
+ $th.addClass('actions');
338
+ }
339
+ $tr.append($th);
340
+ }
341
+ $table.append($tr);
342
+
343
+ // Get all rows (sorted by start time), and inject them into table
344
+ rows = this.getAllRows(lang);
345
+ for (i=0; i < rows.length; i++) {
346
+ rowNum = i + 1;
347
+ rowId = 'able-vts-row-' + rowNum;
348
+ $tr = $('<tr>',{
349
+ 'id': rowId,
350
+ 'class': 'kind-' + rows[i].kind
351
+ });
352
+ // Row #
353
+ $td = $('<td>').text(rowNum);
354
+ $tr.append($td);
355
+
356
+ // Kind
357
+ $td = $('<td>',{
358
+ 'contenteditable': 'true'
359
+ }).text(rows[i].kind);
360
+ $tr.append($td);
361
+
362
+ // Start
363
+ $td = $('<td>',{
364
+ 'contenteditable': 'true'
365
+ }).text(rows[i].start);
366
+ $tr.append($td);
367
+
368
+ // End
369
+ $td = $('<td>',{
370
+ 'contenteditable': 'true'
371
+ }).text(rows[i].end);
372
+ $tr.append($td);
373
+
374
+ // Content
375
+ $td = $('<td>',{
376
+ 'contenteditable': 'true'
377
+ }).text(rows[i].content); // TODO: Preserve tags
378
+ $tr.append($td);
379
+
380
+ // Actions
381
+ $td = this.addVtsActionButtons(rowNum,rows.length);
382
+ $tr.append($td);
383
+
384
+ $table.append($tr);
385
+ }
386
+ $('#able-vts').append($table);
387
+
388
+ // Add credit for action button SVG icons
389
+ $('#able-vts').append(this.getIconCredit());
390
+
391
+ };
392
+
393
+ AblePlayer.prototype.addVtsActionButtons = function(rowNum,numRows) {
394
+
395
+ // rowNum is the number of the current table row (starting with 1)
396
+ // numRows is the total number of rows (excluding the header row)
397
+ // TODO: Position buttons so they're vertically aligned, even if missing an Up or Down button
398
+ var thisObj, $td, buttons, i, button, $button, $svg, $g, pathString, pathString2, $path, $path2;
399
+ thisObj = this;
400
+ $td = $('<td>');
401
+ buttons = ['up','down','insert','delete'];
402
+
403
+ for (i=0; i < buttons.length; i++) {
404
+ button = buttons[i];
405
+ if (button === 'up') {
406
+ if (rowNum > 1) {
407
+ $button = $('<button>',{
408
+ 'id': 'able-vts-button-up-' + rowNum,
409
+ 'title': 'Move up',
410
+ 'aria-label': 'Move Row ' + rowNum + ' up'
411
+ }).on('click', function(el) {
412
+ thisObj.onClickVtsActionButton(el.currentTarget);
413
+ });
414
+ $svg = $('<svg>',{
415
+ 'focusable': 'false',
416
+ 'aria-hidden': 'true',
417
+ 'x': '0px',
418
+ 'y': '0px',
419
+ 'width': '254.296px',
420
+ 'height': '254.296px',
421
+ 'viewBox': '0 0 254.296 254.296',
422
+ 'style': 'enable-background:new 0 0 254.296 254.296'
423
+ });
424
+ pathString = 'M249.628,176.101L138.421,52.88c-6.198-6.929-16.241-6.929-22.407,0l-0.381,0.636L4.648,176.101'
425
+ + 'c-6.198,6.897-6.198,18.052,0,24.981l0.191,0.159c2.892,3.305,6.865,5.371,11.346,5.371h221.937c4.577,0,8.613-2.161,11.41-5.594'
426
+ + 'l0.064,0.064C255.857,194.153,255.857,182.998,249.628,176.101z';
427
+ $path = $('<path>',{
428
+ 'd': pathString
429
+ });
430
+ $g = $('<g>').append($path);
431
+ $svg.append($g);
432
+ $button.append($svg);
433
+ // Refresh button in the DOM in order for browser to process & display the SVG
434
+ $button.html($button.html());
435
+ $td.append($button);
436
+ }
437
+ }
438
+ else if (button === 'down') {
439
+ if (rowNum < numRows) {
440
+ $button = $('<button>',{
441
+ 'id': 'able-vts-button-down-' + rowNum,
442
+ 'title': 'Move down',
443
+ 'aria-label': 'Move Row ' + rowNum + ' down'
444
+ }).on('click', function(el) {
445
+ thisObj.onClickVtsActionButton(el.currentTarget);
446
+ });
447
+ $svg = $('<svg>',{
448
+ 'focusable': 'false',
449
+ 'aria-hidden': 'true',
450
+ 'x': '0px',
451
+ 'y': '0px',
452
+ 'width': '292.362px',
453
+ 'height': '292.362px',
454
+ 'viewBox': '0 0 292.362 292.362',
455
+ 'style': 'enable-background:new 0 0 292.362 292.362'
456
+ });
457
+ pathString = 'M286.935,69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952,0-9.233,1.807-12.85,5.424'
458
+ + 'C1.807,72.998,0,77.279,0,82.228c0,4.948,1.807,9.229,5.424,12.847l127.907,127.907c3.621,3.617,7.902,5.428,12.85,5.428'
459
+ + 's9.233-1.811,12.847-5.428L286.935,95.074c3.613-3.617,5.427-7.898,5.427-12.847C292.362,77.279,290.548,72.998,286.935,69.377z';
460
+ $path = $('<path>',{
461
+ 'd': pathString
462
+ });
463
+ $g = $('<g>').append($path);
464
+ $svg.append($g);
465
+ $button.append($svg);
466
+ // Refresh button in the DOM in order for browser to process & display the SVG
467
+ $button.html($button.html());
468
+ $td.append($button);
469
+ }
470
+ }
471
+ else if (button === 'insert') {
472
+ // Add Insert button to all rows
473
+ $button = $('<button>',{
474
+ 'id': 'able-vts-button-insert-' + rowNum,
475
+ 'title': 'Insert row below',
476
+ 'aria-label': 'Insert row before Row ' + rowNum
477
+ }).on('click', function(el) {
478
+ thisObj.onClickVtsActionButton(el.currentTarget);
479
+ });
480
+ $svg = $('<svg>',{
481
+ 'focusable': 'false',
482
+ 'aria-hidden': 'true',
483
+ 'x': '0px',
484
+ 'y': '0px',
485
+ 'width': '401.994px',
486
+ 'height': '401.994px',
487
+ 'viewBox': '0 0 401.994 401.994',
488
+ 'style': 'enable-background:new 0 0 401.994 401.994'
489
+ });
490
+ pathString = 'M394,154.175c-5.331-5.33-11.806-7.994-19.417-7.994H255.811V27.406c0-7.611-2.666-14.084-7.994-19.414'
491
+ + 'C242.488,2.666,236.02,0,228.398,0h-54.812c-7.612,0-14.084,2.663-19.414,7.993c-5.33,5.33-7.994,11.803-7.994,19.414v118.775'
492
+ + 'H27.407c-7.611,0-14.084,2.664-19.414,7.994S0,165.973,0,173.589v54.819c0,7.618,2.662,14.086,7.992,19.411'
493
+ + 'c5.33,5.332,11.803,7.994,19.414,7.994h118.771V374.59c0,7.611,2.664,14.089,7.994,19.417c5.33,5.325,11.802,7.987,19.414,7.987'
494
+ + 'h54.816c7.617,0,14.086-2.662,19.417-7.987c5.332-5.331,7.994-11.806,7.994-19.417V255.813h118.77'
495
+ + 'c7.618,0,14.089-2.662,19.417-7.994c5.329-5.325,7.994-11.793,7.994-19.411v-54.819C401.991,165.973,399.332,159.502,394,154.175z';
496
+ $path = $('<path>',{
497
+ 'd': pathString
498
+ });
499
+ $g = $('<g>').append($path);
500
+ $svg.append($g);
501
+ $button.append($svg);
502
+ // Refresh button in the DOM in order for browser to process & display the SVG
503
+ $button.html($button.html());
504
+ $td.append($button);
505
+ }
506
+ else if (button === 'delete') {
507
+ // Add Delete button to all rows
508
+ $button = $('<button>',{
509
+ 'id': 'able-vts-button-delete-' + rowNum,
510
+ 'title': 'Delete row ',
511
+ 'aria-label': 'Delete Row ' + rowNum
512
+ }).on('click', function(el) {
513
+ thisObj.onClickVtsActionButton(el.currentTarget);
514
+ });
515
+ $svg = $('<svg>',{
516
+ 'focusable': 'false',
517
+ 'aria-hidden': 'true',
518
+ 'x': '0px',
519
+ 'y': '0px',
520
+ 'width': '508.52px',
521
+ 'height': '508.52px',
522
+ 'viewBox': '0 0 508.52 508.52',
523
+ 'style': 'enable-background:new 0 0 508.52 508.52'
524
+ });
525
+ pathString = 'M397.281,31.782h-63.565C333.716,14.239,319.478,0,301.934,0h-95.347'
526
+ + 'c-17.544,0-31.782,14.239-31.782,31.782h-63.565c-17.544,0-31.782,14.239-31.782,31.782h349.607'
527
+ + 'C429.063,46.021,414.825,31.782,397.281,31.782z';
528
+ $path = $('<path>',{
529
+ 'd': pathString
530
+ });
531
+ pathString2 = 'M79.456,476.737c0,17.544,14.239,31.782,31.782,31.782h286.042'
532
+ + 'c17.544,0,31.782-14.239,31.782-31.782V95.347H79.456V476.737z M333.716,174.804c0-8.772,7.151-15.891,15.891-15.891'
533
+ + 'c8.74,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.151,15.891-15.891,15.891c-8.74,0-15.891-7.151-15.891-15.891V174.804z'
534
+ + 'M238.369,174.804c0-8.772,7.119-15.891,15.891-15.891c8.74,0,15.891,7.119,15.891,15.891v254.26'
535
+ + 'c0,8.74-7.151,15.891-15.891,15.891c-8.772,0-15.891-7.151-15.891-15.891V174.804z M143.021,174.804'
536
+ + 'c0-8.772,7.119-15.891,15.891-15.891c8.772,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.119,15.891-15.891,15.891'
537
+ + 'c-8.772,0-15.891-7.151-15.891-15.891V174.804z';
538
+ $path2 = $('<path>',{
539
+ 'd': pathString2
540
+ });
541
+
542
+ $g = $('<g>').append($path,$path2);
543
+ $svg.append($g);
544
+ $button.append($svg);
545
+ // Refresh button in the DOM in order for browser to process & display the SVG
546
+ $button.html($button.html());
547
+ $td.append($button);
548
+ }
549
+ }
550
+ return $td;
551
+ };
552
+
553
+ AblePlayer.prototype.updateVtsActionButtons = function($buttons,nextRowNum) {
554
+
555
+ // TODO: Add some filters to this function to add or delete 'Up' and 'Down' buttons
556
+ // if row is moved to/from the first/last rows
557
+ var i, $thisButton, id, label, newId, newLabel;
558
+ for (i=0; i < $buttons.length; i++) {
559
+ $thisButton = $buttons.eq(i);
560
+ id = $thisButton.attr('id');
561
+ label = $thisButton.attr('aria-label');
562
+ // replace the integer (id) within each of the above strings
563
+ newId = id.replace(/[0-9]+/g, nextRowNum);
564
+ newLabel = label.replace(/[0-9]+/g, nextRowNum);
565
+ $thisButton.attr('id',newId);
566
+ $thisButton.attr('aria-label',newLabel);
567
+ }
568
+ }
569
+
570
+ AblePlayer.prototype.getIconCredit = function() {
571
+
572
+ var credit;
573
+ credit = '<div id="able-vts-icon-credit">'
574
+ + 'Action buttons made by <a href="https://www.flaticon.com/authors/elegant-themes">Elegant Themes</a> '
575
+ + 'from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> '
576
+ + 'are licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" '
577
+ + 'target="_blank">CC 3.0 BY</a>'
578
+ + '</div>';
579
+ return credit;
580
+ };
581
+
582
+ AblePlayer.prototype.getAllLangs = function(tracks) {
583
+
584
+ // update this.langs with any unique languages found in tracks
585
+ var i;
586
+ for (i in tracks) {
587
+ if (tracks[i].hasOwnProperty('language')) {
588
+ if ($.inArray(tracks[i].language,this.langs) === -1) {
589
+ // this language is not already in the langs array. Add it.
590
+ this.langs[this.langs.length] = tracks[i].language;
591
+ }
592
+ }
593
+ }
594
+ };
595
+
596
+ AblePlayer.prototype.getAllRows = function(lang) {
597
+
598
+ // returns an array of data to be displayed in VTS table
599
+ // includes all cues for tracks of any type with matching lang
600
+ // cues are sorted by start time
601
+ var i, track, c, cues;
602
+ cues = [];
603
+ for (i=0; i < this.vtsTracks.length; i++) {
604
+ track = this.vtsTracks[i];
605
+ if (track.language == lang) {
606
+ // this track matches the language. Add its cues to array
607
+ for (c in track.cues) {
608
+ cues.push({
609
+ 'kind': track.kind,
610
+ 'lang': lang,
611
+ 'id': track.cues[c].id,
612
+ 'start': track.cues[c].start,
613
+ 'end': track.cues[c].end,
614
+ 'content': track.cues[c].content
615
+ });
616
+ }
617
+ }
618
+ }
619
+ // Now sort cues by start time
620
+ cues.sort(function(a,b) {
621
+ return a.start > b.start ? 1 : -1;
622
+ });
623
+ return cues;
624
+ };
625
+
626
+
627
+ AblePlayer.prototype.onClickVtsActionButton = function(el) {
628
+
629
+ // handle click on up, down, insert, or delete button
630
+ var idParts, action, rowNum;
631
+ idParts = $(el).attr('id').split('-');
632
+ action = idParts[3];
633
+ rowNum = idParts[4];
634
+ if (action == 'up') {
635
+ // move the row up
636
+ this.moveRow(rowNum,'up');
637
+ }
638
+ else if (action == 'down') {
639
+ // move the row down
640
+ this.moveRow(rowNum,'down');
641
+ }
642
+ else if (action == 'insert') {
643
+ // insert a row below
644
+ this.insertRow(rowNum);
645
+ }
646
+ else if (action == 'delete') {
647
+ // delete the row
648
+ this.deleteRow(rowNum);
649
+ }
650
+ };
651
+
652
+ AblePlayer.prototype.insertRow = function(rowNum) {
653
+
654
+ // Insert empty row below rowNum
655
+ var $table, $rows, numRows, newRowNum, newRowId, newTimes, $tr, $td;
656
+ var $select, options, i, $option, newKind, newClass, $parentRow;
657
+ var i, nextRowNum, $buttons;
658
+
659
+ $table = $('#able-vts table');
660
+ $rows = $table.find('tr');
661
+
662
+ numRows = $rows.length - 1; // exclude header row
663
+
664
+ newRowNum = parseInt(rowNum) + 1;
665
+ newRowId = 'able-vts-row-' + newRowNum;
666
+
667
+ // Create an empty row
668
+ $tr = $('<tr>',{
669
+ 'id': newRowId
670
+ });
671
+
672
+ // Row #
673
+ $td = $('<td>').text(newRowNum);
674
+ $tr.append($td);
675
+
676
+ // Kind (add a select field for chosing a kind)
677
+ newKind = null;
678
+ $select = $('<select>',{
679
+ 'id': 'able-vts-kind-' + newRowNum,
680
+ 'aria-label': 'What kind of track is this?',
681
+ 'placeholder': 'Select a kind'
682
+ }).on('change',function() {
683
+ newKind = $(this).val();
684
+ newClass = 'kind-' + newKind;
685
+ $parentRow = $(this).closest('tr');
686
+ // replace the select field with the chosen value as text
687
+ $(this).parent().text(newKind);
688
+ // add a class to the parent row
689
+ $parentRow.addClass(newClass);
690
+ });
691
+ options = ['','captions','chapters','descriptions','subtitles'];
692
+ for (i=0; i<options.length; i++) {
693
+ $option = $('<option>',{
694
+ 'value': options[i]
695
+ }).text(options[i]);
696
+ $select.append($option);
697
+ }
698
+ $td = $('<td>').append($select);
699
+ $tr.append($td);
700
+
701
+ // Start
702
+ $td = $('<td>',{
703
+ 'contenteditable': 'true'
704
+ }); // TODO; Intelligently assign a new start time (see getAdjustedTimes())
705
+ $tr.append($td);
706
+
707
+ // End
708
+ $td = $('<td>',{
709
+ 'contenteditable': 'true'
710
+ }); // TODO; Intelligently assign a new end time (see getAdjustedTimes())
711
+ $tr.append($td);
712
+
713
+ // Content
714
+ $td = $('<td>',{
715
+ 'contenteditable': 'true'
716
+ });
717
+ $tr.append($td);
718
+
719
+ // Actions
720
+ $td = this.addVtsActionButtons(newRowNum,numRows);
721
+ $tr.append($td);
722
+
723
+ // Now insert the new row
724
+ $table.find('tr').eq(rowNum).after($tr);
725
+
726
+ // Update row.id, Row # cell, & action items for all rows after the inserted one
727
+ for (i=newRowNum; i <= numRows; i++) {
728
+ nextRowNum = i + 1;
729
+ $rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
730
+ $rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
731
+ $buttons = $rows.eq(i).find('button');
732
+ this.updateVtsActionButtons($buttons,nextRowNum);
733
+ }
734
+
735
+ // Auto-adjust times
736
+ this.adjustTimes(newRowNum);
737
+
738
+ // Announce the insertion
739
+ this.showVtsAlert('A new row ' + newRowNum + ' has been inserted'); // TODO: Localize this
740
+
741
+ // Place focus in new select field
742
+ $select.focus();
743
+
744
+ };
745
+
746
+ AblePlayer.prototype.deleteRow = function(rowNum) {
747
+
748
+ var $table, $rows, numRows, i, nextRowNum, $buttons;
749
+
750
+ $table = $('#able-vts table');
751
+ $table[0].deleteRow(rowNum);
752
+ $rows = $table.find('tr'); // this does not include the deleted row
753
+ numRows = $rows.length - 1; // exclude header row
754
+
755
+ // Update row.id, Row # cell, & action buttons for all rows after the deleted one
756
+ for (i=rowNum; i <= numRows; i++) {
757
+ nextRowNum = i;
758
+ $rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
759
+ $rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
760
+ $buttons = $rows.eq(i).find('button');
761
+ this.updateVtsActionButtons($buttons,nextRowNum);
762
+ }
763
+
764
+ // Announce the deletion
765
+ this.showVtsAlert('Row ' + rowNum + ' has been deleted'); // TODO: Localize this
766
+
767
+ };
768
+
769
+ AblePlayer.prototype.moveRow = function(rowNum,direction) {
770
+
771
+ // swap two rows
772
+ var $rows, $thisRow, otherRowNum, $otherRow, newTimes, msg;
773
+
774
+ $rows = $('#able-vts table').find('tr');
775
+ $thisRow = $('#able-vts table').find('tr').eq(rowNum);
776
+ if (direction == 'up') {
777
+ otherRowNum = parseInt(rowNum) - 1;
778
+ $otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
779
+ $otherRow.before($thisRow);
780
+ }
781
+ else if (direction == 'down') {
782
+ otherRowNum = parseInt(rowNum) + 1;
783
+ $otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
784
+ $otherRow.after($thisRow);
785
+ }
786
+ // Update row.id, Row # cell, & action buttons for the two swapped rows
787
+ $thisRow.attr('id','able-vts-row-' + otherRowNum);
788
+ $thisRow.find('td').eq(0).text(otherRowNum);
789
+ this.updateVtsActionButtons($thisRow.find('button'),otherRowNum);
790
+ $otherRow.attr('id','able-vts-row-' + rowNum);
791
+ $otherRow.find('td').eq(0).text(rowNum);
792
+ this.updateVtsActionButtons($otherRow.find('button'),rowNum);
793
+
794
+ // auto-adjust times
795
+ this.adjustTimes(otherRowNum);
796
+
797
+ // Announce the move (TODO: Localize this)
798
+ msg = 'Row ' + rowNum + ' has been moved ' + direction;
799
+ msg += ' and is now Row ' + otherRowNum;
800
+ this.showVtsAlert(msg);
801
+ };
802
+
803
+ AblePlayer.prototype.adjustTimes = function(rowNum) {
804
+
805
+ // Adjusts start and end times of the current, previous, and next rows in VTS table
806
+ // after a move or insert
807
+ // NOTE: Fully automating this process would be extraordinarily complicated
808
+ // The goal here is simply to make subtle tweaks to ensure rows appear
809
+ // in the new order within the Able Player transcript
810
+ // Additional tweaking will likely be required by the user
811
+
812
+ // HISTORY: Originally set minDuration to 2 seconds for captions and .500 for descriptions
813
+ // However, this can results in significant changes to existing caption timing,
814
+ // with not-so-positive results.
815
+ // As of 3.1.15, setting minDuration to .001 for all track kinds
816
+ // Users will have to make further adjustments manually if needed
817
+
818
+ // TODO: Add WebVTT validation on save, since tweaking times is risky
819
+
820
+ var minDuration, $rows, prevRowNum, nextRowNum, $row, $prevRow, $nextRow,
821
+ kind, prevKind, nextKind,
822
+ start, prevStart, nextStart,
823
+ end, prevEnd, nextEnd;
824
+
825
+ // Define minimum duration (in seconds) for each kind of track
826
+ minDuration = [];
827
+ minDuration['captions'] = .001;
828
+ minDuration['descriptions'] = .001;
829
+ minDuration['chapters'] = .001;
830
+
831
+ // refresh rows object
832
+ $rows = $('#able-vts table').find('tr');
833
+
834
+ // Get kind, start, and end from current row
835
+ $row = $rows.eq(rowNum);
836
+ if ($row.is('[class^="kind-"]')) {
837
+ // row has a class that starts with "kind-"
838
+ // Extract kind from the class name
839
+ kind = this.getKindFromClass($row.attr('class'));
840
+ }
841
+ else {
842
+ // Kind has not been assigned (e.g., newly inserted row)
843
+ // Set as captions row by default
844
+ kind = 'captions';
845
+ }
846
+ start = this.getSecondsFromColonTime($row.find('td').eq(2).text());
847
+ end = this.getSecondsFromColonTime($row.find('td').eq(3).text());
848
+
849
+ // Get kind, start, and end from previous row
850
+ if (rowNum > 1) {
851
+ // this is not the first row. Include the previous row
852
+ prevRowNum = rowNum - 1;
853
+ $prevRow = $rows.eq(prevRowNum);
854
+ if ($prevRow.is('[class^="kind-"]')) {
855
+ // row has a class that starts with "kind-"
856
+ // Extract kind from the class name
857
+ prevKind = this.getKindFromClass($prevRow.attr('class'));
858
+ }
859
+ else {
860
+ // Kind has not been assigned (e.g., newly inserted row)
861
+ prevKind = null;
862
+ }
863
+ prevStart = this.getSecondsFromColonTime($prevRow.find('td').eq(2).text());
864
+ prevEnd = this.getSecondsFromColonTime($prevRow.find('td').eq(3).text());
865
+ }
866
+ else {
867
+ // this is the first row
868
+ prevRowNum = null;
869
+ $prevRow = null;
870
+ prevKind = null;
871
+ prevStart = null;
872
+ prevEnd = null;
873
+ }
874
+
875
+ // Get kind, start, and end from next row
876
+ if (rowNum < ($rows.length - 1)) {
877
+ // this is not the last row. Include the next row
878
+ nextRowNum = rowNum + 1;
879
+ $nextRow = $rows.eq(nextRowNum);
880
+ if ($nextRow.is('[class^="kind-"]')) {
881
+ // row has a class that starts with "kind-"
882
+ // Extract kind from the class name
883
+ nextKind = this.getKindFromClass($nextRow.attr('class'));
884
+ }
885
+ else {
886
+ // Kind has not been assigned (e.g., newly inserted row)
887
+ nextKind = null;
888
+ }
889
+ nextStart = this.getSecondsFromColonTime($nextRow.find('td').eq(2).text());
890
+ nextEnd = this.getSecondsFromColonTime($nextRow.find('td').eq(3).text());
891
+ }
892
+ else {
893
+ // this is the last row
894
+ nextRowNum = null;
895
+ $nextRow = null;
896
+ nextKind = null;
897
+ nextStart = null;
898
+ nextEnd = null;
899
+ }
900
+
901
+ if (isNaN(start)) {
902
+ if (prevKind == null) {
903
+ // The previous row was probably inserted, and user has not yet selected a kind
904
+ // automatically set it to captions
905
+ prevKind = 'captions';
906
+ $prevRow.attr('class','kind-captions');
907
+ $prevRow.find('td').eq(1).html('captions');
908
+ }
909
+ // Current row has no start time (i.e., it's an inserted row)
910
+ if (prevKind === 'captions') {
911
+ // start the new row immediately after the captions end
912
+ start = (parseFloat(prevEnd) + .001).toFixed(3);
913
+ if (nextStart) {
914
+ // end the new row immediately before the next row starts
915
+ end = (parseFloat(nextStart) - .001).toFixed(3);
916
+ }
917
+ else {
918
+ // this is the last row. Use minDuration to calculate end time.
919
+ end = (parseFloat(start) + minDuration[kind]).toFixed(3);
920
+ }
921
+ }
922
+ else if (prevKind === 'chapters') {
923
+ // start the new row immediately after the chapter start (not end)
924
+ start = (parseFloat(prevStart) + .001).toFixed(3);
925
+ if (nextStart) {
926
+ // end the new row immediately before the next row starts
927
+ end = (parseFloat(nextStart) - .001).toFixed(3);
928
+ }
929
+ else {
930
+ // this is the last row. Use minDuration to calculate end time.
931
+ end = (parseFloat(start) + minDurartion[kind]).toFixed(3);
932
+ }
933
+ }
934
+ else if (prevKind === 'descriptions') {
935
+ // start the new row minDuration['descriptions'] after the description starts
936
+ // this will theoretically allow at least a small cushion for the description to be read
937
+ start = (parseFloat(prevStart) + minDuration['descriptions']).toFixed(3);
938
+ end = (parseFloat(start) + minDuration['descriptions']).toFixed(3);
939
+ }
940
+ }
941
+ else {
942
+ // current row has a start time (i.e., an existing row has been moved))
943
+ if (prevStart) {
944
+ // this is not the first row.
945
+ if (prevStart < start) {
946
+ if (start < nextStart) {
947
+ // No change is necessary
948
+ }
949
+ else {
950
+ // nextStart needs to be incremented
951
+ nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
952
+ nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
953
+ // TODO: Ensure nextEnd does not exceed the following start (nextNextStart)
954
+ // Or... maybe this is getting too complicated and should be left up to the user
955
+ }
956
+ }
957
+ else {
958
+ // start needs to be incremented
959
+ start = (parseFloat(prevStart) + minDuration[prevKind]).toFixed(3);
960
+ end = (parseFloat(start) + minDuration[kind]).toFixed(3);
961
+ }
962
+ }
963
+ else {
964
+ // this is the first row
965
+ if (start < nextStart) {
966
+ // No change is necessary
967
+ }
968
+ else {
969
+ // nextStart needs to be incremented
970
+ nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
971
+ nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
972
+ }
973
+ }
974
+ }
975
+
976
+ // check to be sure there is sufficient duration between new start & end times
977
+ if (end - start < minDuration[kind]) {
978
+ // duration is too short. Change end time
979
+ end = (parseFloat(start) + minDuration[kind]).toFixed(3);
980
+ if (nextStart) {
981
+ // this is not the last row
982
+ // increase start time of next row
983
+ nextStart = (parseFloat(end) + .001).toFixed(3);
984
+ }
985
+ }
986
+
987
+ // Update all affected start/end times
988
+ $row.find('td').eq(2).text(this.formatSecondsAsColonTime(start,true));
989
+ $row.find('td').eq(3).text(this.formatSecondsAsColonTime(end,true));
990
+ if ($prevRow) {
991
+ $prevRow.find('td').eq(2).text(this.formatSecondsAsColonTime(prevStart,true));
992
+ $prevRow.find('td').eq(3).text(this.formatSecondsAsColonTime(prevEnd,true));
993
+ }
994
+ if ($nextRow) {
995
+ $nextRow.find('td').eq(2).text(this.formatSecondsAsColonTime(nextStart,true));
996
+ $nextRow.find('td').eq(3).text(this.formatSecondsAsColonTime(nextEnd,true));
997
+ }
998
+ };
999
+
1000
+ AblePlayer.prototype.getKindFromClass = function(myclass) {
1001
+
1002
+ // This function is called when a class with prefix "kind-" is found in the class attribute
1003
+ // TODO: Rewrite this using regular expressions
1004
+ var kindStart, kindEnd, kindLength, kind;
1005
+
1006
+ kindStart = myclass.indexOf('kind-')+5;
1007
+ kindEnd = myclass.indexOf(' ',kindStart);
1008
+ if (kindEnd == -1) {
1009
+ // no spaces found, "kind-" must be the only myclass
1010
+ kindLength = myclass.length - kindStart;
1011
+ }
1012
+ else {
1013
+ kindLength = kindEnd - kindStart;
1014
+ }
1015
+ kind = myclass.substr(kindStart,kindLength);
1016
+ return kind;
1017
+ };
1018
+
1019
+ AblePlayer.prototype.showVtsAlert = function(message) {
1020
+
1021
+ // this is distinct from greater Able Player showAlert()
1022
+ // because it's positioning needs are unique
1023
+ // For now, alertDiv is fixed at top left of screen
1024
+ // but could ultimately be modified to appear near the point of action in the VTS table
1025
+ this.$vtsAlert.text(message).show().delay(3000).fadeOut('slow');
1026
+ };
1027
+
1028
+ AblePlayer.prototype.parseVtsOutput = function($table) {
1029
+
1030
+ // parse table into arrays, then into WebVTT content, for each kind
1031
+ // Display the WebVTT content in textarea fields for users to copy and paste
1032
+ var lang, i, kinds, kind, vtt, $rows, start, end, content, $output;
1033
+
1034
+ lang = $table.attr('lang');
1035
+ kinds = ['captions','chapters','descriptions','subtitles'];
1036
+ vtt = {};
1037
+ for (i=0; i < kinds.length; i++) {
1038
+ kind = kinds[i];
1039
+ vtt[kind] = 'WEBVTT' + "\n\n";
1040
+ }
1041
+ $rows = $table.find('tr');
1042
+ if ($rows.length > 0) {
1043
+ for (i=0; i < $rows.length; i++) {
1044
+ kind = $rows.eq(i).find('td').eq(1).text();
1045
+ if ($.inArray(kind,kinds) !== -1) {
1046
+ start = $rows.eq(i).find('td').eq(2).text();
1047
+ end = $rows.eq(i).find('td').eq(3).text();
1048
+ content = $rows.eq(i).find('td').eq(4).text();
1049
+ if (start !== undefined && end !== undefined) {
1050
+ vtt[kind] += start + ' --> ' + end + "\n";
1051
+ if (content !== 'undefined') {
1052
+ vtt[kind] += content;
1053
+ }
1054
+ vtt[kind] += "\n\n";
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ $output = $('<div>',{
1060
+ 'id': 'able-vts-output'
1061
+ })
1062
+ $('#able-vts').append($output);
1063
+ for (i=0; i < kinds.length; i++) {
1064
+ kind = kinds[i];
1065
+ if (vtt[kind].length > 8) {
1066
+ // some content has been added
1067
+ this.showWebVttOutput(kind,vtt[kind],lang)
1068
+ }
1069
+ }
1070
+ };
1071
+
1072
+ AblePlayer.prototype.showWebVttOutput = function(kind,vttString,lang) {
1073
+
1074
+ var $heading, filename, $p, pText, $textarea;
1075
+
1076
+ $heading = $('<h3>').text(kind.charAt(0).toUpperCase() + kind.slice(1));
1077
+ filename = this.getFilenameFromTracks(kind,lang);
1078
+ pText = 'If you made changes, copy/paste the following content ';
1079
+ if (filename) {
1080
+ pText += 'to replace the original content of your ' + this.getLanguageName(lang) + ' ';
1081
+ pText += '<em>' + kind + '</em> WebVTT file (<strong>' + filename + '</strong>).';
1082
+ }
1083
+ else {
1084
+ pText += 'into a new ' + this.getLanguageName(lang) + ' <em>' + kind + '</em> WebVTT file.';
1085
+ }
1086
+ $p = $('<p>',{
1087
+ 'class': 'able-vts-output-instructions'
1088
+ }).html(pText);
1089
+ $textarea = $('<textarea>').text(vttString);
1090
+ $('#able-vts-output').append($heading,$p,$textarea);
1091
+ };
1092
+
1093
+ })(jQuery);