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.
- package/dist/vidply.css +47 -14
- package/dist/vidply.esm.js +1063 -663
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +28 -10
- package/dist/vidply.js +1063 -663
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +27 -9
- package/package.json +1 -1
- package/src/controls/ControlBar.js +24 -14
- package/src/controls/TranscriptManager.js +344 -578
- package/src/core/Player.js +150 -276
- package/src/i18n/translations.js +50 -5
- package/src/styles/vidply.css +47 -14
- package/src/utils/DraggableResizable.js +771 -0
|
@@ -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
|
+
|