Skip to content

Commit 30e946a

Browse files
author
Nick Heinbaugh
committed
apply patch from gridstack#3076
1 parent 6339147 commit 30e946a

File tree

7 files changed

+207
-28
lines changed

7 files changed

+207
-28
lines changed

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ <h1>Demos</h1>
2828
<li><a href="responsive_break.html">Responsive: using breakpoints</a></li>
2929
<li><a href="responsive_none.html">Responsive: using layout:'none'</a></li>
3030
<li><a href="right-to-left(rtl).html">Right-To-Left (RTL)</a></li>
31+
<li><a href="scrollSpeed.html">Scroll Speed</a></li>
3132
<li><a href="serialization.html">Serialization</a></li>
3233
<li><a href="sizeToContent.html">Size To Content</a></li>
3334
<li><a href="static.html">Static</a></li>

demo/scrollSpeed.html

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>Scroll Speed Demo</title>
8+
9+
<link rel="stylesheet" href="demo.css"/>
10+
<script src="../dist/gridstack-all.js"></script>
11+
12+
</head>
13+
<body>
14+
<div class="container-fluid">
15+
<h1>Scroll Speed Demo</h1>
16+
<p>This demo shows 20 widgets to test scroll speed behavior when dragging items. Try dragging widgets near the top or bottom of the viewport to see the scrolling effect.</p>
17+
<div>
18+
<label for="maxScrollSpeed">Max Scroll Speed:</label>
19+
<input type="number" id="maxScrollSpeed" value="0" min="0" step="1" onchange="updateMaxScrollSpeed()" style="margin-left: 5px; width: 80px;">
20+
<small style="margin-left: 10px;">0 = no limit, higher values = slower scrolling</small>
21+
</div>
22+
<br><br>
23+
<div class="grid-stack"></div>
24+
</div>
25+
<script src="events.js"></script>
26+
<script type="text/javascript">
27+
let grid = GridStack.init({
28+
float: true,
29+
maxScrollSpeed: 0, // default to 0 (no limit)
30+
resizable: { handles: 'all'}
31+
});
32+
addEvents(grid);
33+
34+
// Create 20 widgets with varied sizes and positions (all sizes between 2-5, no overlaps)
35+
let items = [
36+
// Row 1
37+
{x: 0, y: 0, w: 3, h: 2, content: "Widget 1"},
38+
{x: 3, y: 0, w: 4, h: 3, content: "Widget 2"},
39+
{x: 7, y: 0, w: 2, h: 2, content: "Widget 3"},
40+
{x: 9, y: 0, w: 3, h: 2, content: "Widget 4"},
41+
42+
// Row 2
43+
{x: 0, y: 2, w: 2, h: 3, content: "Widget 5"},
44+
{x: 2, y: 2, w: 5, h: 2, content: "Widget 6"},
45+
{x: 9, y: 2, w: 3, h: 3, content: "Widget 7"},
46+
47+
// Row 3
48+
{x: 0, y: 5, w: 4, h: 2, content: "Widget 8"},
49+
{x: 4, y: 5, w: 2, h: 3, content: "Widget 9"},
50+
{x: 6, y: 5, w: 3, h: 2, content: "Widget 10"},
51+
52+
// Row 4
53+
{x: 0, y: 7, w: 3, h: 3, content: "Widget 11"},
54+
{x: 3, y: 7, w: 2, h: 2, content: "Widget 12"},
55+
{x: 5, y: 7, w: 4, h: 3, content: "Widget 13"},
56+
{x: 9, y: 7, w: 3, h: 2, content: "Widget 14"},
57+
58+
// Row 5
59+
{x: 0, y: 10, w: 5, h: 2, content: "Widget 15"},
60+
{x: 5, y: 10, w: 2, h: 3, content: "Widget 16"},
61+
{x: 7, y: 10, w: 3, h: 2, content: "Widget 17"},
62+
63+
// Row 6
64+
{x: 0, y: 12, w: 2, h: 2, content: "Widget 18"},
65+
{x: 2, y: 12, w: 4, h: 3, content: "Widget 19"},
66+
{x: 6, y: 12, w: 3, h: 2, content: "Widget 20"}
67+
];
68+
69+
grid.load(items);
70+
71+
updateMaxScrollSpeed = function() {
72+
const value = parseInt(document.getElementById('maxScrollSpeed').value) || 0;
73+
grid.opts.maxScrollSpeed = value;
74+
console.log('Max scroll speed updated to:', value);
75+
};
76+
</script>
77+
</body>
78+
</html>

