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.
@@ -497,10 +497,20 @@ var translations = {
497
497
  settings: "Transcript settings. Press Enter to open menu, or D to enable drag mode",
498
498
  keyboardDragMode: "Toggle keyboard drag mode with arrow keys. Shortcut: D key",
499
499
  keyboardDragActive: "\u2328\uFE0F Keyboard Drag Mode Active (Arrow keys to move, Shift+Arrows for large steps, D or ESC to exit)",
500
+ dragResizePrompt: "Press D to drag or R to resize. Use Home to reset position, Esc to close.",
501
+ dragModeEnabled: "Keyboard drag mode enabled. Use arrow keys to move, Shift+Arrow for larger steps. Press D or Esc to exit.",
502
+ dragModeDisabled: "Keyboard drag mode disabled.",
500
503
  resizeWindow: "Resize Window",
504
+ disableResizeWindow: "Disable Resize Mode",
505
+ resizeModeHint: "Resize handles enabled. Drag edges or corners to adjust. Press Esc or R to exit.",
506
+ resizeModeEnabled: "Resize mode enabled. Drag edges or corners to adjust. Press Esc or R to exit.",
507
+ resizeModeDisabled: "Resize mode disabled.",
508
+ positionReset: "Transcript position reset.",
501
509
  styleTranscript: "Open transcript style settings",
502
510
  closeMenu: "Close Menu",
503
- styleTitle: "Transcript Style"
511
+ styleTitle: "Transcript Style",
512
+ autoscroll: "Autoscroll",
513
+ settingsMenu: "Settings menu"
504
514
  },
