Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(scroll): prevent wrong offset when the scroll is on the html element #2499

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
97 changes: 32 additions & 65 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,15 +314,26 @@ 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 offset = this.dragOffset;
style.left = e.clientX + offset.offsetLeft - containmentRect.left + 'px';
style.top = e.clientY + offset.offsetTop - containmentRect.top + 'px';
const { scaleX, scaleY } = Utils.getScaleForElement(this.helper);
const parentOfItem = Utils.getContainerOfGridStackItem(this.helper);
const transformParent = Utils.getContainerForPositionFixedElement(parentOfItem);
const scrollParent = Utils.getScrollElement(this.helper);
// We need to be careful here as the html element actually also includes scroll
// so in this case we always need to ignore it
const transformParentRect = transformParent !== document.documentElement ? transformParent.getBoundingClientRect() : { top: 0, left: 0 };
// when an element is scaled, the helper is positioned relative to the first transformed parent, so we need to remove the extra offset
const scroll = transformParent === scrollParent && transformParent !== document.documentElement
? { top: scrollParent.scrollTop, left: scrollParent.scrollLeft }
: { top: 0, left: 0 };
const offsetX = transformParentRect.left;
const offsetY = transformParentRect.top;

// Position the element under the mouse
const x = (e.clientX - offsetX - (this.origRelativeMouse?.x || 0)) / scaleX + scroll.left;
const y = (e.clientY - offsetY - (this.origRelativeMouse?.y || 0)) / scaleY + scroll.top;
style.left = `${x}px`;
style.top = `${y}px`;
}

/** @internal */
Expand All @@ -346,55 +349,19 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
}

/** @internal */
protected _getDragOffset(event: DragEvent, el: HTMLElement, parent: HTMLElement): DragOffset {

// in case ancestor has transform/perspective css properties that change the viewpoint
let xformOffsetX = 0;
let xformOffsetY = 0;
if (parent) {
const testEl = document.createElement('div');
Utils.addElStyles(testEl, {
opacity: '0',
position: 'fixed',
top: 0 + 'px',
left: 0 + 'px',
width: '1px',
height: '1px',
zIndex: '-999999',
});
parent.appendChild(testEl);
const testElPosition = testEl.getBoundingClientRect();
parent.removeChild(testEl);
xformOffsetX = testElPosition.left;
xformOffsetY = testElPosition.top;
// TODO: scale ?
}

const targetOffset = el.getBoundingClientRect();
return {
left: targetOffset.left,
top: targetOffset.top,
offsetLeft: - event.clientX + targetOffset.left - xformOffsetX,
offsetTop: - event.clientY + targetOffset.top - xformOffsetY,
width: targetOffset.width,
height: targetOffset.height
};
}

/** @internal TODO: set to public as called by DDDroppable! */
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);

const scroll = containmentEl.contains(scrollElement) ? scrollElement : { scrollTop: 0, scrollLeft: 0 };
return {
position: { //Current CSS position of the helper as { top, left } object
top: offset.top - containmentRect.top,
left: offset.left - containmentRect.left
position: { // Current CSS position of the helper as { top, left } object
top: (offset.top - containmentRect.top) / scaleY + scroll.scrollTop,
left: (offset.left - containmentRect.left) / scaleX + scroll.scrollLeft,
}
/* not used by GridStack for now...
helper: [this.helper], //The object arr representing the helper that's being dragged.
offset: { top: offset.top, left: offset.left } // Current offset position of the helper as { top, left } object.
*/
};
}
}
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
Loading