View Javadoc

1   /*
2    * Ext GWT 2.2.0 - Ext for GWT
3    * Copyright(c) 2007-2010, Ext JS, LLC.
4    * licensing@extjs.com
5    * 
6    * http://extjs.com/license
7    */
8   package com.extjs.gxt.ui.client.fx;
9   
10  import com.extjs.gxt.ui.client.Style;
11  import com.extjs.gxt.ui.client.core.El;
12  import com.extjs.gxt.ui.client.core.XDOM;
13  import com.extjs.gxt.ui.client.event.BaseObservable;
14  import com.extjs.gxt.ui.client.event.ComponentEvent;
15  import com.extjs.gxt.ui.client.event.DragEvent;
16  import com.extjs.gxt.ui.client.event.DragListener;
17  import com.extjs.gxt.ui.client.event.Events;
18  import com.extjs.gxt.ui.client.event.Listener;
19  import com.extjs.gxt.ui.client.event.PreviewEvent;
20  import com.extjs.gxt.ui.client.util.BaseEventPreview;
21  import com.extjs.gxt.ui.client.util.Rectangle;
22  import com.extjs.gxt.ui.client.widget.Component;
23  import com.extjs.gxt.ui.client.widget.Shim;
24  import com.google.gwt.event.dom.client.KeyCodes;
25  import com.google.gwt.user.client.DOM;
26  import com.google.gwt.user.client.Element;
27  import com.google.gwt.user.client.Event;
28  import com.google.gwt.user.client.Window;
29  
30  /**
31   * Adds drag behavior to any widget. Drag operations can be initiated from the
32   * widget itself, or another widget, such as the header in a dialog.
33   * 
34   * <p/>
35   * It is possible to specify event targets that will be ignored. If the target
36   * element has a 'x-nodrag' style it will not trigger a drag operation.
37   * 
38   * <dl>
39   * <dt><b>Events:</b></dt>
40   * 
41   * <dd><b>DragStart</b> : DragEvent(draggable, component, event) <br>
42   * <div>Fires after a drag has started.</div>
43   * <ul>
44   * <li>draggable : this</li>
45   * <li>component : drag component</li>
46   * <li>event : the dom event</li>
47   * </ul>
48   * </dd>
49   * 
50   * <dd><b>DragMove</b> : DragEvent(draggable, component, event)<br>
51   * <div>Fires after the mouse moves.</div>
52   * <li>draggable : this</li>
53   * <li>component : drag component</li>
54   * <li>event : the dom event</li>
55   * </ul>
56   * </dd>
57   * 
58   * <dd><b>DragCancel</b> : DragEvent(draggable, component, event)<br>
59   * <div>Fires after a drag has been cancelled.</div>
60   * <ul>
61   * <li>draggable : this</li>
62   * <li>component : drag component</li>
63   * <li>event : the dom event</li>
64   * </ul>
65   * </dd>
66   * 
67   * <dd><b>DragEnd</b> : DragEvent(draggable, component, event) <br>
68   * <div>Fires after a drag has ended.</div>
69   * <ul>
70   * <li>draggable : this</li>
71   * <li>component : drag widget</li>
72   * <li>event : the dom event</li>
73   * </ul>
74   * </dd>
75   * </dl>
76   */
77  public class Draggable extends BaseObservable {
78  
79    protected int conX, conY, conWidth, conHeight;
80    protected int dragStartX, dragStartY;
81    protected int lastX, lastY;
82    protected Rectangle startBounds;
83    protected El proxyEl;
84  
85    // config
86    private boolean updateZIndex = true;
87    private boolean sizeProxyToSource = true;
88    private boolean constrainHorizontal;
89    private boolean moveAfterProxyDrag = true;
90    private boolean constrainVertical;
91    private boolean constrainClient = true;
92    private boolean useProxy = true;
93    private int xLeft = Style.DEFAULT, xRight = Style.DEFAULT;
94    private int xTop = Style.DEFAULT, xBottom = Style.DEFAULT;
95    private String proxyStyle = "x-drag-proxy";
96    private Component container;
97    private Component dragWidget;
98    private Component handle;
99    private boolean dragging;
100   private boolean enabled = true;
101   private int clientWidth, clientHeight;
102   private BaseEventPreview preview;
103   private DragEvent dragEvent;
104   private int startDragDistance = 2;
105   private Listener<ComponentEvent> listener;
106 
107   /**
108    * Creates a new draggable instance.
109    * 
110    * @param dragComponent the component to be dragged
111    */
112   public Draggable(Component dragComponent) {
113     this(dragComponent, dragComponent);
114   }
115 
116   /**
117    * Create a new draggable instance.
118    * 
119    * @param dragComponent the component to be dragged
120    * @param handle the component drags will be initiated from
121    */
122   public Draggable(final Component dragComponent, final Component handle) {
123     listener = new Listener<ComponentEvent>() {
124       public void handleEvent(ComponentEvent ce) {
125         onMouseDown(ce);
126       }
127     };
128     this.dragWidget = dragComponent;
129     this.handle = handle;
130 
131     handle.addListener(Events.OnMouseDown, listener);
132 
133     preview = new BaseEventPreview() {
134 
135       @Override
136       public boolean onPreview(PreviewEvent event) {
137         event.preventDefault();
138         switch (event.getEventTypeInt()) {
139           case Event.ONKEYDOWN:
140             if (dragging && event.getKeyCode() == KeyCodes.KEY_ESCAPE) {
141               cancelDrag();
142             }
143             break;
144           case Event.ONMOUSEMOVE:
145             onMouseMove(event.getEvent());
146             break;
147           case Event.ONMOUSEUP:
148             stopDrag(event.getEvent());
149             break;
150         }
151         return true;
152       }
153 
154     };
155     preview.setAutoHide(false);
156 
157     handle.sinkEvents(Event.ONMOUSEDOWN);
158   }
159 
160   /**
161    * Adds a listener to receive drag events.
162    * 
163    * @param listener the drag listener to be added
164    */
165   public void addDragListener(DragListener listener) {
166     addListener(Events.DragStart, listener);
167     addListener(Events.DragMove, listener);
168     addListener(Events.DragCancel, listener);
169     addListener(Events.DragEnd, listener);
170   }
171 
172   /**
173    * Cancels the drag if running.
174    */
175   public void cancelDrag() {
176     preview.remove();
177     if (dragging) {
178       dragging = false;
179       if (isUseProxy()) {
180         proxyEl.disableTextSelection(false);
181         proxyEl.setVisibility(false);
182         proxyEl.remove();
183       } else {
184         dragWidget.el().setPagePosition(startBounds.x, startBounds.y);
185       }
186 
187       fireEvent(Events.DragCancel, new DragEvent(this));
188       afterDrag();
189     }
190   }
191 
192   /**
193    * Returns the drag container.
194    * 
195    * @return the drag container
196    */
197   public Component getContainer() {
198     return container;
199   }
200 
201   /**
202    * Returns the drag handle.
203    * 
204    * @return the drag handle
205    */
206   public Component getDragHandle() {
207     return handle;
208   }
209 
210   /**
211    * Returns the widget being dragged.
212    * 
213    * @return the drag widget
214    */
215   public Component getDragWidget() {
216     return dragWidget;
217   }
218 
219   /**
220    * Returns the proxy style.
221    * 
222    * @return the proxy style
223    */
224   public String getProxyStyle() {
225     return proxyStyle;
226   }
227 
228   /**
229    * Returns the number of pixels the cursor must move before dragging begins.
230    * 
231    * @return the distance in pixels
232    */
233   public int getStartDragDistance() {
234     return startDragDistance;
235   }
236 
237   /**
238    * Returns true if drag is constrained to the viewport.
239    * 
240    * @return the constrain client state
241    */
242   public boolean isConstrainClient() {
243     return constrainClient;
244   }
245 
246   /**
247    * Returns true if horizontal movement is constrained.
248    * 
249    * @return the horizontal constrain state
250    */
251   public boolean isConstrainHorizontal() {
252     return constrainHorizontal;
253   }
254 
255   /**
256    * Returns true if vertical movement is constrained.
257    * 
258    * @return true if vertical movement is constrained
259    */
260   public boolean isConstrainVertical() {
261     return constrainVertical;
262   }
263 
264   /**
265    * Returns <code>true</code> if a drag is in progress.
266    * 
267    * @return the drag state
268    */
269   public boolean isDragging() {
270     return dragging;
271   }
272 
273   /**
274    * Returns <code>true</code> if enabled.
275    * 
276    * @return the enable state
277    */
278   public boolean isEnabled() {
279     return enabled;
280   }
281 
282   /**
283    * Returns true if the drag widget is moved after a proxy drag.
284    * 
285    * @return the move after proxy state
286    */
287   public boolean isMoveAfterProxyDrag() {
288     return moveAfterProxyDrag;
289   }
290 
291   /**
292    * Returns true if the proxy element is sized to match the drag widget.
293    * 
294    * @return the size proxy to source state
295    */
296   public boolean isSizeProxyToSource() {
297     return sizeProxyToSource;
298   }
299 
300   /**
301    * Returns true if the z-index is updated after a drag.
302    * 
303    * @return the update z-index state
304    */
305   public boolean isUpdateZIndex() {
306     return updateZIndex;
307   }
308 
309   /**
310    * Returns true if proxy element is enabled.
311    * 
312    * @return the use proxy state
313    */
314   public boolean isUseProxy() {
315     return useProxy;
316   }
317 
318   /**
319    * Removes the drag handles.
320    */
321   public void release() {
322     cancelDrag();
323     handle.removeListener(Events.OnMouseDown, listener);
324   }
325 
326   /**
327    * Removes a previously added listener.
328    * 
329    * @param listener the listener to be removed
330    */
331   public void removeDragListener(DragListener listener) {
332     if (hasListeners()) {
333       removeListener(Events.DragStart, listener);
334       removeListener(Events.DragMove, listener);
335       removeListener(Events.DragCancel, listener);
336       removeListener(Events.DragEnd, listener);
337     }
338   }
339 
340   /**
341    * True to set constrain movement to the viewport (defaults to true).
342    * 
343    * @param constrainClient true to constrain to viewport
344    */
345   public void setConstrainClient(boolean constrainClient) {
346     this.constrainClient = constrainClient;
347   }
348 
349   /**
350    * True to stop horizontal movement (defaults to false).
351    * 
352    * @param constrainHorizontal true to stop horizontal movement
353    */
354   public void setConstrainHorizontal(boolean constrainHorizontal) {
355     this.constrainHorizontal = constrainHorizontal;
356   }
357 
358   /**
359    * True to stop vertical movement (defaults to false).
360    * 
361    * @param constrainVertical true to stop vertical movement
362    */
363   public void setConstrainVertical(boolean constrainVertical) {
364     this.constrainVertical = constrainVertical;
365   }
366 
367   /**
368    * Specifies a container to which the drag widget is constrained.
369    * 
370    * @param container the container
371    */
372   public void setContainer(Component container) {
373     this.container = container;
374   }
375 
376   /**
377    * Enables dragging if the argument is <code>true</code>, and disables it
378    * otherwise.
379    * 
380    * @param enabled the new enabled state
381    */
382   public void setEnabled(boolean enabled) {
383     this.enabled = enabled;
384   }
385 
386   /**
387    * True to move source widget after a proxy drag (defaults to true).
388    * 
389    * @param moveAfterProxyDrag true to move after a proxy drag
390    */
391   public void setMoveAfterProxyDrag(boolean moveAfterProxyDrag) {
392     this.moveAfterProxyDrag = moveAfterProxyDrag;
393   }
394 
395   /**
396    * Sets the proxy element.
397    * 
398    * @param element the proxy element
399    */
400   public void setProxy(El element) {
401     proxyEl = element;
402   }
403 
404   /**
405    * Sets the style name used for proxy drags (defaults to 'my-drag-proxy').
406    * 
407    * @param proxyStyle the proxy style
408    */
409   public void setProxyStyle(String proxyStyle) {
410     this.proxyStyle = proxyStyle;
411   }
412 
413   /**
414    * True to set proxy dimensions the same as the drag widget (defaults to
415    * true).
416    * 
417    * @param sizeProxyToSource true to update proxy size
418    */
419   public void setSizeProxyToSource(boolean sizeProxyToSource) {
420     this.sizeProxyToSource = sizeProxyToSource;
421   }
422 
423   /**
424    * Specifies how far the cursor must move after mousedown to start dragging
425    * (defaults to 2).
426    * 
427    * @param startDragDistance the start distance in pixels
428    */
429   public void setStartDragDistance(int startDragDistance) {
430     this.startDragDistance = startDragDistance;
431   }
432 
433   /**
434    * True if the CSS z-index should be updated on the widget being dragged.
435    * Setting this value to <code>true</code> will ensure that the dragged
436    * element is always displayed over all other widgets (defaults to true).
437    * 
438    * @param updateZIndex true update the z-index
439    */
440   public void setUpdateZIndex(boolean updateZIndex) {
441     this.updateZIndex = updateZIndex;
442   }
443 
444   /**
445    * True to use a proxy widget during drag operation (defaults to true).
446    * 
447    * @param useProxy true use a proxy
448    */
449   public void setUseProxy(boolean useProxy) {
450     this.useProxy = useProxy;
451   }
452 
453   /**
454    * Constrains the horizontal travel.
455    * 
456    * @param left the number of pixels the element can move to the left
457    * @param right the number of pixels the element can move to the right
458    */
459   public void setXConstraint(int left, int right) {
460     xLeft = left;
461     xRight = right;
462   }
463 
464   /**
465    * Constrains the vertical travel.
466    * 
467    * @param top the number of pixels the element can move to the up
468    * @param bottom the number of pixels the element can move to the down
469    */
470   public void setYConstraint(int top, int bottom) {
471     xTop = top;
472     xBottom = bottom;
473   }
474 
475   protected void afterDrag() {
476     XDOM.getBodyEl().removeStyleName("x-unselectable");
477     XDOM.getBodyEl().removeStyleName("x-dd-cursor");
478     Shim.get().uncover();
479   }
480 
481   protected El createProxy() {
482     proxyEl = new El(DOM.createDiv());
483     proxyEl.setVisibility(false);
484     proxyEl.dom.setClassName(getProxyStyle());
485     proxyEl.disableTextSelection(true);
486     return proxyEl;
487   }
488 
489   protected void onMouseDown(ComponentEvent ce) {
490     if (!enabled || ce.getEvent().getButton() != Event.BUTTON_LEFT) {
491       return;
492     }
493     Element target = ce.getTarget();
494     String s = DOM.getElementProperty(target, "className");
495     if (s != null && s.indexOf("x-nodrag") != -1) {
496       return;
497     }
498 
499     // still allow text selection, prevent drag of other elements
500     if ((!"input".equalsIgnoreCase(ce.getTarget().getTagName()) && !"textarea".equalsIgnoreCase(ce.getTarget().getTagName()))
501         || ce.getTarget().getPropertyBoolean("disabled")) {
502       ce.preventDefault();
503     }
504 
505     startBounds = dragWidget.el().getBounds();
506 
507     dragStartX = ce.getClientX();
508     dragStartY = ce.getClientY();
509 
510     preview.add();
511 
512     clientWidth = Window.getClientWidth() + XDOM.getBodyScrollLeft();
513     clientHeight = Window.getClientHeight() + XDOM.getBodyScrollTop();
514 
515     if (container != null) {
516       conX = container.getAbsoluteLeft();
517       conY = container.getAbsoluteTop();
518       conWidth = container.getOffsetWidth();
519       conHeight = container.getOffsetHeight();
520     }
521 
522     if (startDragDistance == 0) {
523       startDrag(ce.getEvent());
524     }
525 
526   }
527 
528   protected void onMouseMove(Event event) {
529     Element elem = event.getEventTarget().cast();
530     // elem.getClassName throwing GWT exception when dragged component is over
531     // SVG / VML
532     if (hasAttribute(elem, "class")) {
533    	  /* begin laaglu */
534 //      String cls = ((Element) event.getEventTarget().cast()).getClassName();
535         String cls = El.getClassName(((Element) event.getEventTarget().cast()));
536       /* end laaglu */
537 
538       if (cls != null && cls.contains("x-insert")) {
539         return;
540       }
541     }
542 
543     int x = DOM.eventGetClientX(event);
544     int y = DOM.eventGetClientY(event);
545 
546     if (!dragging && (Math.abs(dragStartX - x) > startDragDistance || Math.abs(dragStartY - y) > startDragDistance)) {
547       startDrag(event);
548     }
549 
550     if (dragging) {
551       int left = constrainHorizontal ? startBounds.x : startBounds.x + (x - dragStartX);
552       int top = constrainVertical ? startBounds.y : startBounds.y + (y - dragStartY);
553 
554       if (constrainClient) {
555         if (!constrainHorizontal) {
556           int width = startBounds.width;
557           left = Math.max(left, 0);
558           left = Math.max(0, Math.min(clientWidth - width, left));
559         }
560         if (!constrainVertical) {
561           top = Math.max(top, 0);
562           int height = startBounds.height;
563           if (Math.min(clientHeight - height, top) > 0) {
564             top = Math.max(2, Math.min(clientHeight - height, top));
565           }
566         }
567       }
568 
569       if (container != null) {
570         int width = startBounds.width;
571         int height = startBounds.height;
572         if (!constrainHorizontal) {
573           left = Math.max(left, conX);
574           left = Math.min(conX + conWidth - width, left);
575         }
576         if (!constrainVertical) {
577           top = Math.min(conY + conHeight - height, top);
578           top = Math.max(top, conY);
579         }
580       }
581       if (!constrainHorizontal) {
582         if (xLeft != Style.DEFAULT) {
583           left = Math.max(startBounds.x - xLeft, left);
584         }
585         if (xRight != Style.DEFAULT) {
586           left = Math.min(startBounds.x + xRight, left);
587         }
588       }
589 
590       if (!constrainVertical) {
591         if (xTop != Style.DEFAULT) {
592           top = Math.max(startBounds.y - xTop, top);
593         }
594         if (xBottom != Style.DEFAULT) {
595           top = Math.min(startBounds.y + xBottom, top);
596         }
597       }
598 
599       lastX = left;
600       lastY = top;
601 
602       dragEvent.setSource(this);
603       dragEvent.setComponent(dragWidget);
604       dragEvent.setEvent(event);
605       dragEvent.setCancelled(false);
606       dragEvent.setX(lastX);
607       dragEvent.setY(lastY);
608       fireEvent(Events.DragMove, dragEvent);
609 
610       if (dragEvent.isCancelled()) {
611         cancelDrag();
612         return;
613       }
614 
615       int tl = dragEvent.getX() != lastX ? dragEvent.getX() : lastX;
616       int tt = dragEvent.getY() != lastY ? dragEvent.getY() : lastY;
617       if (useProxy) {
618         proxyEl.setPagePosition(tl, tt);
619       } else {
620         dragWidget.el().setPagePosition(tl, tt);
621       }
622     }
623 
624   }
625 
626   protected void startDrag(Event event) {
627     DragEvent de = new DragEvent(this);
628     de.setComponent(dragWidget);
629     de.setEvent(event);
630     de.setX(startBounds.x);
631     de.setY(startBounds.y);
632 
633     if (fireEvent(Events.DragStart, de)) {
634       dragging = true;
635       XDOM.getBodyEl().addStyleName("x-unselectable");
636       XDOM.getBodyEl().addStyleName("x-dd-cursor");
637       dragWidget.el().makePositionable();
638 
639       event.preventDefault();
640 
641       Shim.get().cover(true);
642 
643       lastX = startBounds.x;
644       lastY = startBounds.y;
645 
646       if (dragEvent == null) {
647         dragEvent = new DragEvent(this);
648       }
649 
650       if (useProxy) {
651         if (proxyEl == null) {
652           createProxy();
653         }
654         if (container == null) {
655           XDOM.getBody().appendChild(proxyEl.dom);
656         } else {
657           container.el().appendChild(proxyEl.dom);
658         }
659         proxyEl.setVisibility(true);
660         proxyEl.setZIndex(XDOM.getTopZIndex());
661         proxyEl.makePositionable(true);
662 
663         if (sizeProxyToSource) {
664           proxyEl.setBounds(startBounds);
665         } else {
666           proxyEl.setXY(startBounds.x, startBounds.y);
667         }
668 
669         // did listeners change size?
670         if (de.getHeight() > 0 && de.getWidth() > 0) {
671           proxyEl.setSize(de.getWidth(), de.getHeight(), true);
672         } else if (de.getHeight() > 0) {
673           proxyEl.setHeight(de.getHeight(), true);
674         } else if (de.getWidth() > 0) {
675           proxyEl.setWidth(de.getWidth(), true);
676         }
677       } else if (updateZIndex) {
678         dragWidget.setZIndex(XDOM.getTopZIndex());
679       }
680     } else {
681       cancelDrag();
682     }
683   }
684 
685   protected void stopDrag(Event event) {
686     preview.remove();
687     if (dragging) {
688       dragging = false;
689       if (isUseProxy()) {
690         if (isMoveAfterProxyDrag()) {
691           Rectangle rect = proxyEl.getBounds();
692           dragWidget.el().setPagePosition(rect.x, rect.y);
693         }
694         proxyEl.setVisibility(false);
695         proxyEl.disableTextSelection(false);
696         proxyEl.remove();
697       }
698       DragEvent de = new DragEvent(this);
699       de.setComponent(dragWidget);
700       de.setEvent(event);
701       de.setX(lastX);
702       de.setY(lastY);
703       fireEvent(Events.DragEnd, de);
704       afterDrag();
705     }
706   }
707 
708   private native boolean hasAttribute(Element elem, String name) /*-{
709     return elem.hasAttribute ? elem.hasAttribute(name) : true;
710   }-*/;
711 
712 }