001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.extensions.ajax.markup.html.modal;
018
019import org.apache.wicket.Component;
020import org.apache.wicket.WicketRuntimeException;
021import org.apache.wicket.ajax.AjaxEventBehavior;
022import org.apache.wicket.ajax.AjaxRequestTarget;
023import org.apache.wicket.ajax.attributes.AjaxCallListener;
024import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
025import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.EventPropagation;
026import org.apache.wicket.extensions.ajax.markup.html.modal.theme.DefaultTheme;
027import org.apache.wicket.markup.html.WebMarkupContainer;
028import org.apache.wicket.markup.html.panel.Panel;
029
030/**
031 * Presents a modal dialog to the user. See {@link #open(Component, AjaxRequestTarget)} and
032 * {@link #close(AjaxRequestTarget)} methods.
033 * <p>
034 * Unlike the deprecated {@link ModalWindow} this component offers no UI controls, you should use
035 * any components as you need in the content of this dialog.
036 * <p>
037 * Note: This component does not provide any styling by itself, so you have can add a
038 * {@link DefaultTheme} to this component if aren't styling these CSS classes by yourself:
039 * <dl>
040 * <dt>modal-dialog-overlay</dt>
041 * <dd>the wrapper around the actual dialog, usually used to overlay the rest of the document</dd>
042 * <dt>modal-dialog</dt>
043 * <dd>the actual dialog</dd>
044 * <dt>modal-dialog-content</dt>
045 * <dd>any additional styling for the content of this dialog</dd>
046 * </dl>
047 * 
048 * @author Igor Vaynberg (ivaynberg)
049 * @author svenmeier
050 */
051public class ModalDialog extends Panel
052{
053
054        private static final long serialVersionUID = 1L;
055
056        private static final String OVERLAY_ID = "overlay";
057
058        private static final String DIALOG_ID = "dialog";
059
060        /**
061         * The id for the content of this dialoh.
062         * 
063         * @see #setContent(Component)
064         * @see #open(Component, AjaxRequestTarget)
065         */
066        public static final String CONTENT_ID = "content";
067
068        private final WebMarkupContainer overlay;
069
070        private final WebMarkupContainer dialog;
071
072        private boolean removeContentOnClose;
073
074        public ModalDialog(String id)
075        {
076                super(id);
077
078                setOutputMarkupId(true);
079
080                overlay = newOverlay(OVERLAY_ID);
081                overlay.setVisible(false);
082                add(overlay);
083
084                dialog = newDialog(DIALOG_ID);
085                overlay.add(dialog);
086        }
087
088        /**
089         * Factory method for the overlay markup around the dialog.
090         * 
091         * @param overlayId
092         *            id
093         * @return overlay
094         */
095        protected WebMarkupContainer newOverlay(String overlayId)
096        {
097                return new WebMarkupContainer(overlayId);
098        }
099
100        /**
101         * Factory method for the dialog markup around the content.
102         * 
103         * @param dialogId
104         *            id
105         * @return overlay
106         */
107        protected WebMarkupContainer newDialog(String dialogId)
108        {
109                return new WebMarkupContainer(dialogId);
110        }
111
112        /**
113         * Set a content.
114         * 
115         * @param content
116         * 
117         * @see #open(AjaxRequestTarget)
118         */
119        public void setContent(Component content)
120        {
121                if (!CONTENT_ID.equals(content.getId()))
122                {
123                        throw new IllegalArgumentException(
124                                "Content must have wicket id set to ModalDialog.CONTENT_ID");
125                }
126
127                dialog.addOrReplace(content);
128
129                removeContentOnClose = false;
130        }
131
132        /**
133         * Open the dialog with a content.
134         * <p>
135         * The content will be removed on close of the dialog.
136         * 
137         * @param content
138         *            the content
139         * @param target
140         *            an optional Ajax target
141         * @return this
142         * 
143         * @see #setContent(Component)
144         * @see #close(AjaxRequestTarget)
145         */
146        public ModalDialog open(Component content, AjaxRequestTarget target)
147        {
148                setContent(content);
149                removeContentOnClose = true;
150
151                overlay.setVisible(true);
152
153                if (target != null)
154                {
155                        target.add(this);
156                }
157
158                return this;
159        }
160
161        /**
162         * Open the dialog.
163         * 
164         * @param target
165         *            an optional Ajax target
166         * @return this
167         * 
168         * @see #setContent(Component)
169         */
170        public ModalDialog open(AjaxRequestTarget target)
171        {
172                if (overlay.size() == 0)
173                {
174                        throw new WicketRuntimeException(String.format("ModalDialog with id '%s' has no content set!", getId()));
175                }
176
177                overlay.setVisible(true);
178
179                if (target != null)
180                {
181                        target.add(this);
182                }
183
184                return this;
185        }
186
187        /**
188         * Is this dialog open.
189         * 
190         * @return <code>true</code> if open
191         */
192        public boolean isOpen()
193        {
194                return overlay.isVisible();
195        }
196
197        /**
198         * Close this dialog.
199         * <p>
200         * If opened via {@link #open(Component, AjaxRequestTarget)}, the content is removed from the
201         * component tree
202         * 
203         * @param target
204         *            an optional Ajax target
205         * @return this
206         * 
207         * @see #open(Component, AjaxRequestTarget)
208         */
209        public ModalDialog close(AjaxRequestTarget target)
210        {
211                overlay.setVisible(false);
212                if (removeContentOnClose)
213                {
214                        dialog.removeAll();
215                }
216
217                if (target != null)
218                {
219                        target.add(this);
220                }
221
222                return this;
223        }
224
225        /**
226         * Close this dialog on press of escape key.
227         * 
228         * @return this
229         */
230        public ModalDialog closeOnEscape()
231        {
232                overlay.add(new CloseBehavior("keydown")
233                {
234                        protected CharSequence getPrecondition()
235                        {
236                                return "return Wicket.Event.keyCode(attrs.event) == 27";
237                        }
238                });
239                return this;
240        }
241
242        /**
243         * Close this dialog on click outside.
244         * 
245         * @return this
246         */
247        public ModalDialog closeOnClick()
248        {
249                overlay.add(new CloseBehavior("click")
250                {
251                        protected CharSequence getPrecondition()
252                        {
253                                return String.format("return attrs.event.target.id === '%s';", overlay.getMarkupId());
254                        }
255                });
256                return this;
257        }
258
259        /**
260         * Convenience method to trap focus inside the overlay.
261         * 
262         * @see TrapFocusBehavior
263         * 
264         * @return this
265         */
266        public ModalDialog trapFocus()
267        {
268                overlay.add(new TrapFocusBehavior());
269
270                return this;
271        }
272
273        private abstract class CloseBehavior extends AjaxEventBehavior
274        {
275                private CloseBehavior(String event)
276                {
277                        super(event);
278                }
279
280                @Override
281                protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
282                {
283                        super.updateAjaxAttributes(attributes);
284
285                        // has to stop immediately to prevent an enclosing dialog to close too
286                        attributes.setEventPropagation(EventPropagation.STOP_IMMEDIATE);
287
288                        attributes.getAjaxCallListeners().add(new AjaxCallListener()
289                        {
290                                @Override
291                                public CharSequence getPrecondition(Component component)
292                                {
293                                        return CloseBehavior.this.getPrecondition();
294                                }
295                        });
296                }
297
298                protected CharSequence getPrecondition()
299                {
300                        return "";
301                }
302
303                @Override
304                protected void onEvent(AjaxRequestTarget target)
305                {
306                        close(target);
307                }
308        }
309}