vidply 1.2.0 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +974 -946
  2. package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-JYYLK5KB.js} +275 -19
  3. package/dist/dev/vidply.AudioDescriptionManager-JYYLK5KB.js.map +7 -0
  4. package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-47RCKQF5.js} +4 -4
  5. package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-VEJ4HDHO.js} +3 -3
  6. package/dist/dev/vidply.DASHRenderer-VEJ4HDHO.js.map +7 -0
  7. package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-C2UVRPZY.js} +5 -5
  8. package/dist/dev/vidply.FloatingPlayerManager-C2UVRPZY.js.map +7 -0
  9. package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZVBK2TNQ.js} +4 -4
  10. package/dist/dev/vidply.HLSRenderer-ZVBK2TNQ.js.map +7 -0
  11. package/dist/dev/vidply.HTML5Renderer-HLRQUOCU.js +12 -0
  12. package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-FLUCBW5H.js} +8 -8
  13. package/dist/dev/vidply.SignLanguageManager-FLUCBW5H.js.map +7 -0
  14. package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-MGMAJ7NK.js} +3 -3
  15. package/dist/dev/vidply.SoundCloudRenderer-MGMAJ7NK.js.map +7 -0
  16. package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-Y2RYXHJW.js} +10 -10
  17. package/dist/dev/vidply.TranscriptManager-Y2RYXHJW.js.map +7 -0
  18. package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-RQFQD7OE.js} +3 -3
  19. package/dist/dev/vidply.VimeoRenderer-RQFQD7OE.js.map +7 -0
  20. package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-EU2BARZF.js} +3 -3
  21. package/dist/dev/vidply.YouTubeRenderer-EU2BARZF.js.map +7 -0
  22. package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-6LYZYI5G.js} +3 -3
  23. package/dist/dev/vidply.chunk-6LYZYI5G.js.map +7 -0
  24. package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-A4ESEOQ2.js} +6 -6
  25. package/dist/dev/vidply.chunk-A4ESEOQ2.js.map +7 -0
  26. package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-CGYIED2I.js} +2 -2
  27. package/dist/dev/vidply.chunk-CGYIED2I.js.map +7 -0
  28. package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-HZ7UOKOU.js} +3 -3
  29. package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-I6EDRYGJ.js} +33 -6
  30. package/dist/dev/vidply.chunk-I6EDRYGJ.js.map +7 -0
  31. package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-JKMQGLCJ.js} +5 -5
  32. package/dist/dev/vidply.chunk-JKMQGLCJ.js.map +7 -0
  33. package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-JZNZX32Z.js} +2 -2
  34. package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-P25MRJ6O.js} +2 -2
  35. package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-PRFNESYA.js} +2 -2
  36. package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-UI3RYCVF.js} +4 -3
  37. package/dist/dev/vidply.chunk-UI3RYCVF.js.map +7 -0
  38. package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-UNFGMABP.js} +2 -2
  39. package/dist/dev/vidply.chunk-UNFGMABP.js.map +7 -0
  40. package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-VAUVSQXC.js} +29 -2
  41. package/dist/dev/vidply.de-VAUVSQXC.js.map +7 -0
  42. package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-G65HFWO6.js} +29 -2
  43. package/dist/dev/vidply.es-G65HFWO6.js.map +7 -0
  44. package/dist/dev/vidply.esm.js +648 -35
  45. package/dist/dev/vidply.esm.js.map +3 -3
  46. package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-MJNOZELM.js} +29 -2
  47. package/dist/dev/vidply.fr-MJNOZELM.js.map +7 -0
  48. package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-INMDHIYJ.js} +29 -2
  49. package/dist/dev/vidply.ja-INMDHIYJ.js.map +7 -0
  50. package/dist/legacy/vidply.js +1062 -34
  51. package/dist/legacy/vidply.js.map +3 -3
  52. package/dist/legacy/vidply.min.js +2 -2
  53. package/dist/legacy/vidply.min.meta.json +127 -55
  54. package/dist/prod/vidply.AudioDescriptionManager-EKWUNOPE.min.js +6 -0
  55. package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-JTWSNHRQ.min.js} +2 -2
  56. package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-CIDAHFWM.min.js} +2 -2
  57. package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-VZXHPCPM.min.js} +2 -2
  58. package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-Q6EFYKID.min.js} +2 -2
  59. package/dist/prod/vidply.HTML5Renderer-XRDJUTOO.min.js +6 -0
  60. package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-JUOSA5AM.min.js} +2 -2
  61. package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-7ODD46PO.min.js} +2 -2
  62. package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-4B2QKYGQ.min.js} +2 -2
  63. package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-MR3UYLY7.min.js} +2 -2
  64. package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-ITRTC7NR.min.js} +2 -2
  65. package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-3EEDXU4T.min.js} +2 -2
  66. package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-5HTX5QXW.min.js} +1 -1
  67. package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-6OGHHIYP.min.js} +2 -2
  68. package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-GUPGMFUZ.min.js} +2 -2
  69. package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-MWEBITCP.min.js} +1 -1
  70. package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-PL3P3JHR.min.js} +1 -1
  71. package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-QQZGV7TD.min.js} +2 -2
  72. package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-U5IRRYG5.min.js} +1 -1
  73. package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-VKHV4QHA.min.js} +2 -2
  74. package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-XA4M574A.min.js} +1 -1
  75. package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-ZPTV4JEP.min.js} +2 -2
  76. package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-UZHFCEZU.min.js} +2 -2
  77. package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-BOISUV2S.min.js} +2 -2
  78. package/dist/prod/vidply.esm.min.js +2 -2
  79. package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-ICSMKPFM.min.js} +2 -2
  80. package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-VQ2HKGAY.min.js} +2 -2
  81. package/dist/vidply.css +5852 -5796
  82. package/dist/vidply.esm.min.meta.json +204 -149
  83. package/dist/vidply.min.css +1 -1
  84. package/package.json +11 -11
  85. package/src/controls/CaptionManager.ts +598 -598
  86. package/src/controls/ControlBar.ts +4191 -4159
  87. package/src/controls/KeyboardHelp.ts +326 -0
  88. package/src/controls/KeyboardManager.ts +387 -383
  89. package/src/controls/SettingsDialog.ts +488 -488
  90. package/src/controls/TranscriptManager.ts +2426 -2426
  91. package/src/core/AudioDescriptionManager.ts +863 -774
  92. package/src/core/DescriptionSpeechManager.ts +232 -0
  93. package/src/core/FloatingPlayerManager.ts +674 -674
  94. package/src/core/LazyInit.ts +80 -80
  95. package/src/core/MediaSessionManager.ts +341 -0
  96. package/src/core/MetadataAlertsManager.ts +514 -514
  97. package/src/core/Player.ts +2973 -2872
  98. package/src/core/PosterManager.ts +169 -169
  99. package/src/core/PseudoFullscreen.ts +179 -179
  100. package/src/core/ResponsiveManager.ts +231 -231
  101. package/src/core/ResumeManager.ts +236 -236
  102. package/src/core/SignLanguageManager.ts +1308 -1308
  103. package/src/core/ThemeManager.ts +157 -157
  104. package/src/features/PlaylistManager.ts +1838 -1827
  105. package/src/i18n/i18n.ts +223 -223
  106. package/src/i18n/languages/de.ts +244 -217
  107. package/src/i18n/languages/en.ts +245 -218
  108. package/src/i18n/languages/es.ts +241 -214
  109. package/src/i18n/languages/fr.ts +241 -214
  110. package/src/i18n/languages/ja.ts +241 -214
  111. package/src/icons/Icons.ts +168 -167
  112. package/src/index.ts +196 -193
  113. package/src/renderers/DASHRenderer.ts +1077 -1077
  114. package/src/renderers/HLSRenderer.ts +944 -944
  115. package/src/renderers/HTML5Renderer.ts +389 -389
  116. package/src/renderers/SoundCloudRenderer.ts +437 -437
  117. package/src/renderers/VimeoRenderer.ts +298 -298
  118. package/src/renderers/YouTubeRenderer.ts +290 -290
  119. package/src/styles/vidply.css +5852 -5796
  120. package/src/types/events.ts +128 -122
  121. package/src/types/globals.d.ts +313 -313
  122. package/src/types/options.ts +199 -182
  123. package/src/types/renderer.ts +58 -58
  124. package/src/utils/DOMUtils.ts +281 -281
  125. package/src/utils/DownloadInfo.ts +171 -171
  126. package/src/utils/DraggablePanel.ts +539 -539
  127. package/src/utils/DraggablePanelMenu.ts +195 -195
  128. package/src/utils/DraggableResizable.ts +997 -997
  129. package/src/utils/EventEmitter.ts +59 -59
  130. package/src/utils/FormUtils.ts +95 -95
  131. package/src/utils/MenuFactory.ts +483 -483
  132. package/src/utils/Sanitize.ts +70 -70
  133. package/src/utils/SettingsMenuFactory.ts +289 -289
  134. package/src/utils/StorageManager.ts +287 -287
  135. package/src/utils/TrackLabelUtils.ts +52 -52
  136. package/src/utils/UrlSafe.ts +69 -69
  137. package/src/utils/VideoFrameCapture.ts +114 -114
  138. package/src/utils/WindowComponents.ts +250 -250
  139. package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
  140. package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
  141. package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
  142. package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
  143. package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
  144. package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
  145. package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
  146. package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
  147. package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
  148. package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
  149. package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
  150. package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
  151. package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
  152. package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
  153. package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
  154. package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
  155. package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
  156. package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
  157. package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
  158. package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
  159. package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
  160. package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
  161. package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
  162. /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-47RCKQF5.js.map} +0 -0
  163. /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-HLRQUOCU.js.map} +0 -0
  164. /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-HZ7UOKOU.js.map} +0 -0
  165. /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-JZNZX32Z.js.map} +0 -0
  166. /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-P25MRJ6O.js.map} +0 -0
  167. /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-PRFNESYA.js.map} +0 -0
