@oat-sa/tao-core-ui 1.6.2 → 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.
@@ -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
+ }
@@ -77,7 +77,6 @@ const defaults = {
77
77
  startMuted: false,
78
78
  maxPlays: 0,
79
79
  replayTimeout: 0,
80
- stalledTimeout: 2000,
81
80
  canPause: true,
82
81
  canSeek: true,
83
82
  loop: false,
@@ -190,8 +189,14 @@ const isResponsiveSize = sizeProps => {
190
189
  /**
191
190
  * Builds a media player instance
192
191
  * @param {Object} config
193
- * @param {String} config.type - The type of media to play
194
- * @param {String|Array} config.url - The URL to the media
192
+ * @param {String} config.type - The type of media to play, say `audio`, `video`, or `youtube`. The default is `video`.
193
+ * It might also contain the MIME type of the media as a shorthand.
194
+ * @param {String|Array} [config.url] - The URL to the media. If several media are proposed as alternatives,
195
+ * please look at the `sources` option instead.
196
+ * @param {String} [config.mimeType] - The MIME type of the media. If omitted, the player will try to extract it
197
+ * from the `type` property, otherwise it will request the server to get the content-type.
198
+ * @param {Array} [config.sources] - A list of URL if several media can be proposed. Each entry may be either a
199
+ * string (single URL), or an object containing both the URL and the MIME type ({src: string, type: string}).
195
200
  * @param {String|jQuery|HTMLElement} [config.renderTo] - An optional container in which renders the player
196
201
  * @param {Boolean} [config.canSeek] - The player allows to reach an arbitrary position within the media using the duration bar
197
202
  * @param {Boolean} [config.loop] - The media will be played continuously
@@ -201,15 +206,14 @@ const isResponsiveSize = sizeProps => {
201
206
  * @param {Number} [config.autoStartAt] - The time position at which the player should start
202
207
  * @param {Number} [config.maxPlays] - Sets a few number of plays (default: infinite)
203
208
  * @param {Number} [config.replayTimeout] - disable the possibility to replay a media after this timeout, in seconds (default: 0)
204
- * @param {Number} [config.stalledTimeout] - delay before considering stalled playback (default: 2000)
205
209
  * @param {Number} [config.volume] - Sets the sound volume (default: 80)
206
210
  * @param {Number} [config.width] - Sets the width of the player (default: depends on media type)
207
211
  * @param {Number} [config.height] - Sets the height of the player (default: depends on media type)
208
212
  * @param {Boolean} [config.preview] - Enables the media preview (load media metadata)
209
213
  * @param {Boolean} [config.debug] - Enables the debug mode
214
+ * @param {number} [config.config.stalledDetectionDelay] - The delay before considering a media is stalled
210
215
  * @event render - Event triggered when the player is rendering
211
216
  * @event error - Event triggered when the player throws an unrecoverable error
212
- * @event recovererror - Event triggered when the player throws a recoverable error
213
217
  * @event ready - Event triggered when the player is fully ready
214
218
  * @event play - Event triggered when the playback is starting
215
219
  * @event update - Event triggered while the player is playing
@@ -234,6 +238,9 @@ function mediaplayerFactory(config) {
234
238
  // load the config set, discard null values in order to allow defaults to be set
235
239
  this.config = _.omit(config || {}, value => typeof value === 'undefined' || value === null);
236
240
  _.defaults(this.config, defaults.options);
241
+ if (!this.config.mimeType && 'string' === typeof this.config.type && this.config.type.indexOf('/') > 0) {
242
+ this.config.mimeType = this.config.type;
243
+ }
237
244
  this._setType(this.config.type || defaults.type);
238
245
 
239
246
  this._reset();
@@ -345,23 +352,11 @@ function mediaplayerFactory(config) {
345
352
  */
346
353
  this.trigger('reload');
347
354
 
348
- // destroy player
349
355
  if (this.player) {
350
- this.player.destroy();
351
- }
352
-
353
- // remove events and component
354
- if (this.$component) {
355
- this._unbindEvents();
356
- this._destroySlider(this.$seekSlider);
357
- this._destroySlider(this.$volumeSlider);
358
-
359
- this.$component.remove();
360
- this.$component = null;
356
+ this.player.recover();
361
357
  }
362
-
363
- // rerender
364
- this.render();
358
+ this._setState('stalled', false);
359
+ this.setInitialStates();
365
360
  },
366
361
 
367
362
  /**
@@ -788,8 +783,12 @@ function mediaplayerFactory(config) {
788
783
  source = _.clone(src);
789
784
  }
790
785
 
791
- if (this.is('youtube') && !source.type) {
792
- source.type = defaults.type;
786
+ if (!source.type) {
787
+ if (this.is('youtube')) {
788
+ source.type = defaults.type;
789
+ } else if (this.config.mimeType) {
790
+ source.type = this.config.mimeType;
791
+ }
793
792
  }
794
793
 
795
794
  if (!source.type) {
@@ -872,7 +871,8 @@ function mediaplayerFactory(config) {
872
871
  type: this.getType(),
873
872
  sources: this.getSources(),
874
873
  preview: this.config.preview,
875
- debug: this.config.debug
874
+ debug: this.config.debug,
875
+ stalledDetectionDelay: this.config.stalledDetectionDelay
876
876
  };
877
877
  this.player = playerFactory(this.$player, playerConfig)
878
878
  .on('resize', (width, height) => {
@@ -887,8 +887,7 @@ function mediaplayerFactory(config) {
887
887
  .on('stalled', () => this._onStalled())
888
888
  .on('playing', () => this._onPlaying())
889
889
  .on('end', () => this._onEnd())
890
- .on('error', () => this._onError())
891
- .on('recovererror', fromLoading => this._onRecoverError(fromLoading));
890
+ .on('error', () => this._onError());
892
891
  }
893
892
 
894
893
  if (this.player) {
@@ -903,7 +902,11 @@ function mediaplayerFactory(config) {
903
902
  this._setState('error', error);
904
903
  this._setState('nogui', !support.canControl());
905
904
  this._setState('preview', this.config.preview);
906
- this._setState('loading', true);
905
+ this._setState('loading', !error);
906
+ if (error) {
907
+ this._setState('ready', true);
908
+ this.trigger('ready');
909
+ }
907
910
  },
908
911
 
909
912
  /**
@@ -1236,7 +1239,7 @@ function mediaplayerFactory(config) {
1236
1239
  */
1237
1240
  _onReady() {
1238
1241
  if (this.is('error')) {
1239
- this._onRecoverError();
1242
+ this._setState('error', false);
1240
1243
  }
1241
1244
 
1242
1245
  const duration = this.player.getDuration();
@@ -1264,11 +1267,6 @@ function mediaplayerFactory(config) {
1264
1267
  if (this.config.preview && this.$container && this.config.height && this.config.height !== 'auto') {
1265
1268
  this._setMaxHeight();
1266
1269
  }
1267
-
1268
- // seek back to the previous position after recover from stalled
1269
- if (this.is('stalled')) {
1270
- this.play(this.positionBeforeStalled);
1271
- }
1272
1270
  },
1273
1271
 
1274
1272
  /**
@@ -1332,26 +1330,6 @@ function mediaplayerFactory(config) {
1332
1330
  this.trigger('error');
1333
1331
  },
1334
1332
 
1335
- /**
1336
- * Event called when the media throws recoverable error
1337
- * @param {Boolean} fromLoading - recover from an error while loading the media
1338
- * @private
1339
- */
1340
- _onRecoverError(fromLoading = false) {
1341
- // recover from playing error
1342
- if (fromLoading && this.is('playing')) {
1343
- this.render();
1344
- }
1345
-
1346
- this._setState('error', false);
1347
-
1348
- /**
1349
- * Triggers a recoverable media error event
1350
- * @event mediaplayer#recovererror
1351
- */
1352
- this.trigger('recovererror');
1353
- },
1354
-
1355
1333
  /**
1356
1334
  * Event called when the media is played
1357
1335
  * @private
@@ -1390,10 +1368,11 @@ function mediaplayerFactory(config) {
1390
1368
  this._playingState(false, true);
1391
1369
  this._updatePosition(0);
1392
1370
 
1393
- // disable GUI when the play limit is reached
1371
+ // disable when the play limit is reached
1394
1372
  if (this._playLimitReached()) {
1395
- this._disableGUI();
1396
-
1373
+ if (!this.is('disabled')) {
1374
+ this.disable();
1375
+ }
1397
1376
  /**
1398
1377
  * Triggers a play limit reached event
1399
1378
  * @event mediaplayer#limitreached
@@ -1428,15 +1407,8 @@ function mediaplayerFactory(config) {
1428
1407
  * @private
1429
1408
  */
1430
1409
  _onStalled() {
1431
- this.stalledTimeUpdateCount = 0;
1432
- this.stalledTimer = window.setTimeout(() => {
1433
- const position = this.getPosition();
1434
- if (position) {
1435
- this.positionBeforeStalled = position;
1436
- }
1437
- this._setState('stalled', true);
1438
- this._setState('ready', false);
1439
- }, this.config.stalledTimeout);
1410
+ this._setState('stalled', true);
1411
+ this._setState('ready', false);
1440
1412
  },
1441
1413
 
1442
1414
  /**
@@ -1444,15 +1416,6 @@ function mediaplayerFactory(config) {
1444
1416
  * @private
1445
1417
  */
1446
1418
  _onTimeUpdate() {
1447
- if (this.stalledTimer) {
1448
- if (this.stalledTimeUpdateCount === 5) {
1449
- window.clearTimeout(this.stalledTimer);
1450
- this.stalledTimer = null;
1451
- } else {
1452
- this.stalledTimeUpdateCount++;
1453
- }
1454
- }
1455
-
1456
1419
  this._updatePosition(this.player.getPosition());
1457
1420
 
1458
1421
  /**
@@ -1473,21 +1436,11 @@ function mediaplayerFactory(config) {
1473
1436
  this.timerId = requestAnimationFrame(this._replayTimeout.bind(this));
1474
1437
 
1475
1438
  if (elapsedSeconds >= parseInt(this.config.replayTimeout, 10)) {
1476
- this._disableGUI();
1477
1439
  this.disable();
1478
1440
  cancelAnimationFrame(this.timerId);
1479
1441
  }
1480
1442
  },
1481
1443
 
1482
- /**
1483
- * Disable the player GUI
1484
- * @private
1485
- */
1486
- _disableGUI() {
1487
- this._setState('ready', false);
1488
- this._setState('canplay', false);
1489
- },
1490
-
1491
1444
  /**
1492
1445
  * Checks if the play limit has been reached
1493
1446
  * @returns {Boolean}
@@ -1605,11 +1558,8 @@ function mediaplayerFactory(config) {
1605
1558
  * @private
1606
1559
  */
1607
1560
  execute(command, ...args) {
1608
- const ctx = this.player;
1609
- const method = ctx && ctx[command];
1610
-
1611
- if (_.isFunction(method)) {
1612
- return method.apply(ctx, args);
1561
+ if (this.player && 'function' === typeof this.player[command]) {
1562
+ return this.player[command](...args);
1613
1563
  }
1614
1564
  }
1615
1565
  };