@cloudnest/redxplyr 1.0.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.
Files changed (127) hide show
  1. package/.editorconfig +10 -0
  2. package/.gitpod.yml +6 -0
  3. package/.node-version +1 -0
  4. package/.prettierrc +7 -0
  5. package/.stickler.yml +5 -0
  6. package/.stylelintrc.json +26 -0
  7. package/CHANGELOG.md +16 -0
  8. package/CONTRIBUTING.md +34 -0
  9. package/CONTROLS.md +49 -0
  10. package/Dockerfile +32 -0
  11. package/LICENSE.md +22 -0
  12. package/README.md +194 -0
  13. package/cspell.json +48 -0
  14. package/dist/redxplyr.css +1 -0
  15. package/dist/redxplyr.js +8801 -0
  16. package/dist/redxplyr.min.js +2 -0
  17. package/dist/redxplyr.min.js.map +1 -0
  18. package/dist/redxplyr.min.mjs +1 -0
  19. package/dist/redxplyr.min.mjs.map +1 -0
  20. package/dist/redxplyr.mjs +8793 -0
  21. package/dist/redxplyr.polyfilled.js +9294 -0
  22. package/dist/redxplyr.polyfilled.min.js +2 -0
  23. package/dist/redxplyr.polyfilled.min.js.map +1 -0
  24. package/dist/redxplyr.polyfilled.min.mjs +1 -0
  25. package/dist/redxplyr.polyfilled.min.mjs.map +1 -0
  26. package/dist/redxplyr.polyfilled.mjs +9286 -0
  27. package/dist/redxplyr.svg +1 -0
  28. package/eslint.config.mjs +39 -0
  29. package/gulpfile.js +8 -0
  30. package/package.json +114 -0
  31. package/pnpm-workspace.yaml +8 -0
  32. package/src/js/captions.js +411 -0
  33. package/src/js/config/defaults.js +459 -0
  34. package/src/js/config/states.js +10 -0
  35. package/src/js/config/types.js +34 -0
  36. package/src/js/console.js +28 -0
  37. package/src/js/controls.js +1870 -0
  38. package/src/js/fullscreen.js +305 -0
  39. package/src/js/html5.js +148 -0
  40. package/src/js/listeners.js +854 -0
  41. package/src/js/media.js +61 -0
  42. package/src/js/plugins/ads.js +647 -0
  43. package/src/js/plugins/preview-thumbnails.js +706 -0
  44. package/src/js/plugins/vimeo.js +443 -0
  45. package/src/js/plugins/youtube.js +451 -0
  46. package/src/js/plyr.d.ts +729 -0
  47. package/src/js/plyr.js +1291 -0
  48. package/src/js/plyr.polyfilled.js +13 -0
  49. package/src/js/source.js +155 -0
  50. package/src/js/storage.js +70 -0
  51. package/src/js/support.js +100 -0
  52. package/src/js/ui.js +297 -0
  53. package/src/js/utils/animation.js +33 -0
  54. package/src/js/utils/arrays.js +23 -0
  55. package/src/js/utils/browser.js +21 -0
  56. package/src/js/utils/elements.js +263 -0
  57. package/src/js/utils/events.js +116 -0
  58. package/src/js/utils/fetch.js +45 -0
  59. package/src/js/utils/i18n.js +47 -0
  60. package/src/js/utils/is.js +81 -0
  61. package/src/js/utils/load-image.js +19 -0
  62. package/src/js/utils/load-script.js +14 -0
  63. package/src/js/utils/load-sprite.js +77 -0
  64. package/src/js/utils/numbers.js +17 -0
  65. package/src/js/utils/objects.js +43 -0
  66. package/src/js/utils/promise.js +14 -0
  67. package/src/js/utils/strings.js +80 -0
  68. package/src/js/utils/style.js +148 -0
  69. package/src/js/utils/time.js +36 -0
  70. package/src/js/utils/urls.js +40 -0
  71. package/src/sass/base.scss +69 -0
  72. package/src/sass/components/badges.scss +12 -0
  73. package/src/sass/components/captions.scss +58 -0
  74. package/src/sass/components/control.scss +52 -0
  75. package/src/sass/components/controls.scss +65 -0
  76. package/src/sass/components/menus.scss +205 -0
  77. package/src/sass/components/poster.scss +27 -0
  78. package/src/sass/components/progress.scss +107 -0
  79. package/src/sass/components/sliders.scss +99 -0
  80. package/src/sass/components/times.scss +20 -0
  81. package/src/sass/components/tooltips.scss +91 -0
  82. package/src/sass/components/volume.scss +18 -0
  83. package/src/sass/lib/animation.scss +31 -0
  84. package/src/sass/lib/css-vars.scss +103 -0
  85. package/src/sass/lib/functions.scss +3 -0
  86. package/src/sass/lib/mixins.scss +82 -0
  87. package/src/sass/plugins/ads.scss +53 -0
  88. package/src/sass/plugins/preview-thumbnails/index.scss +121 -0
  89. package/src/sass/plugins/preview-thumbnails/settings.scss +17 -0
  90. package/src/sass/plyr.scss +46 -0
  91. package/src/sass/settings/badges.scss +7 -0
  92. package/src/sass/settings/breakpoints.scss +9 -0
  93. package/src/sass/settings/captions.scss +10 -0
  94. package/src/sass/settings/colors.scss +18 -0
  95. package/src/sass/settings/controls.scss +30 -0
  96. package/src/sass/settings/cosmetics.scss +5 -0
  97. package/src/sass/settings/helpers.scss +7 -0
  98. package/src/sass/settings/menus.scss +13 -0
  99. package/src/sass/settings/progress.scss +18 -0
  100. package/src/sass/settings/sliders.scss +39 -0
  101. package/src/sass/settings/tooltips.scss +11 -0
  102. package/src/sass/settings/type.scss +16 -0
  103. package/src/sass/states/fullscreen.scss +15 -0
  104. package/src/sass/types/audio.scss +61 -0
  105. package/src/sass/types/video.scss +170 -0
  106. package/src/sass/utils/animation.scss +7 -0
  107. package/src/sass/utils/hidden.scss +28 -0
  108. package/src/sprite/plyr-airplay.svg +8 -0
  109. package/src/sprite/plyr-captions-off.svg +7 -0
  110. package/src/sprite/plyr-captions-on.svg +7 -0
  111. package/src/sprite/plyr-download.svg +8 -0
  112. package/src/sprite/plyr-enter-fullscreen.svg +4 -0
  113. package/src/sprite/plyr-exit-fullscreen.svg +4 -0
  114. package/src/sprite/plyr-fast-forward.svg +3 -0
  115. package/src/sprite/plyr-logo-vimeo.svg +6 -0
  116. package/src/sprite/plyr-logo-youtube.svg +6 -0
  117. package/src/sprite/plyr-muted.svg +8 -0
  118. package/src/sprite/plyr-pause.svg +8 -0
  119. package/src/sprite/plyr-pip.svg +6 -0
  120. package/src/sprite/plyr-play.svg +5 -0
  121. package/src/sprite/plyr-restart.svg +5 -0
  122. package/src/sprite/plyr-rewind.svg +3 -0
  123. package/src/sprite/plyr-settings.svg +5 -0
  124. package/src/sprite/plyr-volume.svg +11 -0
  125. package/tasks/build.js +226 -0
  126. package/tasks/deploy.js +216 -0
  127. package/tasks/utils/publish.js +34 -0
