@ohkit/draggable-box 0.0.1 → 0.0.3

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.
@@ -1,13 +1,19 @@
1
+ /**
2
+ * 检查元素是否创建了新的定位上下文(包含块)
3
+ * @param element 要检查的元素
4
+ * @param includePosition 是否检查position属性(absolute/relative/fixed)
5
+ */
6
+ export declare function isPositioningContextCreator(element: HTMLElement, includePosition: boolean): boolean;
1
7
  /**
2
8
  * 查找影响 fixed 定位的父元素
3
- * 当父元素有 transform/filter/perspective 等属性时,fixed 定位会相对于该父元素
9
+ * 当父元素有 transform/filter/perspective/contain/will-change 等属性时,fixed 定位会相对于该父元素
4
10
  */
5
- export declare function findFixedPositionParent(dom?: HTMLElement | null): HTMLElement;
11
+ export declare function findFixedPositionParent(dom?: HTMLElement | null): HTMLElement | null;
6
12
  /**
7
13
  * 查找 absolute 定位的父元素
8
- * 查找最近的 position 不为 static 的元素
14
+ * 查找最近的包含块创建者,包含所有可能影响absolute定位的属性
9
15
  */
10
- export declare function findAbsolutePositionParent(dom?: HTMLElement | null): HTMLElement;
16
+ export declare function findAbsolutePositionParent(dom?: HTMLElement | null): HTMLElement | null;
11
17
  /**
12
18
  * 获取容器的缩放比例
13
19
  * 通过比较元素的实际尺寸和 getBoundingClientRect 返回的尺寸来计算缩放比例
@@ -16,3 +22,18 @@ export declare function getScaleRatio(dom?: HTMLElement | null): {
16
22
  scaleX: number;
17
23
  scaleY: number;
18
24
  };
25
+ /**
26
+ * 限制数值在指定范围内
27
+ *
28
+ * @param value 要限制的数值
29
+ * @param min 最小值
30
+ * @param max 最大值
31
+ * @returns 限制后的数值,确保在 [min, max] 范围内
32
+ */
33
+ export declare function clamp(value: number, min: number, max: number): number;
34
+ /**
35
+ * 检测当前环境是否支持触摸事件
36
+ *
37
+ * @returns 如果环境支持触摸事件返回 true,否则返回 false
38
+ */
39
+ export declare function supportsTouchEvents(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohkit/draggable-box",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "可拖拽容器",
5
5
  "keywords": [
6
6
  "draggable-box"
@@ -27,12 +27,12 @@
27
27
  "clean": "rm -rf dist"
28
28
  },
29
29
  "dependencies": {
30
- "@ohkit/dom-helper": "0.0.6",
30
+ "@ohkit/dom-helper": "0.0.7",
31
31
  "@ohkit/prefix-classname": "^0.0.3"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": ">=17",
35
35
  "react-dom": ">=17"
36
36
  },
37
- "gitHead": "f6aca250c73c8a72c20fdcbeb86b8d9032367927"
37
+ "gitHead": "e4e819440b17190101d14f35580fe68558fe86a9"
38
38
  }
package/src/index.tsx CHANGED
@@ -4,7 +4,7 @@ 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} 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
 
@@ -20,6 +20,7 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
20
20
  disabled: false,
21
21
  lockAxis: 'none',
22
22
  showDragArea: false,
23
+ showDragAreaOverMoveDistanse: 5,
23
24
  positionMode: 'fixed',
24
25
  };
25
26
 
@@ -38,62 +39,65 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
38
39
  };
39
40
  }
40
41
 
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
- }
42
+ private prePositionMode: DraggableBoxProps['positionMode'];
43
+ private preDraggerRef: HTMLElement | null = null;
44
+ private container: HTMLElement | null = null;
58
45
  /**
59
46
  * 获取定位容器
60
47
  * 根据 positionMode 返回对应的定位父元素
61
48
  */
