Import src directory from OpenRico 2.1
[misc/kostenrechnung] / lib / rico / ricoDragDrop.js
1 /*
2  *  Copyright 2005 Sabre Airline Solutions
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
5  *  file except in compliance with the License. You may obtain a copy of the License at
6  *
7  *         http://www.apache.org/licenses/LICENSE-2.0
8  *
9  *  Unless required by applicable law or agreed to in writing, software distributed under the
10  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11  *  either express or implied. See the License for the specific language governing permissions
12  *  and limitations under the License.
13  */
14
15 Rico.DragAndDrop = Class.create(
16 /** @lends Rico.DragAndDrop# */
17 {
18 /**
19  * @class Implements drag-n-drop manager. Used by {@link dndMgr}.
20  * @constructs
21  */
22    initialize: function() {
23       this.dropZones                = new Array();
24       this.draggables               = new Array();
25       this.currentDragObjects       = new Array();
26       this.dragElement              = null;
27       this.lastSelectedDraggable    = null;
28       this.currentDragObjectVisible = false;
29       this.interestedInMotionEvents = false;
30       this._mouseDown = this._mouseDownHandler.bindAsEventListener(this);
31       this._mouseMove = this._mouseMoveHandler.bindAsEventListener(this);
32       this._mouseUp = this._mouseUpHandler.bindAsEventListener(this);
33    },
34
35    registerDropZone: function(aDropZone) {
36       this.dropZones[ this.dropZones.length ] = aDropZone;
37    },
38
39    deregisterDropZone: function(aDropZone) {
40       var newDropZones = new Array();
41       var j = 0;
42       for ( var i = 0 ; i < this.dropZones.length ; i++ ) {
43          if ( this.dropZones[i] != aDropZone )
44             newDropZones[j++] = this.dropZones[i];
45       }
46
47       this.dropZones = newDropZones;
48    },
49
50    clearDropZones: function() {
51       this.dropZones = new Array();
52    },
53
54    registerDraggable: function( aDraggable ) {
55       this.draggables[ this.draggables.length ] = aDraggable;
56       this._addMouseDownHandler( aDraggable );
57    },
58
59    clearSelection: function() {
60       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
61          this.currentDragObjects[i].deselect();
62       this.currentDragObjects = new Array();
63       this.lastSelectedDraggable = null;
64    },
65
66    hasSelection: function() {
67       return this.currentDragObjects.length > 0;
68    },
69
70    setStartDragFromElement: function( e, mouseDownElement ) {
71       this.origPos = RicoUtil.toDocumentPosition(mouseDownElement);
72       this.startx = e.screenX - this.origPos.x;
73       this.starty = e.screenY - this.origPos.y;
74
75       this.interestedInMotionEvents = this.hasSelection();
76       this._terminateEvent(e);
77    },
78
79    updateSelection: function( draggable, extendSelection ) {
80       if ( ! extendSelection )
81          this.clearSelection();
82
83       if ( draggable.isSelected() ) {
84          this.currentDragObjects=this.currentDragObjects.without(draggable);
85          draggable.deselect();
86          if ( draggable == this.lastSelectedDraggable )
87             this.lastSelectedDraggable = null;
88       }
89       else {
90          draggable.select();
91          if ( draggable.isSelected() ) {
92            this.currentDragObjects.push(draggable);
93            this.lastSelectedDraggable = draggable;
94          }
95       }
96    },
97
98    _mouseDownHandler: function(e) {
99       //Rico.writeDebugMsg("_mouseDownHandler");
100       if ( arguments.length == 0 )
101          e = event;
102
103       // if not button 1 ignore it...
104       var nsEvent = e.which != undefined;
105       if ( (nsEvent && e.which != 1) || (!nsEvent && e.button != 1))
106          return;
107
108       var eventTarget      = e.target ? e.target : e.srcElement;
109       var draggableObject  = eventTarget.ricoDraggable;
110
111       var candidate = eventTarget;
112       while (draggableObject == null && candidate.parentNode) {
113          candidate = candidate.parentNode;
114          draggableObject = candidate.ricoDraggable;
115       }
116    
117       if ( draggableObject == null )
118          return;
119
120       this.updateSelection( draggableObject, e.ctrlKey );
121
122       // clear the drop zones postion cache...
123       if ( this.hasSelection() ) {
124          for ( var i = 0 ; i < this.dropZones.length ; i++ )
125             this.dropZones[i].clearPositionCache();
126       }
127       this.setStartDragFromElement( e, draggableObject.getMouseDownHTMLElement() );
128    },
129
130
131    _mouseMoveHandler: function(e) {
132       var nsEvent = e.which != undefined;
133       if ( !this.interestedInMotionEvents ) {
134          //this._terminateEvent(e);
135          return;
136       }
137
138       if ( ! this.hasSelection() )
139          return;
140
141       if ( ! this.currentDragObjectVisible )
142          this._startDrag(e);
143
144       if ( !this.activatedDropZones )
145          this._activateRegisteredDropZones();
146
147       this._updateDraggableLocation(e);
148       this._updateDropZonesHover(e);
149
150       this._terminateEvent(e);
151    },
152
153    _makeDraggableObjectVisible: function(e)
154    {
155       if ( !this.hasSelection() )
156          return;
157
158       var dragElement;
159       if ( this.currentDragObjects.length > 1 )
160          dragElement = this.currentDragObjects[0].getMultiObjectDragGUI(this.currentDragObjects);
161       else
162          dragElement = this.currentDragObjects[0].getSingleObjectDragGUI();
163
164       // go ahead and absolute position it...
165       this.dragElemPosition=RicoUtil.getElementsComputedStyle(dragElement, "position");
166       if (this.dragElemPosition != "absolute")
167          dragElement.style.position = "absolute";
168
169       // need to parent him into the document...
170       if ( dragElement.parentNode == null || dragElement.parentNode.nodeType == 11 )
171          document.body.appendChild(dragElement);
172
173       this.dragElement = dragElement;
174       this._updateDraggableLocation(e);
175
176       this.currentDragObjectVisible = true;
177    },
178
179    _leftOffset: function(e) {
180            return e.offsetX ? document.body.scrollLeft : 0;
181         },
182
183    _topOffset: function(e) {
184            return e.offsetY ? document.body.scrollTop : 0;
185         },
186
187                 
188    _updateDraggableLocation: function(e) {
189       var dragObjectStyle = this.dragElement.style;
190       dragObjectStyle.left = (e.screenX + this._leftOffset(e) - this.startx) + "px";
191       dragObjectStyle.top  = (e.screenY + this._topOffset(e) - this.starty) + "px";
192    },
193
194    _updateDropZonesHover: function(e) {
195       var i,n = this.dropZones.length;
196       for ( i = 0 ; i < n ; i++ ) {
197          if ( ! this._mousePointInDropZone( e, this.dropZones[i] ) )
198             this.dropZones[i].hideHover();
199       }
200
201       for ( i = 0 ; i < n ; i++ ) {
202          if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
203             if ( this.dropZones[i].canAccept(this.currentDragObjects) )
204                this.dropZones[i].showHover();
205          }
206       }
207    },
208
209    _startDrag: function(e) {
210       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
211          this.currentDragObjects[i].startDrag();
212
213       this._makeDraggableObjectVisible(e);
214    },
215
216    _mouseUpHandler: function(e) {
217       //Rico.writeDebugMsg("_mouseUpHandler");
218       if ( ! this.hasSelection() ) return;
219
220       var nsEvent = e.which != undefined;
221       if ( (nsEvent && e.which != 1) || (!nsEvent && e.button != 1))
222          return;
223
224       this.interestedInMotionEvents = false;
225
226       if ( this._placeDraggableInDropZone(e) )
227          this._completeDropOperation(e);
228       else if (this.dragElement != null) {
229          this._terminateEvent(e);
230          Rico.animate(new Rico.Effect.Position( this.dragElement, this.origPos.x, this.origPos.y),
231                       {duration: 200,
232                        steps: 20,
233                        onFinish : this._doCancelDragProcessing.bind(this) } );
234       }
235
236      Event.stopObserving(document.body, "mousemove", this._mouseMove);
237      Event.stopObserving(document.body, "mouseup",  this._mouseUp);
238    },
239
240    _retTrue: function () {
241       return true;
242    },
243
244    _completeDropOperation: function(e) {
245       if ( this.dragElement != this.currentDragObjects[0].getMouseDownHTMLElement() ) {
246          if ( this.dragElement.parentNode != null )
247             this.dragElement.parentNode.removeChild(this.dragElement);
248       }
249
250       this._deactivateRegisteredDropZones();
251       this._endDrag();
252       this.clearSelection();
253       this.dragElement = null;
254       this.currentDragObjectVisible = false;
255       this._terminateEvent(e);
256    },
257
258    _doCancelDragProcessing: function() {
259       this._cancelDrag();
260       if ( this.dragElement == this.currentDragObjects[0].getMouseDownHTMLElement() ) {
261          this.dragElement.style.position=this.dragElemPosition;
262       } else {
263          if ( this.dragElement && this.dragElement.parentNode != null )
264             this.dragElement.parentNode.removeChild(this.dragElement);
265       }
266       this._deactivateRegisteredDropZones();
267       this.dragElement = null;
268       this.currentDragObjectVisible = false;
269    },
270
271    _placeDraggableInDropZone: function(e) {
272       var foundDropZone = false;
273       var n = this.dropZones.length;
274       for ( var i = 0 ; i < n ; i++ ) {
275          if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
276             if ( this.dropZones[i].canAccept(this.currentDragObjects) ) {
277                this.dropZones[i].hideHover();
278                this.dropZones[i].accept(this.currentDragObjects);
279                foundDropZone = true;
280                break;
281             }
282          }
283       }
284
285       return foundDropZone;
286    },
287
288    _cancelDrag: function() {
289       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
290          this.currentDragObjects[i].cancelDrag();
291    },
292
293    _endDrag: function() {
294       for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
295          this.currentDragObjects[i].endDrag();
296    },
297
298    _mousePointInDropZone: function( e, dropZone ) {
299
300       var absoluteRect = dropZone.getAbsoluteRect();
301
302       return e.clientX  > absoluteRect.left + this._leftOffset(e) &&
303              e.clientX  < absoluteRect.right + this._leftOffset(e) &&
304              e.clientY  > absoluteRect.top + this._topOffset(e)   &&
305              e.clientY  < absoluteRect.bottom + this._topOffset(e);
306    },
307
308    _addMouseDownHandler: function( aDraggable )
309    {
310       var htmlElement  = aDraggable.getMouseDownHTMLElement();
311       if ( htmlElement  != null ) { 
312          htmlElement.ricoDraggable = aDraggable;
313          Event.observe(htmlElement , "mousedown", this._onmousedown.bindAsEventListener(this));
314          Event.observe(htmlElement, "mousedown", this._mouseDown);
315       }
316    },
317
318    _activateRegisteredDropZones: function() {
319       var n = this.dropZones.length;
320       for ( var i = 0 ; i < n ; i++ ) {
321          var dropZone = this.dropZones[i];
322          if ( dropZone.canAccept(this.currentDragObjects) )
323             dropZone.activate();
324       }
325
326       this.activatedDropZones = true;
327    },
328
329    _deactivateRegisteredDropZones: function() {
330       var n = this.dropZones.length;
331       for ( var i = 0 ; i < n ; i++ )
332          this.dropZones[i].deactivate();
333       this.activatedDropZones = false;
334    },
335
336    _onmousedown: function () {
337      //Rico.writeDebugMsg("_onmousedown (attaching events)");
338      Event.observe(document.body, "mousemove", this._mouseMove);
339      Event.observe(document.body, "mouseup",  this._mouseUp);
340    },
341
342    _terminateEvent: function(e) {
343       Event.stop(e);
344    },
345
346
347            initializeEventHandlers: function() {
348               if ( typeof document.implementation != "undefined" &&
349                  document.implementation.hasFeature("HTML",   "1.0") &&
350                  document.implementation.hasFeature("Events", "2.0") &&
351                  document.implementation.hasFeature("CSS",    "2.0") ) {
352                  document.addEventListener("mouseup",   this._mouseUpHandler.bindAsEventListener(this),  false);
353                  document.addEventListener("mousemove", this._mouseMoveHandler.bindAsEventListener(this), false);
354               }
355               else {
356                  document.attachEvent( "onmouseup",   this._mouseUpHandler.bindAsEventListener(this) );
357                  document.attachEvent( "onmousemove", this._mouseMoveHandler.bindAsEventListener(this) );
358               }
359            }
360         });
361
362
363 /** @namespace Instance of {@link Rico.DragAndDrop} that manages the interaction of draggables to drop zones */
364 var dndMgr = new Rico.DragAndDrop();
365
366
367 Rico.Draggable = Class.create(
368 /** @lends Rico.Draggable# */
369 {
370 /**
371  * @class Implements behavior for a draggable element
372  * @constructs
373  */
374    initialize: function( type, htmlElement ) {
375       this.type          = type;
376       this.htmlElement   = $(htmlElement);
377       this.selected      = false;
378    },
379
380    /**
381     *   Returns the HTML element that should have a mouse down event
382     *   added to it in order to initiate a drag operation
383     **/
384    getMouseDownHTMLElement: function() {
385       return this.htmlElement;
386    },
387
388    select: function() {
389       this._select();
390    },
391
392    _select: function() {
393       this.selected = true;
394       if (this.showingSelected) return;
395       this.showingSelected = true;
396
397       var htmlElement = this.getMouseDownHTMLElement();
398       var color = Rico.Color.createColorFromBackground(htmlElement);
399       color.isBright() ? color.darken(0.033) : color.brighten(0.033);
400       this.saveBackground = RicoUtil.getElementsComputedStyle(htmlElement, "backgroundColor", "background-color");
401       htmlElement.style.backgroundColor = color.asHex();
402    },
403
404    deselect: function() {
405       this.selected = false;
406       if (!this.showingSelected) return;
407       var htmlElement = this.getMouseDownHTMLElement();
408       htmlElement.style.backgroundColor = this.saveBackground;
409       this.showingSelected = false;
410    },
411
412    isSelected: function() {
413       return this.selected;
414    },
415
416    startDrag: function() {
417    },
418
419    cancelDrag: function() {
420    },
421
422    endDrag: function() {
423    },
424
425    getSingleObjectDragGUI: function() {
426       return this.htmlElement;
427    },
428
429    getMultiObjectDragGUI: function( draggables ) {
430       return this.htmlElement;
431    },
432
433    getDroppedGUI: function() {
434       return this.htmlElement;
435    },
436
437    toString: function() {
438       return this.type + ":" + this.htmlElement + ":";
439    }
440
441 });
442
443
444 Rico.LiveGridDraggable = Class.create();
445 /** @lends Rico.LiveGridDraggable# */
446 Rico.LiveGridDraggable.prototype = Object.extend(new Rico.Draggable(), {
447 /**
448  * @class Enables draggable behavior for LiveGrid cells.
449  * Called by LiveGrid#appendBlankRow for columns where canDrag is true.
450  * @extends Rico.Draggable
451  * @constructs
452  */
453   initialize: function( grid, rownum, colnum) {
454     this.type        = 'RicoCell';
455     this.htmlElement = grid.cell(rownum,colnum);
456     this.liveGrid    = grid;
457     this.dragRow     = rownum;
458     this.dragCol     = colnum;
459   },
460   
461   select: function() {
462     if (this.dragRow >= this.liveGrid.buffer.totalRows) return;
463     this.selected = true;
464     this.showingSelected = true;
465   },
466
467   deselect: function() {
468     this.selected = false;
469     this.showingSelected = false;
470   },
471
472   getSingleObjectDragGUI: function() {
473     var div = document.createElement("div");
474     div.className = 'LiveGridDraggable';
475     div.style.width = (this.htmlElement.offsetWidth - 10) + "px";
476     Element.insert(div,this.htmlElement.innerHTML);
477     return div;
478   }
479 });
480
481
482 Rico.Dropzone = Class.create(
483 /** @lends Rico.Dropzone# */
484 {
485 /**
486  * @class Implements behavior for a drop zone
487  * @constructs
488  */
489    initialize: function( htmlElement ) {
490       this.htmlElement  = $(htmlElement);
491       this.absoluteRect = null;
492    },
493
494    getHTMLElement: function() {
495       return this.htmlElement;
496    },
497
498    clearPositionCache: function() {
499       this.absoluteRect = null;
500    },
501
502    getAbsoluteRect: function() {
503       if ( this.absoluteRect == null ) {
504          var htmlElement = this.getHTMLElement();
505          var pos = RicoUtil.toViewportPosition(htmlElement);
506
507          this.absoluteRect = {
508             top:    pos.y,
509             left:   pos.x,
510             bottom: pos.y + htmlElement.offsetHeight,
511             right:  pos.x + htmlElement.offsetWidth
512          };
513       }
514       return this.absoluteRect;
515    },
516
517    activate: function() {
518       var htmlElement = this.getHTMLElement();
519       if (htmlElement == null  || this.showingActive)
520          return;
521
522       this.showingActive = true;
523       this.saveBackgroundColor = htmlElement.style.backgroundColor;
524
525       var fallbackColor = "#ffea84";
526       var currentColor = Rico.Color.createColorFromBackground(htmlElement);
527       if ( currentColor == null )
528          htmlElement.style.backgroundColor = fallbackColor;
529       else {
530          currentColor.isBright() ? currentColor.darken(0.2) : currentColor.brighten(0.2);
531          htmlElement.style.backgroundColor = currentColor.asHex();
532       }
533    },
534
535    deactivate: function() {
536       var htmlElement = this.getHTMLElement();
537       if (htmlElement == null || !this.showingActive)
538          return;
539
540       htmlElement.style.backgroundColor = this.saveBackgroundColor;
541       this.showingActive = false;
542       this.saveBackgroundColor = null;
543    },
544
545    showHover: function() {
546       var htmlElement = this.getHTMLElement();
547       if ( htmlElement == null || this.showingHover )
548          return;
549
550       this.saveBorderWidth = htmlElement.style.borderWidth;
551       this.saveBorderStyle = htmlElement.style.borderStyle;
552       this.saveBorderColor = htmlElement.style.borderColor;
553
554       this.showingHover = true;
555       htmlElement.style.borderWidth = "1px";
556       htmlElement.style.borderStyle = "solid";
557       //htmlElement.style.borderColor = "#ff9900";
558       htmlElement.style.borderColor = "#ffff00";
559    },
560
561    hideHover: function() {
562       var htmlElement = this.getHTMLElement();
563       if ( htmlElement == null || !this.showingHover )
564          return;
565
566       htmlElement.style.borderWidth = this.saveBorderWidth;
567       htmlElement.style.borderStyle = this.saveBorderStyle;
568       htmlElement.style.borderColor = this.saveBorderColor;
569       this.showingHover = false;
570    },
571
572    canAccept: function(draggableObjects) {
573       return true;
574    },
575
576    accept: function(draggableObjects) {
577       var htmlElement = this.getHTMLElement();
578       if ( htmlElement == null )
579          return;
580
581       var n = draggableObjects.length;
582       for ( var i = 0 ; i < n ; i++ )
583       {
584          var theGUI = draggableObjects[i].getDroppedGUI();
585 /*         if (Element.getStyle(theGUI,'position')=='absolute')*/
586          if ( RicoUtil.getElementsComputedStyle( theGUI, "position" ) == "absolute" )
587          {
588             theGUI.style.position = "static";
589             theGUI.style.top = "";
590             theGUI.style.top = "";
591          }
592          htmlElement.appendChild(theGUI);
593       }
594    }
595 });
596
597 RicoUtil = Object.extend(RicoUtil, 
598 /** @lends RicoUtil# */
599 {
600 /**
601  * @deprecated Use Prototype's Element#getStyle instead
602  */
603    getElementsComputedStyle: function ( htmlElement, cssProperty, mozillaEquivalentCSS) {
604       if ( arguments.length == 2 )
605          mozillaEquivalentCSS = cssProperty;
606
607       var el = $(htmlElement);
608       if ( el.currentStyle )
609          return el.currentStyle[cssProperty];
610       else
611          return document.defaultView.getComputedStyle(el, null).getPropertyValue(mozillaEquivalentCSS);
612    },
613
614  /**
615  * @deprecated Use Prototype's Element#viewportOffset instead
616  */
617   toViewportPosition: function(element) {
618       return this._toAbsolute(element,true);
619    },
620
621  /**
622  * @deprecated Use Prototype's Element#cumulativeOffset instead
623  */
624    toDocumentPosition: function(element) {
625       return this._toAbsolute(element,false);
626    },
627
628    /**
629     *  Compute the elements position in terms of the window viewport
630     *  so that it can be compared to the position of the mouse (dnd)
631     *  This is additions of all the offsetTop,offsetLeft values up the
632     *  offsetParent hierarchy, ...taking into account any scrollTop,
633     *  scrollLeft values along the way...
634     *
635     * IE has a bug reporting a correct offsetLeft of elements within a
636     * a relatively positioned parent!!!
637     **/
638    _toAbsolute: function(element,accountForDocScroll) {
639
640       if ( !Prototype.Browser.IE && !Prototype.Browser.Opera )
641          return this._toAbsoluteMozilla(element,accountForDocScroll);
642
643       var x = 0;
644       var y = 0;
645       var parent = element;
646       while ( parent ) {
647
648          var borderXOffset = 0;
649          var borderYOffset = 0;
650          if ( parent != element ) {
651             borderXOffset = parseInt(this.getElementsComputedStyle(parent, "borderLeftWidth" ),10);
652             borderYOffset = parseInt(this.getElementsComputedStyle(parent, "borderTopWidth" ),10);
653             borderXOffset = isNaN(borderXOffset) ? 0 : borderXOffset;
654             borderYOffset = isNaN(borderYOffset) ? 0 : borderYOffset;
655          }
656
657          x += parent.offsetLeft - parent.scrollLeft + borderXOffset;
658          y += parent.offsetTop - parent.scrollTop + borderYOffset;
659          parent = parent.offsetParent;
660       }
661
662       if ( accountForDocScroll ) {
663          x -= this.docScrollLeft();
664          y -= this.docScrollTop();
665       }
666
667       return { x:x, y:y };
668    },
669
670    /**
671     *  Mozilla did not report all of the parents up the hierarchy via the
672     *  offsetParent property that IE did.  So for the calculation of the
673     *  offsets we use the offsetParent property, but for the calculation of
674     *  the scrollTop/scrollLeft adjustments we navigate up via the parentNode
675     *  property instead so as to get the scroll offsets...
676     *
677     **/
678    _toAbsoluteMozilla: function(element,accountForDocScroll) {
679       var x = 0;
680       var y = 0;
681       var parent = element;
682       while ( parent ) {
683          x += parent.offsetLeft;
684          y += parent.offsetTop;
685          parent = parent.offsetParent;
686       }
687
688       parent = element;
689       while ( parent &&
690               parent != document.body &&
691               parent != document.documentElement ) {
692          if ( parent.scrollLeft  )
693             x -= parent.scrollLeft;
694          if ( parent.scrollTop )
695             y -= parent.scrollTop;
696          parent = parent.parentNode;
697       }
698
699       if ( accountForDocScroll ) {
700          x -= this.docScrollLeft();
701          y -= this.docScrollTop();
702       }
703
704       return { x:x, y:y };
705    }
706
707 });
708
709 Rico.includeLoaded('ricoDragDrop.js');