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