blob: 061c36ccf288dfe20416477e87dc7dc0e2cfc329 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Behavior for handling dragging elements in a container.
* Draggable elements must have the 'draggable' attribute set.
*/
/**
* @typedef {{
* x: number,
* y: number
* }}
*/
let DragPosition;
/** @polymerBehavior */
const DragBehavior = {
properties: {
/** Whether or not drag is enabled (e.g. not mirrored). */
dragEnabled: Boolean,
},
/**
* The id of the element being dragged, or empty if not dragging.
* @protected {string}
*/
dragId: '',
/** @private {!HTMLDivElement|undefined} */
container_: undefined,
/** @private {?function(string, ?DragPosition):void} */
callback_: null,
/** @private {!DragPosition} */
dragStartLocation_: {x: 0, y: 0},
/**
* Used to ignore unnecessary drag events.
* @private {?DragPosition}
*/
lastTouchLocation_: null,
/** @private {?function(!Event)} */
mouseDownListener_: null,
/** @private {?function(!Event)} */
mouseMoveListener_: null,
/** @private {?function(!Event)} */
touchStartListener_: null,
/** @private {?function(!Event)} */
touchMoveListener_: null,
/** @private {?function(!Event)} */
endDragListener_: null,
/**
* @param {boolean} enabled
* @param {!HTMLDivElement=} opt_container
* @param {!function(string, ?DragPosition):void=} opt_callback
*/
initializeDrag: function(enabled, opt_container, opt_callback) {
this.dragEnabled = enabled;
if (!enabled) {
this.removeListeners_();
return;
}
if (opt_container !== undefined)
this.container_ = opt_container;
this.addListeners_();
if (opt_callback !== undefined)
this.callback_ = opt_callback;
},
/** @private */
addListeners_: function() {
const container = this.container_;
if (!container || this.mouseDownListener_)
return;
this.mouseDownListener_ = this.onMouseDown_.bind(this);
container.addEventListener('mousedown', this.mouseDownListener_);
this.mouseMoveListener_ = this.onMouseMove_.bind(this);
container.addEventListener('mousemove', this.mouseMoveListener_);
this.touchStartListener_ = this.onTouchStart_.bind(this);
container.addEventListener('touchstart', this.touchStartListener_);
this.touchMoveListener_ = this.onTouchMove_.bind(this);
container.addEventListener('touchmove', this.touchMoveListener_);
this.endDragListener_ = this.endDrag_.bind(this);
window.addEventListener('mouseup', this.endDragListener_);
container.addEventListener('touchend', this.endDragListener_);
},
/** @private */
removeListeners_: function() {
const container = this.container_;
if (!container || !this.mouseDownListener_)
return;
container.removeEventListener('mousedown', this.mouseDownListener_);
this.mouseDownListener_ = null;
container.removeEventListener('mousemove', this.mouseMoveListener_);
this.mouseMoveListener_ = null;
container.removeEventListener('touchstart', this.touchStartListener_);
this.touchStartListener_ = null;
container.removeEventListener('touchmove', this.touchMoveListener_);
this.touchMoveListener_ = null;
container.removeEventListener('touchend', this.endDragListener_);
if (this.endDragListener_)
window.removeEventListener('mouseup', this.endDragListener_);
this.endDragListener_ = null;
},
/**
* @param {!Event} e The mouse down event.
* @return {boolean}
* @private
*/
onMouseDown_: function(e) {
if (e.button != 0 || !e.target.getAttribute('draggable'))
return true;
e.preventDefault();
const target = assertInstanceof(e.target, HTMLElement);
return this.startDrag_(target, {x: e.pageX, y: e.pageY});
},
/**
* @param {!Event} e The mouse move event.
* @return {boolean}
* @private
*/
onMouseMove_: function(e) {
e.preventDefault();
return this.processDrag_(e, {x: e.pageX, y: e.pageY});
},
/**
* @param {!Event} e The touch start event.
* @return {boolean}
* @private
*/
onTouchStart_: function(e) {
if (e.touches.length != 1)
return false;
e.preventDefault();
const touch = e.touches[0];
this.lastTouchLocation_ = {x: touch.pageX, y: touch.pageY};
const target = assertInstanceof(e.target, HTMLElement);
return this.startDrag_(target, this.lastTouchLocation_);
},
/**
* @param {!Event} e The touch move event.
* @return {boolean}
* @private
*/
onTouchMove_: function(e) {
if (e.touches.length != 1)
return true;
const touchLocation = {x: e.touches[0].pageX, y: e.touches[0].pageY};
// Touch move events can happen even if the touch location doesn't change
// and on small unintentional finger movements. Ignore these small changes.
if (this.lastTouchLocation_) {
const IGNORABLE_TOUCH_MOVE_PX = 1;
const xDiff = Math.abs(touchLocation.x - this.lastTouchLocation_.x);
const yDiff = Math.abs(touchLocation.y - this.lastTouchLocation_.y);
if (xDiff <= IGNORABLE_TOUCH_MOVE_PX && yDiff <= IGNORABLE_TOUCH_MOVE_PX)
return true;
}
this.lastTouchLocation_ = touchLocation;
e.preventDefault();
return this.processDrag_(e, touchLocation);
},
/**
* @param {!HTMLElement} target
* @param {!DragPosition} eventLocation
* @return {boolean}
* @private
*/
startDrag_: function(target, eventLocation) {
assert(this.dragEnabled);
this.dragId = target.id;
this.dragStartLocation_ = eventLocation;
return false;
},
/**
* @param {!Event} e
* @return {boolean}
* @private
*/
endDrag_: function(e) {
assert(this.dragEnabled);
if (this.dragId && this.callback_)
this.callback_(this.dragId, null);
this.dragId = '';
this.lastTouchLocation_ = null;
return false;
},
/**
* @param {!Event} e The event which triggers this drag.
* @param {DragPosition} eventLocation The location of the event.
* @return {boolean}
* @private
*/
processDrag_: function(e, eventLocation) {
assert(this.dragEnabled);
if (!this.dragId)
return true;
if (this.callback_) {
const delta = {
x: eventLocation.x - this.dragStartLocation_.x,
y: eventLocation.y - this.dragStartLocation_.y,
};
this.callback_(this.dragId, delta);
}
return false;
},
};