doc/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ gridstack.js API
116116
- `marginBottom`: numberOrString
117117
- `marginLeft`: numberOrString
118118
- `maxRow` - maximum rows amount. Default is `0` which means no max.
119+
- `maxScrollSpeed` - (number) limits the speed that the user wll scroll up and down in the grid. This is most noticable in large grids. Default: `0` which indicates no limit on the scroll speed. Any value provided here should be positive.
119120
- `minRow` - minimum rows amount which is handy to prevent grid from collapsing when empty. Default is `0`. You can also do this with `min-height` CSS attribute on the grid div in pixels, which will round to the closest row.
120121
- `nonce` - If you are using a nonce-based Content Security Policy, pass your nonce here and
121122
GridStack will add it to the `<style>` elements it creates.

spec/utils-spec.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,88 @@ describe('gridstack utils', function() {
116116
});
117117
});
118118

119+
describe('_getScrollAmount', () => {
120+
const innerHeight = 800;
121+
const elHeight = 600;
122+
123+
it('should not scroll if element is inside viewport', () => {
124+
const rect = { top: 100, bottom: 700, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
125+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -10);
126+
expect(scrollAmount).toBe(0);
127+
});
128+
129+
it('should not limit the scroll speed if the user has set maxScrollSpeed to 0', () => {
130+
const rect = { top: 220, bottom: 850, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
131+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 50);
132+
expect(scrollAmount).toBe(50);
133+
});
134+
135+
it('should treat a negative maxScrollSpeed as positive', () => {
136+
const rect = { top: 220, bottom: 850, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
137+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 50, -4 );
138+
expect(scrollAmount).toBe(4);
139+
});
140+
141+
describe('scrolling up', () => {
142+
it('should scroll up', () => {
143+
const rect = { top: -20, bottom: 580, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
144+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -30);
145+
expect(scrollAmount).toBe(-20);
146+
});
147+
it('should scroll up to bring dragged element into view', () => {
148+
const rect = { top: -20, bottom: 580, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
149+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -10);
150+
expect(scrollAmount).toBe(-10);
151+
});
152+
it('should scroll up when dragged element is larger than viewport', () => {
153+
const rect = { top: -20, bottom: 880, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
154+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, 900, -30);
155+
expect(scrollAmount).toBe(-30);
156+
});
157+
158+
it('should limit the scroll speed when the expected scroll speed is greater than the maxScrollSpeed', () => {
159+
const rect = { top: -30, bottom: 880, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
160+
const scrollAmountWithoutLimit = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -30);
161+
expect(scrollAmountWithoutLimit).toBe(-30); // be completely sure that the scroll amount should be limited
162+
163+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -30, 10);
164+
expect(scrollAmount).toBe(-10);
165+
});
166+
});
167+
168+
describe('scrolling down', () => {
169+
it('should not scroll down if element is inside viewport', () => {
170+
const rect = { top: 100, bottom: 700, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
171+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 10);
172+
expect(scrollAmount).toBe(0);
173+
});
174+
it('should scroll down', () => {
175+
const rect = { top: 220, bottom: 820, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
176+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 10);
177+
expect(scrollAmount).toBe(10);
178+
});
179+
it('should scroll down to bring dragged element into view', () => {
180+
const rect = { top: 220, bottom: 820, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
181+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 30);
182+
expect(scrollAmount).toBe(20);
183+
});
184+
it('should scroll down when dragged element is larger than viewport', () => {
185+
const rect = { top: -100, bottom: 820, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
186+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, 920, 10);
187+
expect(scrollAmount).toBe(10);
188+
});
189+
190+
it('should limit the scroll speed when the expected scroll speed is greater than the maxScrollSpeed', () => {
191+
const rect = { top: 220, bottom: 850, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
192+
const scrollAmountWithoutLimit = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 50);
193+
expect(scrollAmountWithoutLimit).toBe(50); // be completely sure that the scroll amount should be limited
194+
195+
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 10, 10);
196+
expect(scrollAmount).toBe(10);
197+
});
198+
});
199+
});
200+
119201
describe('clone', () => {
120202
const a: any = {first: 1, second: 'text'};
121203
const b: any = {first: 1, second: {third: 3}};

src/gridstack.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2541,7 +2541,7 @@ export class GridStack {
25412541
const distance = ui.position.top - node._prevYPix;
25422542
node._prevYPix = ui.position.top;
25432543
if (this.opts.draggable.scroll !== false) {
2544-
Utils.updateScrollPosition(el, ui.position, distance);
2544+
Utils.updateScrollPosition(el, ui.position, distance, this.opts.maxScrollSpeed);
25452545
}
25462546

25472547
// get new position taking into account the margin in the direction we are moving! (need to pass mid point by margin)

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ export interface GridStackOptions {
217217
/** maximum rows amount. Default? is 0 which means no maximum rows */
218218
maxRow?: number;
219219

220+
/** maximum scroll speed when dragging items. Any negative value will be converted to the positive value. (default?: 0 = no limit) */
221+
maxScrollSpeed?: number;
222+
220223
/** minimum rows amount. Default is `0`. You can also do this with `min-height` CSS attribute
221224
* on the grid div in pixels, which will round to the closest row.
222225
*/

src/utils.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -351,37 +351,51 @@ export class Utils {
351351
}
352352

353353
/** @internal */
354-
static updateScrollPosition(el: HTMLElement, position: {top: number}, distance: number): void {
354+
static _getScrollAmount(rect: DOMRect, viewportHeight: number, elHeight: number, distance: number, maxScrollSpeed?: number): number {
355+
const offsetDiffDown = rect.bottom - viewportHeight;
356+
const offsetDiffUp = rect.top;
357+
const elementIsLargerThanViewport = elHeight > viewportHeight;
358+
let scrollAmount = 0;
359+
360+
if (rect.top < 0 && distance < 0) {
361+
// moving up
362+
if (elementIsLargerThanViewport) {
363+
scrollAmount = distance;
364+
} else {
365+
scrollAmount = Math.abs(offsetDiffUp) > Math.abs(distance) ? distance : offsetDiffUp;
366+
}
367+
} else if (rect.bottom > viewportHeight && distance > 0) {
368+
// moving down
369+
if (elementIsLargerThanViewport) {
370+
scrollAmount = distance;
371+
} else {
372+
scrollAmount = offsetDiffDown > distance ? distance : offsetDiffDown;
373+
}
374+
}
375+
376+
if (maxScrollSpeed) {
377+
maxScrollSpeed = Math.abs(maxScrollSpeed);
378+
if (scrollAmount > maxScrollSpeed) {
379+
scrollAmount = maxScrollSpeed;
380+
} else if (scrollAmount < -maxScrollSpeed) {
381+
scrollAmount = -maxScrollSpeed;
382+
}
383+
}
384+
return scrollAmount;
385+
}
386+
387+
/** @internal */
388+
static updateScrollPosition(el: HTMLElement, position: {top: number}, distance: number, maxScrollSpeed?: number): void {
355389
// is widget in view?
356390
const rect = el.getBoundingClientRect();
357-
const innerHeightOrClientHeight = (window.innerHeight || document.documentElement.clientHeight);
358-
if (rect.top < 0 ||
359-
rect.bottom > innerHeightOrClientHeight
360-
) {
361-
// set scrollTop of first parent that scrolls
362-
// if parent is larger than el, set as low as possible
363-
// to get entire widget on screen
364-
const offsetDiffDown = rect.bottom - innerHeightOrClientHeight;
365-
const offsetDiffUp = rect.top;
391+
const viewportHeight = (window.innerHeight || document.documentElement.clientHeight);
392+
const scrollAmount = this._getScrollAmount(rect, viewportHeight, el.offsetHeight, distance, maxScrollSpeed);
393+
394+
if (scrollAmount) {
366395
const scrollEl = this.getScrollElement(el);
367-
if (scrollEl !== null) {
396+
if (scrollEl) {
368397
const prevScroll = scrollEl.scrollTop;
369-
if (rect.top < 0 && distance < 0) {
370-
// moving up
371-
if (el.offsetHeight > innerHeightOrClientHeight) {
372-
scrollEl.scrollTop += distance;
373-
} else {
374-
scrollEl.scrollTop += Math.abs(offsetDiffUp) > Math.abs(distance) ? distance : offsetDiffUp;
375-
}
376-
} else if (distance > 0) {
377-
// moving down
378-
if (el.offsetHeight > innerHeightOrClientHeight) {
379-
scrollEl.scrollTop += distance;
380-
} else {
381-
scrollEl.scrollTop += offsetDiffDown > distance ? distance : offsetDiffDown;
382-
}
383-
}
384-
// move widget y by amount scrolled
398+
scrollEl.scrollTop += scrollAmount;
385399
position.top += scrollEl.scrollTop - prevScroll;
386400
}
387401
}

0 commit comments

Comments
 (0)