wai-website-theme 0.1.0

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 (173) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +52 -0
  4. data/_data/lang.json +730 -0
  5. data/_data/techniques.yml +180 -0
  6. data/_data/wcag.yml +125 -0
  7. data/_includes/.DS_Store +0 -0
  8. data/_includes/body-class.html +1 -0
  9. data/_includes/box.html +10 -0
  10. data/_includes/excol.html +13 -0
  11. data/_includes/footer.html +40 -0
  12. data/_includes/head.html +23 -0
  13. data/_includes/header.html +59 -0
  14. data/_includes/icon.html +6 -0
  15. data/_includes/img.html +17 -0
  16. data/_includes/multilang-list-policy-links.html +29 -0
  17. data/_includes/multilang-list.html +35 -0
  18. data/_includes/multilang-policy-title.html +5 -0
  19. data/_includes/multilang-title-full.html +1 -0
  20. data/_includes/multilang-title.html +1 -0
  21. data/_includes/navlist.html +22 -0
  22. data/_includes/notes.html +2 -0
  23. data/_includes/prevnext.html +34 -0
  24. data/_includes/resources.html +19 -0
  25. data/_includes/sidenav.html +65 -0
  26. data/_includes/sidenote.html +14 -0
  27. data/_includes/toc.html +10 -0
  28. data/_includes/video-player.html +99 -0
  29. data/_layouts/default.html +26 -0
  30. data/_layouts/home.html +14 -0
  31. data/_layouts/news.html +21 -0
  32. data/_layouts/none.html +1 -0
  33. data/_layouts/policy.html +72 -0
  34. data/_layouts/sidenav.html +27 -0
  35. data/_layouts/sidenavsidebar.html +22 -0
  36. data/assets/ableplayer/.gitattributes +14 -0
  37. data/assets/ableplayer/.gitignore +7 -0
  38. data/assets/ableplayer/Gruntfile.js +105 -0
  39. data/assets/ableplayer/LICENSE +26 -0
  40. data/assets/ableplayer/README.md +656 -0
  41. data/assets/ableplayer/build/ableplayer.dist.js +12157 -0
  42. data/assets/ableplayer/build/ableplayer.js +12157 -0
  43. data/assets/ableplayer/build/ableplayer.min.css +2 -0
  44. data/assets/ableplayer/build/ableplayer.min.js +8 -0
  45. data/assets/ableplayer/button-icons/able-icons.svg +116 -0
  46. data/assets/ableplayer/button-icons/black/captions.png +0 -0
  47. data/assets/ableplayer/button-icons/black/chapters.png +0 -0
  48. data/assets/ableplayer/button-icons/black/close.png +0 -0
  49. data/assets/ableplayer/button-icons/black/descriptions.png +0 -0
  50. data/assets/ableplayer/button-icons/black/ellipsis.png +0 -0
  51. data/assets/ableplayer/button-icons/black/faster.png +0 -0
  52. data/assets/ableplayer/button-icons/black/forward.png +0 -0
  53. data/assets/ableplayer/button-icons/black/fullscreen-collapse.png +0 -0
  54. data/assets/ableplayer/button-icons/black/fullscreen-expand.png +0 -0
  55. data/assets/ableplayer/button-icons/black/help.png +0 -0
  56. data/assets/ableplayer/button-icons/black/next.png +0 -0
  57. data/assets/ableplayer/button-icons/black/pause.png +0 -0
  58. data/assets/ableplayer/button-icons/black/pipe.png +0 -0
  59. data/assets/ableplayer/button-icons/black/play.png +0 -0
  60. data/assets/ableplayer/button-icons/black/preferences.png +0 -0
  61. data/assets/ableplayer/button-icons/black/previous.png +0 -0
  62. data/assets/ableplayer/button-icons/black/rabbit.png +0 -0
  63. data/assets/ableplayer/button-icons/black/restart.png +0 -0
  64. data/assets/ableplayer/button-icons/black/rewind.png +0 -0
  65. data/assets/ableplayer/button-icons/black/sign.png +0 -0
  66. data/assets/ableplayer/button-icons/black/slower.png +0 -0
  67. data/assets/ableplayer/button-icons/black/stop.png +0 -0
  68. data/assets/ableplayer/button-icons/black/transcript.png +0 -0
  69. data/assets/ableplayer/button-icons/black/turtle.png +0 -0
  70. data/assets/ableplayer/button-icons/black/volume-loud.png +0 -0
  71. data/assets/ableplayer/button-icons/black/volume-medium.png +0 -0
  72. data/assets/ableplayer/button-icons/black/volume-mute.png +0 -0
  73. data/assets/ableplayer/button-icons/black/volume-soft.png +0 -0
  74. data/assets/ableplayer/button-icons/fonts/able.eot +0 -0
  75. data/assets/ableplayer/button-icons/fonts/able.svg +40 -0
  76. data/assets/ableplayer/button-icons/fonts/able.ttf +0 -0
  77. data/assets/ableplayer/button-icons/fonts/able.woff +0 -0
  78. data/assets/ableplayer/button-icons/white/captions.png +0 -0
  79. data/assets/ableplayer/button-icons/white/chapters.png +0 -0
  80. data/assets/ableplayer/button-icons/white/close.png +0 -0
  81. data/assets/ableplayer/button-icons/white/descriptions.png +0 -0
  82. data/assets/ableplayer/button-icons/white/ellipsis.png +0 -0
  83. data/assets/ableplayer/button-icons/white/faster.png +0 -0
  84. data/assets/ableplayer/button-icons/white/forward.png +0 -0
  85. data/assets/ableplayer/button-icons/white/fullscreen-collapse.png +0 -0
  86. data/assets/ableplayer/button-icons/white/fullscreen-expand.png +0 -0
  87. data/assets/ableplayer/button-icons/white/help.png +0 -0
  88. data/assets/ableplayer/button-icons/white/next.png +0 -0
  89. data/assets/ableplayer/button-icons/white/pause.png +0 -0
  90. data/assets/ableplayer/button-icons/white/pipe.png +0 -0
  91. data/assets/ableplayer/button-icons/white/play.png +0 -0
  92. data/assets/ableplayer/button-icons/white/preferences.png +0 -0
  93. data/assets/ableplayer/button-icons/white/previous.png +0 -0
  94. data/assets/ableplayer/button-icons/white/rabbit.png +0 -0
  95. data/assets/ableplayer/button-icons/white/restart.png +0 -0
  96. data/assets/ableplayer/button-icons/white/rewind.png +0 -0
  97. data/assets/ableplayer/button-icons/white/sign.png +0 -0
  98. data/assets/ableplayer/button-icons/white/slower.png +0 -0
  99. data/assets/ableplayer/button-icons/white/stop.png +0 -0
  100. data/assets/ableplayer/button-icons/white/transcript.png +0 -0
  101. data/assets/ableplayer/button-icons/white/turtle.png +0 -0
  102. data/assets/ableplayer/button-icons/white/volume-loud.png +0 -0
  103. data/assets/ableplayer/button-icons/white/volume-medium.png +0 -0
  104. data/assets/ableplayer/button-icons/white/volume-mute.png +0 -0
  105. data/assets/ableplayer/button-icons/white/volume-soft.png +0 -0
  106. data/assets/ableplayer/images/wingrip.png +0 -0
  107. data/assets/ableplayer/package.json +22 -0
  108. data/assets/ableplayer/scripts/JQuery.doWhen.js +113 -0
  109. data/assets/ableplayer/scripts/ableplayer-base.js +440 -0
  110. data/assets/ableplayer/scripts/browser.js +162 -0
  111. data/assets/ableplayer/scripts/buildplayer.js +1609 -0
  112. data/assets/ableplayer/scripts/caption.js +385 -0
  113. data/assets/ableplayer/scripts/chapters.js +242 -0
  114. data/assets/ableplayer/scripts/control.js +1514 -0
  115. data/assets/ableplayer/scripts/description.js +283 -0
  116. data/assets/ableplayer/scripts/dialog.js +147 -0
  117. data/assets/ableplayer/scripts/dragdrop.js +766 -0
  118. data/assets/ableplayer/scripts/event.js +595 -0
  119. data/assets/ableplayer/scripts/initialize.js +725 -0
  120. data/assets/ableplayer/scripts/langs.js +750 -0
  121. data/assets/ableplayer/scripts/metadata.js +134 -0
  122. data/assets/ableplayer/scripts/misc.js +72 -0
  123. data/assets/ableplayer/scripts/preference.js +909 -0
  124. data/assets/ableplayer/scripts/search.js +171 -0
  125. data/assets/ableplayer/scripts/sign.js +92 -0
  126. data/assets/ableplayer/scripts/slider.js +454 -0
  127. data/assets/ableplayer/scripts/track.js +296 -0
  128. data/assets/ableplayer/scripts/transcript.js +590 -0
  129. data/assets/ableplayer/scripts/translation.js +66 -0
  130. data/assets/ableplayer/scripts/volume.js +383 -0
  131. data/assets/ableplayer/scripts/webvtt.js +765 -0
  132. data/assets/ableplayer/scripts/youtube.js +471 -0
  133. data/assets/ableplayer/styles/ableplayer.css +1241 -0
  134. data/assets/ableplayer/thirdparty/js.cookie.js +145 -0
  135. data/assets/ableplayer/thirdparty/modernizr.custom.js +4 -0
  136. data/assets/ableplayer/translations/ca.js +1 -0
  137. data/assets/ableplayer/translations/de.js +1 -0
  138. data/assets/ableplayer/translations/en.js +305 -0
  139. data/assets/ableplayer/translations/es.js +305 -0
  140. data/assets/ableplayer/translations/fr.js +305 -0
  141. data/assets/ableplayer/translations/it.js +303 -0
  142. data/assets/ableplayer/translations/ja.js +305 -0
  143. data/assets/ableplayer/translations/nl.js +305 -0
  144. data/assets/css/style.css +4360 -0
  145. data/assets/css/style.css.map +1 -0
  146. data/assets/fonts/anonymouspro-bold.woff +0 -0
  147. data/assets/fonts/anonymouspro-bold.woff2 +0 -0
  148. data/assets/fonts/anonymouspro-bolditalic.woff +0 -0
  149. data/assets/fonts/anonymouspro-bolditalic.woff2 +0 -0
  150. data/assets/fonts/anonymouspro-italic.woff +0 -0
  151. data/assets/fonts/anonymouspro-italic.woff2 +0 -0
  152. data/assets/fonts/anonymouspro-regular.woff +0 -0
  153. data/assets/fonts/anonymouspro-regular.woff2 +0 -0
  154. data/assets/fonts/notosans-bold.woff +0 -0
  155. data/assets/fonts/notosans-bold.woff2 +0 -0
  156. data/assets/fonts/notosans-bolditalic.woff +0 -0
  157. data/assets/fonts/notosans-bolditalic.woff2 +0 -0
  158. data/assets/fonts/notosans-italic.woff +0 -0
  159. data/assets/fonts/notosans-italic.woff2 +0 -0
  160. data/assets/fonts/notosans-regular.woff +0 -0
  161. data/assets/fonts/notosans-regular.woff2 +0 -0
  162. data/assets/images/.DS_Store +0 -0
  163. data/assets/images/Shape.svg +10 -0
  164. data/assets/images/icon-related-content.svg +14 -0
  165. data/assets/images/icons.svg +126 -0
  166. data/assets/images/teaser-image@1x.jpg +0 -0
  167. data/assets/images/teaser-image@2x.jpg +0 -0
  168. data/assets/images/w3c.sketch +0 -0
  169. data/assets/images/w3c.svg +10 -0
  170. data/assets/scripts/jquery.min.js +4 -0
  171. data/assets/scripts/main.js +208 -0
  172. data/assets/scripts/svg4everybody.js +1 -0
  173. metadata +257 -0
