Skip to content

Patch Scroll Speed PR #3081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ <h1>Demos</h1>
<li><a href="responsive_break.html">Responsive: using breakpoints</a></li>
<li><a href="responsive_none.html">Responsive: using layout:'none'</a></li>
<li><a href="right-to-left(rtl).html">Right-To-Left (RTL)</a></li>
<li><a href="scrollSpeed.html">Scroll Speed</a></li>
<li><a href="serialization.html">Serialization</a></li>
<li><a href="sizeToContent.html">Size To Content</a></li>
<li><a href="static.html">Static</a></li>
Expand Down
78 changes: 78 additions & 0 deletions demo/scrollSpeed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scroll Speed Demo</title>

<link rel="stylesheet" href="demo.css"/>
<script src="../dist/gridstack-all.js"></script>

</head>
<body>
<div class="container-fluid">
<h1>Scroll Speed Demo</h1>
<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>
<div>
<label for="maxScrollSpeed">Max Scroll Speed:</label>
<input type="number" id="maxScrollSpeed" value="0" min="0" step="1" onchange="updateMaxScrollSpeed()" style="margin-left: 5px; width: 80px;">
<small style="margin-left: 10px;">0 = no limit, higher values = slower scrolling</small>
</div>
<br><br>
<div class="grid-stack"></div>
</div>
<script src="events.js"></script>
<script type="text/javascript">
let grid = GridStack.init({
float: true,
maxScrollSpeed: 0, // default to 0 (no limit)
resizable: { handles: 'all'}
});
addEvents(grid);

// Create 20 widgets with varied sizes and positions (all sizes between 2-5, no overlaps)
let items = [
// Row 1
{x: 0, y: 0, w: 3, h: 2, content: "Widget 1"},
{x: 3, y: 0, w: 4, h: 3, content: "Widget 2"},
{x: 7, y: 0, w: 2, h: 2, content: "Widget 3"},
{x: 9, y: 0, w: 3, h: 2, content: "Widget 4"},

// Row 2
{x: 0, y: 2, w: 2, h: 3, content: "Widget 5"},
{x: 2, y: 2, w: 5, h: 2, content: "Widget 6"},
{x: 9, y: 2, w: 3, h: 3, content: "Widget 7"},

// Row 3
{x: 0, y: 5, w: 4, h: 2, content: "Widget 8"},
{x: 4, y: 5, w: 2, h: 3, content: "Widget 9"},
{x: 6, y: 5, w: 3, h: 2, content: "Widget 10"},

// Row 4
{x: 0, y: 7, w: 3, h: 3, content: "Widget 11"},
{x: 3, y: 7, w: 2, h: 2, content: "Widget 12"},
{x: 5, y: 7, w: 4, h: 3, content: "Widget 13"},
{x: 9, y: 7, w: 3, h: 2, content: "Widget 14"},

// Row 5
{x: 0, y: 10, w: 5, h: 2, content: "Widget 15"},
{x: 5, y: 10, w: 2, h: 3, content: "Widget 16"},
{x: 7, y: 10, w: 3, h: 2, content: "Widget 17"},

// Row 6
{x: 0, y: 12, w: 2, h: 2, content: "Widget 18"},
{x: 2, y: 12, w: 4, h: 3, content: "Widget 19"},
{x: 6, y: 12, w: 3, h: 2, content: "Widget 20"}
];

grid.load(items);

updateMaxScrollSpeed = function() {
const value = parseInt(document.getElementById('maxScrollSpeed').value) || 0;
grid.opts.maxScrollSpeed = value;
console.log('Max scroll speed updated to:', value);
};
</script>
</body>
</html>
1 change: 1 addition & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ gridstack.js API
- `marginBottom`: numberOrString
- `marginLeft`: numberOrString
- `maxRow` - maximum rows amount. Default is `0` which means no max.
- `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.
- `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.
- `nonce` - If you are using a nonce-based Content Security Policy, pass your nonce here and
GridStack will add it to the `<style>` elements it creates.
Expand Down
82 changes: 82 additions & 0 deletions spec/utils-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,88 @@ describe('gridstack utils', function() {
});
});

