michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["CrossSlide"]; michael@0: michael@0: // needs DPI adjustment? michael@0: let CrossSlideThresholds = { michael@0: SELECTIONSTART: 25, michael@0: SPEEDBUMPSTART: 30, michael@0: SPEEDBUMPEND: 50, michael@0: REARRANGESTART: 80 michael@0: }; michael@0: michael@0: // see: http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.input.crossslidingstate.ASPx michael@0: let CrossSlidingState = { michael@0: STARTED: 0, michael@0: DRAGGING: 1, michael@0: SELECTING: 2, michael@0: SELECT_SPEED_BUMPING: 3, michael@0: SPEED_BUMPING: 4, michael@0: REARRANGING: 5, michael@0: COMPLETED: 6 michael@0: }; michael@0: michael@0: let CrossSlidingStateNames = [ michael@0: 'started', michael@0: 'dragging', michael@0: 'selecting', michael@0: 'selectSpeedBumping', michael@0: 'speedBumping', michael@0: 'rearranging', michael@0: 'completed' michael@0: ]; michael@0: michael@0: // -------------------------------- michael@0: // module helpers michael@0: // michael@0: michael@0: function isSelectable(aElement) { michael@0: // placeholder logic michael@0: return aElement.nodeName == 'richgriditem' && aElement.hasAttribute("value"); michael@0: } michael@0: function withinCone(aLen, aHeight) { michael@0: // check pt falls within 45deg either side of the cross axis michael@0: return aLen > aHeight; michael@0: } michael@0: function getScrollAxisFromElement(aElement) { michael@0: // keeping it simple - just return apparent scroll axis for the document michael@0: let win = aElement.ownerDocument.defaultView; michael@0: let scrollX = win.scrollMaxX, michael@0: scrollY = win.scrollMaxY; michael@0: // determine scroll axis from scrollable content when possible michael@0: if (scrollX || scrollY) michael@0: return scrollX >= scrollY ? 'x' : 'y'; michael@0: michael@0: // fall back to guessing at scroll axis from document aspect ratio michael@0: let docElem = aElement.ownerDocument.documentElement; michael@0: return docElem.clientWidth >= docElem.clientHeight ? michael@0: 'x' : 'y'; michael@0: } michael@0: function pointFromTouchEvent(aEvent) { michael@0: let touch = aEvent.touches[0]; michael@0: return { x: touch.clientX, y: touch.clientY }; michael@0: } michael@0: // This damping function has these important properties: michael@0: // f(0) = 0 michael@0: // f'(0) = 1 michael@0: // limit as x -> Infinity of f(x) = 1 michael@0: function damp(aX) { michael@0: return 2 / (1 + Math.exp(-2 * aX)) - 1; michael@0: } michael@0: function speedbump(aDelta, aStart, aEnd) { michael@0: let x = Math.abs(aDelta); michael@0: if (x <= aStart) michael@0: return aDelta; michael@0: let sign = aDelta / x; michael@0: michael@0: let d = aEnd - aStart; michael@0: let damped = damp((x - aStart) / d); michael@0: return sign * (aStart + (damped * d)); michael@0: } michael@0: michael@0: michael@0: this.CrossSlide = { michael@0: // ----------------------- michael@0: // Gesture constants michael@0: Thresholds: CrossSlideThresholds, michael@0: State: CrossSlidingState, michael@0: StateNames: CrossSlidingStateNames, michael@0: // ----------------------- michael@0: speedbump: speedbump michael@0: }; michael@0: michael@0: function CrossSlideHandler(aNode, aThresholds) { michael@0: this.node = aNode; michael@0: this.thresholds = Object.create(CrossSlideThresholds); michael@0: // apply per-instance threshold configuration michael@0: if (aThresholds) { michael@0: for(let key in aThresholds) michael@0: this.thresholds[key] = aThresholds[key]; michael@0: } michael@0: aNode.addEventListener("touchstart", this, false); michael@0: aNode.addEventListener("touchmove", this, false); michael@0: aNode.addEventListener("touchend", this, false); michael@0: aNode.addEventListener("touchcancel", this, false); michael@0: aNode.ownerDocument.defaultView.addEventListener("scroll", this, false); michael@0: } michael@0: michael@0: CrossSlideHandler.prototype = { michael@0: node: null, michael@0: drag: null, michael@0: michael@0: getCrossSlideState: function(aCrossAxisDistance, aScrollAxisDistance) { michael@0: if (aCrossAxisDistance <= 0) { michael@0: return CrossSlidingState.STARTED; michael@0: } michael@0: if (aCrossAxisDistance < this.thresholds.SELECTIONSTART) { michael@0: return CrossSlidingState.DRAGGING; michael@0: } michael@0: if (aCrossAxisDistance < this.thresholds.SPEEDBUMPSTART) { michael@0: return CrossSlidingState.SELECTING; michael@0: } michael@0: if (aCrossAxisDistance < this.thresholds.SPEEDBUMPEND) { michael@0: return CrossSlidingState.SELECT_SPEED_BUMPING; michael@0: } michael@0: if (aCrossAxisDistance < this.thresholds.REARRANGESTART) { michael@0: return CrossSlidingState.SPEED_BUMPING; michael@0: } michael@0: // out of bounds cross-slide michael@0: return -1; michael@0: }, michael@0: michael@0: handleEvent: function handleEvent(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "touchstart": michael@0: this._onTouchStart(aEvent); michael@0: break; michael@0: case "touchmove": michael@0: this._onTouchMove(aEvent); michael@0: break; michael@0: case "scroll": michael@0: case "touchcancel": michael@0: this.cancel(aEvent); michael@0: break; michael@0: case "touchend": michael@0: this._onTouchEnd(aEvent); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: cancel: function(aEvent){ michael@0: this._fireProgressEvent("cancelled", aEvent); michael@0: this.drag = null; michael@0: }, michael@0: michael@0: _onTouchStart: function onTouchStart(aEvent){ michael@0: if (aEvent.touches.length > 1) michael@0: return; michael@0: let touch = aEvent.touches[0]; michael@0: // cross-slide is a single touch gesture michael@0: // the top target is the one we need here, touch.target not relevant michael@0: let target = aEvent.target; michael@0: michael@0: if (!isSelectable(target)) michael@0: return; michael@0: michael@0: let scrollAxis = getScrollAxisFromElement(target); michael@0: michael@0: this.drag = { michael@0: scrollAxis: scrollAxis, michael@0: crossAxis: (scrollAxis=='x') ? 'y' : 'x', michael@0: origin: pointFromTouchEvent(aEvent), michael@0: state: -1 michael@0: }; michael@0: }, michael@0: michael@0: _onTouchMove: function(aEvent){ michael@0: if (!this.drag) { michael@0: return; michael@0: } michael@0: michael@0: if (aEvent.touches.length!==1) { michael@0: // cancel if another touch point gets involved michael@0: return this.cancel(aEvent); michael@0: } michael@0: michael@0: let startPt = this.drag.origin; michael@0: let endPt = this.drag.position = pointFromTouchEvent(aEvent); michael@0: michael@0: let scrollAxis = this.drag.scrollAxis, michael@0: crossAxis = this.drag.crossAxis; michael@0: michael@0: // distance from the origin along the axis perpendicular to scrolling michael@0: let crossAxisDistance = Math.abs(endPt[crossAxis] - startPt[crossAxis]); michael@0: // distance along the scrolling axis michael@0: let scrollAxisDistance = Math.abs(endPt[scrollAxis] - startPt[scrollAxis]); michael@0: let currState = this.drag.state; michael@0: let newState = this.getCrossSlideState(crossAxisDistance, scrollAxisDistance); michael@0: michael@0: switch (newState) { michael@0: case -1 : michael@0: // dodgy input/out of bounds michael@0: return this.cancel(aEvent); michael@0: case CrossSlidingState.STARTED : michael@0: break; michael@0: case CrossSlidingState.DRAGGING : michael@0: if (scrollAxisDistance > this.thresholds.SELECTIONSTART) { michael@0: // looks like a pan/scroll was intended michael@0: return this.cancel(aEvent); michael@0: } michael@0: // else fall-thru' michael@0: case CrossSlidingState.SELECTING : michael@0: case CrossSlidingState.SELECT_SPEED_BUMPING : michael@0: case CrossSlidingState.SPEED_BUMPING : michael@0: // we're committed to a cross-slide gesture, michael@0: // so going out of bounds at this point means aborting michael@0: if (!withinCone(crossAxisDistance, scrollAxisDistance)) { michael@0: return this.cancel(aEvent); michael@0: } michael@0: // we're mid-gesture, consume this event michael@0: aEvent.stopPropagation(); michael@0: break; michael@0: } michael@0: michael@0: if (currState !== newState) { michael@0: this.drag.state = newState; michael@0: this._fireProgressEvent( CrossSlidingStateNames[newState], aEvent ); michael@0: } michael@0: }, michael@0: _onTouchEnd: function(aEvent){ michael@0: if (!this.drag) michael@0: return; michael@0: michael@0: if (this.drag.state < CrossSlidingState.SELECTING) { michael@0: return this.cancel(aEvent); michael@0: } michael@0: michael@0: this._fireProgressEvent("completed", aEvent); michael@0: this._fireSelectEvent(aEvent); michael@0: this.drag = null; michael@0: }, michael@0: michael@0: /** michael@0: * Dispatches a custom Event on the drag node. michael@0: * @param aEvent The source event. michael@0: * @param aType The event type. michael@0: */ michael@0: _fireProgressEvent: function CrossSliding_fireEvent(aState, aEvent) { michael@0: if (!this.drag) michael@0: return; michael@0: let event = this.node.ownerDocument.createEvent("Events"); michael@0: let crossAxisName = this.drag.crossAxis; michael@0: event.initEvent("MozCrossSliding", true, true); michael@0: event.crossSlidingState = aState; michael@0: if ('position' in this.drag) { michael@0: event.position = this.drag.position; michael@0: if (crossAxisName) { michael@0: event.direction = crossAxisName; michael@0: if('origin' in this.drag) { michael@0: event.delta = this.drag.position[crossAxisName] - this.drag.origin[crossAxisName]; michael@0: } michael@0: } michael@0: } michael@0: aEvent.target.dispatchEvent(event); michael@0: }, michael@0: michael@0: /** michael@0: * Dispatches a custom Event on the given target node. michael@0: * @param aEvent The source event. michael@0: */ michael@0: _fireSelectEvent: function SelectTarget_fireEvent(aEvent) { michael@0: let event = this.node.ownerDocument.createEvent("Events"); michael@0: event.initEvent("MozCrossSlideSelect", true, true); michael@0: event.position = this.drag.position; michael@0: aEvent.target.dispatchEvent(event); michael@0: } michael@0: }; michael@0: this.CrossSlide.Handler = CrossSlideHandler;