@@ -0,0 +1,1609 @@
1
+ (function ($) {
2
+
3
+ AblePlayer.prototype.injectPlayerCode = function() {
4
+
5
+ // create and inject surrounding HTML structure
6
+ // If IOS:
7
+ // If video:
8
+ // IOS does not support any of the player's functionality
9
+ // - everything plays in its own player
10
+ // Therefore, AblePlayer is not loaded & all functionality is disabled
11
+ // (this all determined. If this is IOS && video, this function is never called)
12
+ // If audio:
13
+ // HTML cannot be injected as a *parent* of the <audio> element
14
+ // It is therefore injected *after* the <audio> element
15
+ // This is only a problem in IOS 6 and earlier,
16
+ // & is a known bug, fixed in IOS 7
17
+
18
+ var thisObj, vidcapContainer, prefsGroups, i;
19
+ thisObj = this;
20
+
21
+ // create three wrappers and wrap them around the media element. From inner to outer:
22
+ // $mediaContainer - contains the original media element
23
+ // $ableDiv - contains the media player and all its objects (e.g., captions, controls, descriptions)
24
+ // $ableWrapper - contains additional widgets (e.g., transcript window, sign window)
25
+ this.$mediaContainer = this.$media.wrap('<div class="able-media-container"></div>').parent();
26
+ this.$ableDiv = this.$mediaContainer.wrap('<div class="able"></div>').parent();
27
+ this.$ableWrapper = this.$ableDiv.wrap('<div class="able-wrapper"></div>').parent();
28
+ if (this.player !== 'youtube') {
29
+ this.$ableWrapper.css({
30
+ 'max-width': this.playerMaxWidth + 'px'
31
+ });
32
+ }
33
+
34
+ this.injectOffscreenHeading();
35
+
36
+ // youtube adds its own big play button
37
+ // if (this.mediaType === 'video' && this.player !== 'youtube') {
38
+ if (this.mediaType === 'video') {
39
+ if (this.iconType == 'font' && this.player !== 'youtube') {
40
+ this.injectBigPlayButton();
41
+ }
42
+
43
+ // add container that captions or description will be appended to
44
+ // Note: new Jquery object must be assigned _after_ wrap, hence the temp vidcapContainer variable
45
+ vidcapContainer = $('<div>',{
46
+ 'class' : 'able-vidcap-container'
47
+ });
48
+ this.$vidcapContainer = this.$mediaContainer.wrap(vidcapContainer).parent();
49
+ }
50
+
51
+ this.injectPlayerControlArea();
52
+ this.injectTextDescriptionArea();
53
+
54
+ if (this.transcriptType) {
55
+ if (this.transcriptType === 'popup' || this.transcriptType === 'external') {
56
+ this.injectTranscriptArea();
57
+ }
58
+ else if (this.transcriptType === 'manual') {
59
+ this.setupManualTranscript();
60
+ }
61
+ this.addTranscriptAreaEvents();
62
+ }
63
+
64
+ this.injectAlert();
65
+ this.injectPlaylist();
66
+ };
67
+
68
+ AblePlayer.prototype.injectOffscreenHeading = function () {
69
+ // Add offscreen heading to the media container.
70
+ // The heading injected in $ableDiv is one level deeper than the closest parent heading
71
+ // as determined by getNextHeadingLevel()
72
+ var headingType;
73
+ this.playerHeadingLevel = this.getNextHeadingLevel(this.$ableDiv); // returns in integer 1-6
74
+ headingType = 'h' + this.playerHeadingLevel.toString();
75
+ this.$headingDiv = $('<' + headingType + '>');
76
+ this.$ableDiv.prepend(this.$headingDiv);
77
+ this.$headingDiv.addClass('able-offscreen');
78
+ this.$headingDiv.text(this.tt.playerHeading);
79
+ };
80
+
81
+ AblePlayer.prototype.injectBigPlayButton = function () {
82
+
83
+ this.$bigPlayButton = $('<button>', {
84
+ 'class': 'able-big-play-button icon-play',
85
+ 'aria-hidden': true,
86
+ 'tabindex': -1
87
+ });
88
+
89
+ var thisObj = this;
90
+ this.$bigPlayButton.click(function () {
91
+ thisObj.handlePlay();
92
+ });
93
+
94
+ this.$mediaContainer.append(this.$bigPlayButton);
95
+ };
96
+
97
+ AblePlayer.prototype.injectPlayerControlArea = function () {
98
+ this.$playerDiv = $('<div>', {
99
+ 'class' : 'able-player',
100
+ 'role' : 'region',
101
+ 'aria-label' : this.mediaType + ' player'
102
+ });
103
+ this.$playerDiv.addClass('able-'+this.mediaType);
104
+
105
+ // The default skin depends a bit on a Now Playing div
106
+ // so go ahead and add one
107
+ // However, it's only populated if this.showNowPlaying = true
108
+ this.$nowPlayingDiv = $('<div>',{
109
+ 'class' : 'able-now-playing',
110
+ 'aria-live' : 'assertive',
111
+ 'aria-atomic': 'true'
112
+ });
113
+
114
+ this.$controllerDiv = $('<div>',{
115
+ 'class' : 'able-controller'
116
+ });
117
+ this.$controllerDiv.addClass('able-' + this.iconColor + '-controls');
118
+
119
+ this.$statusBarDiv = $('<div>',{
120
+ 'class' : 'able-status-bar'
121
+ });
122
+ this.$timer = $('<span>',{
123
+ 'class' : 'able-timer'
124
+ });
125
+ this.$elapsedTimeContainer = $('<span>',{
126
+ 'class': 'able-elapsedTime',
127
+ text: '0:00'
128
+ });
129
+ this.$durationContainer = $('<span>',{
130
+ 'class': 'able-duration'
131
+ });
132
+ this.$timer.append(this.$elapsedTimeContainer).append(this.$durationContainer);
133
+
134
+ this.$speed = $('<span>',{
135
+ 'class' : 'able-speed',
136
+ 'aria-live' : 'assertive'
137
+ }).text(this.tt.speed + ': 1x');
138
+
139
+ this.$status = $('<span>',{
140
+ 'class' : 'able-status',
141
+ 'aria-live' : 'polite'
142
+ });
143
+
144
+ // Put everything together.
145
+ this.$statusBarDiv.append(this.$timer, this.$speed, this.$status);
146
+ this.$playerDiv.append(this.$nowPlayingDiv, this.$controllerDiv, this.$statusBarDiv);
147
+ this.$ableDiv.append(this.$playerDiv);
148
+ };
149
+
150
+ AblePlayer.prototype.injectTextDescriptionArea = function () {
151
+
152
+ // create a div for exposing description
153
+ // description will be exposed via role="alert" & announced by screen readers
154
+ this.$descDiv = $('<div>',{
155
+ 'class': 'able-descriptions',
156
+ 'aria-live': 'assertive',
157
+ 'aria-atomic': 'true'
158
+ });
159
+ // Start off with description hidden.
160
+ // It will be exposed conditionally within description.js > initDescription()
161
+ this.$descDiv.hide();
162
+ this.$ableDiv.append(this.$descDiv);
163
+ };
164
+
165
+ AblePlayer.prototype.getDefaultWidth = function(which) {
166
+
167
+ // return default width of resizable elements
168
+ // these values are somewhat arbitrary, but seem to result in good usability
169
+ // if users disagree, they can resize (and resposition) them
170
+ if (which === 'transcript') {
171
+ return 450;
172
+ }
173
+ else if (which === 'sign') {
174
+ return 400;
175
+ }
176
+ };
177
+
178
+ AblePlayer.prototype.positionDraggableWindow = function (which, width) {
179
+
180
+ // which is either 'transcript' or 'sign'
181
+
182
+ var cookie, cookiePos, $window, dragged, windowPos, currentWindowPos, firstTime, zIndex;
183
+
184
+ cookie = this.getCookie();
185
+ if (which === 'transcript') {
186
+ $window = this.$transcriptArea;
187
+ if (typeof cookie.transcript !== 'undefined') {
188
+ cookiePos = cookie.transcript;
189
+ }
190
+ }
191
+ else if (which === 'sign') {
192
+ $window = this.$signWindow;
193
+ if (typeof cookie.transcript !== 'undefined') {
194
+ cookiePos = cookie.sign;
195
+ }
196
+ }
197
+ if (typeof cookiePos !== 'undefined' && !($.isEmptyObject(cookiePos))) {
198
+ // position window using stored values from cookie
199
+ $window.css({
200
+ 'position': cookiePos['position'],
201
+ 'width': cookiePos['width'],
202
+ 'z-index': cookiePos['zindex']
203
+ });
204
+ if (cookiePos['position'] === 'absolute') {
205
+ $window.css({
206
+ 'top': cookiePos['top'],
207
+ 'left': cookiePos['left']
208
+ });
209
+ }
210
+ // since cookie is not page-specific, z-index needs may vary across different pages
211
+ this.updateZIndex(which);
212
+ }
213
+ else {
214
+ // position window using default values
215
+ windowPos = this.getOptimumPosition(which, width);
216
+ if (typeof width === 'undefined') {
217
+ width = this.getDefaultWidth(which);
218
+ }
219
+ $window.css({
220
+ 'position': windowPos[0],
221
+ 'width': width,
222
+ 'z-index': windowPos[3]
223
+ });
224
+ if (windowPos[0] === 'absolute') {
225
+ $window.css({
226
+ 'top': windowPos[1] + 'px',
227
+ 'left': windowPos[2] + 'px',
228
+ });
229
+ }
230
+ }
231
+ };
232
+
233
+ AblePlayer.prototype.getOptimumPosition = function (targetWindow, targetWidth) {
234
+
235
+ // returns optimum position for targetWindow, as an array with the following structure:
236
+ // 0 - CSS position ('absolute' or 'relative')
237
+ // 1 - top
238
+ // 2 - left
239
+ // 3 - zindex (if not default)
240
+ // targetWindow is either 'transcript' or 'sign'
241
+ // if there is room to the right of the player, position element there
242
+ // else if there is room the left of the player, position element there
243
+ // else position element beneath player
244
+
245
+ var gap, position, ableWidth, ableHeight, ableOffset, ableTop, ableLeft,
246
+ windowWidth, otherWindowWidth, zIndex;
247
+
248
+ if (typeof targetWidth === 'undefined') {
249
+ targetWidth = this.getDefaultWidth(targetWindow);
250
+ }
251
+
252
+ gap = 5; // number of pixels to preserve between Able Player objects
253
+
254
+ position = []; // position, top, left
255
+
256
+ ableWidth = this.$ableDiv.width();
257
+ ableHeight = this.$ableDiv.height();
258
+ ableOffset = this.$ableDiv.offset();
259
+ ableTop = ableOffset.top;
260
+ ableLeft = ableOffset.left;
261
+ windowWidth = $(window).width();
262
+ otherWindowWidth = 0; // width of other visiable draggable windows will be added to this
263
+
264
+ if (targetWindow === 'transcript') {
265
+ if (typeof this.$signWindow !== 'undefined') {
266
+ if (this.$signWindow.is(':visible')) {
267
+ otherWindowWidth = this.$signWindow.width() + gap;
268
+ }
269
+ }
270
+ }
271
+ else if (targetWindow === 'sign') {
272
+ if (typeof this.$transcriptArea !== 'undefined') {
273
+ if (this.$transcriptArea.is(':visible')) {
274
+ otherWindowWidth = this.$transcriptArea.width() + gap;
275
+ }
276
+ }
277
+ }
278
+ if (targetWidth < (windowWidth - (ableLeft + ableWidth + gap + otherWindowWidth))) {
279
+ // there's room to the left of $ableDiv
280
+ position[0] = 'absolute';
281
+ position[1] = 0;
282
+ position[2] = ableWidth + otherWindowWidth + gap;
283
+ }
284
+ else if (targetWidth + gap < ableLeft) {
285
+ // there's room to the right of $ableDiv
286
+ position[0] = 'absolute';
287
+ position[1] = 0;
288
+ position[2] = ableLeft - targetWidth - gap;
289
+ }
290
+ else {
291
+ // position element below $ableDiv
292
+ position[0] = 'relative';
293
+ // no need to define top, left, or z-index
294
+ }
295
+ return position;
296
+ };
297
+
298
+ AblePlayer.prototype.injectPoster = function ($element, context) {
299
+
300
+ // get poster attribute from media element and append that as an img to $element
301
+ // context is either 'youtube' or 'fallback'
302
+ var poster, width, height;
303
+
304
+ if (context === 'youtube') {
305
+ if (typeof this.ytWidth !== 'undefined') {
306
+ width = this.ytWidth;
307
+ height = this.ytHeight;
308
+ }
309
+ else if (typeof this.playerMaxWidth !== 'undefined') {
310
+ width = this.playerMaxWidth;
311
+ height = this.playerMaxHeight;
312
+ }
313
+ else if (typeof this.playerWidth !== 'undefined') {
314
+ width = this.playerWidth;
315
+ height = this.playerHeight;
316
+ }
317
+ }
318
+ else if (context === 'fallback') {
319
+ width = '100%';
320
+ height = 'auto';
321
+ }
322
+
323
+ if (this.$media.attr('poster')) {
324
+ poster = this.$media.attr('poster');
325
+ this.$posterImg = $('<img>',{
326
+ 'class': 'able-poster',
327
+ 'src' : poster,
328
+ 'alt' : "",
329
+ 'role': "presentation",
330
+ 'width': width,
331
+ 'height': height
332
+ });
333
+ $element.append(this.$posterImg);
334
+ }
335
+ };
336
+
337
+ AblePlayer.prototype.injectAlert = function () {
338
+
339
+ // inject two alerts, one visible for all users and one for screen reader users only
340
+
341
+ var top;
342
+
343
+ this.$alertBox = $('<div role="alert"></div>');
344
+ this.$alertBox.addClass('able-alert');
345
+ this.$alertBox.appendTo(this.$ableDiv);
346
+ if (this.mediaType == 'audio') {
347
+ top = -10;
348
+ }
349
+ else {
350
+ top = Math.round(this.$mediaContainer.offset().top * 10) / 10;
351
+ }
352
+ this.$alertBox.css({
353
+ top: top + 'px'
354
+ });
355
+
356
+ this.$srAlertBox = $('<div role="alert"></div>');
357
+ this.$srAlertBox.addClass('able-screenreader-alert');
358
+ this.$srAlertBox.appendTo(this.$ableDiv);
359
+ };
360
+
361
+ AblePlayer.prototype.injectPlaylist = function () {
362
+ if (this.playlistEmbed === true) {
363
+ // move playlist into player, immediately before statusBarDiv
364
+ var playlistClone = this.$playlistDom.clone();
365
+ playlistClone.insertBefore(this.$statusBarDiv);
366
+ // Update to the new playlist copy.
367
+ this.$playlist = playlistClone.find('li');
368
+ }
369
+
370
+ if (this.hasPlaylist && this.$sources.length === 0) {
371
+ // no source elements were provided. Construct them from the first playlist item
372
+ this.initializing = true;
373
+ this.swapSource(0);
374
+ // redefine this.$sources now that media contains one or more <source> elements
375
+ this.$sources = this.$media.find('source');
376
+ }
377
+ };
378
+
379
+ // Create popup div and append to player
380
+ // 'which' parameter is either 'captions', 'chapters', 'prefs', or 'X-window' (e.g., "sign-window")
381
+ AblePlayer.prototype.createPopup = function (which) {
382
+
383
+ var thisObj, $popup, $thisButton, $thisListItem, $prevButton, $nextButton,
384
+ selectedTrackIndex, selectedTrack;
385
+ thisObj = this;
386
+ $popup = $('<div>',{
387
+ 'id': this.mediaId + '-' + which + '-menu',
388
+ 'class': 'able-popup'
389
+ });
390
+ if (which === 'chapters' || which === 'prefs' || which === 'sign-window' || which === 'transcript-window') {
391
+ $popup.addClass('able-popup-no-radio');
392
+ }
393
+ $popup.on('keydown',function (e) {
394
+ $thisButton = $(this).find('input:focus');
395
+ $thisListItem = $thisButton.parent();
396
+ if ($thisListItem.is(':first-child')) {
397
+ // this is the first button
398
+ $prevButton = $(this).find('input').last(); // wrap to bottom
399
+ $nextButton = $thisListItem.next().find('input');
400
+ }
401
+ else if ($thisListItem.is(':last-child')) {
402
+ // this is the last button
403
+ $prevButton = $thisListItem.prev().find('input');
404
+ $nextButton = $(this).find('input').first(); // wrap to top
405
+ }
406
+ else {
407
+ $prevButton = $thisListItem.prev().find('input');
408
+ $nextButton = $thisListItem.next().find('input');
409
+ }
410
+ if (e.which === 9) { // Tab
411
+ if (e.shiftKey) {
412
+ $thisListItem.removeClass('able-focus');
413
+ $prevButton.focus();
414
+ $prevButton.parent().addClass('able-focus');
415
+ }
416
+ else {
417
+ $thisListItem.removeClass('able-focus');
418
+ $nextButton.focus();
419
+ $nextButton.parent().addClass('able-focus');
420
+ }
421
+ }
422
+ else if (e.which === 40 || e.which === 39) { // down or right arrow
423
+ $thisListItem.removeClass('able-focus');
424
+ $nextButton.focus();
425
+ $nextButton.parent().addClass('able-focus');
426
+ }
427
+ else if (e.which == 38 || e.which === 37) { // up or left arrow
428
+ $thisListItem.removeClass('able-focus');
429
+ $prevButton.focus();
430
+ $prevButton.parent().addClass('able-focus');
431
+ }
432
+ else if (e.which === 32 || e.which === 13) { // space or enter
433
+ $('input:focus').click();
434
+ }
435
+ else if (e.which === 27) { // Escape
436
+ $thisListItem.removeClass('able-focus');
437
+ thisObj.closePopups();
438
+ }
439
+ e.preventDefault();
440
+ });
441
+ this.$controllerDiv.append($popup);
442
+ return $popup;
443
+ };
444
+
445
+ AblePlayer.prototype.closePopups = function () {
446
+ if (this.chaptersPopup && this.chaptersPopup.is(':visible')) {
447
+ this.chaptersPopup.hide();
448
+ this.$chaptersButton.focus();
449
+ }
450
+ if (this.captionsPopup && this.captionsPopup.is(':visible')) {
451
+ this.captionsPopup.hide();
452
+ this.$ccButton.focus();
453
+ }
454
+ if (this.prefsPopup && this.prefsPopup.is(':visible')) {
455
+ this.prefsPopup.hide();
456
+ this.$prefsButton.focus();
457
+ }
458
+ if (this.$windowPopup && this.$windowPopup.is(':visible')) {
459
+ this.$windowPopup.hide();
460
+ this.$windowButton.show().focus();
461
+ }
462
+ if (this.$volumeSlider && this.$volumeSlider.is(':visible')) {
463
+ this.$volumeSlider.hide().attr('aria-hidden','true');
464
+ this.$volumeAlert.text(this.tt.volumeSliderClosed);
465
+ this.$volumeButton.focus();
466
+ }
467
+ };
468
+
469
+ AblePlayer.prototype.setupPopups = function (which) {
470
+
471
+ // Create and fill in the popup menu forms for various controls.
472
+ // parameter 'which' is passed if refreshing content of an existing popup ('captions' or 'chapters')
473
+
474
+ var popups, thisObj, hasDefault, i, j,
475
+ tracks, trackList, trackItem, track,
476
+ radioName, radioId, trackButton, trackLabel,
477
+ prefCats, prefCat, prefLabel;
478
+
479
+ popups = [];
480
+ if (typeof which === 'undefined') {
481
+ popups.push('prefs');
482
+ }
483
+
484
+ if (which === 'captions' || (typeof which === 'undefined')) {
485
+ if (typeof this.ytCaptions !== 'undefined') { // setup popup for YouTube captions
486
+ if (this.ytCaptions.length) {
487
+ popups.push('ytCaptions');
488
+ }
489
+ }
490
+ else { // setup popup for local captions
491
+ if (this.captions.length > 0) {
492
+ popups.push('captions');
493
+ }
494
+ }
495
+ }
496
+ if (which === 'chapters' || (typeof which === 'undefined')) {
497
+ if (this.chapters.length > 0 && this.useChaptersButton) {
498
+ popups.push('chapters');
499
+ }
500
+ }
501
+ if (popups.length > 0) {
502
+ thisObj = this;
503
+ for (var i=0; i<popups.length; i++) {
504
+ var popup = popups[i];
505
+ hasDefault = false;
506
+ if (popup == 'prefs') {
507
+ this.prefsPopup = this.createPopup('prefs');
508
+ }
509
+ else if (popup == 'captions') {
510
+ if (typeof this.captionsPopup === 'undefined') {
511
+ this.captionsPopup = this.createPopup('captions');
512
+ }
513
+ tracks = this.captions;
514
+ }
515
+ else if (popup == 'chapters') {
516
+ if (typeof this.chaptersPopup === 'undefined') {
517
+ this.chaptersPopup = this.createPopup('chapters');
518
+ }
519
+ if (this.selectedChapters) {
520
+ tracks = this.selectedChapters.cues;
521
+ }
522
+ else if (this.chapters.length >= 1) {
523
+ tracks = this.chapters[0].cues;
524
+ }
525
+ else {
526
+ tracks = [];
527
+ }
528
+ }
529
+ else if (popup == 'ytCaptions') {
530
+ if (typeof this.captionsPopup === 'undefined') {
531
+ this.captionsPopup = this.createPopup('captions');
532
+ }
533
+ tracks = this.ytCaptions;
534
+ }
535
+ var trackList = $('<ul></ul>');
536
+ radioName = this.mediaId + '-' + popup + '-choice';
537
+ if (popup === 'prefs') {
538
+ prefCats = this.getPreferencesGroups();
539
+ for (j = 0; j < prefCats.length; j++) {
540
+ trackItem = $('<li></li>');
541
+ prefCat = prefCats[j];
542
+ if (prefCat === 'captions') {
543
+ prefLabel = this.tt.prefMenuCaptions;
544
+ }
545
+ else if (prefCat === 'descriptions') {
546
+ prefLabel = this.tt.prefMenuDescriptions;
547
+ }
548
+ else if (prefCat === 'keyboard') {
549
+ prefLabel = this.tt.prefMenuKeyboard;
550
+ }
551
+ else if (prefCat === 'transcript') {
552
+ prefLabel = this.tt.prefMenuTranscript;
553
+ }
554
+ radioId = this.mediaId + '-' + popup + '-' + j;
555
+ trackButton = $('<input>',{
556
+ 'type': 'radio',
557
+ 'val': prefCat,
558
+ 'name': radioName,
559
+ 'id': radioId
560
+ });
561
+ trackLabel = $('<label>',{
562
+ 'for': radioId
563
+ });
564
+ trackLabel.text(prefLabel);
565
+ trackButton.click(function(event) {
566
+ var whichPref = $(this).attr('value');
567
+ thisObj.setFullscreen(false);
568
+ if (whichPref === 'captions') {
569
+ thisObj.captionPrefsDialog.show();
570
+ }
571
+ else if (whichPref === 'descriptions') {
572
+ thisObj.descPrefsDialog.show();
573
+ }
574
+ else if (whichPref === 'keyboard') {
575
+ thisObj.keyboardPrefsDialog.show();
576
+ }
577
+ else if (whichPref === 'transcript') {
578
+ thisObj.transcriptPrefsDialog.show();
579
+ }
580
+ thisObj.closePopups();
581
+ });
582
+ trackItem.append(trackButton,trackLabel);
583
+ trackList.append(trackItem);
584
+ }
585
+ this.prefsPopup.append(trackList);
586
+ }
587
+ else {
588
+ for (j = 0; j < tracks.length; j++) {
589
+ trackItem = $('<li></li>');
590
+ track = tracks[j];
591
+ radioId = this.mediaId + '-' + popup + '-' + j;
592
+ trackButton = $('<input>',{
593
+ 'type': 'radio',
594
+ 'val': j,
595
+ 'name': radioName,
596
+ 'id': radioId
597
+ });
598
+ if (track.def) {
599
+ trackButton.prop('checked',true);
600
+ hasDefault = true;
601
+ }
602
+ trackLabel = $('<label>',{
603
+ 'for': radioId
604
+ });
605
+ if (track.language !== 'undefined') {
606
+ trackButton.attr('lang',track.language);
607
+ }
608
+ if (popup == 'captions' || popup == 'ytCaptions') {
609
+ trackLabel.text(track.label || track.language);
610
+ trackButton.click(this.getCaptionClickFunction(track));
611
+ }
612
+ else if (popup == 'chapters') {
613
+ trackLabel.text(this.flattenCueForCaption(track) + ' - ' + this.formatSecondsAsColonTime(track.start));
614
+ var getClickFunction = function (time) {
615
+ return function () {
616
+ thisObj.seekTo(time);
617
+ // stopgap to prevent spacebar in Firefox from reopening popup
618
+ // immediately after closing it (used in handleChapters())
619
+ thisObj.hidingPopup = true;
620
+ thisObj.chaptersPopup.hide();
621
+ // Ensure stopgap gets cancelled if handleChapters() isn't called
622
+ // e.g., if user triggered button with Enter or mouse click, not spacebar
623
+ setTimeout(function() {
624
+ thisObj.hidingPopup = false;
625
+ }, 100);
626
+ thisObj.$chaptersButton.focus();
627
+ }
628
+ }
629
+ trackButton.on('click keypress',getClickFunction(track.start));
630
+ }
631
+ trackItem.append(trackButton,trackLabel);
632
+ trackList.append(trackItem);
633
+ }
634
+ if (popup == 'captions' || popup == 'ytCaptions') {
635
+ // add a captions off button
636
+ radioId = this.mediaId + '-captions-off';
637
+ trackItem = $('<li></li>');
638
+ trackButton = $('<input>',{
639
+ 'type': 'radio',
640
+ 'name': radioName,
641
+ 'id': radioId
642
+ });
643
+ trackLabel = $('<label>',{
644
+ 'for': radioId
645
+ });
646
+ trackLabel.text(this.tt.captionsOff);
647
+ if (this.prefCaptions === 0) {
648
+ trackButton.prop('checked',true);
649
+ }
650
+ trackButton.click(this.getCaptionOffFunction());
651
+ trackItem.append(trackButton,trackLabel);
652
+ trackList.append(trackItem);
653
+ }
654
+ if (!hasDefault) { // no 'default' attribute was specified on any <track>
655
+ if ((popup == 'captions' || popup == 'ytCaptions') && (trackList.find('input:radio[lang=' + this.captionLang + ']'))) {
656
+ // check the button associated with the default caption language
657
+ // (as determined in control.js > syncTrackLanguages())
658
+ trackList.find('input:radio[lang=' + this.captionLang + ']').prop('checked',true);
659
+ }
660
+ else {
661
+ // check the first button
662
+ trackList.find('input').first().prop('checked',true);
663
+ }
664
+ }
665
+ if (popup === 'captions' || popup === 'ytCaptions') {
666
+ this.captionsPopup.html(trackList);
667
+ }
668
+ else if (popup === 'chapters') {
669
+ this.chaptersPopup.html(trackList);
670
+ }
671
+ }
672
+ }
673
+ }
674
+ };
675
+
676
+ AblePlayer.prototype.provideFallback = function(reason) {
677
+
678
+ // provide ultimate fallback for users who are unable to play the media
679
+ // reason is a specific error message
680
+ // if reason is 'NO SUPPORT', use standard text from translation file
681
+
682
+ var $fallbackDiv, width, mediaClone, fallback, fallbackText,
683
+ showBrowserList, browsers, i, b, browserList;
684
+
685
+ // Could show list of supporting browsers if 99.9% confident the error is truly an outdated browser
686
+ // Too many sites say "You need to update your browser" when in fact I'm using a current version
687
+ showBrowserList = false;
688
+
689
+ $fallbackDiv = $('<div>',{
690
+ 'class' : 'able-fallback',
691
+ 'role' : 'alert',
692
+ });
693
+ // override default width of .able-fallback with player width, if known
694
+ if (typeof this.playerMaxWidth !== 'undefined') {
695
+ width = this.playerMaxWidth + 'px';
696
+ }
697
+ else if (this.$media.attr('width')) {
698
+ width = parseInt(this.$media.attr('width'), 10) + 'px';
699
+ }
700
+ else {
701
+ width = '100%';
702
+ }
703
+ $fallbackDiv.css('max-width',width);
704
+
705
+ // use fallback content that's nested inside the HTML5 media element, if there is any
706
+ mediaClone = this.$media.clone();
707
+ $('source, track', mediaClone).remove();
708
+ fallback = mediaClone.html().trim();
709
+ if (fallback.length) {
710
+ $fallbackDiv.html(fallback);
711
+ }
712
+ else if (reason == 'NO SUPPORT') {
713
+ // not using a supporting browser; use standard text from translation file
714
+ fallbackText = this.tt.fallbackError1 + ' ' + this.tt[this.mediaType] + '. ';
715
+ fallbackText += this.tt.fallbackError2 + ':';
716
+ fallback = $('<p>').text(fallbackText);
717
+ $fallbackDiv.html(fallback);
718
+ showBrowserList = true;
719
+ }
720
+ else {
721
+ // show the reason
722
+ $fallbackDiv.text(reason);
723
+ }
724
+
725
+ if (showBrowserList) {
726
+ browserList = $('<ul>');
727
+ browsers = this.getSupportingBrowsers();
728
+ for (i=0; i<browsers.length; i++) {
729
+ b = $('<li>');
730
+ b.text(browsers[i].name + ' ' + browsers[i].minVersion + ' ' + this.tt.orHigher);
731
+ browserList.append(b);
732
+ }
733
+ $fallbackDiv.append(browserList);
734
+ }
735
+
736
+ // if there's a poster, show that as well
737
+ this.injectPoster($fallbackDiv, 'fallback');
738
+
739
+ // inject $fallbackDiv into the DOM and remove broken content
740
+ if (typeof this.$ableWrapper !== 'undefined') {
741
+ this.$ableWrapper.before($fallbackDiv);
742
+ this.$ableWrapper.remove();
743
+ }
744
+ else if (typeof this.$media !== 'undefined') {
745
+ this.$media.before($fallbackDiv);
746
+ this.$media.remove();
747
+ }
748
+ else {
749
+ $('body').prepend($fallbackDiv);
750
+ }
751
+ };
752
+
753
+ AblePlayer.prototype.getSupportingBrowsers = function() {
754
+
755
+ var browsers = [];
756
+ browsers[0] = {
757
+ name:'Chrome',
758
+ minVersion: '31'
759
+ };
760
+ browsers[1] = {
761
+ name:'Firefox',
762
+ minVersion: '34'
763
+ };
764
+ browsers[2] = {
765
+ name:'Internet Explorer',
766
+ minVersion: '10'
767
+ };
768
+ browsers[3] = {
769
+ name:'Opera',
770
+ minVersion: '26'
771
+ };
772
+ browsers[4] = {
773
+ name:'Safari for Mac OS X',
774
+ minVersion: '7.1'
775
+ };
776
+ browsers[5] = {
777
+ name:'Safari for iOS',
778
+ minVersion: '7.1'
779
+ };
780
+ browsers[6] = {
781
+ name:'Android Browser',
782
+ minVersion: '4.1'
783
+ };
784
+ browsers[7] = {
785
+ name:'Chrome for Android',
786
+ minVersion: '40'
787
+ };
788
+ return browsers;
789
+ }
790
+
791
+ // Calculates the layout for controls based on media and options.
792
+ // Returns an object with keys 'ul', 'ur', 'bl', 'br' for upper-left, etc.
793
+ // Each associated value is array of control names to put at that location.
794
+ AblePlayer.prototype.calculateControlLayout = function () {
795
+ // Removed rewind/forward in favor of seek bar.
796
+
797
+ var controlLayout = {
798
+ 'ul': ['play','restart','rewind','forward'],
799
+ 'ur': ['seek'],
800
+ 'bl': [],
801
+ 'br': []
802
+ }
803
+
804
+ // test for browser support for volume before displaying volume button
805
+ if (this.browserSupportsVolume()) {
806
+ // volume buttons are: 'mute','volume-soft','volume-medium','volume-loud'
807
+ // previously supported button were: 'volume-up','volume-down'
808
+ this.volumeButton = 'volume-' + this.getVolumeName(this.volume);
809
+ controlLayout['ur'].push('volume');
810
+ }
811
+ else {
812
+ this.volume = false;
813
+ }
814
+
815
+ // Calculate the two sides of the bottom-left grouping to see if we need separator pipe.
816
+ var bll = [];
817
+ var blr = [];
818
+
819
+ if (this.isPlaybackRateSupported()) {
820
+ bll.push('slower');
821
+ bll.push('faster');
822
+ }
823
+
824
+ if (this.mediaType === 'video') {
825
+ if (this.hasCaptions) {
826
+ bll.push('captions'); //closed captions
827
+ }
828
+ if (this.hasSignLanguage) {
829
+ bll.push('sign'); // sign language
830
+ }
831
+ if ((this.hasOpenDesc || this.hasClosedDesc) && (this.useDescriptionsButton)) {
832
+ bll.push('descriptions'); //audio description
833
+ }
834
+ }
835
+ if (this.transcriptType === 'popup') {
836
+ bll.push('transcript');
837
+ }
838
+
839
+ if (this.mediaType === 'video' && this.hasChapters && this.useChaptersButton) {
840
+ bll.push('chapters');
841
+ }
842
+
843
+ controlLayout['br'].push('preferences');
844
+
845
+ // TODO: JW currently has a bug with fullscreen, anything that can be done about this?
846
+ if (this.mediaType === 'video' && this.allowFullScreen && this.player !== 'jw') {
847
+ controlLayout['br'].push('fullscreen');
848
+ }
849
+
850
+ // Include the pipe only if we need to.
851
+ if (bll.length > 0 && blr.length > 0) {
852
+ controlLayout['bl'] = bll;
853
+ controlLayout['bl'].push('pipe');
854
+ controlLayout['bl'] = controlLayout['bl'].concat(blr);
855
+ }
856
+ else {
857
+ controlLayout['bl'] = bll.concat(blr);
858
+ }
859
+
860
+ return controlLayout;
861
+ };
862
+
863
+ AblePlayer.prototype.addControls = function() {
864
+
865
+ // determine which controls to show based on several factors:
866
+ // mediaType (audio vs video)
867
+ // availability of tracks (e.g., for closed captions & audio description)
868
+ // browser support (e.g., for sliders and speedButtons)
869
+ // user preferences (???)
870
+ // some controls are aligned on the left, and others on the right
871
+ var useSpeedButtons, useFullScreen,
872
+ i, j, k, controls, $controllerSpan, tooltipId, tooltipX, tooltipY, control,
873
+ buttonImg, buttonImgSrc, buttonTitle, newButton, iconClass, buttonIcon, buttonUse,
874
+ leftWidth, rightWidth, totalWidth, leftWidthStyle, rightWidthStyle,
875
+ controllerStyles, vidcapStyles, captionLabel, popupMenuId;
876
+
877
+ var thisObj = this;
878
+
879
+ var baseSliderWidth = 100;
880
+
881
+ // Initializes the layout into the this.controlLayout variable.
882
+ var controlLayout = this.calculateControlLayout();
883
+
884
+ var sectionByOrder = {0: 'ul', 1:'ur', 2:'bl', 3:'br'};
885
+
886
+ // add an empty div to serve as a tooltip
887
+ tooltipId = this.mediaId + '-tooltip';
888
+ this.$tooltipDiv = $('<div>',{
889
+ 'id': tooltipId,
890
+ 'class': 'able-tooltip'
891
+ });
892
+ this.$controllerDiv.append(this.$tooltipDiv);
893
+
894
+ // step separately through left and right controls
895
+ for (i = 0; i <= 3; i++) {
896
+ controls = controlLayout[sectionByOrder[i]];
897
+ if ((i % 2) === 0) {
898
+ $controllerSpan = $('<div>',{
899
+ 'class': 'able-left-controls'
900
+ });
901
+ }
902
+ else {
903
+ $controllerSpan = $('<div>',{
904
+ 'class': 'able-right-controls'
905
+ });
906
+ }
907
+ this.$controllerDiv.append($controllerSpan);
908
+ for (j=0; j<controls.length; j++) {
909
+ control = controls[j];
910
+ if (control === 'seek') {
911
+ var sliderDiv = $('<div class="able-seekbar"></div>');
912
+ var sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
913
+ $controllerSpan.append(sliderDiv);
914
+ var duration = this.getDuration();
915
+ if (duration == 0) {
916
+ // set arbitrary starting duration, and change it when duration is known
917
+ duration = 100;
918
+ }
919
+ this.seekBar = new AccessibleSlider(this.mediaType, sliderDiv, 'horizontal', baseSliderWidth, 0, duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
920
+ }
921
+ else if (control === 'pipe') {
922
+ // TODO: Unify this with buttons somehow to avoid code duplication
923
+ var pipe = $('<span>', {
924
+ 'tabindex': '-1',
925
+ 'aria-hidden': 'true'
926
+ });
927
+ if (this.iconType === 'font') {
928
+ pipe.addClass('icon-pipe');
929
+ }
930
+ else {
931
+ var pipeImg = $('<img>', {
932
+ src: this.rootPath + 'button-icons/' + this.iconColor + '/pipe.png',
933
+ alt: '',
934
+ role: 'presentation'
935
+ });
936
+ pipe.append(pipeImg);
937
+ }
938
+ $controllerSpan.append(pipe);
939
+ }
940
+ else {
941
+ // this control is a button
942
+ if (control === 'volume') {
943
+ buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + this.volumeButton + '.png';
944
+ }
945
+ else if (control === 'fullscreen') {
946
+ buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/fullscreen-expand.png';
947
+ }
948
+ else if (control === 'slower') {
949
+ if (this.speedIcons === 'animals') {
950
+ buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/turtle.png';
951
+ }
952
+ else {
953
+ buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/slower.png';
954
+ }
955
+ }
956
+ else if (control === 'faster') {
957
+ if (this.speedIcons === 'animals') {
958
+ buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/rabbit.png';
959
+ }
960
+ else {
961
+ buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/faster.png';
962
+ }
963
+ }
964
+ else {
965
+ buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + control + '.png';
966
+ }
967
+ buttonTitle = this.getButtonTitle(control);
968
+
969
+ // icomoon documentation recommends the following markup for screen readers:
970
+ // 1. link element (or in our case, button). Nested inside this element:
971
+ // 2. span that contains the icon font (in our case, buttonIcon)
972
+ // 3. span that contains a visually hidden label for screen readers (buttonLabel)
973
+ // In addition, we are adding aria-label to the button (but not title)
974
+ // And if iconType === 'image', we are replacing #2 with an image (with alt="" and role="presentation")
975
+ // This has been thoroughly tested and works well in all screen reader/browser combinations
976
+ // See https://github.com/ableplayer/ableplayer/issues/81
977
+ newButton = $('<button>',{
978
+ 'type': 'button',
979
+ 'tabindex': '0',
980
+ 'aria-label': buttonTitle,
981
+ 'class': 'able-button-handler-' + control
982
+ });
983
+ if (control === 'volume' || control === 'preferences') {
984
+ // This same ARIA for captions and chapters are added elsewhere
985
+ if (control == 'preferences') {
986
+ popupMenuId = this.mediaId + '-prefs-menu';
987
+ }
988
+ else if (control === 'volume') {
989
+ popupMenuId = this.mediaId + '-volume-slider';
990
+ }
991
+ newButton.attr({
992
+ 'aria-controls': popupMenuId
993
+ });
994
+ }
995
+ if (this.iconType === 'font') {
996
+ if (control === 'volume') {
997
+ iconClass = 'icon-' + this.volumeButton;
998
+ }
999
+ else if (control === 'slower') {
1000
+ if (this.speedIcons === 'animals') {
1001
+ iconClass = 'icon-turtle';
1002
+ }
1003
+ else {
1004
+ iconClass = 'icon-slower';
1005
+ }
1006
+ }
1007
+ else if (control === 'faster') {
1008
+ if (this.speedIcons === 'animals') {
1009
+ iconClass = 'icon-rabbit';
1010
+ }
1011
+ else {
1012
+ iconClass = 'icon-faster';
1013
+ }
1014
+ }
1015
+ else {
1016
+ iconClass = 'icon-' + control;
1017
+ }
1018
+ buttonIcon = $('<span>',{
1019
+ 'class': iconClass,
1020
+ 'aria-hidden': 'true'
1021
+ });
1022
+ newButton.append(buttonIcon);
1023
+ }
1024
+ else if (this.iconType === 'svg') {
1025
+ if (control === 'volume') {
1026
+ iconClass = 'svg-' + this.volumeButton;
1027
+ }
1028
+ else if (control === 'fullscreen') {
1029
+ iconClass = 'svg-fullscreen-expand';
1030
+ }
1031
+ else if (control === 'slower') {
1032
+ if (this.speedIcons === 'animals') {
1033
+ iconClass = 'svg-turtle';
1034
+ }
1035
+ else {
1036
+ iconClass = 'svg-slower';
1037
+ }
1038
+ }
1039
+ else if (control === 'faster') {
1040
+ if (this.speedIcons === 'animals') {
1041
+ iconClass = 'svg-rabbit';
1042
+ }
1043
+ else {
1044
+ iconClass = 'svg-faster';
1045
+ }
1046
+ }
1047
+ else {
1048
+ iconClass = 'svg-' + control;
1049
+ }
1050
+ buttonIcon = $('<svg>',{
1051
+ 'class': iconClass
1052
+ });
1053
+ buttonUse = $('<use>',{
1054
+ 'xlink:href': this.rootPath + 'icons/able-icons.svg#' + iconClass
1055
+ });
1056
+ buttonIcon.append(buttonUse);
1057
+ newButton.html(buttonIcon);
1058
+
1059
+ // Final step: Need to refresh the DOM in order for browser to process & display the SVG
1060
+ newButton.html(newButton.html());
1061
+ }
1062
+ else {
1063
+ // use images
1064
+ buttonImg = $('<img>',{
1065
+ 'src': buttonImgSrc,
1066
+ 'alt': '',
1067
+ 'role': 'presentation'
1068
+ });
1069
+ newButton.append(buttonImg);
1070
+ }
1071
+ // add the visibly-hidden label for screen readers that don't support aria-label on the button
1072
+ var buttonLabel = $('<span>',{
1073
+ 'class': 'able-clipped'
1074
+ }).text(buttonTitle);
1075
+ newButton.append(buttonLabel);
1076
+ // add an event listener that displays a tooltip on mouseenter or focus
1077
+ newButton.on('mouseenter focus',function(event) {
1078
+ var label = $(this).attr('aria-label');
1079
+ // get position of this button
1080
+ var position = $(this).position();
1081
+ var buttonHeight = $(this).height();
1082
+ var buttonWidth = $(this).width();
1083
+ var tooltipY = position.top - buttonHeight - 15;
1084
+ var centerTooltip = true;
1085
+ if ($(this).closest('div').hasClass('able-right-controls')) {
1086
+ // this control is on the right side
1087
+ if ($(this).closest('div').find('button:last').get(0) == $(this).get(0)) {
1088
+ // this is the last control on the right
1089
+ // position tooltip using the "right" property
1090
+ centerTooltip = false;
1091
+ var tooltipX = 0;
1092
+ var tooltipStyle = {
1093
+ left: '',
1094
+ right: tooltipX + 'px',
1095
+ top: tooltipY + 'px'
1096
+ };
1097
+ }
1098
+ }
1099
+ else {
1100
+ // this control is on the left side
1101
+ if ($(this).is(':first-child')) {
1102
+ // this is the first control on the left
1103
+ centerTooltip = false;
1104
+ var tooltipX = position.left;
1105
+ var tooltipStyle = {
1106
+ left: tooltipX + 'px',
1107
+ right: '',
1108
+ top: tooltipY + 'px'
1109
+ };
1110
+ }
1111
+ }
1112
+ if (centerTooltip) {
1113
+ // populate tooltip, then calculate its width before showing it
1114
+ var tooltipWidth = $('#' + tooltipId).text(label).width();
1115
+ // center the tooltip horizontally over the button
1116
+ var tooltipX = position.left - tooltipWidth/2;
1117
+ var tooltipStyle = {
1118
+ left: tooltipX + 'px',
1119
+ right: '',
1120
+ top: tooltipY + 'px'
1121
+ };
1122
+ }
1123
+ var tooltip = $('#' + tooltipId).text(label).css(tooltipStyle);
1124
+ thisObj.showTooltip(tooltip);
1125
+ $(this).on('mouseleave blur',function() {
1126
+ $('#' + tooltipId).text('').hide();
1127
+ })
1128
+ });
1129
+
1130
+ if (control === 'captions') {
1131
+ if (!this.prefCaptions || this.prefCaptions !== 1) {
1132
+ // captions are available, but user has them turned off
1133
+ if (this.captions.length > 1) {
1134
+ captionLabel = this.tt.captions;
1135
+ }
1136
+ else {
1137
+ captionLabel = this.tt.showCaptions;
1138
+ }
1139
+ newButton.addClass('buttonOff').attr('title',captionLabel);
1140
+ }
1141
+ }
1142
+ else if (control === 'descriptions') {
1143
+ if (!this.prefDesc || this.prefDesc !== 1) {
1144
+ // user prefer non-audio described version
1145
+ // Therefore, load media without description
1146
+ // Description can be toggled on later with this button
1147
+ newButton.addClass('buttonOff').attr('title',this.tt.turnOnDescriptions);
1148
+ }
1149
+ }
1150
+
1151
+ $controllerSpan.append(newButton);
1152
+
1153
+ // create variables of buttons that are referenced throughout the AblePlayer object
1154
+ if (control === 'play') {
1155
+ this.$playpauseButton = newButton;
1156
+ }
1157
+ else if (control === 'captions') {
1158
+ this.$ccButton = newButton;
1159
+ }
1160
+ else if (control === 'sign') {
1161
+ this.$signButton = newButton;
1162
+ // gray out sign button if sign language window is not active
1163
+ if (!(this.$signWindow.is(':visible'))) {
1164
+ this.$signButton.addClass('buttonOff');
1165
+ }
1166
+ }
1167
+ else if (control === 'descriptions') {
1168
+ this.$descButton = newButton;
1169
+ // button will be enabled or disabled in description.js > initDescription()
1170
+ }
1171
+ else if (control === 'mute') {
1172
+ this.$muteButton = newButton;
1173
+ }
1174
+ else if (control === 'transcript') {
1175
+ this.$transcriptButton = newButton;
1176
+ // gray out transcript button if transcript is not active
1177
+ if (!(this.$transcriptDiv.is(':visible'))) {
1178
+ this.$transcriptButton.addClass('buttonOff').attr('title',this.tt.showTranscript);
1179
+ }
1180
+ }
1181
+ else if (control === 'fullscreen') {
1182
+ this.$fullscreenButton = newButton;
1183
+ }
1184
+ else if (control === 'chapters') {
1185
+ this.$chaptersButton = newButton;
1186
+ }
1187
+ else if (control === 'preferences') {
1188
+ this.$prefsButton = newButton;
1189
+ }
1190
+ else if (control === 'volume') {
1191
+ this.$volumeButton = newButton;
1192
+ }
1193
+ }
1194
+ if (control === 'volume') {
1195
+ // in addition to the volume button, add a hidden slider
1196
+ this.addVolumeSlider($controllerSpan);
1197
+ }
1198
+ }
1199
+ if ((i % 2) == 1) {
1200
+ this.$controllerDiv.append('<div style="clear:both;"></div>');
1201
+ }
1202
+ }
1203
+
1204
+ if (this.mediaType === 'video') {
1205
+
1206
+ if (typeof this.$captionsDiv !== 'undefined') {
1207
+ // stylize captions based on user prefs
1208
+ this.stylizeCaptions(this.$captionsDiv);
1209
+ }
1210
+ if (typeof this.$descDiv !== 'undefined') {
1211
+ // stylize descriptions based on user's caption prefs
1212
+ this.stylizeCaptions(this.$descDiv);
1213
+ }
1214
+ }
1215
+
1216
+ // combine left and right controls arrays for future reference
1217
+ this.controls = [];
1218
+ for (var sec in controlLayout) if (controlLayout.hasOwnProperty(sec)) {
1219
+ this.controls = this.controls.concat(controlLayout[sec]);
1220
+ }
1221
+
1222
+ // Update state-based display of controls.
1223
+ this.refreshControls();
1224
+ };
1225
+
1226
+ AblePlayer.prototype.useSvg = function () {
1227
+
1228
+ // Modified from IcoMoon.io svgxuse
1229
+ // @copyright Copyright (c) 2016 IcoMoon.io
1230
+ // @license Licensed under MIT license
1231
+ // See https://github.com/Keyamoon/svgxuse
1232
+ // @version 1.1.16
1233
+
1234
+ var cache = Object.create(null); // holds xhr objects to prevent multiple requests
1235
+ var checkUseElems,
1236
+ tid; // timeout id
1237
+ var debouncedCheck = function () {
1238
+ clearTimeout(tid);
1239
+ tid = setTimeout(checkUseElems, 100);
1240
+ };
1241
+ var unobserveChanges = function () {
1242
+ return;
1243
+ };
1244
+ var observeChanges = function () {
1245
+ var observer;
1246
+ window.addEventListener('resize', debouncedCheck, false);
1247
+ window.addEventListener('orientationchange', debouncedCheck, false);
1248
+ if (window.MutationObserver) {
1249
+ observer = new MutationObserver(debouncedCheck);
1250
+ observer.observe(document.documentElement, {
1251
+ childList: true,
1252
+ subtree: true,
1253
+ attributes: true
1254
+ });
1255
+ unobserveChanges = function () {
1256
+ try {
1257
+ observer.disconnect();
1258
+ window.removeEventListener('resize', debouncedCheck, false);
1259
+ window.removeEventListener('orientationchange', debouncedCheck, false);
1260
+ } catch (ignore) {}
1261
+ };
1262
+ }
1263
+ else {
1264
+ document.documentElement.addEventListener('DOMSubtreeModified', debouncedCheck, false);
1265
+ unobserveChanges = function () {
1266
+ document.documentElement.removeEventListener('DOMSubtreeModified', debouncedCheck, false);
1267
+ window.removeEventListener('resize', debouncedCheck, false);
1268
+ window.removeEventListener('orientationchange', debouncedCheck, false);
1269
+ };
1270
+ }
1271
+ };
1272
+ var xlinkNS = 'http://www.w3.org/1999/xlink';
1273
+ checkUseElems = function () {
1274
+ var base,
1275
+ bcr,
1276
+ fallback = '', // optional fallback URL in case no base path to SVG file was given and no symbol definition was found.
1277
+ hash,
1278
+ i,
1279
+ Request,
1280
+ inProgressCount = 0,
1281
+ isHidden,
1282
+ url,
1283
+ uses,
1284
+ xhr;
1285
+ if (window.XMLHttpRequest) {
1286
+ Request = new XMLHttpRequest();
1287
+ if (Request.withCredentials !== undefined) {
1288
+ Request = XMLHttpRequest;
1289
+ }
1290
+ else {
1291
+ Request = XDomainRequest || undefined;
1292
+ }
1293
+ }
1294
+ if (Request === undefined) {
1295
+ return;
1296
+ }
1297
+ function observeIfDone() {
1298
+ // If done with making changes, start watching for chagnes in DOM again
1299
+ inProgressCount -= 1;
1300
+ if (inProgressCount === 0) { // if all xhrs were resolved
1301
+ observeChanges(); // watch for changes to DOM
1302
+ }
1303
+ }
1304
+ function attrUpdateFunc(spec) {
1305
+ return function () {
1306
+ if (cache[spec.base] !== true) {
1307
+ spec.useEl.setAttributeNS(xlinkNS, 'xlink:href', '#' + spec.hash);
1308
+ }
1309
+ };
1310
+ }
1311
+ function onloadFunc(xhr) {
1312
+ return function () {
1313
+ var body = document.body;
1314
+ var x = document.createElement('x');
1315
+ var svg;
1316
+ xhr.onload = null;
1317
+ x.innerHTML = xhr.responseText;
1318
+ svg = x.getElementsByTagName('svg')[0];
1319
+ if (svg) {
1320
+ svg.setAttribute('aria-hidden', 'true');
1321
+ svg.style.position = 'absolute';
1322
+ svg.style.width = 0;
1323
+ svg.style.height = 0;
1324
+ svg.style.overflow = 'hidden';
1325
+ body.insertBefore(svg, body.firstChild);
1326
+ }
1327
+ observeIfDone();
1328
+ };
1329
+ }
1330
+ function onErrorTimeout(xhr) {
1331
+ return function () {
1332
+ xhr.onerror = null;
1333
+ xhr.ontimeout = null;
1334
+ observeIfDone();
1335
+ };
1336
+ }
1337
+ unobserveChanges(); // stop watching for changes to DOM
1338
+ // find all use elements
1339
+ uses = document.getElementsByTagName('use');
1340
+ for (i = 0; i < uses.length; i += 1) {
1341
+ try {
1342
+ bcr = uses[i].getBoundingClientRect();
1343
+ } catch (ignore) {
1344
+ // failed to get bounding rectangle of the use element
1345
+ bcr = false;
1346
+ }
1347
+ url = uses[i].getAttributeNS(xlinkNS, 'href').split('#');
1348
+ base = url[0];
1349
+ hash = url[1];
1350
+ isHidden = bcr && bcr.left === 0 && bcr.right === 0 && bcr.top === 0 && bcr.bottom === 0;
1351
+ if (bcr && bcr.width === 0 && bcr.height === 0 && !isHidden) {
1352
+ // the use element is empty
1353
+ // if there is a reference to an external SVG, try to fetch it
1354
+ // use the optional fallback URL if there is no reference to an external SVG
1355
+ if (fallback && !base.length && hash && !document.getElementById(hash)) {
1356
+ base = fallback;
1357
+ }
1358
+ if (base.length) {
1359
+ // schedule updating xlink:href
1360
+ xhr = cache[base];
1361
+ if (xhr !== true) {
1362
+ // true signifies that prepending the SVG was not required
1363
+ setTimeout(attrUpdateFunc({
1364
+ useEl: uses[i],
1365
+ base: base,
1366
+ hash: hash
1367
+ }), 0);
1368
+ }
1369
+ if (xhr === undefined) {
1370
+ xhr = new Request();
1371
+ cache[base] = xhr;
1372
+ xhr.onload = onloadFunc(xhr);
1373
+ xhr.onerror = onErrorTimeout(xhr);
1374
+ xhr.ontimeout = onErrorTimeout(xhr);
1375
+ xhr.open('GET', base);
1376
+ xhr.send();
1377
+ inProgressCount += 1;
1378
+ }
1379
+ }
1380
+ }
1381
+ else {
1382
+ if (!isHidden) {
1383
+ if (cache[base] === undefined) {
1384
+ // remember this URL if the use element was not empty and no request was sent
1385
+ cache[base] = true;
1386
+ }
1387
+ else if (cache[base].onload) {
1388
+ // if it turns out that prepending the SVG is not necessary,
1389
+ // abort the in-progress xhr.
1390
+ cache[base].abort();
1391
+ cache[base].onload = undefined;
1392
+ cache[base] = true;
1393
+ }
1394
+ }
1395
+ }
1396
+ }
1397
+ uses = '';
1398
+ inProgressCount += 1;
1399
+ observeIfDone();
1400
+ };
1401
+ /*
1402
+ // The load event fires when all resources have finished loading, which allows detecting whether SVG use elements are empty.
1403
+ window.addEventListener('load', function winLoad() {
1404
+ window.removeEventListener('load', winLoad, false); // to prevent memory leaks
1405
+ tid = setTimeout(checkUseElems, 0);
1406
+ }, false);
1407
+ */
1408
+ };
1409
+
1410
+ AblePlayer.prototype.swapSource = function(sourceIndex) {
1411
+
1412
+ // Change media player source file, for instance when moving to the next element in a playlist.
1413
+ // NOTE: Swapping source for audio description is handled elsewhere;
1414
+ // see description.js > swapDescription()
1415
+
1416
+ var $newItem, itemTitle, itemLang, sources, s, jwSource, i, $newSource, nowPlayingSpan;
1417
+
1418
+ this.$media.find('source').remove();
1419
+ $newItem = this.$playlist.eq(sourceIndex);
1420
+ itemTitle = $newItem.html();
1421
+ if ($newItem.attr('lang')) {
1422
+ itemLang = $newItem.attr('lang');
1423
+ }
1424
+ sources = [];
1425
+ s = 0; // index
1426
+ if (this.mediaType === 'audio') {
1427
+ if ($newItem.attr('data-mp3')) {
1428
+ jwSource = $newItem.attr('data-mp3'); // JW Player can play this
1429
+ sources[s] = new Array('audio/mpeg',jwSource);
1430
+ s++;
1431
+ }
1432
+ if ($newItem.attr('data-webm')) {
1433
+ sources[s] = new Array('audio/webm',$newItem.attr('data-webm'));
1434
+ s++;
1435
+ }
1436
+ if ($newItem.attr('data-webma')) {
1437
+ sources[s] = new Array('audio/webm',$newItem.attr('data-webma'));
1438
+ s++;
1439
+ }
1440
+ if ($newItem.attr('data-ogg')) {
1441
+ sources[s] = new Array('audio/ogg',$newItem.attr('data-ogg'));
1442
+ s++;
1443
+ }
1444
+ if ($newItem.attr('data-oga')) {
1445
+ sources[s] = new Array('audio/ogg',$newItem.attr('data-oga'));
1446
+ s++;
1447
+ }
1448
+ if ($newItem.attr('data-wav')) {
1449
+ sources[s] = new Array('audio/wav',$newItem.attr('data-wav'));
1450
+ s++;
1451
+ }
1452
+ }
1453
+ else if (this.mediaType === 'video') {
1454
+ if ($newItem.attr('data-mp4')) {
1455
+ jwSource = $newItem.attr('data-mp4'); // JW Player can play this
1456
+ sources[s] = new Array('video/mp4',jwSource);
1457
+ s++;
1458
+ }
1459
+ if ($newItem.attr('data-webm')) {
1460
+ sources[s] = new Array('video/webm',$newItem.attr('data-webm'));
1461
+ s++;
1462
+ }
1463
+ if ($newItem.attr('data-webmv')) {
1464
+ sources[s] = new Array('video/webm',$newItem.attr('data-webmv'));
1465
+ s++;
1466
+ }
1467
+ if ($newItem.attr('data-ogg')) {
1468
+ sources[s] = new Array('video/ogg',$newItem.attr('data-ogg'));
1469
+ s++;
1470
+ }
1471
+ if ($newItem.attr('data-ogv')) {
1472
+ sources[s] = new Array('video/ogg',$newItem.attr('data-ogv'));
1473
+ s++;
1474
+ }
1475
+ }
1476
+ for (i=0; i<sources.length; i++) {
1477
+ $newSource = $('<source>',{
1478
+ type: sources[i][0],
1479
+ src: sources[i][1]
1480
+ });
1481
+ this.$media.append($newSource);
1482
+ }
1483
+
1484
+ // update playlist to indicate which item is playing
1485
+ //$('.able-playlist li').removeClass('able-current');
1486
+ this.$playlist.removeClass('able-current');
1487
+ $newItem.addClass('able-current');
1488
+
1489
+ // update Now Playing div
1490
+ if (this.showNowPlaying === true) {
1491
+ nowPlayingSpan = $('<span>');
1492
+ if (typeof itemLang !== 'undefined') {
1493
+ nowPlayingSpan.attr('lang',itemLang);
1494
+ }
1495
+ nowPlayingSpan.html('<span>Selected track:</span>' + itemTitle);
1496
+ this.$nowPlayingDiv.html(nowPlayingSpan);
1497
+ }
1498
+
1499
+ // reload audio after sources have been updated
1500
+ // if this.swappingSrc is true, media will autoplay when ready
1501
+ if (this.initializing) { // this is the first track - user hasn't pressed play yet
1502
+ this.swappingSrc = false;
1503
+ }
1504
+ else {
1505
+ this.swappingSrc = true;
1506
+ if (this.player === 'html5') {
1507
+ this.media.load();
1508
+ }
1509
+ else if (this.player === 'jw') {
1510
+ this.jwPlayer.load({file: jwSource});
1511
+ }
1512
+ else if (this.player === 'youtube') {
1513
+ // Does nothing, can't swap source with youtube.
1514
+ // TODO: Anything we need to do to prevent this happening?
1515
+ }
1516
+ }
1517
+ };
1518
+
1519
+ AblePlayer.prototype.getButtonTitle = function(control) {
1520
+
1521
+ var captionsCount;
1522
+
1523
+ if (control === 'playpause') {
1524
+ return this.tt.play;
1525
+ }
1526
+ else if (control === 'play') {
1527
+ return this.tt.play;
1528
+ }
1529
+ else if (control === 'pause') {
1530
+ return this.tt.pause;
1531
+ }
1532
+ else if (control === 'restart') {
1533
+ return this.tt.restart;
1534
+ }
1535
+ else if (control === 'rewind') {
1536
+ return this.tt.rewind;
1537
+ }
1538
+ else if (control === 'forward') {
1539
+ return this.tt.forward;
1540
+ }
1541
+ else if (control === 'captions') {
1542
+ if (this.usingYouTubeCaptions) {
1543
+ captionsCount = this.ytCaptions.length;
1544
+ }
1545
+ else {
1546
+ captionsCount = this.captions.length;
1547
+ }
1548
+ if (captionsCount > 1) {
1549
+ return this.tt.captions;
1550
+ }
1551
+ else {
1552
+ if (this.captionsOn) {
1553
+ return this.tt.hideCaptions;
1554
+ }
1555
+ else {
1556
+ return this.tt.showCaptions;
1557
+ }
1558
+ }
1559
+ }
1560
+ else if (control === 'descriptions') {
1561
+ if (this.descOn) {
1562
+ return this.tt.turnOffDescriptions;
1563
+ }
1564
+ else {
1565
+ return this.tt.turnOnDescriptions;
1566
+ }
1567
+ }
1568
+ else if (control === 'transcript') {
1569
+ if (this.$transcriptDiv.is(':visible')) {
1570
+ return this.tt.hideTranscript;
1571
+ }
1572
+ else {
1573
+ return this.tt.showTranscript;
1574
+ }
1575
+ }
1576
+ else if (control === 'chapters') {
1577
+ return this.tt.chapters;
1578
+ }
1579
+ else if (control === 'sign') {
1580
+ return this.tt.sign;
1581
+ }
1582
+ else if (control === 'volume') {
1583
+ return this.tt.volume;
1584
+ }
1585
+ else if (control === 'faster') {
1586
+ return this.tt.faster;
1587
+ }
1588
+ else if (control === 'slower') {
1589
+ return this.tt.slower;
1590
+ }
1591
+ else if (control === 'preferences') {
1592
+ return this.tt.preferences;
1593
+ }
1594
+ else if (control === 'help') {
1595
+ // return this.tt.help;
1596
+ }
1597
+ else {
1598
+ // there should be no other controls, but just in case:
1599
+ // return the name of the control with first letter in upper case
1600
+ // ultimately will need to get a translated label from this.tt
1601
+ if (this.debug) {
1602
+ console.log('Found an untranslated label: ' + control);
1603
+ }
1604
+ return control.charAt(0).toUpperCase() + control.slice(1);
1605
+ }
1606
+ };
1607
+
1608
+
1609
+ })(jQuery);