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)

    +
    + Add Widget + Zoom in + Zoom out +
    +

    +
    +
    +
    +
    + + + + 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)