@ohkit/draggable-box 0.0.1 → 0.0.2

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,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} from './utils';
8
8
  import {ValidPlacement} from './constants';
9
9
  import {DraggableBoxProps, DraggableBoxState} from './type';
10
10
 
@@ -38,62 +38,65 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
38
38
  };
39
39
  }
40
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
- }
41
+ private prePositionMode: DraggableBoxProps['positionMode'];
42
+ private preDraggerRef: HTMLElement | null = null;
43
+ private container: HTMLElement | null = null;
58
44
  /**
59
45
  * 获取定位容器
60
46
  * 根据 positionMode 返回对应的定位父元素
61
47
  */
62
- private getContainer(): HTMLElement {
48
+ private getContainer(useCache = true) {
63
49
  const { positionMode = 'fixed' } = this.props;
64
- return positionMode === 'fixed'
65
- ? findFixedPositionParent(this.draggerRef)
66
- : findAbsolutePositionParent(this.draggerRef);
50
+ if (!this.container || !useCache || this.prePositionMode !== positionMode || this.preDraggerRef !== this.draggerRef) {
51
+ this.prePositionMode = positionMode;
52
+ this.preDraggerRef = this.draggerRef;
53
+ this.container = positionMode === 'fixed'
54
+ ? findFixedPositionParent(this.draggerRef)
55
+ : findAbsolutePositionParent(this.draggerRef);
56
+ }
57
+ return this.container;
67
58
  }
68
59
 
69
60
  /**
70
61
  * 获取容器的尺寸和位置信息
71
62
  */
72
63
  private getContainerRect() {
73
- const { positionMode = 'fixed' } = this.props;
74
- const isFixed = positionMode === 'fixed';
75
- const container = this.getContainer();
64
+ const container = this.getContainer(false);
65
+ if (!container) {
66
+ return {
67
+ width: window.innerWidth,
68
+ height: window.innerHeight,
69
+ left: 0,
70
+ top: 0,
71
+ scrollLeft: 0,
72
+ scrollTop: 0,
73
+ scrollerScrollLeft: 0,
74
+ scrollerScrollTop: 0,
75
+ borderLeftWidth: 0,
76
+ borderTopWidth: 0
77
+ };
78
+ }
76
79
  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
- }
80
+
81
+ // 获取容器的border宽度(仅 top, left 对坐标计算有影响)
82
+ const containerStyle = window.getComputedStyle(container);
83
+ const borderLeftWidth = parseFloat(containerStyle.borderLeftWidth) || 0;
84
+ const borderTopWidth = parseFloat(containerStyle.borderTopWidth) || 0;
85
+ const borderRightWidth = parseFloat(containerStyle.borderRightWidth) || 0;
86
+ const borderBottomWidth = parseFloat(containerStyle.borderBottomWidth) || 0;
87
+ const yScrollerWidth = container.offsetWidth - container.clientWidth - borderLeftWidth - borderRightWidth;
88
+ const xScrollerHeight = container.offsetHeight - container.clientHeight - borderTopWidth - borderBottomWidth;
89
+ // console.log('yScrollerWidth, xScrollerHeight', yScrollerWidth, xScrollerHeight);
90
90
 
91
- get windowSize() {
92
- const container = this.getContainer();
93
- const { clientWidth, clientHeight } = container;
94
91
  return {
95
- height: clientHeight,
96
- width: clientWidth
92
+ width: containerRect.width / this.cachedScaleX - borderLeftWidth - borderRightWidth - yScrollerWidth,
93
+ height: containerRect.height / this.cachedScaleY - borderTopWidth - borderBottomWidth - xScrollerHeight,
94
+ left: containerRect.left / this.cachedScaleX,
95
+ top: containerRect.top / this.cachedScaleY,
96
+ scrollLeft: container.scrollLeft,
97
+ scrollTop: container.scrollTop,
98
+ borderLeftWidth: borderLeftWidth,
99
+ borderTopWidth: borderTopWidth,
97
100
  };
98
101
  }
99
102
 
@@ -113,19 +116,21 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
113
116
  get dragPositionBoundaries() {
114
117
  const { boundsX, boundsY, placement = 'bottom-right' } = this.props;
115
118
  const dragSize = this.dragBoxSize;
116
- const windowSize = this.windowSize;
119
+ const {width: containerWidth, height: containerHeight} = this.getContainerRect();
117
120
  const [placementY, placementX] = placement.split('-') as ['top' | 'bottom', 'left' | 'right'];
118
121
 
122
+ const defaultBounds = {
123
+ minX: 0,
124
+ maxX: Math.max(containerWidth - dragSize.width, 0),
125
+ minY: 0,
126
+ maxY: Math.max(containerHeight- dragSize.height, 0),
127
+ };
119
128
  // 初始化边界
120
- let minX = 0;
121
- let maxX = windowSize.width - dragSize.width;
122
- let minY = 0;
123
- let maxY = windowSize.height - dragSize.height;
129
+ let {minX, maxX, minY, maxY} = defaultBounds
124
130
 
125
131
  // 处理X轴边界
126
132
  if (boundsX) {
127
133
  const [minBound, maxBound] = boundsX;
128
-
129
134
  if (placementX === 'left') {
130
135
  // 左边位置:boundsX=[左边最小距离, 左边最大距离]
131
136
  if (minBound !== undefined) minX = Math.max(minX, minBound);
@@ -134,19 +139,14 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
134
139
  // 右边位置:boundsX=[右边最小距离, 右边最大距离]
135
140
  // 直接使用边界值作为right的限制
136
141
  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);
142
+ minX = Math.max(minX, containerWidth - maxBound - dragSize.width);
143
+ maxX = Math.min(maxX, containerWidth - minBound - dragSize.width);
139
144
  } else if (minBound !== undefined) {
140
145
  // 只有minBound:设置最大边界,最小边界保持默认
141
- maxX = Math.min(maxX, windowSize.width - minBound - dragSize.width);
146
+ maxX = Math.min(maxX, containerWidth - minBound - dragSize.width);
142
147
  } else if (maxBound !== undefined) {
143
148
  // 只有maxBound:设置最小边界,最大边界保持默认
144
- minX = Math.max(minX, windowSize.width - maxBound - dragSize.width);
145
- }
146
-
147
- // 确保最小边界不大于最大边界
148
- if (minX > maxX) {
149
- [minX, maxX] = [maxX, minX];
149
+ minX = Math.max(minX, containerWidth - maxBound - dragSize.width);
150
150
  }
151
151
  }
152
152
  }
