vidply 1.0.34 → 1.0.36

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 (42) hide show
  1. package/dist/dev/{vidply.SoundCloudRenderer-RIA3QKP3.js → vidply.SoundCloudRenderer-HCMKXHSX.js} +1 -3
  2. package/dist/dev/vidply.SoundCloudRenderer-HCMKXHSX.js.map +7 -0
  3. package/dist/dev/{vidply.TranscriptManager-T3BVTZHZ.js → vidply.TranscriptManager-EIIN5YOF.js} +2 -2
  4. package/dist/dev/{vidply.VimeoRenderer-DY2FG7LZ.js → vidply.VimeoRenderer-SLEBCZTT.js} +1 -2
  5. package/dist/dev/vidply.VimeoRenderer-SLEBCZTT.js.map +7 -0
  6. package/dist/dev/{vidply.YouTubeRenderer-EVXXE34A.js → vidply.YouTubeRenderer-E6F4UGVF.js} +1 -2
  7. package/dist/dev/vidply.YouTubeRenderer-E6F4UGVF.js.map +7 -0
  8. package/dist/dev/{vidply.chunk-74NJTDQI.js → vidply.chunk-AXXU22HR.js} +87 -10
  9. package/dist/dev/{vidply.chunk-74NJTDQI.js.map → vidply.chunk-AXXU22HR.js.map} +2 -2
  10. package/dist/dev/vidply.esm.js +83 -50
  11. package/dist/dev/vidply.esm.js.map +2 -2
  12. package/dist/legacy/vidply.js +170 -58
  13. package/dist/legacy/vidply.js.map +3 -3
  14. package/dist/legacy/vidply.min.js +1 -1
  15. package/dist/legacy/vidply.min.meta.json +18 -18
  16. package/dist/prod/vidply.SoundCloudRenderer-D2FNOEG6.min.js +6 -0
  17. package/dist/prod/{vidply.TranscriptManager-GPAOXEK4.min.js → vidply.TranscriptManager-VXCTCJ7X.min.js} +1 -1
  18. package/dist/prod/vidply.VimeoRenderer-QELFZVDU.min.js +6 -0
  19. package/dist/prod/vidply.YouTubeRenderer-ZL6YUHTF.min.js +6 -0
  20. package/dist/prod/{vidply.chunk-OM7DNW5P.min.js → vidply.chunk-Z6BHMOGK.min.js} +1 -1
  21. package/dist/prod/vidply.esm.min.js +3 -3
  22. package/dist/vidply.css +293 -108
  23. package/dist/vidply.esm.min.meta.json +33 -33
  24. package/dist/vidply.min.css +1 -1
  25. package/package.json +3 -3
  26. package/src/controls/ControlBar.js +3 -0
  27. package/src/controls/KeyboardManager.js +19 -0
  28. package/src/core/Player.js +75 -64
  29. package/src/core/SignLanguageManager.js +18 -4
  30. package/src/index.js +3 -1
  31. package/src/renderers/SoundCloudRenderer.js +0 -2
  32. package/src/renderers/VimeoRenderer.js +0 -1
  33. package/src/renderers/YouTubeRenderer.js +0 -1
  34. package/src/styles/vidply.css +293 -108
  35. package/src/utils/DraggableResizable.js +123 -12
  36. package/dist/dev/vidply.SoundCloudRenderer-RIA3QKP3.js.map +0 -7
  37. package/dist/dev/vidply.VimeoRenderer-DY2FG7LZ.js.map +0 -7
  38. package/dist/dev/vidply.YouTubeRenderer-EVXXE34A.js.map +0 -7
  39. package/dist/prod/vidply.SoundCloudRenderer-BFV5SSIU.min.js +0 -6
  40. package/dist/prod/vidply.VimeoRenderer-UQWHQ4LC.min.js +0 -6
  41. package/dist/prod/vidply.YouTubeRenderer-K7A57ICA.min.js +0 -6
  42. /package/dist/dev/{vidply.TranscriptManager-T3BVTZHZ.js.map → vidply.TranscriptManager-EIIN5YOF.js.map} +0 -0