62
- private getContainer(): HTMLElement {
49
+ private getContainer(useCache = true) {
63
50
  const { positionMode = 'fixed' } = this.props;
64
- return positionMode === 'fixed'
65
- ? findFixedPositionParent(this.draggerRef)
66
- : findAbsolutePositionParent(this.draggerRef);
51
+ if (!this.container || !useCache || this.prePositionMode !== positionMode || this.preDraggerRef !== this.draggerRef) {
52
+ this.prePositionMode = positionMode;
53
+ this.preDraggerRef = this.draggerRef;
54
+ this.container = positionMode === 'fixed'
55
+ ? findFixedPositionParent(this.draggerRef)
56
+ : findAbsolutePositionParent(this.draggerRef);
57
+ }
58
+ return this.container;
67
59
  }
68
60
 
69
61
  /**
70
62
  * 获取容器的尺寸和位置信息
71
63
  */
72
64
  private getContainerRect() {
73
- const { positionMode = 'fixed' } = this.props;
74
- const isFixed = positionMode === 'fixed';
75
- const container = this.getContainer();
65
+ const container = this.getContainer(false);
66
+ if (!container) {
67
+ return {
68
+ width: window.innerWidth,
69
+ height: window.innerHeight,
70
+ left: 0,
71
+ top: 0,
72
+ scrollLeft: 0,
73
+ scrollTop: 0,
74
+ scrollerScrollLeft: 0,
75
+ scrollerScrollTop: 0,
76
+ borderLeftWidth: 0,
77
+ borderTopWidth: 0
78
+ };
79
+ }
76
80
  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
- }
81
+
82
+ // 获取容器的border宽度(仅 top, left 对坐标计算有影响)
83
+ const containerStyle = window.getComputedStyle(container);
84
+ const borderLeftWidth = parseFloat(containerStyle.borderLeftWidth) || 0;
85
+ const borderTopWidth = parseFloat(containerStyle.borderTopWidth) || 0;
86
+ const borderRightWidth = parseFloat(containerStyle.borderRightWidth) || 0;
87
+ const borderBottomWidth = parseFloat(containerStyle.borderBottomWidth) || 0;
88
+ const yScrollerWidth = container.offsetWidth - container.clientWidth - borderLeftWidth - borderRightWidth;
89
+ const xScrollerHeight = container.offsetHeight - container.clientHeight - borderTopWidth - borderBottomWidth;
90
+ // console.log('yScrollerWidth, xScrollerHeight', yScrollerWidth, xScrollerHeight);
90
91
 
91
- get windowSize() {
92
- const container = this.getContainer();
93
- const { clientWidth, clientHeight } = container;
94
92
  return {
95
- height: clientHeight,
96
- width: clientWidth
93
+ width: containerRect.width / this.cachedScaleX - borderLeftWidth - borderRightWidth - yScrollerWidth,
94
+ height: containerRect.height / this.cachedScaleY - borderTopWidth - borderBottomWidth - xScrollerHeight,
95
+ left: containerRect.left / this.cachedScaleX,
96
+ top: containerRect.top / this.cachedScaleY,
97
+ scrollLeft: container.scrollLeft,
98
+ scrollTop: container.scrollTop,
99
+ borderLeftWidth: borderLeftWidth,
100
+ borderTopWidth: borderTopWidth,
97
101
  };
98
102
  }
99
103
 
@@ -113,19 +117,21 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
113
117
  get dragPositionBoundaries() {
114
118
  const { boundsX, boundsY, placement = 'bottom-right' } = this.props;
115
119
  const dragSize = this.dragBoxSize;
116
- const windowSize = this.windowSize;
120
+ const {width: containerWidth, height: containerHeight} = this.getContainerRect();
117
121
  const [placementY, placementX] = placement.split('-') as ['top' | 'bottom', 'left' | 'right'];
118
122
 
123
+ const defaultBounds = {
124
+ minX: 0,
125
+ maxX: Math.max(containerWidth - dragSize.width, 0),
126
+ minY: 0,
127
+ maxY: Math.max(containerHeight- dragSize.height, 0),
128
+ };
119
129
  // 初始化边界
120
- let minX = 0;
121
- let maxX = windowSize.width - dragSize.width;
122
- let minY = 0;
123
- let maxY = windowSize.height - dragSize.height;
130
+ let {minX, maxX, minY, maxY} = defaultBounds
124
131
 
125
132
  // 处理X轴边界
126
133
  if (boundsX) {
127
134
  const [minBound, maxBound] = boundsX;
128
-
129
135
  if (placementX === 'left') {
130
136
  // 左边位置:boundsX=[左边最小距离, 左边最大距离]
131
137
  if (minBound !== undefined) minX = Math.max(minX, minBound);
@@ -134,19 +140,14 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
134
140
  // 右边位置:boundsX=[右边最小距离, 右边最大距离]
135
141
  // 直接使用边界值作为right的限制
136
142
  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);
143
+ minX = Math.max(minX, containerWidth - maxBound - dragSize.width);
144
+ maxX = Math.min(maxX, containerWidth - minBound - dragSize.width);
139
145
  } else if (minBound !== undefined) {
140
146
  // 只有minBound:设置最大边界,最小边界保持默认
141
- maxX = Math.min(maxX, windowSize.width - minBound - dragSize.width);
147
+ maxX = Math.min(maxX, containerWidth - minBound - dragSize.width);
142
148
  } else if (maxBound !== undefined) {
143
149
  // 只有maxBound:设置最小边界,最大边界保持默认
144
- minX = Math.max(minX, windowSize.width - maxBound - dragSize.width);
145
- }
146
-
147
- // 确保最小边界不大于最大边界
148
- if (minX > maxX) {
149
- [minX, maxX] = [maxX, minX];
150
+ minX = Math.max(minX, containerWidth - maxBound - dragSize.width);
150
151
  }
151
152
  }
152
153
  }
