browser/metro/modules/CrossSlide.jsm

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4 "use strict";
michael@0 5
michael@0 6 this.EXPORTED_SYMBOLS = ["CrossSlide"];
michael@0 7
michael@0 8 // needs DPI adjustment?
michael@0 9 let CrossSlideThresholds = {
michael@0 10 SELECTIONSTART: 25,
michael@0 11 SPEEDBUMPSTART: 30,
michael@0 12 SPEEDBUMPEND: 50,
michael@0 13 REARRANGESTART: 80
michael@0 14 };
michael@0 15
michael@0 16 // see: http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.input.crossslidingstate.ASPx
michael@0 17 let CrossSlidingState = {
michael@0 18 STARTED: 0,
michael@0 19 DRAGGING: 1,
michael@0 20 SELECTING: 2,
michael@0 21 SELECT_SPEED_BUMPING: 3,
michael@0 22 SPEED_BUMPING: 4,
michael@0 23 REARRANGING: 5,
michael@0 24 COMPLETED: 6
michael@0 25 };
michael@0 26
michael@0 27 let CrossSlidingStateNames = [
michael@0 28 'started',
michael@0 29 'dragging',
michael@0 30 'selecting',
michael@0 31 'selectSpeedBumping',
michael@0 32 'speedBumping',
michael@0 33 'rearranging',
michael@0 34 'completed'
michael@0 35 ];
michael@0 36
michael@0 37 // --------------------------------
michael@0 38 // module helpers
michael@0 39 //
michael@0 40
michael@0 41 function isSelectable(aElement) {
michael@0 42 // placeholder logic
michael@0 43 return aElement.nodeName == 'richgriditem' && aElement.hasAttribute("value");
michael@0 44 }
michael@0 45 function withinCone(aLen, aHeight) {
michael@0 46 // check pt falls within 45deg either side of the cross axis
michael@0 47 return aLen > aHeight;
michael@0 48 }
michael@0 49 function getScrollAxisFromElement(aElement) {
michael@0 50 // keeping it simple - just return apparent scroll axis for the document
michael@0 51 let win = aElement.ownerDocument.defaultView;
michael@0 52 let scrollX = win.scrollMaxX,
michael@0 53 scrollY = win.scrollMaxY;
michael@0 54 // determine scroll axis from scrollable content when possible
michael@0 55 if (scrollX || scrollY)
michael@0 56 return scrollX >= scrollY ? 'x' : 'y';
michael@0 57
michael@0 58 // fall back to guessing at scroll axis from document aspect ratio
michael@0 59 let docElem = aElement.ownerDocument.documentElement;
michael@0 60 return docElem.clientWidth >= docElem.clientHeight ?
michael@0 61 'x' : 'y';
michael@0 62 }
michael@0 63 function pointFromTouchEvent(aEvent) {
michael@0 64 let touch = aEvent.touches[0];
michael@0 65 return { x: touch.clientX, y: touch.clientY };
michael@0 66 }
michael@0 67 // This damping function has these important properties:
michael@0 68 // f(0) = 0
michael@0 69 // f'(0) = 1
michael@0 70 // limit as x -> Infinity of f(x) = 1
michael@0 71 function damp(aX) {
michael@0 72 return 2 / (1 + Math.exp(-2 * aX)) - 1;
michael@0 73 }
michael@0 74 function speedbump(aDelta, aStart, aEnd) {
michael@0 75 let x = Math.abs(aDelta);
michael@0 76 if (x <= aStart)
michael@0 77 return aDelta;
michael@0 78 let sign = aDelta / x;
michael@0 79
michael@0 80 let d = aEnd - aStart;
michael@0 81 let damped = damp((x - aStart) / d);
michael@0 82 return sign * (aStart + (damped * d));
michael@0 83 }
michael@0 84
michael@0 85
michael@0 86 this.CrossSlide = {
michael@0 87 // -----------------------
michael@0 88 // Gesture constants
michael@0 89 Thresholds: CrossSlideThresholds,
michael@0 90 State: CrossSlidingState,
michael@0 91 StateNames: CrossSlidingStateNames,
michael@0 92 // -----------------------
michael@0 93 speedbump: speedbump
michael@0 94 };
michael@0 95
michael@0 96 function CrossSlideHandler(aNode, aThresholds) {
michael@0 97 this.node = aNode;
michael@0 98 this.thresholds = Object.create(CrossSlideThresholds);
michael@0 99 // apply per-instance threshold configuration
michael@0 100 if (aThresholds) {
michael@0 101 for(let key in aThresholds)
michael@0 102 this.thresholds[key] = aThresholds[key];
michael@0 103 }
michael@0 104 aNode.addEventListener("touchstart", this, false);
michael@0 105 aNode.addEventListener("touchmove", this, false);
michael@0 106 aNode.addEventListener("touchend", this, false);
michael@0 107 aNode.addEventListener("touchcancel", this, false);
michael@0 108 aNode.ownerDocument.defaultView.addEventListener("scroll", this, false);
michael@0 109 }
michael@0 110
michael@0 111 CrossSlideHandler.prototype = {
michael@0 112 node: null,
michael@0 113 drag: null,
michael@0 114
michael@0 115 getCrossSlideState: function(aCrossAxisDistance, aScrollAxisDistance) {
michael@0 116 if (aCrossAxisDistance <= 0) {
michael@0 117 return CrossSlidingState.STARTED;
michael@0 118 }
michael@0 119 if (aCrossAxisDistance < this.thresholds.SELECTIONSTART) {
michael@0 120 return CrossSlidingState.DRAGGING;
michael@0 121 }
michael@0 122 if (aCrossAxisDistance < this.thresholds.SPEEDBUMPSTART) {
michael@0 123 return CrossSlidingState.SELECTING;
michael@0 124 }
michael@0 125 if (aCrossAxisDistance < this.thresholds.SPEEDBUMPEND) {
michael@0 126 return CrossSlidingState.SELECT_SPEED_BUMPING;
michael@0 127 }
michael@0 128 if (aCrossAxisDistance < this.thresholds.REARRANGESTART) {
michael@0 129 return CrossSlidingState.SPEED_BUMPING;
michael@0 130 }
michael@0 131 // out of bounds cross-slide
michael@0 132 return -1;
michael@0 133 },
michael@0 134
michael@0 135 handleEvent: function handleEvent(aEvent) {
michael@0 136 switch (aEvent.type) {
michael@0 137 case "touchstart":
michael@0 138 this._onTouchStart(aEvent);
michael@0 139 break;
michael@0 140 case "touchmove":
michael@0 141 this._onTouchMove(aEvent);
michael@0 142 break;
michael@0 143 case "scroll":
michael@0 144 case "touchcancel":
michael@0 145 this.cancel(aEvent);
michael@0 146 break;
michael@0 147 case "touchend":
michael@0 148 this._onTouchEnd(aEvent);
michael@0 149 break;
michael@0 150 }
michael@0 151 },
michael@0 152
michael@0 153 cancel: function(aEvent){
michael@0 154 this._fireProgressEvent("cancelled", aEvent);
michael@0 155 this.drag = null;
michael@0 156 },
michael@0 157
michael@0 158 _onTouchStart: function onTouchStart(aEvent){
michael@0 159 if (aEvent.touches.length > 1)
michael@0 160 return;
michael@0 161 let touch = aEvent.touches[0];
michael@0 162 // cross-slide is a single touch gesture
michael@0 163 // the top target is the one we need here, touch.target not relevant
michael@0 164 let target = aEvent.target;
michael@0 165
michael@0 166 if (!isSelectable(target))
michael@0 167 return;
michael@0 168
michael@0 169 let scrollAxis = getScrollAxisFromElement(target);
michael@0 170
michael@0 171 this.drag = {
michael@0 172 scrollAxis: scrollAxis,
michael@0 173 crossAxis: (scrollAxis=='x') ? 'y' : 'x',
michael@0 174 origin: pointFromTouchEvent(aEvent),
michael@0 175 state: -1
michael@0 176 };
michael@0 177 },
michael@0 178
michael@0 179 _onTouchMove: function(aEvent){
michael@0 180 if (!this.drag) {
michael@0 181 return;
michael@0 182 }
michael@0 183
michael@0 184 if (aEvent.touches.length!==1) {
michael@0 185 // cancel if another touch point gets involved
michael@0 186 return this.cancel(aEvent);
michael@0 187 }
michael@0 188
michael@0 189 let startPt = this.drag.origin;
michael@0 190 let endPt = this.drag.position = pointFromTouchEvent(aEvent);
michael@0 191
michael@0 192 let scrollAxis = this.drag.scrollAxis,
michael@0 193 crossAxis = this.drag.crossAxis;
michael@0 194
michael@0 195 // distance from the origin along the axis perpendicular to scrolling
michael@0 196 let crossAxisDistance = Math.abs(endPt[crossAxis] - startPt[crossAxis]);
michael@0 197 // distance along the scrolling axis
michael@0 198 let scrollAxisDistance = Math.abs(endPt[scrollAxis] - startPt[scrollAxis]);
michael@0 199 let currState = this.drag.state;
michael@0 200 let newState = this.getCrossSlideState(crossAxisDistance, scrollAxisDistance);
michael@0 201
michael@0 202 switch (newState) {
michael@0 203 case -1 :
michael@0 204 // dodgy input/out of bounds
michael@0 205 return this.cancel(aEvent);
michael@0 206 case CrossSlidingState.STARTED :
michael@0 207 break;
michael@0 208 case CrossSlidingState.DRAGGING :
michael@0 209 if (scrollAxisDistance > this.thresholds.SELECTIONSTART) {
michael@0 210 // looks like a pan/scroll was intended
michael@0 211 return this.cancel(aEvent);
michael@0 212 }
michael@0 213 // else fall-thru'
michael@0 214 case CrossSlidingState.SELECTING :
michael@0 215 case CrossSlidingState.SELECT_SPEED_BUMPING :
michael@0 216 case CrossSlidingState.SPEED_BUMPING :
michael@0 217 // we're committed to a cross-slide gesture,
michael@0 218 // so going out of bounds at this point means aborting
michael@0 219 if (!withinCone(crossAxisDistance, scrollAxisDistance)) {
michael@0 220 return this.cancel(aEvent);
michael@0 221 }
michael@0 222 // we're mid-gesture, consume this event
michael@0 223 aEvent.stopPropagation();
michael@0 224 break;
michael@0 225 }
michael@0 226
michael@0 227 if (currState !== newState) {
michael@0 228 this.drag.state = newState;
michael@0 229 this._fireProgressEvent( CrossSlidingStateNames[newState], aEvent );
michael@0 230 }
michael@0 231 },
michael@0 232 _onTouchEnd: function(aEvent){
michael@0 233 if (!this.drag)
michael@0 234 return;
michael@0 235
michael@0 236 if (this.drag.state < CrossSlidingState.SELECTING) {
michael@0 237 return this.cancel(aEvent);
michael@0 238 }
michael@0 239
michael@0 240 this._fireProgressEvent("completed", aEvent);
michael@0 241 this._fireSelectEvent(aEvent);
michael@0 242 this.drag = null;
michael@0 243 },
michael@0 244
michael@0 245 /**
michael@0 246 * Dispatches a custom Event on the drag node.
michael@0 247 * @param aEvent The source event.
michael@0 248 * @param aType The event type.
michael@0 249 */
michael@0 250 _fireProgressEvent: function CrossSliding_fireEvent(aState, aEvent) {
michael@0 251 if (!this.drag)
michael@0 252 return;
michael@0 253 let event = this.node.ownerDocument.createEvent("Events");
michael@0 254 let crossAxisName = this.drag.crossAxis;
michael@0 255 event.initEvent("MozCrossSliding", true, true);
michael@0 256 event.crossSlidingState = aState;
michael@0 257 if ('position' in this.drag) {
michael@0 258 event.position = this.drag.position;
michael@0 259 if (crossAxisName) {
michael@0 260 event.direction = crossAxisName;
michael@0 261 if('origin' in this.drag) {
michael@0 262 event.delta = this.drag.position[crossAxisName] - this.drag.origin[crossAxisName];
michael@0 263 }
michael@0 264 }
michael@0 265 }
michael@0 266 aEvent.target.dispatchEvent(event);
michael@0 267 },
michael@0 268
michael@0 269 /**
michael@0 270 * Dispatches a custom Event on the given target node.
michael@0 271 * @param aEvent The source event.
michael@0 272 */
michael@0 273 _fireSelectEvent: function SelectTarget_fireEvent(aEvent) {
michael@0 274 let event = this.node.ownerDocument.createEvent("Events");
michael@0 275 event.initEvent("MozCrossSlideSelect", true, true);
michael@0 276 event.position = this.drag.position;
michael@0 277 aEvent.target.dispatchEvent(event);
michael@0 278 }
michael@0 279 };
michael@0 280 this.CrossSlide.Handler = CrossSlideHandler;

mercurial