describe('_getScrollAmount', () => {
const innerHeight = 800;
const elHeight = 600;

it('should not scroll if element is inside viewport', () => {
const rect = { top: 100, bottom: 700, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -10);
expect(scrollAmount).toBe(0);
});

it('should not limit the scroll speed if the user has set maxScrollSpeed to 0', () => {
const rect = { top: 220, bottom: 850, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 50);
expect(scrollAmount).toBe(50);
});

it('should treat a negative maxScrollSpeed as positive', () => {
const rect = { top: 220, bottom: 850, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 50, -4 );
expect(scrollAmount).toBe(4);
});

describe('scrolling up', () => {
it('should scroll up', () => {
const rect = { top: -20, bottom: 580, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -30);
expect(scrollAmount).toBe(-20);
});
it('should scroll up to bring dragged element into view', () => {
const rect = { top: -20, bottom: 580, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -10);
expect(scrollAmount).toBe(-10);
});
it('should scroll up when dragged element is larger than viewport', () => {
const rect = { top: -20, bottom: 880, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, 900, -30);
expect(scrollAmount).toBe(-30);
});

it('should limit the scroll speed when the expected scroll speed is greater than the maxScrollSpeed', () => {
const rect = { top: -30, bottom: 880, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmountWithoutLimit = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -30);
expect(scrollAmountWithoutLimit).toBe(-30); // be completely sure that the scroll amount should be limited

const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, -30, 10);
expect(scrollAmount).toBe(-10);
});
});

describe('scrolling down', () => {
it('should not scroll down if element is inside viewport', () => {
const rect = { top: 100, bottom: 700, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 10);
expect(scrollAmount).toBe(0);
});
it('should scroll down', () => {
const rect = { top: 220, bottom: 820, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 10);
expect(scrollAmount).toBe(10);
});
it('should scroll down to bring dragged element into view', () => {
const rect = { top: 220, bottom: 820, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 30);
expect(scrollAmount).toBe(20);
});
it('should scroll down when dragged element is larger than viewport', () => {
const rect = { top: -100, bottom: 820, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, 920, 10);
expect(scrollAmount).toBe(10);
});

it('should limit the scroll speed when the expected scroll speed is greater than the maxScrollSpeed', () => {
const rect = { top: 220, bottom: 850, left: 0, right: 0, width: 50, height: 50, toJSON: () => '' };
const scrollAmountWithoutLimit = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 50);
expect(scrollAmountWithoutLimit).toBe(50); // be completely sure that the scroll amount should be limited

const scrollAmount = Utils._getScrollAmount(rect as DOMRect, innerHeight, elHeight, 10, 10);
expect(scrollAmount).toBe(10);
});
});
});

describe('clone', () => {
const a: any = {first: 1, second: 'text'};
const b: any = {first: 1, second: {third: 3}};
Expand Down
2 changes: 1 addition & 1 deletion src/gridstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2541,7 +2541,7 @@ export class GridStack {
const distance = ui.position.top - node._prevYPix;
node._prevYPix = ui.position.top;
if (this.opts.draggable.scroll !== false) {
Utils.updateScrollPosition(el, ui.position, distance);
Utils.updateScrollPosition(el, ui.position, distance, this.opts.maxScrollSpeed);
}

// get new position taking into account the margin in the direction we are moving! (need to pass mid point by margin)
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ export interface GridStackOptions {
/** maximum rows amount. Default? is 0 which means no maximum rows */
maxRow?: number;

/** maximum scroll speed when dragging items. Any negative value will be converted to the positive value. (default?: 0 = no limit) */
maxScrollSpeed?: number;

/** minimum rows amount. 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.
*/
Expand Down
68 changes: 41 additions & 27 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,37 +351,51 @@ export class Utils {
}

/** @internal */
static updateScrollPosition(el: HTMLElement, position: {top: number}, distance: number): void {
static _getScrollAmount(rect: DOMRect, viewportHeight: number, elHeight: number, distance: number, maxScrollSpeed?: number): number {
const offsetDiffDown = rect.bottom - viewportHeight;
const offsetDiffUp = rect.top;
const elementIsLargerThanViewport = elHeight > viewportHeight;
let scrollAmount = 0;

if (rect.top < 0 && distance < 0) {
// moving up
if (elementIsLargerThanViewport) {
scrollAmount = distance;
} else {
scrollAmount = Math.abs(offsetDiffUp) > Math.abs(distance) ? distance : offsetDiffUp;
}
} else if (rect.bottom > viewportHeight && distance > 0) {
// moving down
if (elementIsLargerThanViewport) {
scrollAmount = distance;
} else {
scrollAmount = offsetDiffDown > distance ? distance : offsetDiffDown;
}
}

if (maxScrollSpeed) {
maxScrollSpeed = Math.abs(maxScrollSpeed);
if (scrollAmount > maxScrollSpeed) {
scrollAmount = maxScrollSpeed;
} else if (scrollAmount < -maxScrollSpeed) {
scrollAmount = -maxScrollSpeed;
}
}
return scrollAmount;
}

/** @internal */
static updateScrollPosition(el: HTMLElement, position: {top: number}, distance: number, maxScrollSpeed?: number): void {
// is widget in view?
const rect = el.getBoundingClientRect();
const innerHeightOrClientHeight = (window.innerHeight || document.documentElement.clientHeight);
if (rect.top < 0 ||
rect.bottom > innerHeightOrClientHeight
) {
// set scrollTop of first parent that scrolls
// if parent is larger than el, set as low as possible
// to get entire widget on screen
const offsetDiffDown = rect.bottom - innerHeightOrClientHeight;
const offsetDiffUp = rect.top;
const viewportHeight = (window.innerHeight || document.documentElement.clientHeight);
const scrollAmount = this._getScrollAmount(rect, viewportHeight, el.offsetHeight, distance, maxScrollSpeed);

if (scrollAmount) {
const scrollEl = this.getScrollElement(el);
if (scrollEl !== null) {
if (scrollEl) {
const prevScroll = scrollEl.scrollTop;
if (rect.top < 0 && distance < 0) {
// moving up
if (el.offsetHeight > innerHeightOrClientHeight) {
scrollEl.scrollTop += distance;
} else {
scrollEl.scrollTop += Math.abs(offsetDiffUp) > Math.abs(distance) ? distance : offsetDiffUp;
}
} else if (distance > 0) {
// moving down
if (el.offsetHeight > innerHeightOrClientHeight) {
scrollEl.scrollTop += distance;
} else {
scrollEl.scrollTop += offsetDiffDown > distance ? distance : offsetDiffDown;
}
}
// move widget y by amount scrolled
scrollEl.scrollTop += scrollAmount;
position.top += scrollEl.scrollTop - prevScroll;
}
}
Expand Down