videojs-mobile-ui 1.2.0-alpha.0 → 1.2.1

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 (53) hide show
  1. package/README.md +104 -53
  2. package/dist/lang/de.js +1 -3
  3. package/dist/lang/en.js +1 -3
  4. package/dist/lang/it.js +1 -3
  5. package/dist/videojs-mobile-ui.cjs.js +195 -43
  6. package/dist/videojs-mobile-ui.css +104 -2
  7. package/dist/videojs-mobile-ui.es.js +195 -43
  8. package/dist/videojs-mobile-ui.js +204 -53
  9. package/dist/videojs-mobile-ui.min.js +2 -2
  10. package/docs/api/TouchOverlay.html +964 -0
  11. package/docs/api/fonts/OpenSans-Bold-webfont.eot +0 -0
  12. package/docs/api/fonts/OpenSans-Bold-webfont.svg +1830 -0
  13. package/docs/api/fonts/OpenSans-Bold-webfont.woff +0 -0
  14. package/docs/api/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  15. package/docs/api/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  16. package/docs/api/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  17. package/docs/api/fonts/OpenSans-Italic-webfont.eot +0 -0
  18. package/docs/api/fonts/OpenSans-Italic-webfont.svg +1830 -0
  19. package/docs/api/fonts/OpenSans-Italic-webfont.woff +0 -0
  20. package/docs/api/fonts/OpenSans-Light-webfont.eot +0 -0
  21. package/docs/api/fonts/OpenSans-Light-webfont.svg +1831 -0
  22. package/docs/api/fonts/OpenSans-Light-webfont.woff +0 -0
  23. package/docs/api/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  24. package/docs/api/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  25. package/docs/api/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  26. package/docs/api/fonts/OpenSans-Regular-webfont.eot +0 -0
  27. package/docs/api/fonts/OpenSans-Regular-webfont.svg +1831 -0
  28. package/docs/api/fonts/OpenSans-Regular-webfont.woff +0 -0
  29. package/docs/api/global.html +957 -0
  30. package/docs/api/index.html +159 -0
  31. package/docs/api/plugin.js.html +221 -0
  32. package/docs/api/scripts/linenumber.js +25 -0
  33. package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
  34. package/docs/api/scripts/prettify/lang-css.js +2 -0
  35. package/docs/api/scripts/prettify/prettify.js +28 -0
  36. package/docs/api/styles/jsdoc-default.css +358 -0
  37. package/docs/api/styles/prettify-jsdoc.css +111 -0
  38. package/docs/api/styles/prettify-tomorrow.css +132 -0
  39. package/docs/api/swipeFullscreen.js.html +173 -0
  40. package/docs/api/touchOverlay.js.html +211 -0
  41. package/index.html +238 -170
  42. package/package.json +23 -13
  43. package/scripts/lang.js +24 -0
  44. package/scripts/netlify.js +16 -0
  45. package/scripts/postcss.config.js +29 -5
  46. package/scripts/readme-options.js +370 -0
  47. package/scripts/rollup.config.js +0 -8
  48. package/src/plugin.css +6 -0
  49. package/src/plugin.js +65 -39
  50. package/src/swipeFullscreen.js +122 -0
  51. package/src/touchOverlay.js +7 -3
  52. package/test/plugin.test.js +125 -18
  53. package/test/swipeFullscreen.test.js +365 -0
