@ohkit/draggable-box 0.0.2 → 0.0.4
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 +182 -1
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +1 -1
- package/dist/index.modern.mjs.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/types/index.d.ts +26 -10
- package/dist/types/type.d.ts +25 -0
- package/dist/types/utils.d.ts +6 -0
- package/package.json +3 -3
- package/src/index.tsx +145 -45
- package/src/type.ts +26 -0
- package/src/utils.ts +10 -0
package/src/index.tsx
CHANGED
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
classNames as cx,
|
|
5
5
|
} from "@ohkit/prefix-classname";
|
|
6
6
|
import {addEventListener, addClass} from '@ohkit/dom-helper';
|
|
7
|
-
import {findFixedPositionParent, findAbsolutePositionParent, getScaleRatio, clamp} from './utils';
|
|
7
|
+
import {findFixedPositionParent, findAbsolutePositionParent, getScaleRatio, clamp, supportsTouchEvents} from './utils';
|
|
8
8
|
import {ValidPlacement} from './constants';
|
|
9
9
|
import {DraggableBoxProps, DraggableBoxState} from './type';
|
|
10
10
|
|
|
11
11
|
import './style.scss';
|
|
12
12
|
|
|
13
|
+
export * from './utils';
|
|
14
|
+
export * from './type';
|
|
13
15
|
export const c = p("ohkit-draggable-box__");
|
|
14
16
|
export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBoxState> {
|
|
15
17
|
static defaultProps: Partial<DraggableBoxProps> = {
|
|
@@ -20,22 +22,26 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
20
22
|
disabled: false,
|
|
21
23
|
lockAxis: 'none',
|
|
22
24
|
showDragArea: false,
|
|
25
|
+
showDragAreaOverMoveDistanse: 5,
|
|
23
26
|
positionMode: 'fixed',
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
constructor(props: DraggableBoxProps) {
|
|
27
30
|
super(props);
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
const { offsetX, offsetY } = this.props;
|
|
32
|
+
this.state = this.formatState({offsetX, offsetY});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private formatState({offsetX = DraggableBox.defaultProps.offsetX, offsetY = DraggableBox.defaultProps.offsetY} = {}) {
|
|
36
|
+
const { placement = 'bottom-right' } = this.props;
|
|
30
37
|
const [placementY, placementX] = placement.split('-') as ['top' | 'bottom', 'left' | 'right'];
|
|
31
|
-
|
|
32
|
-
// 简化状态初始化
|
|
33
|
-
this.state = {
|
|
38
|
+
const newState = {
|
|
34
39
|
top: placementY === 'top' ? offsetY : undefined,
|
|
35
40
|
bottom: placementY === 'bottom' ? offsetY : undefined,
|
|
36
41
|
left: placementX === 'left' ? offsetX : undefined,
|
|
37
42
|
right: placementX === 'right' ? offsetX : undefined,
|
|
38
|
-
}
|
|
43
|
+
}
|
|
44
|
+
return newState;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
private prePositionMode: DraggableBoxProps['positionMode'];
|
|
@@ -64,8 +70,8 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
64
70
|
const container = this.getContainer(false);
|
|
65
71
|
if (!container) {
|
|
66
72
|
return {
|
|
67
|
-
width: window.innerWidth,
|
|
68
|
-
height: window.innerHeight,
|
|
73
|
+
width: document.documentElement.clientWidth, // window.innerWidth, 避免滚动条影响计算
|
|
74
|
+
height: document.documentElement.clientHeight, // window.innerHeight, 避免滚动条影响计算
|
|
69
75
|
left: 0,
|
|
70
76
|
top: 0,
|
|
71
77
|
scrollLeft: 0,
|
|
@@ -216,20 +222,25 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
216
222
|
dY = 0;
|
|
217
223
|
startTop = 0;
|
|
218
224
|
startLeft = 0;
|
|
225
|
+
startBottom = 0;
|
|
226
|
+
startRight = 0;
|
|
227
|
+
translateX = 0;
|
|
228
|
+
translateY = 0;
|
|
219
229
|
|
|
220
230
|
// 缓存缩放比例,避免在 dragging 中频繁计算
|
|
221
231
|
cachedScaleX = 1;
|
|
222
232
|
cachedScaleY = 1;
|
|
223
233
|
|
|
224
|
-
__moveDisposer?: () => void;
|
|
225
|
-
__clickDisposer?: () => void;
|
|
226
|
-
__bodyClassDisposer?: () => void;
|
|
227
|
-
__upDisposer?: () => void;
|
|
228
|
-
__resizeDisposer?: () => void;
|
|
234
|
+
private __moveDisposer?: () => void;
|
|
235
|
+
private __clickDisposer?: () => void;
|
|
236
|
+
private __bodyClassDisposer?: () => void;
|
|
237
|
+
private __upDisposer?: () => void;
|
|
238
|
+
private __resizeDisposer?: () => void;
|
|
239
|
+
private __preventScrollDisposer?: () => void;
|
|
229
240
|
|
|
230
241
|
dragAreaRef: HTMLDivElement | null = null;
|
|
231
242
|
|
|
232
|
-
reportStartPosition() {
|
|
243
|
+
private reportStartPosition() {
|
|
233
244
|
if (this.draggerRef) {
|
|
234
245
|
// 获取缩放比例
|
|
235
246
|
const { scaleX, scaleY } = getScaleRatio(this.getContainer());
|
|
@@ -242,18 +253,24 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
242
253
|
// 计算相对于容器的位置,并除以缩放比例得到未缩放的坐标
|
|
243
254
|
this.startTop = top / scaleY - containerRect.top + containerRect.scrollTop - containerRect.borderTopWidth;
|
|
244
255
|
this.startLeft = left / scaleY - containerRect.left + containerRect.scrollLeft - containerRect.borderLeftWidth;
|
|
256
|
+
this.startBottom = containerRect.height - this.startTop - this.dragBoxSize.height;
|
|
257
|
+
this.startRight = containerRect.width - this.startLeft - this.dragBoxSize.width;
|
|
245
258
|
}
|
|
246
259
|
}
|
|
247
260
|
|
|
248
|
-
enableDrag = () => {
|
|
261
|
+
enableDrag = (isTouch = false) => {
|
|
249
262
|
this.reportStartPosition();
|
|
250
263
|
this.__moveDisposer?.();
|
|
251
|
-
this.__moveDisposer = addEventListener(document, 'mousemove', (evt) => {
|
|
264
|
+
this.__moveDisposer = addEventListener(isTouch && this.draggerRef ? this.draggerRef : document, isTouch ? 'touchmove' : 'mousemove', (evt) => {
|
|
265
|
+
evt.stopPropagation();
|
|
266
|
+
if (isTouch) {
|
|
267
|
+
evt.preventDefault();
|
|
268
|
+
}
|
|
252
269
|
// INFO: 移动过程中禁止click事件
|
|
253
270
|
if (!this.__clickDisposer) {
|
|
254
271
|
const moveDistanse = Math.sqrt(Math.pow(this.dX, 2) + Math.pow(this.dY, 2));
|
|
255
|
-
// INFO: 移动超过
|
|
256
|
-
if (moveDistanse > 5) {
|
|
272
|
+
// INFO: 移动超过px?? 确保用户有真实的移动意愿,而不是手抖~~
|
|
273
|
+
if (moveDistanse > (this.props.showDragAreaOverMoveDistanse || 5)) {
|
|
257
274
|
this.__clickDisposer = addEventListener(
|
|
258
275
|
document,
|
|
259
276
|
'click',
|
|
@@ -270,17 +287,29 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
270
287
|
}
|
|
271
288
|
}
|
|
272
289
|
}
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
// 调用拖拽开始回调
|
|
291
|
+
if (!this.isDragging && this.props.onDragStart) {
|
|
292
|
+
const positionChange = {
|
|
293
|
+
top: this.startTop,
|
|
294
|
+
left: this.startLeft,
|
|
295
|
+
bottom: this.startBottom,
|
|
296
|
+
right: this.startRight,
|
|
297
|
+
diffX: 0,
|
|
298
|
+
diffY: 0
|
|
299
|
+
};
|
|
300
|
+
this.props.onDragStart(positionChange);
|
|
301
|
+
}
|
|
302
|
+
this.dragging(evt as TouchEvent | MouseEvent);
|
|
303
|
+
}, {
|
|
304
|
+
passive: !isTouch
|
|
305
|
+
});
|
|
275
306
|
|
|
276
307
|
this.__upDisposer?.();
|
|
277
308
|
this.__upDisposer = addEventListener(
|
|
278
309
|
document,
|
|
279
310
|
'mouseup',
|
|
280
|
-
(
|
|
311
|
+
() => {
|
|
281
312
|
this.endDrag();
|
|
282
|
-
evt.stopPropagation();
|
|
283
|
-
evt.preventDefault();
|
|
284
313
|
},
|
|
285
314
|
true
|
|
286
315
|
);
|
|
@@ -294,11 +323,11 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
294
323
|
this.axisX = evt.nativeEvent.pageX;
|
|
295
324
|
this.axisY = evt.nativeEvent.pageY;
|
|
296
325
|
if (!this.props.disabled) {
|
|
297
|
-
|
|
326
|
+
this.enableDrag();
|
|
298
327
|
}
|
|
299
328
|
};
|
|
300
329
|
|
|
301
|
-
dragging = (evt: MouseEvent) => {
|
|
330
|
+
dragging = (evt: MouseEvent | TouchEvent) => {
|
|
302
331
|
this.isDragging = true;
|
|
303
332
|
const { lockAxis } = this.props;
|
|
304
333
|
const { minX, maxX, minY, maxY } = this.dragPositionBoundaries;
|
|
@@ -307,9 +336,21 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
307
336
|
const scaleX = this.cachedScaleX;
|
|
308
337
|
const scaleY = this.cachedScaleY;
|
|
309
338
|
|
|
339
|
+
// 获取坐标
|
|
340
|
+
let pageX: number, pageY: number;
|
|
341
|
+
if (evt instanceof TouchEvent) {
|
|
342
|
+
const touch = evt.touches[0];
|
|
343
|
+
if (!touch) return;
|
|
344
|
+
pageX = touch.pageX;
|
|
345
|
+
pageY = touch.pageY;
|
|
346
|
+
} else {
|
|
347
|
+
pageX = evt.pageX;
|
|
348
|
+
pageY = evt.pageY;
|
|
349
|
+
}
|
|
350
|
+
|
|
310
351
|
// 计算原始偏移量(需要除以缩放比例)
|
|
311
|
-
this.dX = (
|
|
312
|
-
this.dY = (
|
|
352
|
+
this.dX = (pageX - (this.axisX || 0)) / scaleX;
|
|
353
|
+
this.dY = (pageY - (this.axisY || 0)) / scaleY;
|
|
313
354
|
|
|
314
355
|
// 应用方向锁定并计算变换值
|
|
315
356
|
let translateX = this.dX;
|
|
@@ -342,16 +383,45 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
342
383
|
translateY = maxY - this.startTop;
|
|
343
384
|
}
|
|
344
385
|
}
|
|
386
|
+
if (this.translateX === translateX && this.translateY === translateY) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 调用拖拽中回调
|
|
391
|
+
if (this.props.onDrag) {
|
|
392
|
+
const positionChange = {
|
|
393
|
+
top: this.startTop + translateY,
|
|
394
|
+
left: this.startLeft + translateX,
|
|
395
|
+
bottom: this.startBottom - translateY,
|
|
396
|
+
right: this.startRight - translateX,
|
|
397
|
+
diffX: translateX,
|
|
398
|
+
diffY: translateY
|
|
399
|
+
};
|
|
400
|
+
this.props.onDrag(positionChange);
|
|
401
|
+
}
|
|
345
402
|
|
|
346
403
|
if (this.draggerRef) {
|
|
347
404
|
this.draggerRef.style.transform = `translate(${translateX}px, ${translateY}px)`;
|
|
348
405
|
}
|
|
349
|
-
|
|
406
|
+
this.translateX = translateX;
|
|
407
|
+
this.translateY = translateY;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
startTouchDrag = (evt: React.TouchEvent<HTMLDivElement>) => {
|
|
411
|
+
const touch = evt.touches[0];
|
|
412
|
+
if (!touch) return;
|
|
413
|
+
this.axisX = touch.pageX;
|
|
414
|
+
this.axisY = touch.pageY;
|
|
415
|
+
if (!this.props.disabled) {
|
|
416
|
+
this.enableDrag(true);
|
|
417
|
+
}
|
|
350
418
|
};
|
|
351
419
|
|
|
352
420
|
endDrag = () => {
|
|
353
421
|
if (this.isDragging) {
|
|
354
|
-
this.calcPosition();
|
|
422
|
+
const positionChange = this.calcPosition();
|
|
423
|
+
// 调用拖拽结束回调
|
|
424
|
+
this.props.onDragEnd?.(positionChange);
|
|
355
425
|
if (this.draggerRef) {
|
|
356
426
|
this.draggerRef.style.transform = '';
|
|
357
427
|
}
|
|
@@ -362,8 +432,10 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
362
432
|
}
|
|
363
433
|
}
|
|
364
434
|
|
|
365
|
-
this.__moveDisposer
|
|
366
|
-
|
|
435
|
+
if (this.__moveDisposer) {
|
|
436
|
+
this.__moveDisposer();
|
|
437
|
+
this.__moveDisposer = undefined;
|
|
438
|
+
}
|
|
367
439
|
if (this.__clickDisposer) {
|
|
368
440
|
requestAnimationFrame(() => {
|
|
369
441
|
if (this.__clickDisposer) {
|
|
@@ -372,10 +444,14 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
372
444
|
}
|
|
373
445
|
});
|
|
374
446
|
}
|
|
375
|
-
this.__upDisposer
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
447
|
+
if (this.__upDisposer) {
|
|
448
|
+
this.__upDisposer();
|
|
449
|
+
this.__upDisposer = undefined;
|
|
450
|
+
}
|
|
451
|
+
if (this.__bodyClassDisposer) {
|
|
452
|
+
this.__bodyClassDisposer();
|
|
453
|
+
this.__bodyClassDisposer = undefined;
|
|
454
|
+
}
|
|
379
455
|
|
|
380
456
|
this.isDragging = false;
|
|
381
457
|
};
|
|
@@ -441,8 +517,6 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
441
517
|
const realBottom = height - realTop - this.dragBoxSize.height;
|
|
442
518
|
const realRight = width - realLeft - this.dragBoxSize.width;
|
|
443
519
|
if (realTop !== this.state.top || realLeft !== this.state.left || this.state.bottom !== realBottom || this.state.right !== realRight) {
|
|
444
|
-
// console.log(minY, maxY, this.startTop, this.dY, newTop, realTop, 'minY, maxY, this.startTop, this.dY, newTop, realTop --- calcPosition y');
|
|
445
|
-
// console.log(minX, maxX, this.startLeft, this.dX, newLeft, realLeft, 'minX, maxX, this.startLeft, this.dX, newLeft, realLeft ---calcPosition x');
|
|
446
520
|
this.setState({
|
|
447
521
|
top: realTop,
|
|
448
522
|
left: realLeft,
|
|
@@ -450,11 +524,29 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
450
524
|
right: realRight
|
|
451
525
|
});
|
|
452
526
|
}
|
|
527
|
+
const positionChange = {
|
|
528
|
+
top: realTop,
|
|
529
|
+
left: realLeft,
|
|
530
|
+
bottom: realBottom,
|
|
531
|
+
right: realRight,
|
|
532
|
+
diffX: realLeft - this.startLeft,
|
|
533
|
+
diffY: realTop - this.startTop
|
|
534
|
+
};
|
|
453
535
|
this.startTop = realTop;
|
|
454
536
|
this.startLeft = realLeft;
|
|
537
|
+
this.startBottom = realBottom;
|
|
538
|
+
this.startRight = realRight;
|
|
455
539
|
this.dX = this.dY = 0;
|
|
540
|
+
return positionChange;
|
|
456
541
|
};
|
|
457
542
|
|
|
543
|
+
// 更新状态并计算位置 (外部可以调用)
|
|
544
|
+
updateState = ({offsetX, offsetY}: Pick<DraggableBoxProps, 'offsetX' | 'offsetY'> = {}) => {
|
|
545
|
+
this.setState(this.formatState({offsetX, offsetY}), () => {
|
|
546
|
+
this.reportStartPosition();
|
|
547
|
+
this.calcPosition();
|
|
548
|
+
});
|
|
549
|
+
}
|
|
458
550
|
|
|
459
551
|
componentDidMount() {
|
|
460
552
|
// 检查初始位置是否在边界范围内,如果不在则修正
|
|
@@ -464,6 +556,13 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
464
556
|
this.__resizeDisposer = addEventListener(window, 'resize', () => {
|
|
465
557
|
this.calcPosition();
|
|
466
558
|
});
|
|
559
|
+
|
|
560
|
+
// 触屏设备时,阻止拖拽时滚动页面
|
|
561
|
+
if (supportsTouchEvents() && this.draggerRef) {
|
|
562
|
+
this.__preventScrollDisposer = addEventListener(this.draggerRef, 'touchmove', (evt) => {
|
|
563
|
+
evt.preventDefault();
|
|
564
|
+
});
|
|
565
|
+
}
|
|
467
566
|
}
|
|
468
567
|
|
|
469
568
|
componentWillUnmount() {
|
|
@@ -472,18 +571,19 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
472
571
|
this.__moveDisposer?.();
|
|
473
572
|
this.__clickDisposer?.();
|
|
474
573
|
this.__upDisposer?.();
|
|
574
|
+
this.__preventScrollDisposer?.();
|
|
475
575
|
}
|
|
476
576
|
|
|
477
577
|
render() {
|
|
478
578
|
const { className, zIndex, children, showDragArea, positionMode = 'fixed' } = this.props;
|
|
479
|
-
const { startDrag, endDrag } = this;
|
|
579
|
+
const { startDrag, startTouchDrag, endDrag } = this;
|
|
480
580
|
const stl = {
|
|
481
581
|
zIndex,
|
|
482
582
|
...this.position,
|
|
483
583
|
position: positionMode
|
|
484
584
|
};
|
|
485
585
|
return (
|
|
486
|
-
|
|
586
|
+
<React.Fragment>
|
|
487
587
|
{showDragArea && (
|
|
488
588
|
<div
|
|
489
589
|
className={c('drag-area')}
|
|
@@ -508,14 +608,14 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
|
|
|
508
608
|
ref={(r) => {
|
|
509
609
|
this.draggerRef = r;
|
|
510
610
|
}}
|
|
511
|
-
|
|
512
|
-
|
|
611
|
+
onMouseDownCapture={startDrag}
|
|
612
|
+
onMouseUpCapture={endDrag}
|
|
613
|
+
onTouchStartCapture={startTouchDrag}
|
|
614
|
+
onTouchEndCapture={endDrag}
|
|
513
615
|
>
|
|
514
616
|
{children}
|
|
515
617
|
</div>
|
|
516
|
-
|
|
618
|
+
</React.Fragment>
|
|
517
619
|
);
|
|
518
620
|
}
|
|
519
621
|
}
|
|
520
|
-
|
|
521
|
-
export default DraggableBox;
|
package/src/type.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type {ValidPlacement} from './constants';
|
|
2
2
|
|
|
3
|
+
export interface IPositionChange {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
right: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
diffX: number;
|
|
9
|
+
diffY: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
export interface DraggableBoxProps {
|
|
4
13
|
className?: string;
|
|
5
14
|
children?: React.ReactNode;
|
|
@@ -51,6 +60,11 @@ export interface DraggableBoxProps {
|
|
|
51
60
|
* @default false
|
|
52
61
|
*/
|
|
53
62
|
showDragArea?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* 拖拽过程中,超出多少px时才显示拖拽区域可视化
|
|
65
|
+
* @default 5
|
|
66
|
+
*/
|
|
67
|
+
showDragAreaOverMoveDistanse?: number;
|
|
54
68
|
/**
|
|
55
69
|
* 定位模式
|
|
56
70
|
* 'fixed' - 使用 fixed 定位(默认),动态查找影响 fixed 定位的父元素
|
|
@@ -58,6 +72,18 @@ export interface DraggableBoxProps {
|
|
|
58
72
|
* @default 'fixed'
|
|
59
73
|
*/
|
|
60
74
|
positionMode?: 'fixed' | 'absolute';
|
|
75
|
+
/**
|
|
76
|
+
* 拖拽开始回调函数
|
|
77
|
+
*/
|
|
78
|
+
onDragStart?: (positionChange: IPositionChange) => void;
|
|
79
|
+
/**
|
|
80
|
+
* 拖拽中回调函数
|
|
81
|
+
*/
|
|
82
|
+
onDrag?: (positionChange: IPositionChange) => void;
|
|
83
|
+
/**
|
|
84
|
+
* 拖拽结束回调函数
|
|
85
|
+
*/
|
|
86
|
+
onDragEnd?: (positionChange: IPositionChange) => void;
|
|
61
87
|
}
|
|
62
88
|
|
|
63
89
|
export interface DraggableBoxState {
|
package/src/utils.ts
CHANGED
|
@@ -86,3 +86,13 @@ export function getScaleRatio(dom?: HTMLElement | null): { scaleX: number; scale
|
|
|
86
86
|
export function clamp(value: number, min: number, max: number) {
|
|
87
87
|
return Math.min(Math.max(value, min), max);
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 检测当前环境是否支持触摸事件
|
|
93
|
+
*
|
|
94
|
+
* @returns 如果环境支持触摸事件返回 true,否则返回 false
|
|
95
|
+
*/
|
|
96
|
+
export function supportsTouchEvents() {
|
|
97
|
+
return typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
|
98
|
+
};
|