1 /**
  2  * @fileOverview dd support for kissy, drag for dd
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add('dd/draggable', function (S, UA, Node, Base, DDM) {
  6 
  7     var each = S.each,
  8         ie = UA['ie'],
  9         NULL = null,
 10         PREFIX_CLS = DDM.PREFIX_CLS,
 11         doc = S.Env.host.document;
 12 
 13     /**
 14      * @extends Base
 15      * @class Provide abilities to make specified node draggable
 16      * @memberOf DD
 17      */
 18     function Draggable() {
 19         var self = this;
 20         Draggable.superclass.constructor.apply(self, arguments);
 21         self.addTarget(DDM);
 22         S.each([
 23 
 24         /**
 25          * @name DD.DDM#dragalign
 26          * @description fired when need to compute draggable's posititon during dragging
 27          * @event
 28          * @param e
 29          * @param e.drag current draggable object
 30          */
 31 
 32         /**
 33          * @name DD.Draggable#dragalign
 34          * @description fired when need to get draggable 's position during dragging
 35          * @event
 36          * @param e
 37          * @param e.drag current draggable object
 38          */
 39             "dragalign",
 40 
 41         /**
 42          * @name DD.DDM#drag
 43          * @description fired during dragging
 44          * @event
 45          * @param e
 46          * @param e.drag current draggable object
 47          */
 48 
 49         /**
 50          * @name DD.Draggable#drag
 51          * @description fired during dragging
 52          * @event
 53          * @param e
 54          * @param e.drag current draggable object
 55          */
 56             "drag",
 57 
 58         /**
 59          * @name DD.DDM#dragdrophit
 60          * @description fired after drop a draggable onto a droppable object
 61          * @event
 62          * @param e
 63          * @param e.drag current draggable object
 64          * @param e.drop current droppable object
 65          */
 66 
 67         /**
 68          * @name DD.Draggable#dragdrophit
 69          * @description fired after drop a draggable onto a droppable object
 70          * @event
 71          * @param e
 72          * @param e.drag current draggable object
 73          * @param e.drop current droppable object
 74          */
 75             "dragdrophit",
 76 
 77 
 78         /**
 79          * @name DD.DDM#dragend
 80          * @description fired after drag
 81          * @event
 82          * @param e
 83          * @param e.drag current draggable object
 84          */
 85 
 86         /**
 87          * @name DD.Draggable#dragend
 88          * @description fired after drag
 89          * @event
 90          * @param e
 91          * @param e.drag current draggable object
 92          */
 93             "dragend",
 94 
 95 
 96         /**
 97          * @name DD.DDM#dragdropmiss
 98          * @description fired after drop a draggable onto nothing
 99          * @event
100          * @param e
101          * @param e.drag current draggable object
102          */
103 
104         /**
105          * @name DD.Draggable#dragdropmiss
106          * @description fired after drop a draggable onto nothing
107          * @event
108          * @param e
109          * @param e.drag current draggable object
110          */
111             "dragdropmiss",
112 
113 
114         /**
115          * @name DD.DDM#dragexit
116          * @description fired after a draggable leaves a droppable
117          * @event
118          * @param e
119          * @param e.drag current draggable object
120          * @param e.drop current droppable object
121          */
122 
123         /**
124          * @name DD.Draggable#dragexit
125          * @description fired after a draggable leaves a droppable
126          * @event
127          * @param e
128          * @param e.drag current draggable object
129          * @param e.drop current droppable object
130          */
131             "dragexit",
132 
133 
134         /**
135          * @name DD.DDM#dragenter
136          * @description fired after a draggable object mouseenter a droppable object
137          * @event
138          * @param e
139          * @param e.drag current draggable object
140          * @param e.drop current droppable object
141          */
142 
143         /**
144          * @name DD.Draggable#dragenter
145          * @description fired after a draggable object mouseenter a droppable object
146          * @event
147          * @param e
148          * @param e.drag current draggable object
149          * @param e.drop current droppable object
150          */
151             "dragenter",
152 
153 
154         /**
155          * @name DD.DDM#dragover
156          * @description fired after a draggable object mouseover a droppable object
157          * @event
158          * @param e
159          * @param e.drag current draggable object
160          * @param e.drop current droppable object
161          */
162 
163         /**
164          * @name DD.Draggable#dragover
165          * @description fired after a draggable object mouseover a droppable object
166          * @event
167          * @param e
168          * @param e.drag current draggable object
169          * @param e.drop current droppable object
170          */
171             "dragover",
172 
173 
174         /**
175          * @name DD.DDM#dragstart
176          * @description fired after a draggable object start to drag
177          * @event
178          * @param e
179          * @param e.drag current draggable object
180          */
181 
182         /**
183          * @name DD.Draggable#dragstart
184          * @description fired after a draggable object start to drag
185          * @event
186          * @param e
187          * @param e.drag current draggable object
188          */
189             "dragstart"
190         ], function (e) {
191             self.publish(e, {
192                 bubbles:1
193             });
194         });
195         // dragNode is equal to node in single mode
196         self.__set("dragNode", self.get("node"));
197         self.on("afterDisabledChange", self._uiSetDisabledChange, self);
198         var disabled;
199         if (disabled = self.get("disabled")) {
200             self._uiSetDisabledChange(disabled);
201         }
202         self._init();
203     }
204 
205     Draggable['POINT'] = "point";
206     Draggable.INTERSECT = "intersect";
207     Draggable.STRICT = "strict";
208     /**
209      * @ignore
210      */
211     Draggable.ATTRS =
212     /**
213      * @lends DD.Draggable#
214      */
215     {
216 
217         /**
218          * the dragged node. maybe a proxy node.
219          * @type HTMLElement
220          */
221         node:{
222             setter:function (v) {
223                 if (!(v instanceof Node)) {
224                     return Node.one(v);
225                 }
226             }
227         },
228 
229         /**
230          * the number of pixels to move to start a drag operation,default is 3.
231          * @type Number
232          */
233         clickPixelThresh:{
234             valueFn:function () {
235                 return DDM.get("clickPixelThresh");
236             }
237         },
238 
239         /**
240          * the number of milliseconds to start a drag operation after mousedown,default is 1000
241          * @type Number
242          */
243         bufferTime:{
244             valueFn:function () {
245                 return DDM.get("bufferTime");
246             }
247         },
248 
249         /**
250          * the draggable element
251          * @type HTMLElement
252          */
253         dragNode:{},
254 
255         /**
256          * use protective shim to cross iframe.default:true
257          * @type Boolean
258          */
259         shim:{
260             value:true
261         },
262 
263         /**
264          * valid handlers to initiate a drag operation
265          * @type HTMLElement[]|Function[]|String[]
266          */
267         handlers:{
268             value:[],
269             getter:function (vs) {
270                 var self = this;
271                 if (!vs.length) {
272                     vs[0] = self.get("node");
273                 }
274                 each(vs, function (v, i) {
275                     if (S.isFunction(v)) {
276                         v = v.call(self);
277                     }
278                     if (S.isString(v) || v.nodeType) {
279                         v = Node.one(v);
280                     }
281                     vs[i] = v;
282                 });
283                 self.__set("handlers", vs);
284                 return vs;
285             }
286         },
287 
288         /**
289          * the handler which fired the drag event.
290          * @type NodeList
291          */
292         activeHandler:{},
293 
294         /**
295          * indicate whether this draggable object is being dragged
296          * @type Boolean
297          */
298         dragging:{
299             value:false,
300             setter:function (d) {
301                 var self = this;
302                 self.get("dragNode")[d ? 'addClass' : 'removeClass']
303                     (PREFIX_CLS + "dragging");
304             }
305         },
306 
307         /**
308          * <pre>
309          * can be set 'point' or 'intersect' or 'strict'
310          * In point mode, a Drop is targeted by the cursor being over the Target
311          * In intersect mode, a Drop is targeted by "part" of the drag node being over the Target
312          * In strict mode, a Drop is targeted by the "entire" drag node being over the Target
313          * </pre>
314          * @type String
315          */
316         mode:{
317             value:'point'
318         },
319 
320         /**
321          * set to disable this draggable so that it can not be dragged. default:false
322          * @type Boolean
323          */
324         disabled:{
325             value:false
326         },
327 
328         /**
329          * whether the drag node moves with cursor.default:false,can be used to resize element.
330          * @type Boolean
331          */
332         move:{
333             value:false
334         },
335 
336         /**
337          * whether a drag operation can only be trigged by primary(left) mouse button.
338          * Setting false will allow for all mousedown events to trigger drag.
339          * @type Boolean
340          */
341         primaryButtonOnly:{
342             value:true
343         },
344 
345         /**
346          * whether halt mousedown event. default:true
347          * @type Boolean
348          */
349         halt:{
350             value:true
351         },
352 
353         /**
354          * groups this draggable object belongs to
355          * @type Object
356          * @example
357          * <code>
358          * {
359          *     "group1":1,
360          *     "group2":1
361          * }
362          * </code>
363          */
364         groups:{
365             value:{}
366         }
367 
368     };
369 
370 
371     var _ieSelectBack;
372 
373     function fixIEMouseUp() {
374         doc.body.onselectstart = _ieSelectBack;
375     }
376 
377     /**
378      * prevent select text in ie
379      */
380     function fixIEMouseDown() {
381         _ieSelectBack = doc.body.onselectstart;
382         doc.body.onselectstart = fixIESelect;
383     }
384 
385     /**
386      * keeps IE from blowing up on images as drag handlers.
387      * 防止 html5 draggable 元素的拖放默认行为
388      * @param e
389      */
390     function fixDragStart(e) {
391         e.preventDefault();
392     }
393 
394     /**
395      * keeps IE from selecting text
396      */
397     function fixIESelect() {
398         return false;
399     }
400 
401 
402     /**
403      * 鼠标按下时,查看触发源是否是属于 handler 集合,
404      * 保存当前状态
405      * 通知全局管理器开始作用
406      * @param ev
407      */
408     function handleMouseDown(ev) {
409         var self = this,
410             t = ev.target;
411 
412         if (self._checkMouseDown(ev)) {
413 
414             if (!self._check(t)) {
415                 return;
416             }
417 
418             self._prepare(ev);
419         }
420     }
421 
422     S.extend(Draggable, Base,
423         /**
424          * @lends DD.Draggable#
425          */
426         {
427 
428             /**
429              * 开始拖时鼠标所在位置,例如
430              *  {x:100,y:200}
431              * @type Object
432              * @private
433              */
434             startMousePos:NULL,
435 
436             /**
437              * 开始拖时节点所在位置,例如
438              *  {x:100,y:200}
439              * @type Object
440              * @private
441              */
442             startNodePos:NULL,
443 
444             /**
445              * 开始拖时鼠标和节点所在位置的差值
446              */
447             _diff:NULL,
448 
449             /**
450              * mousedown 1秒后自动开始拖的定时器
451              */
452             _bufferTimer:NULL,
453 
454             _uiSetDisabledChange:function (d) {
455                 this.get("dragNode")[d ? 'addClass' :
456                     'removeClass'](PREFIX_CLS + '-disabled');
457             },
458 
459             _init:function () {
460                 var self = this,
461                     node = self.get('node');
462                 node.on('mousedown', handleMouseDown, self)
463                     .on('dragstart', self._fixDragStart);
464             },
465 
466             _fixDragStart:fixDragStart,
467 
468             /**
469              *
470              * @param {HTMLElement} t
471              */
472             _check:function (t) {
473                 var self = this,
474                     handlers = self.get('handlers'),
475                     ret = 0;
476                 each(handlers, function (handler) {
477                     //子区域内点击也可以启动
478                     if (handler.contains(t) ||
479                         handler[0] == t) {
480                         ret = 1;
481                         self.__set("activeHandler", handler);
482                         return false;
483                     }
484                 });
485                 return ret;
486             },
487 
488             _checkMouseDown:function (ev) {
489                 if (this.get('primaryButtonOnly') && ev.button > 1 ||
490                     this.get("disabled")) {
491                     return 0;
492                 }
493                 return 1;
494             },
495 
496             _prepare:function (ev) {
497 
498                 var self = this;
499 
500                 if (ie) {
501                     fixIEMouseDown();
502                 }
503 
504                 // 防止 firefox/chrome 选中 text
505                 if (self.get("halt")) {
506                     ev.halt();
507                 } else {
508                     ev.preventDefault();
509                 }
510 
511                 var node = self.get("node"),
512                     mx = ev.pageX,
513                     my = ev.pageY,
514                     nxy = node.offset();
515                 self.startMousePos = self.mousePos = {
516                     left:mx,
517                     top:my
518                 };
519                 self.startNodePos = nxy;
520                 self._diff = {
521                     left:mx - nxy.left,
522                     top:my - nxy.top
523                 };
524                 DDM._regToDrag(self);
525 
526                 var bufferTime = self.get("bufferTime");
527 
528                 // 是否中央管理,强制限制拖放延迟
529                 if (bufferTime) {
530                     self._bufferTimer = setTimeout(function () {
531                         // 事件到了,仍然是 mousedown 触发!
532                         //S.log("drag start by timeout");
533                         self._start();
534                     }, bufferTime);
535                 }
536 
537             },
538 
539             _clearBufferTimer:function () {
540                 var self = this;
541                 if (self._bufferTimer) {
542                     clearTimeout(self._bufferTimer);
543                     self._bufferTimer = 0;
544                 }
545             },
546 
547             _move:function (ev) {
548                 var self = this,
549                     ret,
550                     diff = self._diff,
551                     startMousePos = self.startMousePos,
552                     pageX = ev.pageX,
553                     pageY = ev.pageY,
554                     left = pageX - diff.left,
555                     top = pageY - diff.top;
556 
557 
558                 if (!self.get("dragging")) {
559                     var clickPixelThresh = self.get("clickPixelThresh");
560                     // 鼠标经过了一定距离,立即开始
561                     if (Math.abs(pageX - startMousePos.left) >= clickPixelThresh ||
562                         Math.abs(pageY - startMousePos.top) >= clickPixelThresh) {
563                         //S.log("start drag by pixel : " + l1 + " : " + l2);
564                         self._start();
565                     }
566                     // 开始后,下轮 move 开始触发 drag 事件
567                     return;
568                 }
569 
570                 self.mousePos = {
571                     left:pageX,
572                     top:pageY
573                 };
574 
575                 ret = {
576                     left:left,
577                     top:top,
578                     pageX:pageX,
579                     pageY:pageY,
580                     drag:self
581                 };
582 
583                 self.fire("dragalign", {
584                     info:ret
585                 });
586 
587                 var def = 1;
588 
589                 if (self.fire("drag", ret) === false) {
590                     def = 0;
591                 }
592 
593                 if (def && self.get("move")) {
594                     // 取 'node' , 改 node 可能是代理哦
595                     self.get('node').offset(ret);
596                 }
597             },
598 
599             /**
600              * force to stop this drag operation
601              */
602             stopDrag:function () {
603                 if (this.get("dragging")) {
604                     DDM._end();
605                 }
606             },
607 
608             _end:function () {
609                 var self = this,
610                     activeDrop;
611 
612                 // 否则清除定时器即可
613                 self._clearBufferTimer();
614                 if (ie) {
615                     fixIEMouseUp();
616                 }
617                 // 如果已经开始,收尾工作
618                 if (self.get("dragging")) {
619                     self.get("node")
620                         .removeClass(PREFIX_CLS + "drag-over");
621                     if (activeDrop = DDM.get("activeDrop")) {
622                         self.fire('dragdrophit', {
623                             drag:self,
624                             drop:activeDrop
625                         });
626                     } else {
627                         self.fire('dragdropmiss', {
628                             drag:self
629                         });
630                     }
631                     self.__set("dragging", 0);
632                     self.fire("dragend", {
633                         drag:self
634                     });
635                 }
636             },
637 
638             _handleOut:function () {
639                 var self = this;
640                 self.get("node").removeClass(PREFIX_CLS + "drag-over");
641                 /**
642                  *  html5 => dragleave
643                  */
644                 self.fire("dragexit", {
645                     drag:self,
646                     drop:DDM.get("activeDrop")
647                 });
648             },
649 
650             /**
651              *
652              * @param e
653              */
654             _handleEnter:function (e) {
655                 var self = this;
656                 self.get("node").addClass(PREFIX_CLS + "drag-over");
657                 //第一次先触发 dropenter, dragenter
658                 self.fire("dragenter", e);
659             },
660 
661 
662             _handleOver:function (e) {
663                 this.fire("dragover", e);
664             },
665 
666             _start:function () {
667                 var self = this;
668                 self._clearBufferTimer();
669                 self.__set("dragging", 1);
670                 DDM._start();
671                 self.fire("dragstart", {
672                     drag:self
673                 });
674             },
675 
676             /**
677              * make the drag node undraggable
678              */
679             destroy:function () {
680                 var self = this,
681                     node = self.get('dragNode');
682                 node.detach('mousedown', handleMouseDown, self)
683                     .detach('dragstart', self._fixDragStart);
684                 self.detach();
685             }
686         });
687 
688     return Draggable;
689 
690 }, {
691     requires:["ua", "node", "base", "./ddm"]
692 });
693