wai-website-theme 1.3.1 → 1.4

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