2 Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
8 YUI.add('dd-drag', function(Y) {
12 * The Drag & Drop Utility allows you to create a draggable interface efficiently, buffering you from browser-level abnormalities and enabling you to focus on the interesting logic surrounding your particular implementation. This component enables you to create a variety of standard draggable objects with just a few lines of code and then, using its extensive API, add your own specific implementation logic.
17 * This class provides the ability to drag a Node.
26 DRAGGING = 'dragging',
27 DRAG_NODE = 'dragNode',
28 OFFSET_HEIGHT = 'offsetHeight',
29 OFFSET_WIDTH = 'offsetWidth',
31 MOUSE_DOWN = 'mousedown',
32 DRAG_START = 'dragstart',
34 * @event drag:mouseDown
35 * @description Handles the mousedown DOM event, checks to see if you have a valid handle then starts the drag timers.
36 * @preventable _defMouseDownFn
37 * @param {Event.Facade} ev The mousedown event.
39 * @type {Event.Custom}
41 EV_MOUSE_DOWN = 'drag:mouseDown',
43 * @event drag:afterMouseDown
44 * @description Fires after the mousedown event has been cleared.
45 * @param {Event.Facade} ev The mousedown event.
47 * @type {Event.Custom}
49 EV_AFTER_MOUSE_DOWN = 'drag:afterMouseDown',
51 * @event drag:removeHandle
52 * @description Fires after a handle is removed.
54 * @type {Event.Custom}
56 EV_REMOVE_HANDLE = 'drag:removeHandle',
58 * @event drag:addHandle
59 * @description Fires after a handle is added.
61 * @type {Event.Custom}
63 EV_ADD_HANDLE = 'drag:addHandle',
65 * @event drag:removeInvalid
66 * @description Fires after an invalid selector is removed.
68 * @type {Event.Custom}
70 EV_REMOVE_INVALID = 'drag:removeInvalid',
72 * @event drag:addInvalid
73 * @description Fires after an invalid selector is added.
75 * @type {Event.Custom}
77 EV_ADD_INVALID = 'drag:addInvalid',
80 * @description Fires at the start of a drag operation.
82 * @type {Event.Custom}
84 EV_START = 'drag:start',
87 * @description Fires at the end of a drag operation.
89 * @type {Event.Custom}
94 * @description Fires every mousemove during a drag operation.
96 * @type {Event.Custom}
98 EV_DRAG = 'drag:drag',
101 * @preventable _defAlignFn
102 * @description Fires when this node is aligned.
104 * @type {Event.Custom}
106 EV_ALIGN = 'drag:align',
109 * @description Fires when this node is over a Drop Target. (Fired from dd-drop)
111 * @type {Event.Custom}
115 * @description Fires when this node enters a Drop Target. (Fired from dd-drop)
117 * @type {Event.Custom}
121 * @description Fires when this node exits a Drop Target. (Fired from dd-drop)
123 * @type {Event.Custom}
126 * @event drag:drophit
127 * @description Fires when this node is dropped on a valid Drop Target. (Fired from dd-ddm-drop)
129 * @type {Event.Custom}
132 * @event drag:dropmiss
133 * @description Fires when this node is dropped on an invalid Drop Target. (Fired from dd-ddm-drop)
135 * @type {Event.Custom}
139 this._lazyAddAttrs = false;
140 Drag.superclass.constructor.apply(this, arguments);
142 var valid = DDM._regDrag(this);
144 Y.error('Failed to register node, already in use: ' + o.node);
153 * @description Y.Node instanace to use as the element to initiate a drag operation
157 setter: function(node) {
160 Y.error('DD.Drag: Invalid Node Given: ' + node);
168 * @attribute dragNode
169 * @description Y.Node instanace to use as the draggable element, defaults to node
173 setter: function(node) {
174 var n = Y.Node.get(node);
176 Y.error('DD.Drag: Invalid dragNode Given: ' + node);
182 * @attribute offsetNode
183 * @description Offset the drag element by the difference in cursor position: default true
190 * @attribute clickPixelThresh
191 * @description The number of pixels to move to start a drag operation, default is 3.
195 value: DDM.get('clickPixelThresh')
198 * @attribute clickTimeThresh
199 * @description The number of milliseconds a mousedown has to pass to start a drag operation, default is 1000.
203 value: DDM.get('clickTimeThresh')
207 * @description Set to lock this drag element so that it can't be dragged: default false.
212 setter: function(lock) {
214 this.get(NODE).addClass(DDM.CSS_PREFIX + '-locked');
216 this.get(NODE).removeClass(DDM.CSS_PREFIX + '-locked');
223 * @description A payload holder to store arbitrary data about this drag object, can be used to store any value.
231 * @description If this is false, the drag element will not move with the cursor: default true. Can be used to "resize" the element.
239 * @description Use the protective shim on all drag operations: default true. Only works with dd-ddm, not dd-ddm-base.
246 * @attribute activeHandle
247 * @description This config option is set by Drag to inform you of which handle fired the drag event (in the case that there are several handles): default false.
254 * @attribute primaryButtonOnly
255 * @description By default a drag operation will only begin if the mousedown occurred with the primary mouse button. Setting this to false will allow for all mousedown events to trigger a drag.
262 * @attribute dragging
263 * @description This attribute is not meant to be used by the implementor, it is meant to be used as an Event tracker so you can listen for it to change.
274 * @description This attribute only works if the dd-drop module has been loaded. It will make this node a drop target as well as draggable.
279 setter: function(config) {
280 this._handleTarget(config);
285 * @attribute dragMode
286 * @description This attribute only works if the dd-drop module is active. It will set the dragMode (point, intersect, strict) of this Drag instance.
291 setter: function(mode) {
292 return DDM._setDragMode(mode);
297 * @description Array of groups to add this drag into.
307 Y.each(this._groups, function(v, k) {
312 setter: function(g) {
314 Y.each(g, function(v, k) {
315 this._groups[v] = true;
322 * @description Array of valid handles to add. Adding something here will set all handles, even if previously added with addHandle
327 setter: function(g) {
330 Y.each(g, function(v, k) {
331 this._handles[v] = true;
334 this._handles = null;
341 * @description Controls the default bubble parent for this Drag instance. Default: Y.DD.DDM. Set to false to disable bubbling.
350 Y.extend(Drag, Y.Base, {
353 * @description Add this Drag instance to a group, this should be used for on-the-fly group additions.
354 * @param {String} g The group to add this Drag Instance to.
358 addToGroup: function(g) {
359 this._groups[g] = true;
360 DDM._activateTargets();
364 * @method removeFromGroup
365 * @description Remove this Drag instance from a group, this should be used for on-the-fly group removals.
366 * @param {String} g The group to remove this Drag Instance from.
370 removeFromGroup: function(g) {
371 delete this._groups[g];
372 DDM._activateTargets();
377 * @description This will be a reference to the Drop instance associated with this drag if the target: true config attribute is set..
383 * @method _handleTarget
384 * @description Attribute handler for the target config attribute.
385 * @param {Boolean/Object}
386 * @return {Boolean/Object}
388 _handleTarget: function(config) {
390 if (config === false) {
392 DDM._unregTarget(this.target);
397 if (!Y.Lang.isObject(config)) {
400 config.bubbles = ('bubbles' in config) ? config.bubbles : this.get('bubbles');
401 config.node = this.get(NODE);
402 config.groups = config.groups || this.get('groups');
403 this.target = new Y.DD.Drop(config);
412 * @description Storage Array for the groups this drag belongs to.
418 * @method _createEvents
419 * @description This method creates all the events for this Event Target and publishes them so we get Event Bubbling.
421 _createEvents: function() {
423 this.publish(EV_MOUSE_DOWN, {
424 defaultFn: this._defMouseDownFn,
431 this.publish(EV_ALIGN, {
432 defaultFn: this._defAlignFn,
439 this.publish(EV_DRAG, {
440 defaultFn: this._defDragFn,
447 this.publish(EV_END, {
448 preventedFn: this._prevEndFn,
469 Y.each(ev, function(v, k) {
480 if (this.get('bubbles')) {
481 this.addTarget(this.get('bubbles'));
489 * @description A private reference to the mousedown DOM event
490 * @type {Event.Facade}
495 * @property _startTime
496 * @description The getTime of the mousedown event. Not used, just here in case someone wants/needs to use it.
503 * @description The getTime of the mouseup event. Not used, just here in case someone wants/needs to use it.
510 * @description A private hash of the valid drag handles
516 * @property _invalids
517 * @description A private hash of the invalid selector strings
523 * @property _invalidsDefault
524 * @description A private hash of the default invalid selector strings: {'textarea': true, 'input': true, 'a': true, 'button': true, 'select': true}
527 _invalidsDefault: {'textarea': true, 'input': true, 'a': true, 'button': true, 'select': true },
530 * @property _dragThreshMet
531 * @description Private flag to see if the drag threshhold was met
534 _dragThreshMet: null,
537 * @property _fromTimeout
538 * @description Flag to determine if the drag operation came from a timeout
544 * @property _clickTimeout
545 * @description Holder for the setTimeout call
551 * @description The offset of the mouse position to the element's position
557 * @description The initial mouse position
563 * @description The initial element position
569 * @description The position of the element as it's moving (for offset calculations)
575 * @description The xy that the node will be set to. Changing this will alter the position as it's dragged.
581 * @description The real xy position of the node.
587 * @description The XY coords of the mousemove
593 * @description A region object associated with this drag, used for checking regions while dragging.
599 * @method _handleMouseUp
600 * @description Handler for the mouseup DOM event
601 * @param {Event.Facade}
603 _handleMouseUp: function(ev) {
604 this._fixIEMouseUp();
605 if (DDM.activeDrag) {
611 * @method _fixDragStart
612 * @description The function we use as the ondragstart handler when we start a drag in Internet Explorer. This keeps IE from blowing up on images as drag handles.
614 _fixDragStart: function(e) {
619 * @method _ieSelectFix
620 * @description The function we use as the onselectstart handler when we start a drag in Internet Explorer
622 _ieSelectFix: function() {
627 * @property _ieSelectBack
628 * @description We will hold a copy of the current "onselectstart" method on this property, and reset it after we are done using it.
633 * @method _fixIEMouseDown
634 * @description This method copies the onselectstart listner on the document to the _ieSelectFix property
636 _fixIEMouseDown: function() {
638 this._ieSelectBack = Y.config.doc.body.onselectstart;
639 Y.config.doc.body.onselectstart = this._ieSelectFix;
644 * @method _fixIEMouseUp
645 * @description This method copies the _ieSelectFix property back to the onselectstart listner on the document.
647 _fixIEMouseUp: function() {
649 Y.config.doc.body.onselectstart = this._ieSelectBack;
654 * @method _handleMouseDownEvent
655 * @description Handler for the mousedown DOM event
656 * @param {Event.Facade}
658 _handleMouseDownEvent: function(ev) {
659 this.fire(EV_MOUSE_DOWN, { ev: ev });
663 * @method _defMouseDownFn
664 * @description Handler for the mousedown DOM event
665 * @param {Event.Facade}
667 _defMouseDownFn: function(e) {
669 this._dragThreshMet = false;
672 if (this.get('primaryButtonOnly') && ev.button > 1) {
675 if (this.validClick(ev)) {
676 this._fixIEMouseDown();
678 this._setStartPosition([ev.pageX, ev.pageY]);
680 DDM.activeDrag = this;
682 this._clickTimeout = Y.later(this.get('clickTimeThresh'), this, this._timeoutCheck);
684 this.fire(EV_AFTER_MOUSE_DOWN, { ev: ev });
688 * @description Method first checks to see if we have handles, if so it validates the click against the handle. Then if it finds a valid handle, it checks it against the invalid handles list. Returns true if a good handle was used, false otherwise.
689 * @param {Event.Facade}
692 validClick: function(ev) {
693 var r = false, n = false,
699 Y.each(this._handles, function(i, n) {
700 if (Y.Lang.isString(n)) {
701 //Am I this or am I inside this
702 if (tar.test(n + ', ' + n + ' *') && !hTest) {
710 if (n.contains(tar) || n.compareTo(tar)) {
715 if (this._invalids) {
716 Y.each(this._invalids, function(i, n) {
717 if (Y.Lang.isString(n)) {
718 //Am I this or am I inside this
719 if (tar.test(n + ', ' + n + ' *')) {
728 els = ev.currentTarget.queryAll(hTest);
730 els.each(function(n, i) {
731 if ((n.contains(tar) || n.compareTo(tar)) && !set) {
733 this.set('activeHandle', n);
737 this.set('activeHandle', this.get(NODE));
744 * @method _setStartPosition
745 * @description Sets the current position of the Element and calculates the offset
746 * @param {Array} xy The XY coords to set the position to.
748 _setStartPosition: function(xy) {
751 this.nodeXY = this.lastXY = this.realXY = this.get(NODE).getXY();
753 if (this.get('offsetNode')) {
754 this.deltaXY = [(this.startXY[0] - this.nodeXY[0]), (this.startXY[1] - this.nodeXY[1])];
756 this.deltaXY = [0, 0];
761 * @method _timeoutCheck
762 * @description The method passed to setTimeout to determine if the clickTimeThreshold was met.
764 _timeoutCheck: function() {
765 if (!this.get('lock') && !this._dragThreshMet) {
766 this._fromTimeout = this._dragThreshMet = true;
768 this._alignNode([this._ev_md.pageX, this._ev_md.pageY], true);
772 * @method removeHandle
773 * @description Remove a Selector added by addHandle
774 * @param {String} str The selector for the handle to be removed.
778 removeHandle: function(str) {
779 if (this._handles[str]) {
780 delete this._handles[str];
781 this.fire(EV_REMOVE_HANDLE, { handle: str });
787 * @description Add a handle to a drag element. Drag only initiates when a mousedown happens on this element.
788 * @param {String} str The selector to test for a valid handle. Must be a child of the element.
792 addHandle: function(str) {
793 if (!this._handles) {
796 if (Y.Lang.isString(str)) {
797 this._handles[str] = true;
798 this.fire(EV_ADD_HANDLE, { handle: str });
803 * @method removeInvalid
804 * @description Remove an invalid handle added by addInvalid
805 * @param {String} str The invalid handle to remove from the internal list.
809 removeInvalid: function(str) {
810 if (this._invalids[str]) {
811 this._invalids[str] = null;
812 delete this._invalids[str];
813 this.fire(EV_REMOVE_INVALID, { handle: str });
819 * @description Add a selector string to test the handle against. If the test passes the drag operation will not continue.
820 * @param {String} str The selector to test against to determine if this is an invalid drag handle.
824 addInvalid: function(str) {
825 if (Y.Lang.isString(str)) {
826 this._invalids[str] = true;
827 this.fire(EV_ADD_INVALID, { handle: str });
833 * @method initializer
834 * @description Internal init handler
836 initializer: function() {
837 this.get(NODE).dd = this;
839 if (!this.get(NODE).get('id')) {
840 var id = Y.stamp(this.get(NODE));
841 this.get(NODE).set('id', id);
846 this._invalids = Y.clone(this._invalidsDefault, true);
848 this._createEvents();
850 if (!this.get(DRAG_NODE)) {
851 this.set(DRAG_NODE, this.get(NODE));
855 //Don't prep the DD instance until all plugins are loaded.
856 this.on('initializedChange', Y.bind(this._prep, this));
858 //Shouldn't have to do this..
859 this.set('groups', this.get('groups'));
864 * @description Attach event listners and add classname
867 this._dragThreshMet = false;
868 var node = this.get(NODE);
869 node.addClass(DDM.CSS_PREFIX + '-draggable');
870 node.on(MOUSE_DOWN, Y.bind(this._handleMouseDownEvent, this));
871 node.on(MOUSE_UP, Y.bind(this._handleMouseUp, this));
872 node.on(DRAG_START, Y.bind(this._fixDragStart, this));
877 * @description Detach event listeners and remove classname
879 _unprep: function() {
880 var node = this.get(NODE);
881 node.removeClass(DDM.CSS_PREFIX + '-draggable');
886 * @description Starts the drag operation
891 if (!this.get('lock') && !this.get(DRAGGING)) {
892 var node = this.get(NODE), ow = node.get(OFFSET_WIDTH), oh = node.get(OFFSET_HEIGHT);
893 this._startTime = (new Date()).getTime();
896 node.addClass(DDM.CSS_PREFIX + '-dragging');
897 this.fire(EV_START, {
898 pageX: this.nodeXY[0],
899 pageY: this.nodeXY[1],
900 startTime: this._startTime
902 var xy = this.nodeXY;
914 this.set(DRAGGING, true);
920 * @description Ends the drag operation
925 this._endTime = (new Date()).getTime();
926 if (this._clickTimeout) {
927 this._clickTimeout.cancel();
929 this._dragThreshMet = false;
930 this._fromTimeout = false;
931 if (!this.get('lock') && this.get(DRAGGING)) {
933 pageX: this.lastXY[0],
934 pageY: this.lastXY[1],
935 startTime: this._startTime,
936 endTime: this._endTime
939 this.get(NODE).removeClass(DDM.CSS_PREFIX + '-dragging');
940 this.set(DRAGGING, false);
941 this.deltaXY = [0, 0];
948 * @description Handler for preventing the drag:end event. It will reset the node back to it's start position
950 _prevEndFn: function(e) {
952 this.get(DRAG_NODE).setXY(this.nodeXY);
957 * @description Calculates the offsets and set's the XY that the element will move to.
958 * @param {Array} xy The xy coords to align with.
960 _align: function(xy) {
961 this.fire(EV_ALIGN, {pageX: xy[0], pageY: xy[1] });
965 * @method _defAlignFn
966 * @description Calculates the offsets and set's the XY that the element will move to.
967 * @param {Event.Facade} e The drag:align event.
969 _defAlignFn: function(e) {
970 this.actXY = [e.pageX - this.deltaXY[0], e.pageY - this.deltaXY[1]];
975 * @description This method performs the alignment before the element move.
976 * @param {Array} eXY The XY to move the element to, usually comes from the mousemove DOM event.
978 _alignNode: function(eXY) {
985 * @description This method performs the actual element move.
987 _moveNode: function(scroll) {
988 //if (!this.get(DRAGGING)) {
991 var diffXY = [], diffXY2 = [], startXY = this.nodeXY, xy = this.actXY;
993 diffXY[0] = (xy[0] - this.lastXY[0]);
994 diffXY[1] = (xy[1] - this.lastXY[1]);
996 diffXY2[0] = (xy[0] - this.nodeXY[0]);
997 diffXY2[1] = (xy[1] - this.nodeXY[1]);
1005 right: xy[0] + this.get(DRAG_NODE).get(OFFSET_WIDTH),
1006 bottom: xy[1] + this.get(DRAG_NODE).get(OFFSET_HEIGHT),
1010 this.fire(EV_DRAG, {
1026 * @method _defDragFn
1027 * @description Default function for drag:drag. Fired from _moveNode.
1028 * @param {Event.Facade} ev The drag:drag event
1030 _defDragFn: function(e) {
1031 if (this.get('move')) {
1033 e.scroll.node.set('scrollTop', e.scroll.top);
1034 e.scroll.node.set('scrollLeft', e.scroll.left);
1036 this.get(DRAG_NODE).setXY([e.pageX, e.pageY]);
1037 this.realXY = [e.pageX, e.pageY];
1043 * @description Fired from DragDropMgr (DDM) on mousemove.
1044 * @param {Event.Facade} ev The mousemove DOM event
1046 _move: function(ev) {
1047 if (this.get('lock')) {
1050 this.mouseXY = [ev.pageX, ev.pageY];
1051 if (!this._dragThreshMet) {
1052 var diffX = Math.abs(this.startXY[0] - ev.pageX),
1053 diffY = Math.abs(this.startXY[1] - ev.pageY);
1054 if (diffX > this.get('clickPixelThresh') || diffY > this.get('clickPixelThresh')) {
1055 this._dragThreshMet = true;
1057 this._alignNode([ev.pageX, ev.pageY]);
1060 if (this._clickTimeout) {
1061 this._clickTimeout.cancel();
1063 this._alignNode([ev.pageX, ev.pageY]);
1069 * @description Method will forcefully stop a drag operation. For example calling this from inside an ESC keypress handler will stop this drag.
1073 stopDrag: function() {
1074 if (this.get(DRAGGING)) {
1081 * @method destructor
1082 * @description Lifecycle destructor, unreg the drag from the DDM and remove listeners
1084 destructor: function() {
1088 this.target.destroy();
1090 DDM._unregDrag(this);
1100 }, '3.0.0' ,{requires:['dd-ddm-base'], skinnable:false});