Special XML conversion code for Firefox >= 20.0
[infodrom/rico3] / minsrc / ricoDragDrop.js
1 /*
2  *  (c) 2005-2009 Richard Cowin (http://openrico.org)
3  *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
4  *
5  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
6  *  file except in compliance with the License. You may obtain a copy of the License at
7  *
8  *         http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software distributed under the
11  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12  *  either express or implied. See the License for the specific language governing permissions
13  *  and limitations under the License.
14  */
15
16 Rico.dndMgrList = [];
17
18 Rico.registerDraggable = function(aDraggable, mgrIdx) {
19   if (typeof mgrIdx != 'number') mgrIdx=0;
20   if (typeof Rico.dndMgrList[mgrIdx] != 'object')
21     Rico.dndMgrList[mgrIdx] = new Rico.dndMgr();
22   Rico.dndMgrList[mgrIdx].registerDraggable(aDraggable);
23 };
24
25 Rico.registerDropZone = function(aDropZone, mgrIdx) {
26   if (typeof mgrIdx != 'number') mgrIdx=0;
27   if (typeof Rico.dndMgrList[mgrIdx] != 'object')
28     Rico.dndMgrList[mgrIdx] = new Rico.dndMgr();
29   Rico.dndMgrList[mgrIdx].registerDropZone(aDropZone);
30 };
31
32 Rico.dndMgr = function() {
33   this.initialize();
34 };
35
36 Rico.dndMgr.prototype = {
37 /**
38  * @class Implements drag-n-drop manager -- a group of linked draggables and drop zones
39  * @constructs
40  */
41    initialize: function() {
42       this.dropZones                = [];
43       this.draggables               = [];
44       this.currentDragObjects       = [];
45       this.dragElement              = null;
46       this.lastSelectedDraggable    = null;
47       this.currentDragObjectVisible = false;
48       this.interestedInMotionEvents = false;
49       this._mouseDown = Rico.eventHandle(this,'_mouseDownHandler');
50       this._mouseMove = Rico.eventHandle(this,'_mouseMoveHandler');
51       this._mouseUp = Rico.eventHandle(this,'_mouseUpHandler');
52    },
53
54    registerDropZone: function(aDropZone) {
55       this.dropZones[ this.dropZones.length ] = aDropZone;
56    },
57
58    deregisterDropZone: function(aDropZone) {
59       var newDropZones = new Array();
60       var j = 0;
61       for ( var i = 0 ; i < this.dropZones.length ; i++ ) {
62          if ( this.dropZones[i] != aDropZone )
63             newDropZones[j++] = this.dropZones[i];
64       }
65
66       this.dropZones = newDropZones;
67    },
68
69    clearDropZones: function() {
70       this.dropZones = new Array();
71    },
72
73    registerDraggable: function( aDraggable ) {
74       this.draggables[ this.draggables.length ] = aDraggable;
75       var htmlElement = aDraggable.getMouseDownHTMLElement();
76       if ( htmlElement != null ) {
77          htmlElement.ricoDraggable = aDraggable;
78          Rico.eventBind(htmlElement, "mousedown", Rico.eventHandle(this,'_attachEvents'));
79          Rico.eventBind(htmlElement, "mousedown", this._mouseDown);
80       }
81    },
82
83    clearSelection: function() {
84       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
85          this.currentDragObjects[i].deselect();
86       this.currentDragObjects = new Array();
87       this.lastSelectedDraggable = null;
88    },
89
90    hasSelection: function() {
91       return this.currentDragObjects.length > 0;
92    },
93
94    setStartDragFromElement: function( e, mouseDownElement ) {
95       this.origPos = Rico.cumulativeOffset(mouseDownElement);
96       var coord=Rico.eventClient(e);
97       this.startx = coord.x - this.origPos.left;
98       this.starty = coord.y - this.origPos.top;
99
100       this.interestedInMotionEvents = this.hasSelection();
101       Rico.eventStop(e);
102    },
103
104    updateSelection: function( draggable, extendSelection ) {
105       if ( ! extendSelection )
106          this.clearSelection();
107
108       if ( draggable.isSelected() ) {
109          this.currentDragObjects=this.currentDragObjects.without(draggable);
110          draggable.deselect();
111          if ( draggable == this.lastSelectedDraggable )
112             this.lastSelectedDraggable = null;
113       }
114       else {
115          draggable.select();
116          if ( draggable.isSelected() ) {
117            this.currentDragObjects.push(draggable);
118            this.lastSelectedDraggable = draggable;
119          }
120       }
121    },
122
123    _mouseDownHandler: function(e) {
124       // if not button 1 ignore it...
125       if (!Rico.eventLeftClick(e)) return;
126
127       var eventTarget      = Rico.eventElement(e);
128       var draggableObject  = eventTarget.ricoDraggable;
129
130       var candidate = eventTarget;
131       while (draggableObject == null && candidate.parentNode) {
132          candidate = candidate.parentNode;
133          draggableObject = candidate.ricoDraggable;
134       }
135
136       if ( draggableObject == null ) return;
137
138       this.updateSelection( draggableObject, e.ctrlKey );
139
140       // clear the drop zones postion cache...
141       if ( this.hasSelection() ) {
142          for ( var i = 0 ; i < this.dropZones.length ; i++ )
143             this.dropZones[i].clearPositionCache();
144       }
145       this.setStartDragFromElement( e, draggableObject.getMouseDownHTMLElement() );
146    },
147
148
149    _mouseMoveHandler: function(e) {
150       if ( !this.interestedInMotionEvents ) {
151          return;
152       }
153
154       if ( ! this.hasSelection() )
155          return;
156
157       if ( ! this.currentDragObjectVisible )
158          this._startDrag(e);
159
160       if ( !this.activatedDropZones )
161          this._activateRegisteredDropZones();
162
163       this._updateDraggableLocation(e);
164       this._updateDropZonesHover(e);
165
166       Rico.eventStop(e);
167    },
168
169    _makeDraggableObjectVisible: function(e) {
170       if ( !this.hasSelection() )
171          return;
172
173       var dragElement;
174       if ( this.currentDragObjects.length > 1 )
175          dragElement = this.currentDragObjects[0].getMultiObjectDragGUI(this.currentDragObjects);
176       else
177          dragElement = this.currentDragObjects[0].getSingleObjectDragGUI();
178
179       // go ahead and absolute position it...
180       this.dragElemPosition=Rico.getStyle(dragElement, "position");
181       if (this.dragElemPosition != "absolute")
182          dragElement.style.position = "absolute";
183
184       // need to parent him into the document...
185       if ( dragElement.parentNode == null || dragElement.parentNode.nodeType == 11 )
186          document.body.appendChild(dragElement);
187
188       this.dragElement = dragElement;
189       this._updateDraggableLocation(e);
190
191       this.currentDragObjectVisible = true;
192    },
193
194    _leftOffset: function(e) {
195            return e.offsetX ? document.body.scrollLeft : 0;
196         },
197
198    _topOffset: function(e) {
199            return e.offsetY ? document.body.scrollTop : 0;
200         },
201
202
203    _updateDraggableLocation: function(e) {
204       var dragObjectStyle = this.dragElement.style;
205       var coord=Rico.eventClient(e);
206       dragObjectStyle.left = (coord.x + this._leftOffset(e) - this.startx) + "px";
207       dragObjectStyle.top  = (coord.y + this._topOffset(e) - this.starty) + "px";
208    },
209
210    _updateDropZonesHover: function(e) {
211       var i,n = this.dropZones.length;
212       for ( i = 0 ; i < n ; i++ ) {
213          if ( ! this._mousePointInDropZone( e, this.dropZones[i] ) )
214             this.dropZones[i].hideHover();
215       }
216
217       for ( i = 0 ; i < n ; i++ ) {
218          if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
219             if ( this.dropZones[i].canAccept(this.currentDragObjects) )
220                this.dropZones[i].showHover();
221          }
222       }
223    },
224
225    _startDrag: function(e) {
226       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
227          this.currentDragObjects[i].startDrag();
228       this._makeDraggableObjectVisible(e);
229    },
230
231    _mouseUpHandler: function(e) {
232       if ( ! this.hasSelection() ) return;
233       if (!Rico.eventLeftClick(e)) return;
234
235       this.interestedInMotionEvents = false;
236
237       if ( this._placeDraggableInDropZone(e) )
238          this._completeDropOperation(e);
239       else if (this.dragElement != null) {
240          Rico.eventStop(e);
241          var self=this;
242          Rico.animate(this.dragElement,
243                       {duration: 300, onEnd: function() { self._doCancelDragProcessing(); } },
244                       {left:this.origPos.left, top:this.origPos.top});
245       }
246
247      Rico.eventUnbind(document.body, "mousemove", this._mouseMove);
248      Rico.eventUnbind(document.body, "mouseup",  this._mouseUp);
249    },
250
251    _retTrue: function () {
252       return true;
253    },
254
255    _completeDropOperation: function(e) {
256       if ( this.dragElement != this.currentDragObjects[0].getMouseDownHTMLElement() ) {
257          if ( this.dragElement.parentNode != null )
258             this.dragElement.parentNode.removeChild(this.dragElement);
259       }
260
261       this._deactivateRegisteredDropZones();
262       this._endDrag();
263       this.clearSelection();
264       this.dragElement = null;
265       this.currentDragObjectVisible = false;
266       Rico.eventStop(e);
267    },
268
269    _doCancelDragProcessing: function() {
270       this._cancelDrag();
271       if ( this.dragElement == this.currentDragObjects[0].getMouseDownHTMLElement() ) {
272          this.dragElement.style.position=this.dragElemPosition;
273       } else {
274          if ( this.dragElement && this.dragElement.parentNode != null )
275             this.dragElement.parentNode.removeChild(this.dragElement);
276       }
277       this._deactivateRegisteredDropZones();
278       this.dragElement = null;
279       this.currentDragObjectVisible = false;
280    },
281
282    _placeDraggableInDropZone: function(e) {
283       var foundDropZone = false;
284       var n = this.dropZones.length;
285       for ( var i = 0 ; i < n ; i++ ) {
286          if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
287             if ( this.dropZones[i].canAccept(this.currentDragObjects) ) {
288                this.dropZones[i].hideHover();
289                this.dropZones[i].accept(this.currentDragObjects);
290                foundDropZone = true;
291                break;
292             }
293          }
294       }
295
296       return foundDropZone;
297    },
298
299    _cancelDrag: function() {
300       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
301          this.currentDragObjects[i].cancelDrag();
302    },
303
304    _endDrag: function() {
305       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
306          this.currentDragObjects[i].endDrag();
307    },
308
309    _mousePointInDropZone: function( e, dropZone ) {
310
311       var absoluteRect = dropZone.getAbsoluteRect();
312       var coord=Rico.eventClient(e);
313
314       return coord.x  > absoluteRect.left + this._leftOffset(e) &&
315              coord.x  < absoluteRect.right + this._leftOffset(e) &&
316              coord.y  > absoluteRect.top + this._topOffset(e)   &&
317              coord.y  < absoluteRect.bottom + this._topOffset(e);
318    },
319
320    _activateRegisteredDropZones: function() {
321       var n = this.dropZones.length;
322       for ( var i = 0 ; i < n ; i++ ) {
323          var dropZone = this.dropZones[i];
324          if ( dropZone.canAccept(this.currentDragObjects) )
325             dropZone.activate();
326       }
327
328       this.activatedDropZones = true;
329    },
330
331    _deactivateRegisteredDropZones: function() {
332       var n = this.dropZones.length;
333       for ( var i = 0 ; i < n ; i++ )
334          this.dropZones[i].deactivate();
335       this.activatedDropZones = false;
336    },
337
338    _attachEvents: function () {
339      Rico.eventBind(document.body, "mousemove", this._mouseMove);
340      Rico.eventBind(document.body, "mouseup",  this._mouseUp);
341    }
342
343 };
344
345
346 Rico.Draggable = function(type, htmlElement) {
347   this.initialize(type, htmlElement);
348 };
349
350 Rico.Draggable.prototype = {
351 /**
352  * @class Implements behavior for a draggable element
353  * @constructs
354  */
355    initialize: function( type, htmlElement ) {
356       this.type          = type;
357       this.htmlElement   = Rico.$(htmlElement);
358       this.selected      = false;
359    },
360
361    /**
362     *   Returns the HTML element that should have a mouse down event
363     *   added to it in order to initiate a drag operation
364     **/
365    getMouseDownHTMLElement: function() {
366       return this.htmlElement;
367    },
368
369    select: function() {
370       this._select();
371    },
372
373    _select: function() {
374       this.selected = true;
375       if (this.showingSelected) return;
376       this.showingSelected = true;
377
378       var htmlElement = this.getMouseDownHTMLElement();
379       var color = Rico.Color.createColorFromBackground(htmlElement);
380       color.isBright() ? color.darken(0.033) : color.brighten(0.033);
381       this.saveBackground = Rico.getStyle(htmlElement, "backgroundColor", "background-color");
382       htmlElement.style.backgroundColor = color.asHex();
383    },
384
385    deselect: function() {
386       this.selected = false;
387       if (!this.showingSelected) return;
388       var htmlElement = this.getMouseDownHTMLElement();
389       htmlElement.style.backgroundColor = this.saveBackground;
390       this.showingSelected = false;
391    },
392
393    isSelected: function() {
394       return this.selected;
395    },
396
397    startDrag: function() {
398    },
399
400    cancelDrag: function() {
401    },
402
403    endDrag: function() {
404    },
405
406    getSingleObjectDragGUI: function() {
407       return this.htmlElement;
408    },
409
410    getMultiObjectDragGUI: function( draggables ) {
411       return this.htmlElement;
412    },
413
414    getDroppedGUI: function() {
415       return this.htmlElement;
416    },
417
418    toString: function() {
419       return this.type + ":" + this.htmlElement + ":";
420    }
421
422 };
423
424
425 Rico.LiveGridDraggable = function(grid, rownum, colnum) {
426   this.initialize(grid, rownum, colnum);
427 };
428
429 Rico.LiveGridDraggable.prototype = Rico.extend(new Rico.Draggable(), {
430 /**
431  * @class Enables draggable behavior for LiveGrid cells.
432  * Called by LiveGrid#appendBlankRow for columns where canDrag is true.
433  * @extends Rico.Draggable
434  * @constructs
435  */
436   initialize: function( grid, rownum, colnum) {\r
437     this.type        = 'RicoCell';\r
438     this.htmlElement = grid.cell(rownum,colnum);\r
439     this.liveGrid    = grid;\r
440     this.dragRow     = rownum;\r
441     this.dragCol     = colnum;\r
442   },\r
443 \r
444   select: function() {
445     if (this.dragRow >= this.liveGrid.buffer.totalRows) return;
446     this.selected = true;
447     this.showingSelected = true;
448   },
449
450   deselect: function() {
451     this.selected = false;
452     this.showingSelected = false;
453   },
454
455   getSingleObjectDragGUI: function() {\r
456     var div = document.createElement("div");\r
457     div.className = 'LiveGridDraggable';\r
458     div.style.width = (this.htmlElement.offsetWidth - 10) + "px";\r
459     div.innerHTML=this.htmlElement.innerHTML;
460     return div;\r
461   }\r
462 });\r
463
464
465 Rico.Dropzone = function(htmlElement) {
466   this.initialize(htmlElement);
467 };
468
469 Rico.Dropzone.prototype = {
470 /**
471  * @class Implements behavior for a drop zone
472  * @constructs
473  */
474    initialize: function( htmlElement ) {
475       this.htmlElement  = Rico.$(htmlElement);
476       this.absoluteRect = null;
477    },
478
479    getHTMLElement: function() {
480       return this.htmlElement;
481    },
482
483    clearPositionCache: function() {
484       this.absoluteRect = null;
485    },
486
487    getAbsoluteRect: function() {
488       if ( this.absoluteRect == null ) {
489          var htmlElement = this.getHTMLElement();
490          var pos = Rico.viewportOffset(htmlElement);
491
492          this.absoluteRect = {
493             top:    pos.top,
494             left:   pos.left,
495             bottom: pos.top + htmlElement.offsetHeight,
496             right:  pos.left + htmlElement.offsetWidth
497          };
498       }
499       return this.absoluteRect;
500    },
501
502    activate: function() {
503       var htmlElement = this.getHTMLElement();
504       if (htmlElement == null  || this.showingActive)
505          return;
506
507       this.showingActive = true;
508       this.saveBackgroundColor = htmlElement.style.backgroundColor;
509
510       var fallbackColor = "#ffea84";
511       var currentColor = Rico.Color.createColorFromBackground(htmlElement);
512       if ( currentColor == null )
513          htmlElement.style.backgroundColor = fallbackColor;
514       else {
515          currentColor.isBright() ? currentColor.darken(0.2) : currentColor.brighten(0.2);
516          htmlElement.style.backgroundColor = currentColor.asHex();
517       }
518    },
519
520    deactivate: function() {
521       var htmlElement = this.getHTMLElement();
522       if (htmlElement == null || !this.showingActive)
523          return;
524
525       htmlElement.style.backgroundColor = this.saveBackgroundColor;
526       this.showingActive = false;
527       this.saveBackgroundColor = null;
528    },
529
530    showHover: function() {
531       var htmlElement = this.getHTMLElement();
532       if ( htmlElement == null || this.showingHover )
533          return;
534
535       this.saveBorderWidth = htmlElement.style.borderWidth;
536       this.saveBorderStyle = htmlElement.style.borderStyle;
537       this.saveBorderColor = htmlElement.style.borderColor;
538
539       this.showingHover = true;
540       htmlElement.style.borderWidth = "1px";
541       htmlElement.style.borderStyle = "solid";
542       //htmlElement.style.borderColor = "#ff9900";
543       htmlElement.style.borderColor = "#ffff00";
544    },
545
546    hideHover: function() {
547       var htmlElement = this.getHTMLElement();
548       if ( htmlElement == null || !this.showingHover )
549          return;
550
551       htmlElement.style.borderWidth = this.saveBorderWidth;
552       htmlElement.style.borderStyle = this.saveBorderStyle;
553       htmlElement.style.borderColor = this.saveBorderColor;
554       this.showingHover = false;
555    },
556
557    canAccept: function(draggableObjects) {
558       return true;
559    },
560
561    accept: function(draggableObjects) {
562       var htmlElement = this.getHTMLElement();
563       if ( htmlElement == null )
564          return;
565
566       var n = draggableObjects.length;
567       for ( var i = 0 ; i < n ; i++ ) {
568          var theGUI = draggableObjects[i].getDroppedGUI();
569          if ( Rico.getStyle( theGUI, "position" ) == "absolute" ) {
570             theGUI.style.position = "static";
571             theGUI.style.top = "";
572             theGUI.style.top = "";
573          }
574          htmlElement.appendChild(theGUI);
575       }
576    }
577 };