@@ -163,31 +164,25 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
163
164
  // 底部位置:boundsY=[底边最小距离, 底边最大距离]
164
165
  // 直接使用边界值作为bottom的限制
165
166
  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);
167
+ minY = Math.max(minY, containerHeight - maxBound - dragSize.height);
168
+ maxY = Math.min(maxY, containerHeight - minBound - dragSize.height);
168
169
  } else if (minBound !== undefined) {
169
170
  // 只有minBound:设置最大边界,最小边界保持默认
170
- maxY = Math.min(maxY, windowSize.height - minBound - dragSize.height);
171
+ maxY = Math.min(maxY, containerHeight - minBound - dragSize.height);
171
172
  } else if (maxBound !== undefined) {
172
173
  // 只有maxBound:设置最小边界,最大边界保持默认
173
- minY = Math.max(minY, windowSize.height - maxBound - dragSize.height);
174
- }
175
-
176
- // 确保最小边界不大于最大边界
177
- if (minY > maxY) {
178
- [minY, maxY] = [maxY, minY];
174
+ minY = Math.max(minY, containerHeight - maxBound - dragSize.height);
179
175
  }
180
176
  }
181
177
  }
178
+ // 确保各个边界值在默认范围内
179
+ minX = clamp(minX, defaultBounds.minX, defaultBounds.maxX);
180
+ maxX = clamp(maxX, minX, defaultBounds.maxX);
181
+ minY = clamp(minY, defaultBounds.minY, defaultBounds.maxY);
182
+ maxY = clamp(maxY, minY, defaultBounds.maxY);
182
183
 
183
184
  return { minX, maxX, minY, maxY };
184
185
  }
185
-
186
- // 保持向后兼容
187
- get dragPositionRang() {
188
- const { maxX, maxY } = this.dragPositionBoundaries;
189
- return { width: maxX, height: maxY };
190
- }
191
186
 
