videojs-mobile-ui 1.1.4 → 1.2.0
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.
- package/README.md +104 -53
- package/dist/videojs-mobile-ui.cjs.js +194 -42
- package/dist/videojs-mobile-ui.css +6 -0
- package/dist/videojs-mobile-ui.es.js +194 -42
- package/dist/videojs-mobile-ui.js +194 -42
- package/dist/videojs-mobile-ui.min.js +2 -2
- package/docs/api/TouchOverlay.html +772 -0
- package/docs/api/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/api/fonts/OpenSans-Bold-webfont.svg +1830 -0
- package/docs/api/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/api/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/api/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
- package/docs/api/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/api/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/api/fonts/OpenSans-Italic-webfont.svg +1830 -0
- package/docs/api/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/api/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/api/fonts/OpenSans-Light-webfont.svg +1831 -0
- package/docs/api/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/api/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/api/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
- package/docs/api/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/api/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/api/fonts/OpenSans-Regular-webfont.svg +1831 -0
- package/docs/api/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/api/global.html +1485 -0
- package/docs/api/index.html +159 -0
- package/docs/api/plugin.js.html +266 -0
- package/docs/api/scripts/linenumber.js +25 -0
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/api/scripts/prettify/lang-css.js +2 -0
- package/docs/api/scripts/prettify/prettify.js +28 -0
- package/docs/api/styles/jsdoc-default.css +358 -0
- package/docs/api/styles/prettify-jsdoc.css +111 -0
- package/docs/api/styles/prettify-tomorrow.css +132 -0
- package/docs/api/swipeFullscreen.js.html +173 -0
- package/docs/api/touchOverlay.js.html +203 -0
- package/index.html +238 -170
- package/package.json +6 -4
- package/scripts/netlify.js +16 -0
- package/scripts/readme-options.js +370 -0
- package/src/plugin.css +6 -0
- package/src/plugin.js +64 -38
- package/src/swipeFullscreen.js +122 -0
- package/src/touchOverlay.js +6 -2
- package/test/plugin.test.js +57 -41
- 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();
|
package/src/plugin.css
CHANGED
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 initSwipe from './swipeFullscreen.js';
|
|
4
5
|
import window from 'global/window';
|
|
5
6
|
|
|
6
|
-
|
|
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 {
|
|
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()
|
|
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((
|
|
93
|
-
videojs.log('Browser refused orientation lock:',
|
|
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
|
-
*
|
|
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 {
|
|
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;
|
package/src/touchOverlay.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import videojs from 'video.js';
|
|
7
7
|
import window from 'global/window';
|
|
8
8
|
|
|
9
|
+
/** @import Player from 'video.js/dist/types/player' */
|
|
10
|
+
|
|
9
11
|
const Component = videojs.getComponent('Component');
|
|
10
12
|
const dom = videojs.dom || videojs;
|
|
11
13
|
|
|
@@ -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 {
|
|
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.
|
|
122
|
+
if (event.cancelable) {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
}
|
|
121
125
|
|
|
122
126
|
this.taps += 1;
|
|
123
127
|
if (this.taps === 1) {
|