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.
- package/README.md +946 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
- package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
- package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
- package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
- package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
- package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
- package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
- package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
- package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
- package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
- package/dist/dev/vidply.esm.js +73 -34
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
- package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
- package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
- package/dist/legacy/vidply.js +345 -32
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +66 -47
- package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
- package/dist/vidply.css +5796 -5796
- package/dist/vidply.esm.min.meta.json +148 -135
- package/package.json +7 -7
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4159 -4159
- package/src/controls/KeyboardManager.ts +383 -383
- package/src/controls/SettingsDialog.ts +488 -488
- package/src/controls/TranscriptManager.ts +2426 -2426
- package/src/core/AudioDescriptionManager.ts +863 -774
- package/src/core/DescriptionSpeechManager.ts +232 -0
- package/src/core/FloatingPlayerManager.ts +674 -674
- package/src/core/LazyInit.ts +80 -80
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2915 -2872
- package/src/core/PosterManager.ts +169 -169
- package/src/core/PseudoFullscreen.ts +179 -179
- package/src/core/ResponsiveManager.ts +231 -231
- package/src/core/ResumeManager.ts +236 -236
- package/src/core/SignLanguageManager.ts +1308 -1308
- package/src/core/ThemeManager.ts +157 -157
- package/src/features/PlaylistManager.ts +1838 -1827
- package/src/i18n/i18n.ts +223 -223
- package/src/i18n/languages/de.ts +217 -217
- package/src/i18n/languages/en.ts +218 -218
- package/src/i18n/languages/es.ts +214 -214
- package/src/i18n/languages/fr.ts +214 -214
- package/src/i18n/languages/ja.ts +214 -214
- package/src/icons/Icons.ts +167 -167
- package/src/index.ts +196 -193
- package/src/renderers/DASHRenderer.ts +1077 -1077
- package/src/renderers/HLSRenderer.ts +944 -944
- package/src/renderers/HTML5Renderer.ts +389 -389
- package/src/renderers/SoundCloudRenderer.ts +437 -437
- package/src/renderers/VimeoRenderer.ts +298 -298
- package/src/renderers/YouTubeRenderer.ts +290 -290
- package/src/styles/vidply.css +5796 -5796
- package/src/types/events.ts +124 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +185 -182
- package/src/types/renderer.ts +58 -58
- package/src/utils/DOMUtils.ts +281 -281
- package/src/utils/DownloadInfo.ts +171 -171
- package/src/utils/DraggablePanel.ts +539 -539
- package/src/utils/DraggablePanelMenu.ts +195 -195
- package/src/utils/DraggableResizable.ts +997 -997
- package/src/utils/EventEmitter.ts +59 -59
- package/src/utils/FormUtils.ts +95 -95
- package/src/utils/MenuFactory.ts +483 -483
- package/src/utils/Sanitize.ts +70 -70
- package/src/utils/SettingsMenuFactory.ts +289 -289
- package/src/utils/StorageManager.ts +287 -287
- package/src/utils/TrackLabelUtils.ts +52 -52
- package/src/utils/UrlSafe.ts +69 -69
- package/src/utils/VideoFrameCapture.ts +114 -114
- package/src/utils/WindowComponents.ts +250 -250
- package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
- package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
- package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
- package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
- package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
- package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
- package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
- package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
- package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
- package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
- package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
- package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
- package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
- package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
- package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
- package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
- package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
- package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
- package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
- package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
- package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
- package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
- package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
- /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
- /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
|
+
|