wai-website-theme 1.3.1 → 1.4

Sign up to get free protection for your applications and to get access to all the features.
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);