SOURCE CODE: Uize.Widget.Drag
VIEW REFERENCE

/*______________
|       ______  |   U I Z E    J A V A S C R I P T    F R A M E W O R K
|     /      /  |   ---------------------------------------------------
|    /    O /   |    MODULE : Uize.Widget.Drag Class
|   /    / /    |
|  /    / /  /| |    ONLINE : http://www.uize.com
| /____/ /__/_| | COPYRIGHT : (c)2005-2012 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://www.uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 6
  codeCompleteness: 100
  testCompleteness: 0
  docCompleteness: 2
*/

/*?
  Introduction
    The =Uize.Widget.Drag= class implements support for managing drag operations and draggable nodes - slider / scrollbar knobs, resizer drag handles, etc.

    *DEVELOPERS:* `Chris van Rensburg`, `Jan Borgersen`
*/

Uize.module ({
  name:'Uize.Widget.Drag',
  required:[
    'Uize.Node',
    'Uize.Node.Event'
  ],
  builder:function  (_superclass) {
    /*** Variables for Scruncher Optimization ***/
      var
        _undefined,
        _true = true,
        _false = false,
        _Uize_Node = Uize.Node,
        _Uize_Node_getEventAbsPos = _Uize_Node.getEventAbsPos,
        _isIe = _Uize_Node.isIe
      ;

    /*** General Variables ***/
      var
        _dragShield,
        _hasStickyDragIssue = _false,
        _useFixedPositioningForShield = _false
      ;
      if (typeof navigator != 'undefined') {
        var _ieMajorVersion = _Uize_Node.ieMajorVersion;
        _hasStickyDragIssue = _isIe && _ieMajorVersion < 9;
        _useFixedPositioningForShield =
          (!_isIe || _ieMajorVersion > 6) && navigator.userAgent.indexOf ('Firefox/2') < 0
        ;
      }

    /*** Class Constructor ***/
      var
        _class = _superclass.subclass (
          function () {
            var _this = this;

            /*** Public Instance Properties ***/
              _this.eventStartPos = _this._eventStartPos = [0,0];
              _this.eventPos = _this._eventPos = [0,0];
              _this._eventPreviousPos = [0,0];
              _this._eventTime = _this._eventPreviousTime = 0;
              _this.eventDeltaPos = _this._eventDeltaPos = [0,0];
          }
        ),
        _classPrototype = _class.prototype
      ;

    /*** Private Instance Methods ***/
      _classPrototype._fireDragRestEvent = function () {
        this._dragRestTimeout = null;
        this.fire ('Drag Rest');
      };

      _classPrototype._flushDragRestTimeout = function () {
        this._dragRestTimeout && clearTimeout (this._dragRestTimeout);
        this._dragRestTimeout = null;
      };

      _classPrototype._updateUiCursor = function () {
        var _this = this;
        if (_this.isWired) {
          var _node = _this.getNode ();
          _this._cursor
            ? _Uize_Node.setStyle (
              _this._inDrag ? [_node,_dragShield] : _node,
              {cursor:_this.get ('enabledInherited') ? _this._cursor : 'not-allowed'}
            )
            : _this.set ({_cursor:_Uize_Node.getStyle (_node,'cursor')})
          ;
        }
      };

    /*** Public Instance Methods ***/
      _classPrototype.initiate = _classPrototype.mousedown = function (_event,_notRelayed) {
        var
          _this = this,
          _eventStartPos = _this._eventStartPos,
          _eventPos = _this._eventPos,
          _eventPreviousPos = _this._eventPreviousPos
        ;

        function _dragDone (_event) {
          if (_this._inDrag) {
            if (_this._fade) {
              _this._fade.stop ();
              _this._fade = _undefined;
            }
            _dragIsDone = _true;
            if (_this._dragRestTimeout) {
              _this._flushDragRestTimeout ();
              _this._fireDragRestEvent ();
            }
            _this.set ({
              _inCancel:_false,
              _inDrag:_false,
              _inReleaseTravel:_false,
              _isTouch:_false
            });
            _this.fire ({name:'Drag Done',domEvent:_event});
            _this.set ({
              _dragCancelled:_false,
              _dragStarted:_false
            });
            if (_isTouch) {
              _this.unwireNode (
                _notRelayed ? '' : _event.target,
                {touchmove:null,touchend:null,touchcancel:null}
              );
              /* TODO:
                Consider why unwiring touch events makes sense *here*, but restoring document mouse events only makes sense in _cleanupAfterMouseDrag function. What am I missing here that it needs to be this way?
              */
            }
          }
        }

        function _fadeDragMoveTo (_phasePropertyName,_endPos,_duration,_fadeProperties) {
          _this.set (_phasePropertyName,_true);
          (
            _this._fade = Uize.Fade.fade (
              _dragMove,
              [_eventPos [0],_eventPos [1]],
              _endPos,
              _duration,
              _fadeProperties
            )
          )
            .wire ('Done',function () {_dragDone (_event)})
          ;
        }

        function _endDragWithPossibleReleaseTravel (_event) {
          if (_this._releaseTravel && Uize.Fade && Uize.Fade.fade) {
            /*
              QUESTION: do we need to have state like dragCancelled that disables user interaction during release travel phase?
            */
            var
              _eventDistanceX = _eventPos [0] - _eventPreviousPos [0],
              _eventDistanceY = _eventPos [1] - _eventPreviousPos [1]
            ;
            if (_eventDistanceX || _eventDistanceY) {
              var
                _eventDistance = Math.sqrt (Math.pow (_eventDistanceX,2) + Math.pow (_eventDistanceY,2)),
                _releaseTravelProperties = _this._releaseTravel (
                  _eventDistance / ((_this._eventTime - _this._eventPreviousTime) || 1) * 1000
                ),
                _eventDistanceFactor = 1 + (_releaseTravelProperties.distance / _eventDistance)
              ;
              _fadeDragMoveTo (
                'inReleaseTravel',
                [
                  _eventPreviousPos [0] + _eventDistanceX * _eventDistanceFactor,
                  _eventPreviousPos [1] + _eventDistanceY * _eventDistanceFactor
                ],
                _releaseTravelProperties.duration * 1000,
                {curve:_releaseTravelProperties.curve}
              );
              return;
            }
          }
          _dragDone (_event);
        }

        function _dragMove (_eventX,_eventY) {
          _this._eventPreviousTime = _this._eventTime;
          _this._eventTime = Uize.now ();
          _eventPreviousPos [0] = _eventPos [0];
          _eventPreviousPos [1] = _eventPos [1];
          var
            _eventDeltaPos = [
              (_eventPos [0] = _eventX) - _eventStartPos [0],
              (_eventPos [1] = _eventY) - _eventStartPos [1]
            ],
            _absEventDeltaPos = [Math.abs (_eventDeltaPos [0]),Math.abs (_eventDeltaPos [1])]
          ;
          for (var _axis = -1; ++_axis < 2;)
            _this._eventDeltaPos [_axis] =
              (
                _this._dragAxisMode == 'both' ||
                _absEventDeltaPos [_axis] > _absEventDeltaPos [1 - _axis] ||
                (_absEventDeltaPos [_axis] == _absEventDeltaPos [1 - _axis] && _axis == 1)
              )
              ? _eventDeltaPos [_axis] : 0
          ;
          _this.fire ('Drag Update');

          _this._flushDragRestTimeout ();
          _this._dragRestTimeout = setTimeout (function () {_this._fireDragRestEvent ()},_this._dragRestTime);
        }

        function _handleMoveEvent (_event) {
          if (!_dragIsDone && !_this._dragCancelled) {
            if (!_this._dragStarted) {
              if (!_isTouch) {
                _class.resizeShield (_dragShield);
                _Uize_Node.display (_dragShield);
              }
              _this.set ({_dragStarted:_true});
              _this.fire ({name:'Drag Start',domEvent:_event});
            }
            var _dragEventPos = _Uize_Node_getEventAbsPos (_event);
            _dragMove (_dragEventPos.left,_dragEventPos.top);
          }
        }

        function _cancelDrag (_event) {
          _this.set ({_dragCancelled:_true});
          if (_this._cancelFade && Uize.Fade && Uize.Fade.fade) {
            _fadeDragMoveTo ('inCancel',_eventStartPos,500,_this._cancelFade);
          } else {
            _dragMove (_eventStartPos [0],_eventStartPos [1]);
            _dragDone (_event);
          }
        }

        if (_this._inCancel || _this._inReleaseTravel)
          _dragDone (_event)
        ;
        if (!_this._inDrag && _this.get ('enabledInherited')) {
          var _isTouch = !!_event.targetTouches;

          _this.set ({_inDrag:_true,_isTouch:_isTouch});
          _this._updateUiCursor ();
          Uize.Node.Event.abort (_event);
          _this._dragAxisMode = _event.shiftKey ? 'one' : 'both';

          _this.fire ({name:'Before Drag Start',domEvent:_event});

          var _dragEventPos = _Uize_Node_getEventAbsPos (_event);
          _eventStartPos [0] = _eventPos [0] = _eventPreviousPos [0] = _dragEventPos.left;
          _eventStartPos [1] = _eventPos [1] = _eventPreviousPos [1] = _dragEventPos.top;
          _this._eventTime = _this._eventPreviousTime = Uize.now ();

          var
            _dragIsDone = _false,
            _oldDocumentEvents
          ;
          if (!_isTouch)
            _oldDocumentEvents = {
              onkeyup:document.onkeyup,
              onmousemove:document.onmousemove,
              onmouseup:document.onmouseup
            }
          ;
          if (_isTouch) {
            _this.wireNode (
              _notRelayed ? '' : _event.target,
              {
                touchmove:function (_event) {
                  _event.preventDefault ();
                  _handleMoveEvent (_event);
                },
                touchend:function (_event) {
                  _event.preventDefault ();
                  _endDragWithPossibleReleaseTravel (_event);
                },
                touchcancel:function (_event) {
                  _event.preventDefault ();
                  _cancelDrag (_event);
                }
              }
            );
          } else {
            function _cleanupAfterMouseDrag (_event) {
              Uize.copyInto (document,_oldDocumentEvents);
              _Uize_Node.display (_dragShield,_false);
              _this._dragCancelled || _endDragWithPossibleReleaseTravel (_event);
            }
            document.onmousemove = function (_event) {
              _event || (_event = window.event);
              _hasStickyDragIssue && _event.button == 0
                ? _this._inDrag && _cleanupAfterMouseDrag (_event)
                  /* NOTE:
                    when the user mouses up outside of the document area, the onmouseup event is not fired, so this is a way to catch the next mouse move inside the document where no mouse button is depressed -- can't do this in Firefox, because Firefox doesn't update the value of the button property for each onmousemove event
                  */
                : _handleMoveEvent (_event)
              ;
              return _false;
            };
            document.onmouseup = function (_event) {
              _cleanupAfterMouseDrag (_event || window.event);
              return _false;
            };
            document.onkeyup = function (_event) {
              Uize.Node.Event.isKeyEscape (_event) && _this._inDrag && _cancelDrag (_event);
            };
          }
        }
        return _false;
      };

      _classPrototype.updateUi = function () {
        var _this = this;
        _this.isWired && !_this.get ('enabledInherited') || _this._cursor && _this._updateUiCursor ();
      };

      _classPrototype.wireUi = function () {
        var _this = this;
        if (!_this.isWired) {
          var _rootNode = _this.getNode ();
          if (_rootNode) {
            _rootNode.onmousedown = Uize.returnFalse;
            function _initiate (_event) {return _this.initiate (_event,_true)}
            _this.wireNode (_rootNode,{mousedown:_initiate,touchstart:_initiate});
          }
          if (!_dragShield) {
            _dragShield = _class.insertShield ({zIndex:50000});
            _useFixedPositioningForShield ||
              _Uize_Node.wire (window,'resize',function () {_class.resizeShield (_dragShield)})
            ;
          }
          _this.wire ({'Changed.enabledInherited':function () {_this._updateUiCursor ()}});

          _superclass.prototype.wireUi.call (_this);
        }
      };

    /*** Public Static Methods ***/
      _class.insertShield = function (_extraStyleProperties) {
        var _styleProperties = {
          display:'none',
          position:'absolute'
        };
        if (_isIe)
          _styleProperties.background = 'url(' + _class.getBlankImageUrl () + ')'
          /* NOTE:
            using a transparent image for the background of the drag shield DIV is not necessary in Firefox and slows rendering on drag refreshes
          */
        ;
        var _shield = document.createElement ('div');
        _Uize_Node.setStyle (_shield,Uize.copyInto (_styleProperties,_extraStyleProperties));
        _shield.Uize_Widget_Drag_shield = _true;
        document.body.appendChild (_shield);
        _class.resizeShield (_shield);
        return _shield;
      };

      _class.resizeShield = function (_shield) {
        /* TO DO:
          - for browsers that support fixed positioning, updating the position and size only needs to happen once: at the time of initializing the shield. For IE6, we need to watch document scroll and resize. The best way to clean this up and factor it out would be to create a shield widget. Uize.Widget.Drag could share one instance, and instances of Uize.Widget.Dialog could each create their own.
        */
        if (_useFixedPositioningForShield) {
          _Uize_Node.setStyle (
            _shield,
            {
              left:'0',
              top:'0',
              width:'100%',
              height:'100%',
              position:'fixed'
            }
          );
        } else {
          /* NOTE:
            This is a workaround for IE6 (which doesn't support fixed positioning) and should be killed as soon as possible.
          */
          /*
            TO DO: just the following will work better, if we watch scroll events and reposition each time...

              var _documentElement = document.documentElement;
              _Uize_Node.setStyle (
                _shield,
                {
                  left:'0',
                  top:'0',
                  width:_documentElement.clientWidth + 'px',
                  height:_documentElement.clientHeight + 'px'
                }
              );
          */
          var
            _shieldStyleDisplay = _Uize_Node.getStyle (_shield,'display'),
            _documentElement = document.documentElement,
            _documentBody = document.body
          ;
          _Uize_Node.display (_shield,_false);
          _Uize_Node.setStyle (
            _shield,
            {
              left:0,
              top:0,
              width:_documentElement.scrollWidth,
              height:
                Math.max (
                  typeof window.innerHeight =='number' ? window.innerHeight : (_documentElement && _documentElement.clientHeight ? _documentElement.clientHeight : (_documentBody && _documentBody.clientHeight ? _documentBody.clientHeight : 0)),
                  _documentElement.scrollHeight
                ),
              display:_shieldStyleDisplay
            }
          );
        }
      };

    /*** Register Properties ***/
      _class.registerProperties ({
        _animation:{
          name:'animation',
          onChange:function () {this.set ({_cancelFade:this._animation ? {duration:500} : _undefined})}
        },
        _cancelFade:'cancelFade',
        _cursor:{
          name:'cursor',
          onChange:_classPrototype._updateUiCursor
        },
        _dragCancelled:'dragCancelled',
        _dragRestTime:{
          name:'dragRestTime',
          value:250
        },
        _dragStarted:'dragStarted',
        _inCancel:'inCancel',
        _inDrag:'inDrag',
        _inReleaseTravel:'inReleaseTravel',
        _isTouch:'isTouch',
        _releaseTravel:'releaseTravel'
      });

    return _class;
  }
});