@@ -145,6 +145,9 @@ export class ControlBar {
145
145
  // Ensure menu doesn't go off top or bottom
146
146
  if (menuRect.top < 10) {
147
147
  menu.style.top = '10px';
148
+ // Important: clear bottom constraint, otherwise top+bottom will squeeze the menu height
149
+ // into a tiny strip on some mobile layouts.
150
+ menu.style.bottom = 'auto';
148
151
  }
149
152
 
150
153
  if (menuRect.bottom > viewportHeight - 10) {
@@ -46,6 +46,25 @@ export class KeyboardManager {
46
46
  if (playlistButton) {
47
47
  return; // Let the playlist handle keyboard events
48
48
  }
49
+
50
+ // Don't steal arrow keys when the sign-language overlay is in keyboard drag/resize mode.
51
+ // DraggableResizable listens on the overlay itself, but we run in CAPTURE phase, so we must opt out here.
52
+ const signWrapper = activeElement.closest('.vidply-sign-language-wrapper');
53
+ if (signWrapper) {
54
+ const draggable = this.player.signLanguageManager?.draggable;
55
+ if (draggable?.keyboardDragMode || draggable?.keyboardResizeMode) {
56
+ return;
57
+ }
58
+ }
59
+
60
+ // Same idea for the transcript floating window (it also uses DraggableResizable).
61
+ const transcriptWindow = activeElement.closest('.vidply-transcript-window');
62
+ if (transcriptWindow) {
63
+ const draggable = this.player.transcriptManager?.draggableResizable;
64
+ if (draggable?.keyboardDragMode || draggable?.keyboardResizeMode) {
65
+ return;
66
+ }
67
+ }
49
68
  }
50
69
 
51
70
  const key = e.key;
@@ -588,6 +588,7 @@ export class Player extends EventEmitter {
588
588
  this.container.classList.add(`${this.options.classPrefix}-responsive`);
589
589
  }
590
590
 
591
+
591
592
  // Create video wrapper (for proper positioning of controls)
592
593
  this.videoWrapper = DOMUtils.createElement('div', {
593
594
  className: `${this.options.classPrefix}-video-wrapper`
@@ -639,35 +640,10 @@ export class Player extends EventEmitter {
639
640
  : this.options.height;
640
641
  }
641
642
 
642
- // If no explicit height is set, ensure video players still have a stable layout box
643
- // even before any media is loaded (important for deferLoad + playlists).
644
- // We use the element's width/height attributes (e.g. from TYPO3) as aspect ratio.
645
- if (this.element.tagName === 'VIDEO' && !this.options.height) {
646
- const wAttr = parseInt(this.element.getAttribute('width') || '', 10);
647
- const hAttr = parseInt(this.element.getAttribute('height') || '', 10);
648
- if (Number.isFinite(wAttr) && Number.isFinite(hAttr) && wAttr > 0 && hAttr > 0) {
649
- // Only set if not already defined by CSS/inline style
650
- if (!this.container.style.aspectRatio) {
651
- this.container.style.aspectRatio = `${wAttr} / ${hAttr}`;
652
- }
653
-
654
- // The actual visual box is the videoWrapper (the video element is 100% height).
655
- // Give the wrapper the same aspect ratio so posters render correctly before metadata is loaded.
656
- if (this.videoWrapper && !this.videoWrapper.style.aspectRatio) {
657
- this.videoWrapper.style.aspectRatio = `${wAttr} / ${hAttr}`;
658
- // Override default CSS height:100% (which depends on parent having a height)
659
- this.videoWrapper.style.height = 'auto';
660
- }
661
- }
662
- }
663
-
664
643
  // Set poster (convert relative paths to absolute URLs)
665
644
  if (this.options.poster && this.element.tagName === 'VIDEO') {
666
645
  const resolvedPoster = this.resolvePosterPath(this.options.poster);
667
646
  this.element.poster = resolvedPoster;
668
- // If we intentionally have no media loaded yet (e.g. deferLoad/playlist),
669
- // use poster aspect ratio to size the wrapper so the poster isn't stretched.
670
- this.applyPosterAspectRatio(resolvedPoster);
671
647
  }
672
648
 
673
649
  // Create centered play button overlay (only for video)
@@ -711,45 +687,6 @@ export class Player extends EventEmitter {
711
687
  }, { once: true });
712
688
  }
713
689
 
714
- /**
715
- * Apply aspect ratio to the video wrapper based on the poster's intrinsic size.
716
- * This helps render correct poster sizing before media metadata is available.
717
- */
718
- applyPosterAspectRatio(posterUrl) {
719
- try {
720
- if (!posterUrl) return;
721
- if (this.element.tagName !== 'VIDEO') return;
722
- if (!this.videoWrapper) return;
723
-
724
- // If user explicitly configured dimensions, don't override.
725
- if (this.options.width || this.options.height) return;
726
-
727
- // Avoid repeated work
728
- if (this._posterAspectAppliedFor === posterUrl) return;
729
- this._posterAspectAppliedFor = posterUrl;
730
-
731
- const img = new Image();
732
- img.decoding = 'async';
733
- img.onload = () => {
734
- const w = img.naturalWidth;
735
- const h = img.naturalHeight;
736
- if (!w || !h) return;
737
-
738
- // Apply to wrapper (the actual layout box)
739
- this.videoWrapper.style.aspectRatio = `${w} / ${h}`;
740
- this.videoWrapper.style.height = 'auto';
741
-
742
- // Also apply to container if not explicitly set
743
- if (this.container && !this.container.style.aspectRatio) {
744
- this.container.style.aspectRatio = `${w} / ${h}`;
745
- }
746
- };
747
- img.src = posterUrl;
748
- } catch (e) {
749
- // ignore
750
- }
751
- }
752
-
753
690
  createPlayButtonOverlay() {
754
691
  // Create complete SVG play button from Icons.js
755
692
  this.playButtonOverlay = createPlayOverlay();
@@ -1675,6 +1612,9 @@ export class Player extends EventEmitter {
1675
1612
  this.state.fullscreen = true;
1676
1613
  this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
1677
1614
 
1615
+ // Add body class for CSS targeting (fallback for browsers without :has() support)
1616
+ document.body.classList.add('vidply-fullscreen-active');
1617
+
1678
1618
  // Store current scroll position for restoration later
1679
1619
  this._originalScrollX = window.scrollX || window.pageXOffset;
1680
1620
  this._originalScrollY = window.scrollY || window.pageYOffset;
@@ -1685,11 +1625,15 @@ export class Player extends EventEmitter {
1685
1625
  this._originalBodyWidth = document.body.style.width;
1686
1626
  this._originalBodyHeight = document.body.style.height;
1687
1627
  this._originalHtmlOverflow = document.documentElement.style.overflow;
1628
+ this._originalBodyBackground = document.body.style.background;
1629
+ this._originalHtmlBackground = document.documentElement.style.background;
1688
1630
 
1689
1631
  document.body.style.overflow = 'hidden';
1690
1632
  document.body.style.width = '100%';
1691
1633
  document.body.style.height = '100%';
1634
+ document.body.style.background = '#000';
1692
1635
  document.documentElement.style.overflow = 'hidden';
1636
+ document.documentElement.style.background = '#000';
1693
1637
 
1694
1638
  // On iOS, also lock the viewport and scroll to top
1695
1639
  this._originalViewport = document.querySelector('meta[name="viewport"]')?.getAttribute('content');
@@ -1701,11 +1645,62 @@ export class Player extends EventEmitter {
1701
1645
  // Scroll to top on iOS to prevent positioning issues
1702
1646
  window.scrollTo(0, 0);
1703
1647
 
1648
+ // Make all other page content inert to prevent keyboard focus escaping to background
1649
+ this._makeBackgroundInert();
1650
+
1704
1651
  this.emit('fullscreenchange', true);
1705
1652
  this.emit('enterfullscreen');
1706
1653
  }
1654
+
1655
+ /**
1656
+ * Makes all page content except the fullscreen player inert (non-focusable)
1657
+ * This prevents keyboard navigation from focusing on hidden background elements
1658
+ */
1659
+ _makeBackgroundInert() {
1660
+ this._inertElements = [];
1661
+
1662
+ // Find all siblings and ancestors' siblings that should be made inert
1663
+ let current = this.container;
1664
+ while (current && current !== document.body && current !== document.documentElement) {
1665
+ const parent = current.parentElement;
1666
+ if (parent) {
1667
+ // Make all siblings inert
1668
+ Array.from(parent.children).forEach(sibling => {
1669
+ if (sibling !== current &&
1670
+ sibling.nodeType === Node.ELEMENT_NODE &&
1671
+ !sibling.hasAttribute('inert') &&
1672
+ sibling.tagName !== 'SCRIPT' &&
1673
+ sibling.tagName !== 'STYLE' &&
1674
+ sibling.tagName !== 'LINK' &&
1675
+ sibling.tagName !== 'META') {
1676
+ sibling.setAttribute('inert', '');
1677
+ this._inertElements.push(sibling);
1678
+ }
1679
+ });
1680
+ }
1681
+ current = parent;
1682
+ }
1683
+ }
1684
+
1685
+ /**
1686
+ * Restores interactivity to elements that were made inert during fullscreen
1687
+ */
1688
+ _restoreBackgroundInteractivity() {
1689
+ if (this._inertElements) {
1690
+ this._inertElements.forEach(el => {
1691
+ el.removeAttribute('inert');
1692
+ });
1693
+ this._inertElements = [];
1694
+ }
1695
+ }
1707
1696
 
1708
1697
  _disablePseudoFullscreen() {
1698
+ // Remove body class for CSS targeting
1699
+ document.body.classList.remove('vidply-fullscreen-active');
1700
+
1701
+ // Restore interactivity to background elements
1702
+ this._restoreBackgroundInteractivity();
1703
+
1709
1704
  // Restore body scrolling
1710
1705
  if (this._originalBodyOverflow !== undefined) {
1711
1706
  document.body.style.overflow = this._originalBodyOverflow;
@@ -1727,6 +1722,14 @@ export class Player extends EventEmitter {
1727
1722
  document.documentElement.style.overflow = this._originalHtmlOverflow;
1728
1723
  delete this._originalHtmlOverflow;
1729
1724
  }
1725
+ if (this._originalBodyBackground !== undefined) {
1726
+ document.body.style.background = this._originalBodyBackground;
1727
+ delete this._originalBodyBackground;
1728
+ }
1729
+ if (this._originalHtmlBackground !== undefined) {
1730
+ document.documentElement.style.background = this._originalHtmlBackground;
1731
+ delete this._originalHtmlBackground;
1732
+ }
1730
1733
 
1731
1734
  // Restore viewport settings
1732
1735
  if (this._originalViewport !== undefined) {
@@ -4522,8 +4525,16 @@ export class Player extends EventEmitter {
4522
4525
 
4523
4526
  if (isFullscreen) {
4524
4527
  this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
4528
+ // Add body class for CSS targeting (fallback for browsers without :has() support)
4529
+ document.body.classList.add('vidply-fullscreen-active');
4530
+ // Make background content inert to prevent keyboard focus escaping
4531
+ this._makeBackgroundInert();
4525
4532
  } else {
4526
4533
  this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
4534
+ // Remove body class for CSS targeting
4535
+ document.body.classList.remove('vidply-fullscreen-active');
4536
+ // Restore background interactivity
4537
+ this._restoreBackgroundInteractivity();
4527
4538
  // Clean up pseudo-fullscreen state when exiting
4528
4539
  this._disablePseudoFullscreen();
4529
4540
  }
@@ -459,8 +459,11 @@ export class SignLanguageManager {
459
459
  _setupInteraction() {
460
460
  const isMobile = window.innerWidth < 768;
461
461
  const isFullscreen = this.player.state.fullscreen;
462
-
463
- if (isMobile && !isFullscreen) {
462
+
463
+ // Historically, drag/resize was disabled on mobile unless fullscreen to avoid scroll conflicts.
464
+ // Now that we support touch/pointer dragging with proper `touch-action` handling, enable it
465
+ // by default on iOS/Android as well. Allow opting out via option.
466
+ if (isMobile && !isFullscreen && this.player?.options?.signLanguageDragOnMobile === false) {
464
467
  if (this.draggable) {
465
468
  this.draggable.destroy();
466
469
  this.draggable = null;
@@ -473,7 +476,9 @@ export class SignLanguageManager {
473
476
  const classPrefix = this.player.options.classPrefix;
474
477
 
475
478
  this.draggable = new DraggableResizable(this.wrapper, {
476
- dragHandle: this.header,
479
+ // Allow dragging from anywhere on the sign-language window (better for touch).
480
+ // We still block dragging when interacting with controls via `onDragStart` below.
481
+ dragHandle: this.wrapper,
477
482
  resizeHandles: this.resizeHandles,
478
483
  constrainToViewport: true,
479
484
  maintainAspectRatio: true,
@@ -761,9 +766,18 @@ export class SignLanguageManager {
761
766
  hasTextClass: true,
762
767
  onClick: () => {
763
768
  this.toggleKeyboardDragMode();
764
- this.hideSettingsMenu();
769
+ // Keep focus off the settings button so arrow keys go to the draggable overlay.
770
+ this.hideSettingsMenu({ focusButton: false });
771
+ // If we just enabled keyboard drag mode, focus the overlay.
772
+ if (this.draggable?.keyboardDragMode) {
773
+ setTimeout(() => {
774
+ this.wrapper?.focus?.({ preventScroll: true });
775
+ }, 20);
776
+ }
765
777
  }
766
778
  });
779
+ // Allow CSS to hide this option on touch/mobile where dragging is always enabled
780
+ dragOption.setAttribute('data-setting', 'keyboard-drag');
767
781
  dragOption.setAttribute('role', 'switch');
768
782
  dragOption.setAttribute('aria-checked', 'false');
769
783
  this._removeTooltipFromMenuItem(dragOption);
package/src/index.js CHANGED
@@ -57,7 +57,9 @@ function parseDataAttributes(dataset) {
57
57
  'keyboard': 'keyboard',
58
58
  'responsive': 'responsive',
59
59
  'pipButton': 'pipButton',
60
- 'fullscreenButton': 'fullscreenButton'
60
+ 'fullscreenButton': 'fullscreenButton',
61
+
62
+ // Layout
61
63
  };
62
64
 
63
65
  // Parse each data attribute
@@ -125,10 +125,8 @@ export class SoundCloudRenderer {
125
125
  // Use different aspect ratio for playlists vs single tracks
126
126
  // Playlists need more height to show the track list
127
127
  if (this.isPlaylist()) {
128
- this.iframe.style.aspectRatio = '16 / 9'; // More height for playlist
129
128
  this.iframe.classList.add('vidply-soundcloud-iframe', 'vidply-soundcloud-playlist');
130
129
  } else {
131
- this.iframe.style.aspectRatio = '16 / 3'; // Banner-like for single track
132
130
  this.iframe.classList.add('vidply-soundcloud-iframe');
133
131
  }
134
132
  this.iframe.style.maxHeight = '100%';
@@ -70,7 +70,6 @@ export class VimeoRenderer {
70
70
  this.iframe = document.createElement('div');
71
71
  this.iframe.id = `vimeo-player-${Math.random().toString(36).substr(2, 9)}`;
72
72
  this.iframe.style.width = '100%';
73
- this.iframe.style.aspectRatio = '16 / 9';
74
73
  this.iframe.style.maxHeight = '100%';
75
74
 
76
75
  this.player.element.parentNode.insertBefore(this.iframe, this.player.element);
@@ -86,7 +86,6 @@ export class YouTubeRenderer {
86
86
  this.iframe = document.createElement('div');
87
87
  this.iframe.id = `youtube-player-${Math.random().toString(36).substr(2, 9)}`;
88
88
  this.iframe.style.width = '100%';
89
- this.iframe.style.aspectRatio = '16 / 9';
90
89
  this.iframe.style.maxHeight = '100%';
91
90
 
92
91
  this.player.element.parentNode.insertBefore(this.iframe, this.player.element);