@oat-sa/tao-core-ui 1.5.4 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/ckeditor/ckConfigurator.js +9 -1
  2. package/dist/mediaEditor/mediaEditorComponent.js +5 -3
  3. package/dist/mediaEditor/plugins/mediaDimension/mediaDimensionComponent.js +46 -25
  4. package/dist/mediaplayer/css/player.css +104 -14
  5. package/dist/mediaplayer/css/player.css.map +1 -1
  6. package/dist/mediaplayer/players/html5.js +767 -0
  7. package/dist/mediaplayer/players/youtube.js +470 -0
  8. package/dist/mediaplayer/players.js +35 -0
  9. package/dist/mediaplayer/support.js +134 -0
  10. package/dist/mediaplayer/utils/reminder.js +198 -0
  11. package/dist/mediaplayer/utils/timeObserver.js +149 -0
  12. package/dist/mediaplayer/youtubeManager.js +177 -0
  13. package/dist/mediaplayer.js +1251 -1912
  14. package/dist/previewer.js +25 -19
  15. package/package.json +1 -1
  16. package/scss/basic.scss +1 -0
  17. package/scss/inc/_jquery.nouislider.scss +254 -0
  18. package/src/ckeditor/ckConfigurator.js +10 -1
  19. package/src/itemButtonList/css/item-button-list.css +225 -0
  20. package/src/itemButtonList/css/item-button-list.css.map +1 -0
  21. package/src/mediaEditor/mediaEditorComponent.js +25 -26
  22. package/src/mediaEditor/plugins/mediaDimension/mediaDimensionComponent.js +83 -63
  23. package/src/mediaplayer/css/player.css +104 -14
  24. package/src/mediaplayer/css/player.css.map +1 -1
  25. package/src/mediaplayer/players/html5.js +564 -0
  26. package/src/mediaplayer/players/youtube.js +323 -0
  27. package/src/mediaplayer/players.js +29 -0
  28. package/src/mediaplayer/scss/player.scss +125 -16
  29. package/src/mediaplayer/support.js +126 -0
  30. package/src/mediaplayer/tpl/audio.tpl +6 -0
  31. package/src/mediaplayer/tpl/player.tpl +11 -32
  32. package/src/mediaplayer/tpl/source.tpl +1 -0
  33. package/src/mediaplayer/tpl/video.tpl +6 -0
  34. package/src/mediaplayer/tpl/youtube.tpl +1 -0
  35. package/src/mediaplayer/utils/reminder.js +184 -0
  36. package/src/mediaplayer/utils/timeObserver.js +143 -0
  37. package/src/mediaplayer/youtubeManager.js +161 -0
  38. package/src/mediaplayer.js +1217 -1901
  39. package/src/previewer.js +40 -33
  40. package/src/searchModal/css/advancedSearch.css +190 -0
  41. package/src/searchModal/css/advancedSearch.css.map +1 -0
  42. package/src/searchModal/css/searchModal.css +506 -0
  43. package/src/searchModal/css/searchModal.css.map +1 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * This program is free software; you can redistribute it and/or
