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