@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/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
- const { placement = 'bottom-right', offsetY = 20, offsetX = 20 } = props;
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: 移动超过5px?? 确保用户有真实的移动意愿,而不是手抖~~
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
- this.dragging(evt);
274
- }, true);
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
- (evt) => {
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
- this.enableDrag();
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 = (evt.pageX - (this.axisX || 0)) / scaleX;
312
- this.dY = (evt.pageY - (this.axisY || 0)) / scaleY;
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
- evt.stopPropagation();
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
- this.__moveDisposer = undefined;
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
- this.__upDisposer = undefined;
377
- this.__bodyClassDisposer?.();
378
- this.__bodyClassDisposer = undefined;
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
- onMouseDown={startDrag}
512
- onMouseUp={endDrag}
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
+ };