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

     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;

mercurial