Skip to content

Commit

Permalink
fix(drag): always put element under the mouse when dragging an element (
Browse files Browse the repository at this point in the history
#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é <vincent.m@forestadmin.com>
  • Loading branch information
VincentMolinie and Vincent Molinié committed Oct 16, 2023
1 parent 25d58e9 commit cc005fc
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 45 deletions.
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ <h1>Demos</h1>
<li><a href="responsive.html">Responsive</a></li>
<li><a href="right-to-left(rtl).html">Right-To-Left (RTL)</a></li>
<li><a href="serialization.html">Serialization</a></li>
<li><a href="scale.html">Scale</a></li>
<li><a href="sizeToContent.html">Size To Content</a></li>
<li><a href="static.html">Static</a></li>
<li><a href="title_drag.html">Title drag</a></li>
Expand Down
82 changes: 82 additions & 0 deletions demo/scale.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Transform (Scale) Parent demo</title>

<link rel="stylesheet" href="demo.css"/>
<script src="../dist/gridstack-all.js"></script>

</head>
<body>
<div class="container-fluid">
<h1>Transform Parent demo</h1>
<p>example where the grid parent has a scale (0.5, 0.5)</p>
<div>
<a class="btn btn-primary" onClick="addNewWidget()" href="#">Add Widget</a>
<a class="btn btn-primary" onClick="zoomIn()" href="#">Zoom in</a>
<a class="btn btn-primary" onClick="zoomOut()" href="#">Zoom out</a>
</div>
<br><br>
<div style="transform: scale(var(--global-scale), var(--global-scale)); transform-origin: 0 0;">
<div class="grid-stack"></div>
</div>
</div>
<script src="events.js"></script>
<script type="text/javascript">
let scale = 0.5;

let grid = GridStack.init({float: true});
addEvents(grid);

let items = [
{x: 0, y: 0, w: 2, h: 2},
{x: 2, y: 0, w: 1},
{x: 3, y: 0, h: 1},
{x: 0, y: 2, w: 2},
];
let count = 0;

getNode = function() {
let n = items[count] || {
x: Math.round(12 * Math.random()),
y: Math.round(5 * Math.random()),
w: Math.round(1 + 3 * Math.random()),
h: Math.round(1 + 3 * Math.random())
};
n.content = n.content || String(count);
count++;
return n;
};

addNewWidget = function() {
let w = grid.addWidget(getNode());
};

const updateScaleCssVariable = () => {
document.body.style.setProperty('--global-scale', `${scale}`);
}

zoomIn = function() {
const scaleStep = scale < 1 ? 0.05 : 0.1;
scale += scaleStep;
updateScaleCssVariable();
}

zoomOut = function() {
const scaleStep = scale < 1 ? 0.05 : 0.1;
scale -= scaleStep;
updateScaleCssVariable();
}

updateScaleCssVariable();


addNewWidget();
addNewWidget();
addNewWidget();
</script>
</body>
</html>
26 changes: 7 additions & 19 deletions src/dd-draggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ export interface DDDraggableOpt {
drag?: (event: Event, ui: DDUIData) => void;
}

interface DragOffset {
left: number;
top: number;
width: number;
height: number;
offsetLeft: number;
offsetTop: number;
}

type DDDragEvent = 'drag' | 'dragstart' | 'dragstop';

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

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

this._setupHelperStyle(e);
Expand Down Expand Up @@ -285,8 +276,9 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
const style = this.helper.style;
style.pointerEvents = 'none'; // needed for over items to get enter/leave
// style.cursor = 'move'; // TODO: can't set with pointerEvents=none ! (done in CSS as well)
style.width = this.dragOffset.width + 'px';
style.height = this.dragOffset.height + 'px';
style.width = this.el.offsetWidth + 'px';
style.height = this.el.offsetHeight + 'px';

style.willChange = 'left, top';
style.position = 'fixed'; // let us drag between grids by not clipping as parent .grid-stack is position: 'relative'
this._dragFollow(e); // now position it
Expand Down Expand Up @@ -322,11 +314,6 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt

/** @internal updates the top/left position to follow the mouse */
protected _dragFollow(e: DragEvent): void {
let containmentRect = { left: 0, top: 0 };
// if (this.helper.style.position === 'absolute') { // we use 'fixed'
// const { left, top } = this.helperContainment.getBoundingClientRect();
// containmentRect = { left, top };
// }
const style = this.helper.style;
const { scaleX, scaleY } = Utils.getScaleForElement(this.helper);
const transformParent = Utils.getContainerForPositionFixedElement(this.helper);
Expand Down Expand Up @@ -363,6 +350,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
/** @internal */
public ui(): DDUIData {
const containmentEl = this.el.parentElement;
const scrollElement = Utils.getScrollElement(this.el.parentElement);
const containmentRect = containmentEl.getBoundingClientRect();
const offset = this.helper.getBoundingClientRect();
const { scaleX, scaleY } = Utils.getScaleForElement(this.helper);
Expand Down
47 changes: 23 additions & 24 deletions src/dd-resizable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,14 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt
/** @internal */
protected _getChange(event: MouseEvent, dir: string): Rect {
const oEvent = this.startEvent;
const containerElement = Utils.getPositionContainerElement(this.el.parentElement);
const containerRect = containerElement.getBoundingClientRect();

const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out.
width: this.originalRect.width,
height: this.originalRect.height + this.scrolled,
left: this.originalRect.left,
top: this.originalRect.top - this.scrolled
left: this.originalRect.left - containerRect.left,
top: this.originalRect.top - this.scrolled - containerRect.top
};

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

/** @internal constrain the size to the set min/max values */
protected _constrainSize(oWidth: number, oHeight: number): Size {
const maxWidth = this.option.maxWidth || Number.MAX_SAFE_INTEGER;
const minWidth = this.option.minWidth || oWidth;
const maxHeight = this.option.maxHeight || Number.MAX_SAFE_INTEGER;
const minHeight = this.option.minHeight || oHeight;
const { scaleX, scaleY } = Utils.getScaleForElement(this.el);
const o = this.option;
const maxWidth = o.maxWidth ? o.maxWidth * scaleX : Number.MAX_SAFE_INTEGER;
const minWidth = o.minWidth ? o.minWidth * scaleX : oWidth;
const maxHeight = o.maxHeight ? o.maxHeight * scaleY : Number.MAX_SAFE_INTEGER;
const minHeight = o.minHeight ? o.minHeight * scaleY : oHeight;
const width = Math.min(maxWidth, Math.max(minWidth, oWidth));
const height = Math.min(maxHeight, Math.max(minHeight, oHeight));
return { width, height };
}

/** @internal */
protected _applyChange(): DDResizable {
let containmentRect = { left: 0, top: 0, width: 0, height: 0 };
if (this.el.style.position === 'absolute') {
const containmentEl = this.el.parentElement;
const { left, top } = containmentEl.getBoundingClientRect();
containmentRect = { left, top, width: 0, height: 0 };
}
if (!this.temporalRect) return this;
Object.keys(this.temporalRect).forEach(key => {
const value = this.temporalRect[key];
this.el.style[key] = value - containmentRect[key] + 'px';
});
const { scaleX, scaleY } = Utils.getScaleForElement(this.el);
this.el.style.width = `${Math.round(this.temporalRect.width / scaleX)}px`;
this.el.style.height = `${Math.round(this.temporalRect.height / scaleY)}px`;
this.el.style.top = `${Math.round(this.temporalRect.top / scaleY)}px`;
this.el.style.left = `${Math.round(this.temporalRect.left / scaleX)}px`;
return this;
}

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

/** @internal */
protected _ui = (): DDUIData => {
const containmentEl = this.el.parentElement;
const containmentRect = containmentEl.getBoundingClientRect();
const { scaleX, scaleY } = Utils.getScaleForElement(this.el);
const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out.
width: this.originalRect.width,
height: this.originalRect.height + this.scrolled,
height: (this.originalRect.height + this.scrolled),
left: this.originalRect.left,
top: this.originalRect.top - this.scrolled
top: (this.originalRect.top - this.scrolled)
};
const rect = this.temporalRect || newRect;
return {
position: {
left: rect.left - containmentRect.left,
top: rect.top - containmentRect.top
left: rect.left / scaleX,
top: rect.top / scaleY,
},
size: {
width: rect.width,
height: rect.height
width: rect.width / scaleX,
height: rect.height / scaleY,
}
/* Gridstack ONLY needs position set above... keep around in case.
element: [this.el], // The object representing the element to be resized
Expand Down
5 changes: 3 additions & 2 deletions src/gridstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1964,9 +1964,10 @@ export class GridStack {

helper = helper || el;
let parent = this.el.getBoundingClientRect();
const { scaleX, scaleY } = Utils.getScaleForElement(helper);
let {top, left} = helper.getBoundingClientRect();
left -= parent.left;
top -= parent.top;
left = (left - parent.left) / scaleX;
top = (top - parent.top) / scaleY;
let ui: DDUIData = {position: {top, left}};

if (node._temporaryRemoved) {
Expand Down
36 changes: 36 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,26 @@ export class Utils {
}
}

static getPositionContainerElement(el: HTMLElement): HTMLElement {
if (!el) return null;

const style = getComputedStyle(el);

if (style.position === 'relative' || style.position === 'absolute' || style.position === 'fixed') {
return el;
} else {
return Utils.getPositionContainerElement(el.parentElement);
}
}

static getContainerForPositionFixedElement(el: HTMLElement): HTMLElement {
while (el !== document.documentElement && el.parentElement && getComputedStyle(el as HTMLElement).transform === 'none') {
el = el.parentElement;
}

return el;
}

/** @internal */
static updateScrollPosition(el: HTMLElement, position: {top: number}, distance: number): void {
// is widget in view?
Expand Down Expand Up @@ -553,6 +573,22 @@ export class Utils {
(target || e.target).dispatchEvent(simulatedEvent);
}

public static getScaleForElement(element: HTMLElement) {
// Check if element is visible, otherwise the width/height will be of 0
while (element && !element.offsetParent) {
element = element.parentElement;
}

if (!element) {
return { scaleX: 1, scaleY: 1 };
}

const boundingClientRect = element.getBoundingClientRect();
const scaleX = boundingClientRect.width / element.offsetWidth;
const scaleY = boundingClientRect.height / element.offsetHeight;
return { scaleX, scaleY };
}

/** returns true if event is inside the given element rectangle */
// Note: Safari Mac has null event.relatedTarget which causes #1684 so check if DragEvent is inside the coordinates instead
// this.el.contains(event.relatedTarget as HTMLElement)
Expand Down

0 comments on commit cc005fc

Please sign in to comment.