Skip to content

Commit ba90fa9

Browse files
authored
Merge pull request #2688 from adumesny/master
'Esc' to cancel drag/resize, 'r' to rotate drag
2 parents 84ba9ac + 7715d7c commit ba90fa9

File tree

6 files changed

+119
-23
lines changed

6 files changed

+119
-23
lines changed

demo/two.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<body>
1919
<div class="container-fluid">
2020
<h1>Two grids demo</h1>
21+
<p>Two grids, one floating one not, showing drag&drop from sidebar and between grids.
22+
<br>New v10.2: use 'Esc' to cancel any move/resize. Use 'r' to rotate as you drag.</p>
2123

2224
<div class="row">
2325
<div class="col-md-3">

doc/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Change log
110110

111111
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
112112
## 10.1.2-dev (TBD)
113+
* feat: [#2682](https://github.com/gridstack/gridstack.js/pull/2682) You can now press 'Esc' to cancel a move|resize, 'r' to rotate during a drag. added `GridStack.rotate()` as well - Thank you John B. for this feature sponsor.
113114
* fix: [#2672](https://github.com/gridstack/gridstack.js/pull/2672) dropping into full grid JS error
114115
* fix: [#2676](https://github.com/gridstack/gridstack.js/issues/2676) handle minW resizing when column count is less
115116
* fix: [#2677](https://github.com/gridstack/gridstack.js/issues/2677) allow button as handle dragging

doc/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ gridstack.js API
6060
- [`removeAll(removeDOM = true)`](#removeallremovedom--true)
6161
- [`resizable(el, val)`](#resizableel-val)
6262
- [`resizeToContent(el: GridItemHTMLElement, useAttrSize = false)`](#resizetocontentel-griditemhtmlelement-useattrsize--false)
63+
- [`rotate(els: GridStackElement, relative?: Position)`](#rotateels-gridstackelement-relative-position)
6364
- [`save(saveContent = true, saveGridOpt = false): GridStackWidget[] | GridStackOptions`](#savesavecontent--true-savegridopt--false-gridstackwidget--gridstackoptions)
6465
- [`setAnimation(doAnimate)`](#setanimationdoanimate)
6566
- [`setStatic(staticValue)`](#setstaticstaticvalue)
@@ -583,6 +584,12 @@ Updates widget height to match the content height to avoid v-scrollbar or dead s
583584
Note: this assumes only 1 child under `resizeToContentParent='.grid-stack-item-content'` (sized to gridItem minus padding) that is at the entire content size wanted.
584585
- `useAttrSize` set to `true` if GridStackNode.h should be used instead of actual container height when we don't need to wait for animation to finish to get actual DOM heights
585586

587+
### `rotate(els: GridStackElement, relative?: Position)`
588+
rotate (by swapping w & h) the passed in node - called when user press 'r' during dragging
589+
590+
- `els` - widget or selector of objects to modify
591+
- `relative` - optional pixel coord relative to upper/left corner to rotate around (will keep that cell under cursor)
592+
586593
### `save(saveContent = true, saveGridOpt = false): GridStackWidget[] | GridStackOptions`
587594

588595
saves the current layout returning a list of widgets for serialization which might include any nested grids.

src/dd-draggable.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { DDManager } from './dd-manager';
77
import { DragTransform, Utils } from './utils';
88
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
9-
import { GridItemHTMLElement, DDUIData } from './types';
9+
import { GridItemHTMLElement, DDUIData, GridStackNode, GridStackPosition } from './types';
1010
import { DDElementHost } from './dd-element';
1111
import { isTouch, touchend, touchmove, touchstart, pointerdown } from './dd-touch';
1212

@@ -33,6 +33,10 @@ interface DragOffset {
3333
offsetTop: number;
3434
}
3535

36+
interface GridStackNodeRotate extends GridStackNode {
37+
_origRotate?: GridStackPosition;
38+
}
39+
3640
type DDDragEvent = 'drag' | 'dragstart' | 'dragstop';
3741

3842
// make sure we are not clicking on known object that handles mouseDown
@@ -53,6 +57,8 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
5357
protected dragEl: HTMLElement;
5458
/** @internal true while we are dragging an item around */
5559
protected dragging: boolean;
60+
/** @internal last drag event */
61+
protected lastDrag: DragEvent;
5662
/** @internal */
5763
protected parentOriginStylePosition: string;
5864
/** @internal */
@@ -69,7 +75,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
6975
yOffset: 0
7076
};
7177

72-
constructor(public el: HTMLElement, public option: DDDraggableOpt = {}) {
78+
constructor(public el: GridItemHTMLElement, public option: DDDraggableOpt = {}) {
7379
super();
7480

7581
// get the element that is actually supposed to be dragged by
@@ -79,6 +85,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
7985
this._mouseDown = this._mouseDown.bind(this);
8086
this._mouseMove = this._mouseMove.bind(this);
8187
this._mouseUp = this._mouseUp.bind(this);
88+
this._keyEvent = this._keyEvent.bind(this);
8289
this.enable();
8390
}
8491

@@ -184,6 +191,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
184191
protected _mouseMove(e: DragEvent): boolean {
185192
// console.log(`${count++} move ${e.x},${e.y}`)
186193
let s = this.mouseDownEvent;
194+
this.lastDrag = e;
187195

188196
if (this.dragging) {
189197
this._dragFollow(e);
@@ -202,25 +210,25 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
202210
this.dragging = true;
203211
DDManager.dragElement = this;
204212
// if we're dragging an actual grid item, set the current drop as the grid (to detect enter/leave)
205-
let grid = (this.el as GridItemHTMLElement).gridstackNode?.grid;
213+
let grid = this.el.gridstackNode?.grid;
206214
if (grid) {
207215
DDManager.dropElement = (grid.el as DDElementHost).ddElement.ddDroppable;
208216
} else {
209217
delete DDManager.dropElement;
210218
}
211219
this.helper = this._createHelper(e);
212220
this._setupHelperContainmentStyle();
213-
this.dragTransform = Utils.getValuesFromTransformedElement(
214-
this.helperContainment
215-
);
221+
this.dragTransform = Utils.getValuesFromTransformedElement(this.helperContainment);
216222
this.dragOffset = this._getDragOffset(e, this.el, this.helperContainment);
217-
const ev = Utils.initEvent<DragEvent>(e, { target: this.el, type: 'dragstart' });
218-
219223
this._setupHelperStyle(e);
224+
225+
const ev = Utils.initEvent<DragEvent>(e, { target: this.el, type: 'dragstart' });
220226
if (this.option.start) {
221227
this.option.start(ev, this.ui());
222228
}
223229
this.triggerEvent('dragstart', ev);
230+
// now track keyboard events to cancel or rotate
231+
document.addEventListener('keydown', this._keyEvent);
224232
}
225233
// e.preventDefault(); // passive = true. OLD: was needed otherwise we get text sweep text selection as we drag around
226234
return true;
@@ -236,6 +244,8 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
236244
}
237245
if (this.dragging) {
238246
delete this.dragging;
247+
delete (this.el.gridstackNode as GridStackNodeRotate)?._origRotate;
248+
document.removeEventListener('keydown', this._keyEvent);
239249

240250
// reset the drop target if dragging over ourself (already parented, just moving during stop callback below)
241251
if (DDManager.dropElement?.el === this.el.parentElement) {
@@ -267,6 +277,37 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
267277
e.preventDefault();
268278
}
269279

280+
/** @internal call when keys are being pressed - use Esc to cancel, R to rotate */
281+
protected _keyEvent(e: KeyboardEvent): void {
282+
const n = this.el.gridstackNode as GridStackNodeRotate;
283+
if (!n) return;
284+
const grid = n.grid;
285+
286+
if (e.key === 'Escape') {
287+
if (n._origRotate) {
288+
n._orig = n._origRotate;
289+
delete n._origRotate;
290+
}
291+
grid.engine.restoreInitial();
292+
this._mouseUp(this.mouseDownEvent);
293+
} else if (e.key === 'r' || e.key === 'R') {
294+
if (n.w === n.h) return;
295+
n._origRotate = n._origRotate || {...n._orig}; // store the real orig size in case we Esc after doing rotation
296+
delete n._moving; // force rotate to happen (move waits for >50% coverage otherwise)
297+
grid.setAnimation(false) // immediate rotate so _getDragOffset() gets the right dom size below
298+
.rotate(n.el, {top: -this.dragOffset.offsetTop, left: -this.dragOffset.offsetLeft})
299+
.setAnimation();
300+
n._moving = true;
301+
this.dragOffset = this._getDragOffset(this.lastDrag, n.el, this.helperContainment);
302+
this.helper.style.width = this.dragOffset.width + 'px';
303+
this.helper.style.height = this.dragOffset.height + 'px';
304+
function swap(o: unknown, a: string, b: string): void { const tmp = o[a]; o[a] = o[b]; o[b] = tmp; }
305+
swap(n._orig, 'w', 'h');
306+
delete n._rect;
307+
this._mouseMove(this.lastDrag);
308+
}
309+
}
310+
270311
/** @internal create a clone copy (or user defined method) of the original drag item if set */
271312
protected _createHelper(event: DragEvent): HTMLElement {
272313
let helper = this.el;

src/dd-resizable-handle.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { isTouch, pointerdown, touchend, touchmove, touchstart } from './dd-touch';
7+
import { GridItemHTMLElement } from './gridstack';
78

89
export interface DDResizableHandleOpt {
910
start?: (event) => void;
@@ -21,11 +22,12 @@ export class DDResizableHandle {
2122
/** @internal */
2223
protected static prefix = 'ui-resizable-';
2324

24-
constructor(protected host: HTMLElement, protected dir: string, protected option: DDResizableHandleOpt) {
25+
constructor(protected host: GridItemHTMLElement, protected dir: string, protected option: DDResizableHandleOpt) {
2526
// create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions)
2627
this._mouseDown = this._mouseDown.bind(this);
2728
this._mouseMove = this._mouseMove.bind(this);
2829
this._mouseUp = this._mouseUp.bind(this);
30+
this._keyEvent = this._keyEvent.bind(this);
2931

3032
this._init();
3133
}
@@ -84,6 +86,8 @@ export class DDResizableHandle {
8486
this.moving = true;
8587
this._triggerEvent('start', this.mouseDownEvent);
8688
this._triggerEvent('move', e);
89+
// now track keyboard events to cancel
90+
document.addEventListener('keydown', this._keyEvent);
8791
}
8892
e.stopPropagation();
8993
// e.preventDefault(); passive = true
@@ -93,6 +97,7 @@ export class DDResizableHandle {
9397
protected _mouseUp(e: MouseEvent): void {
9498
if (this.moving) {
9599
this._triggerEvent('stop', e);
100+
document.removeEventListener('keydown', this._keyEvent);
96101
}
97102
document.removeEventListener('mousemove', this._mouseMove, true);
98103
document.removeEventListener('mouseup', this._mouseUp, true);
@@ -106,6 +111,16 @@ export class DDResizableHandle {
106111
e.preventDefault();
107112
}
108113

114+
/** @internal call when keys are being pressed - use Esc to cancel */
115+
protected _keyEvent(e: KeyboardEvent): void {
116+
if (e.key === 'Escape') {
117+
this.host.gridstackNode?.grid?.engine.restoreInitial();
118+
this._mouseUp(this.mouseDownEvent);
119+
}
120+
}
121+
122+
123+
109124
/** @internal */
110125
protected _triggerEvent(name: string, event: MouseEvent): DDResizableHandle {
111126
if (this.option[name]) this.option[name](event);

src/gridstack.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { Utils, HeightData, obsolete, DragTransform } from './utils';
1010
import {
1111
gridDefaults, ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback,
1212
GridStackNode, GridStackWidget, numberOrString, DDUIData, DDDragInOpt, GridStackPosition, GridStackOptions,
13-
dragInDefaultOptions, GridStackEventHandler, GridStackNodesHandler, AddRemoveFcn, SaveFcn, CompactOptions, GridStackMoveOpts, ResizeToContentFcn, GridStackDroppedHandler, GridStackElementHandler
13+
dragInDefaultOptions, GridStackEventHandler, GridStackNodesHandler, AddRemoveFcn, SaveFcn, CompactOptions, GridStackMoveOpts, ResizeToContentFcn, GridStackDroppedHandler, GridStackElementHandler,
14+
Position
1415
} from './types';
1516

1617
/*
@@ -224,7 +225,7 @@ export class GridStack {
224225
public _isTemp?: boolean;
225226

226227
/** @internal create placeholder DIV as needed */
227-
public get placeholder(): HTMLElement {
228+
public get placeholder(): GridItemHTMLElement {
228229
if (!this._placeholder) {
229230
let placeholderChild = document.createElement('div'); // child so padding match item-content
230231
placeholderChild.className = 'placeholder-content';
@@ -426,7 +427,7 @@ export class GridStack {
426427
}
427428

428429
// if (this.engine.nodes.length) this._updateStyles(); // update based on # of children. done in engine onChange CB
429-
this.setAnimation(opts.animate);
430+
this.setAnimation();
430431

431432
// dynamic grids require pausing during drag to detect over to nest vs push
432433
if (opts.subGridDynamic && !DDManager.pauseDrag) DDManager.pauseDrag = true;
@@ -794,8 +795,8 @@ export class GridStack {
794795
delete this._ignoreLayoutsNodeChange;
795796
delete this._insertNotAppend;
796797
prevCB ? GridStack.addRemoveCB = prevCB : delete GridStack.addRemoveCB;
797-
// delay adding animation back, but check to make sure grid (opt) is still around
798-
if (noAnim && this.opts?.animate) setTimeout(() => { if (this.opts) this.setAnimation(this.opts.animate) });
798+
// delay adding animation back
799+
if (noAnim && this.opts?.animate) this.setAnimation(this.opts.animate, true);
799800
return this;
800801
}
801802

@@ -1255,15 +1256,20 @@ export class GridStack {
12551256
/**
12561257
* Toggle the grid animation state. Toggles the `grid-stack-animate` class.
12571258
* @param doAnimate if true the grid will animate.
1259+
* @param delay if true setting will be set on next event loop.
12581260
*/
1259-
public setAnimation(doAnimate: boolean): GridStack {
1260-
if (doAnimate) {
1261+
public setAnimation(doAnimate = this.opts.animate, delay?: boolean): GridStack {
1262+
if (delay) {
1263+
// delay, but check to make sure grid (opt) is still around
1264+
setTimeout(() => { if (this.opts) this.setAnimation(doAnimate) });
1265+
} else if (doAnimate) {
12611266
this.el.classList.add('grid-stack-animate');
12621267
} else {
12631268
this.el.classList.remove('grid-stack-animate');
12641269
}
12651270
return this;
12661271
}
1272+
12671273
/** @internal */
12681274
private hasAnimationCSS(): boolean { return this.el.classList.contains('grid-stack-animate') }
12691275

@@ -1370,12 +1376,14 @@ export class GridStack {
13701376
}
13711377

13721378
private moveNode(n: GridStackNode, m: GridStackMoveOpts) {
1373-
this.engine.cleanNodes()
1374-
.beginUpdate(n)
1375-
.moveNode(n, m);
1379+
const wasUpdating = n._updating;
1380+
if (!wasUpdating) this.engine.cleanNodes().beginUpdate(n);
1381+
this.engine.moveNode(n, m);
13761382
this._updateContainerHeight();
1377-
this._triggerChangeEvent();
1378-
this.engine.endUpdate();
1383+
if (!wasUpdating) {
1384+
this._triggerChangeEvent();
1385+
this.engine.endUpdate();
1386+
}
13791387
}
13801388

13811389
/**
@@ -1435,6 +1443,27 @@ export class GridStack {
14351443
else this.resizeToContent(el);
14361444
}
14371445

1446+
/** rotate (by swapping w & h) the passed in node - called when user press 'r' during dragging
1447+
* @param els widget or selector of objects to modify
1448+
* @param relative optional pixel coord relative to upper/left corner to rotate around (will keep that cell under cursor)
1449+
*/
1450+
public rotate(els: GridStackElement, relative?: Position): GridStack {
1451+
GridStack.getElements(els).forEach(el => {
1452+
let n = el.gridstackNode;
1453+
if (!n || n.w === n.h) return;
1454+
const rot: GridStackWidget = { w: n.h, h: n.w, minH: n.minW, minW: n.minH, maxH: n.maxW, maxW: n.maxH };
1455+
// if given an offset, adjust x/y by column/row bounds when user presses 'r' during dragging
1456+
if (relative) {
1457+
let pivotX = relative.left > 0 ? Math.floor(relative.left / this.cellWidth()) : 0;
1458+
let pivotY = relative.top > 0 ? Math.floor(relative.top / (this.opts.cellHeight as number)) : 0;
1459+
rot.x = n.x + pivotX - (n.h - (pivotY+1));
1460+
rot.y = (n.y + pivotY) - pivotX;
1461+
}
1462+
this.update(el, rot);
1463+
});
1464+
return this;
1465+
}
1466+
14381467
/**
14391468
* Updates the margins which will set all 4 sides at once - see `GridStackOptions.margin` for format options (CSS string format of 1,2,4 values or single number).
14401469
* @param value margin value
@@ -2289,8 +2318,8 @@ export class GridStack {
22892318
this._gsEventHandler['dropped']({ ...event, type: 'dropped' }, origNode && origNode.grid ? origNode : undefined, node);
22902319
}
22912320

2292-
// delay adding animation back, but check to make sure grid (opt) is still around
2293-
if (noAnim) setTimeout(() => { if (this.opts) this.setAnimation(this.opts.animate) });
2321+
// delay adding animation back
2322+
if (noAnim) this.setAnimation(this.opts.animate, true);
22942323

22952324
return false; // prevent parent from receiving msg (which may be grid as well)
22962325
});
@@ -2435,6 +2464,7 @@ export class GridStack {
24352464
// @ts-ignore
24362465
this._writePosAttr(this.placeholder, node)
24372466
this.el.appendChild(this.placeholder);
2467+
this.placeholder.gridstackNode = node;
24382468
// console.log('_onStartMoving placeholder') // TEST
24392469

24402470
// if the element is inside a grid, it has already been scaled

0 commit comments

Comments
 (0)