From a05cc11f89d107b4d83979c89ce4fb33a96b8c27 Mon Sep 17 00:00:00 2001 From: Charlie Hanekamp Date: Wed, 9 Jul 2025 16:00:56 +0200 Subject: [PATCH 1/3] Move widgets with the keyboard --- src/dd-draggable.ts | 99 +++++++++++++++++++++++++++++++++++++++++ src/gridstack-engine.ts | 2 +- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/dd-draggable.ts b/src/dd-draggable.ts index c1f18316..e6ac486f 100644 --- a/src/dd-draggable.ts +++ b/src/dd-draggable.ts @@ -44,6 +44,9 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt protected dragEls: HTMLElement[]; /** @internal true while we are dragging an item around */ protected dragging: boolean; + + /** @internal true while we are dragging an item around */ + protected keyboardSelected: HTMLElement; /** @internal last drag event */ protected lastDrag: DragEvent; /** @internal */ @@ -74,6 +77,9 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt } // create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions) this._mouseDown = this._mouseDown.bind(this); + this._mouseKeyDown = this._mouseKeyDown.bind(this); + this._keyMove = this._keyMove.bind(this); + this._keyUp = this._keyUp.bind(this); this._mouseMove = this._mouseMove.bind(this); this._mouseUp = this._mouseUp.bind(this); this._keyEvent = this._keyEvent.bind(this); @@ -92,6 +98,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt if (this.disabled === false) return; super.enable(); this.dragEls.forEach(dragEl => { + dragEl.addEventListener('keydown', this._mouseKeyDown) dragEl.addEventListener('mousedown', this._mouseDown); if (isTouch) { dragEl.addEventListener('touchstart', touchstart); @@ -131,6 +138,98 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt return this; } + protected _elCoordinates(element: HTMLElement) { + const rect = element.getBoundingClientRect(); + const clientX = rect.left; + const clientY = rect.top; + const offsetX = element.offsetLeft; + const offsetY = element.offsetTop; + const pageX = window.scrollX + rect.left; + const pageY = window.scrollY + rect.top; + const screenX = window.screenX + rect.left; + const screenY = window.screenY + rect.top; + + return { clientX: clientX, + clientY: clientY, + offsetX: offsetX, + offsetY: offsetY, + pageX: pageX, + pageY: pageY, + screenX: screenX, + screenY: screenY } + } + + protected _elNewCoordinates(event: KeyboardEvent, element: HTMLElement) { + let coordinates = this._elCoordinates(element) + + switch (event.code) { + case 'ArrowRight': + coordinates.clientX = coordinates.clientX + 400 + break + case 'ArrowLeft': + coordinates.clientX = coordinates.clientX - 400 + break + case 'ArrowUp': + coordinates.clientY = coordinates.clientY - 400 + break + case 'ArrowDown': + coordinates.clientY = coordinates.clientY + 400 + break + } + return coordinates + } + + protected _mouseKeyDown(e: KeyboardEvent): void { + if(e.code === 'Space') { + e.preventDefault() + + const handle = e.target as HTMLElement + const item: HTMLElement = handle?.closest('.grid-stack-item') + this.keyboardSelected = item + item.classList.add('grid-stack-item-selected') + + e.target.dispatchEvent(new MouseEvent('mousedown')) + document.addEventListener('keyup', this._keyUp) + } + } + + protected _keyUp() { + document.removeEventListener('keyup', this._keyUp) + document.addEventListener('keydown', this._keyMove) + } + + protected _selectedItem (element: HTMLElement): HTMLElement { + const items = document.querySelectorAll('.grid-stack-item') + + return Array.from(items).filter(item => item === element)[0] as HTMLElement + } + + protected _keyMove(e: KeyboardEvent) { + if (e.code === 'Space') { + e.preventDefault() + + this.keyboardSelected.classList.remove('grid-stack-item-selected') + this.keyboardSelected.dispatchEvent(new MouseEvent('mouseup')) + document.removeEventListener('keydown', this._keyMove) + + return + } + + if (e.code === 'ArrowRight' || + e.code === 'ArrowLeft' || + e.code === 'ArrowUp' || + e.code === 'ArrowDown') { + e.target.dispatchEvent(new MouseEvent('mousemove', { ...this._elCoordinates(this.keyboardSelected)})) + e.target.dispatchEvent(new MouseEvent('mousemove', { ...this._elNewCoordinates(e, this.keyboardSelected)})) + e.target.dispatchEvent(new MouseEvent('mouseup')) + + this.keyboardSelected = this._selectedItem(this.keyboardSelected) + const handle: HTMLElement = this.keyboardSelected.querySelector('.grid-item-handle') + + handle?.dispatchEvent(new MouseEvent('mousedown')) + } + } + /** @internal call when mouse goes down before a dragstart happens */ protected _mouseDown(e: MouseEvent): boolean { // don't let more than one widget handle mouseStart diff --git a/src/gridstack-engine.ts b/src/gridstack-engine.ts index 8e1fe98d..4e257f31 100644 --- a/src/gridstack-engine.ts +++ b/src/gridstack-engine.ts @@ -424,7 +424,7 @@ export class GridStackEngine { copy.w = Math.min(this.defaultColumn, copy.w || 1); this.cacheOneLayout(copy, this.defaultColumn); } - + if (node.w > this.column) { node.w = this.column; } else if (node.w < 1) { From b1463474cca4589685446e878ae689fad4a37351 Mon Sep 17 00:00:00 2001 From: Charlie Hanekamp Date: Wed, 9 Jul 2025 16:24:30 +0200 Subject: [PATCH 2/3] Get the correct new position When using the arrow keys the item moves one colum left or right. Or its own height up or down. --- src/dd-draggable.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/dd-draggable.ts b/src/dd-draggable.ts index e6ac486f..441c5a15 100644 --- a/src/dd-draggable.ts +++ b/src/dd-draggable.ts @@ -77,7 +77,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt } // create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions) this._mouseDown = this._mouseDown.bind(this); - this._mouseKeyDown = this._mouseKeyDown.bind(this); + this._keyDown = this._keyDown.bind(this); this._keyMove = this._keyMove.bind(this); this._keyUp = this._keyUp.bind(this); this._mouseMove = this._mouseMove.bind(this); @@ -98,7 +98,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt if (this.disabled === false) return; super.enable(); this.dragEls.forEach(dragEl => { - dragEl.addEventListener('keydown', this._mouseKeyDown) + dragEl.addEventListener('keydown', this._keyDown) dragEl.addEventListener('mousedown', this._mouseDown); if (isTouch) { dragEl.addEventListener('touchstart', touchstart); @@ -160,26 +160,38 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt } protected _elNewCoordinates(event: KeyboardEvent, element: HTMLElement) { + const node = this.el.gridstackNode + const cellHeight = node.grid.getCellHeight() * node.h + const cellWidth = node.grid.cellWidth() + const maxColumn = node.grid.opts.column + let coordinates = this._elCoordinates(element) switch (event.code) { case 'ArrowRight': - coordinates.clientX = coordinates.clientX + 400 + if(typeof(maxColumn) == 'number' && node.x === (maxColumn - 1)) { break } + + coordinates.clientX = coordinates.clientX + cellWidth + (cellWidth / 2) break case 'ArrowLeft': - coordinates.clientX = coordinates.clientX - 400 + if (node.x === 0) { break } + + coordinates.clientX = coordinates.clientX - cellWidth - (cellWidth / 2) break case 'ArrowUp': - coordinates.clientY = coordinates.clientY - 400 + if (node.y === 0) { break } + + coordinates.clientY = coordinates.clientY - cellHeight break case 'ArrowDown': - coordinates.clientY = coordinates.clientY + 400 + coordinates.clientY = coordinates.clientY + cellHeight break } + return coordinates } - protected _mouseKeyDown(e: KeyboardEvent): void { + protected _keyDown(e: KeyboardEvent): void { if(e.code === 'Space') { e.preventDefault() @@ -219,6 +231,8 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt e.code === 'ArrowLeft' || e.code === 'ArrowUp' || e.code === 'ArrowDown') { + e.preventDefault() + e.target.dispatchEvent(new MouseEvent('mousemove', { ...this._elCoordinates(this.keyboardSelected)})) e.target.dispatchEvent(new MouseEvent('mousemove', { ...this._elNewCoordinates(e, this.keyboardSelected)})) e.target.dispatchEvent(new MouseEvent('mouseup')) From d5498810836c574b532ac074d6dc527ec58d4a9f Mon Sep 17 00:00:00 2001 From: Charlie Hanekamp Date: Wed, 9 Jul 2025 17:25:09 +0200 Subject: [PATCH 3/3] Scroll with the moving item The item is moved with the keyboard and the item stays in view of the user. --- src/dd-draggable.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dd-draggable.ts b/src/dd-draggable.ts index 441c5a15..92cdf461 100644 --- a/src/dd-draggable.ts +++ b/src/dd-draggable.ts @@ -238,6 +238,8 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt e.target.dispatchEvent(new MouseEvent('mouseup')) this.keyboardSelected = this._selectedItem(this.keyboardSelected) + this.keyboardSelected.scrollIntoView({ block: "center" }) + const handle: HTMLElement = this.keyboardSelected.querySelector('.grid-item-handle') handle?.dispatchEvent(new MouseEvent('mousedown'))