001/*
002 *  Copyright 2013 GWT-Bootstrap
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *      http://www.apache.org/licenses/LICENSE-2.0
008 *
009 *  Unless required by applicable law or agreed to in writing, software
010 *  distributed under the License is distributed on an "AS IS" BASIS,
011 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 *  See the License for the specific language governing permissions and
013 *  limitations under the License.
014 */
015package com.github.gwtbootstrap.client.ui;
016
017import com.github.gwtbootstrap.client.ui.base.DivWidget;
018import com.github.gwtbootstrap.client.ui.base.HasVisibility;
019import com.github.gwtbootstrap.client.ui.event.HasVisibleHandlers;
020import com.github.gwtbootstrap.client.ui.base.IsAnimated;
021import com.github.gwtbootstrap.client.ui.constants.BackdropType;
022import com.github.gwtbootstrap.client.ui.constants.Constants;
023import com.github.gwtbootstrap.client.ui.constants.DismissType;
024import com.github.gwtbootstrap.client.ui.event.*;
025import com.google.gwt.core.client.JavaScriptObject;
026import com.google.gwt.dom.client.Element;
027import com.google.gwt.dom.client.Style;
028import com.google.gwt.event.shared.HandlerRegistration;
029import com.google.gwt.user.client.DOM;
030import com.google.gwt.user.client.Event;
031import com.google.gwt.user.client.ui.PopupPanel;
032import com.google.gwt.user.client.ui.RootPanel;
033import com.google.gwt.user.client.ui.Widget;
034
035import java.util.HashSet;
036import java.util.Set;
037
038//@formatter:off
039
040/**
041 * Popup dialog with optional header and {@link ModalFooter footer.}
042 * <p>
043 * By default, all other Modals are closed once a new one is opened. This
044 * setting can be {@link #setHideOthers(boolean) overridden.}
045 * <p/>
046 * <p>
047 * <h3>UiBinder Usage:</h3>
048 * <p/>
049 * <pre>
050 * {@code
051 * <b:Modal title="My Modal" backdrop="STATIC">
052 *     <g:Label>Modal Content!</g:Label>
053 *     <b:ModalFooter>
054 *         <b:Button icon="FILE">Save</b:Button>
055 *     </b:ModalFooter>
056 * </b:Modal>
057 * }
058 * </pre>
059 * <p/>
060 * All arguments are optional.
061 * </p>
062 *
063 * @author Carlos Alexandro Becker
064 * @author Dominik Mayer
065 * @author Danilo Reinert
066 *
067 * @see <a
068 *      href="http://twitter.github.com/bootstrap/javascript.html#modals">Bootstrap
069 *      documentation</a>
070 * @see PopupPanel
071 *
072 * @since 2.0.4.0
073 */
074// @formatter:on
075public class Modal extends DivWidget implements HasVisibility, HasVisibleHandlers, IsAnimated {
076
077    private static Set<Modal> currentlyShown = new HashSet<Modal>();
078
079    private final DivWidget header = new DivWidget();
080
081    private final DivWidget body = new DivWidget("modal-body");
082
083    private boolean keyboard = true;
084
085    private BackdropType backdropType = BackdropType.NORMAL;
086
087    private boolean show = false;
088
089    private boolean hideOthers = true;
090
091    private boolean configured = false;
092
093    private Close close = new Close(DismissType.MODAL);
094
095    private String title;
096
097    /**
098     * Creates an empty, hidden widget.
099     */
100    public Modal() {
101        super("modal");
102        getElement().setAttribute("tabindex", "-1");
103        super.add(header);
104        super.add(body);
105        setVisible(false);
106    }
107
108    /**
109     * Creates an empty, hidden widget with specified show behavior.
110     *
111     * @param animated <code>true</code> if the widget should be animated.
112     */
113    public Modal(boolean animated) {
114        this(animated, false);
115    }
116
117    /**
118     * Creates an empty, hidden widget with specified show behavior.
119     *
120     * @param animated    <code>true</code> if the widget should be animated.
121     * @param dynamicSafe <code>true</code> removes from RootPanel when hidden
122     */
123    public Modal(boolean animated,
124                 boolean dynamicSafe) {
125        this();
126        setAnimation(animated);
127        setDynamicSafe(dynamicSafe);
128    }
129
130    /**
131     * Setup the modal to prevent memory leaks. When modal is hidden, will
132     * remove all event handlers, and them remove the modal DOM from document
133     * DOM.
134     * <p/>
135     * Default is false.
136     *
137     * @param dynamicSafe
138     */
139    public void setDynamicSafe(boolean dynamicSafe) {
140        if (dynamicSafe) {
141            addHiddenHandler(new HiddenHandler() {
142
143                @Override
144                public void onHidden(HiddenEvent hiddenEvent) {
145                    unsetHandlerFunctions(getElement());
146                    Modal.this.removeFromParent();
147                }
148            });
149        }
150    }
151
152    /**
153     * Sets the title of the Modal.
154     *
155     * @param title the title of the Modal
156     */
157    @Override
158    public void setTitle(String title) {
159        this.title = title;
160
161        header.clear();
162        if (title == null || title.isEmpty()) {
163            showHeader(false);
164        } else {
165
166            header.add(close);
167            header.add(new Heading(3, title));
168            showHeader(true);
169        }
170    }
171
172    /**
173     * Gets the title of the Modal.
174     *
175     * @return title
176     */
177    public String getTitle() {
178        return title;
179    }
180
181    private void showHeader(boolean show) {
182        if (show)
183            header.setStyleName(Constants.MODAL_HEADER);
184        else
185            header.removeStyleName(Constants.MODAL_HEADER);
186    }
187
188    /**
189     * {@inheritDoc}
190     */
191    public void setAnimation(boolean animated) {
192        if (animated)
193            addStyleName(Constants.FADE);
194        else
195            removeStyleName(Constants.FADE);
196    }
197
198    /**
199     * {@inheritDoc}
200     */
201    public boolean getAnimation() {
202        return getStyleName().contains(Constants.FADE);
203    }
204
205    /**
206     * Sets whether this Modal appears on top of others or is the only one
207     * visible on screen.
208     *
209     * @param hideOthers <code>true</code> to make sure that this modal is the only one
210     *                   shown. All others will be hidden. Default: <code>true</code>
211     */
212    public void setHideOthers(boolean hideOthers) {
213        this.hideOthers = hideOthers;
214    }
215
216    /**
217     * Sets whether the Modal is closed when the <code>ESC</code> is pressed.
218     *
219     * @param keyboard <code>true</code> if the Modal is closed by <code>ESC</code>
220     *                 key. Default: <code>true</code>
221     */
222    public void setKeyboard(boolean keyboard) {
223        this.keyboard = keyboard;
224        reconfigure();
225    }
226
227    /**
228     * Get Keyboard enable state
229     *
230     * @return true:enable false:disable
231     */
232    public boolean isKeyboardEnable() {
233        return this.keyboard;
234    }
235
236    /**
237     * Sets the type of the backdrop.
238     *
239     * @param type the backdrop type
240     */
241    public void setBackdrop(BackdropType type) {
242        backdropType = type;
243        reconfigure();
244
245    }
246
247    /**
248     * Get backdrop type.
249     *
250     * @return backdrop type.
251     */
252    public BackdropType getBackdropType() {
253        return this.backdropType;
254    }
255
256    /**
257     * Reconfigures the modal with changed settings.
258     */
259    protected void reconfigure() {
260        if (configured) {
261            reconfigure(keyboard, backdropType, show);
262        }
263    }
264
265    /**
266     * {@inheritDoc}
267     */
268    @Override
269    public void add(Widget w) {
270        if (w instanceof ModalFooter) {
271            super.add(w);
272        } else
273            body.add(w);
274    }
275
276    /**
277     * {@inheritDoc}
278     */
279    @Override
280    public void insert(Widget w, int beforeIndex) {
281        body.insert(w, beforeIndex);
282    }
283
284    /**
285     * {@inheritDoc}
286     */
287    public void show() {
288        checkAttachedOnShow();
289        changeVisibility("show");
290        //      centerVertically(getElement());  Note: Doesn't work with Bootstrap 2.3.2
291    }
292
293    /**
294     * {@inheritDoc}
295     */
296    public void show(boolean autoShow) {
297        checkAttachedOnShow();
298        changeVisibility("show", autoShow);
299        //      centerVertically(getElement());  Note: Doesn't work with Bootstrap 2.3.2
300    }
301
302    private void checkAttachedOnShow() {
303        if (!this.isAttached()) {
304            RootPanel.get().add(this);
305        }
306    }
307
308    @Override
309    protected void onAttach() {
310        super.onAttach();
311        configure(keyboard, backdropType, show);
312        setHandlerFunctions(getElement());
313        configured = true;
314    }
315
316    /**
317     * {@inheritDoc}
318     */
319    public void hide() {
320        changeVisibility("hide");
321    }
322
323    /**
324     * {@inheritDoc}
325     */
326    public void hide(boolean autoHidden) {
327        changeVisibility("hide", autoHidden);
328    }
329
330    /**
331     * {@inheritDoc}
332     */
333    public void toggle() {
334        changeVisibility("toggle");
335    }
336
337    /**
338     * {@inheritDoc}
339     */
340    public void toggle(boolean autoToggle) {
341        changeVisibility("toggle", autoToggle);
342    }
343
344    private void changeVisibility(String visibility) {
345        changeVisibility(getElement(), visibility);
346    }
347
348    private void changeVisibility(String visibility, boolean autoTriggered) {
349        changeVisibility(getElement(), visibility, autoTriggered);
350    }
351
352    /**
353     * This method is called immediately when the widget's {@link #hide()}
354     * method is executed.
355     */
356    protected void onHide(Event e) {
357        fireEvent(new HideEvent(e, getAutoTriggered(e)));
358    }
359
360    /**
361     * This method is called once the widget is completely hidden.
362     */
363    protected void onHidden(Event e) {
364        fireEvent(new HiddenEvent(e, getAutoTriggered(e)));
365        currentlyShown.remove(this);
366    }
367
368    /**
369     * This method is called immediately when the widget's {@link #show()}
370     * method is executed.
371     */
372    protected void onShow(Event e) {
373        if (hideOthers)
374            hideShownModals();
375        fireEvent(new ShowEvent(e, getAutoTriggered(e)));
376    }
377
378    private void hideShownModals() {
379        for (Modal m : currentlyShown) {
380            if (!m.equals(this)) {
381                m.hide(true);
382            }
383        }
384    }
385
386    /**
387     * This method is called once the widget is completely shown.
388     */
389    protected void onShown(Event e) {
390        fireEvent(new ShownEvent(e, getAutoTriggered(e)));
391        currentlyShown.add(this);
392    }
393
394    private void reconfigure(boolean keyboard, BackdropType backdropType, boolean show) {
395        if (backdropType == BackdropType.NORMAL) {
396            reconfigure(getElement(), keyboard, true, show);
397        } else if (backdropType == BackdropType.NONE) {
398            reconfigure(getElement(), keyboard, false, show);
399        } else if (backdropType == BackdropType.STATIC) {
400            reconfigure(getElement(), keyboard, BackdropType.STATIC.get(), show);
401        }
402    }
403
404    private void configure(boolean keyboard, BackdropType backdropType, boolean show) {
405        if (backdropType == BackdropType.NORMAL) {
406            configure(getElement(), keyboard, true, show);
407        } else if (backdropType == BackdropType.NONE) {
408            configure(getElement(), keyboard, false, show);
409        } else if (backdropType == BackdropType.STATIC) {
410            configure(getElement(), keyboard, BackdropType.STATIC.get(), show);
411        }
412    }
413
414    //@formatter:off
415
416    private native void reconfigure(Element e, boolean k, boolean b, boolean s) /*-{
417        // Init vars
418        var $e = $wnd.jQuery(e);
419        var modal = $e.data('modal');
420        var wasShown = null;
421
422        // If element is modal, then unset it
423        if (modal) {
424            $e.removeData('modal');
425            wasShown = modal.isShown;
426        }
427
428        // Apply modal again to the element
429        $e.modal({
430            keyboard: k,
431            backdrop: b,
432            show: s
433        });
434
435        // If previous modal was shown, then reset it to current modal
436        if (wasShown) {
437            $e.data('modal').isShown = wasShown;
438        }
439    }-*/;
440
441    private native void reconfigure(Element e, boolean k, String b, boolean s) /*-{
442        // Init vars
443        var $e = $wnd.jQuery(e);
444        var modal = $e.data('modal');
445        var wasShown = null;
446
447        // If element is modal, then unset it
448        if (modal) {
449            $e.removeData('modal');
450            wasShown = modal.isShown;
451        }
452
453        // Apply modal again to the element
454        $e.modal({
455            keyboard: k,
456            backdrop: b,
457            show: s
458        });
459
460        // If previous modal was shown, then reset it to current modal
461        if (wasShown) {
462            $e.data('modal').isShown = wasShown;
463        }
464    }-*/;
465
466
467    private native void configure(Element e, boolean k, boolean b, boolean s) /*-{
468        $wnd.jQuery(e).modal({
469            keyboard: k,
470            backdrop: b,
471            show: s
472        });
473    }-*/;
474
475    private native void configure(Element e, boolean k, String b, boolean s) /*-{
476        $wnd.jQuery(e).modal({
477            keyboard: k,
478            backdrop: b,
479            show: s
480        });
481    }-*/;
482
483    private native void changeVisibility(Element e, String visibility) /*-{
484        $wnd.jQuery(e).modal(visibility);
485    }-*/;
486
487    private native void changeVisibility(Element e, String visibility, boolean autoTriggered) /*-{
488        var $e = $wnd.jQuery(e);
489
490        var modal = $e.data('modal');
491        if (modal)
492            modal.autoTriggered = autoTriggered;
493
494        $e.modal(visibility);
495    }-*/;
496
497    private native boolean getAutoTriggered(JavaScriptObject jso) /*-{
498        // Prevent null result
499        if (jso.autoTriggered) return true;
500        return false;
501    }-*/;
502
503    /**
504     * Links the Java functions that fire the events.
505     */
506    private native void setHandlerFunctions(Element element) /*-{
507        var that = this;
508        var $el = $wnd.jQuery(element);
509        var autoTriggeredCheck = function (event, removeProperty) {
510            var modal = $wnd.jQuery(event.target).data('modal');
511            if (modal && modal.autoTriggered) {
512                event.autoTriggered = true;
513                if (removeProperty)
514                    modal.autoTriggered = false;
515            }
516        };
517
518        $el.on('hide', function (e) {
519            if (e.target === this) {
520                autoTriggeredCheck(e);
521                that.@com.github.gwtbootstrap.client.ui.Modal::onHide(Lcom/google/gwt/user/client/Event;)(e);
522                e.stopPropagation();
523            }
524        });
525        $el.on('hidden', function (e) {
526            if (e.target === this) {
527                autoTriggeredCheck(e, true);
528                that.@com.github.gwtbootstrap.client.ui.Modal::onHidden(Lcom/google/gwt/user/client/Event;)(e);
529                e.stopPropagation();
530            }
531        });
532        $el.on('show', function (e) {
533            if (e.target === this) {
534                autoTriggeredCheck(e);
535                that.@com.github.gwtbootstrap.client.ui.Modal::onShow(Lcom/google/gwt/user/client/Event;)(e);
536                e.stopPropagation();
537            }
538        });
539        $el.on('shown', function (e) {
540            if (e.target === this) {
541                autoTriggeredCheck(e, true);
542                that.@com.github.gwtbootstrap.client.ui.Modal::onShown(Lcom/google/gwt/user/client/Event;)(e);
543                e.stopPropagation();
544            }
545        });
546    }-*/;
547
548    /**
549     * Unlinks all the Java functions that fire the events.
550     */
551    private native void unsetHandlerFunctions(Element e) /*-{
552        var $e = $wnd.jQuery(e);
553        $e.off('hide');
554        $e.off('hidden');
555        $e.off('show');
556        $e.off('shown');
557    }-*/;
558    //@formatter:on
559
560    /**
561     * {@inheritDoc}
562     */
563    public HandlerRegistration addHideHandler(HideHandler handler) {
564        return addHandler(handler, HideEvent.getType());
565    }
566
567    /**
568     * {@inheritDoc}
569     */
570    public HandlerRegistration addHiddenHandler(HiddenHandler handler) {
571        return addHandler(handler, HiddenEvent.getType());
572    }
573
574    /**
575     * {@inheritDoc}
576     */
577    public HandlerRegistration addShowHandler(ShowHandler handler) {
578        return addHandler(handler, ShowEvent.getType());
579    }
580
581    /**
582     * {@inheritDoc}
583     */
584    public HandlerRegistration addShownHandler(ShownHandler handler) {
585        return addHandler(handler, ShownEvent.getType());
586    }
587
588    /**
589     * Show/Hide close button. The Modal must have a title.
590     *
591     * @param visible <b>true</b> for show and <b>false</b> to hide. Defaults is
592     *                <b>true</b>.
593     */
594    public void setCloseVisible(boolean visible) {
595        close.getElement().getStyle().setVisibility(visible
596                ? Style.Visibility.VISIBLE
597                : Style.Visibility.HIDDEN);
598    }
599
600    /**
601     * @deprecated modal do not support setSize method
602     */
603    @Override
604    public void setSize(String width, String height) {
605        throw new UnsupportedOperationException("modal do not support setSize method");
606    }
607
608    /**
609     * Sets the Modal's width.
610     *
611     * @param width Modal's new width, in px
612     */
613    public void setWidth(int width) {
614        DOM.setStyleAttribute(this.getElement(), "width", width + "px");
615        DOM.setStyleAttribute(this.getElement(), "marginLeft", (-width / 2) + "px");
616    }
617
618    /**
619     * Sets the Modal's body maxHeight.
620     *
621     * @param maxHeight the Modal's body new maxHeight, in CSS units (e.g. "10px", "1em")
622     */
623    public void setMaxHeigth(String maxHeight) {
624        DOM.setStyleAttribute(body.getElement(), "maxHeight", maxHeight);
625    }
626
627}