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',
28
DRAG_START = 'dragstart',
30
* @event drag:mouseDown
31
* @description Handles the mousedown DOM event, checks to see if you have a valid handle then starts the drag timers.
32
* @preventable _handleMouseDown
33
* @param {Event} ev The mousedown event.
37
EV_MOUSE_DOWN = 'drag:mouseDown',
39
* @event drag:afterMouseDown
40
* @description Fires after the mousedown event has been cleared.
41
* @param {Event} ev The mousedown event.
45
EV_AFTER_MOUSE_DOWN = 'drag:afterMouseDown',
47
* @event drag:removeHandle
48
* @description Fires after a handle is removed.
52
EV_REMOVE_HANDLE = 'drag:removeHandle',
54
* @event drag:addHandle
55
* @description Fires after a handle is added.
59
EV_ADD_HANDLE = 'drag:addHandle',
61
* @event drag:removeInvalid
62
* @description Fires after an invalid selector is removed.
66
EV_REMOVE_INVALID = 'drag:removeInvalid',
68
* @event drag:addInvalid
69
* @description Fires after an invalid selector is added.
73
EV_ADD_INVALID = 'drag:addInvalid',
76
* @description Fires at the start of a drag operation.
80
EV_START = 'drag:start',
83
* @description Fires at the end of a drag operation.
90
* @description Fires every mousemove during a drag operation.
94
EV_DRAG = 'drag:drag';
99
* @description Fires when this node is over a Drop Target. (Fired from dd-drop)
105
* @description Fires when this node enters a Drop Target. (Fired from dd-drop)
111
* @description Fires when this node exits a Drop Target. (Fired from dd-drop)
116
* @event drag:drophit
117
* @description Fires when this node is dropped on a valid Drop Target. (Fired from dd-ddm-drop)
122
* @event drag:dropmiss
123
* @description Fires when this node is dropped on an invalid Drop Target. (Fired from dd-ddm-drop)
128
var Drag = function() {
129
Drag.superclass.constructor.apply(this, arguments);
139
* @description Y.Node instanace to use as the element to initiate a drag operation
143
set: function(node) {
146
Y.fail('DD.Drag: Invalid Node Given: ' + node);
154
* @attribute dragNode
155
* @description Y.Node instanace to use as the draggable element, defaults to node
159
set: function(node) {
160
var n = Y.Node.get(node);
162
Y.fail('DD.Drag: Invalid dragNode Given: ' + node);
168
* @attribute offsetNode
169
* @description Offset the drag element by the difference in cursor position: default true
176
* @attribute clickPixelThresh
177
* @description The number of pixels to move to start a drag operation, default is 3.
181
value: DDM.get('clickPixelThresh')
184
* @attribute clickTimeThresh
185
* @description The number of milliseconds a mousedown has to pass to start a drag operation, default is 1000.
189
value: DDM.get('clickTimeThresh')
193
* @description Set to lock this drag element so that it can't be dragged: default false.
198
set: function(lock) {
200
this.get(NODE).addClass(DDM.CSS_PREFIX + '-locked');
202
this.get(NODE).removeClass(DDM.CSS_PREFIX + '-locked');
208
* @description A payload holder to store arbitrary data about this drag object, can be used to store any value.
216
* @description If this is false, the drag element will not move with the cursor: default true. Can be used to "resize" the element.
224
* @description Use the protective shim on all drag operations: default true. Only works with dd-ddm, not dd-ddm-base.
231
* @attribute activeHandle
232
* @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.
239
* @attribute primaryButtonOnly
240
* @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.
247
* @attribute dragging
248
* @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.
259
* @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.
264
set: function(config) {
265
this._handleTarget(config);
269
* @attribute dragMode
270
* @description This attribute only works if the dd-drop module is active. It will set the dragMode (point, intersect, strict) of this Drag instance.
275
set: function(mode) {
276
return DDM._setDragMode(mode);
281
* @description Array of groups to add this drag into.
291
Y.each(this._groups, function(v, k) {
298
Y.each(g, function(v, k) {
299
this._groups[v] = true;
305
* @description Array of valid handles to add. Adding something here will set all handles, even if previously added with addHandle
313
Y.each(g, function(v, k) {
314
this._handles[v] = true;
317
this._handles = null;
324
* @description Controls the default bubble parent for this Drag instance. Default: Y.DD.DDM. Set to false to disable bubbling.
333
Y.extend(Drag, Y.Base, {
336
* @description Add this Drag instance to a group, this should be used for on-the-fly group additions.
337
* @param {String} g The group to add this Drag Instance to.
341
addToGroup: function(g) {
342
this._groups[g] = true;
343
DDM._activateTargets();
347
* @method removeFromGroup
348
* @description Remove this Drag instance from a group, this should be used for on-the-fly group removals.
349
* @param {String} g The group to remove this Drag Instance from.
353
removeFromGroup: function(g) {
354
delete this._groups[g];
355
DDM._activateTargets();
360
* @description This will be a reference to the Drop instance associated with this drag if the target: true config attribute is set..
366
* @method _handleTarget
367
* @description Attribute handler for the target config attribute.
368
* @param {Boolean/Object}
369
* @return {Boolean/Object}
371
_handleTarget: function(config) {
373
if (config === false) {
375
DDM._unregTarget(this.target);
380
if (!Y.Lang.isObject(config)) {
383
config.bubbles = this.get('bubbles');
384
config.node = this.get(NODE);
385
this.target = new Y.DD.Drop(config);
394
* @description Storage Array for the groups this drag belongs to.
400
* @method _createEvents
401
* @description This method creates all the events for this Event Target and publishes them so we get Event Bubbling.
403
_createEvents: function() {
405
this.publish(EV_MOUSE_DOWN, {
406
defaultFn: this._handleMouseDown,
428
Y.each(ev, function(v, k) {
438
if (this.get('bubbles')) {
439
this.addTarget(this.get('bubbles'));
447
* @description A private reference to the mousedown DOM event
453
* @property _startTime
454
* @description The getTime of the mousedown event. Not used, just here in case someone wants/needs to use it.
461
* @description The getTime of the mouseup event. Not used, just here in case someone wants/needs to use it.
468
* @description A private hash of the valid drag handles
474
* @property _invalids
475
* @description A private hash of the invalid selector strings
481
* @property _invalidsDefault
482
* @description A private hash of the default invalid selector strings: {'textarea': true, 'input': true, 'a': true, 'button': true}
485
_invalidsDefault: {'textarea': true, 'input': true, 'a': true, 'button': true},
488
* @property _dragThreshMet
489
* @description Private flag to see if the drag threshhold was met
492
_dragThreshMet: null,
495
* @property _fromTimeout
496
* @description Flag to determine if the drag operation came from a timeout
502
* @property _clickTimeout
503
* @description Holder for the setTimeout call
509
* @description The offset of the mouse position to the element's position
515
* @description The initial mouse position
521
* @description The initial element position
527
* @description The position of the element as it's moving (for offset calculations)
534
* @description The XY coords of the mousemove
540
* @description A region object associated with this drag, used for checking regions while dragging.
546
* @method _handleMouseUp
547
* @description Handler for the mouseup DOM event
550
_handleMouseUp: function(ev) {
551
this._fixIEMouseUp();
552
if (DDM.activeDrag) {
558
* @method _fixDragStart
559
* @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.
561
_fixDragStart: function(e) {
566
* @method _ieSelectFix
567
* @description The function we use as the onselectstart handler when we start a drag in Internet Explorer
569
_ieSelectFix: function() {
574
* @property _ieSelectBack
575
* @description We will hold a copy of the current "onselectstart" method on this property, and reset it after we are done using it.
580
* @method _fixIEMouseDown
581
* @description This method copies the onselectstart listner on the document to the _ieSelectFix property
583
_fixIEMouseDown: function() {
585
this._ieSelectBack = Y.config.doc.body.onselectstart;
586
Y.config.doc.body.onselectstart = this._ieSelectFix;
591
* @method _fixIEMouseUp
592
* @description This method copies the _ieSelectFix property back to the onselectstart listner on the document.
594
_fixIEMouseUp: function() {
596
Y.config.doc.body.onselectstart = this._ieSelectBack;
601
* @method _handleMouseDownEvent
602
* @description Handler for the mousedown DOM event
605
_handleMouseDownEvent: function(ev) {
606
this.fire(EV_MOUSE_DOWN, { ev: ev });
610
* @method _handleMouseDown
611
* @description Handler for the mousedown DOM event
614
_handleMouseDown: function(e) {
616
this._dragThreshMet = false;
619
if (this.get('primaryButtonOnly') && ev.button > 1) {
622
if (this.validClick(ev)) {
623
this._fixIEMouseDown();
625
this._setStartPosition([ev.pageX, ev.pageY]);
627
DDM.activeDrag = this;
630
this._clickTimeout = setTimeout(function() {
631
self._timeoutCheck.call(self);
632
}, this.get('clickTimeThresh'));
634
this.fire(EV_AFTER_MOUSE_DOWN, { ev: ev });
638
* @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.
642
validClick: function(ev) {
647
Y.each(this._handles, function(i, n) {
648
if (Y.Lang.isString(n)) {
649
//Am I this or am I inside this
650
if (tar.test(n + ', ' + n + ' *') && !hTest) {
657
if (this.get(NODE).contains(tar) || this.get(NODE).compareTo(tar)) {
662
if (this._invalids) {
663
Y.each(this._invalids, function(i, n) {
664
if (Y.Lang.isString(n)) {
665
//Am I this or am I inside this
666
if (tar.test(n + ', ' + n + ' *')) {
675
var els = ev.currentTarget.queryAll(hTest),
677
els.each(function(n, i) {
678
if ((n.contains(tar) || n.compareTo(tar)) && !set) {
680
this.set('activeHandle', els.item(i));
684
this.set('activeHandle', this.get(NODE));
691
* @method _setStartPosition
692
* @description Sets the current position of the Element and calculates the offset
693
* @param {Array} xy The XY coords to set the position to.
695
_setStartPosition: function(xy) {
698
this.nodeXY = this.get(NODE).getXY();
699
this.lastXY = this.nodeXY;
700
this.realXY = this.nodeXY;
702
if (this.get('offsetNode')) {
703
this.deltaXY = [(this.startXY[0] - this.nodeXY[0]), (this.startXY[1] - this.nodeXY[1])];
705
this.deltaXY = [0, 0];
710
* @method _timeoutCheck
711
* @description The method passed to setTimeout to determine if the clickTimeThreshold was met.
713
_timeoutCheck: function() {
714
if (!this.get('lock')) {
715
this._fromTimeout = true;
716
this._dragThreshMet = true;
718
this._moveNode([this._ev_md.pageX, this._ev_md.pageY], true);
722
* @method removeHandle
723
* @description Remove a Selector added by addHandle
724
* @param {String} str The selector for the handle to be removed.
728
removeHandle: function(str) {
729
if (this._handles[str]) {
730
delete this._handles[str];
731
this.fire(EV_REMOVE_HANDLE, { handle: str });
737
* @description Add a handle to a drag element. Drag only initiates when a mousedown happens on this element.
738
* @param {String} str The selector to test for a valid handle. Must be a child of the element.
742
addHandle: function(str) {
743
if (!this._handles) {
746
if (Y.Lang.isString(str)) {
747
this._handles[str] = true;
748
this.fire(EV_ADD_HANDLE, { handle: str });
753
* @method removeInvalid
754
* @description Remove an invalid handle added by addInvalid
755
* @param {String} str The invalid handle to remove from the internal list.
759
removeInvalid: function(str) {
760
if (this._invalids[str]) {
761
this._invalids[str] = null;
762
delete this._invalids[str];
763
this.fire(EV_REMOVE_INVALID, { handle: str });
769
* @description Add a selector string to test the handle against. If the test passes the drag operation will not continue.
770
* @param {String} str The selector to test against to determine if this is an invalid drag handle.
774
addInvalid: function(str) {
775
if (Y.Lang.isString(str)) {
776
this._invalids[str] = true;
777
this.fire(EV_ADD_INVALID, { handle: str });
784
* @method initializer
785
* @description Internal init handler
787
initializer: function() {
788
//TODO give the node instance a copy of this object
789
//Not supported in PR1 due to Y.Node.get calling a new under the hood.
790
//this.get(NODE).dd = this;
792
if (!this.get(NODE).get('id')) {
793
var id = Y.stamp(this.get(NODE));
794
this.get(NODE).set('id', id);
798
this._invalids = Y.clone(this._invalidsDefault, true);
800
this._createEvents();
802
if (!this.get(DRAG_NODE)) {
803
this.set(DRAG_NODE, this.get(NODE));
806
this._dragThreshMet = false;
811
* @description Attach event listners and add classname
814
var node = this.get(NODE);
815
node.addClass(DDM.CSS_PREFIX + '-draggable');
816
node.on(MOUSE_DOWN, this._handleMouseDownEvent, this, true);
817
node.on(MOUSE_UP, this._handleMouseUp, this, true);
818
node.on(DRAG_START, this._fixDragStart, this, true);
823
* @description Detach event listners and remove classname
825
_unprep: function() {
826
var node = this.get(NODE);
827
node.removeClass(DDM.CSS_PREFIX + '-draggable');
828
node.detach(MOUSE_DOWN, this._handleMouseDownEvent, this, true);
829
node.detach(MOUSE_UP, this._handleMouseUp, this, true);
830
node.detach(DRAG_START, this._fixDragStart, this, true);
834
* @description Starts the drag operation
839
if (!this.get('lock') && !this.get('dragging')) {
840
this.set('dragging', true);
841
DDM._start(this.deltaXY, [this.get(NODE).get(OFFSET_HEIGHT), this.get(NODE).get(OFFSET_WIDTH)]);
842
this.get(NODE).addClass(DDM.CSS_PREFIX + '-dragging');
843
this.fire(EV_START, { pageX: this.nodeXY[0], pageY: this.nodeXY[1] });
844
this.get(DRAG_NODE).on(MOUSE_UP, this._handleMouseUp, this, true);
845
var xy = this.nodeXY;
847
this._startTime = (new Date()).getTime();
854
right: xy[0] + this.get(NODE).get(OFFSET_WIDTH),
855
bottom: xy[1] + this.get(NODE).get(OFFSET_HEIGHT),
864
* @description Ends the drag operation
869
this._endTime = (new Date()).getTime();
870
clearTimeout(this._clickTimeout);
871
this._dragThreshMet = false;
872
this._fromTimeout = false;
873
if (!this.get('lock') && this.get('dragging')) {
874
this.fire(EV_END, { pageX: this.lastXY[0], pageY: this.lastXY[1] });
876
this.get(NODE).removeClass(DDM.CSS_PREFIX + '-dragging');
877
this.set('dragging', false);
878
this.deltaXY = [0, 0];
879
this.get(DRAG_NODE).detach(MOUSE_UP, this._handleMouseUp, this, true);
886
* @description Calculates the offsets and set's the XY that the element will move to.
887
* @param {Array} xy The xy coords to align with.
891
_align: function(xy) {
892
return [xy[0] - this.deltaXY[0], xy[1] - this.deltaXY[1]];
897
* @description This method performs the actual element move.
898
* @param {Array} eXY The XY to move the element to, usually comes from the mousemove DOM event.
899
* @param {Boolean} noFire If true, the drag:drag event will not fire.
901
_moveNode: function(eXY, noFire) {
902
var xy = this._align(eXY), diffXY = [], diffXY2 = [];
904
//This will probably kill your machine ;)
905
diffXY[0] = (xy[0] - this.lastXY[0]);
906
diffXY[1] = (xy[1] - this.lastXY[1]);
908
diffXY2[0] = (xy[0] - this.nodeXY[0]);
909
diffXY2[1] = (xy[1] - this.nodeXY[1]);
912
if (this.get('move')) {
914
this.get(DRAG_NODE).setXY(xy);
916
DDM.setXY(this.get(DRAG_NODE), diffXY);
926
right: xy[0] + this.get(DRAG_NODE).get(OFFSET_WIDTH),
927
bottom: xy[1] + this.get(DRAG_NODE).get(OFFSET_HEIGHT),
931
var startXY = this.nodeXY;
950
* @description Fired from DragDropMgr (DDM) on mousemove.
951
* @param {Event} ev The mousemove DOM event
953
_move: function(ev) {
954
if (this.get('lock')) {
957
this.mouseXY = [ev.pageX, ev.pageY];
958
if (!this._dragThreshMet) {
959
var diffX = Math.abs(this.startXY[0] - ev.pageX);
960
var diffY = Math.abs(this.startXY[1] - ev.pageY);
961
if (diffX > this.get('clickPixelThresh') || diffY > this.get('clickPixelThresh')) {
962
this._dragThreshMet = true;
964
this._moveNode([ev.pageX, ev.pageY]);
968
clearTimeout(this._clickTimeout);
969
this._moveNode([ev.pageX, ev.pageY]);
975
* @description Method will forcefully stop a drag operation. For example calling this from inside an ESC keypress handler will stop this drag.
979
stopDrag: function() {
980
if (this.get('dragging')) {
988
* @description Lifecycle destructor, unreg the drag from the DDM and remove listeners
990
destructor: function() {
991
DDM._unregDrag(this);
995
this.target.destroy();
1004
}, '3.0.0pr2' ,{requires:['dd-ddm-base'], skinnable:false});