@ohkit/draggable-box 0.0.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.
@@ -0,0 +1,67 @@
1
+ /// <reference types="react" />
2
+ import type { ValidPlacement } from './constants';
3
+ export interface DraggableBoxProps {
4
+ className?: string;
5
+ children?: React.ReactNode;
6
+ /**
7
+ * z-index 层级
8
+ * @default 9999
9
+ */
10
+ zIndex?: number;
11
+ /**
12
+ * 初始位置 横向偏移量
13
+ * @default 20
14
+ */
15
+ offsetX?: number;
16
+ /**
17
+ * 初始位置 纵向偏移量
18
+ * @default 20
19
+ */
20
+ offsetY?: number;
21
+ /**
22
+ * 是否禁用拖拽
23
+ * @default false
24
+ */
25
+ disabled?: boolean;
26
+ /**
27
+ * 拖拽位置,可选值:'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
28
+ * @default 'bottom-right'
29
+ */
30
+ placement?: typeof ValidPlacement[number];
31
+ /**
32
+ * 锁定拖拽方向
33
+ * 'none' - 自由拖拽 (默认)
34
+ * 'x' - 只允许水平方向拖拽
35
+ * 'y' - 只允许垂直方向拖拽
36
+ * @default 'none'
37
+ */
38
+ lockAxis?: 'none' | 'x' | 'y';
39
+ /**
40
+ * X轴相对边界 [min, max] - 基于placement的相对距离范围
41
+ * 比如:placement='top-left'时,boundsX=[左边最小距离, 左边最大距离]
42
+ */
43
+ boundsX?: [number?, number?];
44
+ /**
45
+ * Y轴相对边界 [min, max] - 基于placement的相对距离范围
46
+ * 比如:placement='top-left'时,boundsY=[顶边最小距离, 顶边最大距离]
47
+ */
48
+ boundsY?: [number?, number?];
49
+ /**
50
+ * 是否在拖拽过程中展示拖拽区域可视化
51
+ * @default false
52
+ */
53
+ showDragArea?: boolean;
54
+ /**
55
+ * 定位模式
56
+ * 'fixed' - 使用 fixed 定位(默认),动态查找影响 fixed 定位的父元素
57
+ * 'absolute' - 使用 absolute 定位,基于最近的定位父元素
58
+ * @default 'fixed'
59
+ */
60
+ positionMode?: 'fixed' | 'absolute';
61
+ }
62
+ export interface DraggableBoxState {
63
+ top?: number;
64
+ bottom?: number;
65
+ left?: number;
66
+ right?: number;
67
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 查找影响 fixed 定位的父元素
3
+ * 当父元素有 transform/filter/perspective 等属性时,fixed 定位会相对于该父元素
4
+ */
5
+ export declare function findFixedPositionParent(dom?: HTMLElement | null): HTMLElement;
6
+ /**
7
+ * 查找 absolute 定位的父元素
8
+ * 查找最近的 position 不为 static 的元素
9
+ */
10
+ export declare function findAbsolutePositionParent(dom?: HTMLElement | null): HTMLElement;
11
+ /**
12
+ * 获取容器的缩放比例
13
+ * 通过比较元素的实际尺寸和 getBoundingClientRect 返回的尺寸来计算缩放比例
14
+ */
15
+ export declare function getScaleRatio(dom?: HTMLElement | null): {
16
+ scaleX: number;
17
+ scaleY: number;
18
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@ohkit/draggable-box",
3
+ "version": "0.0.1",
4
+ "description": "可拖拽容器",
5
+ "keywords": [
6
+ "draggable-box"
7
+ ],
8
+ "homepage": "https://wuqiuyang.github.io/ohkit/storybook-static",
9
+ "license": "ISC",
10
+ "source": "src/index.@(js|jsx|mjs|ts|tsx)",
11
+ "main": "dist/index.js",
12
+ "umd:main": "dist/index.umd.js",
13
+ "module": "dist/index.es.js",
14
+ "types": "dist/types/index.d.ts",
15
+ "publishConfig": {
16
+ "registry": "https://registry.npmjs.org/",
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/WuQiuYang/ohkit"
22
+ },
23
+ "scripts": {
24
+ "build": "npm run clean && microbundle --jsx React.createElement",
25
+ "dev": "npm run watch",
26
+ "watch": "microbundle watch --jsx React.createElement --no-compress --format es --output dist/index.es.js",
27
+ "clean": "rm -rf dist"
28
+ },
29
+ "dependencies": {
30
+ "@ohkit/dom-helper": "0.0.6",
31
+ "@ohkit/prefix-classname": "^0.0.3"
32
+ },
33
+ "peerDependencies": {
34
+ "react": ">=17",
35
+ "react-dom": ">=17"
36
+ },
37
+ "gitHead": "f6aca250c73c8a72c20fdcbeb86b8d9032367927"
38
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export const ValidPlacement = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const;
package/src/index.tsx ADDED
@@ -0,0 +1,536 @@
1
+ import React from 'react';
2
+ import {
3
+ prefixClassname as p,
4
+ classNames as cx,
5
+ } from "@ohkit/prefix-classname";
6
+ import {addEventListener, addClass} from '@ohkit/dom-helper';
7
+ import {findFixedPositionParent, findAbsolutePositionParent, getScaleRatio} from './utils';
8
+ import {ValidPlacement} from './constants';
9
+ import {DraggableBoxProps, DraggableBoxState} from './type';
10
+
11
+ import './style.scss';
12
+
13
+ export const c = p("ohkit-draggable-box__");
14
+ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBoxState> {
15
+ static defaultProps: Partial<DraggableBoxProps> = {
16
+ zIndex: 9999,
17
+ offsetX: 20,
18
+ offsetY: 20,
19
+ placement: 'bottom-right',
20
+ disabled: false,
21
+ lockAxis: 'none',
22
+ showDragArea: false,
23
+ positionMode: 'fixed',
24
+ };
25
+
26
+ constructor(props: DraggableBoxProps) {
27
+ super(props);
28
+
29
+ const { placement = 'bottom-right', offsetY = 20, offsetX = 20 } = props;
30
+ const [placementY, placementX] = placement.split('-') as ['top' | 'bottom', 'left' | 'right'];
31
+
32
+ // 简化状态初始化
33
+ this.state = {
34
+ top: placementY === 'top' ? offsetY : undefined,
35
+ bottom: placementY === 'bottom' ? offsetY : undefined,
36
+ left: placementX === 'left' ? offsetX : undefined,
37
+ right: placementX === 'right' ? offsetX : undefined,
38
+ };
39
+ }
40
+
41
+ getOtherYKey(yKey: 'top' | 'bottom') {
42
+ return yKey === 'top' ? 'bottom' : 'top';
43
+ }
44
+
45
+ getOtherXKey(xKey: 'left' | 'right') {
46
+ return xKey === 'left' ? 'right' : 'left';
47
+ }
48
+
49
+ // TODO:
50
+ updatePosition(yKey: 'top' | 'bottom', xKey: 'left' | 'right') {
51
+ const oYKey = this.getOtherYKey(yKey);
52
+ const oXKey = this.getOtherXKey(xKey);
53
+ this.setState({
54
+ [oYKey]: this.dragPositionRang.height - (this.state[yKey] || 0),
55
+ [oXKey]: this.dragPositionRang.width - (this.state[xKey] || 0),
56
+ });
57
+ }
58
+ /**
59
+ * 获取定位容器
60
+ * 根据 positionMode 返回对应的定位父元素
61
+ */
62
+ private getContainer(): HTMLElement {
63
+ const { positionMode = 'fixed' } = this.props;
64
+ return positionMode === 'fixed'
65
+ ? findFixedPositionParent(this.draggerRef)
66
+ : findAbsolutePositionParent(this.draggerRef);
67
+ }
68
+
69
+ /**
70
+ * 获取容器的尺寸和位置信息
71
+ */
72
+ private getContainerRect() {
73
+ const { positionMode = 'fixed' } = this.props;
74
+ const isFixed = positionMode === 'fixed';
75
+ const container = this.getContainer();
76
+ const containerRect = container.getBoundingClientRect();
77
+ const rootScrollingElement = window.document.scrollingElement || window.document.body;
78
+ const isRoot = container === window.document.body || container === window.document.documentElement;
79
+ return {
80
+ width: containerRect.width,
81
+ height: containerRect.height,
82
+ left: isFixed && isRoot ? Math.max(containerRect.left, 0) : containerRect.left + rootScrollingElement.scrollLeft,
83
+ top: isFixed && isRoot ? Math.max(containerRect.top, 0) : containerRect.top + rootScrollingElement.scrollTop,
84
+ scrollLeft: isFixed && isRoot ? 0 : container.scrollLeft, // container.scrollLeft,
85
+ scrollTop: isFixed && isRoot ? 0 : container.scrollTop, // container.scrollTop
86
+ scrollerScrollLeft: isFixed && isRoot ? 0 : rootScrollingElement.scrollLeft,
87
+ scrollerScrollTop: isFixed && isRoot ? 0 : rootScrollingElement.scrollTop
88
+ };
89
+ }
90
+
91
+ get windowSize() {
92
+ const container = this.getContainer();
93
+ const { clientWidth, clientHeight } = container;
94
+ return {
95
+ height: clientHeight,
96
+ width: clientWidth
97
+ };
98
+ }
99
+
100
+ get dragBoxSize() {
101
+ let width = 0;
102
+ let height = 0;
103
+ if (this.draggerRef) {
104
+ width = this.draggerRef.offsetWidth;
105
+ height = this.draggerRef.offsetHeight;
106
+ }
107
+ return {
108
+ height,
109
+ width
110
+ };
111
+ }
112
+
113
+ get dragPositionBoundaries() {
114
+ const { boundsX, boundsY, placement = 'bottom-right' } = this.props;
115
+ const dragSize = this.dragBoxSize;
116
+ const windowSize = this.windowSize;
117
+ const [placementY, placementX] = placement.split('-') as ['top' | 'bottom', 'left' | 'right'];
118
+
119
+ // 初始化边界
120
+ let minX = 0;
121
+ let maxX = windowSize.width - dragSize.width;
122
+ let minY = 0;
123
+ let maxY = windowSize.height - dragSize.height;
124
+
125
+ // 处理X轴边界
126
+ if (boundsX) {
127
+ const [minBound, maxBound] = boundsX;
128
+
129
+ if (placementX === 'left') {
130
+ // 左边位置:boundsX=[左边最小距离, 左边最大距离]
131
+ if (minBound !== undefined) minX = Math.max(minX, minBound);
132
+ if (maxBound !== undefined) maxX = Math.min(maxX, maxBound);
133
+ } else {
134
+ // 右边位置:boundsX=[右边最小距离, 右边最大距离]
135
+ // 直接使用边界值作为right的限制
136
+ if (minBound !== undefined && maxBound !== undefined) {
137
+ minX = Math.max(minX, windowSize.width - maxBound - dragSize.width);
138
+ maxX = Math.min(maxX, windowSize.width - minBound - dragSize.width);
139
+ } else if (minBound !== undefined) {
140
+ // 只有minBound:设置最大边界,最小边界保持默认
141
+ maxX = Math.min(maxX, windowSize.width - minBound - dragSize.width);
142
+ } else if (maxBound !== undefined) {
143
+ // 只有maxBound:设置最小边界,最大边界保持默认
144
+ minX = Math.max(minX, windowSize.width - maxBound - dragSize.width);
145
+ }
146
+
147
+ // 确保最小边界不大于最大边界
148
+ if (minX > maxX) {
149
+ [minX, maxX] = [maxX, minX];
150
+ }
151
+ }
152
+ }
153
+
154
+ // 处理Y轴边界
155
+ if (boundsY) {
156
+ const [minBound, maxBound] = boundsY;
157
+
158
+ if (placementY === 'top') {
159
+ // 顶部位置:boundsY=[顶边最小距离, 顶边最大距离]
160
+ if (minBound !== undefined) minY = Math.max(minY, minBound);
161
+ if (maxBound !== undefined) maxY = Math.min(maxY, maxBound);
162
+ } else {
163
+ // 底部位置:boundsY=[底边最小距离, 底边最大距离]
164
+ // 直接使用边界值作为bottom的限制
165
+ if (minBound !== undefined && maxBound !== undefined) {
166
+ minY = Math.max(minY, windowSize.height - maxBound - dragSize.height);
167
+ maxY = Math.min(maxY, windowSize.height - minBound - dragSize.height);
168
+ } else if (minBound !== undefined) {
169
+ // 只有minBound:设置最大边界,最小边界保持默认
170
+ maxY = Math.min(maxY, windowSize.height - minBound - dragSize.height);
171
+ } else if (maxBound !== undefined) {
172
+ // 只有maxBound:设置最小边界,最大边界保持默认
173
+ minY = Math.max(minY, windowSize.height - maxBound - dragSize.height);
174
+ }
175
+
176
+ // 确保最小边界不大于最大边界
177
+ if (minY > maxY) {
178
+ [minY, maxY] = [maxY, minY];
179
+ }
180
+ }
181
+ }
182
+
183
+ return { minX, maxX, minY, maxY };
184
+ }
185
+
186
+ // 保持向后兼容
187
+ get dragPositionRang() {
188
+ const { maxX, maxY } = this.dragPositionBoundaries;
189
+ return { width: maxX, height: maxY };
190
+ }
191
+
192
+ get curPositionKey() {
193
+ let {placement} = this.props;
194
+ if (!placement || !ValidPlacement.includes(placement)) {
195
+ placement = 'bottom-right';
196
+ }
197
+ return placement.split('-') as ['top' | 'bottom', 'left' | 'right']; // [y, x]
198
+ }
199
+
200
+ get position() {
201
+ const [y, x] = this.curPositionKey;
202
+ const positionStyles: Record<string, string> = {};
203
+
204
+ // 确保位置值存在且是有效的数字
205
+ if (this.state[y] !== undefined) {
206
+ positionStyles[y] = `${this.state[y]}px`;
207
+ }
208
+ if (this.state[x] !== undefined) {
209
+ positionStyles[x] = `${this.state[x]}px`;
210
+ }
211
+
212
+ return positionStyles;
213
+ }
214
+
215
+ draggerRef: HTMLDivElement | null = null;
216
+
217
+ isDragging = false;
218
+
219
+ axisX?: number;
220
+ axisY?: number;
221
+ dX = 0;
222
+ dY = 0;
223
+ startTop = 0;
224
+ startLeft = 0;
225
+
226
+ // 缓存缩放比例,避免在 dragging 中频繁计算
227
+ cachedScaleX = 1;
228
+ cachedScaleY = 1;
229
+
230
+ __moveDisposer?: () => void;
231
+ __clickDisposer?: () => void;
232
+ __bodyClassDisposer?: () => void;
233
+ __upDisposer?: () => void;
234
+ __resizeDisposer?: () => void;
235
+
236
+ dragAreaRef: HTMLDivElement | null = null;
237
+
238
+ reportStartPosition() {
239
+ if (this.draggerRef) {
240
+ const { top, left } = this.draggerRef.getBoundingClientRect();
241
+ const containerRect = this.getContainerRect();
242
+ // console.log(containerRect, 'containerRect');
243
+
244
+ // 获取缩放比例
245
+ const { scaleX, scaleY } = getScaleRatio(this.draggerRef);
246
+ this.cachedScaleX = scaleX;
247
+ this.cachedScaleY = scaleY;
248
+
249
+ // 计算相对于容器的位置,并除以缩放比例得到未缩放的坐标
250
+ this.startTop = (top - containerRect.top + containerRect.scrollerScrollTop) / scaleY + containerRect.scrollTop;
251
+ this.startLeft = (left - containerRect.left + containerRect.scrollerScrollLeft) / scaleX + containerRect.scrollLeft;
252
+ }
253
+ }
254
+
255
+ enableDrag = () => {
256
+ this.reportStartPosition();
257
+ this.__moveDisposer?.();
258
+ this.__moveDisposer = addEventListener(document, 'mousemove', (evt) => {
259
+ // INFO: 移动过程中禁止click事件
260
+ if (!this.__clickDisposer) {
261
+ const moveDistanse = Math.sqrt(Math.pow(this.dX, 2) + Math.pow(this.dY, 2));
262
+ // INFO: 移动超过5px?? 确保用户有真实的移动意愿,而不是手抖~~
263
+ if (moveDistanse > 5) {
264
+ this.__clickDisposer = addEventListener(
265
+ document,
266
+ 'click',
267
+ (evt) => {
268
+ evt.stopPropagation();
269
+ },
270
+ true
271
+ );
272
+ this.__bodyClassDisposer = addClass([document.body, this.draggerRef], c('moving')) || undefined;
273
+
274
+ // 显示拖拽区域
275
+ if (this.props.showDragArea && this.dragAreaRef) {
276
+ this.showDragArea();
277
+ }
278
+ }
279
+ }
280
+ this.dragging(evt);
281
+ }, true);
282
+
283
+ this.__upDisposer?.();
284
+ this.__upDisposer = addEventListener(
285
+ document,
286
+ 'mouseup',
287
+ (evt) => {
288
+ this.endDrag();
289
+ evt.stopPropagation();
290
+ evt.preventDefault();
291
+ },
292
+ true
293
+ );
294
+ };
295
+
296
+ startDrag = (evt: React.MouseEvent<HTMLDivElement>) => {
297
+ // 判断鼠标非右击才继续执行
298
+ if (evt.nativeEvent.button === 2) {
299
+ return;
300
+ }
301
+ this.axisX = evt.nativeEvent.pageX;
302
+ this.axisY = evt.nativeEvent.pageY;
303
+ if (!this.props.disabled) {
304
+ this.enableDrag();
305
+ }
306
+ };
307
+
308
+ dragging = (evt: MouseEvent) => {
309
+ this.isDragging = true;
310
+ const { lockAxis } = this.props;
311
+ const { minX, maxX, minY, maxY } = this.dragPositionBoundaries;
312
+
313
+ // 使用缓存的缩放比例,避免频繁计算
314
+ const scaleX = this.cachedScaleX;
315
+ const scaleY = this.cachedScaleY;
316
+
317
+ // 计算原始偏移量(需要除以缩放比例)
318
+ this.dX = (evt.pageX - (this.axisX || 0)) / scaleX;
319
+ this.dY = (evt.pageY - (this.axisY || 0)) / scaleY;
320
+
321
+ // 应用方向锁定并计算变换值
322
+ let translateX = this.dX;
323
+ let translateY = this.dY;
324
+
325
+ if (lockAxis === 'x') {
326
+ translateY = 0; // 锁定Y方向
327
+ } else if (lockAxis === 'y') {
328
+ translateX = 0; // 锁定X方向
329
+ }
330
+
331
+ // 应用边界条件到允许移动的方向
332
+ const potentialLeft = this.startLeft + translateX;
333
+ const potentialTop = this.startTop + translateY;
334
+
335
+ // X轴边界条件(在允许X轴移动时应用)
336
+ if (lockAxis !== 'y') {
337
+ if (potentialLeft < minX) {
338
+ translateX = minX - this.startLeft;
339
+ } else if (potentialLeft > maxX) {
340
+ translateX = maxX - this.startLeft;
341
+ }
342
+ }
343
+
344
+ // Y轴边界条件(在允许Y轴移动时应用)
345
+ if (lockAxis !== 'x') {
346
+ if (potentialTop < minY) {
347
+ translateY = minY - this.startTop;
348
+ } else if (potentialTop > maxY) {
349
+ translateY = maxY - this.startTop;
350
+ }
351
+ }
352
+
353
+ if (this.draggerRef) {
354
+ this.draggerRef.style.transform = `translate(${translateX}px, ${translateY}px)`;
355
+ }
356
+ evt.stopPropagation();
357
+ };
358
+
359
+ endDrag = () => {
360
+ if (this.isDragging) {
361
+ this.calcPosition();
362
+ if (this.draggerRef) {
363
+ this.draggerRef.style.transform = '';
364
+ }
365
+
366
+ // 隐藏拖拽区域
367
+ if (this.props.showDragArea) {
368
+ this.hideDragArea();
369
+ }
370
+ }
371
+
372
+ this.__moveDisposer?.();
373
+ this.__moveDisposer = undefined;
374
+ if (this.__clickDisposer) {
375
+ requestAnimationFrame(() => {
376
+ if (this.__clickDisposer) {
377
+ this.__clickDisposer();
378
+ this.__clickDisposer = undefined;
379
+ }
380
+ });
381
+ }
382
+ this.__upDisposer?.();
383
+ this.__upDisposer = undefined;
384
+ this.__bodyClassDisposer?.();
385
+ this.__bodyClassDisposer = undefined;
386
+
387
+ this.isDragging = false;
388
+ };
389
+
390
+ showDragArea = () => {
391
+ if (!this.props.showDragArea || !this.dragAreaRef) return;
392
+
393
+ const { lockAxis } = this.props;
394
+ const { minX, maxX, minY, maxY } = this.dragPositionBoundaries;
395
+ const dragSize = this.dragBoxSize;
396
+
397
+ // 重置样式
398
+ this.dragAreaRef.style.border = '1px dashed var(--ohkit-color-primary, #1890ff)';
399
+ this.dragAreaRef.style.backgroundColor = 'rgba(173, 216, 230, 0.2)'; // 淡透蓝色
400
+
401
+ if (lockAxis === 'x') {
402
+ // 锁定Y方向,显示为水平虚线区域
403
+ this.dragAreaRef.style.width = `${maxX - minX + dragSize.width}px`;
404
+ this.dragAreaRef.style.height = '2px'; // 更细的虚线高度
405
+ this.dragAreaRef.style.left = `${minX}px`;
406
+ this.dragAreaRef.style.top = `${this.startTop + dragSize.height / 2}px`;
407
+ this.dragAreaRef.style.border = 'none';
408
+ this.dragAreaRef.style.backgroundColor = 'transparent'; // 透明背景
409
+ this.dragAreaRef.style.backgroundImage = 'linear-gradient(to right, var(--ohkit-color-primary, #1890ff) 50%, transparent 50%)';
410
+ this.dragAreaRef.style.backgroundSize = '4px 2px'; // 虚线模式
411
+ } else if (lockAxis === 'y') {
412
+ // 锁定X方向,显示为垂直虚线区域
413
+ this.dragAreaRef.style.width = '2px'; // 更细的虚线宽度
414
+ this.dragAreaRef.style.height = `${maxY - minY + dragSize.height}px`;
415
+ this.dragAreaRef.style.left = `${this.startLeft + dragSize.width / 2}px`;
416
+ this.dragAreaRef.style.top = `${minY}px`;
417
+ this.dragAreaRef.style.border = 'none';
418
+ this.dragAreaRef.style.backgroundColor = 'transparent'; // 透明背景
419
+ this.dragAreaRef.style.backgroundImage = 'linear-gradient(to bottom, var(--ohkit-color-primary, #1890ff) 50%, transparent 50%)';
420
+ this.dragAreaRef.style.backgroundSize = '2px 4px'; // 虚线模式
421
+ } else {
422
+ // 自由拖拽,显示完整区域
423
+ this.dragAreaRef.style.width = `${maxX - minX + dragSize.width}px`;
424
+ this.dragAreaRef.style.height = `${maxY - minY + dragSize.height}px`;
425
+ this.dragAreaRef.style.left = `${minX}px`;
426
+ this.dragAreaRef.style.top = `${minY}px`;
427
+ }
428
+
429
+ this.dragAreaRef.style.display = 'block';
430
+ };
431
+
432
+ hideDragArea = () => {
433
+ if (this.dragAreaRef) {
434
+ this.dragAreaRef.style.display = 'none';
435
+ }
436
+ };
437
+ calcPosition = () => {
438
+ const { lockAxis } = this.props;
439
+ const { minX, maxX, minY, maxY } = this.dragPositionBoundaries;
440
+ const containerSize = this.windowSize;
441
+
442
+ // 计算新的位置
443
+ let newTop = this.startTop;
444
+ let newLeft = this.startLeft;
445
+
446
+ if (lockAxis !== 'y') {
447
+ newLeft += this.dX;
448
+ }
449
+ if (lockAxis !== 'x') {
450
+ newTop += this.dY;
451
+ }
452
+
453
+ // 应用边界限制
454
+ const realTop = Math.min(Math.max(minY, newTop), maxY);
455
+ const realLeft = Math.min(Math.max(minX, newLeft), maxX);
456
+ const realBottom = containerSize.height - realTop - this.dragBoxSize.height;
457
+ const realRight = containerSize.width - realLeft - this.dragBoxSize.width;
458
+ if (realTop !== this.state.top || realLeft !== this.state.left || this.state.bottom !== realBottom || this.state.right !== realRight) {
459
+ // console.log(minY, maxY, this.startTop, this.dY, newTop, realTop, 'calcPosition y');
460
+ // console.log(minX, maxX, this.startLeft, this.dX, newLeft, realLeft, 'calcPosition x');
461
+ this.setState({
462
+ top: realTop,
463
+ left: realLeft,
464
+ bottom: realBottom,
465
+ right: realRight
466
+ });
467
+ }
468
+ this.startTop = realTop;
469
+ this.startLeft = realLeft;
470
+ this.dX = this.dY = 0;
471
+ };
472
+
473
+
474
+ componentDidMount() {
475
+ // 检查初始位置是否在边界范围内,如果不在则修正
476
+ this.reportStartPosition();
477
+ this.calcPosition();
478
+
479
+ this.__resizeDisposer = addEventListener(window, 'resize', () => {
480
+ this.calcPosition();
481
+ });
482
+ }
483
+
484
+ componentWillUnmount() {
485
+ this.__resizeDisposer?.();
486
+ this.__bodyClassDisposer?.();
487
+ this.__moveDisposer?.();
488
+ this.__clickDisposer?.();
489
+ this.__upDisposer?.();
490
+ }
491
+
492
+ render() {
493
+ const { className, zIndex, children, showDragArea, positionMode = 'fixed' } = this.props;
494
+ const { startDrag, endDrag } = this;
495
+ const stl = {
496
+ zIndex,
497
+ ...this.position,
498
+ position: positionMode
499
+ };
500
+ return (
501
+ <>
502
+ {showDragArea && (
503
+ <div
504
+ className={c('drag-area')}
505
+ ref={(r) => {
506
+ this.dragAreaRef = r;
507
+ }}
508
+ style={{
509
+ display: 'none',
510
+ position: positionMode,
511
+ backgroundColor: 'rgba(173, 216, 230, 0.2)', // 淡透蓝色
512
+ border: '1px dashed var(--ohkit-color-primary, #1890ff)',
513
+ pointerEvents: 'none',
514
+ zIndex: (zIndex || 9999) - 1,
515
+ boxSizing: 'border-box',
516
+ borderRadius: this.props.lockAxis !== 'none' ? '2px' : '0',
517
+ }}
518
+ />
519
+ )}
520
+ <div
521
+ className={cx(c('container'), className)}
522
+ style={stl}
523
+ ref={(r) => {
524
+ this.draggerRef = r;
525
+ }}
526
+ onMouseDown={startDrag}
527
+ onMouseUp={endDrag}
528
+ >
529
+ {children}
530
+ </div>
531
+ </>
532
+ );
533
+ }
534
+ }
535
+
536
+ export default DraggableBox;
package/src/style.scss ADDED
@@ -0,0 +1,18 @@
1
+ $prefix: ohkit;
2
+
3
+ .#{$prefix}-draggable-box__ {
4
+ &container {
5
+ // position 由内联样式动态控制,默认为 fixed
6
+ user-select: none;
7
+ cursor: grab;
8
+ }
9
+
10
+ &moving {
11
+ cursor: move !important;
12
+ }
13
+
14
+ &drag-area {
15
+ transition: opacity 0.2s ease-in-out;
16
+ opacity: 0.7;
17
+ }
18
+ }