@@ -163,31 +163,25 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
163
163
  // 底部位置:boundsY=[底边最小距离, 底边最大距离]
164
164
  // 直接使用边界值作为bottom的限制
165
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);
166
+ minY = Math.max(minY, containerHeight - maxBound - dragSize.height);
167
+ maxY = Math.min(maxY, containerHeight - minBound - dragSize.height);
168
168
  } else if (minBound !== undefined) {
169
169
  // 只有minBound:设置最大边界,最小边界保持默认
170
- maxY = Math.min(maxY, windowSize.height - minBound - dragSize.height);
170
+ maxY = Math.min(maxY, containerHeight - minBound - dragSize.height);
171
171
  } else if (maxBound !== undefined) {
172
172
  // 只有maxBound:设置最小边界,最大边界保持默认
173
- minY = Math.max(minY, windowSize.height - maxBound - dragSize.height);
174
- }
175
-
176
- // 确保最小边界不大于最大边界
177
- if (minY > maxY) {
178
- [minY, maxY] = [maxY, minY];
173
+ minY = Math.max(minY, containerHeight - maxBound - dragSize.height);
179
174
  }
180
175
  }
181
176
  }
177
+ // 确保各个边界值在默认范围内
178
+ minX = clamp(minX, defaultBounds.minX, defaultBounds.maxX);
179
+ maxX = clamp(maxX, minX, defaultBounds.maxX);
180
+ minY = clamp(minY, defaultBounds.minY, defaultBounds.maxY);
181
+ maxY = clamp(maxY, minY, defaultBounds.maxY);
182
182
 
