2
Copyright (c) 2008, Yahoo! Inc. All rights reserved.
3
Code licensed under the BSD License:
4
http://developer.yahoo.net/yui/license.txt
7
YUI.add('dd-drag', function(Y) {
10
* 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.
15
* This class provides the ability to drag a Node.
23
DRAG_NODE = 'dragNode',
24
OFFSET_HEIGHT = 'offsetHeight',
25
OFFSET_WIDTH = 'offsetWidth',
27
MOUSE_DOWN = 'mousedown',
29
* @event drag:mouseDown
30
* @description Handles the mousedown DOM event, checks to see if you have a valid handle then starts the drag timers.
31
* @preventable _handleMouseDown
32
* @param {Event} ev The mousedown event.
36
EV_MOUSE_DOWN = 'drag:mouseDown',
38
* @event drag:afterMouseDown
39
* @description Fires after the mousedown event has been cleared.
40
* @param {Event} ev The mousedown event.
44
EV_AFTER_MOUSE_DOWN = 'drag:afterMouseDown',
46
* @event drag:removeHandle
47
* @description Fires after a handle is removed.
51
EV_REMOVE_HANDLE = 'drag:removeHandle',
53
* @event drag:addHandle
54
* @description Fires after a handle is added.
58
EV_ADD_HANDLE = 'drag:addHandle',
60
* @event drag:removeInvalid
61
* @description Fires after an invalid selector is removed.
65
EV_REMOVE_INVALID = 'drag:removeInvalid',
67
* @event drag:addInvalid
68
* @description Fires after an invalid selector is added.
72
EV_ADD_INVALID = 'drag:addInvalid',
75
* @description Fires at the start of a drag operation.
79
EV_START = 'drag:start',
82
* @description Fires at the end of a drag operation.
89
* @description Fires every mousemove during a drag operation.
93
EV_DRAG = 'drag:drag';
98
* @description Fires when this node is over a Drop Target. (Fired from dd-drop)
104
* @description Fires when this node enters a Drop Target. (Fired from dd-drop)
110
* @description Fires when this node exits a Drop Target. (Fired from dd-drop)
115
* @event drag:drophit
116
* @description Fires when this node is dropped on a valid Drop Target. (Fired from dd-ddm-drop)
121
* @event drag:dropmiss
122
* @description Fires when this node is dropped on an invalid Drop Target. (Fired from dd-ddm-drop)
127
var Drag = function() {
128
Drag.superclass.constructor.apply(this, arguments);
137
* @description Y.Node instanace to use as the element to initiate a drag operation
141
set: function(node) {
142
var n = Y.Node.get(node);
144
Y.fail('DD.Drag: Invalid Node Given: ' + node);
150
* @attribute dragNode
151
* @description Y.Node instanace to use as the draggable element, defaults to node
155
set: function(node) {
156
var n = Y.Node.get(node);
158
Y.fail('DD.Drag: Invalid dragNode Given: ' + node);
164
* @attribute offsetNode
165
* @description Offset the drag element by the difference in cursor position: default true
172
* @attribute clickPixelThresh
173
* @description The number of pixels to move to start a drag operation, default is 3.
177
value: DDM.get('clickPixelThresh')
180
* @attribute clickTimeThresh
181
* @description The number of milliseconds a mousedown has to pass to start a drag operation, default is 1000.
185
value: DDM.get('clickTimeThresh')
189
* @description Set to lock this drag element so that it can't be dragged: default false.
194
set: function(lock) {
196
this.get(NODE).addClass(DDM.CSS_PREFIX + '-locked');
198
this.get(NODE).removeClass(DDM.CSS_PREFIX + '-locked');
204
* @description A payload holder to store arbitrary data about this drag object, can be used to store any value.
212
* @description If this is false, the drag element will not move with the cursor: default true. Can be used to "resize" the element.
220
* @description Use the protective shim on all drag operations: default true. Only works with dd-ddm, not dd-ddm-base.
227
* @attribute activeHandle
228
* @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.
235
* @attribute primaryButtonOnly
236
* @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.
243
* @attribute dragging
244
* @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.
252
* @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.
257
set: function(config) {
258
this._handleTarget(config);
262
* @attribute dragMode
263
* @description This attribute only works if the dd-drop module is active. It will set the dragMode (point, intersect, strict) of this Drag instance.
268
set: function(mode) {
269
return DDM._setDragMode(mode);
274
* @description Array of groups to add this drag into.
284
Y.each(this._groups, function(v, k) {
291
Y.each(g, function(v, k) {
292
this._groups[v] = true;
298
* @description Array of valid handles to add. Adding something here will set all handles, even if previously added with addHandle
306
Y.each(g, function(v, k) {
307
this._handles[v] = true;
310
this._handles = null;
317
Y.extend(Drag, Y.Base, {
320
* @description Add this Drag instance to a group, this should be used for on-the-fly group additions.
321
* @param {String} g The group to add this Drag Instance to.
325
addToGroup: function(g) {
326
this._groups[g] = true;
327
DDM._activateTargets();
331
* @method removeFromGroup
332
* @description Remove this Drag instance from a group, this should be used for on-the-fly group removals.
333
* @param {String} g The group to remove this Drag Instance from.
337
removeFromGroup: function(g) {
338
delete this._groups[g];
339
DDM._activateTargets();
344
* @description This will be a reference to the Drop instance associated with this drag if the target: true config attribute is set..
350
* @method _handleTarget
351
* @description Attribute handler for the target config attribute.
352
* @param {Boolean/Object}
353
* @return {Boolean/Object}
355
_handleTarget: function(config) {
357
if (config === false) {
359
DDM._unregTarget(this.target);
364
if (!Y.Lang.isObject(config)) {
367
config.node = this.get(NODE);
368
this.target = new Y.DD.Drop(config);
377
* @description Storage Array for the groups this drag belongs to.
383
* @method _createEvents
384
* @description This method creates all the events for this Event Target and publishes them so we get Event Bubbling.
386
_createEvents: function() {
388
this.publish(EV_MOUSE_DOWN, {
389
defaultFn: this._handleMouseDown,
411
Y.each(ev, function(v, k) {
427
* @description A private reference to the mousedown DOM event
433
* @property _startTime
434
* @description The getTime of the mousedown event. Not used, just here in case someone wants/needs to use it.
441
* @description The getTime of the mouseup event. Not used, just here in case someone wants/needs to use it.
448
* @description A private hash of the valid drag handles
454
* @property _invalids
455
* @description A private hash of the invalid selector strings
461
* @property _invalidsDefault
462
* @description A private hash of the default invalid selector strings: {'textarea': true, 'input': true, 'a': true, 'button': true}
465
_invalidsDefault: {'textarea': true, 'input': true, 'a': true, 'button': true},
468
* @property _dragThreshMet
469
* @description Private flag to see if the drag threshhold was met
472
_dragThreshMet: null,
475
* @property _fromTimeout
476
* @description Flag to determine if the drag operation came from a timeout
482
* @property _clickTimeout
483
* @description Holder for the setTimeout call
489
* @description The offset of the mouse position to the element's position
495
* @description The initial mouse position
501
* @description The initial element position
507
* @description The position of the element as it's moving (for offset calculations)
513
* @description The XY coords of the mousemove
519
* @description A region object associated with this drag, used for checking regions while dragging.
525
* @method _handleMouseUp
526
* @description Handler for the mouseup DOM event
529
_handleMouseUp: function(ev) {
530
this._fixIEMouseUp();
531
if (DDM.activeDrag) {
537
* @method _ieSelectFix
538
* @description The function we use as the onselectstart handler when we start a drag in Internet Explorer
540
_ieSelectFix: function() {
545
* @property _ieSelectBack
546
* @description We will hold a copy of the current "onselectstart" method on this property, and reset it after we are done using it.
551
* @method _fixIEMouseDown
552
* @description This method copies the onselectstart listner on the document to the _ieSelectFix property
554
_fixIEMouseDown: function() {
556
this._ieSelectBack = Y.config.doc.body.onselectstart;
557
Y.config.doc.body.onselectstart = this._ieSelectFix;
562
* @method _fixIEMouseUp
563
* @description This method copies the _ieSelectFix property back to the onselectstart listner on the document.
565
_fixIEMouseUp: function() {
567
Y.config.doc.body.onselectstart = this._ieSelectBack;
572
* @method _handleMouseDownEvent
573
* @description Handler for the mousedown DOM event
576
_handleMouseDownEvent: function(ev) {
577
this.fire(EV_MOUSE_DOWN, { ev: ev });
581
* @method _handleMouseDown
582
* @description Handler for the mousedown DOM event
585
_handleMouseDown: function(e) {
587
this._dragThreshMet = false;
590
if (this.get('primaryButtonOnly') && ev.button > 1) {
593
if (this.validClick(ev)) {
594
this._fixIEMouseDown();
596
this._setStartPosition([ev.pageX, ev.pageY]);
598
DDM.activeDrag = this;
601
this._clickTimeout = setTimeout(function() {
602
self._timeoutCheck.call(self);
603
}, this.get('clickTimeThresh'));
605
this.fire(EV_AFTER_MOUSE_DOWN, { ev: ev });
609
* @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.
613
validClick: function(ev) {
618
Y.each(this._handles, function(i, n) {
619
if (Y.Lang.isString(n)) {
620
//Am I this or am I inside this
621
if (tar.test(n + ', ' + n + ' *')) {
628
if (this.get(NODE).contains(tar) || this.get(NODE).compareTo(tar)) {
633
if (this._invalids) {
634
Y.each(this._invalids, function(i, n) {
635
if (Y.Lang.isString(n)) {
636
//Am I this or am I inside this
637
if (tar.test(n + ', ' + n + ' *')) {
646
var els = ev.currentTarget.queryAll(hTest);
647
els.each(function(n, i) {
648
if (n.contains(tar) || n.compareTo(tar)) {
649
this.set('activeHandle', els.item(i));
653
this.set('activeHandle', this.get(NODE));
660
* @method _setStartPosition
661
* @description Sets the current position of the Element and calculates the offset
662
* @param {Array} xy The XY coords to set the position to.
664
_setStartPosition: function(xy) {
667
this.nodeXY = this.get(NODE).getXY();
668
this.lastXY = this.nodeXY;
670
if (this.get('offsetNode')) {
671
this.deltaXY = [(this.startXY[0] - this.nodeXY[0]), (this.startXY[1] - this.nodeXY[1])];
673
this.deltaXY = [0, 0];
678
* @method _timeoutCheck
679
* @description The method passed to setTimeout to determine if the clickTimeThreshold was met.
681
_timeoutCheck: function() {
682
if (!this.get('lock')) {
683
this._fromTimeout = true;
684
this._dragThreshMet = true;
686
this._moveNode([this._ev_md.pageX, this._ev_md.pageY], true);
690
* @method removeHandle
691
* @description Remove a Selector added by addHandle
692
* @param {String} str The selector for the handle to be removed.
696
removeHandle: function(str) {
697
if (this._handles[str]) {
698
delete this._handles[str];
699
this.fire(EV_REMOVE_HANDLE, { handle: str });
705
* @description Add a handle to a drag element. Drag only initiates when a mousedown happens on this element.
706
* @param {String} str The selector to test for a valid handle. Must be a child of the element.
710
addHandle: function(str) {
711
if (!this._handles) {
714
if (Y.Lang.isString(str)) {
715
this._handles[str] = true;
716
this.fire(EV_ADD_HANDLE, { handle: str });
721
* @method removeInvalid
722
* @description Remove an invalid handle added by addInvalid
723
* @param {String} str The invalid handle to remove from the internal list.
727
removeInvalid: function(str) {
728
if (this._invalids[str]) {
729
delete this._handles[str];
730
this.fire(EV_REMOVE_INVALID, { handle: str });
736
* @description Add a selector string to test the handle against. If the test passes the drag operation will not continue.
737
* @param {String} str The selector to test against to determine if this is an invalid drag handle.
741
addInvalid: function(str) {
742
if (Y.Lang.isString(str)) {
743
this._invalids[str] = true;
744
this.fire(EV_ADD_INVALID, { handle: str });
751
* @method initializer
752
* @description Internal init handler
754
initializer: function() {
755
//TODO give the node instance a copy of this object
756
//Not supported in PR1 due to Y.Node.get calling a new under the hood.
757
//this.get(NODE).dd = this;
759
if (!this.get(NODE).get('id')) {
760
var id = Y.stamp(this.get(NODE));
761
this.get(NODE).set('id', id);
765
this._invalids = this._invalidsDefault;
767
this._createEvents();
769
if (!this.get(DRAG_NODE)) {
770
this.set(DRAG_NODE, this.get(NODE));
773
this._dragThreshMet = false;
778
* @description Attach event listners and add classname
781
var node = this.get(NODE);
782
node.addClass(DDM.CSS_PREFIX + '-draggable');
783
node.on(MOUSE_DOWN, this._handleMouseDownEvent, this, true);
784
node.on(MOUSE_UP, this._handleMouseUp, this, true);
789
* @description Detach event listners and remove classname
791
_unprep: function() {
792
var node = this.get(NODE);
793
node.removeClass(DDM.CSS_PREFIX + '-draggable');
794
node.detach(MOUSE_DOWN, this._handleMouseDownEvent, this, true);
795
node.detach(MOUSE_UP, this._handleMouseUp, this, true);
799
* @description Starts the drag operation
804
if (!this.get('lock') && !this.get('dragging')) {
805
this.set('dragging', true);
806
DDM._start(this.deltaXY, [this.get(NODE).get(OFFSET_HEIGHT), this.get(NODE).get(OFFSET_WIDTH)]);
807
this.get(NODE).addClass(DDM.CSS_PREFIX + '-dragging');
808
this.fire(EV_START, { pageX: this.nodeXY[0], pageY: this.nodeXY[1] });
809
this.get(DRAG_NODE).on(MOUSE_UP, this._handleMouseUp, this, true);
810
var xy = this.nodeXY;
812
this._startTime = (new Date()).getTime();
819
right: xy[0] + this.get(NODE).get(OFFSET_WIDTH),
820
bottom: xy[1] + this.get(NODE).get(OFFSET_HEIGHT),
829
* @description Ends the drag operation
834
this._endTime = (new Date()).getTime();
835
clearTimeout(this._clickTimeout);
836
this._dragThreshMet = false;
837
this._fromTimeout = false;
838
if (!this.get('lock') && this.get('dragging')) {
839
this.fire(EV_END, { pageX: this.lastXY[0], pageY: this.lastXY[1] });
841
this.get(NODE).removeClass(DDM.CSS_PREFIX + '-dragging');
842
this.set('dragging', false);
843
this.deltaXY = [0, 0];
844
this.get(DRAG_NODE).detach(MOUSE_UP, this._handleMouseUp, this, true);
851
* @description Calculates the offsets and set's the XY that the element will move to.
852
* @param {Array} xy The xy coords to align with.
856
_align: function(xy) {
857
return [xy[0] - this.deltaXY[0], xy[1] - this.deltaXY[1]];
862
* @description This method performs the actual element move.
863
* @param {Array} eXY The XY to move the element to, usually comes from the mousemove DOM event.
864
* @param {Boolean} noFire If true, the drag:drag event will not fire.
866
_moveNode: function(eXY, noFire) {
867
var xy = this._align(eXY), diffXY = [], diffXY2 = [];
869
diffXY[0] = (xy[0] - this.lastXY[0]);
870
diffXY[1] = (xy[1] - this.lastXY[1]);
872
diffXY2[0] = (xy[0] - this.nodeXY[0]);
873
diffXY2[1] = (xy[1] - this.nodeXY[1]);
875
if (this.get('move')) {
877
this.get(DRAG_NODE).setXY(xy);
879
DDM.setXY(this.get(DRAG_NODE), diffXY);
888
right: xy[0] + this.get(NODE).get(OFFSET_WIDTH),
889
bottom: xy[1] + this.get(NODE).get(OFFSET_HEIGHT),
893
var startXY = this.nodeXY;
912
* @description Fired from DragDropMgr (DDM) on mousemove.
913
* @param {Event} ev The mousemove DOM event
915
_move: function(ev) {
916
if (this.get('lock')) {
919
this.mouseXY = [ev.pageX, ev.pageY];
920
if (!this._dragThreshMet) {
921
var diffX = Math.abs(this.startXY[0] - ev.pageX);
922
var diffY = Math.abs(this.startXY[1] - ev.pageY);
923
if (diffX > this.get('clickPixelThresh') || diffY > this.get('clickPixelThresh')) {
924
this._dragThreshMet = true;
926
this._moveNode([ev.pageX, ev.pageY]);
930
clearTimeout(this._clickTimeout);
931
this._moveNode([ev.pageX, ev.pageY]);
937
* @description Method will forcefully stop a drag operation. For example calling this from inside an ESC keypress handler will stop this drag.
941
stopDrag: function() {
942
if (this.get('dragging')) {
950
* @description Lifecycle destructor, unreg the drag from the DDM and remove listeners
952
destructor: function() {
953
DDM._unregDrag(this);
957
this.target.destroy();
966
}, '3.0.0pr1' ,{requires:['dd-ddm-base'], skinnable:false});