505
515
  settings: {
506
516
  title: "Settings",
@@ -618,10 +628,20 @@ var translations = {
618
628
  settings: "Transkript-Einstellungen. Eingabetaste zum \xD6ffnen des Men\xFCs dr\xFCcken oder D zum Aktivieren des Verschiebemodus",
619
629
  keyboardDragMode: "Tastatur-Verschiebemodus mit Pfeiltasten umschalten. Tastenkombination: D-Taste",
620
630
  keyboardDragActive: "\u2328\uFE0F Tastatur-Verschiebemodus aktiv (Pfeiltasten zum Bewegen, Umschalt+Pfeiltasten f\xFCr gro\xDFe Schritte, D oder ESC zum Beenden)",
631
+ dragResizePrompt: "Dr\xFCcken Sie D zum Verschieben oder R zur Gr\xF6\xDFen\xE4nderung. Home setzt die Position zur\xFCck, Esc schlie\xDFt.",
632
+ dragModeEnabled: "Tastatur-Verschiebemodus aktiviert. Pfeiltasten zum Bewegen, Umschalt+Pfeiltasten f\xFCr gr\xF6\xDFere Schritte. D oder Esc zum Beenden.",
633
+ dragModeDisabled: "Tastatur-Verschiebemodus deaktiviert.",
621
634
  resizeWindow: "Fenster vergr\xF6\xDFern/verkleinern",
635
+ disableResizeWindow: "Resize-Modus deaktivieren",
636
+ resizeModeHint: "Griffe aktiviert. Ziehen Sie Kanten oder Ecken zum Anpassen. Esc oder R zum Beenden.",
637
+ resizeModeEnabled: "Resize-Modus aktiviert. Kanten oder Ecken ziehen; Esc oder R beendet.",
638
+ resizeModeDisabled: "Resize-Modus deaktiviert.",
639
+ positionReset: "Transkriptposition zur\xFCckgesetzt.",
622
640
  styleTranscript: "Transkript-Stileinstellungen \xF6ffnen",
623
641
  closeMenu: "Men\xFC schlie\xDFen",
624
- styleTitle: "Transkript-Stil"
642
+ styleTitle: "Transkript-Stil",
643
+ autoscroll: "Automatisches Scrollen",
644
+ settingsMenu: "Einstellungsmen\xFC"
625
645
  },
626
646
  settings: {
627
647
  title: "Einstellungen",
@@ -739,10 +759,20 @@ var translations = {
739
759
  settings: "Configuraci\xF3n de transcripci\xF3n. Presione Enter para abrir el men\xFA o D para activar el modo de arrastre",
740
760
  keyboardDragMode: "Alternar modo de arrastre con teclado usando teclas de flecha. Atajo: tecla D",
741
761
  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)",
762
+ dragResizePrompt: "Pulsa D para mover o R para cambiar el tama\xF1o. Home restablece la posici\xF3n; Esc cierra.",
763
+ dragModeEnabled: "Modo de arrastre con teclado activado. Usa flechas para mover, May\xFAs+Flechas para pasos grandes. Pulsa D o Esc para salir.",
764
+ dragModeDisabled: "Modo de arrastre con teclado desactivado.",
742
765
  resizeWindow: "Cambiar tama\xF1o de ventana",
766
+ disableResizeWindow: "Desactivar modo de cambio de tama\xF1o",
767
+ resizeModeHint: "Controladores habilitados. Arrastra bordes o esquinas para ajustar. Pulsa Esc o R para salir.",
768
+ resizeModeEnabled: "Modo de cambio de tama\xF1o activado. Arrastra bordes o esquinas. Pulsa Esc o R para salir.",
769
+ resizeModeDisabled: "Modo de cambio de tama\xF1o desactivado.",
770
+ positionReset: "Posici\xF3n de la transcripci\xF3n restablecida.",
743
771
  styleTranscript: "Abrir configuraci\xF3n de estilo de transcripci\xF3n",
744
772
  closeMenu: "Cerrar men\xFA",
745
- styleTitle: "Estilo de Transcripci\xF3n"
773
+ styleTitle: "Estilo de Transcripci\xF3n",
774
+ autoscroll: "Desplazamiento autom\xE1tico",
775
+ settingsMenu: "Men\xFA de configuraci\xF3n"
746
776
  },
747
777
  settings: {
748
778
  title: "Configuraci\xF3n",
@@ -860,10 +890,20 @@ var translations = {
860
890
  settings: "Param\xE8tres de transcription. Appuyez sur Entr\xE9e pour ouvrir le menu ou D pour activer le mode glissement",
861
891
  keyboardDragMode: "Basculer le mode glissement avec les touches fl\xE9ch\xE9es. Raccourci: touche D",
862
892
  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)",
893
+ dragResizePrompt: "Appuyez sur D pour d\xE9placer ou R pour redimensionner. Home r\xE9initialise la position, \xC9chap ferme.",
894
+ 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.",
895
+ dragModeDisabled: "Mode glissement clavier d\xE9sactiv\xE9.",
863
896
  resizeWindow: "Redimensionner la fen\xEAtre",
897
+ disableResizeWindow: "D\xE9sactiver le mode de redimensionnement",
898
+ resizeModeHint: "Poign\xE9es activ\xE9es. Faites glisser les bords ou les coins pour ajuster. Appuyez sur \xC9chap ou R pour quitter.",
899
+ resizeModeEnabled: "Mode redimensionnement activ\xE9. Faites glisser les bords ou coins. Appuyez sur \xC9chap ou R pour quitter.",
900
+ resizeModeDisabled: "Mode redimensionnement d\xE9sactiv\xE9.",
901
+ positionReset: "Position de la transcription r\xE9initialis\xE9e.",
864
902
  styleTranscript: "Ouvrir les param\xE8tres de style de transcription",
865
903
  closeMenu: "Fermer le menu",
866
- styleTitle: "Style de Transcription"
904
+ styleTitle: "Style de Transcription",
905
+ autoscroll: "D\xE9filement automatique",
906
+ settingsMenu: "Menu des param\xE8tres"
867
907
  },
868
908
  settings: {
869
909
  title: "Param\xE8tres",
@@ -981,10 +1021,20 @@ var translations = {
981
1021
  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",
982
1022
  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",
983
1023
  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",
1024
+ 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",
1025
+ 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",
1026
+ dragModeDisabled: "\u30AD\u30FC\u30DC\u30FC\u30C9\u30C9\u30E9\u30C3\u30B0\u30E2\u30FC\u30C9\u3092\u7121\u52B9\u306B\u3057\u307E\u3057\u305F\u3002",
984
1027
  resizeWindow: "\u30A6\u30A3\u30F3\u30C9\u30A6\u306E\u30B5\u30A4\u30BA\u5909\u66F4",
1028
+ disableResizeWindow: "\u30B5\u30A4\u30BA\u5909\u66F4\u30E2\u30FC\u30C9\u3092\u7121\u52B9\u306B\u3059\u308B",
1029
+ 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",
1030
+ 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",
1031
+ resizeModeDisabled: "\u30B5\u30A4\u30BA\u5909\u66F4\u30E2\u30FC\u30C9\u3092\u7121\u52B9\u306B\u3057\u307E\u3057\u305F\u3002",
1032
+ positionReset: "\u6587\u5B57\u8D77\u3053\u3057\u306E\u4F4D\u7F6E\u3092\u30EA\u30BB\u30C3\u30C8\u3057\u307E\u3057\u305F\u3002",
985
1033
  styleTranscript: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB\u8A2D\u5B9A\u3092\u958B\u304F",
986
1034
  closeMenu: "\u30E1\u30CB\u30E5\u30FC\u3092\u9589\u3058\u308B",
987
- styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB"
1035
+ styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB",
1036
+ autoscroll: "\u81EA\u52D5\u30B9\u30AF\u30ED\u30FC\u30EB",
1037
+ settingsMenu: "\u8A2D\u5B9A\u30E1\u30CB\u30E5\u30FC"
988
1038
  },
989
1039
  settings: {
990
1040
  title: "\u8A2D\u5B9A",
@@ -1163,8 +1213,8 @@ var iconPaths = {
1163
1213
  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"/>`,
1164
1214
  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"/>`,
1165
1215
  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"/>`,
1166
- 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>`,
1167
- 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>`,
1216
+ 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>`,
1217
+ 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>`,
1168
1218
  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>`,
1169
1219
  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>`,
1170
1220
  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"/>`,
@@ -1772,10 +1822,7 @@ var ControlBar = class {
1772
1822
  }
1773
1823
  });
1774
1824
  this.controls.currentTimeDisplay = DOMUtils.createElement("span", {
1775
- className: `${this.player.options.classPrefix}-current-time`,
1776
- attributes: {
1777
- "aria-label": i18n.t("time.seconds", { count: 0 })
1778
- }
1825
+ className: `${this.player.options.classPrefix}-current-time`
1779
1826
  });
1780
1827
  const currentTimeVisual = DOMUtils.createElement("span", {
1781
1828
  textContent: "00:00",
@@ -1783,8 +1830,14 @@ var ControlBar = class {
1783
1830
  "aria-hidden": "true"
1784
1831
  }
1785
1832
  });
1833
+ const currentTimeAccessible = DOMUtils.createElement("span", {
1834
+ className: "vidply-sr-only",
1835
+ textContent: i18n.t("time.seconds", { count: 0 })
1836
+ });
1786
1837
  this.controls.currentTimeDisplay.appendChild(currentTimeVisual);
1838
+ this.controls.currentTimeDisplay.appendChild(currentTimeAccessible);
1787
1839
  this.controls.currentTimeVisual = currentTimeVisual;
1840
+ this.controls.currentTimeAccessible = currentTimeAccessible;
1788
1841
  const separator = DOMUtils.createElement("span", {
1789
1842
  textContent: " / ",
1790
1843
  attributes: {
@@ -1792,10 +1845,7 @@ var ControlBar = class {
1792
1845
  }
1793
1846
  });
1794
1847
  this.controls.durationDisplay = DOMUtils.createElement("span", {
1795
- className: `${this.player.options.classPrefix}-duration`,
1796
- attributes: {
1797
- "aria-label": i18n.t("time.durationPrefix") + i18n.t("time.seconds", { count: 0 })
1798
- }
1848
+ className: `${this.player.options.classPrefix}-duration`
1799
1849
  });
1800
1850
  const durationVisual = DOMUtils.createElement("span", {
1801
1851
  textContent: "00:00",
@@ -1803,8 +1853,14 @@ var ControlBar = class {
1803
1853
  "aria-hidden": "true"
1804
1854
  }
1805
1855
  });
1856
+ const durationAccessible = DOMUtils.createElement("span", {
1857
+ className: "vidply-sr-only",
1858
+ textContent: i18n.t("time.durationPrefix") + i18n.t("time.seconds", { count: 0 })
1859
+ });
1806
1860
  this.controls.durationDisplay.appendChild(durationVisual);
1861
+ this.controls.durationDisplay.appendChild(durationAccessible);
1807
1862
  this.controls.durationVisual = durationVisual;
1863
+ this.controls.durationAccessible = durationAccessible;
1808
1864
  container.appendChild(this.controls.currentTimeDisplay);
1809
1865
  container.appendChild(separator);
1810
1866
  container.appendChild(this.controls.durationDisplay);
@@ -2631,14 +2687,18 @@ var ControlBar = class {
2631
2687
  if (this.controls.currentTimeVisual) {
2632
2688
  const currentTime = this.player.state.currentTime;
2633
2689
  this.controls.currentTimeVisual.textContent = TimeUtils.formatTime(currentTime);
2634
- this.controls.currentTimeDisplay.setAttribute("aria-label", TimeUtils.formatDuration(currentTime));
2690
+ if (this.controls.currentTimeAccessible) {
2691
+ this.controls.currentTimeAccessible.textContent = TimeUtils.formatDuration(currentTime);
2692
+ }
2635
2693
  }
2636
2694
  }
2637
2695
  updateDuration() {
2638
2696
  if (this.controls.durationVisual) {
2639
2697
  const duration = this.player.state.duration;
2640
2698
  this.controls.durationVisual.textContent = TimeUtils.formatTime(duration);
2641
- this.controls.durationDisplay.setAttribute("aria-label", i18n.t("time.durationPrefix") + TimeUtils.formatDuration(duration));
2699
+ if (this.controls.durationAccessible) {
2700
+ this.controls.durationAccessible.textContent = i18n.t("time.durationPrefix") + TimeUtils.formatDuration(duration);
2701
+ }
2642
2702
  }
2643
2703
  }
2644
2704
  updateVolumeDisplay() {
@@ -3293,6 +3353,596 @@ var KeyboardManager = class {
3293
3353
  }
3294
3354
  };
3295
3355
 
3356
+ // src/utils/DraggableResizable.js
3357
+ var DraggableResizable = class {
3358
+ constructor(element, options = {}) {
3359
+ this.element = element;
3360
+ this.options = {
3361
+ dragHandle: null,
3362
+ // Element to use as drag handle (defaults to element itself)
3363
+ resizeHandles: [],
3364
+ // Array of resize handle elements
3365
+ onDragStart: null,
3366
+ onDrag: null,
3367
+ onDragEnd: null,
3368
+ onResizeStart: null,
3369
+ onResize: null,
3370
+ onResizeEnd: null,
3371
+ constrainToViewport: true,
3372
+ // Allow movement outside viewport?
3373
+ minWidth: 150,
3374
+ minHeight: 100,
3375
+ maintainAspectRatio: false,
3376
+ keyboardDragKey: "d",
3377
+ keyboardResizeKey: "r",
3378
+ keyboardStep: 5,
3379
+ keyboardStepLarge: 10,
3380
+ maxWidth: null,
3381
+ maxHeight: null,
3382
+ pointerResizeIndicatorText: null,
3383
+ onPointerResizeToggle: null,
3384
+ classPrefix: "draggable",
3385
+ storage: null,
3386
+ // StorageManager instance for saving position/size
3387
+ storageKey: null,
3388
+ // Key for localStorage (if storage is provided)
3389
+ ...options
3390
+ };
3391
+ this.isDragging = false;
3392
+ this.isResizing = false;
3393
+ this.resizeDirection = null;
3394
+ this.dragOffsetX = 0;
3395
+ this.dragOffsetY = 0;
3396
+ this.positionOffsetX = 0;
3397
+ this.positionOffsetY = 0;
3398
+ this.initialMouseX = 0;
3399
+ this.initialMouseY = 0;
3400
+ this.needsPositionConversion = false;
3401
+ this.resizeStartX = 0;
3402
+ this.resizeStartY = 0;
3403
+ this.resizeStartWidth = 0;
3404
+ this.resizeStartHeight = 0;
3405
+ this.resizeStartLeft = 0;
3406
+ this.resizeStartTop = 0;
3407
+ this.keyboardDragMode = false;
3408
+ this.keyboardResizeMode = false;
3409
+ this.pointerResizeMode = false;
3410
+ this.manuallyPositioned = false;
3411
+ this.resizeHandlesManaged = /* @__PURE__ */ new Map();
3412
+ this.resizeIndicatorElement = null;
3413
+ this.handlers = {
3414
+ mousedown: this.onMouseDown.bind(this),
3415
+ mousemove: this.onMouseMove.bind(this),
3416
+ mouseup: this.onMouseUp.bind(this),
3417
+ touchstart: this.onTouchStart.bind(this),
3418
+ touchmove: this.onTouchMove.bind(this),
3419
+ touchend: this.onTouchEnd.bind(this),
3420
+ keydown: this.onKeyDown.bind(this),
3421
+ resizeHandleMousedown: this.onResizeHandleMouseDown.bind(this)
3422
+ };
3423
+ this.init();
3424
+ }
3425
+ hasManagedResizeHandles() {
3426
+ return Array.from(this.resizeHandlesManaged.values()).some(Boolean);
3427
+ }
3428
+ storeOriginalHandleDisplay(handle) {
3429
+ if (!handle.dataset.originalDisplay) {
3430
+ handle.dataset.originalDisplay = handle.style.display || "";
3431
+ }
3432
+ }
3433
+ hideResizeHandle(handle) {
3434
+ handle.style.display = "none";
3435
+ handle.setAttribute("aria-hidden", "true");
3436
+ }
3437
+ showResizeHandle(handle) {
3438
+ const original = handle.dataset.originalDisplay !== void 0 ? handle.dataset.originalDisplay : "";
3439
+ handle.style.display = original;
3440
+ handle.removeAttribute("aria-hidden");
3441
+ }
3442
+ setManagedHandlesVisible(visible) {
3443
+ if (!this.options.resizeHandles || this.options.resizeHandles.length === 0) {
3444
+ return;
3445
+ }
3446
+ this.options.resizeHandles.forEach((handle) => {
3447
+ if (!this.resizeHandlesManaged.get(handle)) {
3448
+ return;
3449
+ }
3450
+ if (visible) {
3451
+ this.showResizeHandle(handle);
3452
+ } else {
3453
+ this.hideResizeHandle(handle);
3454
+ }
3455
+ });
3456
+ }
3457
+ init() {
3458
+ const dragHandle = this.options.dragHandle || this.element;
3459
+ dragHandle.addEventListener("mousedown", this.handlers.mousedown);
3460
+ dragHandle.addEventListener("touchstart", this.handlers.touchstart);
3461
+ document.addEventListener("mousemove", this.handlers.mousemove);
3462
+ document.addEventListener("mouseup", this.handlers.mouseup);
3463
+ document.addEventListener("touchmove", this.handlers.touchmove, { passive: false });
3464
+ document.addEventListener("touchend", this.handlers.touchend);
3465
+ this.element.addEventListener("keydown", this.handlers.keydown);
3466
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
3467
+ this.options.resizeHandles.forEach((handle) => {
3468
+ handle.addEventListener("mousedown", this.handlers.resizeHandleMousedown);
3469
+ handle.addEventListener("touchstart", this.handlers.resizeHandleMousedown);
3470
+ const managed = handle.dataset.vidplyManagedResize === "true";
3471
+ this.resizeHandlesManaged.set(handle, managed);
3472
+ if (managed) {
3473
+ this.storeOriginalHandleDisplay(handle);
3474
+ this.hideResizeHandle(handle);
3475
+ }
3476
+ });
3477
+ }
3478
+ }
3479
+ onMouseDown(e) {
3480
+ if (e.target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
3481
+ return;
3482
+ }
3483
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
3484
+ return;
3485
+ }
3486
+ this.startDragging(e.clientX, e.clientY);
3487
+ e.preventDefault();
3488
+ }
3489
+ onTouchStart(e) {
3490
+ if (e.target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
3491
+ return;
3492
+ }
3493
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
3494
+ return;
3495
+ }
3496
+ const touch = e.touches[0];
3497
+ this.startDragging(touch.clientX, touch.clientY);
3498
+ }
3499
+ onResizeHandleMouseDown(e) {
3500
+ var _a, _b, _c, _d;
3501
+ e.preventDefault();
3502
+ e.stopPropagation();
3503
+ const handle = e.target;
3504
+ this.resizeDirection = handle.getAttribute("data-direction");
3505
+ const clientX = e.clientX || ((_b = (_a = e.touches) == null ? void 0 : _a[0]) == null ? void 0 : _b.clientX);
3506
+ const clientY = e.clientY || ((_d = (_c = e.touches) == null ? void 0 : _c[0]) == null ? void 0 : _d.clientY);
3507
+ this.startResizing(clientX, clientY);
3508
+ }
3509
+ onMouseMove(e) {
3510
+ if (this.isDragging) {
3511
+ this.drag(e.clientX, e.clientY);
3512
+ e.preventDefault();
3513
+ } else if (this.isResizing) {
3514
+ this.resize(e.clientX, e.clientY);
3515
+ e.preventDefault();
3516
+ }
3517
+ }
3518
+ onTouchMove(e) {
3519
+ if (this.isDragging || this.isResizing) {
3520
+ const touch = e.touches[0];
3521
+ if (this.isDragging) {
3522
+ this.drag(touch.clientX, touch.clientY);
3523
+ } else {
3524
+ this.resize(touch.clientX, touch.clientY);
3525
+ }
3526
+ e.preventDefault();
3527
+ }
3528
+ }
3529
+ onMouseUp() {
3530
+ if (this.isDragging) {
3531
+ this.stopDragging();
3532
+ } else if (this.isResizing) {
3533
+ this.stopResizing();
3534
+ }
3535
+ }
3536
+ onTouchEnd() {
3537
+ if (this.isDragging) {
3538
+ this.stopDragging();
3539
+ } else if (this.isResizing) {
3540
+ this.stopResizing();
3541
+ }
3542
+ }
3543
+ onKeyDown(e) {
3544
+ if (e.key.toLowerCase() === this.options.keyboardDragKey.toLowerCase()) {
3545
+ e.preventDefault();
3546
+ this.toggleKeyboardDragMode();
3547
+ return;
3548
+ }
3549
+ if (e.key.toLowerCase() === this.options.keyboardResizeKey.toLowerCase()) {
3550
+ e.preventDefault();
3551
+ if (this.hasManagedResizeHandles()) {
3552
+ this.togglePointerResizeMode();
3553
+ } else {
3554
+ this.toggleKeyboardResizeMode();
3555
+ }
3556
+ return;
3557
+ }
3558
+ if (e.key === "Escape") {
3559
+ if (this.pointerResizeMode) {
3560
+ e.preventDefault();
3561
+ this.disablePointerResizeMode();
3562
+ return;
3563
+ }
3564
+ if (this.keyboardDragMode || this.keyboardResizeMode) {
3565
+ e.preventDefault();
3566
+ this.disableKeyboardDragMode();
3567
+ this.disableKeyboardResizeMode();
3568
+ return;
3569
+ }
3570
+ }
3571
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
3572
+ if (this.keyboardDragMode) {
3573
+ e.preventDefault();
3574
+ e.stopPropagation();
3575
+ this.keyboardDrag(e.key, e.shiftKey);
3576
+ } else if (this.keyboardResizeMode) {
3577
+ e.preventDefault();
3578
+ e.stopPropagation();
3579
+ this.keyboardResize(e.key, e.shiftKey);
3580
+ }
3581
+ }
3582
+ if (e.key === "Home" && (this.keyboardDragMode || this.keyboardResizeMode)) {
3583
+ e.preventDefault();
3584
+ this.resetPosition();
3585
+ }
3586
+ }
3587
+ startDragging(clientX, clientY) {
3588
+ const rect = this.element.getBoundingClientRect();
3589
+ const computedStyle = window.getComputedStyle(this.element);
3590
+ const needsConversion = computedStyle.right !== "auto" || computedStyle.bottom !== "auto" || computedStyle.transform !== "none";
3591
+ this.positionOffsetX = 0;
3592
+ this.positionOffsetY = 0;
3593
+ if (needsConversion) {
3594
+ let targetLeft, targetTop;
3595
+ if (computedStyle.position === "absolute") {
3596
+ const offsetParent = this.element.offsetParent || document.body;
3597
+ const parentRect = offsetParent.getBoundingClientRect();
3598
+ targetLeft = rect.left - parentRect.left;
3599
+ targetTop = rect.top - parentRect.top;
3600
+ this.positionOffsetX = parentRect.left;
3601
+ this.positionOffsetY = parentRect.top;
3602
+ } else if (computedStyle.position === "fixed") {
3603
+ const parsedLeft = parseFloat(computedStyle.left);
3604
+ const parsedTop = parseFloat(computedStyle.top);
3605
+ const hasLeft = Number.isFinite(parsedLeft);
3606
+ const hasTop = Number.isFinite(parsedTop);
3607
+ targetLeft = hasLeft ? parsedLeft : rect.left;
3608
+ targetTop = hasTop ? parsedTop : rect.top;
3609
+ this.positionOffsetX = rect.left - targetLeft;
3610
+ this.positionOffsetY = rect.top - targetTop;
3611
+ } else {
3612
+ targetLeft = rect.left;
3613
+ targetTop = rect.top;
3614
+ this.positionOffsetX = rect.left - targetLeft;
3615
+ this.positionOffsetY = rect.top - targetTop;
3616
+ }
3617
+ const currentCssText = this.element.style.cssText;
3618
+ let newCssText = currentCssText.split(";").filter((rule) => {
3619
+ const trimmed = rule.trim();
3620
+ return trimmed && !trimmed.startsWith("right:") && !trimmed.startsWith("bottom:") && !trimmed.startsWith("transform:") && !trimmed.startsWith("left:") && !trimmed.startsWith("top:") && !trimmed.startsWith("inset:");
3621
+ }).join("; ");
3622
+ if (newCssText) newCssText += "; ";
3623
+ newCssText += `left: ${targetLeft}px; top: ${targetTop}px; right: auto; bottom: auto; transform: none`;
3624
+ this.element.style.cssText = newCssText;
3625
+ }
3626
+ const finalRect = this.element.getBoundingClientRect();
3627
+ this.dragOffsetX = clientX - finalRect.left;
3628
+ this.dragOffsetY = clientY - finalRect.top;
3629
+ this.isDragging = true;
3630
+ this.element.classList.add(`${this.options.classPrefix}-dragging`);
3631
+ document.body.style.cursor = "grabbing";
3632
+ document.body.style.userSelect = "none";
3633
+ }
3634
+ drag(clientX, clientY) {
3635
+ if (!this.isDragging) return;
3636
+ let newX = clientX - this.dragOffsetX - this.positionOffsetX;
3637
+ let newY = clientY - this.dragOffsetY - this.positionOffsetY;
3638
+ if (this.options.constrainToViewport) {
3639
+ const rect = this.element.getBoundingClientRect();
3640
+ const viewportWidth = document.documentElement.clientWidth;
3641
+ const viewportHeight = document.documentElement.clientHeight;
3642
+ const minVisible = 100;
3643
+ const minX = -(rect.width - minVisible);
3644
+ const minY = -(rect.height - minVisible);
3645
+ const maxX = viewportWidth - minVisible;
3646
+ const maxY = viewportHeight - minVisible;
3647
+ newX = Math.max(minX, Math.min(newX, maxX));
3648
+ newY = Math.max(minY, Math.min(newY, maxY));
3649
+ }
3650
+ this.element.style.left = `${newX}px`;
3651
+ this.element.style.top = `${newY}px`;
3652
+ if (this.options.onDrag) {
3653
+ this.options.onDrag({ x: newX, y: newY });
3654
+ }
3655
+ }
3656
+ stopDragging() {
3657
+ this.isDragging = false;
3658
+ this.element.classList.remove(`${this.options.classPrefix}-dragging`);
3659
+ document.body.style.cursor = "";
3660
+ document.body.style.userSelect = "";
3661
+ this.manuallyPositioned = true;
3662
+ if (this.options.onDragEnd) {
3663
+ this.options.onDragEnd();
3664
+ }
3665
+ }
3666
+ startResizing(clientX, clientY) {
3667
+ this.isResizing = true;
3668
+ this.resizeStartX = clientX;
3669
+ this.resizeStartY = clientY;
3670
+ const rect = this.element.getBoundingClientRect();
3671
+ this.resizeStartWidth = rect.width;
3672
+ this.resizeStartHeight = rect.height;
3673
+ this.resizeStartLeft = rect.left;
3674
+ this.resizeStartTop = rect.top;
3675
+ this.element.classList.add(`${this.options.classPrefix}-resizing`);
3676
+ document.body.style.userSelect = "none";
3677
+ if (this.options.onResizeStart) {
3678
+ this.options.onResizeStart();
3679
+ }
3680
+ }
3681
+ resize(clientX, clientY) {
3682
+ if (!this.isResizing) return;
3683
+ const deltaX = clientX - this.resizeStartX;
3684
+ const deltaY = clientY - this.resizeStartY;
3685
+ let newWidth = this.resizeStartWidth;
3686
+ let newHeight = this.resizeStartHeight;
3687
+ let newLeft = this.resizeStartLeft;
3688
+ let newTop = this.resizeStartTop;
3689
+ if (this.resizeDirection.includes("e")) {
3690
+ newWidth = Math.max(this.options.minWidth, this.resizeStartWidth + deltaX);
3691
+ }
3692
+ if (this.resizeDirection.includes("w")) {
3693
+ const proposedWidth = Math.max(this.options.minWidth, this.resizeStartWidth - deltaX);
3694
+ newLeft = this.resizeStartLeft + (this.resizeStartWidth - proposedWidth);
3695
+ newWidth = proposedWidth;
3696
+ }
3697
+ const maxWidthOption = typeof this.options.maxWidth === "function" ? this.options.maxWidth() : this.options.maxWidth;
3698
+ if (Number.isFinite(maxWidthOption)) {
3699
+ const clampedWidth = Math.min(newWidth, maxWidthOption);
3700
+ if (clampedWidth !== newWidth && this.resizeDirection.includes("w")) {
3701
+ newLeft += newWidth - clampedWidth;
3702
+ }
3703
+ newWidth = clampedWidth;
3704
+ }
3705
+ if (!this.options.maintainAspectRatio) {
3706
+ if (this.resizeDirection.includes("s")) {
3707
+ newHeight = Math.max(this.options.minHeight, this.resizeStartHeight + deltaY);
3708
+ }
3709
+ if (this.resizeDirection.includes("n")) {
3710
+ const proposedHeight = Math.max(this.options.minHeight, this.resizeStartHeight - deltaY);
3711
+ newTop = this.resizeStartTop + (this.resizeStartHeight - proposedHeight);
3712
+ newHeight = proposedHeight;
3713
+ }
3714
+ const maxHeightOption = typeof this.options.maxHeight === "function" ? this.options.maxHeight() : this.options.maxHeight;
3715
+ if (Number.isFinite(maxHeightOption)) {
3716
+ const clampedHeight = Math.min(newHeight, maxHeightOption);
3717
+ if (clampedHeight !== newHeight && this.resizeDirection.includes("n")) {
3718
+ newTop += newHeight - clampedHeight;
3719
+ }
3720
+ newHeight = clampedHeight;
3721
+ }
3722
+ }
3723
+ this.element.style.width = `${newWidth}px`;
3724
+ if (!this.options.maintainAspectRatio) {
3725
+ this.element.style.height = `${newHeight}px`;
3726
+ } else {
3727
+ this.element.style.height = "auto";
3728
+ }
3729
+ if (this.resizeDirection.includes("w")) {
3730
+ this.element.style.left = `${newLeft}px`;
3731
+ }
3732
+ if (this.resizeDirection.includes("n") && !this.options.maintainAspectRatio) {
3733
+ this.element.style.top = `${newTop}px`;
3734
+ }
3735
+ if (this.options.onResize) {
3736
+ this.options.onResize({ width: newWidth, height: newHeight, left: newLeft, top: newTop });
3737
+ }
3738
+ }
3739
+ stopResizing() {
3740
+ this.isResizing = false;
3741
+ this.resizeDirection = null;
3742
+ this.element.classList.remove(`${this.options.classPrefix}-resizing`);
3743
+ document.body.style.userSelect = "";
3744
+ this.manuallyPositioned = true;
3745
+ if (this.options.onResizeEnd) {
3746
+ this.options.onResizeEnd();
3747
+ }
3748
+ }
3749
+ toggleKeyboardDragMode() {
3750
+ if (this.keyboardDragMode) {
3751
+ this.disableKeyboardDragMode();
3752
+ } else {
3753
+ this.enableKeyboardDragMode();
3754
+ }
3755
+ }
3756
+ enableKeyboardDragMode() {
3757
+ this.keyboardDragMode = true;
3758
+ this.keyboardResizeMode = false;
3759
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-drag`);
3760
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
3761
+ this.focusElement();
3762
+ }
3763
+ disableKeyboardDragMode() {
3764
+ this.keyboardDragMode = false;
3765
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
3766
+ }
3767
+ toggleKeyboardResizeMode() {
3768
+ if (this.keyboardResizeMode) {
3769
+ this.disableKeyboardResizeMode();
3770
+ } else {
3771
+ this.enableKeyboardResizeMode();
3772
+ }
3773
+ }
3774
+ enableKeyboardResizeMode() {
3775
+ this.keyboardResizeMode = true;
3776
+ this.keyboardDragMode = false;
3777
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-resize`);
3778
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
3779
+ this.focusElement();
3780
+ }
3781
+ disableKeyboardResizeMode() {
3782
+ this.keyboardResizeMode = false;
3783
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
3784
+ }
3785
+ enablePointerResizeMode({ focus = true } = {}) {
3786
+ if (!this.hasManagedResizeHandles()) {
3787
+ this.enableKeyboardResizeMode();
3788
+ return;
3789
+ }
3790
+ if (this.pointerResizeMode) {
3791
+ return;
3792
+ }
3793
+ this.pointerResizeMode = true;
3794
+ this.setManagedHandlesVisible(true);
3795
+ this.element.classList.add(`${this.options.classPrefix}-resizable`);
3796
+ this.enableKeyboardResizeMode();
3797
+ if (focus) {
3798
+ this.focusElement();
3799
+ }
3800
+ if (typeof this.options.onPointerResizeToggle === "function") {
3801
+ this.options.onPointerResizeToggle(true);
3802
+ }
3803
+ }
3804
+ disablePointerResizeMode({ focus = false } = {}) {
3805
+ if (!this.pointerResizeMode) {
3806
+ return;
3807
+ }
3808
+ this.pointerResizeMode = false;
3809
+ this.setManagedHandlesVisible(false);
3810
+ this.element.classList.remove(`${this.options.classPrefix}-resizable`);
3811
+ this.disableKeyboardResizeMode();
3812
+ if (focus) {
3813
+ this.focusElement();
3814
+ }
3815
+ if (typeof this.options.onPointerResizeToggle === "function") {
3816
+ this.options.onPointerResizeToggle(false);
3817
+ }
3818
+ }
3819
+ togglePointerResizeMode() {
3820
+ if (this.pointerResizeMode) {
3821
+ this.disablePointerResizeMode();
3822
+ } else {
3823
+ this.enablePointerResizeMode();
3824
+ }
3825
+ return this.pointerResizeMode;
3826
+ }
3827
+ focusElement() {
3828
+ if (typeof this.element.focus === "function") {
3829
+ try {
3830
+ this.element.focus({ preventScroll: true });
3831
+ } catch (e) {
3832
+ this.element.focus();
3833
+ }
3834
+ }
3835
+ }
3836
+ keyboardDrag(key, shiftKey) {
3837
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
3838
+ let currentLeft = parseFloat(this.element.style.left) || 0;
3839
+ let currentTop = parseFloat(this.element.style.top) || 0;
3840
+ const computedStyle = window.getComputedStyle(this.element);
3841
+ if (computedStyle.transform !== "none") {
3842
+ const rect = this.element.getBoundingClientRect();
3843
+ currentLeft = rect.left;
3844
+ currentTop = rect.top;
3845
+ this.element.style.transform = "none";
3846
+ this.element.style.left = `${currentLeft}px`;
3847
+ this.element.style.top = `${currentTop}px`;
3848
+ }
3849
+ let newX = currentLeft;
3850
+ let newY = currentTop;
3851
+ switch (key) {
3852
+ case "ArrowLeft":
3853
+ newX -= step;
3854
+ break;
3855
+ case "ArrowRight":
3856
+ newX += step;
3857
+ break;
3858
+ case "ArrowUp":
3859
+ newY -= step;
3860
+ break;
3861
+ case "ArrowDown":
3862
+ newY += step;
3863
+ break;
3864
+ }
3865
+ this.element.style.left = `${newX}px`;
3866
+ this.element.style.top = `${newY}px`;
3867
+ if (this.options.onDrag) {
3868
+ this.options.onDrag({ x: newX, y: newY });
3869
+ }
3870
+ }
3871
+ keyboardResize(key, shiftKey) {
3872
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
3873
+ const rect = this.element.getBoundingClientRect();
3874
+ let width = rect.width;
3875
+ let height = rect.height;
3876
+ switch (key) {
3877
+ case "ArrowLeft":
3878
+ width -= step;
3879
+ break;
3880
+ case "ArrowRight":
3881
+ width += step;
3882
+ break;
3883
+ case "ArrowUp":
3884
+ if (this.options.maintainAspectRatio) {
3885
+ width += step;
3886
+ } else {
3887
+ height -= step;
3888
+ }
3889
+ break;
3890
+ case "ArrowDown":
3891
+ if (this.options.maintainAspectRatio) {
3892
+ width -= step;
3893
+ } else {
3894
+ height += step;
3895
+ }
3896
+ break;
3897
+ }
3898
+ width = Math.max(this.options.minWidth, width);
3899
+ height = Math.max(this.options.minHeight, height);
3900
+ this.element.style.width = `${width}px`;
3901
+ if (!this.options.maintainAspectRatio) {
3902
+ this.element.style.height = `${height}px`;
3903
+ } else {
3904
+ this.element.style.height = "auto";
3905
+ }
3906
+ if (this.options.onResize) {
3907
+ this.options.onResize({ width, height });
3908
+ }
3909
+ }
3910
+ resetPosition() {
3911
+ this.element.style.left = "50%";
3912
+ this.element.style.top = "50%";
3913
+ this.element.style.transform = "translate(-50%, -50%)";
3914
+ this.element.style.right = "";
3915
+ this.element.style.bottom = "";
3916
+ this.manuallyPositioned = false;
3917
+ if (this.options.onDrag) {
3918
+ this.options.onDrag({ centered: true });
3919
+ }
3920
+ }
3921
+ destroy() {
3922
+ const dragHandle = this.options.dragHandle || this.element;
3923
+ this.disablePointerResizeMode();
3924
+ dragHandle.removeEventListener("mousedown", this.handlers.mousedown);
3925
+ dragHandle.removeEventListener("touchstart", this.handlers.touchstart);
3926
+ document.removeEventListener("mousemove", this.handlers.mousemove);
3927
+ document.removeEventListener("mouseup", this.handlers.mouseup);
3928
+ document.removeEventListener("touchmove", this.handlers.touchmove);
3929
+ document.removeEventListener("touchend", this.handlers.touchend);
3930
+ this.element.removeEventListener("keydown", this.handlers.keydown);
3931
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
3932
+ this.options.resizeHandles.forEach((handle) => {
3933
+ handle.removeEventListener("mousedown", this.handlers.resizeHandleMousedown);
3934
+ handle.removeEventListener("touchstart", this.handlers.resizeHandleMousedown);
3935
+ });
3936
+ }
3937
+ this.element.classList.remove(
3938
+ `${this.options.classPrefix}-dragging`,
3939
+ `${this.options.classPrefix}-resizing`,
3940
+ `${this.options.classPrefix}-keyboard-drag`,
3941
+ `${this.options.classPrefix}-keyboard-resize`
3942
+ );
3943
+ }
3944
+ };
3945
+
3296
3946
  // src/controls/TranscriptManager.js
