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
Y.log('_handleMouseDown', 'info', 'dd-drag');
588
this._dragThreshMet = false;
591
if (this.get('primaryButtonOnly') && ev.button > 1) {
592
Y.log('Mousedown was not produced by the primary button', 'warn', 'dd-drag');
595
if (this.validClick(ev)) {
596
this._fixIEMouseDown();
598
this._setStartPosition([ev.pageX, ev.pageY]);
600
DDM.activeDrag = this;
603
this._clickTimeout = setTimeout(function() {
604
self._timeoutCheck.call(self);
605
}, this.get('clickTimeThresh'));
607
this.fire(EV_AFTER_MOUSE_DOWN, { ev: ev });
611
* @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.
615
validClick: function(ev) {
620
Y.log('validClick: We have handles', 'info', 'dd-drag');
621
Y.each(this._handles, function(i, n) {
622
if (Y.Lang.isString(n)) {
623
//Am I this or am I inside this
624
if (tar.test(n + ', ' + n + ' *')) {
625
Y.log('Valid Selector found: ' + n, 'info', 'dd-drag');
632
if (this.get(NODE).contains(tar) || this.get(NODE).compareTo(tar)) {
633
Y.log('validClick: We have a valid click', 'info', 'dd-drag');
638
Y.log('validClick: Check invalid selectors', 'info', 'dd-drag');
639
if (this._invalids) {
640
Y.each(this._invalids, function(i, n) {
641
if (Y.Lang.isString(n)) {
642
//Am I this or am I inside this
643
if (tar.test(n + ', ' + n + ' *')) {
644
Y.log('Invalid Selector found: (' + (n + ', ' + n + ' *') + ')', 'warn', 'dd-drag');
653
var els = ev.currentTarget.queryAll(hTest);
654
els.each(function(n, i) {
655
if (n.contains(tar) || n.compareTo(tar)) {
656
this.set('activeHandle', els.item(i));
660
this.set('activeHandle', this.get(NODE));
667
* @method _setStartPosition
668
* @description Sets the current position of the Element and calculates the offset
669
* @param {Array} xy The XY coords to set the position to.
671
_setStartPosition: function(xy) {
674
this.nodeXY = this.get(NODE).getXY();
675
this.lastXY = this.nodeXY;
677
if (this.get('offsetNode')) {
678
this.deltaXY = [(this.startXY[0] - this.nodeXY[0]), (this.startXY[1] - this.nodeXY[1])];
680
this.deltaXY = [0, 0];
685
* @method _timeoutCheck
686
* @description The method passed to setTimeout to determine if the clickTimeThreshold was met.
688
_timeoutCheck: function() {
689
if (!this.get('lock')) {
690
Y.log("timeout threshold met", "info", "dd-drag");
691
this._fromTimeout = true;
692
this._dragThreshMet = true;
694
this._moveNode([this._ev_md.pageX, this._ev_md.pageY], true);
698
* @method removeHandle
699
* @description Remove a Selector added by addHandle
700
* @param {String} str The selector for the handle to be removed.
704
removeHandle: function(str) {
705
if (this._handles[str]) {
706
delete this._handles[str];
707
this.fire(EV_REMOVE_HANDLE, { handle: str });
713
* @description Add a handle to a drag element. Drag only initiates when a mousedown happens on this element.
714
* @param {String} str The selector to test for a valid handle. Must be a child of the element.
718
addHandle: function(str) {
719
if (!this._handles) {
722
if (Y.Lang.isString(str)) {
723
this._handles[str] = true;
724
this.fire(EV_ADD_HANDLE, { handle: str });
729
* @method removeInvalid
730
* @description Remove an invalid handle added by addInvalid
731
* @param {String} str The invalid handle to remove from the internal list.
735
removeInvalid: function(str) {
736
if (this._invalids[str]) {
737
delete this._handles[str];
738
this.fire(EV_REMOVE_INVALID, { handle: str });
744
* @description Add a selector string to test the handle against. If the test passes the drag operation will not continue.
745
* @param {String} str The selector to test against to determine if this is an invalid drag handle.
749
addInvalid: function(str) {
750
if (Y.Lang.isString(str)) {
751
this._invalids[str] = true;
752
this.fire(EV_ADD_INVALID, { handle: str });
754
Y.log('Selector needs to be a string..', 'warn', 'dd-drag');
760
* @method initializer
761
* @description Internal init handler
763
initializer: function() {
764
//TODO give the node instance a copy of this object
765
//Not supported in PR1 due to Y.Node.get calling a new under the hood.
766
//this.get(NODE).dd = this;
768
if (!this.get(NODE).get('id')) {
769
var id = Y.stamp(this.get(NODE));
770
this.get(NODE).set('id', id);
774
this._invalids = this._invalidsDefault;
776
this._createEvents();
778
if (!this.get(DRAG_NODE)) {
779
this.set(DRAG_NODE, this.get(NODE));
782
this._dragThreshMet = false;
787
* @description Attach event listners and add classname
790
var node = this.get(NODE);
791
node.addClass(DDM.CSS_PREFIX + '-draggable');
792
node.on(MOUSE_DOWN, this._handleMouseDownEvent, this, true);
793
node.on(MOUSE_UP, this._handleMouseUp, this, true);
798
* @description Detach event listners and remove classname
800
_unprep: function() {
801
var node = this.get(NODE);
802
node.removeClass(DDM.CSS_PREFIX + '-draggable');
803
node.detach(MOUSE_DOWN, this._handleMouseDownEvent, this, true);
804
node.detach(MOUSE_UP, this._handleMouseUp, this, true);
808
* @description Starts the drag operation
813
if (!this.get('lock') && !this.get('dragging')) {
814
this.set('dragging', true);
815
DDM._start(this.deltaXY, [this.get(NODE).get(OFFSET_HEIGHT), this.get(NODE).get(OFFSET_WIDTH)]);
816
Y.log('startDrag', 'info', 'dd-drag');
817
this.get(NODE).addClass(DDM.CSS_PREFIX + '-dragging');
818
this.fire(EV_START, { pageX: this.nodeXY[0], pageY: this.nodeXY[1] });
819
this.get(DRAG_NODE).on(MOUSE_UP, this._handleMouseUp, this, true);
820
var xy = this.nodeXY;
822
this._startTime = (new Date()).getTime();
829
right: xy[0] + this.get(NODE).get(OFFSET_WIDTH),
830
bottom: xy[1] + this.get(NODE).get(OFFSET_HEIGHT),
839
* @description Ends the drag operation
844
this._endTime = (new Date()).getTime();
845
clearTimeout(this._clickTimeout);
846
this._dragThreshMet = false;
847
this._fromTimeout = false;
848
if (!this.get('lock') && this.get('dragging')) {
849
Y.log('endDrag', 'info', 'dd-drag');
850
this.fire(EV_END, { pageX: this.lastXY[0], pageY: this.lastXY[1] });
852
this.get(NODE).removeClass(DDM.CSS_PREFIX + '-dragging');
853
this.set('dragging', false);
854
this.deltaXY = [0, 0];
855
this.get(DRAG_NODE).detach(MOUSE_UP, this._handleMouseUp, this, true);
862
* @description Calculates the offsets and set's the XY that the element will move to.
863
* @param {Array} xy The xy coords to align with.
867
_align: function(xy) {
868
return [xy[0] - this.deltaXY[0], xy[1] - this.deltaXY[1]];
873
* @description This method performs the actual element move.
874
* @param {Array} eXY The XY to move the element to, usually comes from the mousemove DOM event.
875
* @param {Boolean} noFire If true, the drag:drag event will not fire.
877
_moveNode: function(eXY, noFire) {
878
var xy = this._align(eXY), diffXY = [], diffXY2 = [];
880
diffXY[0] = (xy[0] - this.lastXY[0]);
881
diffXY[1] = (xy[1] - this.lastXY[1]);
883
diffXY2[0] = (xy[0] - this.nodeXY[0]);
884
diffXY2[1] = (xy[1] - this.nodeXY[1]);
886
if (this.get('move')) {
888
this.get(DRAG_NODE).setXY(xy);
890
DDM.setXY(this.get(DRAG_NODE), diffXY);
899
right: xy[0] + this.get(NODE).get(OFFSET_WIDTH),
900
bottom: xy[1] + this.get(NODE).get(OFFSET_HEIGHT),
904
var startXY = this.nodeXY;
923
* @description Fired from DragDropMgr (DDM) on mousemove.
924
* @param {Event} ev The mousemove DOM event
926
_move: function(ev) {
927
if (this.get('lock')) {
928
Y.log('Drag Locked', 'warn', 'dd-drag');
931
this.mouseXY = [ev.pageX, ev.pageY];
932
if (!this._dragThreshMet) {
933
var diffX = Math.abs(this.startXY[0] - ev.pageX);
934
var diffY = Math.abs(this.startXY[1] - ev.pageY);
935
Y.log("diffX: " + diffX + ", diffY: " + diffY, 'info', 'dd-drag');
936
if (diffX > this.get('clickPixelThresh') || diffY > this.get('clickPixelThresh')) {
937
Y.log("pixel threshold met", "info", "dd-drag");
938
this._dragThreshMet = true;
940
this._moveNode([ev.pageX, ev.pageY]);
944
clearTimeout(this._clickTimeout);
945
this._moveNode([ev.pageX, ev.pageY]);
951
* @description Method will forcefully stop a drag operation. For example calling this from inside an ESC keypress handler will stop this drag.
955
stopDrag: function() {
956
if (this.get('dragging')) {
957
Y.log('stopDrag called', 'warn', 'dd-drag');
965
* @description Lifecycle destructor, unreg the drag from the DDM and remove listeners
967
destructor: function() {
968
DDM._unregDrag(this);
972
this.target.destroy();
981
}, '3.0.0pr1' ,{requires:['dd-ddm-base'], skinnable:false});