@@ -0,0 +1,370 @@
1
+ /* eslint-disable no-console */
2
+
3
+ const fs = require('fs');
4
+
5
+ const PATHS = {
6
+ plugin: './src/plugin.js',
7
+ readme: './README.md'
8
+ };
9
+
10
+ const COLOURS = {
11
+ blue: '\x1b[34m',
12
+ red: '\x1b[31m',
13
+ reset: '\x1b[0m'
14
+ };
15
+
16
+ /**
17
+ * Main execution entry point. Reads files, validates data, and updates README.
18
+ */
19
+ function main() {
20
+ try {
21
+ const pluginContent = fs.readFileSync(PATHS.plugin, 'utf8');
22
+ const readmeContent = fs.readFileSync(PATHS.readme, 'utf8');
23
+
24
+ console.log(`Reading source from ${PATHS.plugin}...`);
25
+
26
+ const properties = parseJSDoc(pluginContent);
27
+
28
+ if (!properties || properties.length === 0) {
29
+ console.error('Could not find "MobileUiOptions" JSDoc in plugin.js');
30
+ return;
31
+ }
32
+
33
+ const defaultsObj = extractAndParseDefaults(pluginContent);
34
+ const defaultsCodeBlock = extractDefaultsCodeBlock(pluginContent);
35
+
36
+ if (defaultsObj) {
37
+ const structureValid = validateStructure(properties, defaultsObj);
38
+ const valuesValid = validateValues(properties, defaultsObj);
39
+
40
+ if (!structureValid || !valuesValid) {
41
+ console.error(`${COLOURS.red}Validation failed. Please fix the inconsistencies above.${COLOURS.reset}`);
42
+ process.exit(1);
43
+ }
44
+ } else {
45
+ console.warn('Could not parse "defaults" object from code. Skipping validation.');
46
+ }
47
+
48
+ const publicProperties = properties.filter(p => !p.isInternal);
49
+ const newDocsMarkdown = generateMarkdown(publicProperties);
50
+
51
+ console.log(`Generated documentation for ${publicProperties.length} public properties.`);
52
+ console.log(`Updating ${PATHS.readme}...`);
53
+
54
+ const updatedReadme = updateReadmeContent(readmeContent, newDocsMarkdown, defaultsCodeBlock);
55
+
56
+ fs.writeFileSync(PATHS.readme, updatedReadme);
57
+ console.log('README.md updated successfully!');
58
+
59
+ } catch (err) {
60
+ console.error('Error:', err.message);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * -------------------------------------------------------
67
+ * PARSING
68
+ * -------------------------------------------------------
69
+ */
70
+
71
+ /**
72
+ * Parses the JSDoc @typedef block for MobileUiOptions into an array of property objects.
73
+ *
74
+ * @param {string} content - The file content to parse.
75
+ * @return {Array} List of property objects.
76
+ */
77
+ function parseJSDoc(content) {
78
+ const blockRegex = /\/\*\*[\s\S]*?@typedef \{Object\} MobileUiOptions[\s\S]*?\*\//;
79
+ const match = content.match(blockRegex);
80
+
81
+ if (!match) {
82
+ return [];
83
+ }
84
+
85
+ const lines = match[0].split('\n');
86
+ const properties = [];
87
+ let currentProp = null;
88
+ let nextIsInternal = false;
89
+
90
+ const propertyRegex = /@property\s+\{(.+?)\}\s+(?:\[(.+?)\]|(\S+))/;
91
+
92
+ lines.forEach(line => {
93
+ const cleanLine = line.trim().replace(/^\*\s?/, '').trim();
94
+
95
+ if (cleanLine.includes('@internal')) {
96
+ nextIsInternal = true;
97
+ return;
98
+ }
99
+
100
+ const propMatch = cleanLine.match(propertyRegex);
101
+
102
+ if (propMatch) {
103
+ if (currentProp) {
104
+ properties.push(currentProp);
105
+ }
106
+
107
+ currentProp = {
108
+ name: propMatch[2] || propMatch[3],
109
+ type: propMatch[1],
110
+ description: [],
111
+ isInternal: nextIsInternal
112
+ };
113
+
114
+ nextIsInternal = false;
115
+
116
+ } else if (currentProp && !cleanLine.startsWith('@') && cleanLine !== '/') {
117
+ if (cleanLine.length > 0) {
118
+ currentProp.description.push(cleanLine);
119
+ }
120
+ }
121
+ });
122
+
123
+ if (currentProp) {
124
+ properties.push(currentProp);
125
+ }
126
+
127
+ return properties;
128
+ }
129
+
130
+ /**
131
+ * Extracts and evaluates the 'defaults' object literal from the source code.
132
+ *
133
+ * @param {string} content - The file content.
134
+ * @return {Object|null} The evaluated JavaScript object or null.
135
+ */
136
+ function extractAndParseDefaults(content) {
137
+ const regex = /const defaults = (\{[\s\S]*?^\};)/m;
138
+ const match = content.match(regex);
139
+
140
+ if (!match) {
141
+ return null;
142
+ }
143
+
144
+ try {
145
+ /* eslint-disable-next-line no-new-func */
146
+ const func = new Function('return ' + match[1]);
147
+
148
+ return func();
149
+ } catch (e) {
150
+ console.error('Failed to parse defaults object:', e.message);
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Extracts the raw string representation of the 'defaults' object for display.
157
+ *
158
+ * @param {string} content - The file content.
159
+ * @return {string|null} The raw code block string.
160
+ */
161
+ function extractDefaultsCodeBlock(content) {
162
+ const regex = /const defaults = (\{[\s\S]*?^\};)/m;
163
+ const match = content.match(regex);
164
+
165
+ return match ? match[1] : null;
166
+ }
167
+
168
+ /**
169
+ * -------------------------------------------------------
170
+ * VALIDATION
171
+ * -------------------------------------------------------
172
+ */
173
+
174
+ /**
175
+ * Validates that all JSDoc properties exist in defaults and vice-versa.
176
+ *
177
+ * @param {Array} properties - Parsed JSDoc properties.
178
+ * @param {Object} defaults - The actual code defaults object.
179
+ * @return {boolean} True if valid.
180
+ */
181
+ function validateStructure(properties, defaults) {
182
+ let isValid = true;
183
+ const errors = [];
184
+ const defaultsKeys = flattenKeys(defaults);
185
+
186
+ // 1. Check JSDoc -> Code
187
+ properties.filter(p => !p.isInternal).forEach(prop => {
188
+ // Check if prop is a leaf node or a container object in defaults
189
+ const isPresent = defaultsKeys.includes(prop.name) ||
190
+ defaultsKeys.some(k => k.startsWith(prop.name + '.'));
191
+
192
+ if (!isPresent) {
193
+ errors.push(`MISSING IN DEFAULTS: JSDoc property "${prop.name}" is missing from the defaults object.`);
194
+ isValid = false;
195
+ }
196
+ });
197
+
198
+ // 2. Check Code -> JSDoc
199
+ defaultsKeys.forEach(defKey => {
200
+ const isDocumented = properties.some(p => p.name === defKey);
201
+
202
+ if (!isDocumented) {
203
+ errors.push(`UNDOCUMENTED PROPERTY: Defaults contains "${defKey}" which is not in the JSDoc.`);
204
+ isValid = false;
205
+ }
206
+ });
207
+
208
+ if (!isValid) {
209
+ console.error(`--- ${COLOURS.blue}DEFAULTS MISMATCH${COLOURS.reset} --------`);
210
+ errors.forEach(e => console.error(e));
211
+ console.error('-------------------------------');
212
+ }
213
+
214
+ return isValid;
215
+ }
216
+
217
+ /**
218
+ * Verifies that JSDoc 'Default' descriptions match actual code values.
219
+ *
220
+ * @param {Array} properties - Parsed JSDoc properties.
221
+ * @param {Object} defaults - The actual code defaults object.
222
+ * @return {boolean} True if valid.
223
+ */
224
+ function validateValues(properties, defaults) {
225
+ let isValid = true;
226
+ const errors = [];
227
+
228
+ properties.forEach(prop => {
229
+ const defaultLine = prop.description.find(line => line.startsWith('Default '));
230
+
231
+ if (defaultLine) {
232
+ let rawValue = defaultLine.substring('Default '.length).trim();
233
+
234
+ rawValue = rawValue.replace(/\.$/, '').replace(/^`|`$/g, '');
235
+
236
+ const expectedValue = castValueToType(rawValue, prop.type);
237
+ const actualValue = getNestedValue(defaults, prop.name);
238
+
239
+ if (actualValue !== undefined && actualValue !== expectedValue) {
240
+ errors.push(`VALUE MISMATCH for "${prop.name}":\n` +
241
+ ` - JSDoc expects: ${JSON.stringify(expectedValue)}\n` +
242
+ ` - Code has: ${JSON.stringify(actualValue)}`);
243
+ isValid = false;
244
+ }
245
+ }
246
+ });
247
+
248
+ if (!isValid) {
249
+ console.error(`--- ${COLOURS.blue}VALUE VALIDATION ERRORS${COLOURS.reset} ---`);
250
+ errors.forEach(e => console.error(e));
251
+ console.error('-------------------------------');
252
+ }
253
+
254
+ return isValid;
255
+ }
256
+
257
+ /**
258
+ * Recursively flattens an object into an array of dot-notation keys.
259
+ *
260
+ * @param {Object} obj - The object to flatten.
261
+ * @param {string} prefix - The current key path prefix.
262
+ * @return {Array<string>} Array of keys.
263
+ */
264
+ function flattenKeys(obj, prefix = '') {
265
+ let keys = [];
266
+
267
+ for (const key in obj) {
268
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
269
+ const fullKey = prefix ? `${prefix}.${key}` : key;
270
+
271
+ if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
272
+ keys = keys.concat(flattenKeys(obj[key], fullKey));
273
+ } else {
274
+ keys.push(fullKey);
275
+ }
276
+ }
277
+ }
278
+ return keys;
279
+ }
280
+
281
+ /**
282
+ * Casts a string value to a JavaScript type based on the JSDoc type definition.
283
+ *
284
+ * @param {string} str - The raw string value.
285
+ * @param {string} type - The JSDoc type (e.g., 'boolean', 'number').
286
+ * @return {*} The casted value.
287
+ */
288
+ function castValueToType(str, type) {
289
+ const lowerType = type.toLowerCase();
290
+
291
+ if (lowerType === 'boolean') {
292
+ return str === 'true';
293
+ }
294
+ if (lowerType === 'number') {
295
+ return Number(str);
296
+ }
297
+ if (lowerType === 'string') {
298
+ return str.replace(/^['"]|['"]$/g, '');
299
+ }
300
+ try {
301
+ return JSON.parse(str);
302
+ } catch (e) {
303
+ return str;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Retrieves a value from a nested object using a dot-notation path string.
309
+ *
310
+ * @param {Object} obj - The source object.
311
+ * @param {string} path - The dot-notation path (e.g., 'fullscreen.enterOnRotate').
312
+ * @return {*} The found value or undefined.
313
+ */
314
+ function getNestedValue(obj, path) {
315
+ return path.split('.').reduce((prev, curr) => {
316
+ return prev ? prev[curr] : undefined;
317
+ }, obj);
318
+ }
319
+
320
+ /**
321
+ * -------------------------------------------------------
322
+ * OUTPUT GENERATION
323
+ * -------------------------------------------------------
324
+ */
325
+
326
+ /**
327
+ * Formats property objects into a specific Markdown list style.
328
+ *
329
+ * @param {Array} properties - List of property objects.
330
+ * @return {string} Formatted Markdown string.
331
+ */
332
+ function generateMarkdown(properties) {
333
+ return properties.map(prop => {
334
+ const descString = prop.description.join(' \n ');
335
+
336
+ return `- **\`${prop.name}\`** {${prop.type}} \n ${descString}`;
337
+ }).join('\n');
338
+ }
339
+
340
+ /**
341
+ * Replaces specific sections in the README content with new documentation.
342
+ *
343
+ * @param {string} readmeContent - The original README text.
344
+ * @param {string} newDocs - The new options list Markdown.
345
+ * @param {string} defaultsCodeBlock - The new defaults code block.
346
+ * @return {string} The updated README text.
347
+ */
348
+ function updateReadmeContent(readmeContent, newDocs, defaultsCodeBlock) {
349
+ let content = readmeContent;
350
+
351
+ if (defaultsCodeBlock) {
352
+ const defaultsRegex = /(### Default options)([\s\S]*?)(### Options)/;
353
+
354
+ if (defaultsRegex.test(content)) {
355
+ const codeBlock = `\`\`\`js\n${defaultsCodeBlock}\n\`\`\``;
356
+
357
+ content = content.replace(defaultsRegex, `$1\n\n${codeBlock}\n\n$3`);
358
+ }
359
+ }
360
+
361
+ const optionsRegex = /(### Options)([\s\S]*?)(## Installation)/;
362
+
363
+ if (!optionsRegex.test(content)) {
364
+ throw new Error('Could not find "### Options" section followed by "## Installation" in README.md');
365
+ }
366
+
367
+ return content.replace(optionsRegex, `$1\n\n${newDocs}\n\n$3`);
368
+ }
369
+
370
+ main();
@@ -7,13 +7,5 @@ const config = generate(options);
7
7
 
8
8
  // Add additonal builds/customization here!
9
9
 
10
- config.settings.globals.browser['@ungap/global-this'] = 'window';
11
- config.settings.globals.module['@ungap/global-this'] = 'window';
12
- config.settings.externals.browser.push('@ungap/global-this');
13
- config.settings.externals.module.push('@ungap/global-this');
14
-
15
- // eslint-disable-next-line no-console
16
- console.log(JSON.stringify(config.settings, null, 2));
17
-
18
10
  // export the builds to rollup
19
11
  export default Object.values(config.builds);
package/src/plugin.css CHANGED
@@ -95,4 +95,10 @@
95
95
  display: none;
96
96
  }
97
97
 
98
+ &:not(:fullscreen).using-fs-swipe-up,
99
+ &:fullscreen.using-fs-swipe-down {
100
+ touch-action: none;
101
+ overflow: hidden;
102
+ }
103
+
98
104
  }
package/src/plugin.js CHANGED
@@ -1,15 +1,64 @@
1
1
  import videojs from 'video.js';
2
2
  import {version as VERSION} from '../package.json';
3
3
  import './touchOverlay.js';
4
- import window from '@ungap/global-this';
4
+ import initSwipe from './swipeFullscreen.js';
5
+ import window from 'global/window';
5
6
 
6
- // Default options for the plugin.
7
+ /**
8
+ * @typedef {Object} MobileUiOptions
9
+ * @property {Object} [fullscreen]
10
+ * Options for fullscreen behaviours.
11
+ * @property {boolean} [fullscreen.enterOnRotate]
12
+ * If the device is rotated, enter fullscreen.
13
+ * Default `true`.
14
+ * @property {boolean} [fullscreen.exitOnRotate]
15
+ * If the device is rotated, exit fullscreen, unless `lockOnRotate` is used.
16
+ * Default `true`.
17
+ * @property {boolean} [fullscreen.lockOnRotate]
18
+ * When going fullscreen in response to rotation (`enterOnRotate`), also lock the orientation (not supported by iOS).
19
+ * Default `true`.
20
+ * @property {boolean} [fullscreen.lockToLandscapeOnEnter]
21
+ * When fullscreen is entered by any means, lock the orientation (not supported by iOS).
22
+ * Default `false`.
23
+ * @property {boolean} [fullscreen.swipeToFullscreen]
24
+ * Swipe up to enter fullscreen.
25
+ * Default `false`.
26
+ * @property {boolean} [fullscreen.swipeFromFullscreen]
27
+ * Swipe down to exit fullscreen.
28
+ * Won't do anything on iOS native fullscreen, which has its own swipe down exit gesture.
29
+ * Default `false`.
30
+ * @property {boolean} [fullscreen.disabled]
31
+ * All fullscreen functionality provided by this plugin disabled.
32
+ * Default `false`.
33
+ * @property {Object} [touchControls]
34
+ * Options for tap overlay.
35
+ * @property {number} [touchControls.seekSeconds]
36
+ * Increment to seek in seconds.
37
+ * Default `10`.
38
+ * @property {number} [touchControls.tapTimeout]
39
+ * Timeout to consider multiple taps as double rather than two single.
40
+ * Default `300`.
41
+ * @property {boolean} [touchControls.disableOnEnd]
42
+ * Disable the touch overlay when the video ends.
43
+ * Useful if an end screen overlay is used to avoid conflict.
44
+ * Default `false`.
45
+ * @property {boolean} [touchControls.disabled]
46
+ * All tap overlay functionality provided by this plugin disabled.
47
+ * Default `false`.
48
+ * @internal
49
+ * @property {boolean} [forceForTesting]
50
+ * Used in unit tests
51
+ */
52
+
53
+ /** @type {MobileUiOptions} */
7
54
  const defaults = {
8
55
  fullscreen: {
9
56
  enterOnRotate: true,
10
57
  exitOnRotate: true,
11
58
  lockOnRotate: true,
12
59
  lockToLandscapeOnEnter: false,
60
+ swipeToFullscreen: false,
61
+ swipeFromFullscreen: false,
13
62
  disabled: false
14
63
  },
15
64
  touchControls: {
@@ -55,7 +104,7 @@ const getOrientation = () => {
55
104
  * @param {Player} player
56
105
  * A Video.js player object.
57
106
  *
58
- * @param {Object} [options={}]
107
+ * @param {MobileUiOptions} [options={}]
59
108
  * A plain object containing options for the plugin.
60
109
  */
61
110
  const onPlayerReady = (player, options) => {
@@ -77,20 +126,26 @@ const onPlayerReady = (player, options) => {
77
126
  return;
78
127
  }
79
128
 
129
+ if (options.fullscreen.swipeToFullscreen || options.fullscreen.swipeFromFullscreen) {
130
+ initSwipe(player, options);
131
+ }
132
+
80
133
  let locked = false;
81
134
 
82
135
  const rotationHandler = () => {
83
136
  const currentOrientation = getOrientation();
84
137
 
85
138
  if (currentOrientation === 'landscape' && options.fullscreen.enterOnRotate) {
86
- if (player.paused() === false) {
87
- player.requestFullscreen();
139
+ if (!player.paused() && !player.isFullscreen()) {
140
+ player.requestFullscreen().catch((err) => {
141
+ player.log.warn('Browser refused fullscreen request:', err);
142
+ });
88
143
  if ((options.fullscreen.lockOnRotate || options.fullscreen.lockToLandscapeOnEnter) &&
89
144
  screen.orientation && screen.orientation.lock) {
90
145
  screen.orientation.lock('landscape').then(() => {
91
146
  locked = true;
92
- }).catch((e) => {
93
- videojs.log('Browser refused orientation lock:', e);
147
+ }).catch((err) => {
148
+ videojs.log.warn('Browser refused orientation lock:', err);
94
149
  });
95
150
  }
96
151
  }
@@ -119,6 +174,7 @@ const onPlayerReady = (player, options) => {
119
174
  }
120
175
 
121
176
  player.on('fullscreenchange', _ => {
177
+ player.log('fullscreenchange', player.isFullscreen(), options.fullscreen.lockToLandscapeOnEnter, getOrientation());
122
178
  if (player.isFullscreen() && options.fullscreen.lockToLandscapeOnEnter && getOrientation() === 'portrait') {
123
179
  screen.orientation.lock('landscape').then(()=>{
124
180
  locked = true;
@@ -140,40 +196,10 @@ const onPlayerReady = (player, options) => {
140
196
  };
141
197
 
142
198
  /**
143
- * A video.js plugin.
144
- *
145
- * Adds a monile UI for player control, and fullscreen orientation control
199
+ * Adds a mobile UI for player control, and fullscreen orientation control
146
200
  *
147
201
  * @function mobileUi
148
- * @param {Object} [options={}]
149
- * Plugin options.
150
- * @param {boolean} [options.forceForTesting=false]
151
- * Enables the display regardless of user agent, for testing purposes
152
- * @param {Object} [options.fullscreen={}]
153
- * Fullscreen options.
154
- * @param {boolean} [options.fullscreen.disabled=false]
155
- * If true no fullscreen handling except the *deprecated* iOS fullwindow hack
156
- * @param {boolean} [options.fullscreen.enterOnRotate=true]
157
- * Whether to go fullscreen when rotating to landscape
158
- * @param {boolean} [options.fullscreen.exitOnRotate=true]
159
- * Whether to leave fullscreen when rotating to portrait (if not locked)
160
- * @param {boolean} [options.fullscreen.lockOnRotate=true]
161
- * Whether to lock orientation when rotating to landscape
162
- * Unlocked when exiting fullscreen or on 'ended
163
- * @param {boolean} [options.fullscreen.lockToLandscapeOnEnter=false]
164
- * Whether to always lock orientation to landscape on fullscreen mode
165
- * Unlocked when exiting fullscreen or on 'ended'
166
- * @param {Object} [options.touchControls={}]
167
- * Touch UI options.
168
- * @param {boolean} [options.touchControls.disabled=false]
169
- * If true no touch controls are added.
170
- * @param {int} [options.touchControls.seekSeconds=10]
171
- * Number of seconds to seek on double-tap
172
- * @param {int} [options.touchControls.tapTimeout=300]
173
- * Interval in ms to be considered a doubletap
174
- * @param {boolean} [options.touchControls.disableOnEnd=false]
175
- * Whether to disable when the video ends (e.g., if there is an endscreen)
176
- * Never shows if the endscreen plugin is present
202
+ * @param {MobileUiOptions} [options={}] Plugin options
177
203
  */
178
204
  const mobileUi = function(options = {}) {
179
205
  if (options.forceForTesting || videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
@@ -0,0 +1,122 @@
1
+ /** @import Player from 'video.js/dist/types/player' */
2
+ /** @import Plugin from 'video.js/dist/types/plugin' */
3
+ /** @import {MobileUiOptions} from './plugin' */
4
+
5
+ /**
6
+ * Sets up swiping to enter and exit fullscreen.
7
+ *
8
+ * @this {Plugin}
9
+ * @param {Player} player
10
+ * The player to initialise on.
11
+ * @param {MobileUiOptions} pluginOptions
12
+ * The options used by the mobile ui plugin.
13
+ */
14
+ const initSwipe = (player, pluginOptions) => {
15
+ const {swipeToFullscreen, swipeFromFullscreen} = pluginOptions.fullscreen;
16
+
17
+ if (swipeToFullscreen) {
18
+ player.addClass('using-fs-swipe-up');
19
+ }
20
+ if (swipeFromFullscreen) {
21
+ player.addClass('using-fs-swipe-down');
22
+ }
23
+
24
+ let touchStartY = 0;
25
+ let couldBeSwiping = false;
26
+ const swipeThreshold = 30;
27
+
28
+ /**
29
+ * Monitor the possible start of a swipe
30
+ *
31
+ * @param {TouchEvent} e Triggering touch event
32
+ */
33
+ const onStart = (e) => {
34
+ const isFullscreen = player.isFullscreen();
35
+
36
+ if (
37
+ (!isFullscreen && !swipeToFullscreen) ||
38
+ (isFullscreen && !swipeFromFullscreen)
39
+ ) {
40
+ couldBeSwiping = false;
41
+ return;
42
+ }
43
+
44
+ touchStartY = e.changedTouches[0].clientY;
45
+ couldBeSwiping = true;
46
+ player.tech_.el().style.transition = '';
47
+ };
48
+
49
+ /**
50
+ * Monitor the movement of a swipe
51
+ *
52
+ * @param {TouchEvent} e Triggering touch event
53
+ */
54
+ const onMove = (e) => {
55
+ if (!couldBeSwiping) {
56
+ return;
57
+ }
58
+
59
+ const currentY = e.touches[0].clientY;
60
+ const deltaY = touchStartY - currentY;
61
+ const isFullscreen = player.isFullscreen();
62
+
63
+ let scale = 1;
64
+
65
+ if (!isFullscreen && deltaY > 0) {
66
+ // Swiping up to enter fullscreen: Zoom in (Max 1.1)
67
+ scale = 1 + Math.min(0.1, deltaY / 500);
68
+ player.tech_.el().style.transform = `scale(${scale})`;
69
+ } else if (isFullscreen && deltaY < 0) {
70
+ // Swiping down to exit fullscreen: Zoom out (Min 0.9)
71
+ scale = 1 - Math.min(0.1, Math.abs(deltaY) / 500);
72
+ player.tech_.el().style.transform = `scale(${scale})`;
73
+ }
74
+ };
75
+
76
+ /**
77
+ * Monitor the touch end to determine a valid swipe
78
+ *
79
+ * @param {TouchEvent} e Triggering touch event
80
+ */
81
+ const onEnd = (e) => {
82
+ if (!couldBeSwiping) {
83
+ return;
84
+ }
85
+ couldBeSwiping = false;
86
+
87
+ player.tech_.el().style.transition = 'transform 0.3s ease-out';
88
+ player.tech_.el().style.transform = 'scale(1)';
89
+
90
+ if (e.type === 'touchcancel') {
91
+ return;
92
+ }
93
+
94
+ const touchEndY = e.changedTouches[0].clientY;
95
+ const deltaY = touchStartY - touchEndY;
96
+
97
+ if (deltaY > swipeThreshold && !player.isFullscreen()) {
98
+ player.requestFullscreen().catch((err) => {
99
+ player.log.warn('Browser refused fullscreen', err);
100
+ });
101
+ } else if (deltaY < -swipeThreshold && player.isFullscreen()) {
102
+ player.exitFullscreen();
103
+ }
104
+ };
105
+
106
+ player.el().addEventListener('touchstart', onStart, { passive: true });
107
+ player.el().addEventListener('touchmove', onMove, { passive: true });
108
+ player.el().addEventListener('touchend', onEnd, { passive: true });
109
+ player.el().addEventListener('touchcancel', onEnd, { passive: true });
110
+
111
+ player.on('dispose', () => {
112
+ player.el().removeEventListener('touchstart', onStart, { passive: true });
113
+ player.el().removeEventListener('touchmove', onMove, { passive: true });
114
+ player.el().removeEventListener('touchend', onEnd, { passive: true });
115
+ player.el().removeEventListener('touchcancel', onEnd, { passive: true });
116
+ player.tech_.el().style.transform = '';
117
+ player.tech_.el().style.transition = '';
118
+ });
119
+
120
+ };
121
+
122
+ export default initSwipe;
@@ -4,7 +4,9 @@
4
4
  */
5
5
 
6
6
  import videojs from 'video.js';
7
- import window from '@ungap/global-this';
7
+ import window from 'global/window';
8
+
9
+ /** @import Player from 'video.js/dist/types/player' */
8
10
 
9
11
  const Component = videojs.getComponent('Component');
10
12
  const dom = videojs.dom || videojs;
@@ -22,7 +24,7 @@ class TouchOverlay extends Component {
22
24
  * @param {Player} player
23
25
  * The `Player` that this class should be attached to.
24
26
  *
25
- * @param {Object} [options]
27
+ * @param {options} [options]
26
28
  * The key/value store of player options.
27
29
  */
28
30
  constructor(player, options) {
@@ -117,7 +119,9 @@ class TouchOverlay extends Component {
117
119
  return;
118
120
  }
119
121
 
120
- event.preventDefault();
122
+ if (event.cancelable) {
123
+ event.preventDefault();
124
+ }
121
125
 
122
126
  this.taps += 1;
123
127
  if (this.taps === 1) {