3297
3947
  var TranscriptManager = class {
3298
3948
  constructor(player) {
@@ -3303,25 +3953,26 @@ var TranscriptManager = class {
3303
3953
  this.currentActiveEntry = null;
3304
3954
  this.isVisible = false;
3305
3955
  this.storage = new StorageManager("vidply");
3306
- this.isDragging = false;
3307
- this.dragOffsetX = 0;
3308
- this.dragOffsetY = 0;
3309
- this.isResizing = false;
3310
- this.resizeDirection = null;
3311
- this.resizeStartX = 0;
3312
- this.resizeStartY = 0;
3313
- this.resizeStartWidth = 0;
3314
- this.resizeStartHeight = 0;
3315
- this.resizeEnabled = false;
3956
+ this.draggableResizable = null;
3316
3957
  this.settingsMenuVisible = false;
3317
3958
  this.settingsMenu = null;
3318
3959
  this.settingsButton = null;
3319
3960
  this.settingsMenuJustOpened = false;
3320
- this.keyboardDragMode = false;
3961
+ this.resizeOptionButton = null;
3962
+ this.resizeOptionText = null;
3963
+ this.resizeModeIndicator = null;
3964
+ this.resizeModeIndicatorTimeout = null;
3965
+ this.transcriptResizeHandles = [];
3966
+ this.liveRegion = null;
3321
3967
  this.styleDialog = null;
3322
3968
  this.styleDialogVisible = false;
3323
3969
  this.styleDialogJustOpened = false;
3970
+ this.languageSelector = null;
3971
+ this.currentTranscriptLanguage = null;
3972
+ this.availableTranscriptLanguages = [];
3973
+ this.languageSelectorHandler = null;
3324
3974
  const savedPreferences = this.storage.getTranscriptPreferences();
3975
+ this.autoscrollEnabled = (savedPreferences == null ? void 0 : savedPreferences.autoscroll) !== void 0 ? savedPreferences.autoscroll : true;
3325
3976
  this.transcriptStyle = {
3326
3977
  fontSize: (savedPreferences == null ? void 0 : savedPreferences.fontSize) || this.player.options.transcriptFontSize || "100%",
3327
3978
  fontFamily: (savedPreferences == null ? void 0 : savedPreferences.fontFamily) || this.player.options.transcriptFontFamily || "sans-serif",
@@ -3332,25 +3983,22 @@ var TranscriptManager = class {
3332
3983
  this.handlers = {
3333
3984
  timeupdate: () => this.updateActiveEntry(),
3334
3985
  resize: null,
3335
- mousemove: null,
3336
- mouseup: null,
3337
- touchmove: null,
3338
- touchend: null,
3339
- mousedown: null,
3340
- touchstart: null,
3341
- keydown: null,
3342
3986
  settingsClick: null,
3343
3987
  settingsKeydown: null,
3344
3988
  documentClick: null,
3345
3989
  styleDialogKeydown: null
3346
3990
  };
3991
+ this.timeouts = /* @__PURE__ */ new Set();
3347
3992
  this.init();
3348
3993
  }
3349
3994
  init() {
3995
+ this.setupMetadataHandlingOnLoad();
3350
3996
  this.player.on("timeupdate", this.handlers.timeupdate);
3351
3997
  this.player.on("fullscreenchange", () => {
3352
3998
  if (this.isVisible) {
3353
- setTimeout(() => this.positionTranscript(), 100);
3999
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4000
+ this.setManagedTimeout(() => this.positionTranscript(), 100);
4001
+ }
3354
4002
  }
3355
4003
  });
3356
4004
  }
@@ -3371,9 +4019,12 @@ var TranscriptManager = class {
3371
4019
  if (this.transcriptWindow) {
3372
4020
  this.transcriptWindow.style.display = "flex";
3373
4021
  this.isVisible = true;
3374
- setTimeout(() => {
3375
- if (this.settingsButton) {
3376
- this.settingsButton.focus();
4022
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === "function") {
4023
+ this.player.controlBar.updateTranscriptButton();
4024
+ }
4025
+ this.setManagedTimeout(() => {
4026
+ if (this.transcriptHeader) {
4027
+ this.transcriptHeader.focus();
3377
4028
  }
3378
4029
  }, 150);
3379
4030
  return;
@@ -3382,10 +4033,12 @@ var TranscriptManager = class {
3382
4033
  this.loadTranscriptData();
3383
4034
  if (this.transcriptWindow) {
3384
4035
  this.transcriptWindow.style.display = "flex";
3385
- setTimeout(() => this.positionTranscript(), 0);
3386
- setTimeout(() => {
3387
- if (this.settingsButton) {
3388
- this.settingsButton.focus();
4036
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4037
+ this.setManagedTimeout(() => this.positionTranscript(), 0);
4038
+ }
4039
+ this.setManagedTimeout(() => {
4040
+ if (this.transcriptHeader) {
4041
+ this.transcriptHeader.focus();
3389
4042
  }
3390
4043
  }, 150);
3391
4044
  }
@@ -3394,11 +4047,27 @@ var TranscriptManager = class {
3394
4047
  /**
3395
4048
  * Hide transcript window
3396
4049
  */
3397
- hideTranscript() {
4050
+ hideTranscript({ focusButton = false } = {}) {
4051
+ var _a, _b;
3398
4052
  if (this.transcriptWindow) {
3399
4053
  this.transcriptWindow.style.display = "none";
3400
4054
  this.isVisible = false;
3401
4055
  }
4056
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
4057
+ this.draggableResizable.disablePointerResizeMode();
4058
+ this.updateResizeOptionState();
4059
+ }
4060
+ this.hideResizeModeIndicator();
4061
+ this.announceLive("");
4062
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === "function") {
4063
+ this.player.controlBar.updateTranscriptButton();
4064
+ }
4065
+ if (focusButton) {
4066
+ const transcriptButton = (_b = (_a = this.player.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.transcript;
4067
+ if (transcriptButton && typeof transcriptButton.focus === "function") {
4068
+ transcriptButton.focus();
4069
+ }
4070
+ }
3402
4071
  }
3403
4072
  /**
3404
4073
  * Create the transcript window UI
@@ -3415,7 +4084,6 @@ var TranscriptManager = class {
3415
4084
  this.transcriptHeader = DOMUtils.createElement("div", {
3416
4085
  className: `${this.player.options.classPrefix}-transcript-header`,
3417
4086
  attributes: {
3418
- "aria-label": "Drag to reposition transcript. Use arrow keys to move, Home to reset position, Escape to close.",
3419
4087
  "tabindex": "0"
3420
4088
  }
3421
4089
  });
@@ -3426,7 +4094,7 @@ var TranscriptManager = class {
3426
4094
  className: `${this.player.options.classPrefix}-transcript-settings`,
3427
4095
  attributes: {
3428
4096
  "type": "button",
3429
- "aria-label": i18n.t("transcript.settings"),
4097
+ "aria-label": i18n.t("transcript.settingsMenu"),
3430
4098
  "aria-expanded": "false"
3431
4099
  }
3432
4100
  });
@@ -3458,10 +4126,43 @@ var TranscriptManager = class {
3458
4126
  };
3459
4127
  this.settingsButton.addEventListener("keydown", this.handlers.settingsKeydown);
3460
4128
  const title = DOMUtils.createElement("h3", {
3461
- textContent: i18n.t("transcript.title")
4129
+ textContent: `${i18n.t("transcript.title")}. ${i18n.t("transcript.dragResizePrompt")}`
4130
+ });
4131
+ const autoscrollLabel = DOMUtils.createElement("label", {
4132
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
4133
+ attributes: {
4134
+ "title": i18n.t("transcript.autoscroll")
4135
+ }
3462
4136
  });
4137
+ this.autoscrollCheckbox = DOMUtils.createElement("input", {
4138
+ attributes: {
4139
+ "type": "checkbox",
4140
+ "checked": this.autoscrollEnabled,
4141
+ "aria-label": i18n.t("transcript.autoscroll")
4142
+ }
4143
+ });
4144
+ const autoscrollText = DOMUtils.createElement("span", {
4145
+ textContent: i18n.t("transcript.autoscroll"),
4146
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-text`
4147
+ });
4148
+ autoscrollLabel.appendChild(this.autoscrollCheckbox);
4149
+ autoscrollLabel.appendChild(autoscrollText);
4150
+ this.autoscrollCheckbox.addEventListener("change", (e) => {
4151
+ this.autoscrollEnabled = e.target.checked;
4152
+ this.saveAutoscrollPreference();
4153
+ });
4154
+ this.transcriptHeader.appendChild(title);
3463
4155
  this.headerLeft.appendChild(this.settingsButton);
3464
- this.headerLeft.appendChild(title);
4156
+ this.headerLeft.appendChild(autoscrollLabel);
4157
+ this.languageSelector = DOMUtils.createElement("select", {
4158
+ className: `${this.player.options.classPrefix}-transcript-language-select`,
4159
+ attributes: {
4160
+ "aria-label": i18n.t("settings.language") || "Language",
4161
+ "style": "display: none;"
4162
+ // Hidden until we detect multiple languages
4163
+ }
4164
+ });
4165
+ this.headerLeft.appendChild(this.languageSelector);
3465
4166
  const closeButton = DOMUtils.createElement("button", {
3466
4167
  className: `${this.player.options.classPrefix}-transcript-close`,
3467
4168
  attributes: {
@@ -3470,7 +4171,7 @@ var TranscriptManager = class {
3470
4171
  }
3471
4172
  });
3472
4173
  closeButton.appendChild(createIconElement("close"));
3473
- closeButton.addEventListener("click", () => this.hideTranscript());
4174
+ closeButton.addEventListener("click", () => this.hideTranscript({ focusButton: true }));
3474
4175
  this.transcriptHeader.appendChild(this.headerLeft);
3475
4176
  this.transcriptHeader.appendChild(closeButton);
3476
4177
  this.transcriptContent = DOMUtils.createElement("div", {
@@ -3478,9 +4179,20 @@ var TranscriptManager = class {
3478
4179
  });
3479
4180
  this.transcriptWindow.appendChild(this.transcriptHeader);
3480
4181
  this.transcriptWindow.appendChild(this.transcriptContent);
4182
+ this.createResizeHandles();
4183
+ this.liveRegion = DOMUtils.createElement("div", {
4184
+ className: "vidply-sr-only",
4185
+ attributes: {
4186
+ "aria-live": "polite",
4187
+ "aria-atomic": "true"
4188
+ }
4189
+ });
4190
+ this.transcriptWindow.appendChild(this.liveRegion);
3481
4191
  this.player.container.appendChild(this.transcriptWindow);
3482
- this.positionTranscript();
3483
4192
  this.setupDragAndDrop();
4193
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4194
+ this.positionTranscript();
4195
+ }
3484
4196
  this.handlers.documentClick = (e) => {
3485
4197
  if (this.settingsMenuJustOpened) {
3486
4198
  return;
@@ -3504,16 +4216,42 @@ var TranscriptManager = class {
3504
4216
  this.documentClickHandlerAdded = false;
3505
4217
  let resizeTimeout;
3506
4218
  this.handlers.resize = () => {
3507
- clearTimeout(resizeTimeout);
3508
- resizeTimeout = setTimeout(() => this.positionTranscript(), 100);
4219
+ if (resizeTimeout) {
4220
+ this.clearManagedTimeout(resizeTimeout);
4221
+ }
4222
+ resizeTimeout = this.setManagedTimeout(() => {
4223
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
4224
+ this.positionTranscript();
4225
+ }
4226
+ }, 100);
3509
4227
  };
3510
4228
  window.addEventListener("resize", this.handlers.resize);
3511
4229
  }
4230
+ createResizeHandles() {
4231
+ if (!this.transcriptWindow) return;
4232
+ const directions = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
4233
+ this.transcriptResizeHandles = directions.map((direction) => {
4234
+ const handle = DOMUtils.createElement("div", {
4235
+ className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
4236
+ attributes: {
4237
+ "data-direction": direction,
4238
+ "data-vidply-managed-resize": "true",
4239
+ "aria-hidden": "true"
4240
+ }
4241
+ });
4242
+ handle.style.display = "none";
4243
+ this.transcriptWindow.appendChild(handle);
4244
+ return handle;
4245
+ });
4246
+ }
3512
4247
  /**
3513
4248
  * Position transcript window next to video
3514
4249
  */
3515
4250
  positionTranscript() {
3516
4251
  if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
4252
+ if (this.draggableResizable && this.draggableResizable.manuallyPositioned) {
4253
+ return;
4254
+ }
3517
4255
  const isMobile = window.innerWidth < 640;
3518
4256
  const videoRect = this.player.videoWrapper.getBoundingClientRect();
3519
4257
  const isFullscreen = this.player.state.fullscreen;
@@ -3546,8 +4284,12 @@ var TranscriptManager = class {
3546
4284
  this.transcriptWindow.style.top = "auto";
3547
4285
  this.transcriptWindow.style.maxHeight = "calc(100vh - 180px)";
3548
4286
  this.transcriptWindow.style.height = "auto";
3549
- this.transcriptWindow.style.width = "400px";
3550
- this.transcriptWindow.style.maxWidth = "400px";
4287
+ const fullscreenMinWidth = 260;
4288
+ const fullscreenAvailable = Math.max(fullscreenMinWidth, window.innerWidth - 40);
4289
+ const fullscreenDesired = parseFloat(this.transcriptWindow.style.width) || 400;
4290
+ const fullscreenWidth = Math.max(fullscreenMinWidth, Math.min(fullscreenDesired, fullscreenAvailable));
4291
+ this.transcriptWindow.style.width = `${fullscreenWidth}px`;
4292
+ this.transcriptWindow.style.maxWidth = "none";
3551
4293
  this.transcriptWindow.style.borderRadius = "8px";
3552
4294
  this.transcriptWindow.style.border = "1px solid var(--vidply-border)";
3553
4295
  this.transcriptWindow.style.borderTop = "";
@@ -3555,15 +4297,30 @@ var TranscriptManager = class {
3555
4297
  this.player.container.appendChild(this.transcriptWindow);
3556
4298
  }
3557
4299
  } else {
4300
+ const transcriptWidth = parseFloat(this.transcriptWindow.style.width) || 400;
4301
+ const padding = 20;
4302
+ const minWidth = 260;
4303
+ const containerRect = this.player.container.getBoundingClientRect();
4304
+ const ensureContainerPositioned = () => {
4305
+ const computed = window.getComputedStyle(this.player.container);
4306
+ if (computed.position === "static") {
4307
+ this.player.container.style.position = "relative";
4308
+ }
4309
+ };
4310
+ ensureContainerPositioned();
4311
+ const left = videoRect.right - containerRect.left + padding;
4312
+ const availableWidth = window.innerWidth - videoRect.right - padding;
4313
+ const appliedWidth = Math.max(minWidth, Math.min(transcriptWidth, availableWidth));
4314
+ const appliedHeight = videoRect.height;
3558
4315
  this.transcriptWindow.style.position = "absolute";
3559
- this.transcriptWindow.style.left = `${videoRect.width + 8}px`;
4316
+ this.transcriptWindow.style.left = `${left}px`;
3560
4317
  this.transcriptWindow.style.right = "auto";
3561
4318
  this.transcriptWindow.style.bottom = "auto";
3562
4319
  this.transcriptWindow.style.top = "0";
3563
- this.transcriptWindow.style.height = `${videoRect.height}px`;
4320
+ this.transcriptWindow.style.height = `${appliedHeight}px`;
3564
4321
  this.transcriptWindow.style.maxHeight = "none";
3565
- this.transcriptWindow.style.width = "400px";
3566
- this.transcriptWindow.style.maxWidth = "400px";
4322
+ this.transcriptWindow.style.width = `${appliedWidth}px`;
4323
+ this.transcriptWindow.style.maxWidth = "none";
3567
4324
  this.transcriptWindow.style.borderRadius = "8px";
3568
4325
  this.transcriptWindow.style.border = "1px solid var(--vidply-border)";
3569
4326
  this.transcriptWindow.style.borderTop = "";
@@ -3575,17 +4332,94 @@ var TranscriptManager = class {
3575
4332
  }
3576
4333
  }
3577
4334
  }
4335
+ /**
4336
+ * Get available transcript languages from tracks
4337
+ */
4338
+ getAvailableTranscriptLanguages() {
4339
+ const textTracks = this.player.textTracks;
4340
+ const languages = /* @__PURE__ */ new Map();
4341
+ textTracks.forEach((track) => {
4342
+ if ((track.kind === "captions" || track.kind === "subtitles") && track.language) {
4343
+ if (!languages.has(track.language)) {
4344
+ languages.set(track.language, {
4345
+ language: track.language,
4346
+ label: track.label || track.language,
4347
+ track
4348
+ });
4349
+ }
4350
+ }
4351
+ });
4352
+ return Array.from(languages.values());
4353
+ }
4354
+ /**
4355
+ * Update language selector dropdown
4356
+ */
4357
+ updateLanguageSelector() {
4358
+ if (!this.languageSelector) return;
4359
+ this.availableTranscriptLanguages = this.getAvailableTranscriptLanguages();
4360
+ this.languageSelector.innerHTML = "";
4361
+ if (this.availableTranscriptLanguages.length < 2) {
4362
+ this.languageSelector.style.display = "none";
4363
+ return;
4364
+ }
4365
+ this.languageSelector.style.display = "block";
4366
+ this.availableTranscriptLanguages.forEach((langInfo, index) => {
4367
+ const option = DOMUtils.createElement("option", {
4368
+ textContent: langInfo.label,
4369
+ attributes: {
4370
+ "value": langInfo.language
4371
+ }
4372
+ });
4373
+ this.languageSelector.appendChild(option);
4374
+ });
4375
+ if (this.currentTranscriptLanguage) {
4376
+ this.languageSelector.value = this.currentTranscriptLanguage;
4377
+ } else if (this.availableTranscriptLanguages.length > 0) {
4378
+ const activeTrack = this.player.textTracks.find(
4379
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.mode === "showing"
4380
+ );
4381
+ this.currentTranscriptLanguage = activeTrack ? activeTrack.language : this.availableTranscriptLanguages[0].language;
4382
+ this.languageSelector.value = this.currentTranscriptLanguage;
4383
+ }
4384
+ if (this.languageSelectorHandler) {
4385
+ this.languageSelector.removeEventListener("change", this.languageSelectorHandler);
4386
+ }
4387
+ this.languageSelectorHandler = (e) => {
4388
+ this.currentTranscriptLanguage = e.target.value;
4389
+ this.loadTranscriptData();
4390
+ };
4391
+ this.languageSelector.addEventListener("change", this.languageSelectorHandler);
4392
+ }
3578
4393
  /**
3579
4394
  * Load transcript data from caption/subtitle tracks
3580
4395
  */
3581
4396
  loadTranscriptData() {
3582
4397
  this.transcriptEntries = [];
3583
4398
  this.transcriptContent.innerHTML = "";
3584
- const textTracks = Array.from(this.player.element.textTracks);
3585
- const captionTrack = textTracks.find(
3586
- (track) => track.kind === "captions" || track.kind === "subtitles"
3587
- );
3588
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
4399
+ const textTracks = this.player.textTracks;
4400
+ let captionTrack = null;
4401
+ if (this.currentTranscriptLanguage) {
4402
+ captionTrack = textTracks.find(
4403
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.language === this.currentTranscriptLanguage
4404
+ );
4405
+ }
4406
+ if (!captionTrack) {
4407
+ captionTrack = textTracks.find(
4408
+ (track) => track.kind === "captions" || track.kind === "subtitles"
4409
+ );
4410
+ if (captionTrack) {
4411
+ this.currentTranscriptLanguage = captionTrack.language;
4412
+ }
4413
+ }
4414
+ let descriptionTrack = null;
4415
+ if (this.currentTranscriptLanguage) {
4416
+ descriptionTrack = textTracks.find(
4417
+ (track) => track.kind === "descriptions" && track.language === this.currentTranscriptLanguage
4418
+ );
4419
+ }
4420
+ if (!descriptionTrack) {
4421
+ descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
4422
+ }
3589
4423
  const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3590
4424
  if (!captionTrack && !descriptionTrack && !metadataTrack) {
3591
4425
  this.showNoTranscriptMessage();
@@ -3614,7 +4448,7 @@ var TranscriptManager = class {
3614
4448
  tracksToLoad.forEach((track) => {
3615
4449
  track.addEventListener("load", onLoad, { once: true });
3616
4450
  });
3617
- setTimeout(() => {
4451
+ this.setManagedTimeout(() => {
3618
4452
  this.loadTranscriptData();
3619
4453
  }, 500);
3620
4454
  return;
@@ -3647,24 +4481,61 @@ var TranscriptManager = class {
3647
4481
  this.transcriptContent.appendChild(entry);
3648
4482
  });
3649
4483
  this.applyTranscriptStyles();
4484
+ this.updateLanguageSelector();
4485
+ }
4486
+ /**
4487
+ * Setup metadata handling on player load
4488
+ * This runs independently of transcript loading
4489
+ */
4490
+ setupMetadataHandlingOnLoad() {
4491
+ const setupMetadata = () => {
4492
+ const textTracks = this.player.textTracks;
4493
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
4494
+ if (metadataTrack) {
4495
+ if (metadataTrack.mode === "disabled") {
4496
+ metadataTrack.mode = "hidden";
4497
+ }
4498
+ if (this.metadataCueChangeHandler) {
4499
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
4500
+ }
4501
+ this.metadataCueChangeHandler = () => {
4502
+ const activeCues = Array.from(metadataTrack.activeCues || []);
4503
+ if (activeCues.length > 0) {
4504
+ if (this.player.options.debug) {
4505
+ console.log("[VidPly Metadata] Active cues:", activeCues.map((c) => ({
4506
+ start: c.startTime,
4507
+ end: c.endTime,
4508
+ text: c.text
4509
+ })));
4510
+ }
4511
+ }
4512
+ activeCues.forEach((cue) => {
4513
+ this.handleMetadataCue(cue);
4514
+ });
4515
+ };
4516
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
4517
+ if (this.player.options.debug) {
4518
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
4519
+ console.log("[VidPly Metadata] Track enabled,", cueCount, "cues available");
4520
+ }
4521
+ } else if (this.player.options.debug) {
4522
+ console.warn("[VidPly Metadata] No metadata track found");
4523
+ }
4524
+ };
4525
+ setupMetadata();
4526
+ this.player.on("loadedmetadata", setupMetadata);
3650
4527
  }
3651
4528
  /**
3652
4529
  * Setup metadata handling
3653
4530
  * Metadata cues are not displayed but can be used programmatically
4531
+ * This is called when transcript data is loaded (for storing cues)
3654
4532
  */
3655
4533
  setupMetadataHandling() {
3656
4534
  if (!this.metadataCues || this.metadataCues.length === 0) {
3657
4535
  return;
3658
4536
  }
3659
- const textTracks = Array.from(this.player.element.textTracks);
3660
- const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3661
- if (metadataTrack) {
3662
- metadataTrack.addEventListener("cuechange", () => {
3663
- const activeCues = Array.from(metadataTrack.activeCues || []);
3664
- activeCues.forEach((cue) => {
3665
- this.handleMetadataCue(cue);
3666
- });
3667
- });
4537
+ if (this.player.options.debug) {
4538
+ console.log("[VidPly Metadata]", this.metadataCues.length, "cues stored from transcript load");
3668
4539
  }
3669
4540
  }
3670
4541
  /**
@@ -3673,6 +4544,12 @@ var TranscriptManager = class {
3673
4544
  */
3674
4545
  handleMetadataCue(cue) {
3675
4546
  const text = cue.text.trim();
4547
+ if (this.player.options.debug) {
4548
+ console.log("[VidPly Metadata] Processing cue:", {
4549
+ time: cue.startTime,
4550
+ text
4551
+ });
4552
+ }
3676
4553
  this.player.emit("metadata", {
3677
4554
  time: cue.startTime,
3678
4555
  endTime: cue.endTime,
@@ -3680,18 +4557,40 @@ var TranscriptManager = class {
3680
4557
  cue
3681
4558
  });
3682
4559
  if (text.includes("PAUSE")) {
4560
+ if (!this.player.state.paused) {
4561
+ if (this.player.options.debug) {
4562
+ console.log("[VidPly Metadata] Pausing video at", cue.startTime);
4563
+ }
4564
+ this.player.pause();
4565
+ }
3683
4566
  this.player.emit("metadata:pause", { time: cue.startTime, text });
3684
4567
  }
3685
4568
  const focusMatch = text.match(/FOCUS:([\w#-]+)/);
3686
4569
  if (focusMatch) {
4570
+ const targetSelector = focusMatch[1];
4571
+ const targetElement = document.querySelector(targetSelector);
4572
+ if (targetElement) {
4573
+ if (this.player.options.debug) {
4574
+ console.log("[VidPly Metadata] Focusing element:", targetSelector);
4575
+ }
4576
+ this.setManagedTimeout(() => {
4577
+ targetElement.focus();
4578
+ }, 10);
4579
+ } else if (this.player.options.debug) {
4580
+ console.warn("[VidPly Metadata] Element not found:", targetSelector);
4581
+ }
3687
4582
  this.player.emit("metadata:focus", {
3688
4583
  time: cue.startTime,
3689
- target: focusMatch[1],
4584
+ target: targetSelector,
4585
+ element: targetElement,
3690
4586
  text
3691
4587
  });
3692
4588
  }
3693
4589
  const hashtags = text.match(/#[\w-]+/g);
3694
4590
  if (hashtags) {
4591
+ if (this.player.options.debug) {
4592
+ console.log("[VidPly Metadata] Hashtags found:", hashtags);
4593
+ }
3695
4594
  this.player.emit("metadata:hashtags", {
3696
4595
  time: cue.startTime,
3697
4596
  hashtags,
@@ -3785,7 +4684,7 @@ var TranscriptManager = class {
3785
4684
  * Scroll transcript window to show active entry
3786
4685
  */
3787
4686
  scrollToEntry(entryElement) {
3788
- if (!this.transcriptContent) return;
4687
+ if (!this.transcriptContent || !this.autoscrollEnabled) return;
3789
4688
  const contentRect = this.transcriptContent.getBoundingClientRect();
3790
4689
  const entryRect = entryElement.getBoundingClientRect();
3791
4690
  if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
@@ -3796,256 +4695,121 @@ var TranscriptManager = class {
3796
4695
  });
3797
4696
  }
3798
4697
  }
4698
+ /**
4699
+ * Save autoscroll preference to localStorage
4700
+ */
4701
+ saveAutoscrollPreference() {
4702
+ const savedPreferences = this.storage.getTranscriptPreferences() || {};
4703
+ savedPreferences.autoscroll = this.autoscrollEnabled;
4704
+ this.storage.saveTranscriptPreferences(savedPreferences);
4705
+ }
3799
4706
  /**
3800
4707
  * Setup drag and drop functionality
3801
4708
  */
3802
4709
  setupDragAndDrop() {
3803
4710
  if (!this.transcriptHeader || !this.transcriptWindow) return;
3804
- this.handlers.mousedown = (e) => {
3805
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
3806
- return;
3807
- }
3808
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3809
- return;
3810
- }
3811
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3812
- return;
3813
- }
3814
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
3815
- return;
3816
- }
3817
- this.startDragging(e.clientX, e.clientY);
3818
- e.preventDefault();
3819
- };
3820
- this.handlers.mousemove = (e) => {
3821
- if (this.isDragging) {
3822
- this.drag(e.clientX, e.clientY);
3823
- }
3824
- };
3825
- this.handlers.mouseup = () => {
3826
- if (this.isDragging) {
3827
- this.stopDragging();
3828
- }
3829
- };
3830
- this.handlers.touchstart = (e) => {
3831
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
3832
- return;
3833
- }
3834
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3835
- return;
3836
- }
3837
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3838
- return;
3839
- }
3840
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
3841
- return;
3842
- }
3843
- const isMobile = window.innerWidth < 640;
3844
- const isFullscreen = this.player.state.fullscreen;
3845
- const touch = e.touches[0];
3846
- if (isMobile && !isFullscreen) {
3847
- return;
3848
- } else {
3849
- this.startDragging(touch.clientX, touch.clientY);
3850
- }
3851
- };
3852
- this.handlers.touchmove = (e) => {
3853
- const isMobile = window.innerWidth < 640;
3854
- const isFullscreen = this.player.state.fullscreen;
3855
- if (isMobile && !isFullscreen) {
3856
- return;
3857
- } else if (this.isDragging) {
3858
- const touch = e.touches[0];
3859
- this.drag(touch.clientX, touch.clientY);
3860
- e.preventDefault();
3861
- }
3862
- };
3863
- this.handlers.touchend = () => {
3864
- if (this.isDragging) {
3865
- this.stopDragging();
3866
- }
3867
- };
3868
- this.handlers.keydown = (e) => {
3869
- if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
3870
- if (!this.keyboardDragMode) {
3871
- return;
4711
+ const isMobile = window.innerWidth < 640;
4712
+ const isFullscreen = this.player.state.fullscreen;
4713
+ if (isMobile && !isFullscreen) {
4714
+ return;
4715
+ }
4716
+ this.draggableResizable = new DraggableResizable(this.transcriptWindow, {
4717
+ dragHandle: this.transcriptHeader,
4718
+ resizeHandles: this.transcriptResizeHandles,
4719
+ constrainToViewport: true,
4720
+ classPrefix: `${this.player.options.classPrefix}-transcript`,
4721
+ keyboardDragKey: "d",
4722
+ keyboardResizeKey: "r",
4723
+ keyboardStep: 10,
4724
+ keyboardStepLarge: 50,
4725
+ minWidth: 300,
4726
+ minHeight: 200,
4727
+ maxWidth: () => Math.max(320, window.innerWidth - 40),
4728
+ maxHeight: () => Math.max(200, window.innerHeight - 120),
4729
+ pointerResizeIndicatorText: i18n.t("transcript.resizeModeHint"),
4730
+ onPointerResizeToggle: (enabled) => this.onPointerResizeModeChange(enabled),
4731
+ onDragStart: (e) => {
4732
+ const ignoreSelectors = [
4733
+ `.${this.player.options.classPrefix}-transcript-close`,
4734
+ `.${this.player.options.classPrefix}-transcript-settings`,
4735
+ `.${this.player.options.classPrefix}-transcript-language-select`,
4736
+ `.${this.player.options.classPrefix}-transcript-settings-menu`,
4737
+ `.${this.player.options.classPrefix}-transcript-style-dialog`
4738
+ ];
4739
+ for (const selector of ignoreSelectors) {
4740
+ if (e.target.closest(selector)) {
4741
+ return false;
4742
+ }
3872
4743
  }
4744
+ return true;
4745
+ }
4746
+ });
4747
+ this.customKeyHandler = (e) => {
4748
+ const key = e.key.toLowerCase();
4749
+ const alreadyPrevented = e.defaultPrevented;
4750
+ if (key === "home") {
3873
4751
  e.preventDefault();
3874
4752
  e.stopPropagation();
3875
- const step = e.shiftKey ? 50 : 10;
3876
- let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
3877
- let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
3878
- const computedStyle = window.getComputedStyle(this.transcriptWindow);
3879
- if (computedStyle.transform !== "none") {
3880
- const rect = this.transcriptWindow.getBoundingClientRect();
3881
- currentLeft = rect.left;
3882
- currentTop = rect.top;
3883
- this.transcriptWindow.style.transform = "none";
3884
- this.transcriptWindow.style.left = `${currentLeft}px`;
3885
- this.transcriptWindow.style.top = `${currentTop}px`;
3886
- }
3887
- let newX = currentLeft;
3888
- let newY = currentTop;
3889
- switch (e.key) {
3890
- case "ArrowLeft":
3891
- newX -= step;
3892
- break;
3893
- case "ArrowRight":
3894
- newX += step;
3895
- break;
3896
- case "ArrowUp":
3897
- newY -= step;
3898
- break;
3899
- case "ArrowDown":
3900
- newY += step;
3901
- break;
4753
+ if (this.draggableResizable) {
4754
+ if (this.draggableResizable.pointerResizeMode) {
4755
+ this.draggableResizable.disablePointerResizeMode();
4756
+ }
4757
+ this.draggableResizable.manuallyPositioned = false;
4758
+ this.positionTranscript();
4759
+ this.updateResizeOptionState();
4760
+ this.announceLive(i18n.t("transcript.positionReset"));
3902
4761
  }
3903
- this.transcriptWindow.style.left = `${newX}px`;
3904
- this.transcriptWindow.style.top = `${newY}px`;
3905
4762
  return;
3906
4763
  }
3907
- if (e.key === "Home") {
4764
+ if (key === "r") {
4765
+ if (alreadyPrevented) {
4766
+ return;
4767
+ }
3908
4768
  e.preventDefault();
3909
4769
  e.stopPropagation();
3910
- this.resetPosition();
4770
+ const enabled = this.toggleResizeMode();
4771
+ if (enabled) {
4772
+ this.transcriptWindow.focus();
4773
+ }
3911
4774
  return;
3912
4775
  }
3913
- if (e.key === "Escape") {
4776
+ if (key === "escape") {
3914
4777
  e.preventDefault();
3915
4778
  e.stopPropagation();
4779
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
4780
+ this.draggableResizable.disablePointerResizeMode();
4781
+ return;
4782
+ }
3916
4783
  if (this.styleDialogVisible) {
3917
4784
  this.hideStyleDialog();
3918
- } else if (this.keyboardDragMode) {
3919
- this.disableKeyboardDragMode();
4785
+ } else if (this.draggableResizable && this.draggableResizable.keyboardDragMode) {
4786
+ this.draggableResizable.disableKeyboardDragMode();
4787
+ this.announceLive(i18n.t("transcript.dragModeDisabled"));
3920
4788
  } else if (this.settingsMenuVisible) {
3921
4789
  this.hideSettingsMenu();
3922
4790
  } else {
3923
- this.hideTranscript();
4791
+ this.hideTranscript({ focusButton: true });
3924
4792
  }
3925
4793
  return;
3926
4794
  }
3927
4795
  };
3928
- this.transcriptHeader.addEventListener("mousedown", this.handlers.mousedown);
3929
- document.addEventListener("mousemove", this.handlers.mousemove);
3930
- document.addEventListener("mouseup", this.handlers.mouseup);
3931
- this.transcriptHeader.addEventListener("touchstart", this.handlers.touchstart);
3932
- document.addEventListener("touchmove", this.handlers.touchmove);
3933
- document.addEventListener("touchend", this.handlers.touchend);
3934
- this.transcriptHeader.addEventListener("keydown", this.handlers.keydown);
3935
- }
3936
- /**
3937
- * Start dragging
3938
- */
3939
- startDragging(clientX, clientY) {
3940
- const rect = this.transcriptWindow.getBoundingClientRect();
3941
- const containerRect = this.player.container.getBoundingClientRect();
3942
- const relativeLeft = rect.left - containerRect.left;
3943
- const relativeTop = rect.top - containerRect.top;
3944
- const computedStyle = window.getComputedStyle(this.transcriptWindow);
3945
- if (computedStyle.transform !== "none") {
3946
- this.transcriptWindow.style.transform = "none";
3947
- this.transcriptWindow.style.left = `${relativeLeft}px`;
3948
- this.transcriptWindow.style.top = `${relativeTop}px`;
3949
- }
3950
- this.dragOffsetX = clientX - rect.left;
3951
- this.dragOffsetY = clientY - rect.top;
3952
- this.isDragging = true;
3953
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-dragging`);
3954
- document.body.style.cursor = "grabbing";
3955
- document.body.style.userSelect = "none";
3956
- }
3957
- /**
3958
- * Perform drag
3959
- */
3960
- drag(clientX, clientY) {
3961
- if (!this.isDragging) return;
3962
- const newViewportX = clientX - this.dragOffsetX;
3963
- const newViewportY = clientY - this.dragOffsetY;
3964
- const containerRect = this.player.container.getBoundingClientRect();
3965
- const newX = newViewportX - containerRect.left;
3966
- const newY = newViewportY - containerRect.top;
3967
- this.transcriptWindow.style.left = `${newX}px`;
3968
- this.transcriptWindow.style.top = `${newY}px`;
3969
- }
3970
- /**
3971
- * Stop dragging
3972
- */
3973
- stopDragging() {
3974
- this.isDragging = false;
3975
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-dragging`);
3976
- document.body.style.cursor = "";
3977
- document.body.style.userSelect = "";
3978
- }
3979
- /**
3980
- * Set window position with boundary constraints
3981
- */
3982
- setPosition(x, y) {
3983
- const rect = this.transcriptWindow.getBoundingClientRect();
3984
- const viewportWidth = document.documentElement.clientWidth;
3985
- const viewportHeight = document.documentElement.clientHeight;
3986
- const minVisible = 100;
3987
- const minX = -(rect.width - minVisible);
3988
- const minY = -(rect.height - minVisible);
3989
- const maxX = viewportWidth - minVisible;
3990
- const maxY = viewportHeight - minVisible;
3991
- x = Math.max(minX, Math.min(x, maxX));
3992
- y = Math.max(minY, Math.min(y, maxY));
3993
- this.transcriptWindow.style.left = `${x}px`;
3994
- this.transcriptWindow.style.top = `${y}px`;
3995
- this.transcriptWindow.style.transform = "none";
3996
- }
3997
- /**
3998
- * Reset position to center
3999
- */
4000
- resetPosition() {
4001
- this.transcriptWindow.style.left = "50%";
4002
- this.transcriptWindow.style.top = "50%";
4003
- this.transcriptWindow.style.transform = "translate(-50%, -50%)";
4796
+ this.transcriptWindow.addEventListener("keydown", this.customKeyHandler);
4004
4797
  }
4005
4798
  /**
4006
4799
  * Toggle keyboard drag mode
4007
4800
  */
4008
4801
  toggleKeyboardDragMode() {
4009
- if (this.keyboardDragMode) {
4010
- this.disableKeyboardDragMode();
4011
- } else {
4012
- this.enableKeyboardDragMode();
4013
- }
4014
- }
4015
- /**
4016
- * Enable keyboard drag mode
4017
- */
4018
- enableKeyboardDragMode() {
4019
- this.keyboardDragMode = true;
4020
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
4021
- if (this.settingsButton) {
4022
- this.settingsButton.setAttribute("aria-label", "Keyboard drag mode active. Use arrow keys to move window. Press D or Escape to exit.");
4023
- }
4024
- const indicator = DOMUtils.createElement("div", {
4025
- className: `${this.player.options.classPrefix}-transcript-drag-indicator`,
4026
- textContent: i18n.t("transcript.keyboardDragActive")
4027
- });
4028
- this.transcriptHeader.appendChild(indicator);
4029
- if (this.settingsMenuVisible) {
4030
- this.hideSettingsMenu();
4031
- }
4032
- this.transcriptHeader.focus();
4033
- }
4034
- /**
4035
- * Disable keyboard drag mode
4036
- */
4037
- disableKeyboardDragMode() {
4038
- this.keyboardDragMode = false;
4039
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
4040
- if (this.settingsButton) {
4041
- this.settingsButton.setAttribute("aria-label", "Transcript settings. Press Enter to open menu, or D to enable drag mode");
4042
- }
4043
- const indicator = this.transcriptHeader.querySelector(`.${this.player.options.classPrefix}-transcript-drag-indicator`);
4044
- if (indicator) {
4045
- indicator.remove();
4046
- }
4047
- if (this.settingsButton) {
4048
- this.settingsButton.focus();
4802
+ if (this.draggableResizable) {
4803
+ const wasEnabled = this.draggableResizable.keyboardDragMode;
4804
+ this.draggableResizable.toggleKeyboardDragMode();
4805
+ const isEnabled = this.draggableResizable.keyboardDragMode;
4806
+ if (!wasEnabled && isEnabled) {
4807
+ this.enableMoveMode();
4808
+ }
4809
+ if (this.settingsMenuVisible) {
4810
+ this.hideSettingsMenu();
4811
+ }
4812
+ this.transcriptWindow.focus();
4049
4813
  }
4050
4814
  }
4051
4815
  /**
@@ -4075,6 +4839,16 @@ var TranscriptManager = class {
4075
4839
  if (this.settingsMenu) {
4076
4840
  this.settingsMenu.style.display = "block";
4077
4841
  this.settingsMenuVisible = true;
4842
+ if (this.settingsButton) {
4843
+ this.settingsButton.setAttribute("aria-expanded", "true");
4844
+ }
4845
+ this.updateResizeOptionState();
4846
+ setTimeout(() => {
4847
+ const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
4848
+ if (firstItem) {
4849
+ firstItem.focus();
4850
+ }
4851
+ }, 0);
4078
4852
  return;
4079
4853
  }
4080
4854
  this.settingsMenu = DOMUtils.createElement("div", {
@@ -4122,19 +4896,35 @@ var TranscriptManager = class {
4122
4896
  className: `${this.player.options.classPrefix}-transcript-settings-item`,
4123
4897
  attributes: {
4124
4898
  "type": "button",
4125
- "aria-label": i18n.t("transcript.resizeWindow")
4899
+ "aria-label": i18n.t("transcript.resizeWindow"),
4900
+ "aria-pressed": "false"
4126
4901
  }
4127
4902
  });
4128
4903
  const resizeIcon = createIconElement("resize");
4129
4904
  const resizeText = DOMUtils.createElement("span", {
4905
+ className: `${this.player.options.classPrefix}-transcript-settings-text`,
4130
4906
  textContent: i18n.t("transcript.resizeWindow")
4131
4907
  });
4132
4908
  resizeOption.appendChild(resizeIcon);
4133
4909
  resizeOption.appendChild(resizeText);
4134
- resizeOption.addEventListener("click", () => {
4135
- this.toggleResizeMode();
4136
- this.hideSettingsMenu();
4910
+ resizeOption.addEventListener("click", (event) => {
4911
+ event.preventDefault();
4912
+ event.stopPropagation();
4913
+ const enabled = this.toggleResizeMode({ focus: false });
4914
+ if (enabled) {
4915
+ this.hideSettingsMenu({ focusButton: false });
4916
+ this.setManagedTimeout(() => {
4917
+ if (this.transcriptWindow) {
4918
+ this.transcriptWindow.focus();
4919
+ }
4920
+ }, 20);
4921
+ } else {
4922
+ this.hideSettingsMenu({ focusButton: true });
4923
+ }
4137
4924
  });
4925
+ this.resizeOptionButton = resizeOption;
4926
+ this.resizeOptionText = resizeText;
4927
+ this.updateResizeOptionState();
4138
4928
  const closeOption = DOMUtils.createElement("button", {
4139
4929
  className: `${this.player.options.classPrefix}-transcript-settings-item`,
4140
4930
  attributes: {
@@ -4165,6 +4955,7 @@ var TranscriptManager = class {
4165
4955
  if (this.settingsButton) {
4166
4956
  this.settingsButton.setAttribute("aria-expanded", "true");
4167
4957
  }
4958
+ this.updateResizeOptionState();
4168
4959
  setTimeout(() => {
4169
4960
  const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
4170
4961
  if (firstItem) {
@@ -4175,14 +4966,16 @@ var TranscriptManager = class {
4175
4966
  /**
4176
4967
  * Hide settings menu
4177
4968
  */
4178
- hideSettingsMenu() {
4969
+ hideSettingsMenu({ focusButton = true } = {}) {
4179
4970
  if (this.settingsMenu) {
4180
4971
  this.settingsMenu.style.display = "none";
4181
4972
  this.settingsMenuVisible = false;
4182
4973
  this.settingsMenuJustOpened = false;
4183
4974
  if (this.settingsButton) {
4184
4975
  this.settingsButton.setAttribute("aria-expanded", "false");
4185
- this.settingsButton.focus();
4976
+ if (focusButton) {
4977
+ this.settingsButton.focus();
4978
+ }
4186
4979
  }
4187
4980
  }
4188
4981
  }
@@ -4190,6 +4983,7 @@ var TranscriptManager = class {
4190
4983
  * Enable move mode (gives visual feedback)
4191
4984
  */
4192
4985
  enableMoveMode() {
4986
+ this.hideResizeModeIndicator();
4193
4987
  this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
4194
4988
  const tooltip = DOMUtils.createElement("div", {
4195
4989
  className: `${this.player.options.classPrefix}-transcript-move-tooltip`,
@@ -4206,155 +5000,64 @@ var TranscriptManager = class {
4206
5000
  /**
4207
5001
  * Toggle resize mode
4208
5002
  */
4209
- toggleResizeMode() {
4210
- this.resizeEnabled = !this.resizeEnabled;
4211
- if (this.resizeEnabled) {
4212
- this.enableResizeHandles();
4213
- } else {
4214
- this.disableResizeHandles();
5003
+ toggleResizeMode({ focus = true } = {}) {
5004
+ if (!this.draggableResizable) {
5005
+ return false;
4215
5006
  }
4216
- }
4217
- /**
4218
- * Enable resize handles
4219
- */
4220
- enableResizeHandles() {
4221
- if (!this.transcriptWindow) return;
4222
- const directions = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
4223
- directions.forEach((direction) => {
4224
- const handle = DOMUtils.createElement("div", {
4225
- className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
4226
- attributes: {
4227
- "data-direction": direction
4228
- }
4229
- });
4230
- handle.addEventListener("mousedown", (e) => this.startResize(e, direction));
4231
- handle.addEventListener("touchstart", (e) => this.startResize(e.touches[0], direction));
4232
- this.transcriptWindow.appendChild(handle);
4233
- });
4234
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizable`);
4235
- this.handlers.resizeMove = (e) => {
4236
- if (this.isResizing) {
4237
- this.performResize(e.clientX, e.clientY);
4238
- }
4239
- };
4240
- this.handlers.resizeEnd = () => {
4241
- if (this.isResizing) {
4242
- this.stopResize();
4243
- }
4244
- };
4245
- this.handlers.resizeTouchMove = (e) => {
4246
- if (this.isResizing) {
4247
- this.performResize(e.touches[0].clientX, e.touches[0].clientY);
4248
- e.preventDefault();
4249
- }
4250
- };
4251
- document.addEventListener("mousemove", this.handlers.resizeMove);
4252
- document.addEventListener("mouseup", this.handlers.resizeEnd);
4253
- document.addEventListener("touchmove", this.handlers.resizeTouchMove);
4254
- document.addEventListener("touchend", this.handlers.resizeEnd);
4255
- }
4256
- /**
4257
- * Disable resize handles
4258
- */
4259
- disableResizeHandles() {
4260
- if (!this.transcriptWindow) return;
4261
- const handles = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-resize-handle`);
4262
- handles.forEach((handle) => handle.remove());
4263
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizable`);
4264
- if (this.handlers.resizeMove) {
4265
- document.removeEventListener("mousemove", this.handlers.resizeMove);
5007
+ if (this.draggableResizable.pointerResizeMode) {
5008
+ this.draggableResizable.disablePointerResizeMode({ focus });
5009
+ return false;
4266
5010
  }
4267
- if (this.handlers.resizeEnd) {
4268
- document.removeEventListener("mouseup", this.handlers.resizeEnd);
5011
+ this.draggableResizable.enablePointerResizeMode({ focus });
5012
+ return true;
5013
+ }
5014
+ updateResizeOptionState() {
5015
+ if (!this.resizeOptionButton) {
5016
+ return;
4269
5017
  }
4270
- if (this.handlers.resizeTouchMove) {
4271
- document.removeEventListener("touchmove", this.handlers.resizeTouchMove);
5018
+ const isEnabled = !!(this.draggableResizable && this.draggableResizable.pointerResizeMode);
5019
+ const label = isEnabled ? i18n.t("transcript.disableResizeWindow") || "Disable Resize Mode" : i18n.t("transcript.resizeWindow");
5020
+ this.resizeOptionButton.setAttribute("aria-pressed", isEnabled ? "true" : "false");
5021
+ this.resizeOptionButton.setAttribute("aria-label", label);
5022
+ this.resizeOptionButton.setAttribute("title", label);
5023
+ if (this.resizeOptionText) {
5024
+ this.resizeOptionText.textContent = label;
4272
5025
  }
4273
- document.removeEventListener("touchend", this.handlers.resizeEnd);
4274
- }
4275
- /**
4276
- * Start resizing
4277
- */
4278
- startResize(e, direction) {
4279
- e.stopPropagation();
4280
- e.preventDefault();
4281
- this.isResizing = true;
4282
- this.resizeDirection = direction;
4283
- this.resizeStartX = e.clientX;
4284
- this.resizeStartY = e.clientY;
4285
- const rect = this.transcriptWindow.getBoundingClientRect();
4286
- this.resizeStartWidth = rect.width;
4287
- this.resizeStartHeight = rect.height;
4288
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizing`);
4289
- document.body.style.cursor = this.getResizeCursor(direction);
4290
- document.body.style.userSelect = "none";
4291
5026
  }
4292
- /**
4293
- * Perform resize
4294
- */
4295
- performResize(clientX, clientY) {
4296
- if (!this.isResizing) return;
4297
- const deltaX = clientX - this.resizeStartX;
4298
- const deltaY = clientY - this.resizeStartY;
4299
- let newWidth = this.resizeStartWidth;
4300
- let newHeight = this.resizeStartHeight;
4301
- const direction = this.resizeDirection;
4302
- if (direction.includes("e")) {
4303
- newWidth = this.resizeStartWidth + deltaX;
4304
- }
4305
- if (direction.includes("w")) {
4306
- newWidth = this.resizeStartWidth - deltaX;
4307
- }
4308
- if (direction.includes("s")) {
4309
- newHeight = this.resizeStartHeight + deltaY;
4310
- }
4311
- if (direction.includes("n")) {
4312
- newHeight = this.resizeStartHeight - deltaY;
4313
- }
4314
- const minWidth = 300;
4315
- const minHeight = 200;
4316
- const maxWidth = window.innerWidth - 40;
4317
- const maxHeight = window.innerHeight - 40;
4318
- newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
4319
- newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
4320
- this.transcriptWindow.style.width = `${newWidth}px`;
4321
- this.transcriptWindow.style.height = `${newHeight}px`;
4322
- this.transcriptWindow.style.maxWidth = `${newWidth}px`;
4323
- this.transcriptWindow.style.maxHeight = `${newHeight}px`;
4324
- if (direction.includes("w")) {
4325
- const currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
4326
- this.transcriptWindow.style.left = `${currentLeft + (this.resizeStartWidth - newWidth)}px`;
4327
- }
4328
- if (direction.includes("n")) {
4329
- const currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
4330
- this.transcriptWindow.style.top = `${currentTop + (this.resizeStartHeight - newHeight)}px`;
5027
+ showResizeModeIndicator() {
5028
+ if (!this.transcriptHeader) {
5029
+ return;
5030
+ }
5031
+ this.hideResizeModeIndicator();
5032
+ const indicator = DOMUtils.createElement("div", {
5033
+ className: `${this.player.options.classPrefix}-transcript-resize-tooltip`,
5034
+ textContent: i18n.t("transcript.resizeModeHint") || "Resize handles enabled. Drag edges or corners to adjust. Press Esc or R to exit."
5035
+ });
5036
+ this.transcriptHeader.appendChild(indicator);
5037
+ this.resizeModeIndicator = indicator;
5038
+ this.resizeModeIndicatorTimeout = this.setManagedTimeout(() => {
5039
+ this.hideResizeModeIndicator();
5040
+ }, 3e3);
5041
+ }
5042
+ hideResizeModeIndicator() {
5043
+ if (this.resizeModeIndicatorTimeout) {
5044
+ this.clearManagedTimeout(this.resizeModeIndicatorTimeout);
5045
+ this.resizeModeIndicatorTimeout = null;
5046
+ }
5047
+ if (this.resizeModeIndicator && this.resizeModeIndicator.parentNode) {
5048
+ this.resizeModeIndicator.remove();
5049
+ }
5050
+ this.resizeModeIndicator = null;
5051
+ }
5052
+ onPointerResizeModeChange(enabled) {
5053
+ this.updateResizeOptionState();
5054
+ if (enabled) {
5055
+ this.showResizeModeIndicator();
5056
+ this.announceLive(i18n.t("transcript.resizeModeEnabled"));
5057
+ } else {
5058
+ this.hideResizeModeIndicator();
5059
+ this.announceLive(i18n.t("transcript.resizeModeDisabled"));
4331
5060
  }
4332
- }
4333
- /**
4334
- * Stop resizing
4335
- */
4336
- stopResize() {
4337
- this.isResizing = false;
4338
- this.resizeDirection = null;
4339
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizing`);
4340
- document.body.style.cursor = "";
4341
- document.body.style.userSelect = "";
4342
- }
4343
- /**
4344
- * Get cursor style for resize direction
4345
- */
4346
- getResizeCursor(direction) {
4347
- const cursors = {
4348
- "n": "ns-resize",
4349
- "s": "ns-resize",
4350
- "e": "ew-resize",
4351
- "w": "ew-resize",
4352
- "ne": "nesw-resize",
4353
- "nw": "nwse-resize",
4354
- "se": "nwse-resize",
4355
- "sw": "nesw-resize"
4356
- };
4357
- return cursors[direction] || "default";
4358
5061
  }
4359
5062
  /**
4360
5063
  * Show style dialog
@@ -4588,30 +5291,50 @@ var TranscriptManager = class {
4588
5291
  entry.style.fontFamily = this.transcriptStyle.fontFamily;
4589
5292
  });
4590
5293
  }
5294
+ /**
5295
+ * Set a managed timeout that will be cleaned up on destroy
5296
+ * @param {Function} callback - Callback function
5297
+ * @param {number} delay - Delay in milliseconds
5298
+ * @returns {number} Timeout ID
5299
+ */
5300
+ setManagedTimeout(callback, delay) {
5301
+ const timeoutId = setTimeout(() => {
5302
+ this.timeouts.delete(timeoutId);
5303
+ callback();
5304
+ }, delay);
5305
+ this.timeouts.add(timeoutId);
5306
+ return timeoutId;
5307
+ }
5308
+ /**
5309
+ * Clear a managed timeout
5310
+ * @param {number} timeoutId - Timeout ID to clear
5311
+ */
5312
+ clearManagedTimeout(timeoutId) {
5313
+ if (timeoutId) {
5314
+ clearTimeout(timeoutId);
5315
+ this.timeouts.delete(timeoutId);
5316
+ }
5317
+ }
4591
5318
  /**
4592
5319
  * Cleanup
4593
5320
  */
4594
5321
  destroy() {
4595
- if (this.resizeEnabled) {
4596
- this.disableResizeHandles();
5322
+ this.hideResizeModeIndicator();
5323
+ if (this.draggableResizable) {
5324
+ if (this.draggableResizable.pointerResizeMode) {
5325
+ this.draggableResizable.disablePointerResizeMode();
5326
+ this.updateResizeOptionState();
5327
+ }
5328
+ this.draggableResizable.destroy();
5329
+ this.draggableResizable = null;
4597
5330
  }
4598
- if (this.keyboardDragMode) {
4599
- this.disableKeyboardDragMode();
5331
+ if (this.transcriptWindow && this.customKeyHandler) {
5332
+ this.transcriptWindow.removeEventListener("keydown", this.customKeyHandler);
5333
+ this.customKeyHandler = null;
4600
5334
  }
4601
5335
  if (this.handlers.timeupdate) {
4602
5336
  this.player.off("timeupdate", this.handlers.timeupdate);
4603
5337
  }
4604
- if (this.transcriptHeader) {
4605
- if (this.handlers.mousedown) {
4606
- this.transcriptHeader.removeEventListener("mousedown", this.handlers.mousedown);
4607
- }
4608
- if (this.handlers.touchstart) {
4609
- this.transcriptHeader.removeEventListener("touchstart", this.handlers.touchstart);
4610
- }
4611
- if (this.handlers.keydown) {
4612
- this.transcriptHeader.removeEventListener("keydown", this.handlers.keydown);
4613
- }
4614
- }
4615
5338
  if (this.settingsButton) {
4616
5339
  if (this.handlers.settingsClick) {
4617
5340
  this.settingsButton.removeEventListener("click", this.handlers.settingsClick);
@@ -4623,24 +5346,14 @@ var TranscriptManager = class {
4623
5346
  if (this.styleDialog && this.handlers.styleDialogKeydown) {
4624
5347
  this.styleDialog.removeEventListener("keydown", this.handlers.styleDialogKeydown);
4625
5348
  }
4626
- if (this.handlers.mousemove) {
4627
- document.removeEventListener("mousemove", this.handlers.mousemove);
4628
- }
4629
- if (this.handlers.mouseup) {
4630
- document.removeEventListener("mouseup", this.handlers.mouseup);
4631
- }
4632
- if (this.handlers.touchmove) {
4633
- document.removeEventListener("touchmove", this.handlers.touchmove);
4634
- }
4635
- if (this.handlers.touchend) {
4636
- document.removeEventListener("touchend", this.handlers.touchend);
4637
- }
4638
5349
  if (this.handlers.documentClick) {
4639
5350
  document.removeEventListener("click", this.handlers.documentClick);
4640
5351
  }
4641
5352
  if (this.handlers.resize) {
4642
5353
  window.removeEventListener("resize", this.handlers.resize);
4643
5354
  }
5355
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
5356
+ this.timeouts.clear();
4644
5357
  this.handlers = null;
4645
5358
  if (this.transcriptWindow && this.transcriptWindow.parentNode) {
4646
5359
  this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
@@ -4651,6 +5364,14 @@ var TranscriptManager = class {
4651
5364
  this.transcriptEntries = [];
4652
5365
  this.settingsMenu = null;
4653
5366
  this.styleDialog = null;
5367
+ this.transcriptResizeHandles = [];
5368
+ this.resizeOptionButton = null;
5369
+ this.resizeOptionText = null;
5370
+ this.liveRegion = null;
5371
+ }
5372
+ announceLive(message) {
5373
+ if (!this.liveRegion) return;
5374
+ this.liveRegion.textContent = message || "";
4654
5375
  }
4655
5376
  };
4656
5377
 
@@ -5312,7 +6033,7 @@ var HLSRenderer = class {
5312
6033
  };
5313
6034
 
5314
6035
  // src/core/Player.js
5315
- var Player = class extends EventEmitter {
6036
+ var Player = class _Player extends EventEmitter {
5316
6037
  constructor(element, options = {}) {
5317
6038
  super();
5318
6039
  this.element = typeof element === "string" ? document.querySelector(element) : element;
@@ -5420,6 +6141,8 @@ var Player = class extends EventEmitter {
5420
6141
  screenReaderAnnouncements: true,
5421
6142
  highContrast: false,
5422
6143
  focusHighlight: true,
6144
+ metadataAlerts: {},
6145
+ metadataHashtags: {},
5423
6146
  // Languages
5424
6147
  language: "en",
5425
6148
  languages: ["en"],
@@ -5438,6 +6161,8 @@ var Player = class extends EventEmitter {
5438
6161
  onError: null,
5439
6162
  ...options
5440
6163
  };
6164
+ this.options.metadataAlerts = this.options.metadataAlerts || {};
6165
+ this.options.metadataHashtags = this.options.metadataHashtags || {};
5441
6166
  this.storage = new StorageManager("vidply");
5442
6167
  const savedPrefs = this.storage.getPlayerPreferences();
5443
6168
  if (savedPrefs) {
@@ -5472,12 +6197,22 @@ var Player = class extends EventEmitter {
5472
6197
  this.audioDescriptionSourceElement = null;
5473
6198
  this.originalAudioDescriptionSource = null;
5474
6199
  this.audioDescriptionCaptionTracks = [];
6200
+ this._audioDescriptionDesiredState = false;
6201
+ this._textTracksCache = null;
6202
+ this._textTracksDirty = true;
6203
+ this._sourceElementsCache = null;
6204
+ this._sourceElementsDirty = true;
6205
+ this._trackElementsCache = null;
6206
+ this._trackElementsDirty = true;
6207
+ this.timeouts = /* @__PURE__ */ new Set();
5475
6208
  this.container = null;
5476
6209
  this.renderer = null;
5477
6210
  this.controlBar = null;
5478
6211
  this.captionManager = null;
5479
6212
  this.keyboardManager = null;
5480
6213
  this.settingsDialog = null;
6214
+ this.metadataCueChangeHandler = null;
6215
+ this.metadataAlertHandlers = /* @__PURE__ */ new Map();
5481
6216
  this.init();
5482
6217
  }
5483
6218
  async init() {
@@ -5509,6 +6244,7 @@ var Player = class extends EventEmitter {
5509
6244
  if (this.options.transcript || this.options.transcriptButton) {
5510
6245
  this.transcriptManager = new TranscriptManager(this);
5511
6246
  }
6247
+ this.setupMetadataHandling();
5512
6248
  if (this.options.keyboard) {
5513
6249
  this.keyboardManager = new KeyboardManager(this);
5514
6250
  }
@@ -5579,7 +6315,6 @@ var Player = class extends EventEmitter {
5579
6315
  this.element.style.height = "100%";
5580
6316
  if (this.element.tagName === "VIDEO" && this.options.playsInline) {
5581
6317
  this.element.setAttribute("playsinline", "");
5582
- this.element.setAttribute("webkit-playsinline", "");
5583
6318
  this.element.playsInline = true;
5584
6319
  }
5585
6320
  if (this.options.width) {
@@ -5594,12 +6329,22 @@ var Player = class extends EventEmitter {
5594
6329
  if (this.element.tagName === "VIDEO") {
5595
6330
  this.createPlayButtonOverlay();
5596
6331
  }
6332
+ this.element.vidply = this;
6333
+ _Player.instances.push(this);
5597
6334
  this.element.style.cursor = "pointer";
5598
6335
  this.element.addEventListener("click", (e) => {
5599
6336
  if (e.target === this.element) {
5600
6337
  this.toggle();
5601
6338
  }
5602
6339
  });
6340
+ this.on("play", () => {
6341
+ this.hidePosterOverlay();
6342
+ });
6343
+ this.on("timeupdate", () => {
6344
+ if (this.state.currentTime > 0) {
6345
+ this.hidePosterOverlay();
6346
+ }
6347
+ });
5603
6348
  }
5604
6349
  createPlayButtonOverlay() {
5605
6350
  this.playButtonOverlay = createPlayOverlay();
@@ -5626,7 +6371,7 @@ var Player = class extends EventEmitter {
5626
6371
  if (!src) {
5627
6372
  throw new Error("No media source found");
5628
6373
  }
5629
- const sourceElements = this.element.querySelectorAll("source");
6374
+ const sourceElements = this.sourceElements;
5630
6375
  for (const sourceEl of sourceElements) {
5631
6376
  const descSrc = sourceEl.getAttribute("data-desc-src");
5632
6377
  const origSrc = sourceEl.getAttribute("data-orig-src");
@@ -5655,7 +6400,7 @@ var Player = class extends EventEmitter {
5655
6400
  }
5656
6401
  }
5657
6402
  }
5658
- const trackElements = this.element.querySelectorAll("track");
6403
+ const trackElements = this.trackElements;
5659
6404
  trackElements.forEach((trackEl) => {
5660
6405
  const trackKind = trackEl.getAttribute("kind");
5661
6406
  const trackDescSrc = trackEl.getAttribute("data-desc-src");
@@ -5676,19 +6421,137 @@ var Player = class extends EventEmitter {
5676
6421
  if (!this.originalSrc) {
5677
6422
  this.originalSrc = src;
5678
6423
  }
5679
- let renderer;
5680
- if (src.includes("youtube.com") || src.includes("youtu.be")) {
5681
- renderer = YouTubeRenderer;
5682
- } else if (src.includes("vimeo.com")) {
5683
- renderer = VimeoRenderer;
5684
- } else if (src.includes(".m3u8")) {
5685
- renderer = HLSRenderer;
5686
- } else {
5687
- renderer = HTML5Renderer;
6424
+ let renderer;
6425
+ if (src.includes("youtube.com") || src.includes("youtu.be")) {
6426
+ renderer = YouTubeRenderer;
6427
+ } else if (src.includes("vimeo.com")) {
6428
+ renderer = VimeoRenderer;
6429
+ } else if (src.includes(".m3u8")) {
6430
+ renderer = HLSRenderer;
6431
+ } else {
6432
+ renderer = HTML5Renderer;
6433
+ }
6434
+ this.log(`Using ${renderer.name} renderer`);
6435
+ this.renderer = new renderer(this);
6436
+ await this.renderer.init();
6437
+ this.invalidateTrackCache();
6438
+ }
6439
+ /**
6440
+ * Get cached text tracks array
6441
+ * @returns {Array} Array of text tracks
6442
+ */
6443
+ get textTracks() {
6444
+ if (!this._textTracksCache || this._textTracksDirty) {
6445
+ this._textTracksCache = Array.from(this.element.textTracks || []);
6446
+ this._textTracksDirty = false;
6447
+ }
6448
+ return this._textTracksCache;
6449
+ }
6450
+ /**
6451
+ * Get cached source elements array
6452
+ * @returns {Array} Array of source elements
6453
+ */
6454
+ get sourceElements() {
6455
+ if (!this._sourceElementsCache || this._sourceElementsDirty) {
6456
+ this._sourceElementsCache = Array.from(this.element.querySelectorAll("source"));
6457
+ this._sourceElementsDirty = false;
6458
+ }
6459
+ return this._sourceElementsCache;
6460
+ }
6461
+ /**
6462
+ * Get cached track elements array
6463
+ * @returns {Array} Array of track elements
6464
+ */
6465
+ get trackElements() {
6466
+ if (!this._trackElementsCache || this._trackElementsDirty) {
6467
+ this._trackElementsCache = Array.from(this.element.querySelectorAll("track"));
6468
+ this._trackElementsDirty = false;
6469
+ }
6470
+ return this._trackElementsCache;
6471
+ }
6472
+ /**
6473
+ * Invalidate DOM query cache (call when tracks/sources change)
6474
+ */
6475
+ invalidateTrackCache() {
6476
+ this._textTracksDirty = true;
6477
+ this._trackElementsDirty = true;
6478
+ this._sourceElementsDirty = true;
6479
+ }
6480
+ /**
6481
+ * Find a text track by kind and optionally language
6482
+ * @param {string} kind - Track kind (captions, subtitles, descriptions, chapters, metadata)
6483
+ * @param {string} [language] - Optional language code
6484
+ * @returns {TextTrack|null} Found track or null
6485
+ */
6486
+ findTextTrack(kind, language = null) {
6487
+ const tracks = this.textTracks;
6488
+ if (language) {
6489
+ return tracks.find((t) => t.kind === kind && t.language === language);
6490
+ }
6491
+ return tracks.find((t) => t.kind === kind);
6492
+ }
6493
+ /**
6494
+ * Find a source element by attribute
6495
+ * @param {string} attribute - Attribute name (e.g., 'data-desc-src')
6496
+ * @param {string} [value] - Optional attribute value
6497
+ * @returns {Element|null} Found source element or null
6498
+ */
6499
+ findSourceElement(attribute, value = null) {
6500
+ const sources = this.sourceElements;
6501
+ if (value) {
6502
+ return sources.find((el) => el.getAttribute(attribute) === value);
6503
+ }
6504
+ return sources.find((el) => el.hasAttribute(attribute));
6505
+ }
6506
+ /**
6507
+ * Find a track element by its associated TextTrack
6508
+ * @param {TextTrack} track - The TextTrack object
6509
+ * @returns {Element|null} Found track element or null
6510
+ */
6511
+ findTrackElement(track) {
6512
+ return this.trackElements.find((el) => el.track === track);
6513
+ }
6514
+ showPosterOverlay() {
6515
+ if (!this.videoWrapper || this.element.tagName !== "VIDEO") {
6516
+ return;
6517
+ }
6518
+ const poster = this.element.getAttribute("poster") || this.element.poster || this.options.poster;
6519
+ if (!poster) {
6520
+ return;
6521
+ }
6522
+ this.videoWrapper.style.setProperty("--vidply-poster-image", `url("${poster}")`);
6523
+ this.videoWrapper.classList.add("vidply-forced-poster");
6524
+ }
6525
+ hidePosterOverlay() {
6526
+ if (!this.videoWrapper) {
6527
+ return;
6528
+ }
6529
+ this.videoWrapper.classList.remove("vidply-forced-poster");
6530
+ this.videoWrapper.style.removeProperty("--vidply-poster-image");
6531
+ }
6532
+ /**
6533
+ * Set a managed timeout that will be cleaned up on destroy
6534
+ * @param {Function} callback - Callback function
6535
+ * @param {number} delay - Delay in milliseconds
6536
+ * @returns {number} Timeout ID
6537
+ */
6538
+ setManagedTimeout(callback, delay) {
6539
+ const timeoutId = setTimeout(() => {
6540
+ this.timeouts.delete(timeoutId);
6541
+ callback();
6542
+ }, delay);
6543
+ this.timeouts.add(timeoutId);
6544
+ return timeoutId;
6545
+ }
6546
+ /**
6547
+ * Clear a managed timeout
6548
+ * @param {number} timeoutId - Timeout ID to clear
6549
+ */
6550
+ clearManagedTimeout(timeoutId) {
6551
+ if (timeoutId) {
6552
+ clearTimeout(timeoutId);
6553
+ this.timeouts.delete(timeoutId);
5688
6554
  }
5689
- this.log(`Using ${renderer.name} renderer`);
5690
- this.renderer = new renderer(this);
5691
- await this.renderer.init();
5692
6555
  }
5693
6556
  /**
5694
6557
  * Load new media source (for playlists)
@@ -5704,8 +6567,9 @@ var Player = class extends EventEmitter {
5704
6567
  if (this.renderer) {
5705
6568
  this.pause();
5706
6569
  }
5707
- const existingTracks = this.element.querySelectorAll("track");
6570
+ const existingTracks = this.trackElements;
5708
6571
  existingTracks.forEach((track) => track.remove());
6572
+ this.invalidateTrackCache();
5709
6573
  this.element.src = config.src;
5710
6574
  if (config.type) {
5711
6575
  this.element.type = config.type;
@@ -5725,6 +6589,7 @@ var Player = class extends EventEmitter {
5725
6589
  }
5726
6590
  this.element.appendChild(track);
5727
6591
  });
6592
+ this.invalidateTrackCache();
5728
6593
  }
5729
6594
  const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
5730
6595
  if (shouldChangeRenderer && this.renderer) {
@@ -5972,7 +6837,7 @@ var Player = class extends EventEmitter {
5972
6837
  }
5973
6838
  // Audio Description
5974
6839
  async enableAudioDescription() {
5975
- const hasSourceElementsWithDesc = Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
6840
+ const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
5976
6841
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
5977
6842
  if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
5978
6843
  console.warn("VidPly: No audio description source, source elements, or tracks provided");
@@ -5980,10 +6845,14 @@ var Player = class extends EventEmitter {
5980
6845
  }
5981
6846
  const currentTime = this.state.currentTime;
5982
6847
  const wasPlaying = this.state.playing;
6848
+ const shouldKeepPoster = !wasPlaying && currentTime === 0;
6849
+ if (shouldKeepPoster) {
6850
+ this.showPosterOverlay();
6851
+ }
5983
6852
  let swappedTracksForTranscript = [];
5984
6853
  if (this.audioDescriptionSourceElement) {
5985
6854
  const currentSrc = this.element.currentSrc || this.element.src;
5986
- const sourceElements = Array.from(this.element.querySelectorAll("source"));
6855
+ const sourceElements = this.sourceElements;
5987
6856
  let sourceElementToUpdate = null;
5988
6857
  let descSrc = this.audioDescriptionSrc;
5989
6858
  for (const sourceEl of sourceElements) {
@@ -6080,8 +6949,9 @@ var Player = class extends EventEmitter {
6080
6949
  trackInfo.trackElement = newTrackElement;
6081
6950
  });
6082
6951
  this.element.load();
6952
+ this.invalidateTrackCache();
6083
6953
  const setupNewTracks = () => {
6084
- setTimeout(() => {
6954
+ this.setManagedTimeout(() => {
6085
6955
  swappedTracksForTranscript.forEach((trackInfo) => {
6086
6956
  const trackElement = trackInfo.trackElement;
6087
6957
  const newTextTrack = trackElement.track;
@@ -6117,7 +6987,7 @@ var Player = class extends EventEmitter {
6117
6987
  const skippedCount = validationResults.length - tracksToSwap.length;
6118
6988
  }
6119
6989
  }
6120
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
6990
+ const allSourceElements = this.sourceElements;
6121
6991
  const sourcesToUpdate = [];
6122
6992
  allSourceElements.forEach((sourceEl) => {
6123
6993
  const descSrcAttr = sourceEl.getAttribute("data-desc-src");
@@ -6161,8 +7031,15 @@ var Player = class extends EventEmitter {
6161
7031
  if (sourceInfo.descSrc) {
6162
7032
  newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6163
7033
  }
6164
- this.element.appendChild(newSource);
7034
+ const firstTrack = this.element.querySelector("track");
7035
+ if (firstTrack) {
7036
+ this.element.insertBefore(newSource, firstTrack);
7037
+ } else {
7038
+ this.element.appendChild(newSource);
7039
+ }
6165
7040
  });
7041
+ this._sourceElementsDirty = true;
7042
+ this._sourceElementsCache = null;
6166
7043
  this.element.load();
6167
7044
  await new Promise((resolve) => {
6168
7045
  const onLoadedMetadata = () => {
@@ -6172,18 +7049,18 @@ var Player = class extends EventEmitter {
6172
7049
  this.element.addEventListener("loadedmetadata", onLoadedMetadata);
6173
7050
  });
6174
7051
  await new Promise((resolve) => setTimeout(resolve, 300));
6175
- if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6176
- if (this.element.readyState >= 1) {
6177
- this.element.currentTime = 1e-3;
6178
- setTimeout(() => {
6179
- this.element.currentTime = 0;
6180
- }, 10);
6181
- }
7052
+ if (currentTime > 0) {
7053
+ this.seek(currentTime);
6182
7054
  }
6183
- this.seek(currentTime);
6184
7055
  if (wasPlaying) {
6185
7056
  this.play();
6186
7057
  }
7058
+ if (!shouldKeepPoster) {
7059
+ this.hidePosterOverlay();
7060
+ }
7061
+ if (!this._audioDescriptionDesiredState) {
7062
+ return;
7063
+ }
6187
7064
  this.state.audioDescriptionEnabled = true;
6188
7065
  this.emit("audiodescriptionenabled");
6189
7066
  } else {
@@ -6295,7 +7172,7 @@ var Player = class extends EventEmitter {
6295
7172
  }, 100);
6296
7173
  }
6297
7174
  }
6298
- const fallbackSourceElements = Array.from(this.element.querySelectorAll("source"));
7175
+ const fallbackSourceElements = this.sourceElements;
6299
7176
  const hasSourceElementsWithDesc2 = fallbackSourceElements.some((el) => el.getAttribute("data-desc-src"));
6300
7177
  if (hasSourceElementsWithDesc2) {
6301
7178
  const fallbackSourcesToUpdate = [];
@@ -6343,6 +7220,7 @@ var Player = class extends EventEmitter {
6343
7220
  this.element.appendChild(newSource);
6344
7221
  });
6345
7222
  this.element.load();
7223
+ this.invalidateTrackCache();
6346
7224
  } else {
6347
7225
  this.element.src = this.audioDescriptionSrc;
6348
7226
  }
@@ -6357,12 +7235,14 @@ var Player = class extends EventEmitter {
6357
7235
  if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6358
7236
  if (this.element.readyState >= 1) {
6359
7237
  this.element.currentTime = 1e-3;
6360
- setTimeout(() => {
7238
+ this.setManagedTimeout(() => {
6361
7239
  this.element.currentTime = 0;
6362
7240
  }, 10);
6363
7241
  }
6364
7242
  }
6365
- this.seek(currentTime);
7243
+ if (currentTime > 0) {
7244
+ this.seek(currentTime);
7245
+ }
6366
7246
  if (wasPlaying) {
6367
7247
  this.play();
6368
7248
  }
@@ -6397,7 +7277,8 @@ var Player = class extends EventEmitter {
6397
7277
  const swappedTracks = typeof swappedTracksForTranscript !== "undefined" ? swappedTracksForTranscript : [];
6398
7278
  if (swappedTracks.length > 0) {
6399
7279
  const onMetadataLoaded = () => {
6400
- const allTextTracks = Array.from(this.element.textTracks);
7280
+ this.invalidateTrackCache();
7281
+ const allTextTracks = this.textTracks;
6401
7282
  const freshTracks = swappedTracks.map((trackInfo) => {
6402
7283
  const trackEl = trackInfo.trackElement;
6403
7284
  const expectedSrc = trackEl.getAttribute("src");
@@ -6407,9 +7288,7 @@ var Player = class extends EventEmitter {
6407
7288
  if (!foundTrack) {
6408
7289
  foundTrack = allTextTracks.find((track) => {
6409
7290
  if (track.language === srclang && (track.kind === kind || kind === "captions" && track.kind === "subtitles")) {
6410
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6411
- (el) => el.track === track
6412
- );
7291
+ const trackElementForTrack = this.findTrackElement(track);
6413
7292
  if (trackElementForTrack) {
6414
7293
  const actualSrc = trackElementForTrack.getAttribute("src");
6415
7294
  if (actualSrc === expectedSrc) {
@@ -6421,9 +7300,7 @@ var Player = class extends EventEmitter {
6421
7300
  });
6422
7301
  }
6423
7302
  if (foundTrack) {
6424
- const trackElement = Array.from(this.element.querySelectorAll("track")).find(
6425
- (el) => el.track === foundTrack
6426
- );
7303
+ const trackElement = this.findTrackElement(foundTrack);
6427
7304
  if (trackElement && trackElement.getAttribute("src") !== expectedSrc) {
6428
7305
  return null;
6429
7306
  }
@@ -6431,7 +7308,7 @@ var Player = class extends EventEmitter {
6431
7308
  return foundTrack;
6432
7309
  }).filter(Boolean);
6433
7310
  if (freshTracks.length === 0) {
6434
- setTimeout(() => {
7311
+ this.setManagedTimeout(() => {
6435
7312
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6436
7313
  this.transcriptManager.loadTranscriptData();
6437
7314
  }
@@ -6447,14 +7324,13 @@ var Player = class extends EventEmitter {
6447
7324
  const checkLoaded = () => {
6448
7325
  loadedCount++;
6449
7326
  if (loadedCount >= freshTracks.length) {
6450
- setTimeout(() => {
7327
+ this.setManagedTimeout(() => {
6451
7328
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6452
- const allTextTracks2 = Array.from(this.element.textTracks);
7329
+ this.invalidateTrackCache();
7330
+ const allTextTracks2 = this.textTracks;
6453
7331
  const swappedTrackSrcs = swappedTracks.map((t) => t.describedSrc);
6454
7332
  const hasCorrectTracks = freshTracks.some((track) => {
6455
- const trackEl = Array.from(this.element.querySelectorAll("track")).find(
6456
- (el) => el.track === track
6457
- );
7333
+ const trackEl = this.findTrackElement(track);
6458
7334
  return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute("src"));
6459
7335
  });
6460
7336
  if (hasCorrectTracks || freshTracks.length > 0) {
@@ -6468,9 +7344,7 @@ var Player = class extends EventEmitter {
6468
7344
  if (track.mode === "disabled") {
6469
7345
  track.mode = "hidden";
6470
7346
  }
6471
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6472
- (el) => el.track === track
6473
- );
7347
+ const trackElementForTrack = this.findTrackElement(track);
6474
7348
  const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute("src") : null;
6475
7349
  const expectedTrackInfo = swappedTracks.find((t) => {
6476
7350
  const tEl = t.trackElement;
@@ -6488,10 +7362,10 @@ var Player = class extends EventEmitter {
6488
7362
  track.mode = "hidden";
6489
7363
  }
6490
7364
  const onTrackLoad = () => {
6491
- setTimeout(checkLoaded, 300);
7365
+ this.setManagedTimeout(checkLoaded, 300);
6492
7366
  };
6493
7367
  if (track.readyState >= 2) {
6494
- setTimeout(() => {
7368
+ this.setManagedTimeout(() => {
6495
7369
  if (track.cues && track.cues.length > 0) {
6496
7370
  checkLoaded();
6497
7371
  } else {
@@ -6508,12 +7382,12 @@ var Player = class extends EventEmitter {
6508
7382
  });
6509
7383
  };
6510
7384
  const waitForTracks = () => {
6511
- setTimeout(() => {
7385
+ this.setManagedTimeout(() => {
6512
7386
  if (this.element.readyState >= 1) {
6513
7387
  onMetadataLoaded();
6514
7388
  } else {
6515
7389
  this.element.addEventListener("loadedmetadata", onMetadataLoaded, { once: true });
6516
- setTimeout(onMetadataLoaded, 2e3);
7390
+ this.setManagedTimeout(onMetadataLoaded, 2e3);
6517
7391
  }
6518
7392
  }, 500);
6519
7393
  };
@@ -6531,6 +7405,12 @@ var Player = class extends EventEmitter {
6531
7405
  }, 800);
6532
7406
  }
6533
7407
  }
7408
+ if (!shouldKeepPoster) {
7409
+ this.hidePosterOverlay();
7410
+ }
7411
+ if (!this._audioDescriptionDesiredState) {
7412
+ return;
7413
+ }
6534
7414
  this.state.audioDescriptionEnabled = true;
6535
7415
  this.emit("audiodescriptionenabled");
6536
7416
  }
@@ -6547,7 +7427,7 @@ var Player = class extends EventEmitter {
6547
7427
  }
6548
7428
  });
6549
7429
  }
6550
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
7430
+ const allSourceElements = this.sourceElements;
6551
7431
  const hasSourceElementsToSwap = allSourceElements.some((el) => el.getAttribute("data-orig-src"));
6552
7432
  if (hasSourceElementsToSwap) {
6553
7433
  const sourcesToRestore = [];
@@ -6590,61 +7470,107 @@ var Player = class extends EventEmitter {
6590
7470
  if (sourceInfo.descSrc) {
6591
7471
  newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6592
7472
  }
6593
- this.element.appendChild(newSource);
7473
+ const firstTrack = this.element.querySelector("track");
7474
+ if (firstTrack) {
7475
+ this.element.insertBefore(newSource, firstTrack);
7476
+ } else {
7477
+ this.element.appendChild(newSource);
7478
+ }
6594
7479
  });
7480
+ this._sourceElementsDirty = true;
7481
+ this._sourceElementsCache = null;
6595
7482
  this.element.load();
6596
7483
  } else {
6597
7484
  const originalSrcToUse = this.originalAudioDescriptionSource || this.originalSrc;
6598
7485
  this.element.src = originalSrcToUse;
6599
7486
  this.element.load();
6600
7487
  }
6601
- await new Promise((resolve) => {
6602
- const onLoadedMetadata = () => {
6603
- this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
6604
- resolve();
6605
- };
6606
- this.element.addEventListener("loadedmetadata", onLoadedMetadata);
6607
- });
6608
- this.seek(currentTime);
6609
- if (wasPlaying) {
6610
- this.play();
7488
+ if (currentTime > 0 || wasPlaying) {
7489
+ await new Promise((resolve) => {
7490
+ const onLoadedMetadata = () => {
7491
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
7492
+ resolve();
7493
+ };
7494
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
7495
+ });
7496
+ if (currentTime > 0) {
7497
+ this.seek(currentTime);
7498
+ }
7499
+ if (wasPlaying) {
7500
+ this.play();
7501
+ }
7502
+ }
7503
+ if (!wasPlaying && currentTime === 0) {
7504
+ this.showPosterOverlay();
7505
+ } else {
7506
+ this.hidePosterOverlay();
6611
7507
  }
6612
7508
  if (this.transcriptManager && this.transcriptManager.isVisible) {
6613
- setTimeout(() => {
7509
+ this.setManagedTimeout(() => {
6614
7510
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6615
7511
  this.transcriptManager.loadTranscriptData();
6616
7512
  }
6617
7513
  }, 500);
6618
7514
  }
7515
+ if (this._audioDescriptionDesiredState) {
7516
+ return;
7517
+ }
6619
7518
  this.state.audioDescriptionEnabled = false;
6620
7519
  this.emit("audiodescriptiondisabled");
6621
7520
  }
6622
7521
  async toggleAudioDescription() {
6623
- const textTracks = Array.from(this.element.textTracks || []);
6624
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
6625
- const hasAudioDescriptionSrc = this.audioDescriptionSrc || Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
7522
+ const descriptionTrack = this.findTextTrack("descriptions");
7523
+ const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6626
7524
  if (descriptionTrack && hasAudioDescriptionSrc) {
6627
7525
  if (this.state.audioDescriptionEnabled) {
7526
+ this._audioDescriptionDesiredState = false;
6628
7527
  descriptionTrack.mode = "hidden";
6629
7528
  await this.disableAudioDescription();
6630
7529
  } else {
7530
+ this._audioDescriptionDesiredState = true;
6631
7531
  await this.enableAudioDescription();
6632
- descriptionTrack.mode = "showing";
7532
+ const enableDescriptionTrack = () => {
7533
+ this.invalidateTrackCache();
7534
+ const descTrack = this.findTextTrack("descriptions");
7535
+ if (descTrack) {
7536
+ if (descTrack.mode === "disabled") {
7537
+ descTrack.mode = "hidden";
7538
+ this.setManagedTimeout(() => {
7539
+ descTrack.mode = "showing";
7540
+ }, 50);
7541
+ } else {
7542
+ descTrack.mode = "showing";
7543
+ }
7544
+ } else if (this.element.readyState < 2) {
7545
+ this.setManagedTimeout(enableDescriptionTrack, 100);
7546
+ }
7547
+ };
7548
+ if (this.element.readyState >= 1) {
7549
+ this.setManagedTimeout(enableDescriptionTrack, 200);
7550
+ } else {
7551
+ this.element.addEventListener("loadedmetadata", () => {
7552
+ this.setManagedTimeout(enableDescriptionTrack, 200);
7553
+ }, { once: true });
7554
+ }
6633
7555
  }
6634
7556
  } else if (descriptionTrack) {
6635
7557
  if (descriptionTrack.mode === "showing") {
7558
+ this._audioDescriptionDesiredState = false;
6636
7559
  descriptionTrack.mode = "hidden";
6637
7560
  this.state.audioDescriptionEnabled = false;
6638
7561
  this.emit("audiodescriptiondisabled");
6639
7562
  } else {
7563
+ this._audioDescriptionDesiredState = true;
6640
7564
  descriptionTrack.mode = "showing";
6641
7565
  this.state.audioDescriptionEnabled = true;
6642
7566
  this.emit("audiodescriptionenabled");
6643
7567
  }
6644
7568
  } else if (hasAudioDescriptionSrc) {
6645
7569
  if (this.state.audioDescriptionEnabled) {
7570
+ this._audioDescriptionDesiredState = false;
6646
7571
  await this.disableAudioDescription();
6647
7572
  } else {
7573
+ this._audioDescriptionDesiredState = true;
6648
7574
  await this.enableAudioDescription();
6649
7575
  }
6650
7576
  }
@@ -6741,177 +7667,22 @@ var Player = class extends EventEmitter {
6741
7667
  }
6742
7668
  setupSignLanguageInteraction() {
6743
7669
  if (!this.signLanguageWrapper) return;
6744
- let isDragging = false;
6745
- let isResizing = false;
6746
- let resizeDirection = null;
6747
- let startX = 0;
6748
- let startY = 0;
6749
- let startLeft = 0;
6750
- let startTop = 0;
6751
- let startWidth = 0;
6752
- let startHeight = 0;
6753
- let dragMode = false;
6754
- let resizeMode = false;
6755
- const onMouseDownVideo = (e) => {
6756
- if (e.target !== this.signLanguageVideo) return;
6757
- e.preventDefault();
6758
- isDragging = true;
6759
- startX = e.clientX;
6760
- startY = e.clientY;
6761
- const rect = this.signLanguageWrapper.getBoundingClientRect();
6762
- startLeft = rect.left;
6763
- startTop = rect.top;
6764
- this.signLanguageWrapper.classList.add("vidply-sign-dragging");
6765
- };
6766
- const onMouseDownHandle = (e) => {
6767
- if (!e.target.classList.contains("vidply-sign-resize-handle")) return;
6768
- e.preventDefault();
6769
- e.stopPropagation();
6770
- isResizing = true;
6771
- resizeDirection = e.target.getAttribute("data-direction");
6772
- startX = e.clientX;
6773
- startY = e.clientY;
6774
- const rect = this.signLanguageWrapper.getBoundingClientRect();
6775
- startLeft = rect.left;
6776
- startTop = rect.top;
6777
- startWidth = rect.width;
6778
- startHeight = rect.height;
6779
- this.signLanguageWrapper.classList.add("vidply-sign-resizing");
6780
- };
6781
- const onMouseMove = (e) => {
6782
- if (isDragging) {
6783
- const deltaX = e.clientX - startX;
6784
- const deltaY = e.clientY - startY;
6785
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
6786
- const containerRect = this.container.getBoundingClientRect();
6787
- const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
6788
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
6789
- const videoWrapperTop = videoWrapperRect.top - containerRect.top;
6790
- let newLeft = startLeft + deltaX - containerRect.left;
6791
- let newTop = startTop + deltaY - containerRect.top;
6792
- const controlsHeight = 95;
6793
- newLeft = Math.max(videoWrapperLeft, Math.min(newLeft, videoWrapperLeft + videoWrapperRect.width - wrapperRect.width));
6794
- newTop = Math.max(videoWrapperTop, Math.min(newTop, videoWrapperTop + videoWrapperRect.height - wrapperRect.height - controlsHeight));
6795
- this.signLanguageWrapper.style.left = `${newLeft}px`;
6796
- this.signLanguageWrapper.style.top = `${newTop}px`;
6797
- this.signLanguageWrapper.style.right = "auto";
6798
- this.signLanguageWrapper.style.bottom = "auto";
6799
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
6800
- } else if (isResizing) {
6801
- const deltaX = e.clientX - startX;
6802
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
6803
- const containerRect = this.container.getBoundingClientRect();
6804
- let newWidth = startWidth;
6805
- let newLeft = startLeft - containerRect.left;
6806
- if (resizeDirection.includes("e")) {
6807
- newWidth = Math.max(150, startWidth + deltaX);
6808
- const maxWidth = videoWrapperRect.right - startLeft;
6809
- newWidth = Math.min(newWidth, maxWidth);
6810
- }
6811
- if (resizeDirection.includes("w")) {
6812
- const proposedWidth = Math.max(150, startWidth - deltaX);
6813
- const proposedLeft = startLeft + (startWidth - proposedWidth) - containerRect.left;
6814
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
6815
- if (proposedLeft >= videoWrapperLeft) {
6816
- newWidth = proposedWidth;
6817
- newLeft = proposedLeft;
6818
- }
6819
- }
6820
- this.signLanguageWrapper.style.width = `${newWidth}px`;
6821
- this.signLanguageWrapper.style.height = "auto";
6822
- if (resizeDirection.includes("w")) {
6823
- this.signLanguageWrapper.style.left = `${newLeft}px`;
6824
- }
6825
- this.signLanguageWrapper.style.right = "auto";
6826
- this.signLanguageWrapper.style.bottom = "auto";
6827
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
6828
- }
6829
- };
6830
- const onMouseUp = () => {
6831
- if (isDragging || isResizing) {
6832
- this.saveSignLanguagePreferences();
6833
- }
6834
- isDragging = false;
6835
- isResizing = false;
6836
- resizeDirection = null;
6837
- this.signLanguageWrapper.classList.remove("vidply-sign-dragging", "vidply-sign-resizing");
6838
- };
6839
- const onKeyDown = (e) => {
6840
- if (e.key === "d" || e.key === "D") {
6841
- dragMode = !dragMode;
6842
- resizeMode = false;
6843
- this.signLanguageWrapper.classList.toggle("vidply-sign-keyboard-drag", dragMode);
6844
- this.signLanguageWrapper.classList.remove("vidply-sign-keyboard-resize");
6845
- e.preventDefault();
6846
- return;
6847
- }
6848
- if (e.key === "r" || e.key === "R") {
6849
- resizeMode = !resizeMode;
6850
- dragMode = false;
6851
- this.signLanguageWrapper.classList.toggle("vidply-sign-keyboard-resize", resizeMode);
6852
- this.signLanguageWrapper.classList.remove("vidply-sign-keyboard-drag");
6853
- e.preventDefault();
6854
- return;
6855
- }
6856
- if (e.key === "Escape") {
6857
- dragMode = false;
6858
- resizeMode = false;
6859
- this.signLanguageWrapper.classList.remove("vidply-sign-keyboard-drag", "vidply-sign-keyboard-resize");
6860
- e.preventDefault();
6861
- return;
6862
- }
6863
- if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
6864
- const step = e.shiftKey ? 10 : 5;
6865
- const rect = this.signLanguageWrapper.getBoundingClientRect();
6866
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
6867
- const containerRect = this.container.getBoundingClientRect();
6868
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
6869
- const videoWrapperTop = videoWrapperRect.top - containerRect.top;
6870
- if (dragMode) {
6871
- let left = rect.left - containerRect.left;
6872
- let top = rect.top - containerRect.top;
6873
- if (e.key === "ArrowLeft") left -= step;
6874
- if (e.key === "ArrowRight") left += step;
6875
- if (e.key === "ArrowUp") top -= step;
6876
- if (e.key === "ArrowDown") top += step;
6877
- const controlsHeight = 95;
6878
- left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperRect.width - rect.width));
6879
- top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperRect.height - rect.height - controlsHeight));
6880
- this.signLanguageWrapper.style.left = `${left}px`;
6881
- this.signLanguageWrapper.style.top = `${top}px`;
6882
- this.signLanguageWrapper.style.right = "auto";
6883
- this.signLanguageWrapper.style.bottom = "auto";
6884
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
6885
- this.saveSignLanguagePreferences();
6886
- e.preventDefault();
6887
- } else if (resizeMode) {
6888
- let width = rect.width;
6889
- if (e.key === "ArrowLeft") width -= step;
6890
- if (e.key === "ArrowRight") width += step;
6891
- if (e.key === "ArrowUp") width += step;
6892
- if (e.key === "ArrowDown") width -= step;
6893
- width = Math.max(150, width);
6894
- width = Math.min(width, videoWrapperRect.width);
6895
- this.signLanguageWrapper.style.width = `${width}px`;
6896
- this.signLanguageWrapper.style.height = "auto";
6897
- this.saveSignLanguagePreferences();
6898
- e.preventDefault();
6899
- }
6900
- }
6901
- };
6902
- this.signLanguageVideo.addEventListener("mousedown", onMouseDownVideo);
6903
- const handles = this.signLanguageWrapper.querySelectorAll(".vidply-sign-resize-handle");
6904
- handles.forEach((handle) => handle.addEventListener("mousedown", onMouseDownHandle));
6905
- document.addEventListener("mousemove", onMouseMove);
6906
- document.addEventListener("mouseup", onMouseUp);
6907
- this.signLanguageWrapper.addEventListener("keydown", onKeyDown);
7670
+ const resizeHandles = Array.from(this.signLanguageWrapper.querySelectorAll(".vidply-sign-resize-handle"));
7671
+ this.signLanguageDraggable = new DraggableResizable(this.signLanguageWrapper, {
7672
+ dragHandle: this.signLanguageVideo,
7673
+ resizeHandles,
7674
+ constrainToViewport: true,
7675
+ maintainAspectRatio: true,
7676
+ minWidth: 150,
7677
+ minHeight: 100,
7678
+ classPrefix: "vidply-sign",
7679
+ keyboardDragKey: "d",
7680
+ keyboardResizeKey: "r",
7681
+ keyboardStep: 5,
7682
+ keyboardStepLarge: 10
7683
+ });
6908
7684
  this.signLanguageInteractionHandlers = {
6909
- mouseDownVideo: onMouseDownVideo,
6910
- mouseDownHandle: onMouseDownHandle,
6911
- mouseMove: onMouseMove,
6912
- mouseUp: onMouseUp,
6913
- keyDown: onKeyDown,
6914
- handles
7685
+ draggable: this.signLanguageDraggable
6915
7686
  };
6916
7687
  }
6917
7688
  constrainSignLanguagePosition() {
@@ -6978,22 +7749,11 @@ var Player = class extends EventEmitter {
6978
7749
  this.off("ratechange", this.signLanguageHandlers.ratechange);
6979
7750
  this.signLanguageHandlers = null;
6980
7751
  }
6981
- if (this.signLanguageInteractionHandlers) {
6982
- if (this.signLanguageVideo) {
6983
- this.signLanguageVideo.removeEventListener("mousedown", this.signLanguageInteractionHandlers.mouseDownVideo);
6984
- }
6985
- if (this.signLanguageInteractionHandlers.handles) {
6986
- this.signLanguageInteractionHandlers.handles.forEach((handle) => {
6987
- handle.removeEventListener("mousedown", this.signLanguageInteractionHandlers.mouseDownHandle);
6988
- });
6989
- }
6990
- document.removeEventListener("mousemove", this.signLanguageInteractionHandlers.mouseMove);
6991
- document.removeEventListener("mouseup", this.signLanguageInteractionHandlers.mouseUp);
6992
- if (this.signLanguageWrapper) {
6993
- this.signLanguageWrapper.removeEventListener("keydown", this.signLanguageInteractionHandlers.keyDown);
6994
- }
6995
- this.signLanguageInteractionHandlers = null;
7752
+ if (this.signLanguageDraggable) {
7753
+ this.signLanguageDraggable.destroy();
7754
+ this.signLanguageDraggable = null;
6996
7755
  }
7756
+ this.signLanguageInteractionHandlers = null;
6997
7757
  if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
6998
7758
  if (this.signLanguageVideo) {
6999
7759
  this.signLanguageVideo.pause();
@@ -7042,9 +7802,25 @@ var Player = class extends EventEmitter {
7042
7802
  }
7043
7803
  }
7044
7804
  // Logging
7045
- log(message, type = "log") {
7046
- if (this.options.debug) {
7047
- console[type](`[VidPly]`, message);
7805
+ log(...messages) {
7806
+ if (!this.options.debug) {
7807
+ return;
7808
+ }
7809
+ let type = "log";
7810
+ if (messages.length > 0) {
7811
+ const potentialType = messages[messages.length - 1];
7812
+ if (typeof potentialType === "string" && console[potentialType]) {
7813
+ type = potentialType;
7814
+ messages = messages.slice(0, -1);
7815
+ }
7816
+ }
7817
+ if (messages.length === 0) {
7818
+ messages = [""];
7819
+ }
7820
+ if (typeof console[type] === "function") {
7821
+ console[type]("[VidPly]", ...messages);
7822
+ } else {
7823
+ console.log("[VidPly]", ...messages);
7048
7824
  }
7049
7825
  }
7050
7826
  // Setup responsive handlers
@@ -7069,7 +7845,9 @@ var Player = class extends EventEmitter {
7069
7845
  this.controlBar.updateControlsForViewport(width);
7070
7846
  }
7071
7847
  if (this.transcriptManager && this.transcriptManager.isVisible) {
7072
- this.transcriptManager.positionTranscript();
7848
+ if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
7849
+ this.transcriptManager.positionTranscript();
7850
+ }
7073
7851
  }
7074
7852
  };
7075
7853
  window.addEventListener("resize", this.resizeHandler);
@@ -7078,7 +7856,9 @@ var Player = class extends EventEmitter {
7078
7856
  this.orientationHandler = (e) => {
7079
7857
  setTimeout(() => {
7080
7858
  if (this.transcriptManager && this.transcriptManager.isVisible) {
7081
- this.transcriptManager.positionTranscript();
7859
+ if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
7860
+ this.transcriptManager.positionTranscript();
7861
+ }
7082
7862
  }
7083
7863
  }, 100);
7084
7864
  };
@@ -7104,7 +7884,7 @@ var Player = class extends EventEmitter {
7104
7884
  this.controlBar.updateFullscreenButton();
7105
7885
  }
7106
7886
  if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== "none") {
7107
- setTimeout(() => {
7887
+ this.setManagedTimeout(() => {
7108
7888
  requestAnimationFrame(() => {
7109
7889
  this.storage.saveSignLanguagePreferences({ size: null });
7110
7890
  this.signLanguageDesiredPosition = "bottom-right";
@@ -7167,12 +7947,368 @@ var Player = class extends EventEmitter {
7167
7947
  document.removeEventListener("MSFullscreenChange", this.fullscreenChangeHandler);
7168
7948
  this.fullscreenChangeHandler = null;
7169
7949
  }
7950
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
7951
+ this.timeouts.clear();
7952
+ if (this.metadataCueChangeHandler) {
7953
+ const textTracks = this.textTracks;
7954
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
7955
+ if (metadataTrack) {
7956
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
7957
+ }
7958
+ this.metadataCueChangeHandler = null;
7959
+ }
7960
+ if (this.metadataAlertHandlers && this.metadataAlertHandlers.size > 0) {
7961
+ this.metadataAlertHandlers.forEach(({ button, handler }) => {
7962
+ if (button && handler) {
7963
+ button.removeEventListener("click", handler);
7964
+ }
7965
+ });
7966
+ this.metadataAlertHandlers.clear();
7967
+ }
7170
7968
  if (this.container && this.container.parentNode) {
7171
7969
  this.container.parentNode.insertBefore(this.element, this.container);
7172
7970
  this.container.parentNode.removeChild(this.container);
7173
7971
  }
7174
7972
  this.removeAllListeners();
7175
7973
  }
7974
+ /**
7975
+ * Setup metadata track handling
7976
+ * This enables metadata tracks and listens for cue changes to trigger actions
7977
+ */
7978
+ setupMetadataHandling() {
7979
+ const setupMetadata = () => {
7980
+ const textTracks = this.textTracks;
7981
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
7982
+ if (metadataTrack) {
7983
+ if (metadataTrack.mode === "disabled") {
7984
+ metadataTrack.mode = "hidden";
7985
+ }
7986
+ if (this.metadataCueChangeHandler) {
7987
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
7988
+ }
7989
+ this.metadataCueChangeHandler = () => {
7990
+ const activeCues = Array.from(metadataTrack.activeCues || []);
7991
+ if (activeCues.length > 0) {
7992
+ if (this.options.debug) {
7993
+ this.log("[Metadata] Active cues:", activeCues.map((c) => ({
7994
+ start: c.startTime,
7995
+ end: c.endTime,
7996
+ text: c.text
7997
+ })));
7998
+ }
7999
+ }
8000
+ activeCues.forEach((cue) => {
8001
+ this.handleMetadataCue(cue);
8002
+ });
8003
+ };
8004
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
8005
+ if (this.options.debug) {
8006
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
8007
+ this.log("[Metadata] Track enabled,", cueCount, "cues available");
8008
+ }
8009
+ } else if (this.options.debug) {
8010
+ this.log("[Metadata] No metadata track found");
8011
+ }
8012
+ };
8013
+ setupMetadata();
8014
+ this.on("loadedmetadata", setupMetadata);
8015
+ }
8016
+ normalizeMetadataSelector(selector) {
8017
+ if (!selector) {
8018
+ return null;
8019
+ }
8020
+ const trimmed = selector.trim();
8021
+ if (!trimmed) {
8022
+ return null;
8023
+ }
8024
+ if (trimmed.startsWith("#") || trimmed.startsWith(".") || trimmed.startsWith("[")) {
8025
+ return trimmed;
8026
+ }
8027
+ return `#${trimmed}`;
8028
+ }
8029
+ resolveMetadataConfig(map, key) {
8030
+ if (!map || !key) {
8031
+ return null;
8032
+ }
8033
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
8034
+ return map[key];
8035
+ }
8036
+ const withoutHash = key.replace(/^#/, "");
8037
+ if (Object.prototype.hasOwnProperty.call(map, withoutHash)) {
8038
+ return map[withoutHash];
8039
+ }
8040
+ return null;
8041
+ }
8042
+ cacheMetadataAlertContent(element, config = {}) {
8043
+ if (!element) {
8044
+ return;
8045
+ }
8046
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
8047
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
8048
+ const titleEl = element.querySelector(titleSelector);
8049
+ if (titleEl && !titleEl.dataset.vidplyAlertTitleOriginal) {
8050
+ titleEl.dataset.vidplyAlertTitleOriginal = titleEl.textContent.trim();
8051
+ }
8052
+ const messageEl = element.querySelector(messageSelector);
8053
+ if (messageEl && !messageEl.dataset.vidplyAlertMessageOriginal) {
8054
+ messageEl.dataset.vidplyAlertMessageOriginal = messageEl.textContent.trim();
8055
+ }
8056
+ }
8057
+ restoreMetadataAlertContent(element, config = {}) {
8058
+ if (!element) {
8059
+ return;
8060
+ }
8061
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
8062
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
8063
+ const titleEl = element.querySelector(titleSelector);
8064
+ if (titleEl && titleEl.dataset.vidplyAlertTitleOriginal) {
8065
+ titleEl.textContent = titleEl.dataset.vidplyAlertTitleOriginal;
8066
+ }
8067
+ const messageEl = element.querySelector(messageSelector);
8068
+ if (messageEl && messageEl.dataset.vidplyAlertMessageOriginal) {
8069
+ messageEl.textContent = messageEl.dataset.vidplyAlertMessageOriginal;
8070
+ }
8071
+ }
8072
+ focusMetadataTarget(target, fallbackElement = null) {
8073
+ var _a, _b;
8074
+ if (!target || target === "none") {
8075
+ return;
8076
+ }
8077
+ if (target === "alert" && fallbackElement) {
8078
+ fallbackElement.focus();
8079
+ return;
8080
+ }
8081
+ if (target === "player") {
8082
+ if (this.container) {
8083
+ this.container.focus();
8084
+ }
8085
+ return;
8086
+ }
8087
+ if (target === "media") {
8088
+ this.element.focus();
8089
+ return;
8090
+ }
8091
+ if (target === "playButton") {
8092
+ const playButton = (_b = (_a = this.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.playPause;
8093
+ if (playButton) {
8094
+ playButton.focus();
8095
+ }
8096
+ return;
8097
+ }
8098
+ if (typeof target === "string") {
8099
+ const targetElement = document.querySelector(target);
8100
+ if (targetElement) {
8101
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
8102
+ targetElement.setAttribute("tabindex", "-1");
8103
+ }
8104
+ targetElement.focus();
8105
+ }
8106
+ }
8107
+ }
8108
+ handleMetadataAlert(selector, options = {}) {
8109
+ if (!selector) {
8110
+ return;
8111
+ }
8112
+ const config = this.resolveMetadataConfig(this.options.metadataAlerts, selector) || {};
8113
+ const element = options.element || document.querySelector(selector);
8114
+ if (!element) {
8115
+ if (this.options.debug) {
8116
+ this.log("[Metadata] Alert element not found:", selector);
8117
+ }
8118
+ return;
8119
+ }
8120
+ if (this.options.debug) {
8121
+ this.log("[Metadata] Handling alert", selector, { reason: options.reason, config });
8122
+ }
8123
+ this.cacheMetadataAlertContent(element, config);
8124
+ if (!element.dataset.vidplyAlertOriginalDisplay) {
8125
+ element.dataset.vidplyAlertOriginalDisplay = element.style.display || "";
8126
+ }
8127
+ if (!element.dataset.vidplyAlertDisplay) {
8128
+ element.dataset.vidplyAlertDisplay = config.display || "block";
8129
+ }
8130
+ const shouldShow = options.show !== void 0 ? options.show : config.show !== false;
8131
+ if (shouldShow) {
8132
+ const displayValue = config.display || element.dataset.vidplyAlertDisplay || "block";
8133
+ element.style.display = displayValue;
8134
+ element.hidden = false;
8135
+ element.removeAttribute("hidden");
8136
+ element.setAttribute("aria-hidden", "false");
8137
+ element.setAttribute("data-vidply-alert-active", "true");
8138
+ }
8139
+ const shouldReset = config.resetContent !== false && options.reason === "focus";
8140
+ if (shouldReset) {
8141
+ this.restoreMetadataAlertContent(element, config);
8142
+ }
8143
+ const shouldFocus = options.focus !== void 0 ? options.focus : config.focusOnShow ?? options.reason !== "focus";
8144
+ if (shouldShow && shouldFocus) {
8145
+ if (element.tabIndex === -1 && !element.hasAttribute("tabindex")) {
8146
+ element.setAttribute("tabindex", "-1");
8147
+ }
8148
+ element.focus();
8149
+ }
8150
+ if (shouldShow && config.autoScroll !== false && options.autoScroll !== false) {
8151
+ element.scrollIntoView({ behavior: "smooth", block: "nearest" });
8152
+ }
8153
+ const continueSelector = config.continueButton;
8154
+ if (continueSelector) {
8155
+ let continueButton = null;
8156
+ if (continueSelector === "self") {
8157
+ continueButton = element;
8158
+ } else if (element.matches(continueSelector)) {
8159
+ continueButton = element;
8160
+ } else {
8161
+ continueButton = element.querySelector(continueSelector) || document.querySelector(continueSelector);
8162
+ }
8163
+ if (continueButton && !this.metadataAlertHandlers.has(selector)) {
8164
+ const handler = () => {
8165
+ const hideOnContinue = config.hideOnContinue !== false;
8166
+ if (hideOnContinue) {
8167
+ const originalDisplay = element.dataset.vidplyAlertOriginalDisplay || "";
8168
+ element.style.display = config.hideDisplay || originalDisplay || "none";
8169
+ element.setAttribute("aria-hidden", "true");
8170
+ element.removeAttribute("data-vidply-alert-active");
8171
+ }
8172
+ if (config.resume !== false && this.state.paused) {
8173
+ this.play();
8174
+ }
8175
+ const focusTarget = config.focusTarget || "playButton";
8176
+ this.setManagedTimeout(() => {
8177
+ this.focusMetadataTarget(focusTarget, element);
8178
+ }, config.focusDelay ?? 100);
8179
+ };
8180
+ continueButton.addEventListener("click", handler);
8181
+ this.metadataAlertHandlers.set(selector, { button: continueButton, handler });
8182
+ }
8183
+ }
8184
+ return element;
8185
+ }
8186
+ handleMetadataHashtags(hashtags) {
8187
+ if (!Array.isArray(hashtags) || hashtags.length === 0) {
8188
+ return;
8189
+ }
8190
+ const configMap = this.options.metadataHashtags;
8191
+ if (!configMap) {
8192
+ return;
8193
+ }
8194
+ hashtags.forEach((tag) => {
8195
+ const config = this.resolveMetadataConfig(configMap, tag);
8196
+ if (!config) {
8197
+ return;
8198
+ }
8199
+ const selector = this.normalizeMetadataSelector(config.alert || config.selector || config.target);
8200
+ if (!selector) {
8201
+ return;
8202
+ }
8203
+ const element = document.querySelector(selector);
8204
+ if (!element) {
8205
+ if (this.options.debug) {
8206
+ this.log("[Metadata] Hashtag target not found:", selector);
8207
+ }
8208
+ return;
8209
+ }
8210
+ if (this.options.debug) {
8211
+ this.log("[Metadata] Handling hashtag", tag, { selector, config });
8212
+ }
8213
+ this.cacheMetadataAlertContent(element, config);
8214
+ if (config.title) {
8215
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
8216
+ const titleEl = element.querySelector(titleSelector);
8217
+ if (titleEl) {
8218
+ titleEl.textContent = config.title;
8219
+ }
8220
+ }
8221
+ if (config.message) {
8222
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
8223
+ const messageEl = element.querySelector(messageSelector);
8224
+ if (messageEl) {
8225
+ messageEl.textContent = config.message;
8226
+ }
8227
+ }
8228
+ const show = config.show !== false;
8229
+ const focus = config.focus !== void 0 ? config.focus : false;
8230
+ this.handleMetadataAlert(selector, {
8231
+ element,
8232
+ show,
8233
+ focus,
8234
+ autoScroll: config.autoScroll,
8235
+ reason: "hashtag"
8236
+ });
8237
+ });
8238
+ }
8239
+ /**
8240
+ * Handle individual metadata cues
8241
+ * Parses metadata text and emits events or triggers actions
8242
+ */
8243
+ handleMetadataCue(cue) {
8244
+ const text = cue.text.trim();
8245
+ if (this.options.debug) {
8246
+ this.log("[Metadata] Processing cue:", {
8247
+ time: cue.startTime,
8248
+ text
8249
+ });
8250
+ }
8251
+ this.emit("metadata", {
8252
+ time: cue.startTime,
8253
+ endTime: cue.endTime,
8254
+ text,
8255
+ cue
8256
+ });
8257
+ if (text.includes("PAUSE")) {
8258
+ if (!this.state.paused) {
8259
+ if (this.options.debug) {
8260
+ this.log("[Metadata] Pausing video at", cue.startTime);
8261
+ }
8262
+ this.pause();
8263
+ }
8264
+ this.emit("metadata:pause", { time: cue.startTime, text });
8265
+ }
8266
+ const focusMatch = text.match(/FOCUS:([\w#-]+)/);
8267
+ if (focusMatch) {
8268
+ const targetSelector = focusMatch[1];
8269
+ const normalizedSelector = this.normalizeMetadataSelector(targetSelector);
8270
+ const targetElement = normalizedSelector ? document.querySelector(normalizedSelector) : null;
8271
+ if (targetElement) {
8272
+ if (this.options.debug) {
8273
+ this.log("[Metadata] Focusing element:", normalizedSelector);
8274
+ }
8275
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
8276
+ targetElement.setAttribute("tabindex", "-1");
8277
+ }
8278
+ this.setManagedTimeout(() => {
8279
+ targetElement.focus();
8280
+ targetElement.scrollIntoView({ behavior: "smooth", block: "nearest" });
8281
+ }, 10);
8282
+ } else if (this.options.debug) {
8283
+ this.log("[Metadata] Element not found:", normalizedSelector || targetSelector);
8284
+ }
8285
+ this.emit("metadata:focus", {
8286
+ time: cue.startTime,
8287
+ target: targetSelector,
8288
+ selector: normalizedSelector,
8289
+ element: targetElement,
8290
+ text
8291
+ });
8292
+ if (normalizedSelector) {
8293
+ this.handleMetadataAlert(normalizedSelector, {
8294
+ element: targetElement,
8295
+ reason: "focus"
8296
+ });
8297
+ }
8298
+ }
8299
+ const hashtags = text.match(/#[\w-]+/g);
8300
+ if (hashtags) {
8301
+ if (this.options.debug) {
8302
+ this.log("[Metadata] Hashtags found:", hashtags);
8303
+ }
8304
+ this.emit("metadata:hashtags", {
8305
+ time: cue.startTime,
8306
+ hashtags,
8307
+ text
8308
+ });
8309
+ this.handleMetadataHashtags(hashtags);
8310
+ }
8311
+ }
7176
8312
  };
7177
8313
  Player.instances = [];
7178
8314