vidply 1.2.0 → 1.2.1

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 (164) hide show
  1. package/README.md +946 -946
  2. package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
  3. package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
  4. package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
  5. package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
  6. package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
  7. package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
  8. package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
  9. package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
  10. package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
  11. package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
  12. package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
  13. package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
  14. package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
  15. package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
  16. package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
  17. package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
  18. package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
  19. package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
  20. package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
  21. package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
  22. package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
  23. package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
  24. package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
  25. package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
  26. package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
  27. package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
  28. package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
  29. package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
  30. package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
  31. package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
  32. package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
  33. package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
  34. package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
  35. package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
  36. package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
  37. package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
  38. package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
  39. package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
  40. package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
  41. package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
  42. package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
  43. package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
  44. package/dist/dev/vidply.esm.js +73 -34
  45. package/dist/dev/vidply.esm.js.map +2 -2
  46. package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
  47. package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
  48. package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
  49. package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
  50. package/dist/legacy/vidply.js +345 -32
  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 +66 -47
  54. package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
  55. package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
  56. package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
  57. package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
  58. package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
  59. package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
  60. package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
  61. package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
  62. package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
  63. package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
  64. package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
  65. package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
  66. package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
  67. package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
  68. package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
  69. package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
  70. package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
  71. package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
  72. package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
  73. package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
  74. package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
  75. package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
  76. package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
  77. package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
  78. package/dist/prod/vidply.esm.min.js +2 -2
  79. package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
  80. package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
  81. package/dist/vidply.css +5796 -5796
  82. package/dist/vidply.esm.min.meta.json +148 -135
  83. package/package.json +7 -7
  84. package/src/controls/CaptionManager.ts +598 -598
  85. package/src/controls/ControlBar.ts +4159 -4159
  86. package/src/controls/KeyboardManager.ts +383 -383
  87. package/src/controls/SettingsDialog.ts +488 -488
  88. package/src/controls/TranscriptManager.ts +2426 -2426
  89. package/src/core/AudioDescriptionManager.ts +863 -774
  90. package/src/core/DescriptionSpeechManager.ts +232 -0
  91. package/src/core/FloatingPlayerManager.ts +674 -674
  92. package/src/core/LazyInit.ts +80 -80
  93. package/src/core/MetadataAlertsManager.ts +514 -514
  94. package/src/core/Player.ts +2915 -2872
  95. package/src/core/PosterManager.ts +169 -169
  96. package/src/core/PseudoFullscreen.ts +179 -179
  97. package/src/core/ResponsiveManager.ts +231 -231
  98. package/src/core/ResumeManager.ts +236 -236
  99. package/src/core/SignLanguageManager.ts +1308 -1308
  100. package/src/core/ThemeManager.ts +157 -157
  101. package/src/features/PlaylistManager.ts +1838 -1827
  102. package/src/i18n/i18n.ts +223 -223
  103. package/src/i18n/languages/de.ts +217 -217
  104. package/src/i18n/languages/en.ts +218 -218
  105. package/src/i18n/languages/es.ts +214 -214
  106. package/src/i18n/languages/fr.ts +214 -214
  107. package/src/i18n/languages/ja.ts +214 -214
  108. package/src/icons/Icons.ts +167 -167
  109. package/src/index.ts +196 -193
  110. package/src/renderers/DASHRenderer.ts +1077 -1077
  111. package/src/renderers/HLSRenderer.ts +944 -944
  112. package/src/renderers/HTML5Renderer.ts +389 -389
  113. package/src/renderers/SoundCloudRenderer.ts +437 -437
  114. package/src/renderers/VimeoRenderer.ts +298 -298
  115. package/src/renderers/YouTubeRenderer.ts +290 -290
  116. package/src/styles/vidply.css +5796 -5796
  117. package/src/types/events.ts +124 -122
  118. package/src/types/globals.d.ts +313 -313
  119. package/src/types/options.ts +185 -182
  120. package/src/types/renderer.ts +58 -58
  121. package/src/utils/DOMUtils.ts +281 -281
  122. package/src/utils/DownloadInfo.ts +171 -171
  123. package/src/utils/DraggablePanel.ts +539 -539
  124. package/src/utils/DraggablePanelMenu.ts +195 -195
  125. package/src/utils/DraggableResizable.ts +997 -997
  126. package/src/utils/EventEmitter.ts +59 -59
  127. package/src/utils/FormUtils.ts +95 -95
  128. package/src/utils/MenuFactory.ts +483 -483
  129. package/src/utils/Sanitize.ts +70 -70
  130. package/src/utils/SettingsMenuFactory.ts +289 -289
  131. package/src/utils/StorageManager.ts +287 -287
  132. package/src/utils/TrackLabelUtils.ts +52 -52
  133. package/src/utils/UrlSafe.ts +69 -69
  134. package/src/utils/VideoFrameCapture.ts +114 -114
  135. package/src/utils/WindowComponents.ts +250 -250
  136. package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
  137. package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
  138. package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
  139. package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
  140. package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
  141. package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
  142. package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
  143. package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
  144. package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
  145. package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
  146. package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
  147. package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
  148. package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
  149. package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
  150. package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
  151. package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
  152. package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
  153. package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
  154. package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
  155. package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
  156. package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
  157. package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
  158. package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
  159. /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
  160. /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
  161. /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
  162. /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
  163. /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
  164. /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-YVCYQZJI.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
+