wai-website-theme 1.3.1 → 1.4

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/_includes/different.html +2 -1
  3. data/_includes/external.html +2 -1
  4. data/_includes/header.html +2 -1
  5. data/_includes/menuitem.html +6 -2
  6. data/_includes/peoplelist.html +21 -0
  7. data/_includes/prevnext-navigation.html +56 -0
  8. data/_includes/{prevnext.html → prevnext-order.html} +9 -0
  9. data/_includes/translation-note-msg.html +5 -3
  10. data/_includes/video-player.html +2 -2
  11. data/_layouts/default.html +8 -1
  12. data/_layouts/news.html +7 -1
  13. data/_layouts/policy.html +7 -1
  14. data/_layouts/sidenav.html +8 -1
  15. data/_layouts/sidenavsidebar.html +8 -1
  16. data/assets/ableplayer/Gruntfile.js +2 -1
  17. data/assets/ableplayer/README.md +158 -85
  18. data/assets/ableplayer/build/ableplayer.dist.js +15445 -13823
  19. data/assets/ableplayer/build/ableplayer.js +15445 -13823
  20. data/assets/ableplayer/build/ableplayer.min.css +1 -2
  21. data/assets/ableplayer/build/ableplayer.min.js +3 -10
  22. data/assets/ableplayer/package-lock.json +944 -346
  23. data/assets/ableplayer/package.json +8 -8
  24. data/assets/ableplayer/scripts/ableplayer-base.js +515 -524
  25. data/assets/ableplayer/scripts/browser.js +158 -158
  26. data/assets/ableplayer/scripts/buildplayer.js +1750 -1682
  27. data/assets/ableplayer/scripts/caption.js +424 -401
  28. data/assets/ableplayer/scripts/chapters.js +259 -259
  29. data/assets/ableplayer/scripts/control.js +1831 -1594
  30. data/assets/ableplayer/scripts/description.js +333 -256
  31. data/assets/ableplayer/scripts/dialog.js +145 -145
  32. data/assets/ableplayer/scripts/dragdrop.js +746 -749
  33. data/assets/ableplayer/scripts/event.js +875 -696
  34. data/assets/ableplayer/scripts/initialize.js +819 -912
  35. data/assets/ableplayer/scripts/langs.js +979 -743
  36. data/assets/ableplayer/scripts/metadata.js +124 -124
  37. data/assets/ableplayer/scripts/misc.js +170 -137
  38. data/assets/ableplayer/scripts/preference.js +904 -904
  39. data/assets/ableplayer/scripts/search.js +172 -172
  40. data/assets/ableplayer/scripts/sign.js +82 -78
  41. data/assets/ableplayer/scripts/slider.js +449 -448
  42. data/assets/ableplayer/scripts/track.js +409 -309
  43. data/assets/ableplayer/scripts/transcript.js +684 -595
  44. data/assets/ableplayer/scripts/translation.js +63 -67
  45. data/assets/ableplayer/scripts/ttml2webvtt.js +85 -85
  46. data/assets/ableplayer/scripts/vimeo.js +448 -0
  47. data/assets/ableplayer/scripts/volume.js +395 -380
  48. data/assets/ableplayer/scripts/vts.js +1077 -1077
  49. data/assets/ableplayer/scripts/webvtt.js +766 -763
  50. data/assets/ableplayer/scripts/youtube.js +695 -478
  51. data/assets/ableplayer/styles/ableplayer.css +54 -46
  52. data/assets/ableplayer/translations/nl.js +54 -54
  53. data/assets/ableplayer/translations/pt-br.js +311 -0
  54. data/assets/ableplayer/translations/tr.js +311 -0
  55. data/assets/ableplayer/translations/zh-tw.js +1 -1
  56. data/assets/css/style.css +1 -1
  57. data/assets/css/style.css.map +1 -1
  58. data/assets/images/icons.svg +5 -5
  59. data/assets/scripts/main.js +7 -0
  60. data/assets/search/tipuesearch.js +3 -3
  61. metadata +8 -3
@@ -4,1090 +4,1090 @@
4
4
  */
5
5
 
6
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'
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
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
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'
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
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
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
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
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
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
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
- };
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
1092
 
1093
1093
  })(jQuery);