183
183
  return { minX, maxX, minY, maxY };
184
184
  }
185
-
186
- // 保持向后兼容
187
- get dragPositionRang() {
188
- const { maxX, maxY } = this.dragPositionBoundaries;
189
- return { width: maxX, height: maxY };
190
- }
191
185
 
192
186
  get curPositionKey() {
193
187
  let {placement} = this.props;
@@ -237,18 +231,17 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
237
231
 
238
232
  reportStartPosition() {
239
233
  if (this.draggerRef) {
240
- const { top, left } = this.draggerRef.getBoundingClientRect();
241
- const containerRect = this.getContainerRect();
242
- // console.log(containerRect, 'containerRect');
243
-
244
234
  // 获取缩放比例
245
- const { scaleX, scaleY } = getScaleRatio(this.draggerRef);
235
+ const { scaleX, scaleY } = getScaleRatio(this.getContainer());
246
236
  this.cachedScaleX = scaleX;
247
237
  this.cachedScaleY = scaleY;
238
+ const { top, left } = this.draggerRef.getBoundingClientRect();
239
+ const containerRect = this.getContainerRect();
240
+ // console.log(containerRect, 'containerRect');
248
241
 
249
242
  // 计算相对于容器的位置,并除以缩放比例得到未缩放的坐标
250
- this.startTop = (top - containerRect.top + containerRect.scrollerScrollTop) / scaleY + containerRect.scrollTop;
251
- this.startLeft = (left - containerRect.left + containerRect.scrollerScrollLeft) / scaleX + containerRect.scrollLeft;
243
+ this.startTop = top / scaleY - containerRect.top + containerRect.scrollTop - containerRect.borderTopWidth;
244
+ this.startLeft = left / scaleY - containerRect.left + containerRect.scrollLeft - containerRect.borderLeftWidth;
252
245
  }
253
246
  }
254
247
 
@@ -404,20 +397,12 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
404
397
  this.dragAreaRef.style.height = '2px'; // 更细的虚线高度
405
398
  this.dragAreaRef.style.left = `${minX}px`;
406
399
  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
400
  } else if (lockAxis === 'y') {
412
401
  // 锁定X方向,显示为垂直虚线区域
413
402
  this.dragAreaRef.style.width = '2px'; // 更细的虚线宽度
414
403
  this.dragAreaRef.style.height = `${maxY - minY + dragSize.height}px`;
415
404
  this.dragAreaRef.style.left = `${this.startLeft + dragSize.width / 2}px`;
416
405
  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
406
  } else {
422
407
  // 自由拖拽,显示完整区域
423
408
  this.dragAreaRef.style.width = `${maxX - minX + dragSize.width}px`;
@@ -437,7 +422,7 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
437
422
  calcPosition = () => {
438
423
  const { lockAxis } = this.props;
439
424
  const { minX, maxX, minY, maxY } = this.dragPositionBoundaries;
440
- const containerSize = this.windowSize;
425
+ const {height, width} = this.getContainerRect();
441
426
 
442
427
  // 计算新的位置
443
428
  let newTop = this.startTop;
@@ -451,13 +436,13 @@ export class DraggableBox extends React.Component<DraggableBoxProps, DraggableBo
451
436
  }
452
437
 
453
438
  // 应用边界限制
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;
439
+ const realTop = clamp(newTop, minY, maxY);
440
+ const realLeft = clamp(newLeft, minX, maxX);
441
+ const realBottom = height - realTop - this.dragBoxSize.height;
442
+ const realRight = width - realLeft - this.dragBoxSize.width;
458
443
  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');
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');
461
446
  this.setState({
462
447
  top: realTop,
463
448
  left: realLeft,
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,26 @@ 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
+ }