diff --git a/src/Draggable/Draggable.js b/src/Draggable/Draggable.js index d4fad6b7..1d783474 100644 --- a/src/Draggable/Draggable.js +++ b/src/Draggable/Draggable.js @@ -5,6 +5,7 @@ import {Focusable, Mirror, Announcement} from './Plugins'; import { MouseSensor, TouchSensor, + KeyboardSensor, } from './Sensors'; import { @@ -119,7 +120,7 @@ export default class Draggable { document.addEventListener('drag:pressure', this[onDragPressure], true); this.addPlugin(...[Mirror, Focusable, Announcement, ...this.options.plugins]); - this.addSensor(...[MouseSensor, TouchSensor, ...this.options.sensors]); + this.addSensor(...[MouseSensor, TouchSensor, KeyboardSensor, ...this.options.sensors]); const draggableInitializedEvent = new DraggableInitializedEvent({ draggable: this, @@ -322,7 +323,7 @@ export default class Draggable { this.source = this.originalSource.cloneNode(true); - if (!isDragEvent(originalEvent)) { + if (!isDragEvent(originalEvent) && !isKeyboardEvent(originalEvent)) { const appendableContainer = this[getAppendableContainer]({source: this.originalSource}); this.mirror = this.source.cloneNode(true); @@ -636,6 +637,10 @@ function isDragEvent(event) { return /^drag/.test(event.type); } +function isKeyboardEvent(event) { + return /^key/.test(event.type); +} + function applyUserSelect(element, value) { element.style.webkitUserSelect = value; element.style.mozUserSelect = value; diff --git a/src/Draggable/Sensors/KeyboardSensor/KeyboardSensor.js b/src/Draggable/Sensors/KeyboardSensor/KeyboardSensor.js new file mode 100644 index 00000000..040e9d17 --- /dev/null +++ b/src/Draggable/Sensors/KeyboardSensor/KeyboardSensor.js @@ -0,0 +1,186 @@ +import {closest, matches} from 'shared/utils'; +import Sensor from './../Sensor'; + +import { + DragStartSensorEvent, + DragMoveSensorEvent, + DragStopSensorEvent, +} from './../SensorEvent'; + +const SPACE_CODE = 32; +const DOWN_CODE = 40; +const RIGHT_CODE = 39; +const UP_CODE = 38; +const LEFT_CODE = 37; + +export default class KeyboardSensor extends Sensor { + constructor(containers = [], options = {}) { + super(containers, options); + + this.dragging = false; + + this._onKeyup = this._onKeyup.bind(this); + this._onFocus = this._onFocus.bind(this); + this._onBlur = this._onBlur.bind(this); + } + + attach() { + document.addEventListener('focus', this._onFocus, true); + document.addEventListener('blur', this._onBlur, true); + document.addEventListener('keydown', this._onKeyup, true); + } + + detach() { + document.removeEventListener('focus', this._onFocus, true); + document.removeEventListener('blur', this._onBlur, true); + document.removeEventListener('keydown', this._onKeyup, true); + } + + _onFocus(event) { + const draggable = event.target; + const container = closest(event.target, this.containers); + const isDraggable = Boolean(matches(event.target, this.options.handle || this.options.draggable)); + const isContainer = Boolean(container); + + if (isDraggable && isContainer) { + this.potentialDraggable = draggable; + this.potentialContainer = container; + } + } + + _onBlur() { + this.potentialDraggable = null; + this.potentialContainer = null; + } + + _onKeyup(event) { + if (!isRelevantKeycode(event)) { + return; + } + + if ((this.potentialDraggable && this.potentialContainer) || this.dragging) { + if (event.keyCode === SPACE_CODE) { + this._toggleDrag(event); + event.preventDefault(); + return; + } + } + + if (!this.dragging) { + return; + } + + let target; + + if (event.keyCode === RIGHT_CODE || event.keyCode === DOWN_CODE) { + // console.log(this._nextDraggable(this.currentDraggable)); + target = this._nextDraggable(this.currentDraggable) || this.allDraggableElements[0]; + + } + + if (event.keyCode === LEFT_CODE || event.keyCode === UP_CODE) { + // console.log(this._previousDraggable(this.currentDraggable)); + target = this._previousDraggable(this.currentDraggable) || this.allDraggableElements[this.allDraggableElements.length - 1]; + // event.preventDefault(); + } + + if (!target) { + return; + } + + console.log(target); + + const rect = target.getBoundingClientRect(); + + const dragMoveEvent = new DragMoveSensorEvent({ + clientX: rect.left + 1, + clientY: rect.top + 1, + target, + container: this.currentContainer, + originalEvent: event, + }); + + this.trigger(this.currentContainer, dragMoveEvent); + event.preventDefault(); + } + + _toggleDrag(event) { + if (this.dragging) { + this._dragStop(event); + } else { + this._dragStart(event); + } + } + + _dragStart(event) { + const target = this.potentialDraggable; + const container = this.potentialContainer; + + const dragStartEvent = new DragStartSensorEvent({ + target, + container, + originalEvent: event, + }); + + this.trigger(container, dragStartEvent); + + this.currentDraggable = target; + this.currentContainer = container; + this.dragging = !dragStartEvent.canceled(); + + requestAnimationFrame(() => { + if (!this.currentContainer) { return; } + this.allDraggableElements = []; + this.containers.forEach((currentContainer) => { + this.allDraggableElements = [ + ...this.allDraggableElements, + ...currentContainer.querySelectorAll(this.options.draggable), + ]; + }); + }); + } + + _dragStop(event) { + const dragStopEvent = new DragStopSensorEvent({ + target: this.potentialDraggable, + container: this.potentialContainer, + originalEvent: event, + }); + + this.trigger(this.currentContainer, dragStopEvent); + this.dragging = false; + this.allDraggableElements = null; + this.currentDraggable = null; + this.currentContainer = null; + } + + _nextDraggable(currentDraggable) { + let currentIndex; + this.allDraggableElements.forEach((draggableElement, index) => { + if (draggableElement === currentDraggable) { + currentIndex = index; + } + }); + return this.allDraggableElements[++currentIndex]; + } + + _previousDraggable(currentDraggable) { + let currentIndex; + this.allDraggableElements.forEach((draggableElement, index) => { + if (draggableElement === currentDraggable) { + currentIndex = index; + } + }); + return this.allDraggableElements[--currentIndex]; + } +} + +function isRelevantKeycode(event) { + return Boolean( + event.keyCode === SPACE_CODE || + event.keyCode === DOWN_CODE || + event.keyCode === RIGHT_CODE || + event.keyCode === UP_CODE || + event.keyCode === LEFT_CODE + ); +} diff --git a/src/Draggable/Sensors/KeyboardSensor/README.md b/src/Draggable/Sensors/KeyboardSensor/README.md new file mode 100644 index 00000000..6abca227 --- /dev/null +++ b/src/Draggable/Sensors/KeyboardSensor/README.md @@ -0,0 +1 @@ +## Keyboard sensor diff --git a/src/Draggable/Sensors/KeyboardSensor/index.js b/src/Draggable/Sensors/KeyboardSensor/index.js new file mode 100644 index 00000000..a30340a7 --- /dev/null +++ b/src/Draggable/Sensors/KeyboardSensor/index.js @@ -0,0 +1,3 @@ +import KeyboardSensor from './KeyboardSensor'; + +export default KeyboardSensor; diff --git a/src/Draggable/Sensors/index.js b/src/Draggable/Sensors/index.js index 9c9bc9af..c7b2563a 100644 --- a/src/Draggable/Sensors/index.js +++ b/src/Draggable/Sensors/index.js @@ -3,6 +3,7 @@ import MouseSensor from './MouseSensor'; import TouchSensor from './TouchSensor'; import DragSensor from './DragSensor'; import ForceTouchSensor from './ForceTouchSensor'; +import KeyboardSensor from './KeyboardSensor'; export { Sensor, @@ -10,4 +11,5 @@ export { TouchSensor, DragSensor, ForceTouchSensor, + KeyboardSensor, }; diff --git a/src/shared/utils/index.js b/src/shared/utils/index.js index b92ac169..02692968 100644 --- a/src/shared/utils/index.js +++ b/src/shared/utils/index.js @@ -1,7 +1,9 @@ import closest from './closest'; import scroll from './scroll'; +import matches from './matches'; export { closest, scroll, + matches, }; diff --git a/src/shared/utils/matches/README.md b/src/shared/utils/matches/README.md new file mode 100644 index 00000000..ba9e19b4 --- /dev/null +++ b/src/shared/utils/matches/README.md @@ -0,0 +1 @@ +## matches diff --git a/src/shared/utils/matches/index.js b/src/shared/utils/matches/index.js new file mode 100644 index 00000000..9d6a8ffd --- /dev/null +++ b/src/shared/utils/matches/index.js @@ -0,0 +1,3 @@ +import matches from './matches'; + +export default matches; diff --git a/src/shared/utils/matches/matches.js b/src/shared/utils/matches/matches.js new file mode 100644 index 00000000..9894cc95 --- /dev/null +++ b/src/shared/utils/matches/matches.js @@ -0,0 +1,8 @@ +const matchFunction = Element.prototype.matches || + Element.prototype.webkitMatchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector; + +export default function matches(element, selector) { + return matchFunction.call(element, selector); +}