wai-website-theme 1.3.1 → 1.4

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