Skip to content

Commit

Permalink
Add AutoScroll plugin for draggable
Browse files Browse the repository at this point in the history
  • Loading branch information
tsov committed Jan 11, 2018
1 parent c0688c6 commit 3c375ed
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 40 deletions.
9 changes: 9 additions & 0 deletions scripts/test/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const requestAnimationFrameTimeout = 15;

window.requestAnimationFrame = (callback) => {
return setTimeout(callback, requestAnimationFrameTimeout);
};

window.cancelAnimationFrame = (id) => {
return clearTimeout(id);
};
7 changes: 4 additions & 3 deletions src/Draggable/Draggable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {closest} from 'shared/utils';

import {Accessibility, Mirror} from './Plugins';
import {Accessibility, Mirror, AutoScroll} from './Plugins';

import {
MouseSensor,
Expand Down Expand Up @@ -68,9 +68,10 @@ export default class Draggable {
* @property {Object} Plugins
* @property {Mirror} Plugins.Mirror
* @property {Accessibility} Plugins.Accessibility
* @property {AutoScroll} Plugins.AutoScroll
* @type {Object}
*/
static Plugins = {Mirror, Accessibility};
static Plugins = {Mirror, Accessibility, AutoScroll};

/**
* Draggable constructor.
Expand Down Expand Up @@ -127,7 +128,7 @@ export default class Draggable {
document.addEventListener('drag:stop', this[onDragStop], true);
document.addEventListener('drag:pressure', this[onDragPressure], true);

this.addPlugin(...[Mirror, Accessibility, ...this.options.plugins]);
this.addPlugin(...[Mirror, Accessibility, AutoScroll, ...this.options.plugins]);
this.addSensor(...[MouseSensor, TouchSensor, ...this.options.sensors]);

const draggableInitializedEvent = new DraggableInitializedEvent({
Expand Down
217 changes: 217 additions & 0 deletions src/Draggable/Plugins/AutoScroll/AutoScroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import {closest} from 'shared/utils';

export const onDragStart = Symbol('onDragStart');
export const onDragMove = Symbol('onDragMove');
export const onDragStop = Symbol('onDragStop');
export const scroll = Symbol('scroll');

/**
* AutoScroll default options
* @property {Object} defaultOptions
* @property {Number} defaultOptions.speed
* @property {Number} defaultOptions.sensitivity
* @type {Object}
*/
export const defaultOptions = {
speed: 10,
sensitivity: 30,
};

/**
* AutoScroll plugin which scrolls the closest scrollable parent
* @class AutoScroll
* @module AutoScroll
*/
export default class AutoScroll {

/**
* AutoScroll constructor.
* @constructs AutoScroll
* @param {Draggable} draggable - Draggable instance
*/
constructor(draggable) {

/**
* Draggable instance
* @property draggable
* @type {Draggable}
*/
this.draggable = draggable;

/**
* AutoScroll options
* @property {Object} options
* @property {Number} options.speed
* @property {Number} options.sensitivity
* @type {Object}
*/
this.options = {
...defaultOptions,
...this.getOptions(),
};

/**
* Keeps current mouse position
* @property {Object} currentMousePosition
* @property {Number} currentMousePosition.clientX
* @property {Number} currentMousePosition.clientY
* @type {Object|null}
*/
this.currentMousePosition = null;

/**
* Scroll animation frame
* @property scrollAnimationFrame
* @type {Number|null}
*/
this.scrollAnimationFrame = null;

/**
* Closest scrollable element
* @property scrollableElement
* @type {HTMLElement|null}
*/
this.scrollableElement = null;

/**
* Animation frame looking for the closest scrollable element
* @property findScrollableElementFrame
* @type {Number|null}
*/
this.findScrollableElementFrame = null;

this[onDragStart] = this[onDragStart].bind(this);
this[onDragMove] = this[onDragMove].bind(this);
this[onDragStop] = this[onDragStop].bind(this);
this[scroll] = this[scroll].bind(this);
}

/**
* Attaches plugins event listeners
*/
attach() {
this.draggable
.on('drag:start', this[onDragStart])
.on('drag:move', this[onDragMove])
.on('drag:stop', this[onDragStop]);
}

/**
* Detaches plugins event listeners
*/
detach() {
this.draggable
.off('drag:start', this[onDragStart])
.off('drag:move', this[onDragMove])
.off('drag:stop', this[onDragStop]);
}

/**
* Returns options passed through draggable
* @return {Object}
*/
getOptions() {
return this.draggable.options.autoScroll || {};
}

/**
* Drag start handler. Finds closest scrollable parent in separate frame
* @private
*/
[onDragStart](dragEvent) {
this.findScrollableElementFrame = requestAnimationFrame(() => {
this.scrollableElement = closestScrollableElement(dragEvent.source);
});
}

/**
* Drag move handler. Remembers mouse position and initiates scrolling
* @private
*/
[onDragMove](dragEvent) {
if (!this.scrollableElement) {
return;
}

const sensorEvent = dragEvent.sensorEvent;

this.currentMousePosition = {
clientX: sensorEvent.clientX,
clientY: sensorEvent.clientY,
};

this.scrollAnimationFrame = requestAnimationFrame(this[scroll]);
}

/**
* Drag stop handler. Cancels scroll animations and resets state
* @private
*/
[onDragStop]() {
cancelAnimationFrame(this.scrollAnimationFrame);
cancelAnimationFrame(this.findScrollableElementFrame);

this.scrollableElement = null;
this.scrollAnimationFrame = null;
this.findScrollableElementFrame = null;
this.currentMousePosition = null;
}

/**
* Scroll function that does the heavylifting
* @private
*/
[scroll]() {
if (!this.scrollableElement) {
return;
}

cancelAnimationFrame(this.scrollAnimationFrame);

const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
const rect = this.scrollableElement.getBoundingClientRect();

let offsetY = (Math.abs(rect.bottom - this.currentMousePosition.clientY) <= this.options.sensitivity) - (Math.abs(rect.top - this.currentMousePosition.clientY) <= this.options.sensitivity);
let offsetX = (Math.abs(rect.right - this.currentMousePosition.clientX) <= this.options.sensitivity) - (Math.abs(rect.left - this.currentMousePosition.clientX) <= this.options.sensitivity);

if (!offsetX && !offsetY) {
offsetX = (windowWidth - this.currentMousePosition.clientX <= this.options.sensitivity) - (this.currentMousePosition.clientX <= this.options.sensitivity);
offsetY = (windowHeight - this.currentMousePosition.clientY <= this.options.sensitivity) - (this.currentMousePosition.clientY <= this.options.sensitivity);
}

this.scrollableElement.scrollTop += offsetY * this.options.speed;
this.scrollableElement.scrollLeft += offsetX * this.options.speed;

this.scrollAnimationFrame = requestAnimationFrame(this[scroll]);
}
}

/**
* Checks if element has overflow
* @param {HTMLElement} element
* @return {Boolean}
* @private
*/
function hasOverflow(element) {
const overflowRegex = /(auto|scroll)/;
const computedStyles = getComputedStyle(element, null);

const overflow = computedStyles.getPropertyValue('overflow') +
computedStyles.getPropertyValue('overflow-y') +
computedStyles.getPropertyValue('overflow-x');

return overflowRegex.test(overflow);
}

/**
* Finds closest scrollable element
* @param {HTMLElement} element
* @return {HTMLElement}
* @private
*/
function closestScrollableElement(element) {
const scrollableElement = closest(element, (currentElement) => hasOverflow(currentElement));

return scrollableElement || document.scrollingElement || document.documentElement || null;
}
48 changes: 48 additions & 0 deletions src/Draggable/Plugins/AutoScroll/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## AutoScroll

The auto scroll plugin listens to Draggables `drag:start`, `drag:move` and `drag:stop` events to determine when to scroll
the document it's on.
This plugin is used by draggable by default, but could potentially be replaced with a custom plugin.

### API

**`new AutoScroll(draggable: Draggable): AutoScroll`**
To create an auto scroll plugin instance.

### Options

**`speed {Number}`**
Determines the scroll speed. Default: `10`

**`sensitivity {Number}`**
Determines the sensitivity of scrolling. Default: `30`

### Examples

```js
import {Draggable} from '@shopify/draggable';

const draggable = new Draggable(document.querySelectorAll('ul'), {
draggable: 'li',
autoScroll: {
speed: 6,
sensitivity: 12,
},
});
```

#### Removing the plugin

```js
import {Draggable} from '@shopify/draggable';

const draggable = new Draggable(document.querySelectorAll('ul'), {
draggable: 'li',
});

// Removes AutoScroll plugin
draggable.removePlugin(Draggable.Plugin.AutoScroll);

// Adds custom scroll plugin
draggable.addPlugin(CustomScrollPlugin);
```
6 changes: 6 additions & 0 deletions src/Draggable/Plugins/AutoScroll/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import AutoScroll, {defaultOptions} from './AutoScroll';

export default AutoScroll;
export {
defaultOptions as defaultAutoScrollOptions,
};
2 changes: 1 addition & 1 deletion src/Draggable/Plugins/Mirror/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import Mirror, {defaultOptions} from './Mirror';

export default Mirror;
export {
defaultOptions as defaultMirrorOption,
defaultOptions as defaultMirrorOptions,
};
7 changes: 5 additions & 2 deletions src/Draggable/Plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Mirror, {defaultMirrorOption} from './Mirror';
import Mirror, {defaultMirrorOptions} from './Mirror';
import AutoScroll, {defaultAutoScrollOptions} from './AutoScroll';
import Accessibility from './Accessibility';

export {
Mirror,
defaultMirrorOption,
defaultMirrorOptions,
AutoScroll,
defaultAutoScrollOptions,
Accessibility,
};
Loading

0 comments on commit 3c375ed

Please sign in to comment.