View Javadoc

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