diff --git a/demo/index.html b/demo/index.html
index bbe2ee101..b909d5466 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -25,6 +25,7 @@
Demos
Responsive
Right-To-Left (RTL)
Serialization
+ Scale
Size To Content
Static
Title drag
diff --git a/demo/scale.html b/demo/scale.html
new file mode 100644
index 000000000..2a900fe10
--- /dev/null
+++ b/demo/scale.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+ Transform (Scale) Parent demo
+
+
+
+
+
+
+
+
Transform Parent demo
+
example where the grid parent has a scale (0.5, 0.5)
+
+
+
+
+
+
+
+
diff --git a/src/dd-draggable.ts b/src/dd-draggable.ts
index e19432a7d..a7deda747 100644
--- a/src/dd-draggable.ts
+++ b/src/dd-draggable.ts
@@ -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
@@ -48,8 +39,6 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
/** @internal */
protected mouseDownEvent: MouseEvent;
/** @internal */
- protected dragOffset: DragOffset;
- /** @internal */
protected dragElementOriginStyle: Array;
/** @internal */
protected dragEl: HTMLElement;
@@ -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();
@@ -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(e, { target: this.el, type: 'dragstart' });
this._setupHelperStyle(e);
@@ -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
@@ -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 */
@@ -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.
- */
};
}
}
diff --git a/src/dd-resizable.ts b/src/dd-resizable.ts
index a540df51e..649fdc26f 100644
--- a/src/dd-resizable.ts
+++ b/src/dd-resizable.ts
@@ -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;
@@ -277,10 +280,12 @@ 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 };
@@ -288,17 +293,12 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt
/** @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;
}
@@ -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
diff --git a/src/gridstack.ts b/src/gridstack.ts
index f13062225..1e7c77873 100644
--- a/src/gridstack.ts
+++ b/src/gridstack.ts
@@ -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) {
diff --git a/src/utils.ts b/src/utils.ts
index d2c72e0f3..dc2a337cf 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -373,6 +373,34 @@ 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 getContainerOfGridStackItem(el: HTMLElement): HTMLElement {
+ if (!el) return null;
+
+ return el.classList.contains('grid-stack-item')
+ ? el.parentElement
+ : Utils.getContainerOfGridStackItem(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?
@@ -553,6 +581,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)