Skip to content

Commit b559a6f

Browse files
VincentMolinieVincent Molinié
and
Vincent Molinié
authored
fix(drag): always put element under the mouse when dragging an element (#2263)
* fix(drag): place the element dragged always below the mouse * fix: review fixes * fix: fix data transfer demo * fix: review fixes * fix: fix position according to first transform parent * review fixes * review fixes * review fixes * fix: fix the refacto --------- Co-authored-by: Vincent Molinié <[email protected]>
1 parent 1307172 commit b559a6f

File tree

7 files changed

+176
-89
lines changed

7 files changed

+176
-89
lines changed

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ <h1>Demos</h1>
2525
<li><a href="responsive.html">Responsive</a></li>
2626
<li><a href="right-to-left(rtl).html">Right-To-Left (RTL)</a></li>
2727
<li><a href="serialization.html">Serialization</a></li>
28+
<li><a href="scale.html">Scale</a></li>
2829
<li><a href="sizeToContent.html">Size To Content</a></li>
2930
<li><a href="static.html">Static</a></li>
3031
<li><a href="title_drag.html">Title drag</a></li>

demo/scale.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>Transform (Scale) Parent demo</title>
8+
9+
<link rel="stylesheet" href="demo.css"/>
10+
<script src="../dist/gridstack-all.js"></script>
11+
12+
</head>
13+
<body>
14+
<div class="container-fluid">
15+
<h1>Transform Parent demo</h1>
16+
<p>example where the grid parent has a scale (0.5, 0.5)</p>
17+
<div>
18+
<a class="btn btn-primary" onClick="addNewWidget()" href="#">Add Widget</a>
19+
<a class="btn btn-primary" onClick="zoomIn()" href="#">Zoom in</a>
20+
<a class="btn btn-primary" onClick="zoomOut()" href="#">Zoom out</a>
21+
</div>
22+
<br><br>
23+
<div style="transform: scale(var(--global-scale), var(--global-scale)); transform-origin: 0 0;">
24+
<div class="grid-stack"></div>
25+
</div>
26+
</div>
27+
<script src="events.js"></script>
28+
<script type="text/javascript">
29+
let scale = 0.5;
30+
31+
let grid = GridStack.init({float: true});
32+
addEvents(grid);
33+
34+
let items = [
35+
{x: 0, y: 0, w: 2, h: 2},
36+
{x: 2, y: 0, w: 1},
37+
{x: 3, y: 0, h: 1},
38+
{x: 0, y: 2, w: 2},
39+
];
40+
let count = 0;
41+
42+
getNode = function() {
43+
let n = items[count] || {
44+
x: Math.round(12 * Math.random()),
45+
y: Math.round(5 * Math.random()),
46+
w: Math.round(1 + 3 * Math.random()),
47+
h: Math.round(1 + 3 * Math.random())
48+
};
49+
n.content = n.content || String(count);
50+
count++;
51+
return n;
52+
};
53+
54+
addNewWidget = function() {
55+
let w = grid.addWidget(getNode());
56+
};
57+
58+
const updateScaleCssVariable = () => {
59+
document.body.style.setProperty('--global-scale', `${scale}`);
60+
}
61+
62+
zoomIn = function() {
63+
const scaleStep = scale < 1 ? 0.05 : 0.1;
64+
scale += scaleStep;
65+
updateScaleCssVariable();
66+
}
67+
68+
zoomOut = function() {
69+
const scaleStep = scale < 1 ? 0.05 : 0.1;
70+
scale -= scaleStep;
71+
updateScaleCssVariable();
72+
}
73+
74+
updateScaleCssVariable();
75+
76+
77+
addNewWidget();
78+
addNewWidget();
79+
addNewWidget();
80+
</script>
81+
</body>
82+
</html>

demo/web2.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
<!-- support for IE -->
1717
<script src="../dist/es5/gridstack-poly.js"></script>
18-
<script src="../dist/es5/gridstack.js"></script>
18+
<script src="../dist/es5/gridstack-all.js"></script>
1919

2020
<style type="text/css">
2121
.grid-stack-item-removing {
@@ -89,4 +89,4 @@ <h1>Advanced Demo</h1>
8989
</script>
9090
</body>
9191

92-
</html>
92+
</html>

src/dd-draggable.ts

Lines changed: 29 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,6 @@ export interface DDDraggableOpt {
2424
drag?: (event: Event, ui: DDUIData) => void;
2525
}
2626

27-
interface DragOffset {
28-
left: number;
29-
top: number;
30-
width: number;
31-
height: number;
32-
offsetLeft: number;
33-
offsetTop: number;
34-
}
35-
3627
type DDDragEvent = 'drag' | 'dragstart' | 'dragstop';
3728

3829
// make sure we are not clicking on known object that handles mouseDown
@@ -48,8 +39,6 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
4839
/** @internal */
4940
protected mouseDownEvent: MouseEvent;
5041
/** @internal */
51-
protected dragOffset: DragOffset;
52-
/** @internal */
5342
protected dragElementOriginStyle: Array<string>;
5443
/** @internal */
5544
protected dragEl: HTMLElement;
@@ -63,6 +52,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
6352
protected static originStyleProp = ['transition', 'pointerEvents', 'position', 'left', 'top', 'minWidth', 'willChange'];
6453
/** @internal pause before we call the actual drag hit collision code */
6554
protected dragTimeout: number;
55+
protected origRelativeMouse: { x: number; y: number; };
6656

6757
constructor(el: HTMLElement, option: DDDraggableOpt = {}) {
6858
super();
@@ -205,9 +195,10 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
205195
} else {
206196
delete DDManager.dropElement;
207197
}
198+
const rect = this.el.getBoundingClientRect();
199+
this.origRelativeMouse = { x: s.clientX - rect.left, y: s.clientY - rect.top };
208200
this.helper = this._createHelper(e);
209201
this._setupHelperContainmentStyle();
210-
this.dragOffset = this._getDragOffset(e, this.el, this.helperContainment);
211202
const ev = Utils.initEvent<DragEvent>(e, { target: this.el, type: 'dragstart' });
212203

213204
this._setupHelperStyle(e);
@@ -285,8 +276,9 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
285276
const style = this.helper.style;
286277
style.pointerEvents = 'none'; // needed for over items to get enter/leave
287278
// style.cursor = 'move'; // TODO: can't set with pointerEvents=none ! (done in CSS as well)
288-
style.width = this.dragOffset.width + 'px';
289-
style.height = this.dragOffset.height + 'px';
279+
style.width = this.el.offsetWidth + 'px';
280+
style.height = this.el.offsetHeight + 'px';
281+
290282
style.willChange = 'left, top';
291283
style.position = 'fixed'; // let us drag between grids by not clipping as parent .grid-stack is position: 'relative'
292284
this._dragFollow(e); // now position it
@@ -322,15 +314,19 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
322314

323315
/** @internal updates the top/left position to follow the mouse */
324316
protected _dragFollow(e: DragEvent): void {
325-
let containmentRect = { left: 0, top: 0 };
326-
// if (this.helper.style.position === 'absolute') { // we use 'fixed'
327-
// const { left, top } = this.helperContainment.getBoundingClientRect();
328-
// containmentRect = { left, top };
329-
// }
330317
const style = this.helper.style;
331-
const offset = this.dragOffset;
332-
style.left = e.clientX + offset.offsetLeft - containmentRect.left + 'px';
333-
style.top = e.clientY + offset.offsetTop - containmentRect.top + 'px';
318+
const { scaleX, scaleY } = Utils.getScaleForElement(this.helper);
319+
const transformParent = Utils.getContainerForPositionFixedElement(this.helper);
320+
const transformParentRect = transformParent.getBoundingClientRect();
321+
// when an element is scaled, the helper is positioned relative to the first transformed parent, so we need to remove the extra offset
322+
const offsetX = transformParentRect.left;
323+
const offsetY = transformParentRect.top;
324+
325+
// Position the element under the mouse
326+
const x = (e.clientX - offsetX - (this.origRelativeMouse?.x || 0)) / scaleX;
327+
const y = (e.clientY - offsetY - (this.origRelativeMouse?.y || 0)) / scaleY;
328+
style.left = `${x}px`;
329+
style.top = `${y}px`;
334330
}
335331

336332
/** @internal */
@@ -345,51 +341,23 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
345341
return this;
346342
}
347343

348-
/** @internal */
349-
protected _getDragOffset(event: DragEvent, el: HTMLElement, parent: HTMLElement): DragOffset {
350-
351-
// in case ancestor has transform/perspective css properties that change the viewpoint
352-
let xformOffsetX = 0;
353-
let xformOffsetY = 0;
354-
if (parent) {
355-
const testEl = document.createElement('div');
356-
Utils.addElStyles(testEl, {
357-
opacity: '0',
358-
position: 'fixed',
359-
top: 0 + 'px',
360-
left: 0 + 'px',
361-
width: '1px',
362-
height: '1px',
363-
zIndex: '-999999',
364-
});
365-
parent.appendChild(testEl);
366-
const testElPosition = testEl.getBoundingClientRect();
367-
parent.removeChild(testEl);
368-
xformOffsetX = testElPosition.left;
369-
xformOffsetY = testElPosition.top;
370-
// TODO: scale ?
371-
}
372-
373-
const targetOffset = el.getBoundingClientRect();
374-
return {
375-
left: targetOffset.left,
376-
top: targetOffset.top,
377-
offsetLeft: - event.clientX + targetOffset.left - xformOffsetX,
378-
offsetTop: - event.clientY + targetOffset.top - xformOffsetY,
379-
width: targetOffset.width,
380-
height: targetOffset.height
381-
};
382-
}
383-
384344
/** @internal TODO: set to public as called by DDDroppable! */
385345
public ui(): DDUIData {
386346
const containmentEl = this.el.parentElement;
347+
const scrollElement = Utils.getScrollElement(this.el.parentElement);
387348
const containmentRect = containmentEl.getBoundingClientRect();
388349
const offset = this.helper.getBoundingClientRect();
350+
const { scaleX, scaleY } = Utils.getScaleForElement(this.helper);
351+
352+
// When an element is inside a scrolled element, the boundingClientRect will return the position of the element minus the scroll.
353+
const parentPositionIncludingScroll = containmentEl === scrollElement
354+
? { top: containmentRect.top + scrollElement.scrollTop, left: containmentRect.left + scrollElement.scrollLeft }
355+
: { top: containmentRect.top, left: containmentRect.left };
356+
389357
return {
390-
position: { //Current CSS position of the helper as { top, left } object
391-
top: offset.top - containmentRect.top,
392-
left: offset.left - containmentRect.left
358+
position: { // Current CSS position of the helper as { top, left } object
359+
top: (offset.top - parentPositionIncludingScroll.top) / scaleY,
360+
left: (offset.left - parentPositionIncludingScroll.left) / scaleX,
393361
}
394362
/* not used by GridStack for now...
395363
helper: [this.helper], //The object arr representing the helper that's being dragged.

src/dd-resizable.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,14 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt
237237
/** @internal */
238238
protected _getChange(event: MouseEvent, dir: string): Rect {
239239
const oEvent = this.startEvent;
240+
const containerElement = Utils.getPositionContainerElement(this.el.parentElement);
241+
const containerRect = containerElement.getBoundingClientRect();
242+
240243
const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out.
241244
width: this.originalRect.width,
242245
height: this.originalRect.height + this.scrolled,
243-
left: this.originalRect.left,
244-
top: this.originalRect.top - this.scrolled
246+
left: this.originalRect.left - containerRect.left,
247+
top: this.originalRect.top - this.scrolled - containerRect.top
245248
};
246249

247250
const offsetX = event.clientX - oEvent.clientX;
@@ -277,28 +280,25 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt
277280

278281
/** @internal constrain the size to the set min/max values */
279282
protected _constrainSize(oWidth: number, oHeight: number): Size {
280-
const maxWidth = this.option.maxWidth || Number.MAX_SAFE_INTEGER;
281-
const minWidth = this.option.minWidth || oWidth;
282-
const maxHeight = this.option.maxHeight || Number.MAX_SAFE_INTEGER;
283-
const minHeight = this.option.minHeight || oHeight;
283+
const { scaleX, scaleY } = Utils.getScaleForElement(this.el);
284+
const o = this.option;
285+
const maxWidth = o.maxWidth ? o.maxWidth * scaleX : Number.MAX_SAFE_INTEGER;
286+
const minWidth = o.minWidth ? o.minWidth * scaleX : oWidth;
287+
const maxHeight = o.maxHeight ? o.maxHeight * scaleY : Number.MAX_SAFE_INTEGER;
288+
const minHeight = o.minHeight ? o.minHeight * scaleY : oHeight;
284289
const width = Math.min(maxWidth, Math.max(minWidth, oWidth));
285290
const height = Math.min(maxHeight, Math.max(minHeight, oHeight));
286291
return { width, height };
287292
}
288293

289294
/** @internal */
290295
protected _applyChange(): DDResizable {
291-
let containmentRect = { left: 0, top: 0, width: 0, height: 0 };
292-
if (this.el.style.position === 'absolute') {
293-
const containmentEl = this.el.parentElement;
294-
const { left, top } = containmentEl.getBoundingClientRect();
295-
containmentRect = { left, top, width: 0, height: 0 };
296-
}
297296
if (!this.temporalRect) return this;
298-
Object.keys(this.temporalRect).forEach(key => {
299-
const value = this.temporalRect[key];
300-
this.el.style[key] = value - containmentRect[key] + 'px';
301-
});
297+
const { scaleX, scaleY } = Utils.getScaleForElement(this.el);
298+
this.el.style.width = `${Math.round(this.temporalRect.width / scaleX)}px`;
299+
this.el.style.height = `${Math.round(this.temporalRect.height / scaleY)}px`;
300+
this.el.style.top = `${Math.round(this.temporalRect.top / scaleY)}px`;
301+
this.el.style.left = `${Math.round(this.temporalRect.left / scaleX)}px`;
302302
return this;
303303
}
304304

@@ -311,23 +311,22 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt
311311

312312
/** @internal */
313313
protected _ui = (): DDUIData => {
314-
const containmentEl = this.el.parentElement;
315-
const containmentRect = containmentEl.getBoundingClientRect();
314+
const { scaleX, scaleY } = Utils.getScaleForElement(this.el);
316315
const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out.
317316
width: this.originalRect.width,
318-
height: this.originalRect.height + this.scrolled,
317+
height: (this.originalRect.height + this.scrolled),
319318
left: this.originalRect.left,
320-
top: this.originalRect.top - this.scrolled
319+
top: (this.originalRect.top - this.scrolled)
321320
};
322321
const rect = this.temporalRect || newRect;
323322
return {
324323
position: {
325-
left: rect.left - containmentRect.left,
326-
top: rect.top - containmentRect.top
324+
left: rect.left / scaleX,
325+
top: rect.top / scaleY,
327326
},
328327
size: {
329-
width: rect.width,
330-
height: rect.height
328+
width: rect.width / scaleX,
329+
height: rect.height / scaleY,
331330
}
332331
/* Gridstack ONLY needs position set above... keep around in case.
333332
element: [this.el], // The object representing the element to be resized

src/gridstack.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,9 +1944,10 @@ export class GridStack {
19441944

19451945
helper = helper || el;
19461946
let parent = this.el.getBoundingClientRect();
1947+
const { scaleX, scaleY } = Utils.getScaleForElement(helper);
19471948
let {top, left} = helper.getBoundingClientRect();
1948-
left -= parent.left;
1949-
top -= parent.top;
1949+
left = (left - parent.left) / scaleX;
1950+
top = (top - parent.top) / scaleY;
19501951
let ui: DDUIData = {position: {top, left}};
19511952

19521953
if (node._temporaryRemoved) {

0 commit comments

Comments
 (0)