diff --git a/src/dd-draggable.ts b/src/dd-draggable.ts index 76515a388..d8946fb46 100644 --- a/src/dd-draggable.ts +++ b/src/dd-draggable.ts @@ -12,11 +12,11 @@ import { isTouch, touchend, touchmove, touchstart, pointerdown, DDTouch } from ' import { GridHTMLElement } from './gridstack'; interface DragOffset { - left: number; + x: number; top: number; width: number; height: number; - offsetLeft: number; + offsetX: number; offsetTop: number; } @@ -51,7 +51,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt /** @internal */ protected helperContainment: HTMLElement; /** @internal properties we change during dragging, and restore back */ - protected static originStyleProp = ['width', 'height', 'transform', 'transform-origin', 'transition', 'pointerEvents', 'position', 'left', 'top', 'minWidth', 'willChange']; + protected static originStyleProp = ['width', 'height', 'transform', 'transform-origin', 'transition', 'pointerEvents', 'position', 'left', 'right', 'top', 'minWidth', 'willChange']; /** @internal pause before we call the actual drag hit collision code */ protected dragTimeout: number; /** @internal */ @@ -289,7 +289,10 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt n._origRotate = n._origRotate || { ...n._orig }; // store the real orig size in case we Esc after doing rotation delete n._moving; // force rotate to happen (move waits for >50% coverage otherwise) grid.setAnimation(false) // immediate rotate so _getDragOffset() gets the right dom size below - .rotate(n.el, { top: -this.dragOffset.offsetTop, left: -this.dragOffset.offsetLeft }) + .rotate(n.el, { + top: -this.dragOffset.offsetTop, + left: -this.dragOffset.offsetX + }) .setAnimation(); n._moving = true; this.dragOffset = this._getDragOffset(this.lastDrag, n.el, this.helperContainment); @@ -326,7 +329,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt // style.cursor = 'move'; // TODO: can't set with pointerEvents=none ! (no longer in CSS either as no-op) style.width = this.dragOffset.width + 'px'; style.height = this.dragOffset.height + 'px'; - style.willChange = 'left, top'; + style.willChange = 'left, right, 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 style.transition = 'none'; // show up instantly @@ -362,15 +365,18 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt /** @internal updates the top/left position to follow the mouse */ public _dragFollow(e: DragEvent): void { - const 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) * this.dragTransform.xScale + 'px'; - style.top = (e.clientY + offset.offsetTop - containmentRect.top) * this.dragTransform.yScale + 'px'; + if (this.option.rtl) { + style.right = ((window.innerWidth - e.clientX) + offset.offsetX) * this.dragTransform.xScale + 'px'; + if (style.left) + style.left = ''; + } else { + style.left = (e.clientX + offset.offsetX) * this.dragTransform.xScale + 'px'; + if (style.right) + style.right = ''; + } + style.top = (e.clientY + offset.offsetTop) * this.dragTransform.yScale + 'px'; } /** @internal */ @@ -397,10 +403,15 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt } const targetOffset = el.getBoundingClientRect(); + let x = this.option.rtl ? targetOffset.right : targetOffset.left; + let offsetX = this.option.rtl + ? (event.clientX - targetOffset.right + xformOffsetX) + : (-event.clientX + targetOffset.left - xformOffsetX); + return { - left: targetOffset.left, + x, top: targetOffset.top, - offsetLeft: - event.clientX + targetOffset.left - xformOffsetX, + offsetX, offsetTop: - event.clientY + targetOffset.top - xformOffsetY, width: targetOffset.width * this.dragTransform.xScale, height: targetOffset.height * this.dragTransform.yScale @@ -473,10 +484,18 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt const containmentEl = this.el.parentElement; const containmentRect = containmentEl.getBoundingClientRect(); const offset = this.helper.getBoundingClientRect(); + + // RTL: GridStack measures column positions from the right side of the container, + // so we report `left` as the distance between the helper's right edge and the + // container's right edge (both in viewport-left coordinates via getBoundingClientRect). + const leftPos = this.option.rtl + ? (containmentRect.right - offset.right) * this.dragTransform.xScale + : (offset.left - containmentRect.left) * this.dragTransform.xScale; + return { position: { //Current CSS position of the helper as { top, left } object top: (offset.top - containmentRect.top) * this.dragTransform.yScale, - left: (offset.left - containmentRect.left) * this.dragTransform.xScale + left: leftPos } /* not used by GridStack for now... helper: [this.helper], //The object arr representing the helper that's being dragged. diff --git a/src/dd-gridstack.ts b/src/dd-gridstack.ts index 35cac4251..9f249f2f3 100644 --- a/src/dd-gridstack.ts +++ b/src/dd-gridstack.ts @@ -96,7 +96,8 @@ export class DDGridStack { ...{ start: opts.start, stop: opts.stop, - resize: opts.resize + resize: opts.resize, + rtl: opts.rtl, } }); } @@ -111,6 +112,7 @@ export class DDGridStack { * @param opts - Drag options or command ('enable', 'disable', 'destroy', 'option', or config object) * @param key - Option key when using 'option' command * @param value - Option value when using 'option' command + * @param rtl - Are we in rtl mode? * @returns this instance for chaining * * @example @@ -133,7 +135,8 @@ export class DDGridStack { // containment: (grid.parentGridNode && grid.opts.dragOut === false) ? grid.el.parentElement : (grid.opts.draggable.containment || null), start: opts.start, stop: opts.stop, - drag: opts.drag + drag: opts.drag, + rtl: opts.rtl, } }); } diff --git a/src/dd-resizable.ts b/src/dd-resizable.ts index ea547257a..4f782852f 100644 --- a/src/dd-resizable.ts +++ b/src/dd-resizable.ts @@ -6,7 +6,7 @@ import { DDResizableHandle } from './dd-resizable-handle'; import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl'; import { Utils } from './utils'; -import { DDResizeOpt, DDUIData, GridItemHTMLElement, GridStackMouseEvent, Rect, Size } from './types'; +import { DDResizeOpt, DDUIData, GridItemHTMLElement, GridStackMouseEvent, Size } from './types'; import { DDManager } from './dd-manager'; // import { GridItemHTMLElement } from './types'; let count = 0; // TEST @@ -22,6 +22,7 @@ export interface DDResizableOpt extends DDResizeOpt { start?: (event: Event, ui: DDUIData) => void; stop?: (event: Event) => void; resize?: (event: Event, ui: DDUIData) => void; + rtl?: boolean; } interface RectScaleReciprocal { @@ -29,15 +30,23 @@ interface RectScaleReciprocal { y: number; } +interface TemporalRect { + width: number; + height: number; + left: number; + right: number; + top: number; +} + export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt { /** @internal */ protected handlers: DDResizableHandle[]; /** @internal */ - protected originalRect: Rect; + protected originalRect: DOMRectReadOnly; /** @internal */ protected rectScale: RectScaleReciprocal = { x: 1, y: 1 }; /** @internal */ - protected temporalRect: Rect; + protected temporalRect: TemporalRect; /** @internal */ protected scrollY: number; /** @internal */ @@ -51,7 +60,7 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt /** @internal */ protected parentOriginStylePosition: string; /** @internal */ - protected static _originStyleProp = ['width', 'height', 'position', 'left', 'top', 'opacity', 'zIndex']; + protected static _originStyleProp = ['width', 'height', 'position', 'left', 'right', 'top', 'opacity', 'zIndex']; /** @internal */ protected sizeToContent: boolean; @@ -185,6 +194,8 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt this._applyChange(); const ev = Utils.initEvent(event, { type: 'resize', target: this.el }); ev.resizeDir = dir; // expose handle direction so _dragOrResize can avoid position drift + ev.hasMovedX = this.option.rtl ? dir.includes('e') : dir.includes('w'); + ev.hasMovedY = dir.includes('n'); if (this.option.resize) { this.option.resize(ev, this._ui()); } @@ -240,13 +251,14 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt } /** @internal */ - protected _getChange(event: MouseEvent, dir: string): Rect { + protected _getChange(event: MouseEvent, dir: string): TemporalRect { const oEvent = this.startEvent; 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 + right: this.originalRect.right, + top: this.originalRect.top - this.scrolled, }; const offsetX = event.clientX - oEvent.clientX; @@ -254,13 +266,22 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt let moveLeft: boolean; let moveUp: boolean; - if (dir.indexOf('e') > -1) { + const isRtl = this.option.rtl; + + if (!isRtl && dir.indexOf('e') > -1) { newRect.width += offsetX; - } else if (dir.indexOf('w') > -1) { + } else if (isRtl && dir.indexOf('w') > -1) { + newRect.width -= offsetX; + } else if (!isRtl && dir.indexOf('w') > -1) { newRect.width -= offsetX; newRect.left += offsetX; moveLeft = true; + } else if (isRtl && dir.indexOf('e') > -1) { + newRect.width += offsetX; + newRect.right += offsetX; + moveLeft = true; } + if (dir.indexOf('s') > -1) { newRect.height += offsetY; } else if (dir.indexOf('n') > -1) { @@ -268,10 +289,13 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt newRect.top += offsetY moveUp = true; } + const constrain = this._constrainSize(newRect.width, newRect.height, moveLeft, moveUp); if (Math.round(newRect.width) !== Math.round(constrain.width)) { // round to ignore slight round-off errors - if (dir.indexOf('w') > -1) { + if (!isRtl && dir.indexOf('w') > -1) { newRect.left += newRect.width - constrain.width; + } else if (isRtl && dir.indexOf('e') > -1) { + newRect.right -= newRect.width - constrain.width; } newRect.width = constrain.width; } @@ -298,17 +322,29 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt /** @internal */ protected _applyChange(): DDResizable { - let containmentRect = { left: 0, top: 0, width: 0, height: 0 }; + let containmentRect = { left: 0, right: 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 }; + const { left, right, top } = containmentEl.getBoundingClientRect(); + containmentRect = { left, right, top, width: 0, height: 0 }; } if (!this.temporalRect) return this; - Object.keys(this.temporalRect).forEach(key => { - const value = this.temporalRect[key]; - const scaleReciprocal = key === 'width' || key === 'left' ? this.rectScale.x : key === 'height' || key === 'top' ? this.rectScale.y : 1; - this.el.style[key] = (value - containmentRect[key]) * scaleReciprocal + 'px'; + Object.entries(this.temporalRect).forEach(([key, value]) => { + if (this.option.rtl ? key === 'left' : key === 'right') + return; + + const scaleReciprocal = key === 'width' || key === 'left' || key === 'right' + ? this.rectScale.x + : key === 'height' || key === 'top' + ? this.rectScale.y + : 1; + let finalValue: string; + if (key === 'right') { + finalValue = (containmentRect.right - value) * this.rectScale.x + 'px'; + } else { + finalValue = (value - containmentRect[key]) * scaleReciprocal + 'px'; + } + this.el.style[key] = finalValue; }); return this; } @@ -328,12 +364,17 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt width: this.originalRect.width, height: this.originalRect.height + this.scrolled, left: this.originalRect.left, + right: this.originalRect.right, top: this.originalRect.top - this.scrolled }; const rect = this.temporalRect || newRect; + + const leftPos = this.option.rtl + ? (containmentRect.right - rect.right) * this.rectScale.x + : (rect.left - containmentRect.left) * this.rectScale.x; return { position: { - left: (rect.left - containmentRect.left) * this.rectScale.x, + left: leftPos, top: (rect.top - containmentRect.top) * this.rectScale.y }, size: { diff --git a/src/gridstack.scss b/src/gridstack.scss index 93b440930..5ac3ef272 100644 --- a/src/gridstack.scss +++ b/src/gridstack.scss @@ -28,7 +28,7 @@ .grid-stack > .grid-stack-item { position: absolute; padding: 0; - top: 0; left: 0; // some default to reduce at least first row/column inline styles + top: 0; // some default to reduce at least first row inline styles width: var(--gs-column-width); // reduce 1x1 items inline styles height: var(--gs-cell-height); @@ -43,9 +43,16 @@ overflow-y: hidden; } } - + + .grid-stack:not(.grid-stack-rtl) > .grid-stack-item { + left: 0; // some default to reduce at least first column inline styles + } + .grid-stack.grid-stack-rtl > .grid-stack-item { + right: 0; // some default to reduce at least first column inline styles + } + .grid-stack { - > .grid-stack-item > .grid-stack-item-content, + > .grid-stack-item > .grid-stack-item-content, > .grid-stack-placeholder > .placeholder-content { top: var(--gs-item-margin-top); right: var(--gs-item-margin-right); @@ -105,7 +112,7 @@ } &.ui-draggable-dragging { - will-change: left, top; + will-change: left, right, top; } &.ui-resizable-resizing { @@ -126,21 +133,22 @@ .grid-stack-animate, .grid-stack-animate .grid-stack-item { - transition: left $animation_speed, top $animation_speed, height $animation_speed, width $animation_speed; + transition: left $animation_speed, right $animation_speed, top $animation_speed, height $animation_speed, width $animation_speed; } .grid-stack-animate .grid-stack-item.ui-draggable-dragging, .grid-stack-animate .grid-stack-item.ui-resizable-resizing, .grid-stack-animate .grid-stack-item.grid-stack-placeholder{ - transition: left 0s, top 0s, height 0s, width 0s; + transition: left 0s, right 0s, top 0s, height 0s, width 0s; } // make those more unique as to not conflict with side panel items, but apply to all column layouts (so not in loop below) .grid-stack > .grid-stack-item[gs-y="0"] { top: 0px; } - .grid-stack > .grid-stack-item[gs-x="0"] { + .grid-stack:not(.grid-stack-rtl) > .grid-stack-item[gs-x="0"] { left: 0%; } - - \ No newline at end of file + .grid-stack.grid-stack-rtl > .grid-stack-item[gs-x="0"] { + right: 0%; + } diff --git a/src/gridstack.ts b/src/gridstack.ts index 394360714..bc0765448 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -1920,9 +1920,10 @@ export class GridStack { protected _writePosAttr(el: HTMLElement, n: GridStackNode): GridStack { // Avoid overwriting the inline style of the element during drag/resize, but always update the placeholder if ((!n._moving && !n._resizing) || this._placeholder === el) { + const xProp = this.opts.rtl ? 'right' : 'left'; // width/height:1 x/y:0 is set by default in the main CSS, so no need to set inlined vars el.style.top = n.y ? (n.y === 1 ? `var(--gs-cell-height)` : `calc(${n.y} * var(--gs-cell-height))`) : null; - el.style.left = n.x ? (n.x === 1 ? `var(--gs-column-width)` : `calc(${n.x} * var(--gs-column-width))`) : null; + el.style[xProp] = n.x ? (n.x === 1 ? `var(--gs-column-width)` : `calc(${n.x} * var(--gs-column-width))`) : null; el.style.width = n.w > 1 ? `calc(${n.w} * var(--gs-column-width))` : null; el.style.height = n.h > 1 ? `calc(${n.h} * var(--gs-cell-height))` : null; } @@ -2797,11 +2798,13 @@ export class GridStack { dd.draggable(el, { start: onStartMoving, stop: onEndMoving, - drag: dragOrResize + drag: dragOrResize, + rtl: this.opts.rtl, }).resizable(el, { start: onStartMoving, stop: onEndMoving, - resize: dragOrResize + resize: dragOrResize, + rtl: this.opts.rtl, }); node._initDD = true; // we've set DD support now } @@ -2930,12 +2933,13 @@ export class GridStack { // only recalculate position for handles that move the top-left corner (N/W). // for SE/S/E handles the top-left is anchored — recalculating from pixels causes // rounding drift on fine grids where cellWidth/cellHeight are only a few pixels. #385 #1356 - const dir = event.resizeDir; - if (dir && (dir.includes('w') || dir.includes('n'))) { + if (event.hasMovedX) { const left = ui.position.left + mLeft; + p.x = Math.round(left / cellWidth); + } + if (event.hasMovedY) { const top = ui.position.top + mTop; - if (dir.includes('w')) p.x = Math.round(left / cellWidth); - if (dir.includes('n')) p.y = Math.round(top / cellHeight); + p.y = Math.round(top / cellHeight); } resizing = true; diff --git a/src/types.ts b/src/types.ts index 41fe5542c..5c31e82fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -497,6 +497,7 @@ export interface DDDragOpt { start?: (event: Event, ui: DDUIData) => void; stop?: (event: Event) => void; drag?: (event: Event, ui: DDUIData) => void; + rtl?: boolean; } export interface Size { width: number; @@ -504,6 +505,11 @@ export interface Size { } export interface Position { top: number; + /** + * Start position of the element on the X axis. + * In LTR mode, this is the coordinate from the left side. + * In RTL mode it's actually the coordinate from the right side. + */ left: number; } export interface Rect extends Size, Position {} @@ -578,4 +584,6 @@ export interface GridStackNode extends GridStackWidget { // add custom field to support drag/resize optimizations export interface GridStackMouseEvent extends MouseEvent { resizeDir?: string; -} \ No newline at end of file + hasMovedX?: boolean; + hasMovedY?: boolean; +}