vidply 1.0.8 → 1.0.10

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/dist/vidply.js CHANGED
@@ -517,10 +517,20 @@ var VidPly = (() => {
517
517
  settings: "Transcript settings. Press Enter to open menu, or D to enable drag mode",
518
518
  keyboardDragMode: "Toggle keyboard drag mode with arrow keys. Shortcut: D key",
519
519
  keyboardDragActive: "\u2328\uFE0F Keyboard Drag Mode Active (Arrow keys to move, Shift+Arrows for large steps, D or ESC to exit)",
520
+ dragResizePrompt: "Press D to drag or R to resize. Use Home to reset position, Esc to close.",
521
+ dragModeEnabled: "Keyboard drag mode enabled. Use arrow keys to move, Shift+Arrow for larger steps. Press D or Esc to exit.",
522
+ dragModeDisabled: "Keyboard drag mode disabled.",
520
523
  resizeWindow: "Resize Window",
524
+ disableResizeWindow: "Disable Resize Mode",
525
+ resizeModeHint: "Resize handles enabled. Drag edges or corners to adjust. Press Esc or R to exit.",
526
+ resizeModeEnabled: "Resize mode enabled. Drag edges or corners to adjust. Press Esc or R to exit.",
527
+ resizeModeDisabled: "Resize mode disabled.",
528
+ positionReset: "Transcript position reset.",
521
529
  styleTranscript: "Open transcript style settings",
522
530
  closeMenu: "Close Menu",
523
- styleTitle: "Transcript Style"
531
+ styleTitle: "Transcript Style",
532
+ autoscroll: "Autoscroll",
533
+ settingsMenu: "Settings menu"
524
534
  },
525
535
  settings: {
526
536
  title: "Settings",
@@ -638,10 +648,20 @@ var VidPly = (() => {
638
648
  settings: "Transkript-Einstellungen. Eingabetaste zum \xD6ffnen des Men\xFCs dr\xFCcken oder D zum Aktivieren des Verschiebemodus",
639
649
  keyboardDragMode: "Tastatur-Verschiebemodus mit Pfeiltasten umschalten. Tastenkombination: D-Taste",
640
650
  keyboardDragActive: "\u2328\uFE0F Tastatur-Verschiebemodus aktiv (Pfeiltasten zum Bewegen, Umschalt+Pfeiltasten f\xFCr gro\xDFe Schritte, D oder ESC zum Beenden)",
651
+ dragResizePrompt: "Dr\xFCcken Sie D zum Verschieben oder R zur Gr\xF6\xDFen\xE4nderung. Home setzt die Position zur\xFCck, Esc schlie\xDFt.",
652
+ dragModeEnabled: "Tastatur-Verschiebemodus aktiviert. Pfeiltasten zum Bewegen, Umschalt+Pfeiltasten f\xFCr gr\xF6\xDFere Schritte. D oder Esc zum Beenden.",
653
+ dragModeDisabled: "Tastatur-Verschiebemodus deaktiviert.",
641
654
  resizeWindow: "Fenster vergr\xF6\xDFern/verkleinern",
655
+ disableResizeWindow: "Resize-Modus deaktivieren",
656
+ resizeModeHint: "Griffe aktiviert. Ziehen Sie Kanten oder Ecken zum Anpassen. Esc oder R zum Beenden.",
657
+ resizeModeEnabled: "Resize-Modus aktiviert. Kanten oder Ecken ziehen; Esc oder R beendet.",
658
+ resizeModeDisabled: "Resize-Modus deaktiviert.",
659
+ positionReset: "Transkriptposition zur\xFCckgesetzt.",
642
660
  styleTranscript: "Transkript-Stileinstellungen \xF6ffnen",
643
661
  closeMenu: "Men\xFC schlie\xDFen",
644
- styleTitle: "Transkript-Stil"
662
+ styleTitle: "Transkript-Stil",
663
+ autoscroll: "Automatisches Scrollen",
664
+ settingsMenu: "Einstellungsmen\xFC"
645
665
  },
646
666
  settings: {
647
667
  title: "Einstellungen",
@@ -759,10 +779,20 @@ var VidPly = (() => {
759
779
  settings: "Configuraci\xF3n de transcripci\xF3n. Presione Enter para abrir el men\xFA o D para activar el modo de arrastre",
760
780
  keyboardDragMode: "Alternar modo de arrastre con teclado usando teclas de flecha. Atajo: tecla D",
761
781
  keyboardDragActive: "\u2328\uFE0F Modo de Arrastre con Teclado Activo (Teclas de flecha para mover, May\xFAs+Flechas para pasos grandes, D o ESC para salir)",
782
+ dragResizePrompt: "Pulsa D para mover o R para cambiar el tama\xF1o. Home restablece la posici\xF3n; Esc cierra.",
783
+ dragModeEnabled: "Modo de arrastre con teclado activado. Usa flechas para mover, May\xFAs+Flechas para pasos grandes. Pulsa D o Esc para salir.",
784
+ dragModeDisabled: "Modo de arrastre con teclado desactivado.",
762
785
  resizeWindow: "Cambiar tama\xF1o de ventana",
786
+ disableResizeWindow: "Desactivar modo de cambio de tama\xF1o",
787
+ resizeModeHint: "Controladores habilitados. Arrastra bordes o esquinas para ajustar. Pulsa Esc o R para salir.",
788
+ resizeModeEnabled: "Modo de cambio de tama\xF1o activado. Arrastra bordes o esquinas. Pulsa Esc o R para salir.",
789
+ resizeModeDisabled: "Modo de cambio de tama\xF1o desactivado.",
790
+ positionReset: "Posici\xF3n de la transcripci\xF3n restablecida.",
763
791
  styleTranscript: "Abrir configuraci\xF3n de estilo de transcripci\xF3n",
764
792
  closeMenu: "Cerrar men\xFA",
765
- styleTitle: "Estilo de Transcripci\xF3n"
793
+ styleTitle: "Estilo de Transcripci\xF3n",
794
+ autoscroll: "Desplazamiento autom\xE1tico",
795
+ settingsMenu: "Men\xFA de configuraci\xF3n"
766
796
  },
767
797
  settings: {
768
798
  title: "Configuraci\xF3n",
@@ -880,10 +910,20 @@ var VidPly = (() => {
880
910
  settings: "Param\xE8tres de transcription. Appuyez sur Entr\xE9e pour ouvrir le menu ou D pour activer le mode glissement",
881
911
  keyboardDragMode: "Basculer le mode glissement avec les touches fl\xE9ch\xE9es. Raccourci: touche D",
882
912
  keyboardDragActive: "\u2328\uFE0F Mode Glissement Clavier Actif (Touches fl\xE9ch\xE9es pour d\xE9placer, Maj+Fl\xE9ch\xE9es pour grands pas, D ou \xC9chap pour quitter)",
913
+ dragResizePrompt: "Appuyez sur D pour d\xE9placer ou R pour redimensionner. Home r\xE9initialise la position, \xC9chap ferme.",
914
+ dragModeEnabled: "Mode glissement clavier activ\xE9. Utilisez les fl\xE8ches pour d\xE9placer, Maj+Fl\xE8ches pour de grands pas. Appuyez sur D ou \xC9chap pour quitter.",
915
+ dragModeDisabled: "Mode glissement clavier d\xE9sactiv\xE9.",
883
916
  resizeWindow: "Redimensionner la fen\xEAtre",
917
+ disableResizeWindow: "D\xE9sactiver le mode de redimensionnement",
918
+ resizeModeHint: "Poign\xE9es activ\xE9es. Faites glisser les bords ou les coins pour ajuster. Appuyez sur \xC9chap ou R pour quitter.",
919
+ resizeModeEnabled: "Mode redimensionnement activ\xE9. Faites glisser les bords ou coins. Appuyez sur \xC9chap ou R pour quitter.",
920
+ resizeModeDisabled: "Mode redimensionnement d\xE9sactiv\xE9.",
921
+ positionReset: "Position de la transcription r\xE9initialis\xE9e.",
884
922
  styleTranscript: "Ouvrir les param\xE8tres de style de transcription",
885
923
  closeMenu: "Fermer le menu",
886
- styleTitle: "Style de Transcription"
924
+ styleTitle: "Style de Transcription",
925
+ autoscroll: "D\xE9filement automatique",
926
+ settingsMenu: "Menu des param\xE8tres"
887
927
  },
888
928
  settings: {
889
929
  title: "Param\xE8tres",
@@ -1001,10 +1041,20 @@ var VidPly = (() => {
1001
1041
  settings: "\u6587\u5B57\u8D77\u3053\u3057\u8A2D\u5B9A\u3002Enter\u30AD\u30FC\u3067\u30E1\u30CB\u30E5\u30FC\u3092\u958B\u304F\u3001\u307E\u305F\u306FD\u30AD\u30FC\u3067\u30C9\u30E9\u30C3\u30B0\u30E2\u30FC\u30C9\u3092\u6709\u52B9\u306B\u3059\u308B",
1002
1042
  keyboardDragMode: "\u77E2\u5370\u30AD\u30FC\u3067\u30AD\u30FC\u30DC\u30FC\u30C9\u30C9\u30E9\u30C3\u30B0\u30E2\u30FC\u30C9\u3092\u5207\u308A\u66FF\u3048\u3002\u30B7\u30E7\u30FC\u30C8\u30AB\u30C3\u30C8\uFF1AD\u30AD\u30FC",
1003
1043
  keyboardDragActive: "\u2328\uFE0F \u30AD\u30FC\u30DC\u30FC\u30C9\u30C9\u30E9\u30C3\u30B0\u30E2\u30FC\u30C9\u6709\u52B9\uFF08\u77E2\u5370\u30AD\u30FC\u3067\u79FB\u52D5\u3001Shift+\u77E2\u5370\u30AD\u30FC\u3067\u5927\u304D\u304F\u79FB\u52D5\u3001D\u307E\u305F\u306FESC\u3067\u7D42\u4E86\uFF09",
1044
+ dragResizePrompt: "D\u30AD\u30FC\u3067\u79FB\u52D5\u3001R\u30AD\u30FC\u3067\u30B5\u30A4\u30BA\u5909\u66F4\u3002Home\u3067\u4F4D\u7F6E\u3092\u30EA\u30BB\u30C3\u30C8\u3001Esc\u3067\u9589\u3058\u307E\u3059\u3002",
1045
+ dragModeEnabled: "\u30AD\u30FC\u30DC\u30FC\u30C9\u30C9\u30E9\u30C3\u30B0\u30E2\u30FC\u30C9\u3092\u6709\u52B9\u306B\u3057\u307E\u3057\u305F\u3002\u77E2\u5370\u30AD\u30FC\u3067\u79FB\u52D5\u3001Shift+\u77E2\u5370\u30AD\u30FC\u3067\u5927\u304D\u304F\u79FB\u52D5\u3067\u304D\u307E\u3059\u3002\u7D42\u4E86\u3059\u308B\u306B\u306F D \u307E\u305F\u306F Esc \u3092\u62BC\u3057\u307E\u3059\u3002",
1046
+ dragModeDisabled: "\u30AD\u30FC\u30DC\u30FC\u30C9\u30C9\u30E9\u30C3\u30B0\u30E2\u30FC\u30C9\u3092\u7121\u52B9\u306B\u3057\u307E\u3057\u305F\u3002",
1004
1047
  resizeWindow: "\u30A6\u30A3\u30F3\u30C9\u30A6\u306E\u30B5\u30A4\u30BA\u5909\u66F4",
1048
+ disableResizeWindow: "\u30B5\u30A4\u30BA\u5909\u66F4\u30E2\u30FC\u30C9\u3092\u7121\u52B9\u306B\u3059\u308B",
1049
+ resizeModeHint: "\u30EA\u30B5\u30A4\u30BA\u30CF\u30F3\u30C9\u30EB\u304C\u6709\u52B9\u306B\u306A\u308A\u307E\u3057\u305F\u3002\u8FBA\u3084\u89D2\u3092\u30C9\u30E9\u30C3\u30B0\u3057\u3066\u8ABF\u6574\u3057\u307E\u3059\u3002Esc \u307E\u305F\u306F R \u3067\u7D42\u4E86\u3057\u307E\u3059\u3002",
1050
+ resizeModeEnabled: "\u30B5\u30A4\u30BA\u5909\u66F4\u30E2\u30FC\u30C9\u3092\u6709\u52B9\u306B\u3057\u307E\u3057\u305F\u3002\u8FBA\u3084\u89D2\u3092\u30C9\u30E9\u30C3\u30B0\u3057\u3066\u8ABF\u6574\u3057\u307E\u3059\u3002Esc \u307E\u305F\u306F R \u3067\u7D42\u4E86\u3057\u307E\u3059\u3002",
1051
+ resizeModeDisabled: "\u30B5\u30A4\u30BA\u5909\u66F4\u30E2\u30FC\u30C9\u3092\u7121\u52B9\u306B\u3057\u307E\u3057\u305F\u3002",
1052
+ positionReset: "\u6587\u5B57\u8D77\u3053\u3057\u306E\u4F4D\u7F6E\u3092\u30EA\u30BB\u30C3\u30C8\u3057\u307E\u3057\u305F\u3002",
1005
1053
  styleTranscript: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB\u8A2D\u5B9A\u3092\u958B\u304F",
1006
1054
  closeMenu: "\u30E1\u30CB\u30E5\u30FC\u3092\u9589\u3058\u308B",
1007
- styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB"
1055
+ styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB",
1056
+ autoscroll: "\u81EA\u52D5\u30B9\u30AF\u30ED\u30FC\u30EB",
1057
+ settingsMenu: "\u8A2D\u5B9A\u30E1\u30CB\u30E5\u30FC"
1008
1058
  },
1009
1059
  settings: {
1010
1060
  title: "\u8A2D\u5B9A",
@@ -1183,8 +1233,8 @@ var VidPly = (() => {
1183
1233
  language: `<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/>`,
1184
1234
  hd: `<path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-8 12H9.5v-2h-2v2H6V9h1.5v2.5h2V9H11v6zm7-1c0 .55-.45 1-1 1h-.75v1.5h-1.5V15H14c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v4zm-3.5-.5h2v-3h-2v3z"/>`,
1185
1235
  transcript: `<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>`,
1186
- audioDescription: `<rect x="2" y="5" width="20" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="currentColor">AD</text>`,
1187
- audioDescriptionOn: `<rect x="2" y="5" width="20" height="14" rx="2" fill="#1a1a1a" stroke="#1a1a1a" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="#ffffff">AD</text>`,
1236
+ audioDescription: `<rect x="2" y="5" width="20" height="14" rx="2" fill="#ffffff" stroke="#ffffff" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="#1a1a1a">AD</text>`,
1237
+ audioDescriptionOn: `<rect x="2" y="5" width="20" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="currentColor">AD</text>`,
1188
1238
  signLanguage: `<g transform="scale(1.5)"><path d="M16 11.3c-.1-.9-4.8 1.3-5.4 1.1-2.6-1 5.8-1.3 5.1-2.9s-5.1 1.5-6 1.4C6.5 9.4 16.5 9.1 13.5 8c-1.9-.6-8.8 2.9-6.8.4.7-.6.7-1.9-.7-1.7-9.7 7.2-.7 12.2 8.8 7 0-1.3-3.5.4-4.1.4-2.6 0 5.6-2 5.4-3ZM3.9 7.8c3.2-4.2 3.7 1.2 6 .1s.2-.2.2-.3c.7-2.7 2.5-7.5-1.5-1.3-1.6 0 1.1-4 1-4.6C8.9-1 7.3 4.4 7.2 4.9c-1.6.7-.9-1.4-.7-1.5 3-6-.6-3.1-.9.4-2.5 1.8 0-2.8 0-3.5C2.8-.9 4 9.4 1.1 4.9S.1 4.6 0 5c-.4 2.7 2.6 7.2 3.9 2.8Z"/></g>`,
1189
1239
  signLanguageOn: `<g transform="scale(1.5)"><path d="M16 11.3c-.1-.9-4.8 1.3-5.4 1.1-2.6-1 5.8-1.3 5.1-2.9s-5.1 1.5-6 1.4C6.5 9.4 16.5 9.1 13.5 8c-1.9-.6-8.8 2.9-6.8.4.7-.6.7-1.9-.7-1.7-9.7 7.2-.7 12.2 8.8 7 0-1.3-3.5.4-4.1.4-2.6 0 5.6-2 5.4-3ZM3.9 7.8c3.2-4.2 3.7 1.2 6 .1s.2-.2.2-.3c.7-2.7 2.5-7.5-1.5-1.3-1.6 0 1.1-4 1-4.6C8.9-1 7.3 4.4 7.2 4.9c-1.6.7-.9-1.4-.7-1.5 3-6-.6-3.1-.9.4-2.5 1.8 0-2.8 0-3.5C2.8-.9 4 9.4 1.1 4.9S.1 4.6 0 5c-.4 2.7 2.6 7.2 3.9 2.8Z"/></g>`,
1190
1240
  speaker: `<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>`,
@@ -1792,10 +1842,7 @@ var VidPly = (() => {
1792
1842
  }
1793
1843
  });
1794
1844
  this.controls.currentTimeDisplay = DOMUtils.createElement("span", {
1795
- className: `${this.player.options.classPrefix}-current-time`,
1796
- attributes: {
1797
- "aria-label": i18n.t("time.seconds", { count: 0 })
1798
- }
1845
+ className: `${this.player.options.classPrefix}-current-time`
1799
1846
  });
1800
1847
  const currentTimeVisual = DOMUtils.createElement("span", {
1801
1848
  textContent: "00:00",
@@ -1803,8 +1850,14 @@ var VidPly = (() => {
1803
1850
  "aria-hidden": "true"
1804
1851
  }
1805
1852
  });
1853
+ const currentTimeAccessible = DOMUtils.createElement("span", {
1854
+ className: "vidply-sr-only",
1855
+ textContent: i18n.t("time.seconds", { count: 0 })
1856
+ });
1806
1857
  this.controls.currentTimeDisplay.appendChild(currentTimeVisual);
1858
+ this.controls.currentTimeDisplay.appendChild(currentTimeAccessible);
1807
1859
  this.controls.currentTimeVisual = currentTimeVisual;
1860
+ this.controls.currentTimeAccessible = currentTimeAccessible;
1808
1861
  const separator = DOMUtils.createElement("span", {
1809
1862
  textContent: " / ",
1810
1863
  attributes: {
@@ -1812,10 +1865,7 @@ var VidPly = (() => {
1812
1865
  }
1813
1866
  });
1814
1867
  this.controls.durationDisplay = DOMUtils.createElement("span", {
1815
- className: `${this.player.options.classPrefix}-duration`,
1816
- attributes: {
1817
- "aria-label": i18n.t("time.durationPrefix") + i18n.t("time.seconds", { count: 0 })
1818
- }
1868
+ className: `${this.player.options.classPrefix}-duration`
1819
1869
  });
1820
1870
  const durationVisual = DOMUtils.createElement("span", {
1821
1871
  textContent: "00:00",
@@ -1823,8 +1873,14 @@ var VidPly = (() => {
1823
1873
  "aria-hidden": "true"
1824
1874
  }
1825
1875
  });
1876
+ const durationAccessible = DOMUtils.createElement("span", {
1877
+ className: "vidply-sr-only",
1878
+ textContent: i18n.t("time.durationPrefix") + i18n.t("time.seconds", { count: 0 })
1879
+ });
1826
1880
  this.controls.durationDisplay.appendChild(durationVisual);
1881
+ this.controls.durationDisplay.appendChild(durationAccessible);
1827
1882
  this.controls.durationVisual = durationVisual;
1883
+ this.controls.durationAccessible = durationAccessible;
1828
1884
  container.appendChild(this.controls.currentTimeDisplay);
1829
1885
  container.appendChild(separator);
1830
1886
  container.appendChild(this.controls.durationDisplay);
@@ -2651,14 +2707,18 @@ var VidPly = (() => {
2651
2707
  if (this.controls.currentTimeVisual) {
2652
2708
  const currentTime = this.player.state.currentTime;
2653
2709
  this.controls.currentTimeVisual.textContent = TimeUtils.formatTime(currentTime);
2654
- this.controls.currentTimeDisplay.setAttribute("aria-label", TimeUtils.formatDuration(currentTime));
2710
+ if (this.controls.currentTimeAccessible) {
2711
+ this.controls.currentTimeAccessible.textContent = TimeUtils.formatDuration(currentTime);
2712
+ }
2655
2713
  }
2656
2714
  }
2657
2715
  updateDuration() {
2658
2716
  if (this.controls.durationVisual) {
2659
2717
  const duration = this.player.state.duration;
2660
2718
  this.controls.durationVisual.textContent = TimeUtils.formatTime(duration);
2661
- this.controls.durationDisplay.setAttribute("aria-label", i18n.t("time.durationPrefix") + TimeUtils.formatDuration(duration));
2719
+ if (this.controls.durationAccessible) {
2720
+ this.controls.durationAccessible.textContent = i18n.t("time.durationPrefix") + TimeUtils.formatDuration(duration);
2721
+ }
2662
2722
  }
2663
2723
  }
2664
2724
  updateVolumeDisplay() {
@@ -3313,6 +3373,596 @@ var VidPly = (() => {
3313
3373
  }
3314
3374
  };
3315
3375
 
3376
+ // src/utils/DraggableResizable.js
3377
+ var DraggableResizable = class {
3378
+ constructor(element, options = {}) {
3379
+ this.element = element;
3380
+ this.options = {
3381
+ dragHandle: null,
3382
+ // Element to use as drag handle (defaults to element itself)
3383
+ resizeHandles: [],
3384
+ // Array of resize handle elements
3385
+ onDragStart: null,
3386
+ onDrag: null,
3387
+ onDragEnd: null,
3388
+ onResizeStart: null,
3389
+ onResize: null,
3390
+ onResizeEnd: null,
3391
+ constrainToViewport: true,
3392
+ // Allow movement outside viewport?
3393
+ minWidth: 150,
3394
+ minHeight: 100,
3395
+ maintainAspectRatio: false,
3396
+ keyboardDragKey: "d",
3397
+ keyboardResizeKey: "r",
3398
+ keyboardStep: 5,
3399
+ keyboardStepLarge: 10,
3400
+ maxWidth: null,
3401
+ maxHeight: null,
3402
+ pointerResizeIndicatorText: null,
3403
+ onPointerResizeToggle: null,
3404
+ classPrefix: "draggable",
3405
+ storage: null,
3406
+ // StorageManager instance for saving position/size
3407
+ storageKey: null,
3408
+ // Key for localStorage (if storage is provided)
3409
+ ...options
3410
+ };
3411
+ this.isDragging = false;
3412
+ this.isResizing = false;
3413
+ this.resizeDirection = null;
3414
+ this.dragOffsetX = 0;
3415
+ this.dragOffsetY = 0;
3416
+ this.positionOffsetX = 0;
3417
+ this.positionOffsetY = 0;
3418
+ this.initialMouseX = 0;
3419
+ this.initialMouseY = 0;
3420
+ this.needsPositionConversion = false;
3421
+ this.resizeStartX = 0;
3422
+ this.resizeStartY = 0;
3423
+ this.resizeStartWidth = 0;
3424
+ this.resizeStartHeight = 0;
3425
+ this.resizeStartLeft = 0;
3426
+ this.resizeStartTop = 0;
3427
+ this.keyboardDragMode = false;
3428
+ this.keyboardResizeMode = false;
3429
+ this.pointerResizeMode = false;
3430
+ this.manuallyPositioned = false;
3431
+ this.resizeHandlesManaged = /* @__PURE__ */ new Map();
3432
+ this.resizeIndicatorElement = null;
3433
+ this.handlers = {
3434
+ mousedown: this.onMouseDown.bind(this),
3435
+ mousemove: this.onMouseMove.bind(this),
3436
+ mouseup: this.onMouseUp.bind(this),
3437
+ touchstart: this.onTouchStart.bind(this),
3438
+ touchmove: this.onTouchMove.bind(this),
3439
+ touchend: this.onTouchEnd.bind(this),
3440
+ keydown: this.onKeyDown.bind(this),
3441
+ resizeHandleMousedown: this.onResizeHandleMouseDown.bind(this)
3442
+ };
3443
+ this.init();
3444
+ }
3445
+ hasManagedResizeHandles() {
3446
+ return Array.from(this.resizeHandlesManaged.values()).some(Boolean);
3447
+ }
3448
+ storeOriginalHandleDisplay(handle) {
3449
+ if (!handle.dataset.originalDisplay) {
3450
+ handle.dataset.originalDisplay = handle.style.display || "";
3451
+ }
3452
+ }
3453
+ hideResizeHandle(handle) {
3454
+ handle.style.display = "none";
3455
+ handle.setAttribute("aria-hidden", "true");
3456
+ }
3457
+ showResizeHandle(handle) {
3458
+ const original = handle.dataset.originalDisplay !== void 0 ? handle.dataset.originalDisplay : "";
3459
+ handle.style.display = original;
3460
+ handle.removeAttribute("aria-hidden");
3461
+ }
3462
+ setManagedHandlesVisible(visible) {
3463
+ if (!this.options.resizeHandles || this.options.resizeHandles.length === 0) {
3464
+ return;
3465
+ }
3466
+ this.options.resizeHandles.forEach((handle) => {
3467
+ if (!this.resizeHandlesManaged.get(handle)) {
3468
+ return;
3469
+ }
3470
+ if (visible) {
3471
+ this.showResizeHandle(handle);
3472
+ } else {
3473
+ this.hideResizeHandle(handle);
3474
+ }
3475
+ });
3476
+ }
3477
+ init() {
3478
+ const dragHandle = this.options.dragHandle || this.element;
3479
+ dragHandle.addEventListener("mousedown", this.handlers.mousedown);
3480
+ dragHandle.addEventListener("touchstart", this.handlers.touchstart);
3481
+ document.addEventListener("mousemove", this.handlers.mousemove);
3482
+ document.addEventListener("mouseup", this.handlers.mouseup);
3483
+ document.addEventListener("touchmove", this.handlers.touchmove, { passive: false });
3484
+ document.addEventListener("touchend", this.handlers.touchend);
3485
+ this.element.addEventListener("keydown", this.handlers.keydown);
3486
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
3487
+ this.options.resizeHandles.forEach((handle) => {
3488
+ handle.addEventListener("mousedown", this.handlers.resizeHandleMousedown);
3489
+ handle.addEventListener("touchstart", this.handlers.resizeHandleMousedown);
3490
+ const managed = handle.dataset.vidplyManagedResize === "true";
3491
+ this.resizeHandlesManaged.set(handle, managed);
3492
+ if (managed) {
3493
+ this.storeOriginalHandleDisplay(handle);
3494
+ this.hideResizeHandle(handle);
3495
+ }
3496
+ });
3497
+ }
3498
+ }
3499
+ onMouseDown(e) {
3500
+ if (e.target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
3501
+ return;
3502
+ }
3503
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
3504
+ return;
3505
+ }
3506
+ this.startDragging(e.clientX, e.clientY);
3507
+ e.preventDefault();
3508
+ }
3509
+ onTouchStart(e) {
3510
+ if (e.target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
3511
+ return;
3512
+ }
3513
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
3514
+ return;
3515
+ }
3516
+ const touch = e.touches[0];
3517
+ this.startDragging(touch.clientX, touch.clientY);
3518
+ }
3519
+ onResizeHandleMouseDown(e) {
3520
+ var _a, _b, _c, _d;
3521
+ e.preventDefault();
3522
+ e.stopPropagation();
3523
+ const handle = e.target;
3524
+ this.resizeDirection = handle.getAttribute("data-direction");
3525
+ const clientX = e.clientX || ((_b = (_a = e.touches) == null ? void 0 : _a[0]) == null ? void 0 : _b.clientX);
3526
+ const clientY = e.clientY || ((_d = (_c = e.touches) == null ? void 0 : _c[0]) == null ? void 0 : _d.clientY);
3527
+ this.startResizing(clientX, clientY);
3528
+ }
3529
+ onMouseMove(e) {
3530
+ if (this.isDragging) {
3531
+ this.drag(e.clientX, e.clientY);
3532
+ e.preventDefault();
3533
+ } else if (this.isResizing) {
3534
+ this.resize(e.clientX, e.clientY);
3535
+ e.preventDefault();
3536
+ }
3537
+ }
3538
+ onTouchMove(e) {
3539
+ if (this.isDragging || this.isResizing) {
3540
+ const touch = e.touches[0];
3541
+ if (this.isDragging) {
3542
+ this.drag(touch.clientX, touch.clientY);
3543
+ } else {
3544
+ this.resize(touch.clientX, touch.clientY);
3545
+ }
3546
+ e.preventDefault();
3547
+ }
3548
+ }
3549
+ onMouseUp() {
3550
+ if (this.isDragging) {
3551
+ this.stopDragging();
3552
+ } else if (this.isResizing) {
3553
+ this.stopResizing();
3554
+ }
3555
+ }
3556
+ onTouchEnd() {
3557
+ if (this.isDragging) {
3558
+ this.stopDragging();
3559
+ } else if (this.isResizing) {
3560
+ this.stopResizing();
3561
+ }
3562
+ }
3563
+ onKeyDown(e) {
3564
+ if (e.key.toLowerCase() === this.options.keyboardDragKey.toLowerCase()) {
3565
+ e.preventDefault();
3566
+ this.toggleKeyboardDragMode();
3567
+ return;
3568
+ }
3569
+ if (e.key.toLowerCase() === this.options.keyboardResizeKey.toLowerCase()) {
3570
+ e.preventDefault();
3571
+ if (this.hasManagedResizeHandles()) {
3572
+ this.togglePointerResizeMode();
3573
+ } else {
3574
+ this.toggleKeyboardResizeMode();
3575
+ }
3576
+ return;
3577
+ }
3578
+ if (e.key === "Escape") {
3579
+ if (this.pointerResizeMode) {
3580
+ e.preventDefault();
3581
+ this.disablePointerResizeMode();
3582
+ return;
3583
+ }
3584
+ if (this.keyboardDragMode || this.keyboardResizeMode) {
3585
+ e.preventDefault();
3586
+ this.disableKeyboardDragMode();
3587
+ this.disableKeyboardResizeMode();
3588
+ return;
3589
+ }
3590
+ }
3591
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
3592
+ if (this.keyboardDragMode) {
3593
+ e.preventDefault();
3594
+ e.stopPropagation();
3595
+ this.keyboardDrag(e.key, e.shiftKey);
3596
+ } else if (this.keyboardResizeMode) {
3597
+ e.preventDefault();
3598
+ e.stopPropagation();
3599
+ this.keyboardResize(e.key, e.shiftKey);
3600
+ }
3601
+ }
3602
+ if (e.key === "Home" && (this.keyboardDragMode || this.keyboardResizeMode)) {
3603
+ e.preventDefault();
3604
+ this.resetPosition();
3605
+ }
3606
+ }
3607
+ startDragging(clientX, clientY) {
3608
+ const rect = this.element.getBoundingClientRect();
3609
+ const computedStyle = window.getComputedStyle(this.element);
3610
+ const needsConversion = computedStyle.right !== "auto" || computedStyle.bottom !== "auto" || computedStyle.transform !== "none";
3611
+ this.positionOffsetX = 0;
3612
+ this.positionOffsetY = 0;
3613
+ if (needsConversion) {
3614
+ let targetLeft, targetTop;
3615
+ if (computedStyle.position === "absolute") {
3616
+ const offsetParent = this.element.offsetParent || document.body;
3617
+ const parentRect = offsetParent.getBoundingClientRect();
3618
+ targetLeft = rect.left - parentRect.left;
3619
+ targetTop = rect.top - parentRect.top;
3620
+ this.positionOffsetX = parentRect.left;
3621
+ this.positionOffsetY = parentRect.top;
3622
+ } else if (computedStyle.position === "fixed") {
3623
+ const parsedLeft = parseFloat(computedStyle.left);
3624
+ const parsedTop = parseFloat(computedStyle.top);
3625
+ const hasLeft = Number.isFinite(parsedLeft);
3626
+ const hasTop = Number.isFinite(parsedTop);
3627
+ targetLeft = hasLeft ? parsedLeft : rect.left;
3628
+ targetTop = hasTop ? parsedTop : rect.top;
3629
+ this.positionOffsetX = rect.left - targetLeft;
3630
+ this.positionOffsetY = rect.top - targetTop;
3631
+ } else {
3632
+ targetLeft = rect.left;
3633
+ targetTop = rect.top;
3634
+ this.positionOffsetX = rect.left - targetLeft;
3635
+ this.positionOffsetY = rect.top - targetTop;
3636
+ }
3637
+ const currentCssText = this.element.style.cssText;
3638
+ let newCssText = currentCssText.split(";").filter((rule) => {
3639
+ const trimmed = rule.trim();
3640
+ return trimmed && !trimmed.startsWith("right:") && !trimmed.startsWith("bottom:") && !trimmed.startsWith("transform:") && !trimmed.startsWith("left:") && !trimmed.startsWith("top:") && !trimmed.startsWith("inset:");
3641
+ }).join("; ");
3642
+ if (newCssText) newCssText += "; ";
3643
+ newCssText += `left: ${targetLeft}px; top: ${targetTop}px; right: auto; bottom: auto; transform: none`;
3644
+ this.element.style.cssText = newCssText;
3645
+ }
3646
+ const finalRect = this.element.getBoundingClientRect();
3647
+ this.dragOffsetX = clientX - finalRect.left;
3648
+ this.dragOffsetY = clientY - finalRect.top;
3649
+ this.isDragging = true;
3650
+ this.element.classList.add(`${this.options.classPrefix}-dragging`);
3651
+ document.body.style.cursor = "grabbing";
3652
+ document.body.style.userSelect = "none";
3653
+ }
3654
+ drag(clientX, clientY) {
3655
+ if (!this.isDragging) return;
3656
+ let newX = clientX - this.dragOffsetX - this.positionOffsetX;
3657
+ let newY = clientY - this.dragOffsetY - this.positionOffsetY;
3658
+ if (this.options.constrainToViewport) {
3659
+ const rect = this.element.getBoundingClientRect();
3660
+ const viewportWidth = document.documentElement.clientWidth;
3661
+ const viewportHeight = document.documentElement.clientHeight;
3662
+ const minVisible = 100;
3663
+ const minX = -(rect.width - minVisible);
3664
+ const minY = -(rect.height - minVisible);
3665
+ const maxX = viewportWidth - minVisible;
3666
+ const maxY = viewportHeight - minVisible;
3667
+ newX = Math.max(minX, Math.min(newX, maxX));
3668
+ newY = Math.max(minY, Math.min(newY, maxY));
3669
+ }
3670
+ this.element.style.left = `${newX}px`;
3671
+ this.element.style.top = `${newY}px`;
3672
+ if (this.options.onDrag) {
3673
+ this.options.onDrag({ x: newX, y: newY });
3674
+ }
3675
+ }
3676
+ stopDragging() {
3677
+ this.isDragging = false;
3678
+ this.element.classList.remove(`${this.options.classPrefix}-dragging`);
3679
+ document.body.style.cursor = "";
3680
+ document.body.style.userSelect = "";
3681
+ this.manuallyPositioned = true;
3682
+ if (this.options.onDragEnd) {
3683
+ this.options.onDragEnd();
3684
+ }
3685
+ }
3686
+ startResizing(clientX, clientY) {
3687
+ this.isResizing = true;
3688
+ this.resizeStartX = clientX;
3689
+ this.resizeStartY = clientY;
3690
+ const rect = this.element.getBoundingClientRect();
3691
+ this.resizeStartWidth = rect.width;
3692
+ this.resizeStartHeight = rect.height;
3693
+ this.resizeStartLeft = rect.left;
3694
+ this.resizeStartTop = rect.top;
3695
+ this.element.classList.add(`${this.options.classPrefix}-resizing`);
3696
+ document.body.style.userSelect = "none";
3697
+ if (this.options.onResizeStart) {
3698
+ this.options.onResizeStart();
3699
+ }
3700
+ }
3701
+ resize(clientX, clientY) {
3702
+ if (!this.isResizing) return;
3703
+ const deltaX = clientX - this.resizeStartX;
3704
+ const deltaY = clientY - this.resizeStartY;
3705
+ let newWidth = this.resizeStartWidth;
3706
+ let newHeight = this.resizeStartHeight;
3707
+ let newLeft = this.resizeStartLeft;
3708
+ let newTop = this.resizeStartTop;
3709
+ if (this.resizeDirection.includes("e")) {
3710
+ newWidth = Math.max(this.options.minWidth, this.resizeStartWidth + deltaX);
3711
+ }
3712
+ if (this.resizeDirection.includes("w")) {
3713
+ const proposedWidth = Math.max(this.options.minWidth, this.resizeStartWidth - deltaX);
3714
+ newLeft = this.resizeStartLeft + (this.resizeStartWidth - proposedWidth);
3715
+ newWidth = proposedWidth;
3716
+ }
3717
+ const maxWidthOption = typeof this.options.maxWidth === "function" ? this.options.maxWidth() : this.options.maxWidth;
3718
+ if (Number.isFinite(maxWidthOption)) {
3719
+ const clampedWidth = Math.min(newWidth, maxWidthOption);
3720
+ if (clampedWidth !== newWidth && this.resizeDirection.includes("w")) {
3721
+ newLeft += newWidth - clampedWidth;
3722
+ }
3723
+ newWidth = clampedWidth;
3724
+ }
3725
+ if (!this.options.maintainAspectRatio) {
3726
+ if (this.resizeDirection.includes("s")) {
3727
+ newHeight = Math.max(this.options.minHeight, this.resizeStartHeight + deltaY);
3728
+ }
3729
+ if (this.resizeDirection.includes("n")) {
3730
+ const proposedHeight = Math.max(this.options.minHeight, this.resizeStartHeight - deltaY);
3731
+ newTop = this.resizeStartTop + (this.resizeStartHeight - proposedHeight);
3732
+ newHeight = proposedHeight;
3733
+ }
3734
+ const maxHeightOption = typeof this.options.maxHeight === "function" ? this.options.maxHeight() : this.options.maxHeight;
3735
+ if (Number.isFinite(maxHeightOption)) {
3736
+ const clampedHeight = Math.min(newHeight, maxHeightOption);
3737
+ if (clampedHeight !== newHeight && this.resizeDirection.includes("n")) {
3738
+ newTop += newHeight - clampedHeight;
3739
+ }
3740
+ newHeight = clampedHeight;
3741
+ }
3742
+ }
3743
+ this.element.style.width = `${newWidth}px`;
3744
+ if (!this.options.maintainAspectRatio) {
3745
+ this.element.style.height = `${newHeight}px`;
3746
+ } else {
3747
+ this.element.style.height = "auto";
3748
+ }
3749
+ if (this.resizeDirection.includes("w")) {
3750
+ this.element.style.left = `${newLeft}px`;
3751
+ }
3752
+ if (this.resizeDirection.includes("n") && !this.options.maintainAspectRatio) {
3753
+ this.element.style.top = `${newTop}px`;
3754
+ }
3755
+ if (this.options.onResize) {
3756
+ this.options.onResize({ width: newWidth, height: newHeight, left: newLeft, top: newTop });
3757
+ }
3758
+ }
3759
+ stopResizing() {
3760
+ this.isResizing = false;
3761
+ this.resizeDirection = null;
3762
+ this.element.classList.remove(`${this.options.classPrefix}-resizing`);
3763
+ document.body.style.userSelect = "";
3764
+ this.manuallyPositioned = true;
3765
+ if (this.options.onResizeEnd) {
3766
+ this.options.onResizeEnd();
3767
+ }
3768
+ }
3769
+ toggleKeyboardDragMode() {
3770
+ if (this.keyboardDragMode) {
3771
+ this.disableKeyboardDragMode();
3772
+ } else {
3773
+ this.enableKeyboardDragMode();
3774
+ }
3775
+ }
3776
+ enableKeyboardDragMode() {
3777
+ this.keyboardDragMode = true;
3778
+ this.keyboardResizeMode = false;
3779
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-drag`);
3780
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
3781
+ this.focusElement();
3782
+ }
3783
+ disableKeyboardDragMode() {
3784
+ this.keyboardDragMode = false;
3785
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
3786
+ }
3787
+ toggleKeyboardResizeMode() {
3788
+ if (this.keyboardResizeMode) {
3789
+ this.disableKeyboardResizeMode();
3790
+ } else {
3791
+ this.enableKeyboardResizeMode();
3792
+ }
3793
+ }
3794
+ enableKeyboardResizeMode() {
3795
+ this.keyboardResizeMode = true;
3796
+ this.keyboardDragMode = false;
3797
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-resize`);
3798
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
3799
+ this.focusElement();
3800
+ }
3801
+ disableKeyboardResizeMode() {
3802
+ this.keyboardResizeMode = false;
3803
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
3804
+ }
3805
+ enablePointerResizeMode({ focus = true } = {}) {
3806
+ if (!this.hasManagedResizeHandles()) {
3807
+ this.enableKeyboardResizeMode();
3808
+ return;
3809
+ }
3810
+ if (this.pointerResizeMode) {
3811
+ return;
3812
+ }
3813
+ this.pointerResizeMode = true;
3814
+ this.setManagedHandlesVisible(true);
3815
+ this.element.classList.add(`${this.options.classPrefix}-resizable`);
3816
+ this.enableKeyboardResizeMode();
3817
+ if (focus) {
3818
+ this.focusElement();
3819
+ }
3820
+ if (typeof this.options.onPointerResizeToggle === "function") {
3821
+ this.options.onPointerResizeToggle(true);
3822
+ }
3823
+ }
3824
+ disablePointerResizeMode({ focus = false } = {}) {
3825
+ if (!this.pointerResizeMode) {
3826
+ return;
3827
+ }
3828
+ this.pointerResizeMode = false;
3829
+ this.setManagedHandlesVisible(false);
3830
+ this.element.classList.remove(`${this.options.classPrefix}-resizable`);
3831
+ this.disableKeyboardResizeMode();
3832
+ if (focus) {
3833
+ this.focusElement();
3834
+ }
3835
+ if (typeof this.options.onPointerResizeToggle === "function") {
3836
+ this.options.onPointerResizeToggle(false);
3837
+ }
3838
+ }
3839
+ togglePointerResizeMode() {
3840
+ if (this.pointerResizeMode) {
3841
+ this.disablePointerResizeMode();
3842
+ } else {
3843
+ this.enablePointerResizeMode();
3844
+ }
3845
+ return this.pointerResizeMode;
3846
+ }
3847
+ focusElement() {
3848
+ if (typeof this.element.focus === "function") {
3849
+ try {
3850
+ this.element.focus({ preventScroll: true });
3851
+ } catch (e) {
3852
+ this.element.focus();
3853
+ }
3854
+ }
3855
+ }
3856
+ keyboardDrag(key, shiftKey) {
3857
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
3858
+ let currentLeft = parseFloat(this.element.style.left) || 0;
3859
+ let currentTop = parseFloat(this.element.style.top) || 0;
3860
+ const computedStyle = window.getComputedStyle(this.element);
3861
+ if (computedStyle.transform !== "none") {
3862
+ const rect = this.element.getBoundingClientRect();
3863
+ currentLeft = rect.left;
3864
+ currentTop = rect.top;
3865
+ this.element.style.transform = "none";
3866
+ this.element.style.left = `${currentLeft}px`;
3867
+ this.element.style.top = `${currentTop}px`;
3868
+ }
3869
+ let newX = currentLeft;
3870
+ let newY = currentTop;
3871
+ switch (key) {
3872
+ case "ArrowLeft":
3873
+ newX -= step;
3874
+ break;
3875
+ case "ArrowRight":
3876
+ newX += step;
3877
+ break;
3878
+ case "ArrowUp":
3879
+ newY -= step;
3880
+ break;
3881
+ case "ArrowDown":
3882
+ newY += step;
3883
+ break;
3884
+ }
3885
+ this.element.style.left = `${newX}px`;
3886
+ this.element.style.top = `${newY}px`;
3887
+ if (this.options.onDrag) {
3888
+ this.options.onDrag({ x: newX, y: newY });
3889
+ }
3890
+ }
3891
+ keyboardResize(key, shiftKey) {
3892
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
3893
+ const rect = this.element.getBoundingClientRect();
3894
+ let width = rect.width;
3895
+ let height = rect.height;
3896
+ switch (key) {
3897
+ case "ArrowLeft":
3898
+ width -= step;
3899
+ break;
3900
+ case "ArrowRight":
3901
+ width += step;
3902
+ break;
3903
+ case "ArrowUp":
3904
+ if (this.options.maintainAspectRatio) {
3905
+ width += step;
3906
+ } else {
3907
+ height -= step;
3908
+ }
3909
+ break;
3910
+ case "ArrowDown":
3911
+ if (this.options.maintainAspectRatio) {
3912
+ width -= step;
3913
+ } else {
3914
+ height += step;
3915
+ }
3916
+ break;
3917
+ }
3918
+ width = Math.max(this.options.minWidth, width);
3919
+ height = Math.max(this.options.minHeight, height);
3920
+ this.element.style.width = `${width}px`;
3921
+ if (!this.options.maintainAspectRatio) {
3922
+ this.element.style.height = `${height}px`;
3923
+ } else {
3924
+ this.element.style.height = "auto";
3925
+ }
3926
+ if (this.options.onResize) {
3927
+ this.options.onResize({ width, height });
3928
+ }
3929
+ }
3930
+ resetPosition() {
3931
+ this.element.style.left = "50%";
3932
+ this.element.style.top = "50%";
3933
+ this.element.style.transform = "translate(-50%, -50%)";
3934
+ this.element.style.right = "";
3935
+ this.element.style.bottom = "";
3936
+ this.manuallyPositioned = false;
3937
+ if (this.options.onDrag) {
3938
+ this.options.onDrag({ centered: true });
3939
+ }
3940
+ }
3941
+ destroy() {
3942
+ const dragHandle = this.options.dragHandle || this.element;
3943
+ this.disablePointerResizeMode();
3944
+ dragHandle.removeEventListener("mousedown", this.handlers.mousedown);
3945
+ dragHandle.removeEventListener("touchstart", this.handlers.touchstart);
3946
+ document.removeEventListener("mousemove", this.handlers.mousemove);
3947
+ document.removeEventListener("mouseup", this.handlers.mouseup);
3948
+ document.removeEventListener("touchmove", this.handlers.touchmove);
3949
+ document.removeEventListener("touchend", this.handlers.touchend);
3950
+ this.element.removeEventListener("keydown", this.handlers.keydown);
3951
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
3952
+ this.options.resizeHandles.forEach((handle) => {
3953
+ handle.removeEventListener("mousedown", this.handlers.resizeHandleMousedown);
3954
+ handle.removeEventListener("touchstart", this.handlers.resizeHandleMousedown);
3955
+ });
3956
+ }
3957
+ this.element.classList.remove(
3958
+ `${this.options.classPrefix}-dragging`,
3959
+ `${this.options.classPrefix}-resizing`,
3960
+ `${this.options.classPrefix}-keyboard-drag`,
3961
+ `${this.options.classPrefix}-keyboard-resize`
3962
+ );
3963
+ }
3964
+ };
3965
+
3316
3966
  // src/controls/TranscriptManager.js
3317
3967
  var TranscriptManager = class {
3318
3968
  constructor(player) {
@@ -3323,25 +3973,26 @@ var VidPly = (() => {
3323
3973
  this.currentActiveEntry = null;
3324
3974
  this.isVisible = false;
3325
3975
  this.storage = new StorageManager("vidply");
3326
- this.isDragging = false;
3327
- this.dragOffsetX = 0;
3328
- this.dragOffsetY = 0;
3329
- this.isResizing = false;
3330
- this.resizeDirection = null;
3331
- this.resizeStartX = 0;
3332
- this.resizeStartY = 0;
3333
- this.resizeStartWidth = 0;
3334
- this.resizeStartHeight = 0;
3335
- this.resizeEnabled = false;
3976
+ this.draggableResizable = null;
3336
3977
  this.settingsMenuVisible = false;
3337
3978
  this.settingsMenu = null;
3338
3979
  this.settingsButton = null;
3339
3980
  this.settingsMenuJustOpened = false;
3340
- this.keyboardDragMode = false;
3981
+ this.resizeOptionButton = null;
3982
+ this.resizeOptionText = null;
3983
+ this.resizeModeIndicator = null;
3984
+ this.resizeModeIndicatorTimeout = null;
3985
+ this.transcriptResizeHandles = [];
3986
+ this.liveRegion = null;
3341
3987
  this.styleDialog = null;
3342
3988
  this.styleDialogVisible = false;
3343
3989
  this.styleDialogJustOpened = false;
3990
+ this.languageSelector = null;
3991
+ this.currentTranscriptLanguage = null;
3992
+ this.availableTranscriptLanguages = [];
3993
+ this.languageSelectorHandler = null;
3344
3994
  const savedPreferences = this.storage.getTranscriptPreferences();
3995
+ this.autoscrollEnabled = (savedPreferences == null ? void 0 : savedPreferences.autoscroll) !== void 0 ? savedPreferences.autoscroll : true;
3345
3996
  this.transcriptStyle = {
3346
3997
  fontSize: (savedPreferences == null ? void 0 : savedPreferences.fontSize) || this.player.options.transcriptFontSize || "100%",
3347
3998
  fontFamily: (savedPreferences == null ? void 0 : savedPreferences.fontFamily) || this.player.options.transcriptFontFamily || "sans-serif",
@@ -3352,25 +4003,22 @@ var VidPly = (() => {
3352
4003
  this.handlers = {
3353
4004
  timeupdate: () => this.updateActiveEntry(),
3354
4005
  resize: null,
3355
- mousemove: null,
3356
- mouseup: null,
3357
- touchmove: null,
3358
- touchend: null,
3359
- mousedown: null,
3360
- touchstart: null,
3361
- keydown: null,
3362
4006
  settingsClick: null,
3363
4007
  settingsKeydown: null,
3364
4008
  documentClick: null,
3365
4009
  styleDialogKeydown: null
3366
4010
  };
4011
+ this.timeouts = /* @__PURE__ */ new Set();
3367
4012
  this.init();
3368
4013
  }
3369
4014
  init() {
4015
+ this.setupMetadataHandlingOnLoad();
3370
4016
  this.player.on("timeupdate", this.handlers.timeupdate);
3371
4017
  this.player.on("fullscreenchange", () => {
3372
4018
  if (this.isVisible) {
3373
- setTimeout(() => this.positionTranscript(), 100);
4019
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4020
+ this.setManagedTimeout(() => this.positionTranscript(), 100);
4021
+ }
3374
4022
  }
3375
4023
  });
3376
4024
  }
@@ -3391,9 +4039,12 @@ var VidPly = (() => {
3391
4039
  if (this.transcriptWindow) {
3392
4040
  this.transcriptWindow.style.display = "flex";
3393
4041
  this.isVisible = true;
3394
- setTimeout(() => {
3395
- if (this.settingsButton) {
3396
- this.settingsButton.focus();
4042
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === "function") {
4043
+ this.player.controlBar.updateTranscriptButton();
4044
+ }
4045
+ this.setManagedTimeout(() => {
4046
+ if (this.transcriptHeader) {
4047
+ this.transcriptHeader.focus();
3397
4048
  }
3398
4049
  }, 150);
3399
4050
  return;
@@ -3402,10 +4053,12 @@ var VidPly = (() => {
3402
4053
  this.loadTranscriptData();
3403
4054
  if (this.transcriptWindow) {
3404
4055
  this.transcriptWindow.style.display = "flex";
3405
- setTimeout(() => this.positionTranscript(), 0);
3406
- setTimeout(() => {
3407
- if (this.settingsButton) {
3408
- this.settingsButton.focus();
4056
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4057
+ this.setManagedTimeout(() => this.positionTranscript(), 0);
4058
+ }
4059
+ this.setManagedTimeout(() => {
4060
+ if (this.transcriptHeader) {
4061
+ this.transcriptHeader.focus();
3409
4062
  }
3410
4063
  }, 150);
3411
4064
  }
@@ -3414,11 +4067,27 @@ var VidPly = (() => {
3414
4067
  /**
3415
4068
  * Hide transcript window
3416
4069
  */
3417
- hideTranscript() {
4070
+ hideTranscript({ focusButton = false } = {}) {
4071
+ var _a, _b;
3418
4072
  if (this.transcriptWindow) {
3419
4073
  this.transcriptWindow.style.display = "none";
3420
4074
  this.isVisible = false;
3421
4075
  }
4076
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
4077
+ this.draggableResizable.disablePointerResizeMode();
4078
+ this.updateResizeOptionState();
4079
+ }
4080
+ this.hideResizeModeIndicator();
4081
+ this.announceLive("");
4082
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === "function") {
4083
+ this.player.controlBar.updateTranscriptButton();
4084
+ }
4085
+ if (focusButton) {
4086
+ const transcriptButton = (_b = (_a = this.player.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.transcript;
4087
+ if (transcriptButton && typeof transcriptButton.focus === "function") {
4088
+ transcriptButton.focus();
4089
+ }
4090
+ }
3422
4091
  }
3423
4092
  /**
3424
4093
  * Create the transcript window UI
@@ -3435,7 +4104,6 @@ var VidPly = (() => {
3435
4104
  this.transcriptHeader = DOMUtils.createElement("div", {
3436
4105
  className: `${this.player.options.classPrefix}-transcript-header`,
3437
4106
  attributes: {
3438
- "aria-label": "Drag to reposition transcript. Use arrow keys to move, Home to reset position, Escape to close.",
3439
4107
  "tabindex": "0"
3440
4108
  }
3441
4109
  });
@@ -3446,7 +4114,7 @@ var VidPly = (() => {
3446
4114
  className: `${this.player.options.classPrefix}-transcript-settings`,
3447
4115
  attributes: {
3448
4116
  "type": "button",
3449
- "aria-label": i18n.t("transcript.settings"),
4117
+ "aria-label": i18n.t("transcript.settingsMenu"),
3450
4118
  "aria-expanded": "false"
3451
4119
  }
3452
4120
  });
@@ -3478,10 +4146,43 @@ var VidPly = (() => {
3478
4146
  };
3479
4147
  this.settingsButton.addEventListener("keydown", this.handlers.settingsKeydown);
3480
4148
  const title = DOMUtils.createElement("h3", {
3481
- textContent: i18n.t("transcript.title")
4149
+ textContent: `${i18n.t("transcript.title")}. ${i18n.t("transcript.dragResizePrompt")}`
4150
+ });
4151
+ const autoscrollLabel = DOMUtils.createElement("label", {
4152
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
4153
+ attributes: {
4154
+ "title": i18n.t("transcript.autoscroll")
4155
+ }
3482
4156
  });
4157
+ this.autoscrollCheckbox = DOMUtils.createElement("input", {
4158
+ attributes: {
4159
+ "type": "checkbox",
4160
+ "checked": this.autoscrollEnabled,
4161
+ "aria-label": i18n.t("transcript.autoscroll")
4162
+ }
4163
+ });
4164
+ const autoscrollText = DOMUtils.createElement("span", {
4165
+ textContent: i18n.t("transcript.autoscroll"),
4166
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-text`
4167
+ });
4168
+ autoscrollLabel.appendChild(this.autoscrollCheckbox);
4169
+ autoscrollLabel.appendChild(autoscrollText);
4170
+ this.autoscrollCheckbox.addEventListener("change", (e) => {
4171
+ this.autoscrollEnabled = e.target.checked;
4172
+ this.saveAutoscrollPreference();
4173
+ });
4174
+ this.transcriptHeader.appendChild(title);
3483
4175
  this.headerLeft.appendChild(this.settingsButton);
3484
- this.headerLeft.appendChild(title);
4176
+ this.headerLeft.appendChild(autoscrollLabel);
4177
+ this.languageSelector = DOMUtils.createElement("select", {
4178
+ className: `${this.player.options.classPrefix}-transcript-language-select`,
4179
+ attributes: {
4180
+ "aria-label": i18n.t("settings.language") || "Language",
4181
+ "style": "display: none;"
4182
+ // Hidden until we detect multiple languages
4183
+ }
4184
+ });
4185
+ this.headerLeft.appendChild(this.languageSelector);
3485
4186
  const closeButton = DOMUtils.createElement("button", {
3486
4187
  className: `${this.player.options.classPrefix}-transcript-close`,
3487
4188
  attributes: {
@@ -3490,7 +4191,7 @@ var VidPly = (() => {
3490
4191
  }
3491
4192
  });
3492
4193
  closeButton.appendChild(createIconElement("close"));
3493
- closeButton.addEventListener("click", () => this.hideTranscript());
4194
+ closeButton.addEventListener("click", () => this.hideTranscript({ focusButton: true }));
3494
4195
  this.transcriptHeader.appendChild(this.headerLeft);
3495
4196
  this.transcriptHeader.appendChild(closeButton);
3496
4197
  this.transcriptContent = DOMUtils.createElement("div", {
@@ -3498,9 +4199,20 @@ var VidPly = (() => {
3498
4199
  });
3499
4200
  this.transcriptWindow.appendChild(this.transcriptHeader);
3500
4201
  this.transcriptWindow.appendChild(this.transcriptContent);
4202
+ this.createResizeHandles();
4203
+ this.liveRegion = DOMUtils.createElement("div", {
4204
+ className: "vidply-sr-only",
4205
+ attributes: {
4206
+ "aria-live": "polite",
4207
+ "aria-atomic": "true"
4208
+ }
4209
+ });
4210
+ this.transcriptWindow.appendChild(this.liveRegion);
3501
4211
  this.player.container.appendChild(this.transcriptWindow);
3502
- this.positionTranscript();
3503
4212
  this.setupDragAndDrop();
4213
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4214
+ this.positionTranscript();
4215
+ }
3504
4216
  this.handlers.documentClick = (e) => {
3505
4217
  if (this.settingsMenuJustOpened) {
3506
4218
  return;
@@ -3524,16 +4236,42 @@ var VidPly = (() => {
3524
4236
  this.documentClickHandlerAdded = false;
3525
4237
  let resizeTimeout;
3526
4238
  this.handlers.resize = () => {
3527
- clearTimeout(resizeTimeout);
3528
- resizeTimeout = setTimeout(() => this.positionTranscript(), 100);
4239
+ if (resizeTimeout) {
4240
+ this.clearManagedTimeout(resizeTimeout);
4241
+ }
4242
+ resizeTimeout = this.setManagedTimeout(() => {
4243
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4244
+ this.positionTranscript();
4245
+ }
4246
+ }, 100);
3529
4247
  };
3530
4248
  window.addEventListener("resize", this.handlers.resize);
3531
4249
  }
4250
+ createResizeHandles() {
4251
+ if (!this.transcriptWindow) return;
4252
+ const directions = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
4253
+ this.transcriptResizeHandles = directions.map((direction) => {
4254
+ const handle = DOMUtils.createElement("div", {
4255
+ className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
4256
+ attributes: {
4257
+ "data-direction": direction,
4258
+ "data-vidply-managed-resize": "true",
4259
+ "aria-hidden": "true"
4260
+ }
4261
+ });
4262
+ handle.style.display = "none";
4263
+ this.transcriptWindow.appendChild(handle);
4264
+ return handle;
4265
+ });
4266
+ }
3532
4267
  /**
3533
4268
  * Position transcript window next to video
3534
4269
  */
3535
4270
  positionTranscript() {
3536
4271
  if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
4272
+ if (this.draggableResizable && this.draggableResizable.manuallyPositioned) {
4273
+ return;
4274
+ }
3537
4275
  const isMobile = window.innerWidth < 640;
3538
4276
  const videoRect = this.player.videoWrapper.getBoundingClientRect();
3539
4277
  const isFullscreen = this.player.state.fullscreen;
@@ -3566,8 +4304,12 @@ var VidPly = (() => {
3566
4304
  this.transcriptWindow.style.top = "auto";
3567
4305
  this.transcriptWindow.style.maxHeight = "calc(100vh - 180px)";
3568
4306
  this.transcriptWindow.style.height = "auto";
3569
- this.transcriptWindow.style.width = "400px";
3570
- this.transcriptWindow.style.maxWidth = "400px";
4307
+ const fullscreenMinWidth = 260;
4308
+ const fullscreenAvailable = Math.max(fullscreenMinWidth, window.innerWidth - 40);
4309
+ const fullscreenDesired = parseFloat(this.transcriptWindow.style.width) || 400;
4310
+ const fullscreenWidth = Math.max(fullscreenMinWidth, Math.min(fullscreenDesired, fullscreenAvailable));
4311
+ this.transcriptWindow.style.width = `${fullscreenWidth}px`;
4312
+ this.transcriptWindow.style.maxWidth = "none";
3571
4313
  this.transcriptWindow.style.borderRadius = "8px";
3572
4314
  this.transcriptWindow.style.border = "1px solid var(--vidply-border)";
3573
4315
  this.transcriptWindow.style.borderTop = "";
@@ -3575,15 +4317,30 @@ var VidPly = (() => {
3575
4317
  this.player.container.appendChild(this.transcriptWindow);
3576
4318
  }
3577
4319
  } else {
4320
+ const transcriptWidth = parseFloat(this.transcriptWindow.style.width) || 400;
4321
+ const padding = 20;
4322
+ const minWidth = 260;
4323
+ const containerRect = this.player.container.getBoundingClientRect();
4324
+ const ensureContainerPositioned = () => {
4325
+ const computed = window.getComputedStyle(this.player.container);
4326
+ if (computed.position === "static") {
4327
+ this.player.container.style.position = "relative";
4328
+ }
4329
+ };
4330
+ ensureContainerPositioned();
4331
+ const left = videoRect.right - containerRect.left + padding;
4332
+ const availableWidth = window.innerWidth - videoRect.right - padding;
4333
+ const appliedWidth = Math.max(minWidth, Math.min(transcriptWidth, availableWidth));
4334
+ const appliedHeight = videoRect.height;
3578
4335
  this.transcriptWindow.style.position = "absolute";
3579
- this.transcriptWindow.style.left = `${videoRect.width + 8}px`;
4336
+ this.transcriptWindow.style.left = `${left}px`;
3580
4337
  this.transcriptWindow.style.right = "auto";
3581
4338
  this.transcriptWindow.style.bottom = "auto";
3582
4339
  this.transcriptWindow.style.top = "0";
3583
- this.transcriptWindow.style.height = `${videoRect.height}px`;
4340
+ this.transcriptWindow.style.height = `${appliedHeight}px`;
3584
4341
  this.transcriptWindow.style.maxHeight = "none";
3585
- this.transcriptWindow.style.width = "400px";
3586
- this.transcriptWindow.style.maxWidth = "400px";
4342
+ this.transcriptWindow.style.width = `${appliedWidth}px`;
4343
+ this.transcriptWindow.style.maxWidth = "none";
3587
4344
  this.transcriptWindow.style.borderRadius = "8px";
3588
4345
  this.transcriptWindow.style.border = "1px solid var(--vidply-border)";
3589
4346
  this.transcriptWindow.style.borderTop = "";
@@ -3595,17 +4352,94 @@ var VidPly = (() => {
3595
4352
  }
3596
4353
  }
3597
4354
  }
4355
+ /**
4356
+ * Get available transcript languages from tracks
4357
+ */
4358
+ getAvailableTranscriptLanguages() {
4359
+ const textTracks = this.player.textTracks;
4360
+ const languages = /* @__PURE__ */ new Map();
4361
+ textTracks.forEach((track) => {
4362
+ if ((track.kind === "captions" || track.kind === "subtitles") && track.language) {
4363
+ if (!languages.has(track.language)) {
4364
+ languages.set(track.language, {
4365
+ language: track.language,
4366
+ label: track.label || track.language,
4367
+ track
4368
+ });
4369
+ }
4370
+ }
4371
+ });
4372
+ return Array.from(languages.values());
4373
+ }
4374
+ /**
4375
+ * Update language selector dropdown
4376
+ */
4377
+ updateLanguageSelector() {
4378
+ if (!this.languageSelector) return;
4379
+ this.availableTranscriptLanguages = this.getAvailableTranscriptLanguages();
4380
+ this.languageSelector.innerHTML = "";
4381
+ if (this.availableTranscriptLanguages.length < 2) {
4382
+ this.languageSelector.style.display = "none";
4383
+ return;
4384
+ }
4385
+ this.languageSelector.style.display = "block";
4386
+ this.availableTranscriptLanguages.forEach((langInfo, index) => {
4387
+ const option = DOMUtils.createElement("option", {
4388
+ textContent: langInfo.label,
4389
+ attributes: {
4390
+ "value": langInfo.language
4391
+ }
4392
+ });
4393
+ this.languageSelector.appendChild(option);
4394
+ });
4395
+ if (this.currentTranscriptLanguage) {
4396
+ this.languageSelector.value = this.currentTranscriptLanguage;
4397
+ } else if (this.availableTranscriptLanguages.length > 0) {
4398
+ const activeTrack = this.player.textTracks.find(
4399
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.mode === "showing"
4400
+ );
4401
+ this.currentTranscriptLanguage = activeTrack ? activeTrack.language : this.availableTranscriptLanguages[0].language;
4402
+ this.languageSelector.value = this.currentTranscriptLanguage;
4403
+ }
4404
+ if (this.languageSelectorHandler) {
4405
+ this.languageSelector.removeEventListener("change", this.languageSelectorHandler);
4406
+ }
4407
+ this.languageSelectorHandler = (e) => {
4408
+ this.currentTranscriptLanguage = e.target.value;
4409
+ this.loadTranscriptData();
4410
+ };
4411
+ this.languageSelector.addEventListener("change", this.languageSelectorHandler);
4412
+ }
3598
4413
  /**
3599
4414
  * Load transcript data from caption/subtitle tracks
3600
4415
  */
3601
4416
  loadTranscriptData() {
3602
4417
  this.transcriptEntries = [];
3603
4418
  this.transcriptContent.innerHTML = "";
3604
- const textTracks = Array.from(this.player.element.textTracks);
3605
- const captionTrack = textTracks.find(
3606
- (track) => track.kind === "captions" || track.kind === "subtitles"
3607
- );
3608
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
4419
+ const textTracks = this.player.textTracks;
4420
+ let captionTrack = null;
4421
+ if (this.currentTranscriptLanguage) {
4422
+ captionTrack = textTracks.find(
4423
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.language === this.currentTranscriptLanguage
4424
+ );
4425
+ }
4426
+ if (!captionTrack) {
4427
+ captionTrack = textTracks.find(
4428
+ (track) => track.kind === "captions" || track.kind === "subtitles"
4429
+ );
4430
+ if (captionTrack) {
4431
+ this.currentTranscriptLanguage = captionTrack.language;
4432
+ }
4433
+ }
4434
+ let descriptionTrack = null;
4435
+ if (this.currentTranscriptLanguage) {
4436
+ descriptionTrack = textTracks.find(
4437
+ (track) => track.kind === "descriptions" && track.language === this.currentTranscriptLanguage
4438
+ );
4439
+ }
4440
+ if (!descriptionTrack) {
4441
+ descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
4442
+ }
3609
4443
  const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3610
4444
  if (!captionTrack && !descriptionTrack && !metadataTrack) {
3611
4445
  this.showNoTranscriptMessage();
@@ -3634,7 +4468,7 @@ var VidPly = (() => {
3634
4468
  tracksToLoad.forEach((track) => {
3635
4469
  track.addEventListener("load", onLoad, { once: true });
3636
4470
  });
3637
- setTimeout(() => {
4471
+ this.setManagedTimeout(() => {
3638
4472
  this.loadTranscriptData();
3639
4473
  }, 500);
3640
4474
  return;
@@ -3667,24 +4501,61 @@ var VidPly = (() => {
3667
4501
  this.transcriptContent.appendChild(entry);
3668
4502
  });
3669
4503
  this.applyTranscriptStyles();
4504
+ this.updateLanguageSelector();
4505
+ }
4506
+ /**
4507
+ * Setup metadata handling on player load
4508
+ * This runs independently of transcript loading
4509
+ */
4510
+ setupMetadataHandlingOnLoad() {
4511
+ const setupMetadata = () => {
4512
+ const textTracks = this.player.textTracks;
4513
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
4514
+ if (metadataTrack) {
4515
+ if (metadataTrack.mode === "disabled") {
4516
+ metadataTrack.mode = "hidden";
4517
+ }
4518
+ if (this.metadataCueChangeHandler) {
4519
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
4520
+ }
4521
+ this.metadataCueChangeHandler = () => {
4522
+ const activeCues = Array.from(metadataTrack.activeCues || []);
4523
+ if (activeCues.length > 0) {
4524
+ if (this.player.options.debug) {
4525
+ console.log("[VidPly Metadata] Active cues:", activeCues.map((c) => ({
4526
+ start: c.startTime,
4527
+ end: c.endTime,
4528
+ text: c.text
4529
+ })));
4530
+ }
4531
+ }
4532
+ activeCues.forEach((cue) => {
4533
+ this.handleMetadataCue(cue);
4534
+ });
4535
+ };
4536
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
4537
+ if (this.player.options.debug) {
4538
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
4539
+ console.log("[VidPly Metadata] Track enabled,", cueCount, "cues available");
4540
+ }
4541
+ } else if (this.player.options.debug) {
4542
+ console.warn("[VidPly Metadata] No metadata track found");
4543
+ }
4544
+ };
4545
+ setupMetadata();
4546
+ this.player.on("loadedmetadata", setupMetadata);
3670
4547
  }
3671
4548
  /**
3672
4549
  * Setup metadata handling
3673
4550
  * Metadata cues are not displayed but can be used programmatically
4551
+ * This is called when transcript data is loaded (for storing cues)
3674
4552
  */
3675
4553
  setupMetadataHandling() {
3676
4554
  if (!this.metadataCues || this.metadataCues.length === 0) {
3677
4555
  return;
3678
4556
  }
3679
- const textTracks = Array.from(this.player.element.textTracks);
3680
- const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3681
- if (metadataTrack) {
3682
- metadataTrack.addEventListener("cuechange", () => {
3683
- const activeCues = Array.from(metadataTrack.activeCues || []);
3684
- activeCues.forEach((cue) => {
3685
- this.handleMetadataCue(cue);
3686
- });
3687
- });
4557
+ if (this.player.options.debug) {
4558
+ console.log("[VidPly Metadata]", this.metadataCues.length, "cues stored from transcript load");
3688
4559
  }
3689
4560
  }
3690
4561
  /**
@@ -3693,6 +4564,12 @@ var VidPly = (() => {
3693
4564
  */
3694
4565
  handleMetadataCue(cue) {
3695
4566
  const text = cue.text.trim();
4567
+ if (this.player.options.debug) {
4568
+ console.log("[VidPly Metadata] Processing cue:", {
4569
+ time: cue.startTime,
4570
+ text
4571
+ });
4572
+ }
3696
4573
  this.player.emit("metadata", {
3697
4574
  time: cue.startTime,
3698
4575
  endTime: cue.endTime,
@@ -3700,18 +4577,40 @@ var VidPly = (() => {
3700
4577
  cue
3701
4578
  });
3702
4579
  if (text.includes("PAUSE")) {
4580
+ if (!this.player.state.paused) {
4581
+ if (this.player.options.debug) {
4582
+ console.log("[VidPly Metadata] Pausing video at", cue.startTime);
4583
+ }
4584
+ this.player.pause();
4585
+ }
3703
4586
  this.player.emit("metadata:pause", { time: cue.startTime, text });
3704
4587
  }
3705
4588
  const focusMatch = text.match(/FOCUS:([\w#-]+)/);
3706
4589
  if (focusMatch) {
4590
+ const targetSelector = focusMatch[1];
4591
+ const targetElement = document.querySelector(targetSelector);
4592
+ if (targetElement) {
4593
+ if (this.player.options.debug) {
4594
+ console.log("[VidPly Metadata] Focusing element:", targetSelector);
4595
+ }
4596
+ this.setManagedTimeout(() => {
4597
+ targetElement.focus();
4598
+ }, 10);
4599
+ } else if (this.player.options.debug) {
4600
+ console.warn("[VidPly Metadata] Element not found:", targetSelector);
4601
+ }
3707
4602
  this.player.emit("metadata:focus", {
3708
4603
  time: cue.startTime,
3709
- target: focusMatch[1],
4604
+ target: targetSelector,
4605
+ element: targetElement,
3710
4606
  text
3711
4607
  });
3712
4608
  }
3713
4609
  const hashtags = text.match(/#[\w-]+/g);
3714
4610
  if (hashtags) {
4611
+ if (this.player.options.debug) {
4612
+ console.log("[VidPly Metadata] Hashtags found:", hashtags);
4613
+ }
3715
4614
  this.player.emit("metadata:hashtags", {
3716
4615
  time: cue.startTime,
3717
4616
  hashtags,
@@ -3805,7 +4704,7 @@ var VidPly = (() => {
3805
4704
  * Scroll transcript window to show active entry
3806
4705
  */
3807
4706
  scrollToEntry(entryElement) {
3808
- if (!this.transcriptContent) return;
4707
+ if (!this.transcriptContent || !this.autoscrollEnabled) return;
3809
4708
  const contentRect = this.transcriptContent.getBoundingClientRect();
3810
4709
  const entryRect = entryElement.getBoundingClientRect();
3811
4710
  if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
@@ -3816,256 +4715,121 @@ var VidPly = (() => {
3816
4715
  });
3817
4716
  }
3818
4717
  }
4718
+ /**
4719
+ * Save autoscroll preference to localStorage
4720
+ */
4721
+ saveAutoscrollPreference() {
4722
+ const savedPreferences = this.storage.getTranscriptPreferences() || {};
4723
+ savedPreferences.autoscroll = this.autoscrollEnabled;
4724
+ this.storage.saveTranscriptPreferences(savedPreferences);
4725
+ }
3819
4726
  /**
3820
4727
  * Setup drag and drop functionality
3821
4728
  */
3822
4729
  setupDragAndDrop() {
3823
4730
  if (!this.transcriptHeader || !this.transcriptWindow) return;
3824
- this.handlers.mousedown = (e) => {
3825
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
3826
- return;
3827
- }
3828
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3829
- return;
3830
- }
3831
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3832
- return;
3833
- }
3834
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
3835
- return;
3836
- }
3837
- this.startDragging(e.clientX, e.clientY);
3838
- e.preventDefault();
3839
- };
3840
- this.handlers.mousemove = (e) => {
3841
- if (this.isDragging) {
3842
- this.drag(e.clientX, e.clientY);
3843
- }
3844
- };
3845
- this.handlers.mouseup = () => {
3846
- if (this.isDragging) {
3847
- this.stopDragging();
3848
- }
3849
- };
3850
- this.handlers.touchstart = (e) => {
3851
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
3852
- return;
3853
- }
3854
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3855
- return;
3856
- }
3857
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3858
- return;
3859
- }
3860
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
3861
- return;
3862
- }
3863
- const isMobile = window.innerWidth < 640;
3864
- const isFullscreen = this.player.state.fullscreen;
3865
- const touch = e.touches[0];
3866
- if (isMobile && !isFullscreen) {
3867
- return;
3868
- } else {
3869
- this.startDragging(touch.clientX, touch.clientY);
3870
- }
3871
- };
3872
- this.handlers.touchmove = (e) => {
3873
- const isMobile = window.innerWidth < 640;
3874
- const isFullscreen = this.player.state.fullscreen;
3875
- if (isMobile && !isFullscreen) {
3876
- return;
3877
- } else if (this.isDragging) {
3878
- const touch = e.touches[0];
3879
- this.drag(touch.clientX, touch.clientY);
3880
- e.preventDefault();
3881
- }
3882
- };
3883
- this.handlers.touchend = () => {
3884
- if (this.isDragging) {
3885
- this.stopDragging();
3886
- }
3887
- };
3888
- this.handlers.keydown = (e) => {
3889
- if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
3890
- if (!this.keyboardDragMode) {
3891
- return;
4731
+ const isMobile = window.innerWidth < 640;
4732
+ const isFullscreen = this.player.state.fullscreen;
4733
+ if (isMobile && !isFullscreen) {
4734
+ return;
4735
+ }
4736
+ this.draggableResizable = new DraggableResizable(this.transcriptWindow, {
4737
+ dragHandle: this.transcriptHeader,
4738
+ resizeHandles: this.transcriptResizeHandles,
4739
+ constrainToViewport: true,
4740
+ classPrefix: `${this.player.options.classPrefix}-transcript`,
4741
+ keyboardDragKey: "d",
4742
+ keyboardResizeKey: "r",
4743
+ keyboardStep: 10,
4744
+ keyboardStepLarge: 50,
4745
+ minWidth: 300,
4746
+ minHeight: 200,
4747
+ maxWidth: () => Math.max(320, window.innerWidth - 40),
4748
+ maxHeight: () => Math.max(200, window.innerHeight - 120),
4749
+ pointerResizeIndicatorText: i18n.t("transcript.resizeModeHint"),
4750
+ onPointerResizeToggle: (enabled) => this.onPointerResizeModeChange(enabled),
4751
+ onDragStart: (e) => {
4752
+ const ignoreSelectors = [
4753
+ `.${this.player.options.classPrefix}-transcript-close`,
4754
+ `.${this.player.options.classPrefix}-transcript-settings`,
4755
+ `.${this.player.options.classPrefix}-transcript-language-select`,
4756
+ `.${this.player.options.classPrefix}-transcript-settings-menu`,
4757
+ `.${this.player.options.classPrefix}-transcript-style-dialog`
4758
+ ];
4759
+ for (const selector of ignoreSelectors) {
4760
+ if (e.target.closest(selector)) {
4761
+ return false;
4762
+ }
3892
4763
  }
4764
+ return true;
4765
+ }
4766
+ });
4767
+ this.customKeyHandler = (e) => {
4768
+ const key = e.key.toLowerCase();
4769
+ const alreadyPrevented = e.defaultPrevented;
4770
+ if (key === "home") {
3893
4771
  e.preventDefault();
3894
4772
  e.stopPropagation();
3895
- const step = e.shiftKey ? 50 : 10;
3896
- let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
3897
- let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
3898
- const computedStyle = window.getComputedStyle(this.transcriptWindow);
3899
- if (computedStyle.transform !== "none") {
3900
- const rect = this.transcriptWindow.getBoundingClientRect();
3901
- currentLeft = rect.left;
3902
- currentTop = rect.top;
3903
- this.transcriptWindow.style.transform = "none";
3904
- this.transcriptWindow.style.left = `${currentLeft}px`;
3905
- this.transcriptWindow.style.top = `${currentTop}px`;
3906
- }
3907
- let newX = currentLeft;
3908
- let newY = currentTop;
3909
- switch (e.key) {
3910
- case "ArrowLeft":
3911
- newX -= step;
3912
- break;
3913
- case "ArrowRight":
3914
- newX += step;
3915
- break;
3916
- case "ArrowUp":
3917
- newY -= step;
3918
- break;
3919
- case "ArrowDown":
3920
- newY += step;
3921
- break;
4773
+ if (this.draggableResizable) {
4774
+ if (this.draggableResizable.pointerResizeMode) {
4775
+ this.draggableResizable.disablePointerResizeMode();
4776
+ }
4777
+ this.draggableResizable.manuallyPositioned = false;
4778
+ this.positionTranscript();
4779
+ this.updateResizeOptionState();
4780
+ this.announceLive(i18n.t("transcript.positionReset"));
3922
4781
  }
3923
- this.transcriptWindow.style.left = `${newX}px`;
3924
- this.transcriptWindow.style.top = `${newY}px`;
3925
4782
  return;
3926
4783
  }
3927
- if (e.key === "Home") {
4784
+ if (key === "r") {
4785
+ if (alreadyPrevented) {
4786
+ return;
4787
+ }
3928
4788
  e.preventDefault();
3929
4789
  e.stopPropagation();
3930
- this.resetPosition();
4790
+ const enabled = this.toggleResizeMode();
4791
+ if (enabled) {
4792
+ this.transcriptWindow.focus();
4793
+ }
3931
4794
  return;
3932
4795
  }
3933
- if (e.key === "Escape") {
4796
+ if (key === "escape") {
3934
4797
  e.preventDefault();
3935
4798
  e.stopPropagation();
4799
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
4800
+ this.draggableResizable.disablePointerResizeMode();
4801
+ return;
4802
+ }
3936
4803
  if (this.styleDialogVisible) {
3937
4804
  this.hideStyleDialog();
3938
- } else if (this.keyboardDragMode) {
3939
- this.disableKeyboardDragMode();
4805
+ } else if (this.draggableResizable && this.draggableResizable.keyboardDragMode) {
4806
+ this.draggableResizable.disableKeyboardDragMode();
4807
+ this.announceLive(i18n.t("transcript.dragModeDisabled"));
3940
4808
  } else if (this.settingsMenuVisible) {
3941
4809
  this.hideSettingsMenu();
3942
4810
  } else {
3943
- this.hideTranscript();
4811
+ this.hideTranscript({ focusButton: true });
3944
4812
  }
3945
4813
  return;
3946
4814
  }
3947
4815
  };
3948
- this.transcriptHeader.addEventListener("mousedown", this.handlers.mousedown);
3949
- document.addEventListener("mousemove", this.handlers.mousemove);
3950
- document.addEventListener("mouseup", this.handlers.mouseup);
3951
- this.transcriptHeader.addEventListener("touchstart", this.handlers.touchstart);
3952
- document.addEventListener("touchmove", this.handlers.touchmove);
3953
- document.addEventListener("touchend", this.handlers.touchend);
3954
- this.transcriptHeader.addEventListener("keydown", this.handlers.keydown);
3955
- }
3956
- /**
3957
- * Start dragging
3958
- */
3959
- startDragging(clientX, clientY) {
3960
- const rect = this.transcriptWindow.getBoundingClientRect();
3961
- const containerRect = this.player.container.getBoundingClientRect();
3962
- const relativeLeft = rect.left - containerRect.left;
3963
- const relativeTop = rect.top - containerRect.top;
3964
- const computedStyle = window.getComputedStyle(this.transcriptWindow);
3965
- if (computedStyle.transform !== "none") {
3966
- this.transcriptWindow.style.transform = "none";
3967
- this.transcriptWindow.style.left = `${relativeLeft}px`;
3968
- this.transcriptWindow.style.top = `${relativeTop}px`;
3969
- }
3970
- this.dragOffsetX = clientX - rect.left;
3971
- this.dragOffsetY = clientY - rect.top;
3972
- this.isDragging = true;
3973
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-dragging`);
3974
- document.body.style.cursor = "grabbing";
3975
- document.body.style.userSelect = "none";
3976
- }
3977
- /**
3978
- * Perform drag
3979
- */
3980
- drag(clientX, clientY) {
3981
- if (!this.isDragging) return;
3982
- const newViewportX = clientX - this.dragOffsetX;
3983
- const newViewportY = clientY - this.dragOffsetY;
3984
- const containerRect = this.player.container.getBoundingClientRect();
3985
- const newX = newViewportX - containerRect.left;
3986
- const newY = newViewportY - containerRect.top;
3987
- this.transcriptWindow.style.left = `${newX}px`;
3988
- this.transcriptWindow.style.top = `${newY}px`;
3989
- }
3990
- /**
3991
- * Stop dragging
3992
- */
3993
- stopDragging() {
3994
- this.isDragging = false;
3995
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-dragging`);
3996
- document.body.style.cursor = "";
3997
- document.body.style.userSelect = "";
3998
- }
3999
- /**
4000
- * Set window position with boundary constraints
4001
- */
4002
- setPosition(x, y) {
4003
- const rect = this.transcriptWindow.getBoundingClientRect();
4004
- const viewportWidth = document.documentElement.clientWidth;
4005
- const viewportHeight = document.documentElement.clientHeight;
4006
- const minVisible = 100;
4007
- const minX = -(rect.width - minVisible);
4008
- const minY = -(rect.height - minVisible);
4009
- const maxX = viewportWidth - minVisible;
4010
- const maxY = viewportHeight - minVisible;
4011
- x = Math.max(minX, Math.min(x, maxX));
4012
- y = Math.max(minY, Math.min(y, maxY));
4013
- this.transcriptWindow.style.left = `${x}px`;
4014
- this.transcriptWindow.style.top = `${y}px`;
4015
- this.transcriptWindow.style.transform = "none";
4016
- }
4017
- /**
4018
- * Reset position to center
4019
- */
4020
- resetPosition() {
4021
- this.transcriptWindow.style.left = "50%";
4022
- this.transcriptWindow.style.top = "50%";
4023
- this.transcriptWindow.style.transform = "translate(-50%, -50%)";
4816
+ this.transcriptWindow.addEventListener("keydown", this.customKeyHandler);
4024
4817
  }
4025
4818
  /**
4026
4819
  * Toggle keyboard drag mode
4027
4820
  */
4028
4821
  toggleKeyboardDragMode() {
4029
- if (this.keyboardDragMode) {
4030
- this.disableKeyboardDragMode();
4031
- } else {
4032
- this.enableKeyboardDragMode();
4033
- }
4034
- }
4035
- /**
4036
- * Enable keyboard drag mode
4037
- */
4038
- enableKeyboardDragMode() {
4039
- this.keyboardDragMode = true;
4040
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
4041
- if (this.settingsButton) {
4042
- this.settingsButton.setAttribute("aria-label", "Keyboard drag mode active. Use arrow keys to move window. Press D or Escape to exit.");
4043
- }
4044
- const indicator = DOMUtils.createElement("div", {
4045
- className: `${this.player.options.classPrefix}-transcript-drag-indicator`,
4046
- textContent: i18n.t("transcript.keyboardDragActive")
4047
- });
4048
- this.transcriptHeader.appendChild(indicator);
4049
- if (this.settingsMenuVisible) {
4050
- this.hideSettingsMenu();
4051
- }
4052
- this.transcriptHeader.focus();
4053
- }
4054
- /**
4055
- * Disable keyboard drag mode
4056
- */
4057
- disableKeyboardDragMode() {
4058
- this.keyboardDragMode = false;
4059
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
4060
- if (this.settingsButton) {
4061
- this.settingsButton.setAttribute("aria-label", "Transcript settings. Press Enter to open menu, or D to enable drag mode");
4062
- }
4063
- const indicator = this.transcriptHeader.querySelector(`.${this.player.options.classPrefix}-transcript-drag-indicator`);
4064
- if (indicator) {
4065
- indicator.remove();
4066
- }
4067
- if (this.settingsButton) {
4068
- this.settingsButton.focus();
4822
+ if (this.draggableResizable) {
4823
+ const wasEnabled = this.draggableResizable.keyboardDragMode;
4824
+ this.draggableResizable.toggleKeyboardDragMode();
4825
+ const isEnabled = this.draggableResizable.keyboardDragMode;
4826
+ if (!wasEnabled && isEnabled) {
4827
+ this.enableMoveMode();
4828
+ }
4829
+ if (this.settingsMenuVisible) {
4830
+ this.hideSettingsMenu();
4831
+ }
4832
+ this.transcriptWindow.focus();
4069
4833
  }
4070
4834
  }
4071
4835
  /**
@@ -4095,6 +4859,16 @@ var VidPly = (() => {
4095
4859
  if (this.settingsMenu) {
4096
4860
  this.settingsMenu.style.display = "block";
4097
4861
  this.settingsMenuVisible = true;
4862
+ if (this.settingsButton) {
4863
+ this.settingsButton.setAttribute("aria-expanded", "true");
4864
+ }
4865
+ this.updateResizeOptionState();
4866
+ setTimeout(() => {
4867
+ const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
4868
+ if (firstItem) {
4869
+ firstItem.focus();
4870
+ }
4871
+ }, 0);
4098
4872
  return;
4099
4873
  }
4100
4874
  this.settingsMenu = DOMUtils.createElement("div", {
@@ -4142,19 +4916,35 @@ var VidPly = (() => {
4142
4916
  className: `${this.player.options.classPrefix}-transcript-settings-item`,
4143
4917
  attributes: {
4144
4918
  "type": "button",
4145
- "aria-label": i18n.t("transcript.resizeWindow")
4919
+ "aria-label": i18n.t("transcript.resizeWindow"),
4920
+ "aria-pressed": "false"
4146
4921
  }
4147
4922
  });
4148
4923
  const resizeIcon = createIconElement("resize");
4149
4924
  const resizeText = DOMUtils.createElement("span", {
4925
+ className: `${this.player.options.classPrefix}-transcript-settings-text`,
4150
4926
  textContent: i18n.t("transcript.resizeWindow")
4151
4927
  });
4152
4928
  resizeOption.appendChild(resizeIcon);
4153
4929
  resizeOption.appendChild(resizeText);
4154
- resizeOption.addEventListener("click", () => {
4155
- this.toggleResizeMode();
4156
- this.hideSettingsMenu();
4930
+ resizeOption.addEventListener("click", (event) => {
4931
+ event.preventDefault();
4932
+ event.stopPropagation();
4933
+ const enabled = this.toggleResizeMode({ focus: false });
4934
+ if (enabled) {
4935
+ this.hideSettingsMenu({ focusButton: false });
4936
+ this.setManagedTimeout(() => {
4937
+ if (this.transcriptWindow) {
4938
+ this.transcriptWindow.focus();
4939
+ }
4940
+ }, 20);
4941
+ } else {
4942
+ this.hideSettingsMenu({ focusButton: true });
4943
+ }
4157
4944
  });
4945
+ this.resizeOptionButton = resizeOption;
4946
+ this.resizeOptionText = resizeText;
4947
+ this.updateResizeOptionState();
4158
4948
  const closeOption = DOMUtils.createElement("button", {
4159
4949
  className: `${this.player.options.classPrefix}-transcript-settings-item`,
4160
4950
  attributes: {
@@ -4185,6 +4975,7 @@ var VidPly = (() => {
4185
4975
  if (this.settingsButton) {
4186
4976
  this.settingsButton.setAttribute("aria-expanded", "true");
4187
4977
  }
4978
+ this.updateResizeOptionState();
4188
4979
  setTimeout(() => {
4189
4980
  const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
4190
4981
  if (firstItem) {
@@ -4195,14 +4986,16 @@ var VidPly = (() => {
4195
4986
  /**
4196
4987
  * Hide settings menu
4197
4988
  */
4198
- hideSettingsMenu() {
4989
+ hideSettingsMenu({ focusButton = true } = {}) {
4199
4990
  if (this.settingsMenu) {
4200
4991
  this.settingsMenu.style.display = "none";
4201
4992
  this.settingsMenuVisible = false;
4202
4993
  this.settingsMenuJustOpened = false;
4203
4994
  if (this.settingsButton) {
4204
4995
  this.settingsButton.setAttribute("aria-expanded", "false");
4205
- this.settingsButton.focus();
4996
+ if (focusButton) {
4997
+ this.settingsButton.focus();
4998
+ }
4206
4999
  }
4207
5000
  }
4208
5001
  }
@@ -4210,6 +5003,7 @@ var VidPly = (() => {
4210
5003
  * Enable move mode (gives visual feedback)
4211
5004
  */
4212
5005
  enableMoveMode() {
5006
+ this.hideResizeModeIndicator();
4213
5007
  this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
4214
5008
  const tooltip = DOMUtils.createElement("div", {
4215
5009
  className: `${this.player.options.classPrefix}-transcript-move-tooltip`,
@@ -4226,155 +5020,64 @@ var VidPly = (() => {
4226
5020
  /**
4227
5021
  * Toggle resize mode
4228
5022
  */
4229
- toggleResizeMode() {
4230
- this.resizeEnabled = !this.resizeEnabled;
4231
- if (this.resizeEnabled) {
4232
- this.enableResizeHandles();
4233
- } else {
4234
- this.disableResizeHandles();
5023
+ toggleResizeMode({ focus = true } = {}) {
5024
+ if (!this.draggableResizable) {
5025
+ return false;
4235
5026
  }
4236
- }
4237
- /**
4238
- * Enable resize handles
4239
- */
4240
- enableResizeHandles() {
4241
- if (!this.transcriptWindow) return;
4242
- const directions = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
4243
- directions.forEach((direction) => {
4244
- const handle = DOMUtils.createElement("div", {
4245
- className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
4246
- attributes: {
4247
- "data-direction": direction
4248
- }
4249
- });
4250
- handle.addEventListener("mousedown", (e) => this.startResize(e, direction));
4251
- handle.addEventListener("touchstart", (e) => this.startResize(e.touches[0], direction));
4252
- this.transcriptWindow.appendChild(handle);
4253
- });
4254
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizable`);
4255
- this.handlers.resizeMove = (e) => {
4256
- if (this.isResizing) {
4257
- this.performResize(e.clientX, e.clientY);
4258
- }
4259
- };
4260
- this.handlers.resizeEnd = () => {
4261
- if (this.isResizing) {
4262
- this.stopResize();
4263
- }
4264
- };
4265
- this.handlers.resizeTouchMove = (e) => {
4266
- if (this.isResizing) {
4267
- this.performResize(e.touches[0].clientX, e.touches[0].clientY);
4268
- e.preventDefault();
4269
- }
4270
- };
4271
- document.addEventListener("mousemove", this.handlers.resizeMove);
4272
- document.addEventListener("mouseup", this.handlers.resizeEnd);
4273
- document.addEventListener("touchmove", this.handlers.resizeTouchMove);
4274
- document.addEventListener("touchend", this.handlers.resizeEnd);
4275
- }
4276
- /**
4277
- * Disable resize handles
4278
- */
4279
- disableResizeHandles() {
4280
- if (!this.transcriptWindow) return;
4281
- const handles = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-resize-handle`);
4282
- handles.forEach((handle) => handle.remove());
4283
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizable`);
4284
- if (this.handlers.resizeMove) {
4285
- document.removeEventListener("mousemove", this.handlers.resizeMove);
5027
+ if (this.draggableResizable.pointerResizeMode) {
5028
+ this.draggableResizable.disablePointerResizeMode({ focus });
5029
+ return false;
4286
5030
  }
4287
- if (this.handlers.resizeEnd) {
4288
- document.removeEventListener("mouseup", this.handlers.resizeEnd);
5031
+ this.draggableResizable.enablePointerResizeMode({ focus });
5032
+ return true;
5033
+ }
5034
+ updateResizeOptionState() {
5035
+ if (!this.resizeOptionButton) {
5036
+ return;
4289
5037
  }
4290
- if (this.handlers.resizeTouchMove) {
4291
- document.removeEventListener("touchmove", this.handlers.resizeTouchMove);
5038
+ const isEnabled = !!(this.draggableResizable && this.draggableResizable.pointerResizeMode);
5039
+ const label = isEnabled ? i18n.t("transcript.disableResizeWindow") || "Disable Resize Mode" : i18n.t("transcript.resizeWindow");
5040
+ this.resizeOptionButton.setAttribute("aria-pressed", isEnabled ? "true" : "false");
5041
+ this.resizeOptionButton.setAttribute("aria-label", label);
5042
+ this.resizeOptionButton.setAttribute("title", label);
5043
+ if (this.resizeOptionText) {
5044
+ this.resizeOptionText.textContent = label;
4292
5045
  }
4293
- document.removeEventListener("touchend", this.handlers.resizeEnd);
4294
- }
4295
- /**
4296
- * Start resizing
4297
- */
4298
- startResize(e, direction) {
4299
- e.stopPropagation();
4300
- e.preventDefault();
4301
- this.isResizing = true;
4302
- this.resizeDirection = direction;
4303
- this.resizeStartX = e.clientX;
4304
- this.resizeStartY = e.clientY;
4305
- const rect = this.transcriptWindow.getBoundingClientRect();
4306
- this.resizeStartWidth = rect.width;
4307
- this.resizeStartHeight = rect.height;
4308
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizing`);
4309
- document.body.style.cursor = this.getResizeCursor(direction);
4310
- document.body.style.userSelect = "none";
4311
5046
  }
4312
- /**
4313
- * Perform resize
4314
- */
4315
- performResize(clientX, clientY) {
4316
- if (!this.isResizing) return;
4317
- const deltaX = clientX - this.resizeStartX;
4318
- const deltaY = clientY - this.resizeStartY;
4319
- let newWidth = this.resizeStartWidth;
4320
- let newHeight = this.resizeStartHeight;
4321
- const direction = this.resizeDirection;
4322
- if (direction.includes("e")) {
4323
- newWidth = this.resizeStartWidth + deltaX;
4324
- }
4325
- if (direction.includes("w")) {
4326
- newWidth = this.resizeStartWidth - deltaX;
4327
- }
4328
- if (direction.includes("s")) {
4329
- newHeight = this.resizeStartHeight + deltaY;
4330
- }
4331
- if (direction.includes("n")) {
4332
- newHeight = this.resizeStartHeight - deltaY;
4333
- }
4334
- const minWidth = 300;
4335
- const minHeight = 200;
4336
- const maxWidth = window.innerWidth - 40;
4337
- const maxHeight = window.innerHeight - 40;
4338
- newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
4339
- newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
4340
- this.transcriptWindow.style.width = `${newWidth}px`;
4341
- this.transcriptWindow.style.height = `${newHeight}px`;
4342
- this.transcriptWindow.style.maxWidth = `${newWidth}px`;
4343
- this.transcriptWindow.style.maxHeight = `${newHeight}px`;
4344
- if (direction.includes("w")) {
4345
- const currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
4346
- this.transcriptWindow.style.left = `${currentLeft + (this.resizeStartWidth - newWidth)}px`;
4347
- }
4348
- if (direction.includes("n")) {
4349
- const currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
4350
- this.transcriptWindow.style.top = `${currentTop + (this.resizeStartHeight - newHeight)}px`;
5047
+ showResizeModeIndicator() {
5048
+ if (!this.transcriptHeader) {
5049
+ return;
5050
+ }
5051
+ this.hideResizeModeIndicator();
5052
+ const indicator = DOMUtils.createElement("div", {
5053
+ className: `${this.player.options.classPrefix}-transcript-resize-tooltip`,
5054
+ textContent: i18n.t("transcript.resizeModeHint") || "Resize handles enabled. Drag edges or corners to adjust. Press Esc or R to exit."
5055
+ });
5056
+ this.transcriptHeader.appendChild(indicator);
5057
+ this.resizeModeIndicator = indicator;
5058
+ this.resizeModeIndicatorTimeout = this.setManagedTimeout(() => {
5059
+ this.hideResizeModeIndicator();
5060
+ }, 3e3);
5061
+ }
5062
+ hideResizeModeIndicator() {
5063
+ if (this.resizeModeIndicatorTimeout) {
5064
+ this.clearManagedTimeout(this.resizeModeIndicatorTimeout);
5065
+ this.resizeModeIndicatorTimeout = null;
5066
+ }
5067
+ if (this.resizeModeIndicator && this.resizeModeIndicator.parentNode) {
5068
+ this.resizeModeIndicator.remove();
5069
+ }
5070
+ this.resizeModeIndicator = null;
5071
+ }
5072
+ onPointerResizeModeChange(enabled) {
5073
+ this.updateResizeOptionState();
5074
+ if (enabled) {
5075
+ this.showResizeModeIndicator();
5076
+ this.announceLive(i18n.t("transcript.resizeModeEnabled"));
5077
+ } else {
5078
+ this.hideResizeModeIndicator();
5079
+ this.announceLive(i18n.t("transcript.resizeModeDisabled"));
4351
5080
  }
4352
- }
4353
- /**
4354
- * Stop resizing
4355
- */
4356
- stopResize() {
4357
- this.isResizing = false;
4358
- this.resizeDirection = null;
4359
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizing`);
4360
- document.body.style.cursor = "";
4361
- document.body.style.userSelect = "";
4362
- }
4363
- /**
4364
- * Get cursor style for resize direction
4365
- */
4366
- getResizeCursor(direction) {
4367
- const cursors = {
4368
- "n": "ns-resize",
4369
- "s": "ns-resize",
4370
- "e": "ew-resize",
4371
- "w": "ew-resize",
4372
- "ne": "nesw-resize",
4373
- "nw": "nwse-resize",
4374
- "se": "nwse-resize",
4375
- "sw": "nesw-resize"
4376
- };
4377
- return cursors[direction] || "default";
4378
5081
  }
4379
5082
  /**
4380
5083
  * Show style dialog
@@ -4608,30 +5311,50 @@ var VidPly = (() => {
4608
5311
  entry.style.fontFamily = this.transcriptStyle.fontFamily;
4609
5312
  });
4610
5313
  }
5314
+ /**
5315
+ * Set a managed timeout that will be cleaned up on destroy
5316
+ * @param {Function} callback - Callback function
5317
+ * @param {number} delay - Delay in milliseconds
5318
+ * @returns {number} Timeout ID
5319
+ */
5320
+ setManagedTimeout(callback, delay) {
5321
+ const timeoutId = setTimeout(() => {
5322
+ this.timeouts.delete(timeoutId);
5323
+ callback();
5324
+ }, delay);
5325
+ this.timeouts.add(timeoutId);
5326
+ return timeoutId;
5327
+ }
5328
+ /**
5329
+ * Clear a managed timeout
5330
+ * @param {number} timeoutId - Timeout ID to clear
5331
+ */
5332
+ clearManagedTimeout(timeoutId) {
5333
+ if (timeoutId) {
5334
+ clearTimeout(timeoutId);
5335
+ this.timeouts.delete(timeoutId);
5336
+ }
5337
+ }
4611
5338
  /**
4612
5339
  * Cleanup
4613
5340
  */
4614
5341
  destroy() {
4615
- if (this.resizeEnabled) {
4616
- this.disableResizeHandles();
5342
+ this.hideResizeModeIndicator();
5343
+ if (this.draggableResizable) {
5344
+ if (this.draggableResizable.pointerResizeMode) {
5345
+ this.draggableResizable.disablePointerResizeMode();
5346
+ this.updateResizeOptionState();
5347
+ }
5348
+ this.draggableResizable.destroy();
5349
+ this.draggableResizable = null;
4617
5350
  }
4618
- if (this.keyboardDragMode) {
4619
- this.disableKeyboardDragMode();
5351
+ if (this.transcriptWindow && this.customKeyHandler) {
5352
+ this.transcriptWindow.removeEventListener("keydown", this.customKeyHandler);
5353
+ this.customKeyHandler = null;
4620
5354
  }
4621
5355
  if (this.handlers.timeupdate) {
4622
5356
  this.player.off("timeupdate", this.handlers.timeupdate);
4623
5357
  }
4624
- if (this.transcriptHeader) {
4625
- if (this.handlers.mousedown) {
4626
- this.transcriptHeader.removeEventListener("mousedown", this.handlers.mousedown);
4627
- }
4628
- if (this.handlers.touchstart) {
4629
- this.transcriptHeader.removeEventListener("touchstart", this.handlers.touchstart);
4630
- }
4631
- if (this.handlers.keydown) {
4632
- this.transcriptHeader.removeEventListener("keydown", this.handlers.keydown);
4633
- }
4634
- }
4635
5358
  if (this.settingsButton) {
4636
5359
  if (this.handlers.settingsClick) {
4637
5360
  this.settingsButton.removeEventListener("click", this.handlers.settingsClick);
@@ -4643,24 +5366,14 @@ var VidPly = (() => {
4643
5366
  if (this.styleDialog && this.handlers.styleDialogKeydown) {
4644
5367
  this.styleDialog.removeEventListener("keydown", this.handlers.styleDialogKeydown);
4645
5368
  }
4646
- if (this.handlers.mousemove) {
4647
- document.removeEventListener("mousemove", this.handlers.mousemove);
4648
- }
4649
- if (this.handlers.mouseup) {
4650
- document.removeEventListener("mouseup", this.handlers.mouseup);
4651
- }
4652
- if (this.handlers.touchmove) {
4653
- document.removeEventListener("touchmove", this.handlers.touchmove);
4654
- }
4655
- if (this.handlers.touchend) {
4656
- document.removeEventListener("touchend", this.handlers.touchend);
4657
- }
4658
5369
  if (this.handlers.documentClick) {
4659
5370
  document.removeEventListener("click", this.handlers.documentClick);
4660
5371
  }
4661
5372
  if (this.handlers.resize) {
4662
5373
  window.removeEventListener("resize", this.handlers.resize);
4663
5374
  }
5375
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
5376
+ this.timeouts.clear();
4664
5377
  this.handlers = null;
4665
5378
  if (this.transcriptWindow && this.transcriptWindow.parentNode) {
4666
5379
  this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
@@ -4671,6 +5384,14 @@ var VidPly = (() => {
4671
5384
  this.transcriptEntries = [];
4672
5385
  this.settingsMenu = null;
4673
5386
  this.styleDialog = null;
5387
+ this.transcriptResizeHandles = [];
5388
+ this.resizeOptionButton = null;
5389
+ this.resizeOptionText = null;
5390
+ this.liveRegion = null;
5391
+ }
5392
+ announceLive(message) {
5393
+ if (!this.liveRegion) return;
5394
+ this.liveRegion.textContent = message || "";
4674
5395
  }
4675
5396
  };
4676
5397
 
@@ -5332,7 +6053,7 @@ var VidPly = (() => {
5332
6053
  };
5333
6054
 
5334
6055
  // src/core/Player.js
5335
- var Player = class extends EventEmitter {
6056
+ var Player = class _Player extends EventEmitter {
5336
6057
  constructor(element, options = {}) {
5337
6058
  super();
5338
6059
  this.element = typeof element === "string" ? document.querySelector(element) : element;
@@ -5440,6 +6161,8 @@ var VidPly = (() => {
5440
6161
  screenReaderAnnouncements: true,
5441
6162
  highContrast: false,
5442
6163
  focusHighlight: true,
6164
+ metadataAlerts: {},
6165
+ metadataHashtags: {},
5443
6166
  // Languages
5444
6167
  language: "en",
5445
6168
  languages: ["en"],
@@ -5458,6 +6181,8 @@ var VidPly = (() => {
5458
6181
  onError: null,
5459
6182
  ...options
5460
6183
  };
6184
+ this.options.metadataAlerts = this.options.metadataAlerts || {};
6185
+ this.options.metadataHashtags = this.options.metadataHashtags || {};
5461
6186
  this.storage = new StorageManager("vidply");
5462
6187
  const savedPrefs = this.storage.getPlayerPreferences();
5463
6188
  if (savedPrefs) {
@@ -5492,12 +6217,22 @@ var VidPly = (() => {
5492
6217
  this.audioDescriptionSourceElement = null;
5493
6218
  this.originalAudioDescriptionSource = null;
5494
6219
  this.audioDescriptionCaptionTracks = [];
6220
+ this._audioDescriptionDesiredState = false;
6221
+ this._textTracksCache = null;
6222
+ this._textTracksDirty = true;
6223
+ this._sourceElementsCache = null;
6224
+ this._sourceElementsDirty = true;
6225
+ this._trackElementsCache = null;
6226
+ this._trackElementsDirty = true;
6227
+ this.timeouts = /* @__PURE__ */ new Set();
5495
6228
  this.container = null;
5496
6229
  this.renderer = null;
5497
6230
  this.controlBar = null;
5498
6231
  this.captionManager = null;
5499
6232
  this.keyboardManager = null;
5500
6233
  this.settingsDialog = null;
6234
+ this.metadataCueChangeHandler = null;
6235
+ this.metadataAlertHandlers = /* @__PURE__ */ new Map();
5501
6236
  this.init();
5502
6237
  }
5503
6238
  async init() {
@@ -5529,6 +6264,7 @@ var VidPly = (() => {
5529
6264
  if (this.options.transcript || this.options.transcriptButton) {
5530
6265
  this.transcriptManager = new TranscriptManager(this);
5531
6266
  }
6267
+ this.setupMetadataHandling();
5532
6268
  if (this.options.keyboard) {
5533
6269
  this.keyboardManager = new KeyboardManager(this);
5534
6270
  }
@@ -5599,7 +6335,6 @@ var VidPly = (() => {
5599
6335
  this.element.style.height = "100%";
5600
6336
  if (this.element.tagName === "VIDEO" && this.options.playsInline) {
5601
6337
  this.element.setAttribute("playsinline", "");
5602
- this.element.setAttribute("webkit-playsinline", "");
5603
6338
  this.element.playsInline = true;
5604
6339
  }
5605
6340
  if (this.options.width) {
@@ -5614,12 +6349,22 @@ var VidPly = (() => {
5614
6349
  if (this.element.tagName === "VIDEO") {
5615
6350
  this.createPlayButtonOverlay();
5616
6351
  }
6352
+ this.element.vidply = this;
6353
+ _Player.instances.push(this);
5617
6354
  this.element.style.cursor = "pointer";
5618
6355
  this.element.addEventListener("click", (e) => {
5619
6356
  if (e.target === this.element) {
5620
6357
  this.toggle();
5621
6358
  }
5622
6359
  });
6360
+ this.on("play", () => {
6361
+ this.hidePosterOverlay();
6362
+ });
6363
+ this.on("timeupdate", () => {
6364
+ if (this.state.currentTime > 0) {
6365
+ this.hidePosterOverlay();
6366
+ }
6367
+ });
5623
6368
  }
5624
6369
  createPlayButtonOverlay() {
5625
6370
  this.playButtonOverlay = createPlayOverlay();
@@ -5646,7 +6391,7 @@ var VidPly = (() => {
5646
6391
  if (!src) {
5647
6392
  throw new Error("No media source found");
5648
6393
  }
5649
- const sourceElements = this.element.querySelectorAll("source");
6394
+ const sourceElements = this.sourceElements;
5650
6395
  for (const sourceEl of sourceElements) {
5651
6396
  const descSrc = sourceEl.getAttribute("data-desc-src");
5652
6397
  const origSrc = sourceEl.getAttribute("data-orig-src");
@@ -5675,7 +6420,7 @@ var VidPly = (() => {
5675
6420
  }
5676
6421
  }
5677
6422
  }
5678
- const trackElements = this.element.querySelectorAll("track");
6423
+ const trackElements = this.trackElements;
5679
6424
  trackElements.forEach((trackEl) => {
5680
6425
  const trackKind = trackEl.getAttribute("kind");
5681
6426
  const trackDescSrc = trackEl.getAttribute("data-desc-src");
@@ -5696,19 +6441,137 @@ var VidPly = (() => {
5696
6441
  if (!this.originalSrc) {
5697
6442
  this.originalSrc = src;
5698
6443
  }
5699
- let renderer;
5700
- if (src.includes("youtube.com") || src.includes("youtu.be")) {
5701
- renderer = YouTubeRenderer;
5702
- } else if (src.includes("vimeo.com")) {
5703
- renderer = VimeoRenderer;
5704
- } else if (src.includes(".m3u8")) {
5705
- renderer = HLSRenderer;
5706
- } else {
5707
- renderer = HTML5Renderer;
6444
+ let renderer;
6445
+ if (src.includes("youtube.com") || src.includes("youtu.be")) {
6446
+ renderer = YouTubeRenderer;
6447
+ } else if (src.includes("vimeo.com")) {
6448
+ renderer = VimeoRenderer;
6449
+ } else if (src.includes(".m3u8")) {
6450
+ renderer = HLSRenderer;
6451
+ } else {
6452
+ renderer = HTML5Renderer;
6453
+ }
6454
+ this.log(`Using ${renderer.name} renderer`);
6455
+ this.renderer = new renderer(this);
6456
+ await this.renderer.init();
6457
+ this.invalidateTrackCache();
6458
+ }
6459
+ /**
6460
+ * Get cached text tracks array
6461
+ * @returns {Array} Array of text tracks
6462
+ */
6463
+ get textTracks() {
6464
+ if (!this._textTracksCache || this._textTracksDirty) {
6465
+ this._textTracksCache = Array.from(this.element.textTracks || []);
6466
+ this._textTracksDirty = false;
6467
+ }
6468
+ return this._textTracksCache;
6469
+ }
6470
+ /**
6471
+ * Get cached source elements array
6472
+ * @returns {Array} Array of source elements
6473
+ */
6474
+ get sourceElements() {
6475
+ if (!this._sourceElementsCache || this._sourceElementsDirty) {
6476
+ this._sourceElementsCache = Array.from(this.element.querySelectorAll("source"));
6477
+ this._sourceElementsDirty = false;
6478
+ }
6479
+ return this._sourceElementsCache;
6480
+ }
6481
+ /**
6482
+ * Get cached track elements array
6483
+ * @returns {Array} Array of track elements
6484
+ */
6485
+ get trackElements() {
6486
+ if (!this._trackElementsCache || this._trackElementsDirty) {
6487
+ this._trackElementsCache = Array.from(this.element.querySelectorAll("track"));
6488
+ this._trackElementsDirty = false;
6489
+ }
6490
+ return this._trackElementsCache;
6491
+ }
6492
+ /**
6493
+ * Invalidate DOM query cache (call when tracks/sources change)
6494
+ */
6495
+ invalidateTrackCache() {
6496
+ this._textTracksDirty = true;
6497
+ this._trackElementsDirty = true;
6498
+ this._sourceElementsDirty = true;
6499
+ }
6500
+ /**
6501
+ * Find a text track by kind and optionally language
6502
+ * @param {string} kind - Track kind (captions, subtitles, descriptions, chapters, metadata)
6503
+ * @param {string} [language] - Optional language code
6504
+ * @returns {TextTrack|null} Found track or null
6505
+ */
6506
+ findTextTrack(kind, language = null) {
6507
+ const tracks = this.textTracks;
6508
+ if (language) {
6509
+ return tracks.find((t) => t.kind === kind && t.language === language);
6510
+ }
6511
+ return tracks.find((t) => t.kind === kind);
6512
+ }
6513
+ /**
6514
+ * Find a source element by attribute
6515
+ * @param {string} attribute - Attribute name (e.g., 'data-desc-src')
6516
+ * @param {string} [value] - Optional attribute value
6517
+ * @returns {Element|null} Found source element or null
6518
+ */
6519
+ findSourceElement(attribute, value = null) {
6520
+ const sources = this.sourceElements;
6521
+ if (value) {
6522
+ return sources.find((el) => el.getAttribute(attribute) === value);
6523
+ }
6524
+ return sources.find((el) => el.hasAttribute(attribute));
6525
+ }
6526
+ /**
6527
+ * Find a track element by its associated TextTrack
6528
+ * @param {TextTrack} track - The TextTrack object
6529
+ * @returns {Element|null} Found track element or null
6530
+ */
6531
+ findTrackElement(track) {
6532
+ return this.trackElements.find((el) => el.track === track);
6533
+ }
6534
+ showPosterOverlay() {
6535
+ if (!this.videoWrapper || this.element.tagName !== "VIDEO") {
6536
+ return;
6537
+ }
6538
+ const poster = this.element.getAttribute("poster") || this.element.poster || this.options.poster;
6539
+ if (!poster) {
6540
+ return;
6541
+ }
6542
+ this.videoWrapper.style.setProperty("--vidply-poster-image", `url("${poster}")`);
6543
+ this.videoWrapper.classList.add("vidply-forced-poster");
6544
+ }
6545
+ hidePosterOverlay() {
6546
+ if (!this.videoWrapper) {
6547
+ return;
6548
+ }
6549
+ this.videoWrapper.classList.remove("vidply-forced-poster");
6550
+ this.videoWrapper.style.removeProperty("--vidply-poster-image");
6551
+ }
6552
+ /**
6553
+ * Set a managed timeout that will be cleaned up on destroy
6554
+ * @param {Function} callback - Callback function
6555
+ * @param {number} delay - Delay in milliseconds
6556
+ * @returns {number} Timeout ID
6557
+ */
6558
+ setManagedTimeout(callback, delay) {
6559
+ const timeoutId = setTimeout(() => {
6560
+ this.timeouts.delete(timeoutId);
6561
+ callback();
6562
+ }, delay);
6563
+ this.timeouts.add(timeoutId);
6564
+ return timeoutId;
6565
+ }
6566
+ /**
6567
+ * Clear a managed timeout
6568
+ * @param {number} timeoutId - Timeout ID to clear
6569
+ */
6570
+ clearManagedTimeout(timeoutId) {
6571
+ if (timeoutId) {
6572
+ clearTimeout(timeoutId);
6573
+ this.timeouts.delete(timeoutId);
5708
6574
  }
5709
- this.log(`Using ${renderer.name} renderer`);
5710
- this.renderer = new renderer(this);
5711
- await this.renderer.init();
5712
6575
  }
5713
6576
  /**
5714
6577
  * Load new media source (for playlists)
@@ -5724,8 +6587,9 @@ var VidPly = (() => {
5724
6587
  if (this.renderer) {
5725
6588
  this.pause();
5726
6589
  }
5727
- const existingTracks = this.element.querySelectorAll("track");
6590
+ const existingTracks = this.trackElements;
5728
6591
  existingTracks.forEach((track) => track.remove());
6592
+ this.invalidateTrackCache();
5729
6593
  this.element.src = config.src;
5730
6594
  if (config.type) {
5731
6595
  this.element.type = config.type;
@@ -5745,6 +6609,7 @@ var VidPly = (() => {
5745
6609
  }
5746
6610
  this.element.appendChild(track);
5747
6611
  });
6612
+ this.invalidateTrackCache();
5748
6613
  }
5749
6614
  const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
5750
6615
  if (shouldChangeRenderer && this.renderer) {
@@ -5992,7 +6857,7 @@ var VidPly = (() => {
5992
6857
  }
5993
6858
  // Audio Description
5994
6859
  async enableAudioDescription() {
5995
- const hasSourceElementsWithDesc = Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
6860
+ const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
5996
6861
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
5997
6862
  if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
5998
6863
  console.warn("VidPly: No audio description source, source elements, or tracks provided");
@@ -6000,10 +6865,14 @@ var VidPly = (() => {
6000
6865
  }
6001
6866
  const currentTime = this.state.currentTime;
6002
6867
  const wasPlaying = this.state.playing;
6868
+ const shouldKeepPoster = !wasPlaying && currentTime === 0;
6869
+ if (shouldKeepPoster) {
6870
+ this.showPosterOverlay();
6871
+ }
6003
6872
  let swappedTracksForTranscript = [];
6004
6873
  if (this.audioDescriptionSourceElement) {
6005
6874
  const currentSrc = this.element.currentSrc || this.element.src;
6006
- const sourceElements = Array.from(this.element.querySelectorAll("source"));
6875
+ const sourceElements = this.sourceElements;
6007
6876
  let sourceElementToUpdate = null;
6008
6877
  let descSrc = this.audioDescriptionSrc;
6009
6878
  for (const sourceEl of sourceElements) {
@@ -6100,8 +6969,9 @@ var VidPly = (() => {
6100
6969
  trackInfo.trackElement = newTrackElement;
6101
6970
  });
6102
6971
  this.element.load();
6972
+ this.invalidateTrackCache();
6103
6973
  const setupNewTracks = () => {
6104
- setTimeout(() => {
6974
+ this.setManagedTimeout(() => {
6105
6975
  swappedTracksForTranscript.forEach((trackInfo) => {
6106
6976
  const trackElement = trackInfo.trackElement;
6107
6977
  const newTextTrack = trackElement.track;
@@ -6137,7 +7007,7 @@ var VidPly = (() => {
6137
7007
  const skippedCount = validationResults.length - tracksToSwap.length;
6138
7008
  }
6139
7009
  }
6140
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
7010
+ const allSourceElements = this.sourceElements;
6141
7011
  const sourcesToUpdate = [];
6142
7012
  allSourceElements.forEach((sourceEl) => {
6143
7013
  const descSrcAttr = sourceEl.getAttribute("data-desc-src");
@@ -6181,8 +7051,15 @@ var VidPly = (() => {
6181
7051
  if (sourceInfo.descSrc) {
6182
7052
  newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6183
7053
  }
6184
- this.element.appendChild(newSource);
7054
+ const firstTrack = this.element.querySelector("track");
7055
+ if (firstTrack) {
7056
+ this.element.insertBefore(newSource, firstTrack);
7057
+ } else {
7058
+ this.element.appendChild(newSource);
7059
+ }
6185
7060
  });
7061
+ this._sourceElementsDirty = true;
7062
+ this._sourceElementsCache = null;
6186
7063
  this.element.load();
6187
7064
  await new Promise((resolve) => {
6188
7065
  const onLoadedMetadata = () => {
@@ -6192,18 +7069,18 @@ var VidPly = (() => {
6192
7069
  this.element.addEventListener("loadedmetadata", onLoadedMetadata);
6193
7070
  });
6194
7071
  await new Promise((resolve) => setTimeout(resolve, 300));
6195
- if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6196
- if (this.element.readyState >= 1) {
6197
- this.element.currentTime = 1e-3;
6198
- setTimeout(() => {
6199
- this.element.currentTime = 0;
6200
- }, 10);
6201
- }
7072
+ if (currentTime > 0) {
7073
+ this.seek(currentTime);
6202
7074
  }
6203
- this.seek(currentTime);
6204
7075
  if (wasPlaying) {
6205
7076
  this.play();
6206
7077
  }
7078
+ if (!shouldKeepPoster) {
7079
+ this.hidePosterOverlay();
7080
+ }
7081
+ if (!this._audioDescriptionDesiredState) {
7082
+ return;
7083
+ }
6207
7084
  this.state.audioDescriptionEnabled = true;
6208
7085
  this.emit("audiodescriptionenabled");
6209
7086
  } else {
@@ -6315,7 +7192,7 @@ var VidPly = (() => {
6315
7192
  }, 100);
6316
7193
  }
6317
7194
  }
6318
- const fallbackSourceElements = Array.from(this.element.querySelectorAll("source"));
7195
+ const fallbackSourceElements = this.sourceElements;
6319
7196
  const hasSourceElementsWithDesc2 = fallbackSourceElements.some((el) => el.getAttribute("data-desc-src"));
6320
7197
  if (hasSourceElementsWithDesc2) {
6321
7198
  const fallbackSourcesToUpdate = [];
@@ -6363,6 +7240,7 @@ var VidPly = (() => {
6363
7240
  this.element.appendChild(newSource);
6364
7241
  });
6365
7242
  this.element.load();
7243
+ this.invalidateTrackCache();
6366
7244
  } else {
6367
7245
  this.element.src = this.audioDescriptionSrc;
6368
7246
  }
@@ -6377,12 +7255,14 @@ var VidPly = (() => {
6377
7255
  if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6378
7256
  if (this.element.readyState >= 1) {
6379
7257
  this.element.currentTime = 1e-3;
6380
- setTimeout(() => {
7258
+ this.setManagedTimeout(() => {
6381
7259
  this.element.currentTime = 0;
6382
7260
  }, 10);
6383
7261
  }
6384
7262
  }
6385
- this.seek(currentTime);
7263
+ if (currentTime > 0) {
7264
+ this.seek(currentTime);
7265
+ }
6386
7266
  if (wasPlaying) {
6387
7267
  this.play();
6388
7268
  }
@@ -6417,7 +7297,8 @@ var VidPly = (() => {
6417
7297
  const swappedTracks = typeof swappedTracksForTranscript !== "undefined" ? swappedTracksForTranscript : [];
6418
7298
  if (swappedTracks.length > 0) {
6419
7299
  const onMetadataLoaded = () => {
6420
- const allTextTracks = Array.from(this.element.textTracks);
7300
+ this.invalidateTrackCache();
7301
+ const allTextTracks = this.textTracks;
6421
7302
  const freshTracks = swappedTracks.map((trackInfo) => {
6422
7303
  const trackEl = trackInfo.trackElement;
6423
7304
  const expectedSrc = trackEl.getAttribute("src");
@@ -6427,9 +7308,7 @@ var VidPly = (() => {
6427
7308
  if (!foundTrack) {
6428
7309
  foundTrack = allTextTracks.find((track) => {
6429
7310
  if (track.language === srclang && (track.kind === kind || kind === "captions" && track.kind === "subtitles")) {
6430
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6431
- (el) => el.track === track
6432
- );
7311
+ const trackElementForTrack = this.findTrackElement(track);
6433
7312
  if (trackElementForTrack) {
6434
7313
  const actualSrc = trackElementForTrack.getAttribute("src");
6435
7314
  if (actualSrc === expectedSrc) {
@@ -6441,9 +7320,7 @@ var VidPly = (() => {
6441
7320
  });
6442
7321
  }
6443
7322
  if (foundTrack) {
6444
- const trackElement = Array.from(this.element.querySelectorAll("track")).find(
6445
- (el) => el.track === foundTrack
6446
- );
7323
+ const trackElement = this.findTrackElement(foundTrack);
6447
7324
  if (trackElement && trackElement.getAttribute("src") !== expectedSrc) {
6448
7325
  return null;
6449
7326
  }
@@ -6451,7 +7328,7 @@ var VidPly = (() => {
6451
7328
  return foundTrack;
6452
7329
  }).filter(Boolean);
6453
7330
  if (freshTracks.length === 0) {
6454
- setTimeout(() => {
7331
+ this.setManagedTimeout(() => {
6455
7332
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6456
7333
  this.transcriptManager.loadTranscriptData();
6457
7334
  }
@@ -6467,14 +7344,13 @@ var VidPly = (() => {
6467
7344
  const checkLoaded = () => {
6468
7345
  loadedCount++;
6469
7346
  if (loadedCount >= freshTracks.length) {
6470
- setTimeout(() => {
7347
+ this.setManagedTimeout(() => {
6471
7348
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6472
- const allTextTracks2 = Array.from(this.element.textTracks);
7349
+ this.invalidateTrackCache();
7350
+ const allTextTracks2 = this.textTracks;
6473
7351
  const swappedTrackSrcs = swappedTracks.map((t) => t.describedSrc);
6474
7352
  const hasCorrectTracks = freshTracks.some((track) => {
6475
- const trackEl = Array.from(this.element.querySelectorAll("track")).find(
6476
- (el) => el.track === track
6477
- );
7353
+ const trackEl = this.findTrackElement(track);
6478
7354
  return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute("src"));
6479
7355
  });
6480
7356
  if (hasCorrectTracks || freshTracks.length > 0) {
@@ -6488,9 +7364,7 @@ var VidPly = (() => {
6488
7364
  if (track.mode === "disabled") {
6489
7365
  track.mode = "hidden";
6490
7366
  }
6491
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6492
- (el) => el.track === track
6493
- );
7367
+ const trackElementForTrack = this.findTrackElement(track);
6494
7368
  const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute("src") : null;
6495
7369
  const expectedTrackInfo = swappedTracks.find((t) => {
6496
7370
  const tEl = t.trackElement;
@@ -6508,10 +7382,10 @@ var VidPly = (() => {
6508
7382
  track.mode = "hidden";
6509
7383
  }
6510
7384
  const onTrackLoad = () => {
6511
- setTimeout(checkLoaded, 300);
7385
+ this.setManagedTimeout(checkLoaded, 300);
6512
7386
  };
6513
7387
  if (track.readyState >= 2) {
6514
- setTimeout(() => {
7388
+ this.setManagedTimeout(() => {
6515
7389
  if (track.cues && track.cues.length > 0) {
6516
7390
  checkLoaded();
6517
7391
  } else {
@@ -6528,12 +7402,12 @@ var VidPly = (() => {
6528
7402
  });
6529
7403
  };
6530
7404
  const waitForTracks = () => {
6531
- setTimeout(() => {
7405
+ this.setManagedTimeout(() => {
6532
7406
  if (this.element.readyState >= 1) {
6533
7407
  onMetadataLoaded();
6534
7408
  } else {
6535
7409
  this.element.addEventListener("loadedmetadata", onMetadataLoaded, { once: true });
6536
- setTimeout(onMetadataLoaded, 2e3);
7410
+ this.setManagedTimeout(onMetadataLoaded, 2e3);
6537
7411
  }
6538
7412
  }, 500);
6539
7413
  };
@@ -6551,6 +7425,12 @@ var VidPly = (() => {
6551
7425
  }, 800);
6552
7426
  }
6553
7427
  }
7428
+ if (!shouldKeepPoster) {
7429
+ this.hidePosterOverlay();
7430
+ }
7431
+ if (!this._audioDescriptionDesiredState) {
7432
+ return;
7433
+ }
6554
7434
  this.state.audioDescriptionEnabled = true;
6555
7435
  this.emit("audiodescriptionenabled");
6556
7436
  }
@@ -6567,7 +7447,7 @@ var VidPly = (() => {
6567
7447
  }
6568
7448
  });
6569
7449
  }
6570
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
7450
+ const allSourceElements = this.sourceElements;
6571
7451
  const hasSourceElementsToSwap = allSourceElements.some((el) => el.getAttribute("data-orig-src"));
6572
7452
  if (hasSourceElementsToSwap) {
6573
7453
  const sourcesToRestore = [];
@@ -6610,61 +7490,107 @@ var VidPly = (() => {
6610
7490
  if (sourceInfo.descSrc) {
6611
7491
  newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6612
7492
  }
6613
- this.element.appendChild(newSource);
7493
+ const firstTrack = this.element.querySelector("track");
7494
+ if (firstTrack) {
7495
+ this.element.insertBefore(newSource, firstTrack);
7496
+ } else {
7497
+ this.element.appendChild(newSource);
7498
+ }
6614
7499
  });
7500
+ this._sourceElementsDirty = true;
7501
+ this._sourceElementsCache = null;
6615
7502
  this.element.load();
6616
7503
  } else {
6617
7504
  const originalSrcToUse = this.originalAudioDescriptionSource || this.originalSrc;
6618
7505
  this.element.src = originalSrcToUse;
6619
7506
  this.element.load();
6620
7507
  }
6621
- await new Promise((resolve) => {
6622
- const onLoadedMetadata = () => {
6623
- this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
6624
- resolve();
6625
- };
6626
- this.element.addEventListener("loadedmetadata", onLoadedMetadata);
6627
- });
6628
- this.seek(currentTime);
6629
- if (wasPlaying) {
6630
- this.play();
7508
+ if (currentTime > 0 || wasPlaying) {
7509
+ await new Promise((resolve) => {
7510
+ const onLoadedMetadata = () => {
7511
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
7512
+ resolve();
7513
+ };
7514
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
7515
+ });
7516
+ if (currentTime > 0) {
7517
+ this.seek(currentTime);
7518
+ }
7519
+ if (wasPlaying) {
7520
+ this.play();
7521
+ }
7522
+ }
7523
+ if (!wasPlaying && currentTime === 0) {
7524
+ this.showPosterOverlay();
7525
+ } else {
7526
+ this.hidePosterOverlay();
6631
7527
  }
6632
7528
  if (this.transcriptManager && this.transcriptManager.isVisible) {
6633
- setTimeout(() => {
7529
+ this.setManagedTimeout(() => {
6634
7530
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6635
7531
  this.transcriptManager.loadTranscriptData();
6636
7532
  }
6637
7533
  }, 500);
6638
7534
  }
7535
+ if (this._audioDescriptionDesiredState) {
7536
+ return;
7537
+ }
6639
7538
  this.state.audioDescriptionEnabled = false;
6640
7539
  this.emit("audiodescriptiondisabled");
6641
7540
  }
6642
7541
  async toggleAudioDescription() {
6643
- const textTracks = Array.from(this.element.textTracks || []);
6644
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
6645
- const hasAudioDescriptionSrc = this.audioDescriptionSrc || Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
7542
+ const descriptionTrack = this.findTextTrack("descriptions");
7543
+ const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6646
7544
  if (descriptionTrack && hasAudioDescriptionSrc) {
6647
7545
  if (this.state.audioDescriptionEnabled) {
7546
+ this._audioDescriptionDesiredState = false;
6648
7547
  descriptionTrack.mode = "hidden";
6649
7548
  await this.disableAudioDescription();
6650
7549
  } else {
7550
+ this._audioDescriptionDesiredState = true;
6651
7551
  await this.enableAudioDescription();
6652
- descriptionTrack.mode = "showing";
7552
+ const enableDescriptionTrack = () => {
7553
+ this.invalidateTrackCache();
7554
+ const descTrack = this.findTextTrack("descriptions");
7555
+ if (descTrack) {
7556
+ if (descTrack.mode === "disabled") {
7557
+ descTrack.mode = "hidden";
7558
+ this.setManagedTimeout(() => {
7559
+ descTrack.mode = "showing";
7560
+ }, 50);
7561
+ } else {
7562
+ descTrack.mode = "showing";
7563
+ }
7564
+ } else if (this.element.readyState < 2) {
7565
+ this.setManagedTimeout(enableDescriptionTrack, 100);
7566
+ }
7567
+ };
7568
+ if (this.element.readyState >= 1) {
7569
+ this.setManagedTimeout(enableDescriptionTrack, 200);
7570
+ } else {
7571
+ this.element.addEventListener("loadedmetadata", () => {
7572
+ this.setManagedTimeout(enableDescriptionTrack, 200);
7573
+ }, { once: true });
7574
+ }
6653
7575
  }
6654
7576
  } else if (descriptionTrack) {
6655
7577
  if (descriptionTrack.mode === "showing") {
7578
+ this._audioDescriptionDesiredState = false;
6656
7579
  descriptionTrack.mode = "hidden";
6657
7580
  this.state.audioDescriptionEnabled = false;
6658
7581
  this.emit("audiodescriptiondisabled");
6659
7582
  } else {
7583
+ this._audioDescriptionDesiredState = true;
6660
7584
  descriptionTrack.mode = "showing";
6661
7585
  this.state.audioDescriptionEnabled = true;
6662
7586
  this.emit("audiodescriptionenabled");
6663
7587
  }
6664
7588
  } else if (hasAudioDescriptionSrc) {
6665
7589
  if (this.state.audioDescriptionEnabled) {
7590
+ this._audioDescriptionDesiredState = false;
6666
7591
  await this.disableAudioDescription();
6667
7592
  } else {
7593
+ this._audioDescriptionDesiredState = true;
6668
7594
  await this.enableAudioDescription();
6669
7595
  }
6670
7596
  }
@@ -6761,177 +7687,22 @@ var VidPly = (() => {
6761
7687
  }
6762
7688
  setupSignLanguageInteraction() {
6763
7689
  if (!this.signLanguageWrapper) return;
6764
- let isDragging = false;
6765
- let isResizing = false;
6766
- let resizeDirection = null;
6767
- let startX = 0;
6768
- let startY = 0;
6769
- let startLeft = 0;
6770
- let startTop = 0;
6771
- let startWidth = 0;
6772
- let startHeight = 0;
6773
- let dragMode = false;
6774
- let resizeMode = false;
6775
- const onMouseDownVideo = (e) => {
6776
- if (e.target !== this.signLanguageVideo) return;
6777
- e.preventDefault();
6778
- isDragging = true;
6779
- startX = e.clientX;
6780
- startY = e.clientY;
6781
- const rect = this.signLanguageWrapper.getBoundingClientRect();
6782
- startLeft = rect.left;
6783
- startTop = rect.top;
6784
- this.signLanguageWrapper.classList.add("vidply-sign-dragging");
6785
- };
6786
- const onMouseDownHandle = (e) => {
6787
- if (!e.target.classList.contains("vidply-sign-resize-handle")) return;
6788
- e.preventDefault();
6789
- e.stopPropagation();
6790
- isResizing = true;
6791
- resizeDirection = e.target.getAttribute("data-direction");
6792
- startX = e.clientX;
6793
- startY = e.clientY;
6794
- const rect = this.signLanguageWrapper.getBoundingClientRect();
6795
- startLeft = rect.left;
6796
- startTop = rect.top;
6797
- startWidth = rect.width;
6798
- startHeight = rect.height;
6799
- this.signLanguageWrapper.classList.add("vidply-sign-resizing");
6800
- };
6801
- const onMouseMove = (e) => {
6802
- if (isDragging) {
6803
- const deltaX = e.clientX - startX;
6804
- const deltaY = e.clientY - startY;
6805
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
6806
- const containerRect = this.container.getBoundingClientRect();
6807
- const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
6808
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
6809
- const videoWrapperTop = videoWrapperRect.top - containerRect.top;
6810
- let newLeft = startLeft + deltaX - containerRect.left;
6811
- let newTop = startTop + deltaY - containerRect.top;
6812
- const controlsHeight = 95;
6813
- newLeft = Math.max(videoWrapperLeft, Math.min(newLeft, videoWrapperLeft + videoWrapperRect.width - wrapperRect.width));
6814
- newTop = Math.max(videoWrapperTop, Math.min(newTop, videoWrapperTop + videoWrapperRect.height - wrapperRect.height - controlsHeight));
6815
- this.signLanguageWrapper.style.left = `${newLeft}px`;
6816
- this.signLanguageWrapper.style.top = `${newTop}px`;
6817
- this.signLanguageWrapper.style.right = "auto";
6818
- this.signLanguageWrapper.style.bottom = "auto";
6819
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
6820
- } else if (isResizing) {
6821
- const deltaX = e.clientX - startX;
6822
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
6823
- const containerRect = this.container.getBoundingClientRect();
6824
- let newWidth = startWidth;
6825
- let newLeft = startLeft - containerRect.left;
6826
- if (resizeDirection.includes("e")) {
6827
- newWidth = Math.max(150, startWidth + deltaX);
6828
- const maxWidth = videoWrapperRect.right - startLeft;
6829
- newWidth = Math.min(newWidth, maxWidth);
6830
- }
6831
- if (resizeDirection.includes("w")) {
6832
- const proposedWidth = Math.max(150, startWidth - deltaX);
6833
- const proposedLeft = startLeft + (startWidth - proposedWidth) - containerRect.left;
6834
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
6835
- if (proposedLeft >= videoWrapperLeft) {
6836
- newWidth = proposedWidth;
6837
- newLeft = proposedLeft;
6838
- }
6839
- }
6840
- this.signLanguageWrapper.style.width = `${newWidth}px`;
6841
- this.signLanguageWrapper.style.height = "auto";
6842
- if (resizeDirection.includes("w")) {
6843
- this.signLanguageWrapper.style.left = `${newLeft}px`;
6844
- }
6845
- this.signLanguageWrapper.style.right = "auto";
6846
- this.signLanguageWrapper.style.bottom = "auto";
6847
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
6848
- }
6849
- };
6850
- const onMouseUp = () => {
6851
- if (isDragging || isResizing) {
6852
- this.saveSignLanguagePreferences();
6853
- }
6854
- isDragging = false;
6855
- isResizing = false;
6856
- resizeDirection = null;
6857
- this.signLanguageWrapper.classList.remove("vidply-sign-dragging", "vidply-sign-resizing");
6858
- };
6859
- const onKeyDown = (e) => {
6860
- if (e.key === "d" || e.key === "D") {
6861
- dragMode = !dragMode;
6862
- resizeMode = false;
6863
- this.signLanguageWrapper.classList.toggle("vidply-sign-keyboard-drag", dragMode);
6864
- this.signLanguageWrapper.classList.remove("vidply-sign-keyboard-resize");
6865
- e.preventDefault();
6866
- return;
6867
- }
6868
- if (e.key === "r" || e.key === "R") {
6869
- resizeMode = !resizeMode;
6870
- dragMode = false;
6871
- this.signLanguageWrapper.classList.toggle("vidply-sign-keyboard-resize", resizeMode);
6872
- this.signLanguageWrapper.classList.remove("vidply-sign-keyboard-drag");
6873
- e.preventDefault();
6874
- return;
6875
- }
6876
- if (e.key === "Escape") {
6877
- dragMode = false;
6878
- resizeMode = false;
6879
- this.signLanguageWrapper.classList.remove("vidply-sign-keyboard-drag", "vidply-sign-keyboard-resize");
6880
- e.preventDefault();
6881
- return;
6882
- }
6883
- if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
6884
- const step = e.shiftKey ? 10 : 5;
6885
- const rect = this.signLanguageWrapper.getBoundingClientRect();
6886
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
6887
- const containerRect = this.container.getBoundingClientRect();
6888
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
6889
- const videoWrapperTop = videoWrapperRect.top - containerRect.top;
6890
- if (dragMode) {
6891
- let left = rect.left - containerRect.left;
6892
- let top = rect.top - containerRect.top;
6893
- if (e.key === "ArrowLeft") left -= step;
6894
- if (e.key === "ArrowRight") left += step;
6895
- if (e.key === "ArrowUp") top -= step;
6896
- if (e.key === "ArrowDown") top += step;
6897
- const controlsHeight = 95;
6898
- left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperRect.width - rect.width));
6899
- top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperRect.height - rect.height - controlsHeight));
6900
- this.signLanguageWrapper.style.left = `${left}px`;
6901
- this.signLanguageWrapper.style.top = `${top}px`;
6902
- this.signLanguageWrapper.style.right = "auto";
6903
- this.signLanguageWrapper.style.bottom = "auto";
6904
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
6905
- this.saveSignLanguagePreferences();
6906
- e.preventDefault();
6907
- } else if (resizeMode) {
6908
- let width = rect.width;
6909
- if (e.key === "ArrowLeft") width -= step;
6910
- if (e.key === "ArrowRight") width += step;
6911
- if (e.key === "ArrowUp") width += step;
6912
- if (e.key === "ArrowDown") width -= step;
6913
- width = Math.max(150, width);
6914
- width = Math.min(width, videoWrapperRect.width);
6915
- this.signLanguageWrapper.style.width = `${width}px`;
6916
- this.signLanguageWrapper.style.height = "auto";
6917
- this.saveSignLanguagePreferences();
6918
- e.preventDefault();
6919
- }
6920
- }
6921
- };
6922
- this.signLanguageVideo.addEventListener("mousedown", onMouseDownVideo);
6923
- const handles = this.signLanguageWrapper.querySelectorAll(".vidply-sign-resize-handle");
6924
- handles.forEach((handle) => handle.addEventListener("mousedown", onMouseDownHandle));
6925
- document.addEventListener("mousemove", onMouseMove);
6926
- document.addEventListener("mouseup", onMouseUp);
6927
- this.signLanguageWrapper.addEventListener("keydown", onKeyDown);
7690
+ const resizeHandles = Array.from(this.signLanguageWrapper.querySelectorAll(".vidply-sign-resize-handle"));
7691
+ this.signLanguageDraggable = new DraggableResizable(this.signLanguageWrapper, {
7692
+ dragHandle: this.signLanguageVideo,
7693
+ resizeHandles,
7694
+ constrainToViewport: true,
7695
+ maintainAspectRatio: true,
7696
+ minWidth: 150,
7697
+ minHeight: 100,
7698
+ classPrefix: "vidply-sign",
7699
+ keyboardDragKey: "d",
7700
+ keyboardResizeKey: "r",
7701
+ keyboardStep: 5,
7702
+ keyboardStepLarge: 10
7703
+ });
6928
7704
  this.signLanguageInteractionHandlers = {
6929
- mouseDownVideo: onMouseDownVideo,
6930
- mouseDownHandle: onMouseDownHandle,
6931
- mouseMove: onMouseMove,
6932
- mouseUp: onMouseUp,
6933
- keyDown: onKeyDown,
6934
- handles
7705
+ draggable: this.signLanguageDraggable
6935
7706
  };
6936
7707
  }
6937
7708
  constrainSignLanguagePosition() {
@@ -6998,22 +7769,11 @@ var VidPly = (() => {
6998
7769
  this.off("ratechange", this.signLanguageHandlers.ratechange);
6999
7770
  this.signLanguageHandlers = null;
7000
7771
  }
7001
- if (this.signLanguageInteractionHandlers) {
7002
- if (this.signLanguageVideo) {
7003
- this.signLanguageVideo.removeEventListener("mousedown", this.signLanguageInteractionHandlers.mouseDownVideo);
7004
- }
7005
- if (this.signLanguageInteractionHandlers.handles) {
7006
- this.signLanguageInteractionHandlers.handles.forEach((handle) => {
7007
- handle.removeEventListener("mousedown", this.signLanguageInteractionHandlers.mouseDownHandle);
7008
- });
7009
- }
7010
- document.removeEventListener("mousemove", this.signLanguageInteractionHandlers.mouseMove);
7011
- document.removeEventListener("mouseup", this.signLanguageInteractionHandlers.mouseUp);
7012
- if (this.signLanguageWrapper) {
7013
- this.signLanguageWrapper.removeEventListener("keydown", this.signLanguageInteractionHandlers.keyDown);
7014
- }
7015
- this.signLanguageInteractionHandlers = null;
7772
+ if (this.signLanguageDraggable) {
7773
+ this.signLanguageDraggable.destroy();
7774
+ this.signLanguageDraggable = null;
7016
7775
  }
7776
+ this.signLanguageInteractionHandlers = null;
7017
7777
  if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
7018
7778
  if (this.signLanguageVideo) {
7019
7779
  this.signLanguageVideo.pause();
@@ -7062,9 +7822,25 @@ var VidPly = (() => {
7062
7822
  }
7063
7823
  }
7064
7824
  // Logging
7065
- log(message, type = "log") {
7066
- if (this.options.debug) {
7067
- console[type](`[VidPly]`, message);
7825
+ log(...messages) {
7826
+ if (!this.options.debug) {
7827
+ return;
7828
+ }
7829
+ let type = "log";
7830
+ if (messages.length > 0) {
7831
+ const potentialType = messages[messages.length - 1];
7832
+ if (typeof potentialType === "string" && console[potentialType]) {
7833
+ type = potentialType;
7834
+ messages = messages.slice(0, -1);
7835
+ }
7836
+ }
7837
+ if (messages.length === 0) {
7838
+ messages = [""];
7839
+ }
7840
+ if (typeof console[type] === "function") {
7841
+ console[type]("[VidPly]", ...messages);
7842
+ } else {
7843
+ console.log("[VidPly]", ...messages);
7068
7844
  }
7069
7845
  }
7070
7846
  // Setup responsive handlers
@@ -7089,7 +7865,9 @@ var VidPly = (() => {
7089
7865
  this.controlBar.updateControlsForViewport(width);
7090
7866
  }
7091
7867
  if (this.transcriptManager && this.transcriptManager.isVisible) {
7092
- this.transcriptManager.positionTranscript();
7868
+ if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
7869
+ this.transcriptManager.positionTranscript();
7870
+ }
7093
7871
  }
7094
7872
  };
7095
7873
  window.addEventListener("resize", this.resizeHandler);
@@ -7098,7 +7876,9 @@ var VidPly = (() => {
7098
7876
  this.orientationHandler = (e) => {
7099
7877
  setTimeout(() => {
7100
7878
  if (this.transcriptManager && this.transcriptManager.isVisible) {
7101
- this.transcriptManager.positionTranscript();
7879
+ if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
7880
+ this.transcriptManager.positionTranscript();
7881
+ }
7102
7882
  }
7103
7883
  }, 100);
7104
7884
  };
@@ -7124,7 +7904,7 @@ var VidPly = (() => {
7124
7904
  this.controlBar.updateFullscreenButton();
7125
7905
  }
7126
7906
  if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== "none") {
7127
- setTimeout(() => {
7907
+ this.setManagedTimeout(() => {
7128
7908
  requestAnimationFrame(() => {
7129
7909
  this.storage.saveSignLanguagePreferences({ size: null });
7130
7910
  this.signLanguageDesiredPosition = "bottom-right";
@@ -7187,12 +7967,368 @@ var VidPly = (() => {
7187
7967
  document.removeEventListener("MSFullscreenChange", this.fullscreenChangeHandler);
7188
7968
  this.fullscreenChangeHandler = null;
7189
7969
  }
7970
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
7971
+ this.timeouts.clear();
7972
+ if (this.metadataCueChangeHandler) {
7973
+ const textTracks = this.textTracks;
7974
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
7975
+ if (metadataTrack) {
7976
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
7977
+ }
7978
+ this.metadataCueChangeHandler = null;
7979
+ }
7980
+ if (this.metadataAlertHandlers && this.metadataAlertHandlers.size > 0) {
7981
+ this.metadataAlertHandlers.forEach(({ button, handler }) => {
7982
+ if (button && handler) {
7983
+ button.removeEventListener("click", handler);
7984
+ }
7985
+ });
7986
+ this.metadataAlertHandlers.clear();
7987
+ }
7190
7988
  if (this.container && this.container.parentNode) {
7191
7989
  this.container.parentNode.insertBefore(this.element, this.container);
7192
7990
  this.container.parentNode.removeChild(this.container);
7193
7991
  }
7194
7992
  this.removeAllListeners();
7195
7993
  }
7994
+ /**
7995
+ * Setup metadata track handling
7996
+ * This enables metadata tracks and listens for cue changes to trigger actions
7997
+ */
7998
+ setupMetadataHandling() {
7999
+ const setupMetadata = () => {
8000
+ const textTracks = this.textTracks;
8001
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
8002
+ if (metadataTrack) {
8003
+ if (metadataTrack.mode === "disabled") {
8004
+ metadataTrack.mode = "hidden";
8005
+ }
8006
+ if (this.metadataCueChangeHandler) {
8007
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
8008
+ }
8009
+ this.metadataCueChangeHandler = () => {
8010
+ const activeCues = Array.from(metadataTrack.activeCues || []);
8011
+ if (activeCues.length > 0) {
8012
+ if (this.options.debug) {
8013
+ this.log("[Metadata] Active cues:", activeCues.map((c) => ({
8014
+ start: c.startTime,
8015
+ end: c.endTime,
8016
+ text: c.text
8017
+ })));
8018
+ }
8019
+ }
8020
+ activeCues.forEach((cue) => {
8021
+ this.handleMetadataCue(cue);
8022
+ });
8023
+ };
8024
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
8025
+ if (this.options.debug) {
8026
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
8027
+ this.log("[Metadata] Track enabled,", cueCount, "cues available");
8028
+ }
8029
+ } else if (this.options.debug) {
8030
+ this.log("[Metadata] No metadata track found");
8031
+ }
8032
+ };
8033
+ setupMetadata();
8034
+ this.on("loadedmetadata", setupMetadata);
8035
+ }
8036
+ normalizeMetadataSelector(selector) {
8037
+ if (!selector) {
8038
+ return null;
8039
+ }
8040
+ const trimmed = selector.trim();
8041
+ if (!trimmed) {
8042
+ return null;
8043
+ }
8044
+ if (trimmed.startsWith("#") || trimmed.startsWith(".") || trimmed.startsWith("[")) {
8045
+ return trimmed;
8046
+ }
8047
+ return `#${trimmed}`;
8048
+ }
8049
+ resolveMetadataConfig(map, key) {
8050
+ if (!map || !key) {
8051
+ return null;
8052
+ }
8053
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
8054
+ return map[key];
8055
+ }
8056
+ const withoutHash = key.replace(/^#/, "");
8057
+ if (Object.prototype.hasOwnProperty.call(map, withoutHash)) {
8058
+ return map[withoutHash];
8059
+ }
8060
+ return null;
8061
+ }
8062
+ cacheMetadataAlertContent(element, config = {}) {
8063
+ if (!element) {
8064
+ return;
8065
+ }
8066
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
8067
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
8068
+ const titleEl = element.querySelector(titleSelector);
8069
+ if (titleEl && !titleEl.dataset.vidplyAlertTitleOriginal) {
8070
+ titleEl.dataset.vidplyAlertTitleOriginal = titleEl.textContent.trim();
8071
+ }
8072
+ const messageEl = element.querySelector(messageSelector);
8073
+ if (messageEl && !messageEl.dataset.vidplyAlertMessageOriginal) {
8074
+ messageEl.dataset.vidplyAlertMessageOriginal = messageEl.textContent.trim();
8075
+ }
8076
+ }
8077
+ restoreMetadataAlertContent(element, config = {}) {
8078
+ if (!element) {
8079
+ return;
8080
+ }
8081
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
8082
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
8083
+ const titleEl = element.querySelector(titleSelector);
8084
+ if (titleEl && titleEl.dataset.vidplyAlertTitleOriginal) {
8085
+ titleEl.textContent = titleEl.dataset.vidplyAlertTitleOriginal;
8086
+ }
8087
+ const messageEl = element.querySelector(messageSelector);
8088
+ if (messageEl && messageEl.dataset.vidplyAlertMessageOriginal) {
8089
+ messageEl.textContent = messageEl.dataset.vidplyAlertMessageOriginal;
8090
+ }
8091
+ }
8092
+ focusMetadataTarget(target, fallbackElement = null) {
8093
+ var _a, _b;
8094
+ if (!target || target === "none") {
8095
+ return;
8096
+ }
8097
+ if (target === "alert" && fallbackElement) {
8098
+ fallbackElement.focus();
8099
+ return;
8100
+ }
8101
+ if (target === "player") {
8102
+ if (this.container) {
8103
+ this.container.focus();
8104
+ }
8105
+ return;
8106
+ }
8107
+ if (target === "media") {
8108
+ this.element.focus();
8109
+ return;
8110
+ }
8111
+ if (target === "playButton") {
8112
+ const playButton = (_b = (_a = this.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.playPause;
8113
+ if (playButton) {
8114
+ playButton.focus();
8115
+ }
8116
+ return;
8117
+ }
8118
+ if (typeof target === "string") {
8119
+ const targetElement = document.querySelector(target);
8120
+ if (targetElement) {
8121
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
8122
+ targetElement.setAttribute("tabindex", "-1");
8123
+ }
8124
+ targetElement.focus();
8125
+ }
8126
+ }
8127
+ }
8128
+ handleMetadataAlert(selector, options = {}) {
8129
+ if (!selector) {
8130
+ return;
8131
+ }
8132
+ const config = this.resolveMetadataConfig(this.options.metadataAlerts, selector) || {};
8133
+ const element = options.element || document.querySelector(selector);
8134
+ if (!element) {
8135
+ if (this.options.debug) {
8136
+ this.log("[Metadata] Alert element not found:", selector);
8137
+ }
8138
+ return;
8139
+ }
8140
+ if (this.options.debug) {
8141
+ this.log("[Metadata] Handling alert", selector, { reason: options.reason, config });
8142
+ }
8143
+ this.cacheMetadataAlertContent(element, config);
8144
+ if (!element.dataset.vidplyAlertOriginalDisplay) {
8145
+ element.dataset.vidplyAlertOriginalDisplay = element.style.display || "";
8146
+ }
8147
+ if (!element.dataset.vidplyAlertDisplay) {
8148
+ element.dataset.vidplyAlertDisplay = config.display || "block";
8149
+ }
8150
+ const shouldShow = options.show !== void 0 ? options.show : config.show !== false;
8151
+ if (shouldShow) {
8152
+ const displayValue = config.display || element.dataset.vidplyAlertDisplay || "block";
8153
+ element.style.display = displayValue;
8154
+ element.hidden = false;
8155
+ element.removeAttribute("hidden");
8156
+ element.setAttribute("aria-hidden", "false");
8157
+ element.setAttribute("data-vidply-alert-active", "true");
8158
+ }
8159
+ const shouldReset = config.resetContent !== false && options.reason === "focus";
8160
+ if (shouldReset) {
8161
+ this.restoreMetadataAlertContent(element, config);
8162
+ }
8163
+ const shouldFocus = options.focus !== void 0 ? options.focus : config.focusOnShow ?? options.reason !== "focus";
8164
+ if (shouldShow && shouldFocus) {
8165
+ if (element.tabIndex === -1 && !element.hasAttribute("tabindex")) {
8166
+ element.setAttribute("tabindex", "-1");
8167
+ }
8168
+ element.focus();
8169
+ }
8170
+ if (shouldShow && config.autoScroll !== false && options.autoScroll !== false) {
8171
+ element.scrollIntoView({ behavior: "smooth", block: "nearest" });
8172
+ }
8173
+ const continueSelector = config.continueButton;
8174
+ if (continueSelector) {
8175
+ let continueButton = null;
8176
+ if (continueSelector === "self") {
8177
+ continueButton = element;
8178
+ } else if (element.matches(continueSelector)) {
8179
+ continueButton = element;
8180
+ } else {
8181
+ continueButton = element.querySelector(continueSelector) || document.querySelector(continueSelector);
8182
+ }
8183
+ if (continueButton && !this.metadataAlertHandlers.has(selector)) {
8184
+ const handler = () => {
8185
+ const hideOnContinue = config.hideOnContinue !== false;
8186
+ if (hideOnContinue) {
8187
+ const originalDisplay = element.dataset.vidplyAlertOriginalDisplay || "";
8188
+ element.style.display = config.hideDisplay || originalDisplay || "none";
8189
+ element.setAttribute("aria-hidden", "true");
8190
+ element.removeAttribute("data-vidply-alert-active");
8191
+ }
8192
+ if (config.resume !== false && this.state.paused) {
8193
+ this.play();
8194
+ }
8195
+ const focusTarget = config.focusTarget || "playButton";
8196
+ this.setManagedTimeout(() => {
8197
+ this.focusMetadataTarget(focusTarget, element);
8198
+ }, config.focusDelay ?? 100);
8199
+ };
8200
+ continueButton.addEventListener("click", handler);
8201
+ this.metadataAlertHandlers.set(selector, { button: continueButton, handler });
8202
+ }
8203
+ }
8204
+ return element;
8205
+ }
8206
+ handleMetadataHashtags(hashtags) {
8207
+ if (!Array.isArray(hashtags) || hashtags.length === 0) {
8208
+ return;
8209
+ }
8210
+ const configMap = this.options.metadataHashtags;
8211
+ if (!configMap) {
8212
+ return;
8213
+ }
8214
+ hashtags.forEach((tag) => {
8215
+ const config = this.resolveMetadataConfig(configMap, tag);
8216
+ if (!config) {
8217
+ return;
8218
+ }
8219
+ const selector = this.normalizeMetadataSelector(config.alert || config.selector || config.target);
8220
+ if (!selector) {
8221
+ return;
8222
+ }
8223
+ const element = document.querySelector(selector);
8224
+ if (!element) {
8225
+ if (this.options.debug) {
8226
+ this.log("[Metadata] Hashtag target not found:", selector);
8227
+ }
8228
+ return;
8229
+ }
8230
+ if (this.options.debug) {
8231
+ this.log("[Metadata] Handling hashtag", tag, { selector, config });
8232
+ }
8233
+ this.cacheMetadataAlertContent(element, config);
8234
+ if (config.title) {
8235
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
8236
+ const titleEl = element.querySelector(titleSelector);
8237
+ if (titleEl) {
8238
+ titleEl.textContent = config.title;
8239
+ }
8240
+ }
8241
+ if (config.message) {
8242
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
8243
+ const messageEl = element.querySelector(messageSelector);
8244
+ if (messageEl) {
8245
+ messageEl.textContent = config.message;
8246
+ }
8247
+ }
8248
+ const show = config.show !== false;
8249
+ const focus = config.focus !== void 0 ? config.focus : false;
8250
+ this.handleMetadataAlert(selector, {
8251
+ element,
8252
+ show,
8253
+ focus,
8254
+ autoScroll: config.autoScroll,
8255
+ reason: "hashtag"
8256
+ });
8257
+ });
8258
+ }
8259
+ /**
8260
+ * Handle individual metadata cues
8261
+ * Parses metadata text and emits events or triggers actions
8262
+ */
8263
+ handleMetadataCue(cue) {
8264
+ const text = cue.text.trim();
8265
+ if (this.options.debug) {
8266
+ this.log("[Metadata] Processing cue:", {
8267
+ time: cue.startTime,
8268
+ text
8269
+ });
8270
+ }
8271
+ this.emit("metadata", {
8272
+ time: cue.startTime,
8273
+ endTime: cue.endTime,
8274
+ text,
8275
+ cue
8276
+ });
8277
+ if (text.includes("PAUSE")) {
8278
+ if (!this.state.paused) {
8279
+ if (this.options.debug) {
8280
+ this.log("[Metadata] Pausing video at", cue.startTime);
8281
+ }
8282
+ this.pause();
8283
+ }
8284
+ this.emit("metadata:pause", { time: cue.startTime, text });
8285
+ }
8286
+ const focusMatch = text.match(/FOCUS:([\w#-]+)/);
8287
+ if (focusMatch) {
8288
+ const targetSelector = focusMatch[1];
8289
+ const normalizedSelector = this.normalizeMetadataSelector(targetSelector);
8290
+ const targetElement = normalizedSelector ? document.querySelector(normalizedSelector) : null;
8291
+ if (targetElement) {
8292
+ if (this.options.debug) {
8293
+ this.log("[Metadata] Focusing element:", normalizedSelector);
8294
+ }
8295
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
8296
+ targetElement.setAttribute("tabindex", "-1");
8297
+ }
8298
+ this.setManagedTimeout(() => {
8299
+ targetElement.focus();
8300
+ targetElement.scrollIntoView({ behavior: "smooth", block: "nearest" });
8301
+ }, 10);
8302
+ } else if (this.options.debug) {
8303
+ this.log("[Metadata] Element not found:", normalizedSelector || targetSelector);
8304
+ }
8305
+ this.emit("metadata:focus", {
8306
+ time: cue.startTime,
8307
+ target: targetSelector,
8308
+ selector: normalizedSelector,
8309
+ element: targetElement,
8310
+ text
8311
+ });
8312
+ if (normalizedSelector) {
8313
+ this.handleMetadataAlert(normalizedSelector, {
8314
+ element: targetElement,
8315
+ reason: "focus"
8316
+ });
8317
+ }
8318
+ }
8319
+ const hashtags = text.match(/#[\w-]+/g);
8320
+ if (hashtags) {
8321
+ if (this.options.debug) {
8322
+ this.log("[Metadata] Hashtags found:", hashtags);
8323
+ }
8324
+ this.emit("metadata:hashtags", {
8325
+ time: cue.startTime,
8326
+ hashtags,
8327
+ text
8328
+ });
8329
+ this.handleMetadataHashtags(hashtags);
8330
+ }
8331
+ }
7196
8332
  };
7197
8333
  Player.instances = [];
7198
8334