1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/modules/CrossSlide.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,280 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +"use strict"; 1.8 + 1.9 +this.EXPORTED_SYMBOLS = ["CrossSlide"]; 1.10 + 1.11 +// needs DPI adjustment? 1.12 +let CrossSlideThresholds = { 1.13 + SELECTIONSTART: 25, 1.14 + SPEEDBUMPSTART: 30, 1.15 + SPEEDBUMPEND: 50, 1.16 + REARRANGESTART: 80 1.17 +}; 1.18 + 1.19 +// see: http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.input.crossslidingstate.ASPx 1.20 +let CrossSlidingState = { 1.21 + STARTED: 0, 1.22 + DRAGGING: 1, 1.23 + SELECTING: 2, 1.24 + SELECT_SPEED_BUMPING: 3, 1.25 + SPEED_BUMPING: 4, 1.26 + REARRANGING: 5, 1.27 + COMPLETED: 6 1.28 +}; 1.29 + 1.30 +let CrossSlidingStateNames = [ 1.31 + 'started', 1.32 + 'dragging', 1.33 + 'selecting', 1.34 + 'selectSpeedBumping', 1.35 + 'speedBumping', 1.36 + 'rearranging', 1.37 + 'completed' 1.38 +]; 1.39 + 1.40 +// -------------------------------- 1.41 +// module helpers 1.42 +// 1.43 + 1.44 +function isSelectable(aElement) { 1.45 + // placeholder logic 1.46 + return aElement.nodeName == 'richgriditem' && aElement.hasAttribute("value"); 1.47 +} 1.48 +function withinCone(aLen, aHeight) { 1.49 + // check pt falls within 45deg either side of the cross axis 1.50 + return aLen > aHeight; 1.51 +} 1.52 +function getScrollAxisFromElement(aElement) { 1.53 + // keeping it simple - just return apparent scroll axis for the document 1.54 + let win = aElement.ownerDocument.defaultView; 1.55 + let scrollX = win.scrollMaxX, 1.56 + scrollY = win.scrollMaxY; 1.57 + // determine scroll axis from scrollable content when possible 1.58 + if (scrollX || scrollY) 1.59 + return scrollX >= scrollY ? 'x' : 'y'; 1.60 + 1.61 + // fall back to guessing at scroll axis from document aspect ratio 1.62 + let docElem = aElement.ownerDocument.documentElement; 1.63 + return docElem.clientWidth >= docElem.clientHeight ? 1.64 + 'x' : 'y'; 1.65 +} 1.66 +function pointFromTouchEvent(aEvent) { 1.67 + let touch = aEvent.touches[0]; 1.68 + return { x: touch.clientX, y: touch.clientY }; 1.69 +} 1.70 +// This damping function has these important properties: 1.71 +// f(0) = 0 1.72 +// f'(0) = 1 1.73 +// limit as x -> Infinity of f(x) = 1 1.74 +function damp(aX) { 1.75 + return 2 / (1 + Math.exp(-2 * aX)) - 1; 1.76 +} 1.77 +function speedbump(aDelta, aStart, aEnd) { 1.78 + let x = Math.abs(aDelta); 1.79 + if (x <= aStart) 1.80 + return aDelta; 1.81 + let sign = aDelta / x; 1.82 + 1.83 + let d = aEnd - aStart; 1.84 + let damped = damp((x - aStart) / d); 1.85 + return sign * (aStart + (damped * d)); 1.86 +} 1.87 + 1.88 + 1.89 +this.CrossSlide = { 1.90 + // ----------------------- 1.91 + // Gesture constants 1.92 + Thresholds: CrossSlideThresholds, 1.93 + State: CrossSlidingState, 1.94 + StateNames: CrossSlidingStateNames, 1.95 + // ----------------------- 1.96 + speedbump: speedbump 1.97 +}; 1.98 + 1.99 +function CrossSlideHandler(aNode, aThresholds) { 1.100 + this.node = aNode; 1.101 + this.thresholds = Object.create(CrossSlideThresholds); 1.102 + // apply per-instance threshold configuration 1.103 + if (aThresholds) { 1.104 + for(let key in aThresholds) 1.105 + this.thresholds[key] = aThresholds[key]; 1.106 + } 1.107 + aNode.addEventListener("touchstart", this, false); 1.108 + aNode.addEventListener("touchmove", this, false); 1.109 + aNode.addEventListener("touchend", this, false); 1.110 + aNode.addEventListener("touchcancel", this, false); 1.111 + aNode.ownerDocument.defaultView.addEventListener("scroll", this, false); 1.112 +} 1.113 + 1.114 +CrossSlideHandler.prototype = { 1.115 + node: null, 1.116 + drag: null, 1.117 + 1.118 + getCrossSlideState: function(aCrossAxisDistance, aScrollAxisDistance) { 1.119 + if (aCrossAxisDistance <= 0) { 1.120 + return CrossSlidingState.STARTED; 1.121 + } 1.122 + if (aCrossAxisDistance < this.thresholds.SELECTIONSTART) { 1.123 + return CrossSlidingState.DRAGGING; 1.124 + } 1.125 + if (aCrossAxisDistance < this.thresholds.SPEEDBUMPSTART) { 1.126 + return CrossSlidingState.SELECTING; 1.127 + } 1.128 + if (aCrossAxisDistance < this.thresholds.SPEEDBUMPEND) { 1.129 + return CrossSlidingState.SELECT_SPEED_BUMPING; 1.130 + } 1.131 + if (aCrossAxisDistance < this.thresholds.REARRANGESTART) { 1.132 + return CrossSlidingState.SPEED_BUMPING; 1.133 + } 1.134 + // out of bounds cross-slide 1.135 + return -1; 1.136 + }, 1.137 + 1.138 + handleEvent: function handleEvent(aEvent) { 1.139 + switch (aEvent.type) { 1.140 + case "touchstart": 1.141 + this._onTouchStart(aEvent); 1.142 + break; 1.143 + case "touchmove": 1.144 + this._onTouchMove(aEvent); 1.145 + break; 1.146 + case "scroll": 1.147 + case "touchcancel": 1.148 + this.cancel(aEvent); 1.149 + break; 1.150 + case "touchend": 1.151 + this._onTouchEnd(aEvent); 1.152 + break; 1.153 + } 1.154 + }, 1.155 + 1.156 + cancel: function(aEvent){ 1.157 + this._fireProgressEvent("cancelled", aEvent); 1.158 + this.drag = null; 1.159 + }, 1.160 + 1.161 + _onTouchStart: function onTouchStart(aEvent){ 1.162 + if (aEvent.touches.length > 1) 1.163 + return; 1.164 + let touch = aEvent.touches[0]; 1.165 + // cross-slide is a single touch gesture 1.166 + // the top target is the one we need here, touch.target not relevant 1.167 + let target = aEvent.target; 1.168 + 1.169 + if (!isSelectable(target)) 1.170 + return; 1.171 + 1.172 + let scrollAxis = getScrollAxisFromElement(target); 1.173 + 1.174 + this.drag = { 1.175 + scrollAxis: scrollAxis, 1.176 + crossAxis: (scrollAxis=='x') ? 'y' : 'x', 1.177 + origin: pointFromTouchEvent(aEvent), 1.178 + state: -1 1.179 + }; 1.180 + }, 1.181 + 1.182 + _onTouchMove: function(aEvent){ 1.183 + if (!this.drag) { 1.184 + return; 1.185 + } 1.186 + 1.187 + if (aEvent.touches.length!==1) { 1.188 + // cancel if another touch point gets involved 1.189 + return this.cancel(aEvent); 1.190 + } 1.191 + 1.192 + let startPt = this.drag.origin; 1.193 + let endPt = this.drag.position = pointFromTouchEvent(aEvent); 1.194 + 1.195 + let scrollAxis = this.drag.scrollAxis, 1.196 + crossAxis = this.drag.crossAxis; 1.197 + 1.198 + // distance from the origin along the axis perpendicular to scrolling 1.199 + let crossAxisDistance = Math.abs(endPt[crossAxis] - startPt[crossAxis]); 1.200 + // distance along the scrolling axis 1.201 + let scrollAxisDistance = Math.abs(endPt[scrollAxis] - startPt[scrollAxis]); 1.202 + let currState = this.drag.state; 1.203 + let newState = this.getCrossSlideState(crossAxisDistance, scrollAxisDistance); 1.204 + 1.205 + switch (newState) { 1.206 + case -1 : 1.207 + // dodgy input/out of bounds 1.208 + return this.cancel(aEvent); 1.209 + case CrossSlidingState.STARTED : 1.210 + break; 1.211 + case CrossSlidingState.DRAGGING : 1.212 + if (scrollAxisDistance > this.thresholds.SELECTIONSTART) { 1.213 + // looks like a pan/scroll was intended 1.214 + return this.cancel(aEvent); 1.215 + } 1.216 + // else fall-thru' 1.217 + case CrossSlidingState.SELECTING : 1.218 + case CrossSlidingState.SELECT_SPEED_BUMPING : 1.219 + case CrossSlidingState.SPEED_BUMPING : 1.220 + // we're committed to a cross-slide gesture, 1.221 + // so going out of bounds at this point means aborting 1.222 + if (!withinCone(crossAxisDistance, scrollAxisDistance)) { 1.223 + return this.cancel(aEvent); 1.224 + } 1.225 + // we're mid-gesture, consume this event 1.226 + aEvent.stopPropagation(); 1.227 + break; 1.228 + } 1.229 + 1.230 + if (currState !== newState) { 1.231 + this.drag.state = newState; 1.232 + this._fireProgressEvent( CrossSlidingStateNames[newState], aEvent ); 1.233 + } 1.234 + }, 1.235 + _onTouchEnd: function(aEvent){ 1.236 + if (!this.drag) 1.237 + return; 1.238 + 1.239 + if (this.drag.state < CrossSlidingState.SELECTING) { 1.240 + return this.cancel(aEvent); 1.241 + } 1.242 + 1.243 + this._fireProgressEvent("completed", aEvent); 1.244 + this._fireSelectEvent(aEvent); 1.245 + this.drag = null; 1.246 + }, 1.247 + 1.248 + /** 1.249 + * Dispatches a custom Event on the drag node. 1.250 + * @param aEvent The source event. 1.251 + * @param aType The event type. 1.252 + */ 1.253 + _fireProgressEvent: function CrossSliding_fireEvent(aState, aEvent) { 1.254 + if (!this.drag) 1.255 + return; 1.256 + let event = this.node.ownerDocument.createEvent("Events"); 1.257 + let crossAxisName = this.drag.crossAxis; 1.258 + event.initEvent("MozCrossSliding", true, true); 1.259 + event.crossSlidingState = aState; 1.260 + if ('position' in this.drag) { 1.261 + event.position = this.drag.position; 1.262 + if (crossAxisName) { 1.263 + event.direction = crossAxisName; 1.264 + if('origin' in this.drag) { 1.265 + event.delta = this.drag.position[crossAxisName] - this.drag.origin[crossAxisName]; 1.266 + } 1.267 + } 1.268 + } 1.269 + aEvent.target.dispatchEvent(event); 1.270 + }, 1.271 + 1.272 + /** 1.273 + * Dispatches a custom Event on the given target node. 1.274 + * @param aEvent The source event. 1.275 + */ 1.276 + _fireSelectEvent: function SelectTarget_fireEvent(aEvent) { 1.277 + let event = this.node.ownerDocument.createEvent("Events"); 1.278 + event.initEvent("MozCrossSlideSelect", true, true); 1.279 + event.position = this.drag.position; 1.280 + aEvent.target.dispatchEvent(event); 1.281 + } 1.282 +}; 1.283 +this.CrossSlide.Handler = CrossSlideHandler;