3
+ * modify it under the terms of the GNU General Public License
4
+ * as published by the Free Software Foundation; under version 2
5
+ * of the License (non-upgradable).
6
+ *
7
+ * This program is distributed in the hope that it will be useful,
8
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ * GNU General Public License for more details.
11
+ *
12
+ * You should have received a copy of the GNU General Public License
13
+ * along with this program; if not, write to the Free Software
14
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15
+ *
16
+ * Copyright (c) 2015-2021 (original work) Open Assessment Technologies SA ;
17
+ */
18
+
19
+ /**
20
+ * A Regex to detect Apple mobile browsers
21
+ * @type {RegExp}
22
+ * @private
23
+ */
24
+ const reAppleMobiles = /ip(hone|od)/i;
25
+
26
+ /**
27
+ * A list of MIME types with codec declaration
28
+ * @type {Object}
29
+ * @private
30
+ */
31
+ const supportedMimeTypes = {
32
+ // video
33
+ 'video/webm': 'video/webm; codecs="vp8, vorbis"',
34
+ 'video/mp4': 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
35
+ 'video/ogg': 'video/ogg; codecs="theora, vorbis"',
36
+ // audio
37
+ 'audio/mpeg': 'audio/mpeg;',
38
+ 'audio/mp4': 'audio/mp4; codecs="mp4a.40.5"',
39
+ 'audio/ogg': 'audio/ogg; codecs="vorbis"',
40
+ 'audio/wav': 'audio/wav; codecs="1"'
41
+ };
42
+
43
+ /**
44
+ * Checks support for a MIME type.
45
+ * @param {HTMLMediaElement} media The media element on which check support
46
+ * @param {String} mimeType A MIME type to check the support for
47
+ * @returns {string}
48
+ * @private
49
+ */
50
+ const findSupport = (media, mimeType) => media.canPlayType(mimeType).replace(/no/, '');
51
+
52
+ /**
53
+ * Support detection
54
+ * @type {Object}
55
+ */
56
+ export default {
57
+ /**
58
+ * Checks if the browser can play media
59
+ * @param {HTMLMediaElement} media The media element on which check support
60
+ * @param {String} [mimeType] An optional MIME type to precise the support
61
+ * @returns {Boolean}
62
+ * @private
63
+ */
64
+ checkSupport(media, mimeType) {
65
+ const support = media.canPlayType;
66
+ if (support && mimeType) {
67
+ return !!(
68
+ (supportedMimeTypes[mimeType] && findSupport(media, supportedMimeTypes[mimeType])) ||
69
+ findSupport(media, mimeType)
70
+ );
71
+ }
72
+
73
+ return !!support;
74
+ },
75
+
76
+ /**
77
+ * Checks if the browser can play video and audio
78
+ * @param {String} [type] The type of media (audio or video)
79
+ * @param {String} [mime] A media MIME type to check
80
+ * @returns {Boolean}
81
+ */
82
+ canPlay(type, mime) {
83
+ if (type) {
84
+ switch (type.toLowerCase()) {
85
+ case 'audio':
86
+ return this.canPlayAudio(mime);
87
+
88
+ case 'youtube':
89
+ return this.canPlayVideo();
90
+
91
+ case 'video':
92
+ return this.canPlayVideo(mime);
93
+
94
+ default:
95
+ return false;
96
+ }
97
+ }
98
+ return this.canPlayAudio() && this.canPlayVideo();
99
+ },
100
+
101
+ /**
102
+ * Checks if the browser can play audio
103
+ * @param {String} [mime] A media MIME type to check
104
+ * @returns {Boolean}
105
+ */
106
+ canPlayAudio(mime) {
107
+ return this.checkSupport(document.createElement('audio'), mime);
108
+ },
109
+
110
+ /**
111
+ * Checks if the browser can play video
112
+ * @param {String} [mime] A media MIME type to check
113
+ * @returns {Boolean}
114
+ */
115
+ canPlayVideo(mime) {
116
+ return this.checkSupport(document.createElement('video'), mime);
117
+ },
118
+
119
+ /**
120
+ * Checks if the browser allows to control the media playback
121
+ * @returns {Boolean}
122
+ */
123
+ canControl() {
124
+ return !reAppleMobiles.test(window.navigator.userAgent);
125
+ }
126
+ };
@@ -0,0 +1,6 @@
1
+ <audio class="media audio" preload="{{preload}}" controls {{#if cors}}crossorigin{{/if}}>
2
+ {{__ 'Your browser doesn’t support the audio player.'}}
3
+ {{#if link}}
4
+ <a href="{{link}}">{{__ 'Please download the track and listen offline.'}}</a>
5
+ {{/if}}
6
+ </audio>
@@ -1,37 +1,16 @@
1
1
  <div class="mediaplayer {{type}}">
2
2
  <div class="player">
3
- {{#if is.video}}
4
- {{#if is.youtube}}
5
- {{#each sources}}
6
- <div class="media video youtube" data-video-src="{{src}}" data-video-id="{{id}}" data-type="youtube"></div>
7
- {{/each}}
8
- {{else}}
9
- <video class="media video" poster="{{poster}}" controls {{#if is.cors}}crossorigin{{/if}}>
10
- {{#each sources}}
11
- <source src="{{src}}" type="{{type}}">
12
- {{/each}}
13
-
14
- {{__ 'Your browser doesn’t support the video player.'}}
15
- {{#if link}}
16
- <a href="{{link}}">{{__ 'Please download the video and view offline.'}}</a>
17
- {{/if}}
18
- </video>
19
- {{/if}}
20
- {{else}}
21
- <audio class="media audio" controls {{#if is.cors}}crossorigin{{/if}}>
22
- {{#each sources}}
23
- <source src="{{src}}" type="{{type}}">
24
- {{/each}}
25
-
26
- {{__ 'Your browser doesn’t support the audio player.'}}
27
- {{#if link}}
28
- <a href="{{link}}">{{__ 'Please download the track and listen offline.'}}</a>
29
- {{/if}}
30
- </audio>
31
- {{/if}}
32
- <div class="overlay">
3
+ <div class="player-overlay">
33
4
  <a class="action play" data-control="play"><span class="icon icon-play" title="{{__ 'Play'}}"></span></a>
34
5
  <a class="action play" data-control="pause"><span class="icon icon-pause" title="{{__ 'Pause'}}"></span></a>
6
+ <a class="action reload" data-control="start">
7
+ <span class="icon icon-play" title="{{__ 'Start'}}"></span>
8
+ <div class="message">{{__ 'Click to start'}}</div>
9
+ </a>
10
+ <a class="action reload" data-control="reload">
11
+ <div class="icon icon-reload" title="{{__ 'Reload'}}"></div>
12
+ <div class="message">{{__ 'You are encountering a prolonged connectivity loss.'}} {{__ 'Click to reload.'}}</div>
13
+ </a>
35
14
  </div>
36
15
  </div>
37
16
  <div class="controls">
@@ -42,8 +21,8 @@
42
21
  </div>
43
22
  <div class="control seek"><div class="slider"></div></div>
44
23
  <div class="control infos timer">
45
- <span class="info time" data-control="time-cur" title="{{__ 'Current playback position'}}">00:00</span>
46
- <span class="info time" data-control="time-end" title="{{__ 'Total duration'}}">00:00</span>
24
+ <span class="info time" data-control="time-cur" title="{{__ 'Current playback position'}}">--:--</span>
25
+ <span class="info time" data-control="time-end" title="{{__ 'Total duration'}}">--:--</span>
47
26
  </div>
48
27
  <div class="control actions sound">
49
28
  <div class="volume"><div class="slider"></div></div>
@@ -0,0 +1 @@
1
+ <source src="{{src}}" type="{{type}}">
@@ -0,0 +1,6 @@
1
+ <video class="media video" preload="{{preload}}" poster="{{poster}}" controls {{#if cors}}crossorigin{{/if}}>
2
+ {{__ 'Your browser doesn’t support the video player.'}}
3
+ {{#if link}}
4
+ <a href="{{link}}">{{__ 'Please download the video and view offline.'}}</a>
5
+ {{/if}}
6
+ </video>
@@ -0,0 +1 @@
1
+ <div class="media video youtube" data-video-src="{{src}}" data-video-id="{{id}}" data-type="youtube"></div>
@@ -0,0 +1,184 @@
1
+ /**
2
+ * This program is free software; you can redistribute it and/or
3
+ * modify it under the terms of the GNU General Public License
4
+ * as published by the Free Software Foundation; under version 2
5
+ * of the License (non-upgradable).
6
+ *
7
+ * This program is distributed in the hope that it will be useful,
8
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ * GNU General Public License for more details.
11
+ *
12
+ * You should have received a copy of the GNU General Public License
13
+ * along with this program; if not, write to the Free Software
14
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15
+ *
16
+ * Copyright (c) 2021 (original work) Open Assessment Technologies SA ;
17
+ */
18
+
19
+ /**
20
+ * Creates a reminder manager.
21
+ *
22
+ * A reminder manager allows to register callback functions that will be called after a particular amount of time.
23
+ * The schedule can be created and cancelled at any time.
24
+ *
25
+ * @example
26
+ * // Create a reminder manager
27
+ * const manager = reminderManagerFactory();
28
+ *
29
+ * // Add a reminder that will be called after 2s (delay is given in milliseconds)
30
+ * manager.remind(() => console.log('Hello!'), 2000);
31
+ *
32
+ * // Start the schedule
33
+ * manager.start();
34
+ *
35
+ * // We can know how many time elapsed since the last schedule
36
+ * const elapsed = manager.elapsed;
37
+ *
38
+ * // The schedule can be cancelled
39
+ * if (needToCancel) {
40
+ * manager.stop();
41
+ * }
42
+ *
43
+ * // The schedule should be cancelled
44
+ * console.log('schedule running:', manager.running)
45
+ *
46
+ * @returns {reminderManager}
47
+ */
48
+ export default function reminderManagerFactory() {
49
+ // Keep track of the running state
50
+ let running = false;
51
+
52
+ // Timestamp of the last start
53
+ let last = 0;
54
+
55
+ // A list of reminders to callback
56
+ const reminders = new Map();
57
+
58
+ /**
59
+ * Cancels a schedule for a particular reminder.
60
+ * @param {object} state - A sate object containing the timeout handler for the reminder.
61
+ * @private
62
+ */
63
+ const stopReminder = state => {
64
+ if (state && state.timeout) {
65
+ clearTimeout(state.timeout);
66
+ state.timeout = null;
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Cancel the schedule for all reminders.
72
+ * @private
73
+ */
74
+ const stopAllReminders = () => reminders.forEach(stopReminder);
75
+
76
+ /**
77
+ * Schedule all reminders.
78
+ * @private
79
+ */
80
+ const startAllReminders = () => {
81
+ reminders.forEach((state, reminder) => {
82
+ stopReminder(state);
83
+ state.timeout = setTimeout(reminder, state.delay);
84
+ });
85
+ };
86
+
87
+ /**
88
+ * Defines the API of a reminder manager.
89
+ *
90
+ * A reminder manager allows to register callback functions that will be called after a particular amount of time.
91
+ * The schedule can be created and cancelled at any time.
92
+ *
93
+ * @namespace reminderManager
94
+ */
95
+ return {
96
+ /**
97
+ * Tells whether or not the schedule is running.
98
+ * @type {boolean}
99
+ * @member running
100
+ * @memberOf reminderManager
101
+ */
102
+ get running() {
103
+ return running;
104
+ },
105
+
106
+ /**
107
+ * Gives the amount of time elapsed since the start of the schedule. It is given in milliseconds.
108
+ * If the schedule is not running, it will always be 0.
109
+ * @type {number}
110
+ * @member running
111
+ * @memberOf reminderManager
112
+ */
113
+ get elapsed() {
114
+ if (!running) {
115
+ return 0;
116
+ }
117
+ return performance.now() - last;
118
+ },
119
+
120
+ /**
121
+ * Schedules all reminders from now on.
122
+ *
123
+ * @returns {reminderManager}
124
+ * @function start
125
+ * @memberOf reminderManager
126
+ */
127
+ start() {
128
+ running = true;
129
+ last = performance.now();
130
+ startAllReminders();
131
+ return this;
132
+ },
133
+
134
+ /**
135
+ * Cancels all scheduled reminders.
136
+ *
137
+ * @returns {reminderManager}
138
+ * @function stop
139
+ * @memberOf reminderManager
140
+ */
141
+ stop() {
142
+ running = false;
143
+ stopAllReminders();
144
+ return this;
145
+ },
146
+
147
+ /**
148
+ * Adds a callback to be scheduled.
149
+ * It won't be scheduled until the schedule is restarted.
150
+ *
151
+ * @param {Function} cb - A function to call after the delay elapsed.
152
+ * @param {number} delay - The delay after what call back the reminder. It is given in milliseconds.
153
+ * @returns {reminderManager}
154
+ * @function remind
155
+ * @memberOf reminderManager
156
+ */
157
+ remind(cb, delay) {
158
+ if ('function' === typeof cb && delay) {
159
+ stopReminder(reminders.get(cb));
160
+ reminders.set(cb, { delay });
161
+ }
162
+ return this;
163
+ },
164
+
165
+ /**
166
+ * Removes a scheduled callback. If a schedule was running, it will be cancelled first.
167
+ *
168
+ * @param {Function} [cb] - The callback function to remove. If omitted, all reminders will be removed.
169
+ * @returns {reminderManager}
170
+ * @function forget
171
+ * @memberOf reminderManager
172
+ */
173
+ forget(cb) {
174
+ if ('undefined' !== typeof cb) {
175
+ stopReminder(reminders.get(cb));
176
+ reminders.delete(cb);
177
+ } else {
178
+ stopAllReminders();
179
+ reminders.clear();
180
+ }
181
+ return this;
182
+ }
183
+ };
184
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * This program is free software; you can redistribute it and/or
3
+ * modify it under the terms of the GNU General Public License
4
+ * as published by the Free Software Foundation; under version 2
5
+ * of the License (non-upgradable).
6
+ *
7
+ * This program is distributed in the hope that it will be useful,
8
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ * GNU General Public License for more details.
11
+ *
12
+ * You should have received a copy of the GNU General Public License
13
+ * along with this program; if not, write to the Free Software
14
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15
+ *
16
+ * Copyright (c) 2021 (original work) Open Assessment Technologies SA ;
17
+ */
18
+
19
+ import eventifier from 'core/eventifier';
20
+
21
+ /**
22
+ * Creates a time observer.
23
+ *
24
+ * It observes the updates applied to a timeline, raising a flag when an irregularity occurs.
25
+ *
26
+ * It works as follow:
27
+ * - an initial state is defined (example: current: 0, duration: 100)
28
+ * - each time a position is forced (say the position is changed outside of the regular time update), the observer needs
29
+ * to be notified.
30
+ * - each time the position is updated (say regular time update), the observer needs to be called.
31
+ * - if the difference between the last regular update and the last one is too high, an event is triggered
32
+ *
33
+ * @example
34
+ * // Create a time observer with an expected interval of 2 seconds
35
+ * const observer = timeObserverFactory(2);
36
+ *
37
+ * // Init the state
38
+ * observer.start(player.position, player.duration);
39
+ *
40
+ * // Update on a regular basis
41
+ * player.on('timeupdate', () => observer.update(player.position));
42
+ *
43
+ * // Notify any position change outside of the regular update
44
+ * player.on('seek', () => observer.seek(player.position));
45
+ *
46
+ * // Gets informed from any irregularity
47
+ * observer.on('irregularity', () => console.log('irregular jump in time');
48
+ *
49
+ * @param {number} interval - The typical interval expected between two updates. It is given in seconds.
50
+ * @returns {timeObserver}
51
+ */
52
+ export default function timeObserverFactory(interval = 1) {
53
+ // Current time position
54
+ let position = 0;
55
+
56
+ // Total duration expected
57
+ let duration = 0;
58
+
59
+ // Last position forced
60
+ let seek = 0;
61
+
62
+ /**
63
+ * Defines the API of a time observer.
64
+ *
65
+ * It observes the updates applied to a timeline, raising a flag when an irregularity occurs.
66
+ *
67
+ * @namespace timeObserver
68
+ */
69
+ return eventifier({
70
+ /**
71
+ * Gets the current time position reported to the observer.
72
+ *
73
+ * @returns {number}
74
+ * @type {number}
75
+ * @member position
76
+ * @memberOf timeObserver
77
+ */
78
+ get position() {
79
+ return position;
80
+ },
81
+
82
+ /**
83
+ * Gets the total duration reported to the observer.
84
+ *
85
+ * @returns {number}
86
+ * @type {number}
87
+ * @member duration
88
+ * @memberOf timeObserver
89
+ */
90
+ get duration() {
91
+ return duration;
92
+ },
93
+
94
+ /**
95
+ * Initialises the time state.
96
+ *
97
+ * @param {number} initPosition - The initial time position
98
+ * @param {number} initDuration - The total duration expected
99
+ * @returns {timeObserver}
100
+ * @function init
101
+ * @memberOf timeObserver
102
+ */
103
+ init(initPosition, initDuration) {
104
+ position = seek = initPosition;
105
+ duration = initDuration;
106
+ return this;
107
+ },
108
+
109
+ /**
110
+ * Updates the time position. If the difference with the previous update is too high, an `irregularity` event
111
+ * will be emitted.
112
+ *
113
+ * @param {number} newPosition - The new time position
114
+ * @returns {timeObserver}
115
+ *
116
+ * @fires irregularity
117
+ */
118
+ update(newPosition) {
119
+ if (newPosition > seek && newPosition - position > interval) {
120
+ /**
121
+ * Notifies an irregularity in the time update
122
+ * @event irregularity
123
+ * @param {number} position - last regular position
124
+ * @param {number} newPosition - new irregular position
125
+ */
126
+ this.trigger('irregularity', position, newPosition);
127
+ }
128
+ position = newPosition;
129
+ return this;
130
+ },
131
+
132
+ /**
133
+ * Notifies the observer about a change in the position outside of the regular update.
134
+ *
135
+ * @param {number} seekPosition
136
+ * @returns {timeObserver}
137
+ */
138
+ seek(seekPosition) {
139
+ position = seek = seekPosition;
140
+ return this;
141
+ }
142
+ });
143
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * This program is free software; you can redistribute it and/or
3
+ * modify it under the terms of the GNU General Public License
4
+ * as published by the Free Software Foundation; under version 2
5
+ * of the License (non-upgradable).
6
+ *
7
+ * This program is distributed in the hope that it will be useful,
8
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ * GNU General Public License for more details.
11
+ *
12
+ * You should have received a copy of the GNU General Public License
13
+ * along with this program; if not, write to the Free Software
14
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15
+ *
16
+ * Copyright (c) 2015-2021 (original work) Open Assessment Technologies SA ;
17
+ */
18
+
19
+ import $ from 'jquery';
20
+
21
+ /**
22
+ * CDN for the YouTube API
23
+ * @type {String}
24
+ */
25
+ const youtubeApi = 'https://www.youtube.com/iframe_api';
26
+
27
+ /**
28
+ * A Regex to extract ID from Youtube URLs
29
+ * @type {RegExp}
30
+ */
31
+ const reYoutube = /([?&\/]v[=\/])([\w-]+)([&\/]?)/;
32
+
33
+ /**
34
+ * Installs a Youtube player. The Youtube API must be ready
35
+ * @param {String|jQuery|HTMLElement} elem
36
+ * @param {Object} player
37
+ * @param {Object} [options]
38
+ * @param {Boolean} [options.controls]
39
+ */
40
+ function addYoutubePlayer(elem, player, options = {}) {
41
+ const $elem = $(elem);
42
+
43
+ new window.YT.Player($elem.get(0), {
44
+ height: '360',
45
+ width: '640',
46
+ videoId: $elem.data('videoId'),
47
+ playerVars: {
48
+ //hd: true,
49
+ autoplay: 0,
50
+ controls: options && options.controls ? 1 : 0,
51
+ rel: 0,
52
+ showinfo: 0,
53
+ wmode: 'transparent',
54
+ modestbranding: 1,
55
+ disablekb: 1,
56
+ playsinline: 1,
57
+ enablejsapi: 1,
58
+ origin: location.hostname
59
+ },
60
+ events: {
61
+ onReady: ev => player.onReady(ev),
62
+ onStateChange: ev => player.onStateChange(ev)
63
+ }
64
+ });
65
+ }
66
+
67
+ /**
68
+ * A local manager for Youtube players.
69
+ * Relies on https://developers.google.com/youtube/iframe_api_reference
70
+ * @type {Object}
71
+ */
72
+ export default function youtubeManagerFactory() {
73
+ // The Youtube API injection state
74
+ let injected = false;
75
+
76
+ // The Youtube API ready state
77
+ let ready = false;
78
+
79
+ // A list of pending players
80
+ let pending = [];
81
+
82
+ /**
83
+ * Checks if the Youtube API is ready to use
84
+ * @returns {Boolean}
85
+ */
86
+ function isApiReady() {
87
+ const apiReady = typeof window.YT !== 'undefined' && typeof window.YT.Player !== 'undefined';
88
+ if (apiReady && !ready) {
89
+ ready = true;
90
+ pending.forEach(args => {
91
+ if (args) {
92
+ addYoutubePlayer(...args);
93
+ }
94
+ });
95
+ pending = [];
96
+ }
97
+ return apiReady;
98
+ }
99
+
100
+ /**
101
+ * Injects the Youtube API into the page
102
+ */
103
+ function injectApi() {
104
+ if (!isApiReady()) {
105
+ window.require([youtubeApi], () => {
106
+ const check = () => {
107
+ if (!isApiReady()) {
108
+ setTimeout(check, 100);
109
+ }
110
+ };
111
+ check();
112
+ });
113
+ }
114
+
115
+ injected = true;
116
+ }
117
+
118
+ return {
119
+ /**
120
+ * Adds a Youtube player
121
+ * @param {String|jQuery|HTMLElement} elem
122
+ * @param {Object} player
123
+ * @param {Object} [options]
124
+ * @param {Boolean} [options.controls]
125
+ */
126
+ add(elem, player, options) {
127
+ if (ready) {
128
+ addYoutubePlayer(elem, player, options);
129
+ } else {
130
+ pending.push([elem, player, options]);
131
+
132
+ if (!injected) {
133
+ injectApi();
134
+ }
135
+ }
136
+ },
137
+
138
+ /**
139
+ * Removes a pending Youtube player
140
+ * @param {String|jQuery|HTMLElement} elem
141
+ * @param {Object} player
142
+ */
143
+ remove(elem, player) {
144
+ pending.forEach((args, idx) => {
145
+ if (args && elem === args[0] && player === args[1]) {
146
+ pending[idx] = null;
147
+ }
148
+ });
149
+ },
150
+
151
+ /**
152
+ * Extracts the ID of a Youtube video from an URL
153
+ * @param {String} url
154
+ * @returns {String}
155
+ */
156
+ extractYoutubeId(url) {
157
+ const res = reYoutube.exec(url);
158
+ return (res && res[2]) || url;
159
+ }
160
+ };
161
+ }