@@ -1,997 +1,997 @@
1
- import type { StorageManager } from './StorageManager.js';
2
-
3
- export interface DraggableResizableOptions {
4
- dragHandle?: HTMLElement | null;
5
- resizeHandles?: HTMLElement[];
6
- onDragStart?: ((e: Event) => boolean | void) | null;
7
- onDrag?: ((pos: { x?: number; y?: number; centered?: boolean }) => void) | null;
8
- onDragEnd?: (() => void) | null;
9
- onResizeStart?: (() => void) | null;
10
- onResize?: ((dims: { width: number; height: number; left?: number; top?: number }) => void) | null;
11
- onResizeEnd?: (() => void) | null;
12
- constrainToViewport?: boolean;
13
- minWidth?: number;
14
- minHeight?: number;
15
- maintainAspectRatio?: boolean;
16
- keyboardDragKey?: string;
17
- keyboardResizeKey?: string;
18
- keyboardStep?: number;
19
- keyboardStepLarge?: number;
20
- maxWidth?: number | (() => number) | null;
21
- maxHeight?: number | (() => number) | null;
22
- pointerResizeIndicatorText?: string | null;
23
- onPointerResizeToggle?: ((enabled: boolean) => void) | null;
24
- classPrefix?: string;
25
- storage?: StorageManager | null;
26
- storageKey?: string | null;
27
- }
28
-
29
- interface ResolvedOptions extends Required<Omit<DraggableResizableOptions, 'maxWidth' | 'maxHeight'>> {
30
- maxWidth: number | (() => number) | null;
31
- maxHeight: number | (() => number) | null;
32
- }
33
-
34
- export class DraggableResizable {
35
- element: HTMLElement;
36
- options: ResolvedOptions;
37
- isDragging: boolean;
38
- isResizing: boolean;
39
- resizeDirection: string | null;
40
- dragOffsetX: number;
41
- dragOffsetY: number;
42
- positionOffsetX: number;
43
- positionOffsetY: number;
44
- initialMouseX: number;
45
- initialMouseY: number;
46
- needsPositionConversion: boolean;
47
- resizeStartX: number;
48
- resizeStartY: number;
49
- resizeStartWidth: number;
50
- resizeStartHeight: number;
51
- resizeStartLeft: number;
52
- resizeStartTop: number;
53
- keyboardDragMode: boolean;
54
- keyboardResizeMode: boolean;
55
- pointerResizeMode: boolean;
56
- manuallyPositioned: boolean;
57
- resizeHandlesManaged: Map<HTMLElement, boolean>;
58
- resizeIndicatorElement: HTMLElement | null;
59
- handlers: {
60
- mousedown: (e: MouseEvent) => void;
61
- mousemove: (e: MouseEvent) => void;
62
- mouseup: () => void;
63
- touchstart: (e: TouchEvent) => void;
64
- touchmove: (e: TouchEvent) => void;
65
- touchend: () => void;
66
- pointerdown: (e: PointerEvent) => void;
67
- pointermove: (e: PointerEvent) => void;
68
- pointerup: (e: PointerEvent) => void;
69
- pointercancel: (e: PointerEvent) => void;
70
- keydown: (e: KeyboardEvent) => void;
71
- resizeHandleMousedown: (e: MouseEvent | TouchEvent) => void;
72
- resizeHandlePointerDown: (e: PointerEvent) => void;
73
- };
74
- activePointerId: number | null;
75
- activePointerType: string | null;
76
-
77
- constructor(element: HTMLElement, options: DraggableResizableOptions = {}) {
78
- this.element = element;
79
- this.options = {
80
- dragHandle: null,
81
- resizeHandles: [],
82
- onDragStart: null,
83
- onDrag: null,
84
- onDragEnd: null,
85
- onResizeStart: null,
86
- onResize: null,
87
- onResizeEnd: null,
88
- constrainToViewport: true,
89
- minWidth: 150,
90
- minHeight: 100,
91
- maintainAspectRatio: false,
92
- keyboardDragKey: 'd',
93
- keyboardResizeKey: 'r',
94
- keyboardStep: 5,
95
- keyboardStepLarge: 10,
96
- maxWidth: null,
97
- maxHeight: null,
98
- pointerResizeIndicatorText: null,
99
- onPointerResizeToggle: null,
100
- classPrefix: 'draggable',
101
- storage: null,
102
- storageKey: null,
103
- ...options
104
- } as ResolvedOptions;
105
-
106
- // State
107
- this.isDragging = false;
108
- this.isResizing = false;
109
- this.resizeDirection = null;
110
- this.dragOffsetX = 0;
111
- this.dragOffsetY = 0;
112
- this.positionOffsetX = 0;
113
- this.positionOffsetY = 0;
114
- this.initialMouseX = 0;
115
- this.initialMouseY = 0;
116
- this.needsPositionConversion = false;
117
- this.resizeStartX = 0;
118
- this.resizeStartY = 0;
119
- this.resizeStartWidth = 0;
120
- this.resizeStartHeight = 0;
121
- this.resizeStartLeft = 0;
122
- this.resizeStartTop = 0;
123
- this.keyboardDragMode = false;
124
- this.keyboardResizeMode = false;
125
- this.pointerResizeMode = false;
126
- this.manuallyPositioned = false; // Flag to track if user has manually moved/resized
127
- this.resizeHandlesManaged = new Map();
128
- this.resizeIndicatorElement = null;
129
-
130
- // Event handlers
131
- this.handlers = {
132
- mousedown: this.onMouseDown.bind(this),
133
- mousemove: this.onMouseMove.bind(this),
134
- mouseup: this.onMouseUp.bind(this),
135
- touchstart: this.onTouchStart.bind(this),
136
- touchmove: this.onTouchMove.bind(this),
137
- touchend: this.onTouchEnd.bind(this),
138
- pointerdown: this.onPointerDown.bind(this),
139
- pointermove: this.onPointerMove.bind(this),
140
- pointerup: this.onPointerUp.bind(this),
141
- pointercancel: this.onPointerUp.bind(this),
142
- keydown: this.onKeyDown.bind(this),
143
- resizeHandleMousedown: this.onResizeHandleMouseDown.bind(this),
144
- resizeHandlePointerDown: this.onResizeHandlePointerDown.bind(this)
145
- };
146
-
147
- // Pointer tracking (Pointer Events unify mouse + touch)
148
- this.activePointerId = null;
149
- this.activePointerType = null;
150
-
151
- this.init();
152
- }
153
-
154
- hasManagedResizeHandles(): boolean {
155
- return Array.from(this.resizeHandlesManaged.values()).some(Boolean);
156
- }
157
-
158
- storeOriginalHandleDisplay(handle: HTMLElement): void {
159
- if (!handle.dataset.originalDisplay) {
160
- handle.dataset.originalDisplay = handle.style.display || '';
161
- }
162
- }
163
-
164
- hideResizeHandle(handle: HTMLElement): void {
165
- handle.style.display = 'none';
166
- handle.setAttribute('aria-hidden', 'true');
167
- }
168
-
169
- showResizeHandle(handle: HTMLElement): void {
170
- const original = handle.dataset.originalDisplay !== undefined ? handle.dataset.originalDisplay : '';
171
- handle.style.display = original;
172
- handle.removeAttribute('aria-hidden');
173
- }
174
-
175
- setManagedHandlesVisible(visible: boolean): void {
176
- if (!this.options.resizeHandles || this.options.resizeHandles.length === 0) {
177
- return;
178
- }
179
-
180
- this.options.resizeHandles.forEach(handle => {
181
- if (!this.resizeHandlesManaged.get(handle)) {
182
- return;
183
- }
184
-
185
- if (visible) {
186
- this.showResizeHandle(handle);
187
- } else {
188
- this.hideResizeHandle(handle);
189
- }
190
- });
191
- }
192
-
193
- init(): void {
194
- const dragHandle = this.options.dragHandle || this.element;
195
-
196
- // Prefer Pointer Events when available (covers mouse + touch in one codepath)
197
- if (typeof window !== 'undefined' && 'PointerEvent' in window) {
198
- dragHandle.addEventListener('pointerdown', this.handlers.pointerdown);
199
-
200
- // Document-level up/cancel events (always needed to end a drag/resize that
201
- // started inside the element even when the pointer leaves the element).
202
- // pointermove is NOT registered here — it is added lazily in startDragging()
203
- // and startResizing() with { passive: false } so that it can call
204
- // e.preventDefault() to suppress scroll during an active interaction, and
205
- // removed again in stopDragging() / stopResizing(). Keeping the listener
206
- // absent while the element is merely visible prevents the non-passive
207
- // handler from blocking compositor-threaded scroll on the rest of the page.
208
- document.addEventListener('pointerup', this.handlers.pointerup);
209
- document.addEventListener('pointercancel', this.handlers.pointercancel);
210
- } else {
211
- // Fallback for very old browsers
212
- dragHandle.addEventListener('mousedown', this.handlers.mousedown);
213
- dragHandle.addEventListener('touchstart', this.handlers.touchstart, { passive: false });
214
-
215
- // Document-level move/up events
216
- document.addEventListener('mousemove', this.handlers.mousemove);
217
- document.addEventListener('mouseup', this.handlers.mouseup);
218
- document.addEventListener('touchmove', this.handlers.touchmove, { passive: false });
219
- document.addEventListener('touchend', this.handlers.touchend);
220
- }
221
-
222
- // Keyboard events
223
- this.element.addEventListener('keydown', this.handlers.keydown);
224
-
225
- // Resize handles
226
- if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
227
- this.options.resizeHandles.forEach(handle => {
228
- if (typeof window !== 'undefined' && 'PointerEvent' in window) {
229
- handle.addEventListener('pointerdown', this.handlers.resizeHandlePointerDown);
230
- } else {
231
- handle.addEventListener('mousedown', this.handlers.resizeHandleMousedown);
232
- handle.addEventListener('touchstart', this.handlers.resizeHandleMousedown, { passive: false });
233
- }
234
-
235
- const managed = handle.dataset.vidplyManagedResize === 'true';
236
- this.resizeHandlesManaged.set(handle, managed);
237
- if (managed) {
238
- this.storeOriginalHandleDisplay(handle);
239
- this.hideResizeHandle(handle);
240
- }
241
- });
242
- }
243
- }
244
-
245
- onPointerDown(e: PointerEvent): void {
246
- if (e.isPrimary === false) return;
247
- if (e.pointerType === 'mouse' && e.button !== 0) return;
248
-
249
- const target = e.target as HTMLElement;
250
- if (target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
251
- return;
252
- }
253
-
254
- if (this.options.onDragStart && !this.options.onDragStart(e)) {
255
- return;
256
- }
257
-
258
- this.activePointerId = e.pointerId;
259
- this.activePointerType = e.pointerType;
260
-
261
- try {
262
- (e.currentTarget as HTMLElement)?.setPointerCapture?.(e.pointerId);
263
- } catch {
264
- // ignore
265
- }
266
-
267
- this.startDragging(e.clientX, e.clientY);
268
- e.preventDefault();
269
- }
270
-
271
- onPointerMove(e: PointerEvent): void {
272
- if (this.activePointerId !== null && e.pointerId !== this.activePointerId) return;
273
-
274
- if (this.isDragging) {
275
- this.drag(e.clientX, e.clientY);
276
- e.preventDefault();
277
- } else if (this.isResizing) {
278
- this.resize(e.clientX, e.clientY);
279
- e.preventDefault();
280
- }
281
- }
282
-
283
- onPointerUp(e: PointerEvent): void {
284
- if (this.activePointerId !== null && e.pointerId !== this.activePointerId) return;
285
-
286
- if (this.isDragging) {
287
- this.stopDragging();
288
- } else if (this.isResizing) {
289
- this.stopResizing();
290
- }
291
-
292
- this.activePointerId = null;
293
- this.activePointerType = null;
294
- }
295
-
296
- onMouseDown(e: MouseEvent): void {
297
- const target = e.target as HTMLElement;
298
- if (target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
299
- return;
300
- }
301
-
302
- // Call custom handler if provided
303
- if (this.options.onDragStart && !this.options.onDragStart(e)) {
304
- return;
305
- }
306
-
307
- this.startDragging(e.clientX, e.clientY);
308
- e.preventDefault();
309
- }
310
-
311
- onTouchStart(e: TouchEvent): void {
312
- const target = e.target as HTMLElement;
313
- if (target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
314
- return;
315
- }
316
-
317
- // Call custom handler if provided
318
- if (this.options.onDragStart && !this.options.onDragStart(e)) {
319
- return;
320
- }
321
-
322
- const touch = e.touches[0];
323
- if (!touch) return;
324
- this.startDragging(touch.clientX, touch.clientY);
325
- // Prevent page scroll while dragging (requires passive:false listener)
326
- e.preventDefault();
327
- }
328
-
329
- onResizeHandlePointerDown(e: PointerEvent): void {
330
- // Only handle primary pointer
331
- if (e.isPrimary === false) return;
332
-
333
- // Only left click for mouse
334
- if (e.pointerType === 'mouse' && e.button !== 0) return;
335
-
336
- e.preventDefault();
337
- e.stopPropagation();
338
-
339
- const handle = e.target as HTMLElement;
340
- this.resizeDirection = handle.getAttribute('data-direction');
341
-
342
- this.activePointerId = e.pointerId;
343
- this.activePointerType = e.pointerType;
344
-
345
- try {
346
- (e.currentTarget as HTMLElement)?.setPointerCapture?.(e.pointerId);
347
- } catch {
348
- // ignore
349
- }
350
-
351
- this.startResizing(e.clientX, e.clientY);
352
- }
353
-
354
- onResizeHandleMouseDown(e: MouseEvent | TouchEvent): void {
355
- e.preventDefault();
356
- e.stopPropagation();
357
-
358
- const handle = e.target as HTMLElement;
359
- this.resizeDirection = handle.getAttribute('data-direction');
360
-
361
- const clientX = 'clientX' in e ? e.clientX : e.touches?.[0]?.clientX;
362
- const clientY = 'clientY' in e ? e.clientY : e.touches?.[0]?.clientY;
363
- if (clientX === undefined || clientY === undefined) return;
364
-
365
- this.startResizing(clientX, clientY);
366
- }
367
-
368
- onMouseMove(e: MouseEvent): void {
369
- if (this.isDragging) {
370
- this.drag(e.clientX, e.clientY);
371
- e.preventDefault();
372
- } else if (this.isResizing) {
373
- this.resize(e.clientX, e.clientY);
374
- e.preventDefault();
375
- }
376
- }
377
-
378
- onTouchMove(e: TouchEvent): void {
379
- if (this.isDragging || this.isResizing) {
380
- const touch = e.touches[0];
381
- if (!touch) return;
382
- if (this.isDragging) {
383
- this.drag(touch.clientX, touch.clientY);
384
- } else {
385
- this.resize(touch.clientX, touch.clientY);
386
- }
387
- e.preventDefault();
388
- }
389
- }
390
-
391
- onMouseUp(): void {
392
- if (this.isDragging) {
393
- this.stopDragging();
394
- } else if (this.isResizing) {
395
- this.stopResizing();
396
- }
397
- }
398
-
399
- onTouchEnd(): void {
400
- if (this.isDragging) {
401
- this.stopDragging();
402
- } else if (this.isResizing) {
403
- this.stopResizing();
404
- }
405
- }
406
-
407
- onKeyDown(e: KeyboardEvent): void {
408
- // Toggle drag mode
409
- if (e.key.toLowerCase() === this.options.keyboardDragKey.toLowerCase()) {
410
- e.preventDefault();
411
- this.toggleKeyboardDragMode();
412
- return;
413
- }
414
-
415
- // Toggle resize mode
416
- if (e.key.toLowerCase() === this.options.keyboardResizeKey.toLowerCase()) {
417
- e.preventDefault();
418
- if (this.hasManagedResizeHandles()) {
419
- this.togglePointerResizeMode();
420
- } else {
421
- this.toggleKeyboardResizeMode();
422
- }
423
- return;
424
- }
425
-
426
- // Exit modes with Escape
427
- if (e.key === 'Escape') {
428
- if (this.pointerResizeMode) {
429
- e.preventDefault();
430
- this.disablePointerResizeMode();
431
- return;
432
- }
433
- if (this.keyboardDragMode || this.keyboardResizeMode) {
434
- e.preventDefault();
435
- this.disableKeyboardDragMode();
436
- this.disableKeyboardResizeMode();
437
- return;
438
- }
439
- }
440
-
441
- // Arrow keys for drag/resize
442
- if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
443
- if (this.keyboardDragMode) {
444
- e.preventDefault();
445
- e.stopPropagation();
446
- this.keyboardDrag(e.key, e.shiftKey);
447
- } else if (this.keyboardResizeMode) {
448
- e.preventDefault();
449
- e.stopPropagation();
450
- this.keyboardResize(e.key, e.shiftKey);
451
- }
452
- }
453
-
454
- // Home key to reset position
455
- if (e.key === 'Home' && (this.keyboardDragMode || this.keyboardResizeMode)) {
456
- e.preventDefault();
457
- this.resetPosition();
458
- }
459
- }
460
-
461
- startDragging(clientX: number, clientY: number): void {
462
- // Get current rendered position BEFORE any changes
463
- const rect = this.element.getBoundingClientRect();
464
-
465
- // Convert position to left/top IMMEDIATELY (before setting any state)
466
- // Check if element is using right/bottom/transform positioning
467
- const computedStyle = window.getComputedStyle(this.element);
468
- const needsConversion = computedStyle.right !== 'auto' ||
469
- computedStyle.bottom !== 'auto' ||
470
- computedStyle.transform !== 'none';
471
-
472
- this.positionOffsetX = 0;
473
- this.positionOffsetY = 0;
474
-
475
- if (needsConversion) {
476
- // Determine the correct left/top values based on position type
477
- let targetLeft, targetTop;
478
-
479
- if (computedStyle.position === 'absolute') {
480
- // position: absolute uses container-relative coordinates
481
- const offsetParent = this.element.offsetParent || document.body;
482
- const parentRect = offsetParent.getBoundingClientRect();
483
- targetLeft = rect.left - parentRect.left;
484
- targetTop = rect.top - parentRect.top;
485
- this.positionOffsetX = parentRect.left;
486
- this.positionOffsetY = parentRect.top;
487
- } else if (computedStyle.position === 'fixed') {
488
- const parsedLeft = parseFloat(computedStyle.left);
489
- const parsedTop = parseFloat(computedStyle.top);
490
- const hasLeft = Number.isFinite(parsedLeft);
491
- const hasTop = Number.isFinite(parsedTop);
492
- targetLeft = hasLeft ? parsedLeft : rect.left;
493
- targetTop = hasTop ? parsedTop : rect.top;
494
- this.positionOffsetX = rect.left - targetLeft;
495
- this.positionOffsetY = rect.top - targetTop;
496
- } else {
497
- // fallback: treat as viewport-relative
498
- targetLeft = rect.left;
499
- targetTop = rect.top;
500
- this.positionOffsetX = rect.left - targetLeft;
501
- this.positionOffsetY = rect.top - targetTop;
502
- }
503
-
504
- // Build complete style update atomically
505
- const currentCssText = this.element.style.cssText;
506
- let newCssText = currentCssText
507
- .split(';')
508
- .filter(rule => {
509
- const trimmed = rule.trim();
510
- // Filter out empty rules
511
- if (!trimmed) return false;
512
-
513
- const colonIndex = trimmed.indexOf(':');
514
- if (colonIndex === -1) return false;
515
-
516
- const property = trimmed.substring(0, colonIndex).trim();
517
- const value = trimmed.substring(colonIndex + 1).trim();
518
-
519
- // Skip if value is empty or just whitespace (this catches empty border properties)
520
- if (!value || value === '') return false;
521
-
522
- // Skip positioning properties
523
- if (property === 'right' || property === 'bottom' || property === 'transform' ||
524
- property === 'left' || property === 'top' || property === 'inset') {
525
- return false;
526
- }
527
-
528
- // Skip border-image properties (these can have empty values that cause parse errors)
529
- if (property.startsWith('border-image')) {
530
- return false;
531
- }
532
-
533
- return true;
534
- })
535
- .join('; ');
536
-
537
- if (newCssText) newCssText += '; ';
538
- newCssText += `left: ${targetLeft}px; top: ${targetTop}px; right: auto; bottom: auto; transform: none`;
539
-
540
- // Apply all at once
541
- this.element.style.cssText = newCssText;
542
- }
543
-
544
- // Now calculate offsets based on CURRENT position (after conversion)
545
- // Re-get rect after potential position change
546
- const finalRect = this.element.getBoundingClientRect();
547
- this.dragOffsetX = clientX - finalRect.left;
548
- this.dragOffsetY = clientY - finalRect.top;
549
-
550
- this.isDragging = true;
551
- this.element.classList.add(`${this.options.classPrefix}-dragging`);
552
- document.body.style.cursor = 'grabbing';
553
- document.body.style.userSelect = 'none';
554
-
555
- // Register the non-passive pointermove listener only for the duration of
556
- // the drag so it does not block compositor-threaded scroll at other times.
557
- if ('PointerEvent' in window) {
558
- document.addEventListener('pointermove', this.handlers.pointermove, { passive: false });
559
- }
560
- }
561
-
562
- drag(clientX: number, clientY: number): void {
563
- if (!this.isDragging) return;
564
-
565
- // Calculate new position: current mouse position minus the offset where user clicked
566
- let newX = clientX - this.dragOffsetX - this.positionOffsetX;
567
- let newY = clientY - this.dragOffsetY - this.positionOffsetY;
568
-
569
- // Constrain to viewport if needed
570
- if (this.options.constrainToViewport) {
571
- const rect = this.element.getBoundingClientRect();
572
- const viewportWidth = document.documentElement.clientWidth;
573
- const viewportHeight = document.documentElement.clientHeight;
574
-
575
- // Keep at least 100px visible
576
- const minVisible = 100;
577
- const minX = -(rect.width - minVisible);
578
- const minY = -(rect.height - minVisible);
579
- const maxX = viewportWidth - minVisible;
580
- const maxY = viewportHeight - minVisible;
581
-
582
- newX = Math.max(minX, Math.min(newX, maxX));
583
- newY = Math.max(minY, Math.min(newY, maxY));
584
- }
585
-
586
- this.element.style.left = `${newX}px`;
587
- this.element.style.top = `${newY}px`;
588
-
589
- // Call custom handler if provided
590
- if (this.options.onDrag) {
591
- this.options.onDrag({ x: newX, y: newY });
592
- }
593
- }
594
-
595
- stopDragging(): void {
596
- this.isDragging = false;
597
- this.element.classList.remove(`${this.options.classPrefix}-dragging`);
598
- document.body.style.cursor = '';
599
- document.body.style.userSelect = '';
600
-
601
- // Remove the non-passive pointermove listener that was added in startDragging().
602
- if ('PointerEvent' in window) {
603
- document.removeEventListener('pointermove', this.handlers.pointermove);
604
- }
605
-
606
- // Mark as manually positioned
607
- this.manuallyPositioned = true;
608
-
609
- // Call custom handler if provided
610
- if (this.options.onDragEnd) {
611
- this.options.onDragEnd();
612
- }
613
- }
614
-
615
- startResizing(clientX: number, clientY: number): void {
616
- this.isResizing = true;
617
- this.resizeStartX = clientX;
618
- this.resizeStartY = clientY;
619
-
620
- const rect = this.element.getBoundingClientRect();
621
- this.resizeStartWidth = rect.width;
622
- this.resizeStartHeight = rect.height;
623
- this.resizeStartLeft = rect.left;
624
- this.resizeStartTop = rect.top;
625
-
626
- this.element.classList.add(`${this.options.classPrefix}-resizing`);
627
- document.body.style.userSelect = 'none';
628
-
629
- // Register the non-passive pointermove listener only for the duration of
630
- // the resize so it does not block compositor-threaded scroll at other times.
631
- if ('PointerEvent' in window) {
632
- document.addEventListener('pointermove', this.handlers.pointermove, { passive: false });
633
- }
634
-
635
- // Call custom handler if provided
636
- if (this.options.onResizeStart) {
637
- this.options.onResizeStart();
638
- }
639
- }
640
-
641
- resize(clientX: number, clientY: number): void {
642
- if (!this.isResizing) return;
643
-
644
- const deltaX = clientX - this.resizeStartX;
645
- const deltaY = clientY - this.resizeStartY;
646
-
647
- let newWidth = this.resizeStartWidth;
648
- let newHeight = this.resizeStartHeight;
649
- let newLeft = this.resizeStartLeft;
650
- let newTop = this.resizeStartTop;
651
-
652
- // Handle horizontal resizing
653
- const resizeDirection = this.resizeDirection ?? '';
654
-
655
- if (resizeDirection.includes('e')) {
656
- newWidth = Math.max(this.options.minWidth, this.resizeStartWidth + deltaX);
657
- }
658
- if (resizeDirection.includes('w')) {
659
- const proposedWidth = Math.max(this.options.minWidth, this.resizeStartWidth - deltaX);
660
- newLeft = this.resizeStartLeft + (this.resizeStartWidth - proposedWidth);
661
- newWidth = proposedWidth;
662
- }
663
-
664
- const maxWidthOption = typeof this.options.maxWidth === 'function'
665
- ? this.options.maxWidth()
666
- : this.options.maxWidth;
667
- if (typeof maxWidthOption === 'number' && Number.isFinite(maxWidthOption)) {
668
- const clampedWidth = Math.min(newWidth, maxWidthOption);
669
- if (clampedWidth !== newWidth && resizeDirection.includes('w')) {
670
- newLeft += newWidth - clampedWidth;
671
- }
672
- newWidth = clampedWidth;
673
- }
674
-
675
- // Handle vertical resizing (if not maintaining aspect ratio)
676
- if (!this.options.maintainAspectRatio) {
677
- if (resizeDirection.includes('s')) {
678
- newHeight = Math.max(this.options.minHeight, this.resizeStartHeight + deltaY);
679
- }
680
- if (resizeDirection.includes('n')) {
681
- const proposedHeight = Math.max(this.options.minHeight, this.resizeStartHeight - deltaY);
682
- newTop = this.resizeStartTop + (this.resizeStartHeight - proposedHeight);
683
- newHeight = proposedHeight;
684
- }
685
-
686
- const maxHeightOption = typeof this.options.maxHeight === 'function'
687
- ? this.options.maxHeight()
688
- : this.options.maxHeight;
689
- if (typeof maxHeightOption === 'number' && Number.isFinite(maxHeightOption)) {
690
- const clampedHeight = Math.min(newHeight, maxHeightOption);
691
- if (clampedHeight !== newHeight && resizeDirection.includes('n')) {
692
- newTop += newHeight - clampedHeight;
693
- }
694
- newHeight = clampedHeight;
695
- }
696
- }
697
-
698
- // Apply new dimensions
699
- this.element.style.width = `${newWidth}px`;
700
- if (!this.options.maintainAspectRatio) {
701
- this.element.style.height = `${newHeight}px`;
702
- } else {
703
- this.element.style.height = 'auto';
704
- }
705
-
706
- // Apply new position if resizing from west or north
707
- if (resizeDirection.includes('w')) {
708
- this.element.style.left = `${newLeft}px`;
709
- }
710
- if (resizeDirection.includes('n') && !this.options.maintainAspectRatio) {
711
- this.element.style.top = `${newTop}px`;
712
- }
713
-
714
- // Call custom handler if provided
715
- if (this.options.onResize) {
716
- this.options.onResize({ width: newWidth, height: newHeight, left: newLeft, top: newTop });
717
- }
718
- }
719
-
720
- stopResizing(): void {
721
- this.isResizing = false;
722
- this.resizeDirection = null;
723
- this.element.classList.remove(`${this.options.classPrefix}-resizing`);
724
- document.body.style.userSelect = '';
725
-
726
- // Remove the non-passive pointermove listener that was added in startResizing().
727
- if ('PointerEvent' in window) {
728
- document.removeEventListener('pointermove', this.handlers.pointermove);
729
- }
730
-
731
- // Mark as manually positioned
732
- this.manuallyPositioned = true;
733
-
734
- // Call custom handler if provided
735
- if (this.options.onResizeEnd) {
736
- this.options.onResizeEnd();
737
- }
738
- }
739
-
740
- toggleKeyboardDragMode(): void {
741
- if (this.keyboardDragMode) {
742
- this.disableKeyboardDragMode();
743
- } else {
744
- this.enableKeyboardDragMode();
745
- }
746
- }
747
-
748
- enableKeyboardDragMode(): void {
749
- this.keyboardDragMode = true;
750
- this.keyboardResizeMode = false;
751
- this.element.classList.add(`${this.options.classPrefix}-keyboard-drag`);
752
- this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
753
- this.focusElement();
754
- }
755
-
756
- disableKeyboardDragMode(): void {
757
- this.keyboardDragMode = false;
758
- this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
759
- }
760
-
761
- toggleKeyboardResizeMode(): void {
762
- if (this.keyboardResizeMode) {
763
- this.disableKeyboardResizeMode();
764
- } else {
765
- this.enableKeyboardResizeMode();
766
- }
767
- }
768
-
769
- enableKeyboardResizeMode(): void {
770
- this.keyboardResizeMode = true;
771
- this.keyboardDragMode = false;
772
- this.element.classList.add(`${this.options.classPrefix}-keyboard-resize`);
773
- this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
774
- this.focusElement();
775
- }
776
-
777
- disableKeyboardResizeMode(): void {
778
- this.keyboardResizeMode = false;
779
- this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
780
- }
781
-
782
- enablePointerResizeMode({ focus = true }: { focus?: boolean } = {}): void {
783
- if (!this.hasManagedResizeHandles()) {
784
- this.enableKeyboardResizeMode();
785
- return;
786
- }
787
-
788
- if (this.pointerResizeMode) {
789
- return;
790
- }
791
-
792
- this.pointerResizeMode = true;
793
- this.setManagedHandlesVisible(true);
794
- this.element.classList.add(`${this.options.classPrefix}-resizable`);
795
- this.enableKeyboardResizeMode();
796
-
797
- if (focus) {
798
- this.focusElement();
799
- }
800
-
801
- if (typeof this.options.onPointerResizeToggle === 'function') {
802
- this.options.onPointerResizeToggle(true);
803
- }
804
- }
805
-
806
- disablePointerResizeMode({ focus = false }: { focus?: boolean } = {}): void {
807
- if (!this.pointerResizeMode) {
808
- return;
809
- }
810
-
811
- this.pointerResizeMode = false;
812
- this.setManagedHandlesVisible(false);
813
- this.element.classList.remove(`${this.options.classPrefix}-resizable`);
814
- this.disableKeyboardResizeMode();
815
-
816
- if (focus) {
817
- this.focusElement();
818
- }
819
-
820
- if (typeof this.options.onPointerResizeToggle === 'function') {
821
- this.options.onPointerResizeToggle(false);
822
- }
823
- }
824
-
825
- togglePointerResizeMode(): boolean {
826
- if (this.pointerResizeMode) {
827
- this.disablePointerResizeMode();
828
- } else {
829
- this.enablePointerResizeMode();
830
- }
831
- return this.pointerResizeMode;
832
- }
833
-
834
- focusElement(): void {
835
- if (typeof this.element.focus === 'function') {
836
- try {
837
- this.element.focus({ preventScroll: true });
838
- } catch {
839
- // Some browsers do not support the preventScroll option; fallback without it
840
- this.element.focus();
841
- }
842
- }
843
- }
844
-
845
- keyboardDrag(key: string, shiftKey: boolean): void {
846
- const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
847
-
848
- // Get current position
849
- let currentLeft = parseFloat(this.element.style.left) || 0;
850
- let currentTop = parseFloat(this.element.style.top) || 0;
851
-
852
- // If element is still centered with transform, convert to absolute position first
853
- const computedStyle = window.getComputedStyle(this.element);
854
- if (computedStyle.transform !== 'none') {
855
- const rect = this.element.getBoundingClientRect();
856
- currentLeft = rect.left;
857
- currentTop = rect.top;
858
- this.element.style.transform = 'none';
859
- this.element.style.left = `${currentLeft}px`;
860
- this.element.style.top = `${currentTop}px`;
861
- }
862
-
863
- // Calculate new position
864
- let newX = currentLeft;
865
- let newY = currentTop;
866
-
867
- switch(key) {
868
- case 'ArrowLeft':
869
- newX -= step;
870
- break;
871
- case 'ArrowRight':
872
- newX += step;
873
- break;
874
- case 'ArrowUp':
875
- newY -= step;
876
- break;
877
- case 'ArrowDown':
878
- newY += step;
879
- break;
880
- }
881
-
882
- // Apply position
883
- this.element.style.left = `${newX}px`;
884
- this.element.style.top = `${newY}px`;
885
-
886
- // Call custom handler if provided
887
- if (this.options.onDrag) {
888
- this.options.onDrag({ x: newX, y: newY });
889
- }
890
- }
891
-
892
- keyboardResize(key: string, shiftKey: boolean): void {
893
- const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
894
- const rect = this.element.getBoundingClientRect();
895
-
896
- let width = rect.width;
897
- let height = rect.height;
898
-
899
- // Adjust width/height based on arrow key
900
- switch(key) {
901
- case 'ArrowLeft':
902
- width -= step;
903
- break;
904
- case 'ArrowRight':
905
- width += step;
906
- break;
907
- case 'ArrowUp':
908
- if (this.options.maintainAspectRatio) {
909
- width += step;
910
- } else {
911
- height -= step;
912
- }
913
- break;
914
- case 'ArrowDown':
915
- if (this.options.maintainAspectRatio) {
916
- width -= step;
917
- } else {
918
- height += step;
919
- }
920
- break;
921
- }
922
-
923
- // Constrain to minimum dimensions
924
- width = Math.max(this.options.minWidth, width);
925
- height = Math.max(this.options.minHeight, height);
926
-
927
- // Apply new dimensions
928
- this.element.style.width = `${width}px`;
929
- if (!this.options.maintainAspectRatio) {
930
- this.element.style.height = `${height}px`;
931
- } else {
932
- this.element.style.height = 'auto';
933
- }
934
-
935
- // Call custom handler if provided
936
- if (this.options.onResize) {
937
- this.options.onResize({ width, height });
938
- }
939
- }
940
-
941
- resetPosition(): void {
942
- this.element.style.left = '50%';
943
- this.element.style.top = '50%';
944
- this.element.style.transform = 'translate(-50%, -50%)';
945
- this.element.style.right = '';
946
- this.element.style.bottom = '';
947
-
948
- // Clear manual positioning flag
949
- this.manuallyPositioned = false;
950
-
951
- // Call custom handler if provided
952
- if (this.options.onDrag) {
953
- this.options.onDrag({ centered: true });
954
- }
955
- }
956
-
957
- destroy(): void {
958
- const dragHandle = this.options.dragHandle || this.element;
959
-
960
- this.disablePointerResizeMode();
961
-
962
- // Remove drag events
963
- dragHandle.removeEventListener('mousedown', this.handlers.mousedown);
964
- dragHandle.removeEventListener('touchstart', this.handlers.touchstart);
965
- dragHandle.removeEventListener('pointerdown', this.handlers.pointerdown);
966
-
967
- // Remove document-level events
968
- document.removeEventListener('mousemove', this.handlers.mousemove);
969
- document.removeEventListener('mouseup', this.handlers.mouseup);
970
- document.removeEventListener('touchmove', this.handlers.touchmove);
971
- document.removeEventListener('touchend', this.handlers.touchend);
972
- document.removeEventListener('pointermove', this.handlers.pointermove);
973
- document.removeEventListener('pointerup', this.handlers.pointerup);
974
- document.removeEventListener('pointercancel', this.handlers.pointercancel);
975
-
976
- // Remove keyboard events
977
- this.element.removeEventListener('keydown', this.handlers.keydown);
978
-
979
- // Remove resize handle events
980
- if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
981
- this.options.resizeHandles.forEach(handle => {
982
- handle.removeEventListener('mousedown', this.handlers.resizeHandleMousedown);
983
- handle.removeEventListener('touchstart', this.handlers.resizeHandleMousedown);
984
- handle.removeEventListener('pointerdown', this.handlers.resizeHandlePointerDown);
985
- });
986
- }
987
-
988
- // Clean up classes
989
- this.element.classList.remove(
990
- `${this.options.classPrefix}-dragging`,
991
- `${this.options.classPrefix}-resizing`,
992
- `${this.options.classPrefix}-keyboard-drag`,
993
- `${this.options.classPrefix}-keyboard-resize`
994
- );
995
- }
996
- }
997
-
1
+ import type { StorageManager } from './StorageManager.js';
2
+
3
+ export interface DraggableResizableOptions {
4
+ dragHandle?: HTMLElement | null;
5
+ resizeHandles?: HTMLElement[];
6
+ onDragStart?: ((e: Event) => boolean | void) | null;
7
+ onDrag?: ((pos: { x?: number; y?: number; centered?: boolean }) => void) | null;
8
+ onDragEnd?: (() => void) | null;
9
+ onResizeStart?: (() => void) | null;
10
+ onResize?: ((dims: { width: number; height: number; left?: number; top?: number }) => void) | null;
11
+ onResizeEnd?: (() => void) | null;
12
+ constrainToViewport?: boolean;
13
+ minWidth?: number;
14
+ minHeight?: number;
15
+ maintainAspectRatio?: boolean;
16
+ keyboardDragKey?: string;
17
+ keyboardResizeKey?: string;
18
+ keyboardStep?: number;
19
+ keyboardStepLarge?: number;
20
+ maxWidth?: number | (() => number) | null;
21
+ maxHeight?: number | (() => number) | null;
22
+ pointerResizeIndicatorText?: string | null;
23
+ onPointerResizeToggle?: ((enabled: boolean) => void) | null;
24
+ classPrefix?: string;
25
+ storage?: StorageManager | null;
26
+ storageKey?: string | null;
27
+ }
28
+
29
+ interface ResolvedOptions extends Required<Omit<DraggableResizableOptions, 'maxWidth' | 'maxHeight'>> {
30
+ maxWidth: number | (() => number) | null;
31
+ maxHeight: number | (() => number) | null;
32
+ }
33
+
34
+ export class DraggableResizable {
35
+ element: HTMLElement;
36
+ options: ResolvedOptions;
37
+ isDragging: boolean;
38
+ isResizing: boolean;
39
+ resizeDirection: string | null;
40
+ dragOffsetX: number;
41
+ dragOffsetY: number;
42
+ positionOffsetX: number;
43
+ positionOffsetY: number;
44
+ initialMouseX: number;
45
+ initialMouseY: number;
46
+ needsPositionConversion: boolean;
47
+ resizeStartX: number;
48
+ resizeStartY: number;
49
+ resizeStartWidth: number;
50
+ resizeStartHeight: number;
51
+ resizeStartLeft: number;
52
+ resizeStartTop: number;
53
+ keyboardDragMode: boolean;
54
+ keyboardResizeMode: boolean;
55
+ pointerResizeMode: boolean;
56
+ manuallyPositioned: boolean;
57
+ resizeHandlesManaged: Map<HTMLElement, boolean>;
58
+ resizeIndicatorElement: HTMLElement | null;
59
+ handlers: {
60
+ mousedown: (e: MouseEvent) => void;
61
+ mousemove: (e: MouseEvent) => void;
62
+ mouseup: () => void;
63
+ touchstart: (e: TouchEvent) => void;
64
+ touchmove: (e: TouchEvent) => void;
65
+ touchend: () => void;
66
+ pointerdown: (e: PointerEvent) => void;
67
+ pointermove: (e: PointerEvent) => void;
68
+ pointerup: (e: PointerEvent) => void;
69
+ pointercancel: (e: PointerEvent) => void;
70
+ keydown: (e: KeyboardEvent) => void;
71
+ resizeHandleMousedown: (e: MouseEvent | TouchEvent) => void;
72
+ resizeHandlePointerDown: (e: PointerEvent) => void;
73
+ };
74
+ activePointerId: number | null;
75
+ activePointerType: string | null;
76
+
77
+ constructor(element: HTMLElement, options: DraggableResizableOptions = {}) {
78
+ this.element = element;
79
+ this.options = {
80
+ dragHandle: null,
81
+ resizeHandles: [],
82
+ onDragStart: null,
83
+ onDrag: null,
84
+ onDragEnd: null,
85
+ onResizeStart: null,
86
+ onResize: null,
87
+ onResizeEnd: null,
88
+ constrainToViewport: true,
89
+ minWidth: 150,
90
+ minHeight: 100,
91
+ maintainAspectRatio: false,
92
+ keyboardDragKey: 'd',
93
+ keyboardResizeKey: 'r',
94
+ keyboardStep: 5,
95
+ keyboardStepLarge: 10,
96
+ maxWidth: null,
97
+ maxHeight: null,
98
+ pointerResizeIndicatorText: null,
99
+ onPointerResizeToggle: null,
100
+ classPrefix: 'draggable',
101
+ storage: null,
102
+ storageKey: null,
103
+ ...options
104
+ } as ResolvedOptions;
105
+
106
+ // State
107
+ this.isDragging = false;
108
+ this.isResizing = false;
109
+ this.resizeDirection = null;
110
+ this.dragOffsetX = 0;
111
+ this.dragOffsetY = 0;
112
+ this.positionOffsetX = 0;
113
+ this.positionOffsetY = 0;
114
+ this.initialMouseX = 0;
115
+ this.initialMouseY = 0;
116
+ this.needsPositionConversion = false;
117
+ this.resizeStartX = 0;
118
+ this.resizeStartY = 0;
119
+ this.resizeStartWidth = 0;
120
+ this.resizeStartHeight = 0;
121
+ this.resizeStartLeft = 0;
122
+ this.resizeStartTop = 0;
123
+ this.keyboardDragMode = false;
124
+ this.keyboardResizeMode = false;
125
+ this.pointerResizeMode = false;
126
+ this.manuallyPositioned = false; // Flag to track if user has manually moved/resized
127
+ this.resizeHandlesManaged = new Map();
128
+ this.resizeIndicatorElement = null;
129
+
130
+ // Event handlers
131
+ this.handlers = {
132
+ mousedown: this.onMouseDown.bind(this),
133
+ mousemove: this.onMouseMove.bind(this),
134
+ mouseup: this.onMouseUp.bind(this),
135
+ touchstart: this.onTouchStart.bind(this),
136
+ touchmove: this.onTouchMove.bind(this),
137
+ touchend: this.onTouchEnd.bind(this),
138
+ pointerdown: this.onPointerDown.bind(this),
139
+ pointermove: this.onPointerMove.bind(this),
140
+ pointerup: this.onPointerUp.bind(this),
141
+ pointercancel: this.onPointerUp.bind(this),
142
+ keydown: this.onKeyDown.bind(this),
143
+ resizeHandleMousedown: this.onResizeHandleMouseDown.bind(this),
144
+ resizeHandlePointerDown: this.onResizeHandlePointerDown.bind(this)
145
+ };
146
+
147
+ // Pointer tracking (Pointer Events unify mouse + touch)
148
+ this.activePointerId = null;
149
+ this.activePointerType = null;
150
+
151
+ this.init();
152
+ }
153
+
154
+ hasManagedResizeHandles(): boolean {
155
+ return Array.from(this.resizeHandlesManaged.values()).some(Boolean);
156
+ }
157
+
158
+ storeOriginalHandleDisplay(handle: HTMLElement): void {
159
+ if (!handle.dataset.originalDisplay) {
160
+ handle.dataset.originalDisplay = handle.style.display || '';
161
+ }
162
+ }
163
+
164
+ hideResizeHandle(handle: HTMLElement): void {
165
+ handle.style.display = 'none';
166
+ handle.setAttribute('aria-hidden', 'true');
167
+ }
168
+
169
+ showResizeHandle(handle: HTMLElement): void {
170
+ const original = handle.dataset.originalDisplay !== undefined ? handle.dataset.originalDisplay : '';
171
+ handle.style.display = original;
172
+ handle.removeAttribute('aria-hidden');
173
+ }
174
+
175
+ setManagedHandlesVisible(visible: boolean): void {
176
+ if (!this.options.resizeHandles || this.options.resizeHandles.length === 0) {
177
+ return;
178
+ }
179
+
180
+ this.options.resizeHandles.forEach(handle => {
181
+ if (!this.resizeHandlesManaged.get(handle)) {
182
+ return;
183
+ }
184
+
185
+ if (visible) {
186
+ this.showResizeHandle(handle);
187
+ } else {
188
+ this.hideResizeHandle(handle);
189
+ }
190
+ });
191
+ }
192
+
193
+ init(): void {
194
+ const dragHandle = this.options.dragHandle || this.element;
195
+
196
+ // Prefer Pointer Events when available (covers mouse + touch in one codepath)
197
+ if (typeof window !== 'undefined' && 'PointerEvent' in window) {
198
+ dragHandle.addEventListener('pointerdown', this.handlers.pointerdown);
199
+
200
+ // Document-level up/cancel events (always needed to end a drag/resize that
201
+ // started inside the element even when the pointer leaves the element).
202
+ // pointermove is NOT registered here — it is added lazily in startDragging()
203
+ // and startResizing() with { passive: false } so that it can call
204
+ // e.preventDefault() to suppress scroll during an active interaction, and
205
+ // removed again in stopDragging() / stopResizing(). Keeping the listener
206
+ // absent while the element is merely visible prevents the non-passive
207
+ // handler from blocking compositor-threaded scroll on the rest of the page.
208
+ document.addEventListener('pointerup', this.handlers.pointerup);
209
+ document.addEventListener('pointercancel', this.handlers.pointercancel);
210
+ } else {
211
+ // Fallback for very old browsers
212
+ dragHandle.addEventListener('mousedown', this.handlers.mousedown);
213
+ dragHandle.addEventListener('touchstart', this.handlers.touchstart, { passive: false });
214
+
215
+ // Document-level move/up events
216
+ document.addEventListener('mousemove', this.handlers.mousemove);
217
+ document.addEventListener('mouseup', this.handlers.mouseup);
218
+ document.addEventListener('touchmove', this.handlers.touchmove, { passive: false });
219
+ document.addEventListener('touchend', this.handlers.touchend);
220
+ }
221
+
222
+ // Keyboard events
223
+ this.element.addEventListener('keydown', this.handlers.keydown);
224
+
225
+ // Resize handles
226
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
227
+ this.options.resizeHandles.forEach(handle => {
228
+ if (typeof window !== 'undefined' && 'PointerEvent' in window) {
229
+ handle.addEventListener('pointerdown', this.handlers.resizeHandlePointerDown);
230
+ } else {
231
+ handle.addEventListener('mousedown', this.handlers.resizeHandleMousedown);
232
+ handle.addEventListener('touchstart', this.handlers.resizeHandleMousedown, { passive: false });
233
+ }
234
+
235
+ const managed = handle.dataset.vidplyManagedResize === 'true';
236
+ this.resizeHandlesManaged.set(handle, managed);
237
+ if (managed) {
238
+ this.storeOriginalHandleDisplay(handle);
239
+ this.hideResizeHandle(handle);
240
+ }
241
+ });
242
+ }
243
+ }
244
+
245
+ onPointerDown(e: PointerEvent): void {
246
+ if (e.isPrimary === false) return;
247
+ if (e.pointerType === 'mouse' && e.button !== 0) return;
248
+
249
+ const target = e.target as HTMLElement;
250
+ if (target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
251
+ return;
252
+ }
253
+
254
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
255
+ return;
256
+ }
257
+
258
+ this.activePointerId = e.pointerId;
259
+ this.activePointerType = e.pointerType;
260
+
261
+ try {
262
+ (e.currentTarget as HTMLElement)?.setPointerCapture?.(e.pointerId);
263
+ } catch {
264
+ // ignore
265
+ }
266
+
267
+ this.startDragging(e.clientX, e.clientY);
268
+ e.preventDefault();
269
+ }
270
+
271
+ onPointerMove(e: PointerEvent): void {
272
+ if (this.activePointerId !== null && e.pointerId !== this.activePointerId) return;
273
+
274
+ if (this.isDragging) {
275
+ this.drag(e.clientX, e.clientY);
276
+ e.preventDefault();
277
+ } else if (this.isResizing) {
278
+ this.resize(e.clientX, e.clientY);
279
+ e.preventDefault();
280
+ }
281
+ }
282
+
283
+ onPointerUp(e: PointerEvent): void {
284
+ if (this.activePointerId !== null && e.pointerId !== this.activePointerId) return;
285
+
286
+ if (this.isDragging) {
287
+ this.stopDragging();
288
+ } else if (this.isResizing) {
289
+ this.stopResizing();
290
+ }
291
+
292
+ this.activePointerId = null;
293
+ this.activePointerType = null;
294
+ }
295
+
296
+ onMouseDown(e: MouseEvent): void {
297
+ const target = e.target as HTMLElement;
298
+ if (target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
299
+ return;
300
+ }
301
+
302
+ // Call custom handler if provided
303
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
304
+ return;
305
+ }
306
+
307
+ this.startDragging(e.clientX, e.clientY);
308
+ e.preventDefault();
309
+ }
310
+
311
+ onTouchStart(e: TouchEvent): void {
312
+ const target = e.target as HTMLElement;
313
+ if (target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
314
+ return;
315
+ }
316
+
317
+ // Call custom handler if provided
318
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
319
+ return;
320
+ }
321
+
322
+ const touch = e.touches[0];
323
+ if (!touch) return;
324
+ this.startDragging(touch.clientX, touch.clientY);
325
+ // Prevent page scroll while dragging (requires passive:false listener)
326
+ e.preventDefault();
327
+ }
328
+
329
+ onResizeHandlePointerDown(e: PointerEvent): void {
330
+ // Only handle primary pointer
331
+ if (e.isPrimary === false) return;
332
+
333
+ // Only left click for mouse
334
+ if (e.pointerType === 'mouse' && e.button !== 0) return;
335
+
336
+ e.preventDefault();
337
+ e.stopPropagation();
338
+
339
+ const handle = e.target as HTMLElement;
340
+ this.resizeDirection = handle.getAttribute('data-direction');
341
+
342
+ this.activePointerId = e.pointerId;
343
+ this.activePointerType = e.pointerType;
344
+
345
+ try {
346
+ (e.currentTarget as HTMLElement)?.setPointerCapture?.(e.pointerId);
347
+ } catch {
348
+ // ignore
349
+ }
350
+
351
+ this.startResizing(e.clientX, e.clientY);
352
+ }
353
+
354
+ onResizeHandleMouseDown(e: MouseEvent | TouchEvent): void {
355
+ e.preventDefault();
356
+ e.stopPropagation();
357
+
358
+ const handle = e.target as HTMLElement;
359
+ this.resizeDirection = handle.getAttribute('data-direction');
360
+
361
+ const clientX = 'clientX' in e ? e.clientX : e.touches?.[0]?.clientX;
362
+ const clientY = 'clientY' in e ? e.clientY : e.touches?.[0]?.clientY;
363
+ if (clientX === undefined || clientY === undefined) return;
364
+
365
+ this.startResizing(clientX, clientY);
366
+ }
367
+
368
+ onMouseMove(e: MouseEvent): void {
369
+ if (this.isDragging) {
370
+ this.drag(e.clientX, e.clientY);
371
+ e.preventDefault();
372
+ } else if (this.isResizing) {
373
+ this.resize(e.clientX, e.clientY);
374
+ e.preventDefault();
375
+ }
376
+ }
377
+
378
+ onTouchMove(e: TouchEvent): void {
379
+ if (this.isDragging || this.isResizing) {
380
+ const touch = e.touches[0];
381
+ if (!touch) return;
382
+ if (this.isDragging) {
383
+ this.drag(touch.clientX, touch.clientY);
384
+ } else {
385
+ this.resize(touch.clientX, touch.clientY);
386
+ }
387
+ e.preventDefault();
388
+ }
389
+ }
390
+
391
+ onMouseUp(): void {
392
+ if (this.isDragging) {
393
+ this.stopDragging();
394
+ } else if (this.isResizing) {
395
+ this.stopResizing();
396
+ }
397
+ }
398
+
399
+ onTouchEnd(): void {
400
+ if (this.isDragging) {
401
+ this.stopDragging();
402
+ } else if (this.isResizing) {
403
+ this.stopResizing();
404
+ }
405
+ }
406
+
407
+ onKeyDown(e: KeyboardEvent): void {
408
+ // Toggle drag mode
409
+ if (e.key.toLowerCase() === this.options.keyboardDragKey.toLowerCase()) {
410
+ e.preventDefault();
411
+ this.toggleKeyboardDragMode();
412
+ return;
413
+ }
414
+
415
+ // Toggle resize mode
416
+ if (e.key.toLowerCase() === this.options.keyboardResizeKey.toLowerCase()) {
417
+ e.preventDefault();
418
+ if (this.hasManagedResizeHandles()) {
419
+ this.togglePointerResizeMode();
420
+ } else {
421
+ this.toggleKeyboardResizeMode();
422
+ }
423
+ return;
424
+ }
425
+
426
+ // Exit modes with Escape
427
+ if (e.key === 'Escape') {
428
+ if (this.pointerResizeMode) {
429
+ e.preventDefault();
430
+ this.disablePointerResizeMode();
431
+ return;
432
+ }
433
+ if (this.keyboardDragMode || this.keyboardResizeMode) {
434
+ e.preventDefault();
435
+ this.disableKeyboardDragMode();
436
+ this.disableKeyboardResizeMode();
437
+ return;
438
+ }
439
+ }
440
+
441
+ // Arrow keys for drag/resize
442
+ if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
443
+ if (this.keyboardDragMode) {
444
+ e.preventDefault();
445
+ e.stopPropagation();
446
+ this.keyboardDrag(e.key, e.shiftKey);
447
+ } else if (this.keyboardResizeMode) {
448
+ e.preventDefault();
449
+ e.stopPropagation();
450
+ this.keyboardResize(e.key, e.shiftKey);
451
+ }
452
+ }
453
+
454
+ // Home key to reset position
455
+ if (e.key === 'Home' && (this.keyboardDragMode || this.keyboardResizeMode)) {
456
+ e.preventDefault();
457
+ this.resetPosition();
458
+ }
459
+ }
460
+
461
+ startDragging(clientX: number, clientY: number): void {
462
+ // Get current rendered position BEFORE any changes
463
+ const rect = this.element.getBoundingClientRect();
464
+
465
+ // Convert position to left/top IMMEDIATELY (before setting any state)
466
+ // Check if element is using right/bottom/transform positioning
467
+ const computedStyle = window.getComputedStyle(this.element);
468
+ const needsConversion = computedStyle.right !== 'auto' ||
469
+ computedStyle.bottom !== 'auto' ||
470
+ computedStyle.transform !== 'none';
471
+
472
+ this.positionOffsetX = 0;
473
+ this.positionOffsetY = 0;
474
+
475
+ if (needsConversion) {
476
+ // Determine the correct left/top values based on position type
477
+ let targetLeft, targetTop;
478
+
479
+ if (computedStyle.position === 'absolute') {
480
+ // position: absolute uses container-relative coordinates
481
+ const offsetParent = this.element.offsetParent || document.body;
482
+ const parentRect = offsetParent.getBoundingClientRect();
483
+ targetLeft = rect.left - parentRect.left;
484
+ targetTop = rect.top - parentRect.top;
485
+ this.positionOffsetX = parentRect.left;
486
+ this.positionOffsetY = parentRect.top;
487
+ } else if (computedStyle.position === 'fixed') {
488
+ const parsedLeft = parseFloat(computedStyle.left);
489
+ const parsedTop = parseFloat(computedStyle.top);
490
+ const hasLeft = Number.isFinite(parsedLeft);
491
+ const hasTop = Number.isFinite(parsedTop);
492
+ targetLeft = hasLeft ? parsedLeft : rect.left;
493
+ targetTop = hasTop ? parsedTop : rect.top;
494
+ this.positionOffsetX = rect.left - targetLeft;
495
+ this.positionOffsetY = rect.top - targetTop;
496
+ } else {
497
+ // fallback: treat as viewport-relative
498
+ targetLeft = rect.left;
499
+ targetTop = rect.top;
500
+ this.positionOffsetX = rect.left - targetLeft;
501
+ this.positionOffsetY = rect.top - targetTop;
502
+ }
503
+
504
+ // Build complete style update atomically
505
+ const currentCssText = this.element.style.cssText;
506
+ let newCssText = currentCssText
507
+ .split(';')
508
+ .filter(rule => {
509
+ const trimmed = rule.trim();
510
+ // Filter out empty rules
511
+ if (!trimmed) return false;
512
+
513
+ const colonIndex = trimmed.indexOf(':');
514
+ if (colonIndex === -1) return false;
515
+
516
+ const property = trimmed.substring(0, colonIndex).trim();
517
+ const value = trimmed.substring(colonIndex + 1).trim();
518
+
519
+ // Skip if value is empty or just whitespace (this catches empty border properties)
520
+ if (!value || value === '') return false;
521
+
522
+ // Skip positioning properties
523
+ if (property === 'right' || property === 'bottom' || property === 'transform' ||
524
+ property === 'left' || property === 'top' || property === 'inset') {
525
+ return false;
526
+ }
527
+
528
+ // Skip border-image properties (these can have empty values that cause parse errors)
529
+ if (property.startsWith('border-image')) {
530
+ return false;
531
+ }
532
+
533
+ return true;
534
+ })
535
+ .join('; ');
536
+
537
+ if (newCssText) newCssText += '; ';
538
+ newCssText += `left: ${targetLeft}px; top: ${targetTop}px; right: auto; bottom: auto; transform: none`;
539
+
540
+ // Apply all at once
541
+ this.element.style.cssText = newCssText;
542
+ }
543
+
544
+ // Now calculate offsets based on CURRENT position (after conversion)
545
+ // Re-get rect after potential position change
546
+ const finalRect = this.element.getBoundingClientRect();
547
+ this.dragOffsetX = clientX - finalRect.left;
548
+ this.dragOffsetY = clientY - finalRect.top;
549
+
550
+ this.isDragging = true;
551
+ this.element.classList.add(`${this.options.classPrefix}-dragging`);
552
+ document.body.style.cursor = 'grabbing';
553
+ document.body.style.userSelect = 'none';
554
+
555
+ // Register the non-passive pointermove listener only for the duration of
556
+ // the drag so it does not block compositor-threaded scroll at other times.
557
+ if ('PointerEvent' in window) {
558
+ document.addEventListener('pointermove', this.handlers.pointermove, { passive: false });
559
+ }
560
+ }
561
+
562
+ drag(clientX: number, clientY: number): void {
563
+ if (!this.isDragging) return;
564
+
565
+ // Calculate new position: current mouse position minus the offset where user clicked
566
+ let newX = clientX - this.dragOffsetX - this.positionOffsetX;
567
+ let newY = clientY - this.dragOffsetY - this.positionOffsetY;
568
+
569
+ // Constrain to viewport if needed
570
+ if (this.options.constrainToViewport) {
571
+ const rect = this.element.getBoundingClientRect();
572
+ const viewportWidth = document.documentElement.clientWidth;
573
+ const viewportHeight = document.documentElement.clientHeight;
574
+
575
+ // Keep at least 100px visible
576
+ const minVisible = 100;
577
+ const minX = -(rect.width - minVisible);
578
+ const minY = -(rect.height - minVisible);
579
+ const maxX = viewportWidth - minVisible;
580
+ const maxY = viewportHeight - minVisible;
581
+
582
+ newX = Math.max(minX, Math.min(newX, maxX));
583
+ newY = Math.max(minY, Math.min(newY, maxY));
584
+ }
585
+
586
+ this.element.style.left = `${newX}px`;
587
+ this.element.style.top = `${newY}px`;
588
+
589
+ // Call custom handler if provided
590
+ if (this.options.onDrag) {
591
+ this.options.onDrag({ x: newX, y: newY });
592
+ }
593
+ }
594
+
595
+ stopDragging(): void {
596
+ this.isDragging = false;
597
+ this.element.classList.remove(`${this.options.classPrefix}-dragging`);
598
+ document.body.style.cursor = '';
599
+ document.body.style.userSelect = '';
600
+
601
+ // Remove the non-passive pointermove listener that was added in startDragging().
602
+ if ('PointerEvent' in window) {
603
+ document.removeEventListener('pointermove', this.handlers.pointermove);
604
+ }
605
+
606
+ // Mark as manually positioned
607
+ this.manuallyPositioned = true;
608
+
609
+ // Call custom handler if provided
610
+ if (this.options.onDragEnd) {
611
+ this.options.onDragEnd();
612
+ }
613
+ }
614
+
615
+ startResizing(clientX: number, clientY: number): void {
616
+ this.isResizing = true;
617
+ this.resizeStartX = clientX;
618
+ this.resizeStartY = clientY;
619
+
620
+ const rect = this.element.getBoundingClientRect();
621
+ this.resizeStartWidth = rect.width;
622
+ this.resizeStartHeight = rect.height;
623
+ this.resizeStartLeft = rect.left;
624
+ this.resizeStartTop = rect.top;
625
+
626
+ this.element.classList.add(`${this.options.classPrefix}-resizing`);
627
+ document.body.style.userSelect = 'none';
628
+
629
+ // Register the non-passive pointermove listener only for the duration of
630
+ // the resize so it does not block compositor-threaded scroll at other times.
631
+ if ('PointerEvent' in window) {
632
+ document.addEventListener('pointermove', this.handlers.pointermove, { passive: false });
633
+ }
634
+
635
+ // Call custom handler if provided
636
+ if (this.options.onResizeStart) {
637
+ this.options.onResizeStart();
638
+ }
639
+ }
640
+
641
+ resize(clientX: number, clientY: number): void {
642
+ if (!this.isResizing) return;
643
+
644
+ const deltaX = clientX - this.resizeStartX;
645
+ const deltaY = clientY - this.resizeStartY;
646
+
647
+ let newWidth = this.resizeStartWidth;
648
+ let newHeight = this.resizeStartHeight;
649
+ let newLeft = this.resizeStartLeft;
650
+ let newTop = this.resizeStartTop;
651
+
652
+ // Handle horizontal resizing
653
+ const resizeDirection = this.resizeDirection ?? '';
654
+
655
+ if (resizeDirection.includes('e')) {
656
+ newWidth = Math.max(this.options.minWidth, this.resizeStartWidth + deltaX);
657
+ }
658
+ if (resizeDirection.includes('w')) {
659
+ const proposedWidth = Math.max(this.options.minWidth, this.resizeStartWidth - deltaX);
660
+ newLeft = this.resizeStartLeft + (this.resizeStartWidth - proposedWidth);
661
+ newWidth = proposedWidth;
662
+ }
663
+
664
+ const maxWidthOption = typeof this.options.maxWidth === 'function'
665
+ ? this.options.maxWidth()
666
+ : this.options.maxWidth;
667
+ if (typeof maxWidthOption === 'number' && Number.isFinite(maxWidthOption)) {
668
+ const clampedWidth = Math.min(newWidth, maxWidthOption);
669
+ if (clampedWidth !== newWidth && resizeDirection.includes('w')) {
670
+ newLeft += newWidth - clampedWidth;
671
+ }
672
+ newWidth = clampedWidth;
673
+ }
674
+
675
+ // Handle vertical resizing (if not maintaining aspect ratio)
676
+ if (!this.options.maintainAspectRatio) {
677
+ if (resizeDirection.includes('s')) {
678
+ newHeight = Math.max(this.options.minHeight, this.resizeStartHeight + deltaY);
679
+ }
680
+ if (resizeDirection.includes('n')) {
681
+ const proposedHeight = Math.max(this.options.minHeight, this.resizeStartHeight - deltaY);
682
+ newTop = this.resizeStartTop + (this.resizeStartHeight - proposedHeight);
683
+ newHeight = proposedHeight;
684
+ }
685
+
686
+ const maxHeightOption = typeof this.options.maxHeight === 'function'
687
+ ? this.options.maxHeight()
688
+ : this.options.maxHeight;
689
+ if (typeof maxHeightOption === 'number' && Number.isFinite(maxHeightOption)) {
690
+ const clampedHeight = Math.min(newHeight, maxHeightOption);
691
+ if (clampedHeight !== newHeight && resizeDirection.includes('n')) {
692
+ newTop += newHeight - clampedHeight;
693
+ }
694
+ newHeight = clampedHeight;
695
+ }
696
+ }
697
+
698
+ // Apply new dimensions
699
+ this.element.style.width = `${newWidth}px`;
700
+ if (!this.options.maintainAspectRatio) {
701
+ this.element.style.height = `${newHeight}px`;
702
+ } else {
703
+ this.element.style.height = 'auto';
704
+ }
705
+
706
+ // Apply new position if resizing from west or north
707
+ if (resizeDirection.includes('w')) {
708
+ this.element.style.left = `${newLeft}px`;
709
+ }
710
+ if (resizeDirection.includes('n') && !this.options.maintainAspectRatio) {
711
+ this.element.style.top = `${newTop}px`;
712
+ }
713
+
714
+ // Call custom handler if provided
715
+ if (this.options.onResize) {
716
+ this.options.onResize({ width: newWidth, height: newHeight, left: newLeft, top: newTop });
717
+ }
718
+ }
719
+
720
+ stopResizing(): void {
721
+ this.isResizing = false;
722
+ this.resizeDirection = null;
723
+ this.element.classList.remove(`${this.options.classPrefix}-resizing`);
724
+ document.body.style.userSelect = '';
725
+
726
+ // Remove the non-passive pointermove listener that was added in startResizing().
727
+ if ('PointerEvent' in window) {
728
+ document.removeEventListener('pointermove', this.handlers.pointermove);
729
+ }
730
+
731
+ // Mark as manually positioned
732
+ this.manuallyPositioned = true;
733
+
734
+ // Call custom handler if provided
735
+ if (this.options.onResizeEnd) {
736
+ this.options.onResizeEnd();
737
+ }
738
+ }
739
+
740
+ toggleKeyboardDragMode(): void {
741
+ if (this.keyboardDragMode) {
742
+ this.disableKeyboardDragMode();
743
+ } else {
744
+ this.enableKeyboardDragMode();
745
+ }
746
+ }
747
+
748
+ enableKeyboardDragMode(): void {
749
+ this.keyboardDragMode = true;
750
+ this.keyboardResizeMode = false;
751
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-drag`);
752
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
753
+ this.focusElement();
754
+ }
755
+
756
+ disableKeyboardDragMode(): void {
757
+ this.keyboardDragMode = false;
758
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
759
+ }
760
+
761
+ toggleKeyboardResizeMode(): void {
762
+ if (this.keyboardResizeMode) {
763
+ this.disableKeyboardResizeMode();
764
+ } else {
765
+ this.enableKeyboardResizeMode();
766
+ }
767
+ }
768
+
769
+ enableKeyboardResizeMode(): void {
770
+ this.keyboardResizeMode = true;
771
+ this.keyboardDragMode = false;
772
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-resize`);
773
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
774
+ this.focusElement();
775
+ }
776
+
777
+ disableKeyboardResizeMode(): void {
778
+ this.keyboardResizeMode = false;
779
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
780
+ }
781
+
782
+ enablePointerResizeMode({ focus = true }: { focus?: boolean } = {}): void {
783
+ if (!this.hasManagedResizeHandles()) {
784
+ this.enableKeyboardResizeMode();
785
+ return;
786
+ }
787
+
788
+ if (this.pointerResizeMode) {
789
+ return;
790
+ }
791
+
792
+ this.pointerResizeMode = true;
793
+ this.setManagedHandlesVisible(true);
794
+ this.element.classList.add(`${this.options.classPrefix}-resizable`);
795
+ this.enableKeyboardResizeMode();
796
+
797
+ if (focus) {
798
+ this.focusElement();
799
+ }
800
+
801
+ if (typeof this.options.onPointerResizeToggle === 'function') {
802
+ this.options.onPointerResizeToggle(true);
803
+ }
804
+ }
805
+
806
+ disablePointerResizeMode({ focus = false }: { focus?: boolean } = {}): void {
807
+ if (!this.pointerResizeMode) {
808
+ return;
809
+ }
810
+
811
+ this.pointerResizeMode = false;
812
+ this.setManagedHandlesVisible(false);
813
+ this.element.classList.remove(`${this.options.classPrefix}-resizable`);
814
+ this.disableKeyboardResizeMode();
815
+
816
+ if (focus) {
817
+ this.focusElement();
818
+ }
819
+
820
+ if (typeof this.options.onPointerResizeToggle === 'function') {
821
+ this.options.onPointerResizeToggle(false);
822
+ }
823
+ }
824
+
825
+ togglePointerResizeMode(): boolean {
826
+ if (this.pointerResizeMode) {
827
+ this.disablePointerResizeMode();
828
+ } else {
829
+ this.enablePointerResizeMode();
830
+ }
831
+ return this.pointerResizeMode;
832
+ }
833
+
834
+ focusElement(): void {
835
+ if (typeof this.element.focus === 'function') {
836
+ try {
837
+ this.element.focus({ preventScroll: true });
838
+ } catch {
839
+ // Some browsers do not support the preventScroll option; fallback without it
840
+ this.element.focus();
841
+ }
842
+ }
843
+ }
844
+
845
+ keyboardDrag(key: string, shiftKey: boolean): void {
846
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
847
+
848
+ // Get current position
849
+ let currentLeft = parseFloat(this.element.style.left) || 0;
850
+ let currentTop = parseFloat(this.element.style.top) || 0;
851
+
852
+ // If element is still centered with transform, convert to absolute position first
853
+ const computedStyle = window.getComputedStyle(this.element);
854
+ if (computedStyle.transform !== 'none') {
855
+ const rect = this.element.getBoundingClientRect();
856
+ currentLeft = rect.left;
857
+ currentTop = rect.top;
858
+ this.element.style.transform = 'none';
859
+ this.element.style.left = `${currentLeft}px`;
860
+ this.element.style.top = `${currentTop}px`;
861
+ }
862
+
863
+ // Calculate new position
864
+ let newX = currentLeft;
865
+ let newY = currentTop;
866
+
867
+ switch(key) {
868
+ case 'ArrowLeft':
869
+ newX -= step;
870
+ break;
871
+ case 'ArrowRight':
872
+ newX += step;
873
+ break;
874
+ case 'ArrowUp':
875
+ newY -= step;
876
+ break;
877
+ case 'ArrowDown':
878
+ newY += step;
879
+ break;
880
+ }
881
+
882
+ // Apply position
883
+ this.element.style.left = `${newX}px`;
884
+ this.element.style.top = `${newY}px`;
885
+
886
+ // Call custom handler if provided
887
+ if (this.options.onDrag) {
888
+ this.options.onDrag({ x: newX, y: newY });
889
+ }
890
+ }
891
+
892
+ keyboardResize(key: string, shiftKey: boolean): void {
893
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
894
+ const rect = this.element.getBoundingClientRect();
895
+
896
+ let width = rect.width;
897
+ let height = rect.height;
898
+
899
+ // Adjust width/height based on arrow key
900
+ switch(key) {
901
+ case 'ArrowLeft':
902
+ width -= step;
903
+ break;
904
+ case 'ArrowRight':
905
+ width += step;
906
+ break;
907
+ case 'ArrowUp':
908
+ if (this.options.maintainAspectRatio) {
909
+ width += step;
910
+ } else {
911
+ height -= step;
912
+ }
913
+ break;
914
+ case 'ArrowDown':
915
+ if (this.options.maintainAspectRatio) {
916
+ width -= step;
917
+ } else {
918
+ height += step;
919
+ }
920
+ break;
921
+ }
922
+
923
+ // Constrain to minimum dimensions
924
+ width = Math.max(this.options.minWidth, width);
925
+ height = Math.max(this.options.minHeight, height);
926
+
927
+ // Apply new dimensions
928
+ this.element.style.width = `${width}px`;
929
+ if (!this.options.maintainAspectRatio) {
930
+ this.element.style.height = `${height}px`;
931
+ } else {
932
+ this.element.style.height = 'auto';
933
+ }
934
+
935
+ // Call custom handler if provided
936
+ if (this.options.onResize) {
937
+ this.options.onResize({ width, height });
938
+ }
939
+ }
940
+
941
+ resetPosition(): void {
942
+ this.element.style.left = '50%';
943
+ this.element.style.top = '50%';
944
+ this.element.style.transform = 'translate(-50%, -50%)';
945
+ this.element.style.right = '';
946
+ this.element.style.bottom = '';
947
+
948
+ // Clear manual positioning flag
949
+ this.manuallyPositioned = false;
950
+
951
+ // Call custom handler if provided
952
+ if (this.options.onDrag) {
953
+ this.options.onDrag({ centered: true });
954
+ }
955
+ }
956
+
957
+ destroy(): void {
958
+ const dragHandle = this.options.dragHandle || this.element;
959
+
960
+ this.disablePointerResizeMode();
961
+
962
+ // Remove drag events
963
+ dragHandle.removeEventListener('mousedown', this.handlers.mousedown);
964
+ dragHandle.removeEventListener('touchstart', this.handlers.touchstart);
965
+ dragHandle.removeEventListener('pointerdown', this.handlers.pointerdown);
966
+
967
+ // Remove document-level events
968
+ document.removeEventListener('mousemove', this.handlers.mousemove);
969
+ document.removeEventListener('mouseup', this.handlers.mouseup);
970
+ document.removeEventListener('touchmove', this.handlers.touchmove);
971
+ document.removeEventListener('touchend', this.handlers.touchend);
972
+ document.removeEventListener('pointermove', this.handlers.pointermove);
973
+ document.removeEventListener('pointerup', this.handlers.pointerup);
974
+ document.removeEventListener('pointercancel', this.handlers.pointercancel);
975
+
976
+ // Remove keyboard events
977
+ this.element.removeEventListener('keydown', this.handlers.keydown);
978
+
979
+ // Remove resize handle events
980
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
981
+ this.options.resizeHandles.forEach(handle => {
982
+ handle.removeEventListener('mousedown', this.handlers.resizeHandleMousedown);
983
+ handle.removeEventListener('touchstart', this.handlers.resizeHandleMousedown);
984
+ handle.removeEventListener('pointerdown', this.handlers.resizeHandlePointerDown);
985
+ });
986
+ }
987
+
988
+ // Clean up classes
989
+ this.element.classList.remove(
990
+ `${this.options.classPrefix}-dragging`,
991
+ `${this.options.classPrefix}-resizing`,
992
+ `${this.options.classPrefix}-keyboard-drag`,
993
+ `${this.options.classPrefix}-keyboard-resize`
994
+ );
995
+ }
996
+ }
997
+