001 
002 /*
003  *  JScripter Standard 1.0 - To Script In Java
004  *  Copyright (C) 2008-2011  J.J.Liu<jianjunliu@126.com> <http://www.jscripter.org>
005  *  
006  *  This program is free software: you can redistribute it and/or modify
007  *  it under the terms of the GNU Affero General Public License as published by
008  *  the Free Software Foundation, either version 3 of the License, or
009  *  (at your option) any later version.
010  *  
011  *  This program is distributed in the hope that it will be useful,
012  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014  *  GNU Affero General Public License for more details.
015  *  
016  *  You should have received a copy of the GNU Affero General Public License
017  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
018  */
019 
020 package jsx.ui.dd;
021 
022 import js.Id;
023 import js.Initializer;
024 import js.Js;
025 import js.ObjectLike;
026 import jsx.Configurable;
027 import jsx.client.Browser;
028 import jsx.core.ObjectLikes;
029 import jsx.dom.Elements;
030 import jsx.dom.Styles;
031 import jsx.ui.Component;
032 import jsx.ui.Widget;
033 import jsx.ui.dd.event.DragMove;
034 import jsx.ui.dd.event.DragStart;
035 import jsx.ui.dd.event.DragStop;
036 import jsx.ui.dd.event.OnDragMove;
037 import jsx.ui.dd.event.OnDragStart;
038 import jsx.ui.dd.event.OnDragStop;
039 import jsx.ui.event.Popup;
040 import jsx.ui.event.Position;
041 import jsx.ui.fx.event.Animation;
042 
043 /**
044  * <p>A base class for wrapper widgets that can be dragged by mouse handles.</p>
045  * <p>A {@link Draggable} widget is a {@link Widget} that makes its underlying HTML 
046  * element movable or {@link Resizable} in accordance with a {@link Mouse} widget 
047  * to which it listens mouse events.</p>
048  * <p>A {@link Draggable} widget is {@link Configurable} and is also an event source 
049  * which fires {@link jsx.ui.Widget.Event} events.</p>
050  * 
051  * @author <a href="mailto:jianjunliu@126.com">J.J.Liu (Jianjun Liu)</a> at <a href="http://www.jscripter.org" target="_blank">http://www.jscripter.org</a>
052  */
053 public class Draggable extends Widget implements OnDragStart, OnDragMove, OnDragStop
054 {
055     /**
056      * <p>A global identifier for a configurable property of a {@link Draggable} object.</p>
057      * <p>The identified configurable property of a {@link Draggable} object refers to 
058      * a boolean value specifying whether to create a ghost to show dragging of the 
059      * widget.</p>
060      * @since 1.0
061      */
062     public final static Id<Boolean> DELEGABLE = new Id<Boolean>();
063 
064     /**
065      * <p>This constant is the default value for the {@link #LIMIT} configurable property 
066      * of a {@link Draggable} object meaning dragging is free in X and Y dimensions. 
067      * The constant is also used as an argument in calling the method {@link #limit(int)}.</p>
068      * @since 1.0
069      */
070     public final static int NOLIMIT = 0;
071     /**
072      * <p>This constant is a legal value for the {@link #LIMIT} configurable property 
073      * of a {@link Draggable} object meaning only the Y dimension is free for dragging. 
074      * The constant is also used as an argument in calling the method {@link #limit(int)}.</p>
075      * @since 1.0
076      */
077     public final static int LIMIT_X = 1;
078     /**
079      * <p>This constant is a legal value for the {@link #LIMIT} configurable property 
080      * of a {@link Draggable} object meaning only the X dimension is free for dragging. 
081      * The constant is also used as an argument in calling the method {@link #limit(int)}.</p>
082      * @since 1.0
083      */
084     public final static int LIMIT_Y = 2;
085 
086     /**
087      * <p>A global identifier for a configurable property of a {@link Draggable} object.</p>
088      * <p>The identified configurable property of a {@link Draggable} object refers to 
089      * an integer value specifying whether and how to limit dragging. Possible values 
090      * are:
091      * <ul>
092      * <li>{@link #NOLIMIT}: No limitations. Dragging is free in both dimensions.</li>
093      * <li>{@link #LIMIT_X}: Limited in X dimension but free in Y dimension.</li>
094      * <li>{@link #LIMIT_Y}: Limited in Y dimension but free in X dimension.</li>
095      * </ul>
096      * </p>
097      * @since 1.0
098      */
099     protected final static Id<Integer> LIMIT = new Id<Integer>();
100     /**
101      * <p>A global identifier for a configurable property of a {@link Draggable} object.</p>
102      * <p>The identified configurable property of a {@link Draggable} widget refers to 
103      * the mouse handle of the draggable widget.</p>
104      * @since 1.0
105      */
106     protected final static Id<Mouse> MOUSE = new Id<Mouse>();
107 
108     /**
109      * <p>A typical constructor that constructs a wrapper widget of this type and forces 
110      * constructors of subclasses to pass initializing data.</p>
111      * <p>This constructor invokes the typical constructor of the superclass passing 
112      * the specified initializing object as the argument and attaches the draggable 
113      * widget being created to the configured mouse widget or a new mouse handle 
114      * obtained from the wrapped component if such a mouse handle is not specified in 
115      * the passed initializing object.</p>
116      * @param ini The initializing object.
117      * @since 1.0
118      */
119     protected Draggable(ObjectLike ini) {
120         super(ini);
121         addClasses();
122         Mouse h = getMouse();
123         if (Js.be(h)) {
124             attach(h);
125         }
126     }
127 
128     /**
129      * <p>Constructs a draggable widget that wraps a specified component and makes it 
130      * draggable in accordance with the mouse handle specified as an argument to which 
131      * the newly constructed widget will listen mouse events.</p>
132      * <p>This constructor invokes the typical constructor of this class passing 
133      * an new initializing object as the argument and setting the configurable property 
134      * {@link Widget#COMPONENT} to the argument component, {@link #MOUSE} to the 
135      * argument mouse widget.</p>
136      * @param e The component to be wrapped by the wrapper widget being created.
137      * @param h The mouse widget to be attached to the draggable widget being created.
138      * @since 1.0
139      */
140     public Draggable(Component e, Mouse h) {
141         this(new Initializer().set(COMPONENT, e).set(MOUSE, h).var());
142     }
143 
144     /**
145      * <p>Constructs a draggable widget that wraps and drags a specified component.</p>
146      * <p>This constructor simply invokes the constructor {@link #Draggable(Component, Mouse)} 
147      * passing the specified component as the first argument and the mouse widget 
148      * obtained from the same component as the second argument.</p>
149      * @param e The component to be wrapped and dragged by the wrapper widget being 
150      * created.
151      * @since 1.0
152      */
153     public Draggable(Component e) {
154         this(e, Mouse.getMouse(e));
155     }
156 
157     /**
158      * <p>Sets a limitation for dragging the draggable widget.</p>
159      * <p>This method simply sets the configurable property {@link #LIMIT} 
160      * of the current draggable widget to the argument value.</p>
161      * @param limit An integer value indicating whether and how to limit dragging. 
162      * See {@link #LIMIT} for possible values. 
163      * @since 1.0
164      * @see #LIMIT
165      * @see #NOLIMIT
166      * @see #LIMIT_X
167      * @see #LIMIT_Y
168      */
169     public final void limit(int limit) {
170         ini(this).var(LIMIT, limit & (LIMIT_X & LIMIT_Y));
171     }
172 
173     /**
174      * <p>Gets the mouse handle currently attached to the draggable widget.</p>
175      * <p>This method simply returns the configurable property {@link #MOUSE} 
176      * of the current draggable widget.</p>
177      * @return The mouse widget attached to the draggable.
178      * @since 1.0
179      */
180     public final Mouse getMouse() {
181         return ini(this).var(MOUSE);
182     }
183 
184     /**
185      * <p>Attaches a specified mouse handle to the draggable widget.</p>
186      * <p>This method detaches the old mouse handle if there is one and attaches the 
187      * specified one to the current draggable widget enabling it to drag and the 
188      * draggable widget to listen mouse events from the mouse handle with a call to 
189      * the method {@link #listen(Mouse)}. Call this method from event listeners with 
190      * caution.</p>
191      * @param h The new mouse widget to be attached to the draggable.
192      * @since 1.0
193      * @see #detach()
194      */
195     public final void attach(Mouse h) {
196         if (Js.be(getMouse())) {
197             detach();
198         }
199         ini(this).var(MOUSE, h);
200         listen(h);
201     }
202 
203     /**
204      * <p>Detaches the current mouse handle from the draggable widget.</p>
205      * <p>This method removes the mouse handle currently attached to the draggable 
206      * widget and unregisters it as an event listener from the mouse widget with all 
207      * event types. Call the method from event listeners with caution.</p>
208      * @since 1.0
209      * @see #attach(Mouse)
210      */
211     public final void detach() {
212         Mouse h = getMouse();
213         if (Js.be(h)) {
214             unlisten(h);
215             ObjectLikes.delete(ini(this), MOUSE);
216         }
217     }
218 
219     /**
220      * <p>Registers this draggable widget as an event listener to a specified mouse 
221      * handle with the necessary event types.</p>
222      * <p>A subclass may either override and invoke this method to register with more event 
223      * types or override but not invoke this method to register with a new set of event 
224      * types. Call the method from event listeners with caution.</p>
225      * @param h A mouse widget.
226      * @since 1.0
227      * @see #unlisten(Mouse)
228      */
229     protected void listen(Mouse h) {
230         h.addListener(DragStart.class, this);
231         h.addListener(DragMove .class, this);
232         h.addListener(DragStop .class, this);
233     }
234 
235     /**
236      * <p>unregisters this draggable widget as an event listener from a specified mouse 
237      * handle with the necessary event types.</p>
238      * <p>A subclass may either override and invoke this method or override but not 
239      * invoke this method to unregister with all the registered event types. Call this 
240      * method from event listeners with caution.</p>
241      * @param h A mouse widget.
242      * @since 1.0
243      * @see #listen(Mouse)
244      */
245     protected void unlisten(Mouse h) {
246         h.removeListener(DragStart.class, this);
247         h.removeListener(DragMove .class, this);
248         h.removeListener(DragStop .class, this);
249     }
250 
251     /**
252      * <p>A global identifier for a configurable property of a {@link Draggable} object.</p>
253      * <p>The identified configurable property of a {@link Draggable} object refers to 
254      * a numerical value caching the X coordinate of the mouse position.</p>
255      * @since 1.0
256      */
257     protected final static Id<Double> X = new Id<Double>();
258     /**
259      * <p>A global identifier for a configurable property of a {@link Draggable} object.</p>
260      * <p>The identified configurable property of a {@link Draggable} object refers to 
261      * a numerical value caching the Y coordinate of the mouse position.</p>
262      * @since 1.0
263      */
264     protected final static Id<Double> Y = new Id<Double>();
265     /**
266      * <p>A global identifier for a configurable property of a {@link Draggable} object.</p>
267      * <p>The identified configurable property of a {@link Draggable} object refers to 
268      * a boolean value specifying whether the mouse has started to drag.</p>
269      * @since 1.0
270      */
271     protected final static Id<Boolean> START = new Id<Boolean>();
272     /**
273      * <p>A global identifier for a configurable property of a {@link Draggable} object.</p>
274      * <p>The identified configurable property of a {@link Draggable} object refers to 
275      * a cached ghost component that is delegated to show dragging process.</p>
276      * @since 1.0
277      */
278     protected final static Id<Component> GHOST = new Id<Component>();
279 
280     /**
281      * <p>Performs an action on the dispatched event.</p>
282      * <p>This method calls the method {@link #press(Draggable, DragStart)} to prepare 
283      * for dragging and then enters the dragging mode if it is not started.</p>
284      * @param evt The event dispatched to this listener.
285      * @since 1.0
286      */
287     public void onEvent(DragStart evt) {
288         if (Js.not(ini(this).var(START))) {
289             press(this, evt);
290             ini(this).var(START, true);
291         }
292     }
293 
294     /**
295      * <p>Determines whether a specified draggable widget needs to delegate the dragging 
296      * process to a ghost component.</p>
297      * @param d A draggable widget.
298      * @return <tt>true</tt> if the configurable property {@link #DELEGABLE} of the 
299      * specified draggable widget is <tt>true</tt> or the underlying component wrapped by the 
300      * specified draggable widget is fixed. Otherwise, the method returns <tt>false</tt>.
301      * @since 1.0
302      */
303     protected static final boolean delegable(Draggable d) {
304         return Js.be(ini(d).var(DELEGABLE)) || Component.fixed(d.unwrap());
305     }
306 
307     private static final void press(Component e, DragStart evt) {
308         ObjectLike ini = ini(e);
309         ini.var(DragStart.X, ini(evt).var(DragStart.X));
310         ini.var(DragStart.Y, ini(evt).var(DragStart.Y));
311         ini.var(X, Component.left(e));
312         ini.var(Y, Component.top (e));
313     }
314 
315     /**
316      * <p>Prepares a draggable widget for dragging with a specified mouse press event.</p>
317      * <p>This method caches the necessary position data of both mouse and underlying 
318      * component and creates a ghost component if it is delegable according to a call to 
319      * the method {@link #delegable(Draggable)}. A subclass may also call this method to 
320      * meet its particular needs.</p>
321      * @param d A draggable widget that is being dragged.
322      * @param evt The mouse press event that starts the dragging mode.
323      * @since 1.0
324      */
325     protected static final void press(Draggable d, DragStart evt) {
326         d.exec(new Popup());
327         Component e = d.unwrap();
328         press(e, evt);
329         if (delegable(d)) {
330             Component g = ghost(e);
331             ini(d).var(GHOST, g);
332             press(g, evt);
333         }
334     }
335 
336     /**
337      * <p>Performs an action on the dispatched event.</p>
338      * <p>This method moves the draggable widget or its ghost component with a call to 
339      * the method {@link #move(Component, Position, Integer)} accordingly.</p>
340      * @param evt The event dispatched to this listener.
341      * @since 1.0
342      */
343     public void onEvent(DragMove evt) {
344         ObjectLike ini = ini(this);
345         if (Js.be(ini.var(START))) {
346             if (!delegable(this)) {
347                 move(unwrap(), evt, ini.var(LIMIT));
348             } else {
349                 move(ini.var(GHOST), evt, ini.var(LIMIT));
350             }
351         }
352     }
353 
354     /**
355      * <p>Performs an action on the dispatched event.</p>
356      * <p>This method calls the method {@link #release(Draggable, Position)} to end 
357      * dragging if it is in a dragging mode.</p>
358      * @param evt The event dispatched to this listener.
359      * @since 1.0
360      */
361     public void onEvent(DragStop evt) {
362         if (Js.be(ini(this).var(START))) {
363             release(this, evt);
364         }
365     }
366 
367     /**
368      * <p>Releases a draggable widget from dragging mode with a specified mouse position event.</p>
369      * <p>This method moves the underlying component of the argument draggable widget 
370      * with a call to the method {@link #move(Component, Position, Integer)} and deletes 
371      * the ghost component if necessary. A subclass may also call this method to meet its 
372      * particular needs.</p>
373      * @param d A draggable widget that is to exit dragging mode.
374      * @param evt A mouse position event ending the dragging and providing the position 
375      * of mouse at that time.
376      * @since 1.0
377      */
378     protected static final void release(Draggable d, Position<?> evt) {
379         ObjectLike ini = ini(d);
380         ObjectLikes.delete(ini, START);
381         if (!Component.fixed(d.unwrap())) {
382             move(d.unwrap(), evt, ini.var(LIMIT));
383         }
384         if (delegable(d)) {
385             Component.detach(ini.var(GHOST));
386         }
387     }
388 
389     /**
390      * <p>Moves a specified component with a specified mouse position and a required limitation.</p>
391      * <p>This method moves the specified component by applying a style object created 
392      * with the cached position data and the return result of a call to the method 
393      * {@link #move(double, double, Integer)}. It fires an {@link Animation} event, with 
394      * the style object that has just been created, from the specified component. 
395      * A subclass may also call this method to meet its particular needs.</p>
396      * @param e A component need to move.
397      * @param evt A mouse position providing the end position.
398      * @param limit The limitation of the motion. See {@link #LIMIT} for possible values.
399      * @since 1.0
400      */
401     protected static final void move(Component e, Position<?> evt, Integer limit) {
402         ObjectLike ini = ini(e);
403         ObjectLike p = move(
404                 ini.var(X) + abs(ini(evt).var(Position.X)) - ini.var(Position.X).doubleValue(),
405                 ini.var(Y) + abs(ini(evt).var(Position.Y)) - ini.var(Position.Y).doubleValue(),
406                 limit
407         );
408         if (Js.not(e.exec(new Animation(p)))) {
409             Js.apply(Elements.style(Component.getHTMLElement(e)), p);
410         }
411     }
412 
413     /**
414      * <p>Creates and returns a style object of position data with the specified 
415      * coordinates and limitation.</p>
416      * <p>A subclass may also call this method to meet its particular needs.</p>
417      * @param x The X coordinate.
418      * @param y The Y coordinate.
419      * @param limit Limits a coordinate in an axis direction. See {@link #LIMIT} for 
420      * possible values.
421      * @return The created style object.
422      * @since 1.0
423      */
424     protected static final ObjectLike move(double x, double y, Integer limit) {
425         ObjectLike p = new Initializer().var();
426         if (Js.not(limit) || Js.not(limit & LIMIT_X)) {
427             Styles.left(p, Styles.px(x));
428         }
429         if (Js.not(limit) || Js.not(limit & LIMIT_Y)) {
430             Styles.top (p, Styles.px(y));
431         }
432         return p;
433     }
434 
435     /**
436      * <p>Returns the absolute value of an integer.</p>
437      * <p>A subclass may also call this method to meet its particular needs.</p>
438      * @param x A number value.
439      * @return The absolute value of the integer. It is never less than 0.
440      * @since 1.0
441      */
442     protected static final double abs(Number x) {
443         return x.doubleValue() < 0 ? 0 : x.doubleValue();
444     }
445 
446     private static final Component ghost(Component e) {
447         Component g = Component.div();
448         Component.addClass(g, css(Resizable.class, "ghost"));
449         Component.appendChild(e.getParent(), g);
450         ObjectLike p = new Initializer().var();
451         Styles.left(p, Styles.px(
452                 Component.left(g, Component.offsetLeft(e)) - Component.contentLeft(g)
453         ));
454         Styles.top(p, Styles.px(
455                 Component.top (g, Component.offsetTop (e)) - Component.contentTop (g)
456         ));
457         double w = Component.offsetWidth (e);
458         double h = Component.offsetHeight(e);
459         if (Browser.isIE) {
460             w += Component.contentLeft(g) + Component.contentRight (g);
461             h += Component.contentTop (g) + Component.contentBottom(g);
462         }
463         Styles.width (p, Styles.px(w));
464         Styles.height(p, Styles.px(h));
465         Component.applyStyle(g, p);
466         return g;
467     }
468 }