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 * Note: This component does not provide any styling by itself, so you have can add a
035 * {@link DefaultTheme} to this component if aren't styling these CSS classes by yourself:
036 * <dl>
037 * <dt>modal-dialog-overlay</dt>
038 * <dd>the wrapper around the actual dialog, usually used to overlay the rest of the document</dd>
039 * <dt>modal-dialog</dt>
040 * <dd>the actual dialog</dd>
041 * <dt>modal-dialog-content</dt>
042 * <dd>any additional styling for the content of this dialog</dd>
043 * </dl>
044 *
045 * @author Igor Vaynberg (ivaynberg)
046 * @author svenmeier
047 */
048public class ModalDialog extends Panel
049{
050
051        private static final long serialVersionUID = 1L;
052
053        private static final String OVERLAY_ID = "overlay";
054
055        private static final String DIALOG_ID = "dialog";
056
057        /**
058         * The id for the content of this dialoh.
059         *
060         * @see #setContent(Component)
061         * @see #open(Component, AjaxRequestTarget)
062         */
063        public static final String CONTENT_ID = "content";
064
065        private final WebMarkupContainer overlay;
066
067        private final WebMarkupContainer dialog;
068
069        private boolean removeContentOnClose;
070
071        public ModalDialog(String id)
072        {
073                super(id);
074
075                setOutputMarkupId(true);
076
077                overlay = newOverlay(OVERLAY_ID);
078                overlay.setVisible(false);
079                add(overlay);
080
081                dialog = newDialog(DIALOG_ID);
082                overlay.add(dialog);
083        }
084
085        /**
086         * Factory method for the overlay markup around the dialog.
087         *
088         * @param overlayId
089         *      id
090         * @return overlay
091         */
092        protected WebMarkupContainer newOverlay(String overlayId)
093        {
094                return new WebMarkupContainer(overlayId);
095        }
096
097        /**
098         * Factory method for the dialog markup around the content.
099         *
100         * @param dialogId
101         *      id
102         * @return overlay
103         */
104        protected WebMarkupContainer newDialog(String dialogId)
105        {
106                return new WebMarkupContainer(dialogId);
107        }
108
109        /**
110         * Set a content.
111         *
112         * @param content
113         * @see #open(AjaxRequestTarget)
114         */
115        public void setContent(Component content)
116        {
117                if (!CONTENT_ID.equals(content.getId()))
118                {
119                        throw new IllegalArgumentException(
120                                "Content must have wicket id set to ModalDialog.CONTENT_ID");
121                }
122
123                dialog.addOrReplace(content);
124
125                removeContentOnClose = false;
126        }
127
128        /**
129         * Open the dialog with a content.
130         * <p>
131         * The content will be removed on close of the dialog.
132         *
133         * @param content
134         *      the content
135         * @param target
136         *      an optional Ajax target
137         * @return this
138         * @see #setContent(Component)
139         * @see #close(AjaxRequestTarget)
140         */
141        public ModalDialog open(Component content, AjaxRequestTarget target)
142        {
143                setContent(content);
144                removeContentOnClose = true;
145
146                overlay.setVisible(true);
147
148                if (target != null)
149                {
150                        target.add(this);
151                }
152
153                return this;
154        }
155
156        /**
157         * Open the dialog.
158         *
159         * @param target
160         *      an optional Ajax target
161         * @return this
162         * @see #setContent(Component)
163         */
164        public ModalDialog open(AjaxRequestTarget target)
165        {
166                if (overlay.size() == 0)
167                {
168                        throw new WicketRuntimeException(
169                                String.format("ModalDialog with id '%s' has no content set!", getId()));
170                }
171
172                overlay.setVisible(true);
173
174                if (target != null)
175                {
176                        target.add(this);
177                }
178
179                return this;
180        }
181
182        /**
183         * Is this dialog open.
184         *
185         * @return <code>true</code> if open
186         */
187        public boolean isOpen()
188        {
189                return overlay.isVisible();
190        }
191
192        /**
193         * Close this dialog.
194         * <p>
195         * If opened via {@link #open(Component, AjaxRequestTarget)}, the content is removed from the
196         * component tree
197         *
198         * @param target
199         *      an optional Ajax target
200         * @return this
201         * @see #open(Component, AjaxRequestTarget)
202         */
203        public ModalDialog close(AjaxRequestTarget target)
204        {
205                overlay.setVisible(false);
206                if (removeContentOnClose)
207                {
208                        dialog.removeAll();
209                }
210
211                if (target != null)
212                {
213                        target.add(this);
214                }
215
216                return this;
217        }
218
219        /**
220         * Close this dialog on press of escape key.
221         *
222         * @return this
223         */
224        public ModalDialog closeOnEscape()
225        {
226                overlay.add(new CloseBehavior("keydown")
227                {
228                        protected CharSequence getPrecondition()
229                        {
230                                return "return Wicket.Event.keyCode(attrs.event) == 27";
231                        }
232                });
233                return this;
234        }
235
236        /**
237         * Close this dialog on click outside.
238         *
239         * @return this
240         */
241        public ModalDialog closeOnClick()
242        {
243                overlay.add(new CloseBehavior("click")
244                {
245                        protected CharSequence getPrecondition()
246                        {
247                                return String.format("return attrs.event.target.id === '%s';",
248                                        overlay.getMarkupId());
249                        }
250                });
251                return this;
252        }
253
254        /**
255         * Convenience method to trap focus inside the overlay.
256         *
257         * @return this
258         * @see TrapFocusBehavior
259         */
260        public ModalDialog trapFocus()
261        {
262                overlay.add(new TrapFocusBehavior());
263
264                return this;
265        }
266
267        /**
268         * can be overridden to change the {@link AjaxRequestAttributes} of the default
269         * {@link CloseBehavior} executed on close. For example to change the Ajax Channel.
270         *
271         * @param attributes
272         *      the {@link AjaxRequestAttributes} of the default {@link CloseBehavior}
273         */
274        protected void postProcessCloseBehaviorAjaxAttributes(AjaxRequestAttributes attributes)
275        {
276                // no op
277        }
278
279        private abstract class CloseBehavior extends AjaxEventBehavior
280        {
281                private CloseBehavior(String event)
282                {
283                        super(event);
284                }
285
286                @Override
287                protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
288                {
289                        super.updateAjaxAttributes(attributes);
290
291                        // has to stop immediately to prevent an enclosing dialog to close too
292                        attributes.setEventPropagation(EventPropagation.STOP_IMMEDIATE);
293
294                        attributes.getAjaxCallListeners().add(new AjaxCallListener()
295                        {
296                                @Override
297                                public CharSequence getPrecondition(Component component)
298                                {
299                                        return CloseBehavior.this.getPrecondition();
300                                }
301                        });
302
303                        postProcessCloseBehaviorAjaxAttributes(attributes);
304                }
305
306                protected CharSequence getPrecondition()
307                {
308                        return "";
309                }
310
311                @Override
312                protected void onEvent(AjaxRequestTarget target)
313                {
314                        close(target);
315                }
316        }
317}