| // 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. |
| * @private {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; |
| }, |
| }; |