@@ -0,0 +1,13 @@
1
+ // ==========================================================================
2
+ // redxplyr Polyfilled Build
3
+ // redxplyr.js v1.0.0
4
+ // https://github.com/xgauravyaduvanshii/redxplyr
5
+ // License: The MIT License (MIT)
6
+ // ==========================================================================
7
+
8
+ import Plyr from './plyr';
9
+ import 'custom-event-polyfill';
10
+
11
+ import 'url-polyfill';
12
+
13
+ export default Plyr;
@@ -0,0 +1,155 @@
1
+ // ==========================================================================
2
+ // Plyr source update
3
+ // ==========================================================================
4
+
5
+ import { providers } from './config/types';
6
+ import html5 from './html5';
7
+ import media from './media';
8
+ import PreviewThumbnails from './plugins/preview-thumbnails';
9
+ import support from './support';
10
+ import ui from './ui';
11
+ import { createElement, insertElement, removeElement } from './utils/elements';
12
+ import is from './utils/is';
13
+ import { getDeep } from './utils/objects';
14
+
15
+ const source = {
16
+ // Add elements to HTML5 media (source, tracks, etc)
17
+ insertElements(type, attributes) {
18
+ if (is.string(attributes)) {
19
+ insertElement(type, this.media, {
20
+ src: attributes,
21
+ });
22
+ }
23
+ else if (is.array(attributes)) {
24
+ attributes.forEach((attribute) => {
25
+ insertElement(type, this.media, attribute);
26
+ });
27
+ }
28
+ },
29
+
30
+ // Update source
31
+ // Sources are not checked for support so be careful
32
+ change(input) {
33
+ if (!getDeep(input, 'sources.length')) {
34
+ this.debug.warn('Invalid source format');
35
+ return;
36
+ }
37
+
38
+ // Cancel current network requests
39
+ html5.cancelRequests.call(this);
40
+
41
+ // Destroy instance and re-setup
42
+ this.destroy(() => {
43
+ // Reset quality options
44
+ this.options.quality = [];
45
+
46
+ // Remove elements
47
+ removeElement(this.media);
48
+ this.media = null;
49
+
50
+ // Reset class name
51
+ if (is.element(this.elements.container)) {
52
+ this.elements.container.removeAttribute('class');
53
+ }
54
+
55
+ // Set the type and provider
56
+ const { sources, type } = input;
57
+ const [{ provider = providers.html5, src }] = sources;
58
+ const tagName = provider === 'html5' ? type : 'div';
59
+ const attributes = provider === 'html5' ? {} : { src };
60
+
61
+ Object.assign(this, {
62
+ provider,
63
+ type,
64
+ // Check for support
65
+ supported: support.check(type, provider, this.config.playsinline),
66
+ // Create new element
67
+ media: createElement(tagName, attributes),
68
+ });
69
+
70
+ // Inject the new element
71
+ this.elements.container.appendChild(this.media);
72
+
73
+ // Autoplay the new source?
74
+ if (is.boolean(input.autoplay)) {
75
+ this.config.autoplay = input.autoplay;
76
+ }
77
+
78
+ // Set attributes for audio and video
79
+ if (this.isHTML5) {
80
+ if (this.config.crossorigin) {
81
+ this.media.setAttribute('crossorigin', '');
82
+ }
83
+ if (this.config.autoplay) {
84
+ this.media.setAttribute('autoplay', '');
85
+ }
86
+ if (!is.empty(input.poster)) {
87
+ this.poster = input.poster;
88
+ }
89
+ if (this.config.loop.active) {
90
+ this.media.setAttribute('loop', '');
91
+ }
92
+ if (this.config.muted) {
93
+ this.media.setAttribute('muted', '');
94
+ }
95
+ if (this.config.playsinline) {
96
+ this.media.setAttribute('playsinline', '');
97
+ }
98
+ }
99
+
100
+ // Restore class hook
101
+ ui.addStyleHook.call(this);
102
+
103
+ // Set new sources for html5
104
+ if (this.isHTML5) {
105
+ source.insertElements.call(this, 'source', sources);
106
+ }
107
+
108
+ // Set video title
109
+ this.config.title = input.title;
110
+
111
+ // Set up from scratch
112
+ media.setup.call(this);
113
+
114
+ // HTML5 stuff
115
+ if (this.isHTML5) {
116
+ // Setup captions
117
+ if (Object.keys(input).includes('tracks')) {
118
+ source.insertElements.call(this, 'track', input.tracks);
119
+ }
120
+ }
121
+
122
+ // If HTML5 or embed but not fully supported, setupInterface and call ready now
123
+ if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
124
+ // Setup interface
125
+ ui.build.call(this);
126
+ }
127
+
128
+ // Load HTML5 sources
129
+ if (this.isHTML5) {
130
+ this.media.load();
131
+ }
132
+
133
+ // Update previewThumbnails config & reload plugin
134
+ if (!is.empty(input.previewThumbnails)) {
135
+ Object.assign(this.config.previewThumbnails, input.previewThumbnails);
136
+
137
+ // Cleanup previewThumbnails plugin if it was loaded
138
+ if (this.previewThumbnails && this.previewThumbnails.loaded) {
139
+ this.previewThumbnails.destroy();
140
+ this.previewThumbnails = null;
141
+ }
142
+
143
+ // Create new instance if it is still enabled
144
+ if (this.config.previewThumbnails.enabled) {
145
+ this.previewThumbnails = new PreviewThumbnails(this);
146
+ }
147
+ }
148
+
149
+ // Update the fullscreen support
150
+ this.fullscreen.update();
151
+ }, true);
152
+ },
153
+ };
154
+
155
+ export default source;
@@ -0,0 +1,70 @@
1
+ // ==========================================================================
2
+ // Plyr storage
3
+ // ==========================================================================
4
+
5
+ import is from './utils/is';
6
+ import { extend } from './utils/objects';
7
+
8
+ class Storage {
9
+ constructor(player) {
10
+ this.enabled = player.config.storage.enabled;
11
+ this.key = player.config.storage.key;
12
+ }
13
+
14
+ // Check for actual support (see if we can use it)
15
+ static get supported() {
16
+ try {
17
+ if (!('localStorage' in window)) return false;
18
+ const test = '___test';
19
+ // Try to use it (it might be disabled, e.g. user is in private mode)
20
+ // see: https://github.com/xgauravyaduvanshii/redxplyr/issues/131
21
+ window.localStorage.setItem(test, test);
22
+ window.localStorage.removeItem(test);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ get = (key) => {
31
+ if (!Storage.supported || !this.enabled) {
32
+ return null;
33
+ }
34
+ const store = window.localStorage.getItem(this.key);
35
+ if (is.empty(store)) return null;
36
+ const json = JSON.parse(store);
37
+ return is.string(key) && key.length ? json[key] : json;
38
+ };
39
+
40
+ set = (object) => {
41
+ // Bail if we don't have localStorage support or it's disabled
42
+ if (!Storage.supported || !this.enabled) {
43
+ return;
44
+ }
45
+
46
+ // Can only store objects
47
+ if (!is.object(object)) {
48
+ return;
49
+ }
50
+
51
+ // Get current storage
52
+ let storage = this.get();
53
+
54
+ // Default to empty object
55
+ if (is.empty(storage)) {
56
+ storage = {};
57
+ }
58
+
59
+ // Update the working copy of the values
60
+ extend(storage, object);
61
+
62
+ // Update storage
63
+ try {
64
+ window.localStorage.setItem(this.key, JSON.stringify(storage));
65
+ }
66
+ catch { }
67
+ };
68
+ }
69
+
70
+ export default Storage;
@@ -0,0 +1,100 @@
1
+ // ==========================================================================
2
+ // Plyr support checks
3
+ // ==========================================================================
4
+
5
+ import { transitionEndEvent } from './utils/animation';
6
+ import { createElement } from './utils/elements';
7
+ import is from './utils/is';
8
+
9
+ // Default codecs for checking mimetype support
10
+ const defaultCodecs = {
11
+ 'audio/ogg': 'vorbis',
12
+ 'audio/wav': '1',
13
+ 'video/webm': 'vp8, vorbis',
14
+ 'video/mp4': 'avc1.42E01E, mp4a.40.2',
15
+ 'video/ogg': 'theora',
16
+ };
17
+
18
+ // Check for feature support
19
+ const support = {
20
+ // Basic support
21
+ audio: 'canPlayType' in document.createElement('audio'),
22
+ video: 'canPlayType' in document.createElement('video'),
23
+
24
+ // Check for support
25
+ // Basic functionality vs full UI
26
+ check(type, provider) {
27
+ const api = support[type] || provider !== 'html5';
28
+ const ui = api && support.rangeInput;
29
+
30
+ return {
31
+ api,
32
+ ui,
33
+ };
34
+ },
35
+
36
+ // Picture-in-picture support
37
+ pip: (() => {
38
+ return (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture);
39
+ })(),
40
+
41
+ // Airplay support
42
+ // Safari only currently
43
+ airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
44
+
45
+ // Inline playback support
46
+ // https://webkit.org/blog/6784/new-video-policies-for-ios/
47
+ playsinline: 'playsInline' in document.createElement('video'),
48
+
49
+ // Check for mime type support against a player instance
50
+ // Credits: http://diveintohtml5.info/everything.html
51
+ // Related: http://www.leanbackplayer.com/test/h5mt.html
52
+ mime(input) {
53
+ if (is.empty(input)) {
54
+ return false;
55
+ }
56
+
57
+ const [mediaType] = input.split('/');
58
+ let type = input;
59
+
60
+ // Verify we're using HTML5 and there's no media type mismatch
61
+ if (!this.isHTML5 || mediaType !== this.type) {
62
+ return false;
63
+ }
64
+
65
+ // Add codec if required
66
+ if (Object.keys(defaultCodecs).includes(type)) {
67
+ type += `; codecs="${defaultCodecs[input]}"`;
68
+ }
69
+
70
+ try {
71
+ return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ },
77
+
78
+ // Check for textTracks support
79
+ textTracks: 'textTracks' in document.createElement('video'),
80
+
81
+ // <input type="range"> Sliders
82
+ rangeInput: (() => {
83
+ const range = document.createElement('input');
84
+ range.type = 'range';
85
+ return range.type === 'range';
86
+ })(),
87
+
88
+ // Touch
89
+ // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
90
+ touch: 'ontouchstart' in document.documentElement,
91
+
92
+ // Detect transitions support
93
+ transitions: transitionEndEvent !== false,
94
+
95
+ // Reduced motion iOS & MacOS setting
96
+ // https://webkit.org/blog/7551/responsive-design-for-motion/
97
+ reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
98
+ };
99
+
100
+ export default support;
package/src/js/ui.js ADDED
@@ -0,0 +1,297 @@
1
+ // ==========================================================================
2
+ // Plyr UI
3
+ // ==========================================================================
4
+
5
+ import captions from './captions';
6
+ import controls from './controls';
7
+ import support from './support';
8
+ import { getElement, toggleClass } from './utils/elements';
9
+ import { ready, triggerEvent } from './utils/events';
10
+ import i18n from './utils/i18n';
11
+ import is from './utils/is';
12
+ import loadImage from './utils/load-image';
13
+
14
+ const ui = {
15
+ addStyleHook() {
16
+ toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
17
+ toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
18
+ },
19
+
20
+ // Toggle native HTML5 media controls
21
+ toggleNativeControls(toggle = false) {
22
+ if (toggle && this.isHTML5) {
23
+ this.media.setAttribute('controls', '');
24
+ }
25
+ else {
26
+ this.media.removeAttribute('controls');
27
+ }
28
+ },
29
+
30
+ // Setup the UI
31
+ build() {
32
+ // Re-attach media element listeners
33
+ // TODO: Use event bubbling?
34
+ this.listeners.media();
35
+
36
+ // Don't setup interface if no support
37
+ if (!this.supported.ui) {
38
+ this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
39
+
40
+ // Restore native controls
41
+ ui.toggleNativeControls.call(this, true);
42
+
43
+ // Bail
44
+ return;
45
+ }
46
+
47
+ // Inject custom controls if not present
48
+ if (!is.element(this.elements.controls)) {
49
+ // Inject custom controls
50
+ controls.inject.call(this);
51
+
52
+ // Re-attach control listeners
53
+ this.listeners.controls();
54
+ }
55
+
56
+ // Remove native controls
57
+ ui.toggleNativeControls.call(this);
58
+
59
+ // Setup captions for HTML5
60
+ if (this.isHTML5) {
61
+ captions.setup.call(this);
62
+ }
63
+
64
+ // Reset volume
65
+ this.volume = null;
66
+
67
+ // Reset mute state
68
+ this.muted = null;
69
+
70
+ // Reset loop state
71
+ this.loop = null;
72
+
73
+ // Reset quality setting
74
+ this.quality = null;
75
+
76
+ // Reset speed
77
+ this.speed = null;
78
+
79
+ // Reset volume display
80
+ controls.updateVolume.call(this);
81
+
82
+ // Reset time display
83
+ controls.timeUpdate.call(this);
84
+
85
+ // Reset duration display
86
+ controls.durationUpdate.call(this);
87
+
88
+ // Update the UI
89
+ ui.checkPlaying.call(this);
90
+
91
+ // Check for picture-in-picture support
92
+ toggleClass(
93
+ this.elements.container,
94
+ this.config.classNames.pip.supported,
95
+ support.pip && this.isHTML5 && this.isVideo,
96
+ );
97
+
98
+ // Check for airplay support
99
+ toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
100
+
101
+ // Add touch class
102
+ toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
103
+
104
+ // Ready for API calls
105
+ this.ready = true;
106
+
107
+ // Ready event at end of execution stack
108
+ setTimeout(() => {
109
+ triggerEvent.call(this, this.media, 'ready');
110
+ }, 0);
111
+
112
+ // Set the title
113
+ ui.setTitle.call(this);
114
+
115
+ // Assure the poster image is set, if the property was added before the element was created
116
+ if (this.poster) {
117
+ ui.setPoster.call(this, this.poster, false).catch(() => {});
118
+ }
119
+
120
+ // Manually set the duration if user has overridden it.
121
+ // The event listeners for it doesn't get called if preload is disabled (#701)
122
+ if (this.config.duration) {
123
+ controls.durationUpdate.call(this);
124
+ }
125
+
126
+ // Media metadata
127
+ if (this.config.mediaMetadata) {
128
+ controls.setMediaMetadata.call(this);
129
+ }
130
+ },
131
+
132
+ // Setup aria attribute for play and iframe title
133
+ setTitle() {
134
+ // Find the current text
135
+ let label = i18n.get('play', this.config);
136
+
137
+ // If there's a media title set, use that for the label
138
+ if (is.string(this.config.title) && !is.empty(this.config.title)) {
139
+ label += `, ${this.config.title}`;
140
+ }
141
+
142
+ // If there's a play button, set label
143
+ Array.from(this.elements.buttons.play || []).forEach((button) => {
144
+ button.setAttribute('aria-label', label);
145
+ });
146
+
147
+ // Set iframe title
148
+ // https://github.com/xgauravyaduvanshii/redxplyr/issues/124
149
+ if (this.isEmbed) {
150
+ const iframe = getElement.call(this, 'iframe');
151
+
152
+ if (!is.element(iframe)) {
153
+ return;
154
+ }
155
+
156
+ // Default to media type
157
+ const title = !is.empty(this.config.title) ? this.config.title : 'video';
158
+ const format = i18n.get('frameTitle', this.config);
159
+
160
+ iframe.setAttribute('title', format.replace('{title}', title));
161
+ }
162
+ },
163
+
164
+ // Toggle poster
165
+ togglePoster(enable) {
166
+ toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
167
+ },
168
+
169
+ // Set the poster image (async)
170
+ // Used internally for the poster setter, with the passive option forced to false
171
+ setPoster(poster, passive = true) {
172
+ // Don't override if call is passive
173
+ if (passive && this.poster) {
174
+ return Promise.reject(new Error('Poster already set'));
175
+ }
176
+
177
+ // Set property synchronously to respect the call order
178
+ this.media.setAttribute('data-poster', poster);
179
+
180
+ // Show the poster
181
+ this.elements.poster.removeAttribute('hidden');
182
+
183
+ // Wait until ui is ready
184
+ return (
185
+ ready
186
+ .call(this)
187
+ // Load image
188
+ .then(() => loadImage(poster))
189
+ .catch((error) => {
190
+ // Hide poster on error unless it's been set by another call
191
+ if (poster === this.poster) {
192
+ ui.togglePoster.call(this, false);
193
+ }
194
+ // Rethrow
195
+ throw error;
196
+ })
197
+ .then(() => {
198
+ // Prevent race conditions
199
+ if (poster !== this.poster) {
200
+ throw new Error('setPoster cancelled by later call to setPoster');
201
+ }
202
+ })
203
+ .then(() => {
204
+ Object.assign(this.elements.poster.style, {
205
+ backgroundImage: `url('${poster}')`,
206
+ // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
207
+ backgroundSize: '',
208
+ });
209
+
210
+ ui.togglePoster.call(this, true);
211
+
212
+ return poster;
213
+ })
214
+ );
215
+ },
216
+
217
+ // Check playing state
218
+ checkPlaying(event) {
219
+ // Class hooks
220
+ toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
221
+ toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
222
+ toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
223
+
224
+ // Set state
225
+ Array.from(this.elements.buttons.play || []).forEach((target) => {
226
+ Object.assign(target, { pressed: this.playing });
227
+ target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
228
+ });
229
+
230
+ // Only update controls on non timeupdate events
231
+ if (is.event(event) && event.type === 'timeupdate') {
232
+ return;
233
+ }
234
+
235
+ // Toggle controls
236
+ ui.toggleControls.call(this);
237
+ },
238
+
239
+ // Check if media is loading
240
+ checkLoading(event) {
241
+ this.loading = ['stalled', 'waiting'].includes(event.type);
242
+
243
+ // Clear timer
244
+ clearTimeout(this.timers.loading);
245
+
246
+ // Timer to prevent flicker when seeking
247
+ this.timers.loading = setTimeout(
248
+ () => {
249
+ // Update progress bar loading class state
250
+ toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
251
+
252
+ // Update controls visibility
253
+ ui.toggleControls.call(this);
254
+ },
255
+ this.loading ? 250 : 0,
256
+ );
257
+ },
258
+
259
+ // Toggle controls based on state and `force` argument
260
+ toggleControls(force) {
261
+ const { controls: controlsElement } = this.elements;
262
+
263
+ if (controlsElement && this.config.hideControls) {
264
+ // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
265
+ const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
266
+
267
+ // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
268
+ this.toggleControls(
269
+ Boolean(
270
+ force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
271
+ ),
272
+ );
273
+ }
274
+ },
275
+
276
+ // Migrate any custom properties from the media to the parent
277
+ migrateStyles() {
278
+ // Loop through values (as they are the keys when the object is spread 🤔)
279
+ Object.values({ ...this.media.style })
280
+ // We're only fussed about Plyr specific properties
281
+ .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
282
+ .forEach((key) => {
283
+ // Set on the container
284
+ this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
285
+
286
+ // Clean up from media element
287
+ this.media.style.removeProperty(key);
288
+ });
289
+
290
+ // Remove attribute if empty
291
+ if (is.empty(this.media.style)) {
292
+ this.media.removeAttribute('style');
293
+ }
294
+ },
295
+ };
296
+
297
+ export default ui;
@@ -0,0 +1,33 @@
1
+ // ==========================================================================
2
+ // Animation utils
3
+ // ==========================================================================
4
+
5
+ import is from './is';
6
+
7
+ export const transitionEndEvent = (() => {
8
+ const element = document.createElement('span');
9
+
10
+ const events = {
11
+ WebkitTransition: 'webkitTransitionEnd',
12
+ MozTransition: 'transitionend',
13
+ OTransition: 'oTransitionEnd otransitionend',
14
+ transition: 'transitionend',
15
+ };
16
+
17
+ const type = Object.keys(events).find(event => element.style[event] !== undefined);
18
+
19
+ return is.string(type) ? events[type] : false;
20
+ })();
21
+
22
+ // Force repaint of element
23
+ export function repaint(element, delay) {
24
+ setTimeout(() => {
25
+ try {
26
+ element.hidden = true;
27
+ // eslint-disable-next-line no-unused-expressions
28
+ element.offsetHeight;
29
+ element.hidden = false;
30
+ }
31
+ catch {}
32
+ }, delay);
33
+ }
@@ -0,0 +1,23 @@
1
+ // ==========================================================================
2
+ // Array utils
3
+ // ==========================================================================
4
+
5
+ import is from './is';
6
+
7
+ // Remove duplicates in an array
8
+ export function dedupe(array) {
9
+ if (!is.array(array)) {
10
+ return array;
11
+ }
12
+
13
+ return array.filter((item, index) => array.indexOf(item) === index);
14
+ }
15
+
16
+ // Get the closest value in an array
17
+ export function closest(array, value) {
18
+ if (!is.array(array) || !array.length) {
19
+ return null;
20
+ }
21
+
22
+ return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
23
+ }