192
187
  get curPositionKey() {
193
188
  let {placement} = this.props;
@@ -232,35 +227,39 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
232
227
  __bodyClassDisposer?: () => void;
233
228
  __upDisposer?: () => void;
234
229
  __resizeDisposer?: () => void;
230
+ __preventScrollDisposer?: () => void;
235
231
 
236
232
  dragAreaRef: HTMLDivElement | null = null;
237
233
 
238
234
  reportStartPosition() {
239
235
  if (this.draggerRef) {
240
- const { top, left } = this.draggerRef.getBoundingClientRect();
241
- const containerRect = this.getContainerRect();
242
- // console.log(containerRect, 'containerRect');
243
-
244
236
  // 获取缩放比例
245
- const { scaleX, scaleY } = getScaleRatio(this.draggerRef);
237
+ const { scaleX, scaleY } = getScaleRatio(this.getContainer());
246
238
  this.cachedScaleX = scaleX;
247
239
  this.cachedScaleY = scaleY;
240
+ const { top, left } = this.draggerRef.getBoundingClientRect();
241
+ const containerRect = this.getContainerRect();
242
+ // console.log(containerRect, 'containerRect');
248
243
 
249
244
  // 计算相对于容器的位置,并除以缩放比例得到未缩放的坐标
250
- this.startTop = (top - containerRect.top + containerRect.scrollerScrollTop) / scaleY + containerRect.scrollTop;
251
- this.startLeft = (left - containerRect.left + containerRect.scrollerScrollLeft) / scaleX + containerRect.scrollLeft;
245
+ this.startTop = top / scaleY - containerRect.top + containerRect.scrollTop - containerRect.borderTopWidth;
246
+ this.startLeft = left / scaleY - containerRect.left + containerRect.scrollLeft - containerRect.borderLeftWidth;
252
247
  }
253
248
  }
254
249
 
255
- enableDrag = () => {
250
+ enableDrag = (isTouch = false) => {
256
251
  this.reportStartPosition();
257
252
  this.__moveDisposer?.();
258
- this.__moveDisposer = addEventListener(document, 'mousemove', (evt) => {
253
+ this.__moveDisposer = addEventListener(isTouch && this.draggerRef ? this.draggerRef : document, isTouch ? 'touchmove' : 'mousemove', (evt) => {
254
+ evt.stopPropagation();
255
+ if (isTouch) {
256
+ evt.preventDefault();
257
+ }
259
258
  // INFO: 移动过程中禁止click事件
260
259
  if (!this.__clickDisposer) {
261
260
  const moveDistanse = Math.sqrt(Math.pow(this.dX, 2) + Math.pow(this.dY, 2));
262
- // INFO: 移动超过5px?? 确保用户有真实的移动意愿,而不是手抖~~
263
- if (moveDistanse > 5) {
261
+ // INFO: 移动超过px?? 确保用户有真实的移动意愿,而不是手抖~~
262
+ if (moveDistanse > (this.props.showDragAreaOverMoveDistanse || 5)) {
264
263
  this.__clickDisposer = addEventListener(
265
264
  document,
266
265
  'click',
@@ -277,17 +276,17 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
277
276
  }
278
277
  }
279
278
  }
280
- this.dragging(evt);
281
- }, true);
279
+ this.dragging(evt as TouchEvent | MouseEvent);
280
+ }, {
281
+ passive: !isTouch
282
+ });
282
283
 
283
284
  this.__upDisposer?.();
284
285
  this.__upDisposer = addEventListener(
285
286
  document,
286
287
  'mouseup',
287
- (evt) => {
288
+ () => {
288
289
  this.endDrag();
289
- evt.stopPropagation();
290
- evt.preventDefault();
291
290
  },
292
291
  true
293
292
  );
@@ -301,11 +300,11 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
301
300
  this.axisX = evt.nativeEvent.pageX;
302
301
  this.axisY = evt.nativeEvent.pageY;
303
302
  if (!this.props.disabled) {
304
- this.enableDrag();
303
+ this.enableDrag();
305
304
  }
306
305
  };
307
306
 
308
- dragging = (evt: MouseEvent) => {
307
+ dragging = (evt: MouseEvent | TouchEvent) => {
309
308
  this.isDragging = true;
310
309
  const { lockAxis } = this.props;
311
310
  const { minX, maxX, minY, maxY } = this.dragPositionBoundaries;
@@ -314,9 +313,21 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
314
313
  const scaleX = this.cachedScaleX;
315
314
  const scaleY = this.cachedScaleY;
316
315
 
316
+ // 获取坐标
317
+ let pageX: number, pageY: number;
318
+ if (evt instanceof TouchEvent) {
319
+ const touch = evt.touches[0];
320
+ if (!touch) return;
321
+ pageX = touch.pageX;
322
+ pageY = touch.pageY;
323
+ } else {
324
+ pageX = evt.pageX;
325
+ pageY = evt.pageY;
326
+ }
327
+
317
328
  // 计算原始偏移量(需要除以缩放比例)
318
- this.dX = (evt.pageX - (this.axisX || 0)) / scaleX;
319
- this.dY = (evt.pageY - (this.axisY || 0)) / scaleY;
329
+ this.dX = (pageX - (this.axisX || 0)) / scaleX;
330
+ this.dY = (pageY - (this.axisY || 0)) / scaleY;
320
331
 
321
332
  // 应用方向锁定并计算变换值
322
333
  let translateX = this.dX;
@@ -353,7 +364,16 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
353
364
  if (this.draggerRef) {
354
365
  this.draggerRef.style.transform = `translate(${translateX}px, ${translateY}px)`;
355
366
  }
356
- evt.stopPropagation();
367
+ };
368
+
369
+ startTouchDrag = (evt: React.TouchEvent<HTMLDivElement>) => {
370
+ const touch = evt.touches[0];
371
+ if (!touch) return;
372
+ this.axisX = touch.pageX;
373
+ this.axisY = touch.pageY;
374
+ if (!this.props.disabled) {
375
+ this.enableDrag(true);
376
+ }
357
377
  };
358
378
 
359
379
  endDrag = () => {
@@ -369,8 +389,10 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
369
389
  }
370
390
  }
371
391
 
372
- this.__moveDisposer?.();
373
- this.__moveDisposer = undefined;
392
+ if (this.__moveDisposer) {
393
+ this.__moveDisposer();
394
+ this.__moveDisposer = undefined;
395
+ }
374
396
  if (this.__clickDisposer) {
375
397
  requestAnimationFrame(() => {
376
398
  if (this.__clickDisposer) {
@@ -379,10 +401,14 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
379
401
  }
380
402
  });
381
403
  }
382
- this.__upDisposer?.();
383
- this.__upDisposer = undefined;
384
- this.__bodyClassDisposer?.();
385
- this.__bodyClassDisposer = undefined;
404
+ if (this.__upDisposer) {
405
+ this.__upDisposer();
406
+ this.__upDisposer = undefined;
407
+ }
408
+ if (this.__bodyClassDisposer) {
409
+ this.__bodyClassDisposer();
410
+ this.__bodyClassDisposer = undefined;
411
+ }
386
412
 
387
413
  this.isDragging = false;
388
414
  };
@@ -404,20 +430,12 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
404
430
  this.dragAreaRef.style.height = '2px'; // 更细的虚线高度
405
431
  this.dragAreaRef.style.left = `${minX}px`;
406
432
  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
433
  } else if (lockAxis === 'y') {
412
434
  // 锁定X方向,显示为垂直虚线区域
413
435
  this.dragAreaRef.style.width = '2px'; // 更细的虚线宽度
414
436
  this.dragAreaRef.style.height = `${maxY - minY + dragSize.height}px`;
415
437
  this.dragAreaRef.style.left = `${this.startLeft + dragSize.width / 2}px`;
416
438
  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
439
  } else {
422
440
  // 自由拖拽,显示完整区域
423
441
  this.dragAreaRef.style.width = `${maxX - minX + dragSize.width}px`;
@@ -437,7 +455,7 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
437
455
  calcPosition = () => {
438
456
  const { lockAxis } = this.props;
439
457
  const { minX, maxX, minY, maxY } = this.dragPositionBoundaries;
440
- const containerSize = this.windowSize;
458
+ const {height, width} = this.getContainerRect();
441
459
 
442
460
  // 计算新的位置
443
461
  let newTop = this.startTop;
@@ -451,13 +469,13 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
451
469
  }
452
470
 
453
471
  // 应用边界限制
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;
472
+ const realTop = clamp(newTop, minY, maxY);
473
+ const realLeft = clamp(newLeft, minX, maxX);
474
+ const realBottom = height - realTop - this.dragBoxSize.height;
475
+ const realRight = width - realLeft - this.dragBoxSize.width;
458
476
  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');
477
+ // console.log(minY, maxY, this.startTop, this.dY, newTop, realTop, 'minY, maxY, this.startTop, this.dY, newTop, realTop --- calcPosition y');
478
+ // console.log(minX, maxX, this.startLeft, this.dX, newLeft, realLeft, 'minX, maxX, this.startLeft, this.dX, newLeft, realLeft ---calcPosition x');
461
479
  this.setState({
462
480
  top: realTop,
463
481
  left: realLeft,
@@ -479,6 +497,13 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
479
497
  this.__resizeDisposer = addEventListener(window, 'resize', () => {
480
498
  this.calcPosition();
481
499
  });
500
+
501
+ // 触屏设备时,阻止拖拽时滚动页面
502
+ if (supportsTouchEvents() && this.draggerRef) {
503
+ this.__preventScrollDisposer = addEventListener(this.draggerRef, 'touchmove', (evt) => {
504
+ evt.preventDefault();
505
+ });
506
+ }
482
507
  }
483
508
 
484
509
  componentWillUnmount() {
@@ -487,11 +512,12 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
487
512
  this.__moveDisposer?.();
488
513
  this.__clickDisposer?.();
489
514
  this.__upDisposer?.();
515
+ this.__preventScrollDisposer?.();
490
516
  }
491
517
 
492
518
  render() {
493
519
  const { className, zIndex, children, showDragArea, positionMode = 'fixed' } = this.props;
494
- const { startDrag, endDrag } = this;
520
+ const { startDrag, startTouchDrag, endDrag } = this;
495
521
  const stl = {
496
522
  zIndex,
497
523
  ...this.position,
@@ -523,8 +549,10 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
523
549
  ref={(r) => {
524
550
  this.draggerRef = r;
525
551
  }}
526
- onMouseDown={startDrag}
527
- onMouseUp={endDrag}
552
+ onMouseDownCapture={startDrag}
553
+ onMouseUpCapture={endDrag}
554
+ onTouchStartCapture={startTouchDrag}
555
+ onTouchEndCapture={endDrag}
528
556
  >
529
557
  {children}
530
558
  </div>
package/src/type.ts CHANGED
@@ -51,6 +51,11 @@ export interface DraggableBoxProps {
51
51
  * @default false
52
52
  */
53
53
  showDragArea?: boolean;
54
+ /**
55
+ * 拖拽过程中,超出多少px时才显示拖拽区域可视化
56
+ * @default 5
57
+ */
58
+ showDragAreaOverMoveDistanse?: number;
54
59
  /**
55
60
  * 定位模式
56
61
  * 'fixed' - 使用 fixed 定位(默认),动态查找影响 fixed 定位的父元素
package/src/utils.ts CHANGED
@@ -1,42 +1,56 @@
1
1
  import {findParent} from '@ohkit/dom-helper';
2
2
 
3
+ /**
4
+ * 检查元素是否创建了新的定位上下文(包含块)
5
+ * @param element 要检查的元素
6
+ * @param includePosition 是否检查position属性(absolute/relative/fixed)
7
+ */
8
+ export function isPositioningContextCreator(element: HTMLElement, includePosition: boolean): boolean {
9
+ const style = window.getComputedStyle(element);
10
+ const position = style.getPropertyValue('position');
11
+ const transform = style.getPropertyValue('transform');
12
+ const filter = style.getPropertyValue('filter');
13
+ const perspective = style.getPropertyValue('perspective');
14
+ const contain = style.getPropertyValue('contain');
15
+ const willChange = style.getPropertyValue('will-change');
16
+
17
+ return (includePosition && position !== 'static') ||
18
+ transform !== 'none' ||
19
+ filter !== 'none' ||
20
+ perspective !== 'none' ||
21
+ contain.includes('paint') ||
22
+ contain.includes('layout') ||
23
+ contain.includes('strict') ||
24
+ willChange.includes('transform') ||
25
+ willChange.includes('perspective') ||
26
+ willChange.includes('filter');
27
+ }
28
+
3
29
  /**
4
30
  * 查找影响 fixed 定位的父元素
5
- * 当父元素有 transform/filter/perspective 等属性时,fixed 定位会相对于该父元素
31
+ * 当父元素有 transform/filter/perspective/contain/will-change 等属性时,fixed 定位会相对于该父元素
6
32
  */
7
- export function findFixedPositionParent(dom?: HTMLElement | null): HTMLElement {
33
+ export function findFixedPositionParent(dom?: HTMLElement | null) {
8
34
  if (!dom) {
9
35
  return document.documentElement;
10
36
  }
11
37
  const fixedPositionParent = findParent(dom, (parent) => {
12
- const style = window.getComputedStyle(parent);
13
- const transform = style.getPropertyValue('transform');
14
- const filter = style.getPropertyValue('filter');
15
- const perspective = style.getPropertyValue('perspective');
16
-
17
- // 检查是否有影响 fixed 定位的属性
18
- if (transform !== 'none' || filter !== 'none' || perspective !== 'none') {
19
- return true;
20
- }
21
- }, {excludeOwn: true}) || document.documentElement; // 没有找到,返回 window(通过 document.documentElement)
38
+ return isPositioningContextCreator(parent, false);
39
+ }, {excludeOwn: true}); // 没有找到,返回 window(通过 document.documentElement)
22
40
  return fixedPositionParent;
23
41
  }
24
42
 
25
43
  /**
26
44
  * 查找 absolute 定位的父元素
27
- * 查找最近的 position 不为 static 的元素
45
+ * 查找最近的包含块创建者,包含所有可能影响absolute定位的属性
28
46
  */
29
- export function findAbsolutePositionParent(dom?: HTMLElement | null): HTMLElement {
47
+ export function findAbsolutePositionParent(dom?: HTMLElement | null) {
30
48
  if (!dom) {
31
49
  return document.body;
32
50
  }
33
51
  return findParent(dom, (parent) => {
34
- const style = window.getComputedStyle(parent);
35
- const position = style.getPropertyValue('position');
36
- if (position !== 'static') {
37
- return true;
38
- }
39
- }, {excludeOwn: true}) || document.body;
52
+ return isPositioningContextCreator(parent, true);
53
+ }, {excludeOwn: true});
40
54
  }
41
55
 
42
56
  /**
@@ -49,9 +63,36 @@ export function getScaleRatio(dom?: HTMLElement | null): { scaleX: number; scale
49
63
  }
50
64
  // 通过比较 offsetWidth 和 getBoundingClientRect().width 来获取缩放比例
51
65
  const rect = dom.getBoundingClientRect();
52
- // 扩大10倍进行计算,避免浮点数精度问题
53
- const scaleX = dom.offsetWidth > 0 ? Math.round(rect.width / dom.offsetWidth * 10) / 10 : 1;
54
- const scaleY = dom.offsetHeight > 0 ? Math.round(rect.height / dom.offsetHeight * 10) / 10 : 1;
66
+ let scaleX = 1;
67
+ let scaleY = 1;
68
+ // 扩大{magnification}倍进行计算, 相当于保留小数点后4位
69
+ const magnification = 10000;
70
+ scaleX = dom.offsetWidth > 0 ? Math.round(rect.width / dom.offsetWidth * magnification) / magnification : 1;
71
+ scaleY = dom.offsetHeight > 0 ? Math.round(rect.height / dom.offsetHeight * magnification) / magnification : 1;
72
+ // console.log('rect.width dom.offsetWidth', rect.width, dom.offsetWidth);
73
+ // console.log('rect.height dom.offsetHeight', rect.height, dom.offsetHeight);
55
74
  // console.log('scaleX', scaleX, 'scaleY', scaleY);
56
75
  return { scaleX, scaleY };
57
76
  }
77
+
78
+ /**
79
+ * 限制数值在指定范围内
80
+ *
81
+ * @param value 要限制的数值
82
+ * @param min 最小值
83
+ * @param max 最大值
84
+ * @returns 限制后的数值,确保在 [min, max] 范围内
85
+ */
86
+ export function clamp(value: number, min: number, max: number) {
87
+ return Math.min(Math.max(value, min), max);
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
+ };