vidply 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,771 @@
1
+ /**
2
+ * DraggableResizable Utility
3
+ * Provides shared drag and resize functionality for floating windows/elements
4
+ */
5
+
6
+ export class DraggableResizable {
7
+ constructor(element, options = {}) {
8
+ this.element = element;
9
+ this.options = {
10
+ dragHandle: null, // Element to use as drag handle (defaults to element itself)
11
+ resizeHandles: [], // Array of resize handle elements
12
+ onDragStart: null,
13
+ onDrag: null,
14
+ onDragEnd: null,
15
+ onResizeStart: null,
16
+ onResize: null,
17
+ onResizeEnd: null,
18
+ constrainToViewport: true, // Allow movement outside viewport?
19
+ minWidth: 150,
20
+ minHeight: 100,
21
+ maintainAspectRatio: false,
22
+ keyboardDragKey: 'd',
23
+ keyboardResizeKey: 'r',
24
+ keyboardStep: 5,
25
+ keyboardStepLarge: 10,
26
+ maxWidth: null,
27
+ maxHeight: null,
28
+ pointerResizeIndicatorText: null,
29
+ onPointerResizeToggle: null,
30
+ classPrefix: 'draggable',
31
+ storage: null, // StorageManager instance for saving position/size
32
+ storageKey: null, // Key for localStorage (if storage is provided)
33
+ ...options
34
+ };
35
+
36
+ // State
37
+ this.isDragging = false;
38
+ this.isResizing = false;
39
+ this.resizeDirection = null;
40
+ this.dragOffsetX = 0;
41
+ this.dragOffsetY = 0;
42
+ this.positionOffsetX = 0;
43
+ this.positionOffsetY = 0;
44
+ this.initialMouseX = 0;
45
+ this.initialMouseY = 0;
46
+ this.needsPositionConversion = false;
47
+ this.resizeStartX = 0;
48
+ this.resizeStartY = 0;
49
+ this.resizeStartWidth = 0;
50
+ this.resizeStartHeight = 0;
51
+ this.resizeStartLeft = 0;
52
+ this.resizeStartTop = 0;
53
+ this.keyboardDragMode = false;
54
+ this.keyboardResizeMode = false;
55
+ this.pointerResizeMode = false;
56
+ this.manuallyPositioned = false; // Flag to track if user has manually moved/resized
57
+ this.resizeHandlesManaged = new Map();
58
+ this.resizeIndicatorElement = null;
59
+
60
+ // Event handlers
61
+ this.handlers = {
62
+ mousedown: this.onMouseDown.bind(this),
63
+ mousemove: this.onMouseMove.bind(this),
64
+ mouseup: this.onMouseUp.bind(this),
65
+ touchstart: this.onTouchStart.bind(this),
66
+ touchmove: this.onTouchMove.bind(this),
67
+ touchend: this.onTouchEnd.bind(this),
68
+ keydown: this.onKeyDown.bind(this),
69
+ resizeHandleMousedown: this.onResizeHandleMouseDown.bind(this)
70
+ };
71
+
72
+ this.init();
73
+ }
74
+
75
+ hasManagedResizeHandles() {
76
+ return Array.from(this.resizeHandlesManaged.values()).some(Boolean);
77
+ }
78
+
79
+ storeOriginalHandleDisplay(handle) {
80
+ if (!handle.dataset.originalDisplay) {
81
+ handle.dataset.originalDisplay = handle.style.display || '';
82
+ }
83
+ }
84
+
85
+ hideResizeHandle(handle) {
86
+ handle.style.display = 'none';
87
+ handle.setAttribute('aria-hidden', 'true');
88
+ }
89
+
90
+ showResizeHandle(handle) {
91
+ const original = handle.dataset.originalDisplay !== undefined ? handle.dataset.originalDisplay : '';
92
+ handle.style.display = original;
93
+ handle.removeAttribute('aria-hidden');
94
+ }
95
+
96
+ setManagedHandlesVisible(visible) {
97
+ if (!this.options.resizeHandles || this.options.resizeHandles.length === 0) {
98
+ return;
99
+ }
100
+
101
+ this.options.resizeHandles.forEach(handle => {
102
+ if (!this.resizeHandlesManaged.get(handle)) {
103
+ return;
104
+ }
105
+
106
+ if (visible) {
107
+ this.showResizeHandle(handle);
108
+ } else {
109
+ this.hideResizeHandle(handle);
110
+ }
111
+ });
112
+ }
113
+
114
+ init() {
115
+ const dragHandle = this.options.dragHandle || this.element;
116
+
117
+ // Drag events
118
+ dragHandle.addEventListener('mousedown', this.handlers.mousedown);
119
+ dragHandle.addEventListener('touchstart', this.handlers.touchstart);
120
+
121
+ // Document-level move/up events
122
+ document.addEventListener('mousemove', this.handlers.mousemove);
123
+ document.addEventListener('mouseup', this.handlers.mouseup);
124
+ document.addEventListener('touchmove', this.handlers.touchmove, { passive: false });
125
+ document.addEventListener('touchend', this.handlers.touchend);
126
+
127
+ // Keyboard events
128
+ this.element.addEventListener('keydown', this.handlers.keydown);
129
+
130
+ // Resize handles
131
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
132
+ this.options.resizeHandles.forEach(handle => {
133
+ handle.addEventListener('mousedown', this.handlers.resizeHandleMousedown);
134
+ handle.addEventListener('touchstart', this.handlers.resizeHandleMousedown);
135
+
136
+ const managed = handle.dataset.vidplyManagedResize === 'true';
137
+ this.resizeHandlesManaged.set(handle, managed);
138
+ if (managed) {
139
+ this.storeOriginalHandleDisplay(handle);
140
+ this.hideResizeHandle(handle);
141
+ }
142
+ });
143
+ }
144
+ }
145
+
146
+ onMouseDown(e) {
147
+ // Don't drag if clicking on resize handle
148
+ if (e.target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
149
+ return;
150
+ }
151
+
152
+ // Call custom handler if provided
153
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
154
+ return;
155
+ }
156
+
157
+ this.startDragging(e.clientX, e.clientY);
158
+ e.preventDefault();
159
+ }
160
+
161
+ onTouchStart(e) {
162
+ // Don't drag if touching resize handle
163
+ if (e.target.classList.contains(`${this.options.classPrefix}-resize-handle`)) {
164
+ return;
165
+ }
166
+
167
+ // Call custom handler if provided
168
+ if (this.options.onDragStart && !this.options.onDragStart(e)) {
169
+ return;
170
+ }
171
+
172
+ const touch = e.touches[0];
173
+ this.startDragging(touch.clientX, touch.clientY);
174
+ }
175
+
176
+ onResizeHandleMouseDown(e) {
177
+ e.preventDefault();
178
+ e.stopPropagation();
179
+
180
+ const handle = e.target;
181
+ this.resizeDirection = handle.getAttribute('data-direction');
182
+
183
+ const clientX = e.clientX || e.touches?.[0]?.clientX;
184
+ const clientY = e.clientY || e.touches?.[0]?.clientY;
185
+
186
+ this.startResizing(clientX, clientY);
187
+ }
188
+
189
+ onMouseMove(e) {
190
+ if (this.isDragging) {
191
+ this.drag(e.clientX, e.clientY);
192
+ e.preventDefault();
193
+ } else if (this.isResizing) {
194
+ this.resize(e.clientX, e.clientY);
195
+ e.preventDefault();
196
+ }
197
+ }
198
+
199
+ onTouchMove(e) {
200
+ if (this.isDragging || this.isResizing) {
201
+ const touch = e.touches[0];
202
+ if (this.isDragging) {
203
+ this.drag(touch.clientX, touch.clientY);
204
+ } else {
205
+ this.resize(touch.clientX, touch.clientY);
206
+ }
207
+ e.preventDefault();
208
+ }
209
+ }
210
+
211
+ onMouseUp() {
212
+ if (this.isDragging) {
213
+ this.stopDragging();
214
+ } else if (this.isResizing) {
215
+ this.stopResizing();
216
+ }
217
+ }
218
+
219
+ onTouchEnd() {
220
+ if (this.isDragging) {
221
+ this.stopDragging();
222
+ } else if (this.isResizing) {
223
+ this.stopResizing();
224
+ }
225
+ }
226
+
227
+ onKeyDown(e) {
228
+ // Toggle drag mode
229
+ if (e.key.toLowerCase() === this.options.keyboardDragKey.toLowerCase()) {
230
+ e.preventDefault();
231
+ this.toggleKeyboardDragMode();
232
+ return;
233
+ }
234
+
235
+ // Toggle resize mode
236
+ if (e.key.toLowerCase() === this.options.keyboardResizeKey.toLowerCase()) {
237
+ e.preventDefault();
238
+ if (this.hasManagedResizeHandles()) {
239
+ this.togglePointerResizeMode();
240
+ } else {
241
+ this.toggleKeyboardResizeMode();
242
+ }
243
+ return;
244
+ }
245
+
246
+ // Exit modes with Escape
247
+ if (e.key === 'Escape') {
248
+ if (this.pointerResizeMode) {
249
+ e.preventDefault();
250
+ this.disablePointerResizeMode();
251
+ return;
252
+ }
253
+ if (this.keyboardDragMode || this.keyboardResizeMode) {
254
+ e.preventDefault();
255
+ this.disableKeyboardDragMode();
256
+ this.disableKeyboardResizeMode();
257
+ return;
258
+ }
259
+ }
260
+
261
+ // Arrow keys for drag/resize
262
+ if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
263
+ if (this.keyboardDragMode) {
264
+ e.preventDefault();
265
+ e.stopPropagation();
266
+ this.keyboardDrag(e.key, e.shiftKey);
267
+ } else if (this.keyboardResizeMode) {
268
+ e.preventDefault();
269
+ e.stopPropagation();
270
+ this.keyboardResize(e.key, e.shiftKey);
271
+ }
272
+ }
273
+
274
+ // Home key to reset position
275
+ if (e.key === 'Home' && (this.keyboardDragMode || this.keyboardResizeMode)) {
276
+ e.preventDefault();
277
+ this.resetPosition();
278
+ }
279
+ }
280
+
281
+ startDragging(clientX, clientY) {
282
+ // Get current rendered position BEFORE any changes
283
+ const rect = this.element.getBoundingClientRect();
284
+
285
+ // Convert position to left/top IMMEDIATELY (before setting any state)
286
+ // Check if element is using right/bottom/transform positioning
287
+ const computedStyle = window.getComputedStyle(this.element);
288
+ const needsConversion = computedStyle.right !== 'auto' ||
289
+ computedStyle.bottom !== 'auto' ||
290
+ computedStyle.transform !== 'none';
291
+
292
+ this.positionOffsetX = 0;
293
+ this.positionOffsetY = 0;
294
+
295
+ if (needsConversion) {
296
+ // Determine the correct left/top values based on position type
297
+ let targetLeft, targetTop;
298
+
299
+ if (computedStyle.position === 'absolute') {
300
+ // position: absolute uses container-relative coordinates
301
+ const offsetParent = this.element.offsetParent || document.body;
302
+ const parentRect = offsetParent.getBoundingClientRect();
303
+ targetLeft = rect.left - parentRect.left;
304
+ targetTop = rect.top - parentRect.top;
305
+ this.positionOffsetX = parentRect.left;
306
+ this.positionOffsetY = parentRect.top;
307
+ } else if (computedStyle.position === 'fixed') {
308
+ const parsedLeft = parseFloat(computedStyle.left);
309
+ const parsedTop = parseFloat(computedStyle.top);
310
+ const hasLeft = Number.isFinite(parsedLeft);
311
+ const hasTop = Number.isFinite(parsedTop);
312
+ targetLeft = hasLeft ? parsedLeft : rect.left;
313
+ targetTop = hasTop ? parsedTop : rect.top;
314
+ this.positionOffsetX = rect.left - targetLeft;
315
+ this.positionOffsetY = rect.top - targetTop;
316
+ } else {
317
+ // fallback: treat as viewport-relative
318
+ targetLeft = rect.left;
319
+ targetTop = rect.top;
320
+ this.positionOffsetX = rect.left - targetLeft;
321
+ this.positionOffsetY = rect.top - targetTop;
322
+ }
323
+
324
+ // Build complete style update atomically
325
+ const currentCssText = this.element.style.cssText;
326
+ let newCssText = currentCssText
327
+ .split(';')
328
+ .filter(rule => {
329
+ const trimmed = rule.trim();
330
+ return trimmed &&
331
+ !trimmed.startsWith('right:') &&
332
+ !trimmed.startsWith('bottom:') &&
333
+ !trimmed.startsWith('transform:') &&
334
+ !trimmed.startsWith('left:') &&
335
+ !trimmed.startsWith('top:') &&
336
+ !trimmed.startsWith('inset:'); // CRITICAL: Clear inset shorthand!
337
+ })
338
+ .join('; ');
339
+
340
+ if (newCssText) newCssText += '; ';
341
+ newCssText += `left: ${targetLeft}px; top: ${targetTop}px; right: auto; bottom: auto; transform: none`;
342
+
343
+ // Apply all at once
344
+ this.element.style.cssText = newCssText;
345
+ }
346
+
347
+ // Now calculate offsets based on CURRENT position (after conversion)
348
+ // Re-get rect after potential position change
349
+ const finalRect = this.element.getBoundingClientRect();
350
+ this.dragOffsetX = clientX - finalRect.left;
351
+ this.dragOffsetY = clientY - finalRect.top;
352
+
353
+ this.isDragging = true;
354
+ this.element.classList.add(`${this.options.classPrefix}-dragging`);
355
+ document.body.style.cursor = 'grabbing';
356
+ document.body.style.userSelect = 'none';
357
+ }
358
+
359
+ drag(clientX, clientY) {
360
+ if (!this.isDragging) return;
361
+
362
+ // Calculate new position: current mouse position minus the offset where user clicked
363
+ let newX = clientX - this.dragOffsetX - this.positionOffsetX;
364
+ let newY = clientY - this.dragOffsetY - this.positionOffsetY;
365
+
366
+ // Constrain to viewport if needed
367
+ if (this.options.constrainToViewport) {
368
+ const rect = this.element.getBoundingClientRect();
369
+ const viewportWidth = document.documentElement.clientWidth;
370
+ const viewportHeight = document.documentElement.clientHeight;
371
+
372
+ // Keep at least 100px visible
373
+ const minVisible = 100;
374
+ const minX = -(rect.width - minVisible);
375
+ const minY = -(rect.height - minVisible);
376
+ const maxX = viewportWidth - minVisible;
377
+ const maxY = viewportHeight - minVisible;
378
+
379
+ newX = Math.max(minX, Math.min(newX, maxX));
380
+ newY = Math.max(minY, Math.min(newY, maxY));
381
+ }
382
+
383
+ this.element.style.left = `${newX}px`;
384
+ this.element.style.top = `${newY}px`;
385
+
386
+ // Call custom handler if provided
387
+ if (this.options.onDrag) {
388
+ this.options.onDrag({ x: newX, y: newY });
389
+ }
390
+ }
391
+
392
+ stopDragging() {
393
+ this.isDragging = false;
394
+ this.element.classList.remove(`${this.options.classPrefix}-dragging`);
395
+ document.body.style.cursor = '';
396
+ document.body.style.userSelect = '';
397
+
398
+ // Mark as manually positioned
399
+ this.manuallyPositioned = true;
400
+
401
+ // Call custom handler if provided
402
+ if (this.options.onDragEnd) {
403
+ this.options.onDragEnd();
404
+ }
405
+ }
406
+
407
+ startResizing(clientX, clientY) {
408
+ this.isResizing = true;
409
+ this.resizeStartX = clientX;
410
+ this.resizeStartY = clientY;
411
+
412
+ const rect = this.element.getBoundingClientRect();
413
+ this.resizeStartWidth = rect.width;
414
+ this.resizeStartHeight = rect.height;
415
+ this.resizeStartLeft = rect.left;
416
+ this.resizeStartTop = rect.top;
417
+
418
+ this.element.classList.add(`${this.options.classPrefix}-resizing`);
419
+ document.body.style.userSelect = 'none';
420
+
421
+ // Call custom handler if provided
422
+ if (this.options.onResizeStart) {
423
+ this.options.onResizeStart();
424
+ }
425
+ }
426
+
427
+ resize(clientX, clientY) {
428
+ if (!this.isResizing) return;
429
+
430
+ const deltaX = clientX - this.resizeStartX;
431
+ const deltaY = clientY - this.resizeStartY;
432
+
433
+ let newWidth = this.resizeStartWidth;
434
+ let newHeight = this.resizeStartHeight;
435
+ let newLeft = this.resizeStartLeft;
436
+ let newTop = this.resizeStartTop;
437
+
438
+ // Handle horizontal resizing
439
+ if (this.resizeDirection.includes('e')) {
440
+ newWidth = Math.max(this.options.minWidth, this.resizeStartWidth + deltaX);
441
+ }
442
+ if (this.resizeDirection.includes('w')) {
443
+ const proposedWidth = Math.max(this.options.minWidth, this.resizeStartWidth - deltaX);
444
+ newLeft = this.resizeStartLeft + (this.resizeStartWidth - proposedWidth);
445
+ newWidth = proposedWidth;
446
+ }
447
+
448
+ const maxWidthOption = typeof this.options.maxWidth === 'function'
449
+ ? this.options.maxWidth()
450
+ : this.options.maxWidth;
451
+ if (Number.isFinite(maxWidthOption)) {
452
+ const clampedWidth = Math.min(newWidth, maxWidthOption);
453
+ if (clampedWidth !== newWidth && this.resizeDirection.includes('w')) {
454
+ newLeft += newWidth - clampedWidth;
455
+ }
456
+ newWidth = clampedWidth;
457
+ }
458
+
459
+ // Handle vertical resizing (if not maintaining aspect ratio)
460
+ if (!this.options.maintainAspectRatio) {
461
+ if (this.resizeDirection.includes('s')) {
462
+ newHeight = Math.max(this.options.minHeight, this.resizeStartHeight + deltaY);
463
+ }
464
+ if (this.resizeDirection.includes('n')) {
465
+ const proposedHeight = Math.max(this.options.minHeight, this.resizeStartHeight - deltaY);
466
+ newTop = this.resizeStartTop + (this.resizeStartHeight - proposedHeight);
467
+ newHeight = proposedHeight;
468
+ }
469
+
470
+ const maxHeightOption = typeof this.options.maxHeight === 'function'
471
+ ? this.options.maxHeight()
472
+ : this.options.maxHeight;
473
+ if (Number.isFinite(maxHeightOption)) {
474
+ const clampedHeight = Math.min(newHeight, maxHeightOption);
475
+ if (clampedHeight !== newHeight && this.resizeDirection.includes('n')) {
476
+ newTop += newHeight - clampedHeight;
477
+ }
478
+ newHeight = clampedHeight;
479
+ }
480
+ }
481
+
482
+ // Apply new dimensions
483
+ this.element.style.width = `${newWidth}px`;
484
+ if (!this.options.maintainAspectRatio) {
485
+ this.element.style.height = `${newHeight}px`;
486
+ } else {
487
+ this.element.style.height = 'auto';
488
+ }
489
+
490
+ // Apply new position if resizing from west or north
491
+ if (this.resizeDirection.includes('w')) {
492
+ this.element.style.left = `${newLeft}px`;
493
+ }
494
+ if (this.resizeDirection.includes('n') && !this.options.maintainAspectRatio) {
495
+ this.element.style.top = `${newTop}px`;
496
+ }
497
+
498
+ // Call custom handler if provided
499
+ if (this.options.onResize) {
500
+ this.options.onResize({ width: newWidth, height: newHeight, left: newLeft, top: newTop });
501
+ }
502
+ }
503
+
504
+ stopResizing() {
505
+ this.isResizing = false;
506
+ this.resizeDirection = null;
507
+ this.element.classList.remove(`${this.options.classPrefix}-resizing`);
508
+ document.body.style.userSelect = '';
509
+
510
+ // Mark as manually positioned
511
+ this.manuallyPositioned = true;
512
+
513
+ // Call custom handler if provided
514
+ if (this.options.onResizeEnd) {
515
+ this.options.onResizeEnd();
516
+ }
517
+ }
518
+
519
+ toggleKeyboardDragMode() {
520
+ if (this.keyboardDragMode) {
521
+ this.disableKeyboardDragMode();
522
+ } else {
523
+ this.enableKeyboardDragMode();
524
+ }
525
+ }
526
+
527
+ enableKeyboardDragMode() {
528
+ this.keyboardDragMode = true;
529
+ this.keyboardResizeMode = false;
530
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-drag`);
531
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
532
+ this.focusElement();
533
+ }
534
+
535
+ disableKeyboardDragMode() {
536
+ this.keyboardDragMode = false;
537
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
538
+ }
539
+
540
+ toggleKeyboardResizeMode() {
541
+ if (this.keyboardResizeMode) {
542
+ this.disableKeyboardResizeMode();
543
+ } else {
544
+ this.enableKeyboardResizeMode();
545
+ }
546
+ }
547
+
548
+ enableKeyboardResizeMode() {
549
+ this.keyboardResizeMode = true;
550
+ this.keyboardDragMode = false;
551
+ this.element.classList.add(`${this.options.classPrefix}-keyboard-resize`);
552
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-drag`);
553
+ this.focusElement();
554
+ }
555
+
556
+ disableKeyboardResizeMode() {
557
+ this.keyboardResizeMode = false;
558
+ this.element.classList.remove(`${this.options.classPrefix}-keyboard-resize`);
559
+ }
560
+
561
+ enablePointerResizeMode({ focus = true } = {}) {
562
+ if (!this.hasManagedResizeHandles()) {
563
+ this.enableKeyboardResizeMode();
564
+ return;
565
+ }
566
+
567
+ if (this.pointerResizeMode) {
568
+ return;
569
+ }
570
+
571
+ this.pointerResizeMode = true;
572
+ this.setManagedHandlesVisible(true);
573
+ this.element.classList.add(`${this.options.classPrefix}-resizable`);
574
+ this.enableKeyboardResizeMode();
575
+
576
+ if (focus) {
577
+ this.focusElement();
578
+ }
579
+
580
+ if (typeof this.options.onPointerResizeToggle === 'function') {
581
+ this.options.onPointerResizeToggle(true);
582
+ }
583
+ }
584
+
585
+ disablePointerResizeMode({ focus = false } = {}) {
586
+ if (!this.pointerResizeMode) {
587
+ return;
588
+ }
589
+
590
+ this.pointerResizeMode = false;
591
+ this.setManagedHandlesVisible(false);
592
+ this.element.classList.remove(`${this.options.classPrefix}-resizable`);
593
+ this.disableKeyboardResizeMode();
594
+
595
+ if (focus) {
596
+ this.focusElement();
597
+ }
598
+
599
+ if (typeof this.options.onPointerResizeToggle === 'function') {
600
+ this.options.onPointerResizeToggle(false);
601
+ }
602
+ }
603
+
604
+ togglePointerResizeMode() {
605
+ if (this.pointerResizeMode) {
606
+ this.disablePointerResizeMode();
607
+ } else {
608
+ this.enablePointerResizeMode();
609
+ }
610
+ return this.pointerResizeMode;
611
+ }
612
+
613
+ focusElement() {
614
+ if (typeof this.element.focus === 'function') {
615
+ try {
616
+ this.element.focus({ preventScroll: true });
617
+ } catch (e) {
618
+ // Some browsers do not support the preventScroll option; fallback without it
619
+ this.element.focus();
620
+ }
621
+ }
622
+ }
623
+
624
+ keyboardDrag(key, shiftKey) {
625
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
626
+
627
+ // Get current position
628
+ let currentLeft = parseFloat(this.element.style.left) || 0;
629
+ let currentTop = parseFloat(this.element.style.top) || 0;
630
+
631
+ // If element is still centered with transform, convert to absolute position first
632
+ const computedStyle = window.getComputedStyle(this.element);
633
+ if (computedStyle.transform !== 'none') {
634
+ const rect = this.element.getBoundingClientRect();
635
+ currentLeft = rect.left;
636
+ currentTop = rect.top;
637
+ this.element.style.transform = 'none';
638
+ this.element.style.left = `${currentLeft}px`;
639
+ this.element.style.top = `${currentTop}px`;
640
+ }
641
+
642
+ // Calculate new position
643
+ let newX = currentLeft;
644
+ let newY = currentTop;
645
+
646
+ switch(key) {
647
+ case 'ArrowLeft':
648
+ newX -= step;
649
+ break;
650
+ case 'ArrowRight':
651
+ newX += step;
652
+ break;
653
+ case 'ArrowUp':
654
+ newY -= step;
655
+ break;
656
+ case 'ArrowDown':
657
+ newY += step;
658
+ break;
659
+ }
660
+
661
+ // Apply position
662
+ this.element.style.left = `${newX}px`;
663
+ this.element.style.top = `${newY}px`;
664
+
665
+ // Call custom handler if provided
666
+ if (this.options.onDrag) {
667
+ this.options.onDrag({ x: newX, y: newY });
668
+ }
669
+ }
670
+
671
+ keyboardResize(key, shiftKey) {
672
+ const step = shiftKey ? this.options.keyboardStepLarge : this.options.keyboardStep;
673
+ const rect = this.element.getBoundingClientRect();
674
+
675
+ let width = rect.width;
676
+ let height = rect.height;
677
+
678
+ // Adjust width/height based on arrow key
679
+ switch(key) {
680
+ case 'ArrowLeft':
681
+ width -= step;
682
+ break;
683
+ case 'ArrowRight':
684
+ width += step;
685
+ break;
686
+ case 'ArrowUp':
687
+ if (this.options.maintainAspectRatio) {
688
+ width += step;
689
+ } else {
690
+ height -= step;
691
+ }
692
+ break;
693
+ case 'ArrowDown':
694
+ if (this.options.maintainAspectRatio) {
695
+ width -= step;
696
+ } else {
697
+ height += step;
698
+ }
699
+ break;
700
+ }
701
+
702
+ // Constrain to minimum dimensions
703
+ width = Math.max(this.options.minWidth, width);
704
+ height = Math.max(this.options.minHeight, height);
705
+
706
+ // Apply new dimensions
707
+ this.element.style.width = `${width}px`;
708
+ if (!this.options.maintainAspectRatio) {
709
+ this.element.style.height = `${height}px`;
710
+ } else {
711
+ this.element.style.height = 'auto';
712
+ }
713
+
714
+ // Call custom handler if provided
715
+ if (this.options.onResize) {
716
+ this.options.onResize({ width, height });
717
+ }
718
+ }
719
+
720
+ resetPosition() {
721
+ this.element.style.left = '50%';
722
+ this.element.style.top = '50%';
723
+ this.element.style.transform = 'translate(-50%, -50%)';
724
+ this.element.style.right = '';
725
+ this.element.style.bottom = '';
726
+
727
+ // Clear manual positioning flag
728
+ this.manuallyPositioned = false;
729
+
730
+ // Call custom handler if provided
731
+ if (this.options.onDrag) {
732
+ this.options.onDrag({ centered: true });
733
+ }
734
+ }
735
+
736
+ destroy() {
737
+ const dragHandle = this.options.dragHandle || this.element;
738
+
739
+ this.disablePointerResizeMode();
740
+
741
+ // Remove drag events
742
+ dragHandle.removeEventListener('mousedown', this.handlers.mousedown);
743
+ dragHandle.removeEventListener('touchstart', this.handlers.touchstart);
744
+
745
+ // Remove document-level events
746
+ document.removeEventListener('mousemove', this.handlers.mousemove);
747
+ document.removeEventListener('mouseup', this.handlers.mouseup);
748
+ document.removeEventListener('touchmove', this.handlers.touchmove);
749
+ document.removeEventListener('touchend', this.handlers.touchend);
750
+
751
+ // Remove keyboard events
752
+ this.element.removeEventListener('keydown', this.handlers.keydown);
753
+
754
+ // Remove resize handle events
755
+ if (this.options.resizeHandles && this.options.resizeHandles.length > 0) {
756
+ this.options.resizeHandles.forEach(handle => {
757
+ handle.removeEventListener('mousedown', this.handlers.resizeHandleMousedown);
758
+ handle.removeEventListener('touchstart', this.handlers.resizeHandleMousedown);
759
+ });
760
+ }
761
+
762
+ // Clean up classes
763
+ this.element.classList.remove(
764
+ `${this.options.classPrefix}-dragging`,
765
+ `${this.options.classPrefix}-resizing`,
766
+ `${this.options.classPrefix}-keyboard-drag`,
767
+ `${this.options.classPrefix}-keyboard-resize`
768
+ );
769
+ }
770
+ }
771
+