You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by sv...@apache.org on 2019/07/10 17:06:45 UTC

[wicket] 01/01: WICKET-6666 modal dialog

This is an automated email from the ASF dual-hosted git repository.

svenmeier pushed a commit to branch WICKET-6666_modal-dialog-2
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 557d955fd93516ea0ed6b35c9e72a61b3e7bf2a1
Author: Sven Meier <sv...@apache.org>
AuthorDate: Wed Jun 19 18:09:44 2019 +0200

    WICKET-6666 modal dialog
    
    ModalWindow successor
---
 .../wicket/ajax/res/js/wicket-ajax-jquery.js       |  25 ++
 .../examples/ajax/builtin/AjaxApplication.java     |   2 +
 .../apache/wicket/examples/ajax/builtin/Index.html |   2 +
 .../ajax/builtin/modal/ModalDialogPage.html        |  63 +++++
 .../ajax/builtin/modal/ModalDialogPage.java        | 180 +++++++++++++
 .../ajax/builtin/modal/MyDialogLayout.html         |  34 +++
 .../ajax/builtin/modal/MyDialogLayout.java         |  38 +++
 .../ajax/markup/html/modal/ModalDialog.html        |  26 ++
 .../ajax/markup/html/modal/ModalDialog.java        | 281 +++++++++++++++++++++
 .../ajax/markup/html/modal/TrapFocusBehavior.java  |  50 ++++
 .../ajax/markup/html/modal/theme/DefaultTheme.java |  50 ++++
 .../ajax/markup/html/modal/theme/theme.css         |  88 +++++++
 .../ajax/markup/html/modal/trap-focus.js           | 116 +++++++++
 .../ajax/markup/html/repeater/AjaxListPanel.html   |  22 ++
 .../ajax/markup/html/repeater/AjaxListPanel.java   | 114 +++++++++
 15 files changed, 1091 insertions(+)

diff --git a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
index 47ed601..dec21cd 100644
--- a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
+++ b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
@@ -1507,6 +1507,31 @@
 					we.publish(topic.DOM_NODE_ADDED, newElement);
 				}
 			},
+			
+			add: function (element, text) {
+				var we = Wicket.Event;
+				var topic = we.Topic;
+
+				// jQuery 1.9+ expects '<' as the very first character in text
+				var cleanedText = jQuery.trim(text);
+
+				var $newElement = jQuery(cleanedText);
+				jQuery(element).append($newElement);
+
+				var newElement = Wicket.$(element.id);
+				if (newElement) {
+					we.publish(topic.DOM_NODE_ADDED, newElement);
+				}
+			},
+
+			remove: function (element) {
+				var we = Wicket.Event;
+				var topic = we.Topic;
+
+				we.publish(topic.DOM_NODE_REMOVING, element);
+
+				jQuery(element).remove();
+			},
 
 			// Method for serializing DOM nodes to string
 			// original taken from Tacos (http://tacoscomponents.jot.com)
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java
index b43828e..01d3cd5 100644
--- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/AjaxApplication.java
@@ -21,6 +21,7 @@ import org.apache.wicket.Page;
 import org.apache.wicket.ajax.AjaxNewWindowNotifyingBehavior;
 import org.apache.wicket.application.IComponentInitializationListener;
 import org.apache.wicket.examples.WicketExampleApplication;
+import org.apache.wicket.examples.ajax.builtin.modal.ModalDialogPage;
 import org.apache.wicket.examples.ajax.builtin.modal.ModalWindowPage;
 import org.apache.wicket.markup.html.WebPage;
 import org.apache.wicket.response.filter.AjaxServerAndClientTimeFilter;
@@ -63,6 +64,7 @@ public class AjaxApplication extends WicketExampleApplication
 		mountPage("lazy-loading", LazyLoadingPage.class);
 		mountPage("links", LinksPage.class);
 		mountPage("modal-window", ModalWindowPage.class);
+		mountPage("modal-dialog", ModalDialogPage.class);
 		mountPage("on-change-ajax-behavior", OnChangeAjaxBehaviorPage.class);
 		mountPage("pageables", PageablesPage.class);
 		mountPage("ratings", RatingsPage.class);
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html
index 2da2160..30aa04c 100644
--- a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/Index.html
@@ -24,6 +24,8 @@
 <br/><br/>
 <a href="modal/ModalWindowPage.html">Modal window</a>: javascript modal window example
 <br/><br/>
+<a href="modal/ModalDialogPage.html">Modal dialog (replaces deprecated Modal Window)</a>: javascript modal dialog example
+<br/><br/>
 <a href="OnChangeAjaxBehaviorPage.html">On Change Ajax Updater Example</a>: demonstrates updating page with ajax when text field value is changed	
 <br/><br/>
 <a href="PageablesPage.html">Pageables Example</a>: shows ajax paging
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.html
new file mode 100644
index 0000000..bf34917
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.html
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<html>
+	<head>
+		<wicket:head>
+			<style>
+				.modal-dialog {
+					border-radius: 5px;
+					overflow: hidden;
+				}
+			
+				h1.example-header {
+					background: #ffb158;
+					padding-top: 4px;
+					text-align: center;
+				}
+				
+				.example-content {
+					padding: 20px;
+					min-width: 100px;
+					min-height: 100px;
+					overflow-x: hidden;
+					overflow-y: auto;
+					resize: both;
+				}
+			</style>
+		</wicket:head>
+	</head>
+	<body>
+		<wicket:extend xmlns:wicket="http://wicket.apache.org">
+		
+			<p wicket:id="stacked">
+				<input type="radio" wicket:id="no" /> <label wicket:for="no">nested</label>
+				<input type="radio" wicket:id="yes" /> <label wicket:for="yes">stacked dialogs</label> 
+			</p>
+			
+			<div wicket:id="start"></div>
+			
+			<div wicket:id="stackedDialogs"></div>	
+			
+			<wicket:fragment wicket:id="fragment">
+				<p>
+					<input type="text" wicket:id="text" placeholder="Focus and press ENTER to open dialog" size="32" autofocus="autofocus"/>
+				</p>
+		
+				<div wicket:id="nestedDialog"></div>
+		
+				<p>
+					<a wicket:id="openDialog">Open dialog</a>
+					|
+					<a wicket:id="ajaxOpenDialog">via Ajax</a>
+					
+					<span wicket:id="closing">
+						|
+						<a wicket:id="close">Close dialog</a>
+						|
+						via Ajax by pressing ESC
+					</span>
+				</p>
+			</wicket:fragment>
+		
+		</wicket:extend>
+	</body>
+</html>
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.java
new file mode 100644
index 0000000..aad0eca
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.examples.ajax.builtin.modal;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxCallListener;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.examples.ajax.builtin.BasePage;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalDialog;
+import org.apache.wicket.extensions.ajax.markup.html.modal.theme.DefaultTheme;
+import org.apache.wicket.extensions.ajax.markup.html.repeater.AjaxListPanel;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.form.Radio;
+import org.apache.wicket.markup.html.form.RadioGroup;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.PropertyModel;
+
+/**
+ * @author Igor Vaynberg (ivaynberg)
+ */
+public class ModalDialogPage extends BasePage
+{
+
+	private AjaxListPanel stackedDialogs;
+
+	/**
+	 * Should dialogs be stacked rather than nested
+	 */
+	private boolean stacked = false;
+
+	public ModalDialogPage()
+	{
+
+		queue(new RadioGroup("stacked", new PropertyModel<>(this, "stacked"))
+			.setRenderBodyOnly(false).add(new AjaxFormChoiceComponentUpdatingBehavior()
+			{
+				@Override
+				protected void onUpdate(AjaxRequestTarget target)
+				{
+				}
+			}));
+
+		queue(new Radio<Boolean>("yes", Model.of(true)));
+		queue(new Radio<Boolean>("no", Model.of(false)));
+
+		queue(new ModalFragment("start"));
+
+		stackedDialogs = new AjaxListPanel("stackedDialogs");
+		queue(stackedDialogs);
+	}
+
+	private class ModalFragment extends Fragment
+	{
+
+		private ModalDialog nestedDialog;
+
+		public ModalFragment(String id)
+		{
+			super(id, "fragment", ModalDialogPage.this);
+
+			nestedDialog = new ModalDialog("nestedDialog");
+			nestedDialog.add(new DefaultTheme());
+			nestedDialog.trapFocus();
+			nestedDialog.closeOnEscape();
+			queue(nestedDialog);
+
+			queue(new AjaxLink<Void>("ajaxOpenDialog")
+			{
+				@Override
+				public void onClick(AjaxRequestTarget target)
+				{
+					openDialog(target);
+				}
+			});
+
+			queue(new Link<Void>("openDialog")
+			{
+				@Override
+				public void onClick()
+				{
+					openDialog(null);
+				}
+			});
+
+			queue(new TextField("text").add(new AjaxEventBehavior("keydown")
+			{
+				@Override
+				protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
+				{
+					super.updateAjaxAttributes(attributes);
+
+					attributes.getAjaxCallListeners().add(new AjaxCallListener()
+					{
+						@Override
+						public CharSequence getPrecondition(Component component)
+						{
+							return "return Wicket.Event.keyCode(attrs.event) == 13;";
+						}
+					});
+				}
+
+				@Override
+				protected void onEvent(AjaxRequestTarget target)
+				{
+					openDialog(target);
+				}
+			}));
+
+			queue(new WebMarkupContainer("closing")
+			{
+				@Override
+				protected void onConfigure()
+				{
+					super.onConfigure();
+
+					setVisible(findParent(ModalDialog.class) != null);
+				}
+			});
+
+			queue(new Link<Void>("close")
+			{
+				@Override
+				public void onClick()
+				{
+					findParent(ModalDialog.class).close(null);
+				}
+			});
+		}
+
+		private void openDialog(AjaxRequestTarget target)
+		{
+			Component content = new MyDialogLayout(ModalDialog.CONTENT_ID, Model.of("Dialog"),
+				new ModalFragment(ModalDialog.CONTENT_ID));
+
+			if (stacked)
+			{
+				// stack a new dialog
+				ModalDialog dialog = new ModalDialog(stackedDialogs.newChildId())
+				{
+					@Override
+					public ModalDialog close(AjaxRequestTarget target)
+					{
+						return stackedDialogs.delete(this, target);
+					}
+				};
+				dialog.add(new DefaultTheme());
+				dialog.trapFocus();
+				dialog.closeOnEscape();
+				dialog.setContent(content);
+				stackedDialogs.append(dialog, target).open(target);
+			}
+			else
+			{
+				// use the nested dialog
+				nestedDialog.open(content, target);
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/MyDialogLayout.html b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/MyDialogLayout.html
new file mode 100644
index 0000000..56af543
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/MyDialogLayout.html
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<html>
+	<head>
+		<wicket:head>
+			<style>
+				.modal-dialog {
+					border-radius: 5px;
+					overflow: hidden;
+				}
+			
+				h1.example-header {
+					background: #ffb158;
+					padding-top: 4px;
+					text-align: center;
+				}
+				
+				.example-content {
+					padding: 20px;
+					min-width: 100px;
+					min-height: 100px;
+					overflow-x: hidden;
+					overflow-y: auto;
+					resize: both;
+				}
+			</style>
+		</wicket:head>
+	</head>
+	<body>
+		<wicket:panel xmlns:wicket="http://wicket.apache.org">
+			<h1 wicket:id="header" class="example-header"></h1>
+			<div wicket:id="content" class="example-content"></div>
+		</wicket:panel>
+	</body>
+</html>
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/MyDialogLayout.java b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/MyDialogLayout.java
new file mode 100644
index 0000000..506447e
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/MyDialogLayout.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.examples.ajax.builtin.modal;
+
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.IModel;
+
+/**
+ * @author svenmeier
+ */
+public class MyDialogLayout extends Panel
+{
+
+	public MyDialogLayout(String id, IModel<String> header, WebMarkupContainer content)
+	{
+		super(id);
+
+		add(new Label("header", header));
+
+		add(content);
+	}
+}
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.html b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.html
new file mode 100644
index 0000000..35b7bef
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.html
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<wicket:panel xmlns:wicket="http://wicket.apache.org">
+	<div wicket:id="overlay" class="modal-dialog-overlay">
+		<div wicket:id="dialog" class='modal-dialog'>
+			<div class='modal-dialog-content'>
+				<div wicket:id="content"></div>
+			</div>
+		</div>
+	</div>
+</wicket:panel>
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.java
new file mode 100644
index 0000000..ce88ae4
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.java
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.modal;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxCallListener;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.EventPropagation;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Panel;
+
+/**
+ * Presents a modal dialog to the user. See {@link #open(Component, AjaxRequestTarget)} and {@link #close(AjaxRequestTarget)} methods.
+ * 
+ * @author Igor Vaynberg (ivaynberg)
+ * @author svenmeier
+ */
+public class ModalDialog extends Panel
+{
+
+	private static final long serialVersionUID = 1L;
+
+	private static final String OVERLAY_ID = "overlay";
+
+	private static final String DIALOG_ID = "dialog";
+
+	public static final String CONTENT_ID = "content";
+
+	private WebMarkupContainer overlay;
+	
+	private WebMarkupContainer dialog;
+	
+	private boolean removeContentOnClose;
+
+	public ModalDialog(String id)
+	{
+		super(id);
+
+		setOutputMarkupId(true);
+
+		overlay = newOverlay(OVERLAY_ID);
+		overlay.setVisible(false);
+		add(overlay);
+		
+		dialog = newDialog(DIALOG_ID);
+		overlay.add(dialog);
+	}
+
+	/**
+	 * Factory method for the overlay markup around the dialog.
+	 * 
+	 * @param overlayId
+	 *            id
+	 * @return overlay
+	 */
+	protected WebMarkupContainer newOverlay(String overlayId)
+	{
+		return new WebMarkupContainer(overlayId);
+	}
+
+	/**
+	 * Factory method for the dialog markup around the content.
+	 * 
+	 * @param overlayId
+	 *            id
+	 * @return overlay
+	 */
+	protected WebMarkupContainer newDialog(String dialogId)
+	{
+		return new WebMarkupContainer(dialogId);
+	}
+
+	/**
+	 * Set a content.
+	 * 
+	 * @param content
+	 * 
+	 * @see #open(AjaxRequestTarget)
+	 */
+	public void setContent(Component content)
+	{
+		if (!content.getId().equals(CONTENT_ID))
+		{
+			throw new IllegalArgumentException(
+				"Content must have wicket id set to ModalDialog.CONTENT_ID");
+		}
+
+		dialog.addOrReplace(content);
+
+		removeContentOnClose = false;
+	}
+
+	/**
+	 * Open the dialog with a content.
+	 * <p>
+	 * The content will be removed on close of the dialog.
+	 * 
+	 * @param content
+	 *            the content
+	 * @param target
+	 *            an optional Ajax target
+	 * @return this
+	 * 
+	 * @see #close(AjaxRequestTarget)
+	 */
+	public ModalDialog open(Component content, AjaxRequestTarget target)
+	{
+		setContent(content);
+		removeContentOnClose = true;
+		
+		overlay.setVisible(true);
+
+		if (target != null)
+		{
+			target.add(this);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Open the dialog.
+	 * 
+	 * @param target
+	 *            an optional Ajax target
+	 * @return this
+	 * 
+	 * @see #setContent(Component)
+	 */
+	public ModalDialog open(AjaxRequestTarget target)
+	{
+		if (overlay.size() == 0) {
+			throw new WicketRuntimeException("no content set");
+		}
+		
+		overlay.setVisible(true);
+
+		if (target != null)
+		{
+			target.add(this);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Is this dialog open.
+	 * 
+	 * @return <code>true</code> if open
+	 */
+	public boolean isOpen()
+	{
+		return overlay.isVisible();
+	}
+
+	/**
+	 * Close this dialog.
+	 * <p>
+	 * If opened via {@link #open(Component, AjaxRequestTarget)}, the content is removed from the component tree
+	 * 
+	 * @param target
+	 *            an optional Ajax target
+	 * @return this
+	 * 
+	 * @see #open(Component, AjaxRequestTarget)
+	 */
+	public ModalDialog close(AjaxRequestTarget target)
+	{
+		overlay.setVisible(false);
+		if (removeContentOnClose) {
+			dialog.removeAll();
+		}
+
+		if (target != null)
+		{
+			target.add(this);
+		}
+
+		return this;
+	}
+
+	/**
+	 * Close this dialog on press of escape key.
+	 * 
+	 * @return this
+	 */
+	public ModalDialog closeOnEscape()
+	{
+		overlay.add(new CloseBehavior("keydown")
+		{
+			protected CharSequence getPrecondition()
+			{
+				return "return Wicket.Event.keyCode(attrs.event) == 27";
+			}
+		});
+		return this;
+	}
+
+	/**
+	 * Close this dialog on click outside.
+	 * 
+	 * @return this
+	 */
+	public ModalDialog closeOnClick()
+	{
+		overlay.add(new CloseBehavior("click") {
+			protected CharSequence getPrecondition()
+			{
+				return String.format("return attrs.event.target.id == '%s';", overlay.getMarkupId());
+			}
+		});
+		return this;
+	}
+
+	/**
+	 * Convenience method to trap focus inside the overlay.
+	 * 
+	 * @see {@link TrapFocusBehavior}
+	 * 
+	 * @return this
+	 */
+	public ModalDialog trapFocus()
+	{
+		overlay.add(new TrapFocusBehavior());
+
+		return this;
+	}
+	
+	private abstract class CloseBehavior extends AjaxEventBehavior
+	{
+		private CloseBehavior(String event)
+		{
+			super(event);
+		}
+
+		@Override
+		protected void updateAjaxAttributes(AjaxRequestAttributes attributes)
+		{
+			super.updateAjaxAttributes(attributes);
+
+			// has to stop immediately to prevent an enclosing dialog to close too 
+			attributes.setEventPropagation(EventPropagation.STOP_IMMEDIATE);
+
+			attributes.getAjaxCallListeners().add(new AjaxCallListener()
+			{
+				@Override
+				public CharSequence getPrecondition(Component component)
+				{
+					return CloseBehavior.this.getPrecondition();
+				}
+			});
+		}
+
+		protected CharSequence getPrecondition() {
+			return "";
+		}
+
+		@Override
+		protected void onEvent(AjaxRequestTarget target)
+		{
+			close(target);
+		}
+	}
+}
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/TrapFocusBehavior.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/TrapFocusBehavior.java
new file mode 100644
index 0000000..3c37226
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/TrapFocusBehavior.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.modal;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+import org.apache.wicket.markup.head.PriorityHeaderItem;
+import org.apache.wicket.request.resource.JavaScriptResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+
+/**
+ * Trap focus inside a component's markup.
+ * 
+ * @author svenmeier
+ */
+public class TrapFocusBehavior extends Behavior
+{
+
+	private static final long serialVersionUID = 1L;
+	
+	private static final ResourceReference JS = new JavaScriptResourceReference(
+		TrapFocusBehavior.class, "trap-focus.js");
+
+	@Override
+	public void renderHead(Component component, IHeaderResponse response)
+	{
+		response.render(JavaScriptHeaderItem.forReference(JS));
+		
+		CharSequence script = String.format("Wicket.trapFocus('%s');", component.getMarkupId());
+		
+		response.render(new PriorityHeaderItem(OnDomReadyHeaderItem.forScript(script)));
+	}
+}
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/DefaultTheme.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/DefaultTheme.java
new file mode 100644
index 0000000..f408d07
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/DefaultTheme.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.modal.theme;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.extensions.ajax.markup.html.modal.ModalDialog;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.request.resource.CssResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+
+/**
+ * Default theme for {@link ModalDialog}.
+ * 
+ * @author svenmeier
+ */
+public class DefaultTheme extends Behavior
+{
+	private static final long serialVersionUID = 1L;
+
+	private static final ResourceReference CSS = new CssResourceReference(DefaultTheme.class, "theme.css");
+
+	@Override
+	public void onComponentTag(Component component, ComponentTag tag)
+	{
+		tag.append("class", "dialog-theme-default", " ");
+	}
+
+	@Override
+	public void renderHead(Component component, IHeaderResponse response)
+	{
+		response.render(CssHeaderItem.forReference(CSS));
+	}
+}
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/theme.css b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/theme.css
new file mode 100644
index 0000000..1b99eae
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/theme/theme.css
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.dialog-theme-default .modal-dialog-overlay {
+	position: fixed;
+	left: 0;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	margin: 0;
+	padding: 0;
+	z-index: 1000;
+
+	/* flex-box to center .modal-dialog */
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
+}
+
+.dialog-theme-default .modal-dialog-overlay::before {
+  	content: "";
+	display: block;
+	position: absolute;
+	width: 100%;
+	height: 100%;
+	margin: 0;
+
+	background: rgba(0, 0, 0, 0.2);
+}
+
+.dialog-theme-default .modal-dialog {
+	position: absolute;
+	top: 10%;
+
+	background: white;
+	box-shadow: 0 0 60px 10px rgba(0, 0, 0, 0.7);
+}
+
+.dialog-theme-default .modal-dialog-content {
+}
+
+/* shift nested dialogs */
+
+.dialog-theme-default .modal-dialog .modal-dialog {
+	margin-top: 32px;
+	margin-left: 32px;
+}
+
+.dialog-theme-default .modal-dialog .modal-dialog .modal-dialog {
+	margin-top: 64px;
+	margin-left: 64px;
+}
+
+.dialog-theme-default .modal-dialog .modal-dialog .modal-dialog .modal-dialog {
+	margin-top: 96px;
+	margin-left: 96px;
+}
+
+/* shift sibling dialogs */
+
+.dialog-theme-default ~ .dialog-theme-default .modal-dialog {
+	margin-top: 32px;
+	margin-left: 32px;
+}
+
+.dialog-theme-default ~ .dialog-theme-default ~ .dialog-theme-default .modal-dialog {
+	margin-top: 64px;
+	margin-left: 64px;
+}
+
+.dialog-theme-default ~ .dialog-theme-default ~ .dialog-theme-default ~ .dialog-theme-default .modal-dialog {
+	margin-top: 96px;
+	margin-left: 96px;
+}
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js
new file mode 100644
index 0000000..eeb0539
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+;
+(function($, window, document, undefined) {
+	'use strict';
+
+	if (window.Wicket && window.Wicket.trapFocus) {
+		return;
+	}
+	
+	/** Finds all elements inside container that can receive focus */
+	function findFocusable(container) {
+		var focusables = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]';
+		return container.find(focusables).filter(":visible, *:not([tabindex=-1])");
+	}
+
+	// special handler listening for 'trapfocusremove' handlers - it is
+	// invoked when any element with that event handler is removed from the DOM
+	$.event.special.trapfocusremove = {
+		remove: function(handleObj) {
+			// forward notification of removal
+			handleObj.handler();
+		}
+	};
+
+	// global handler for 'focusin'
+	var focusin = $.noop;
+
+	// setup focus trap for an element
+	window.Wicket.trapFocus = function(element) {
+		
+		// keep old active element
+		var oldActive = document.activeElement;
+		Wicket.Log.debug("trap-focus: focus was on element", oldActive);
+
+		var $element = $('#' + element);
+
+		// allow focus on element itself
+		$element.attr('tabindex', 0);
+		
+		// handles focus navigation via tab key
+		$element.on("keydown", function(e) {
+			if (Wicket.Event.keyCode(e) === 9) { // tab
+				var $focusable = findFocusable($element);
+				if ($focusable.length > 0) {
+					var firstFocusable = $focusable.get(0);
+					var lastFocusable  = $focusable.get($focusable.length - 1);
+
+					if (e.shiftKey) {
+						if (e.target === firstFocusable || $element.is(e.target)) {
+							e.preventDefault();
+							lastFocusable.focus();
+						}
+					} else {
+						if (e.target === lastFocusable || $element.is(e.target)) {
+							e.preventDefault();
+							firstFocusable.focus();
+						}
+					}
+				}
+			}
+		});
+		
+		// turn off possible previous 'focusin' handler
+		var previousfocusin = focusin;
+		$(document).off("focusin", focusin);
+
+		// ... pull in focus
+		findFocusable($element).first().focus();
+		
+		// ... and install new handler
+		focusin = function() {
+			if (!$.contains($element[0], document.activeElement) && $element[0] !== document.activeElement) {
+				// focus is outside of element, so pull in focus
+				findFocusable($element).first().focus();
+			}
+		};
+		$(document).on("focusin", focusin);
+		
+		// listen for removal
+		$element.on("trapfocusremove", function() {
+			// turn off 'focusin' handler
+			$(document).off("focusin", focusin);
+			
+			// ... restore old focus
+			if (oldActive) {
+				try {
+					oldActive.focus();
+					Wicket.Log.debug("trap-focus: restored focus to element ", oldActive);
+				} catch (error) {
+					Wicket.Log.error("trap-focus: error restoring focus. Attempted to set focus to element, but got an exception", oldActive, error);
+				}
+			}
+			
+			// ... and re-install previous 'focusin' handler
+			focusin = previousfocusin;
+			$(document).on("focusin", focusin);
+		});
+	};
+
+}(jQuery, window, document, undefined));
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.html b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.html
new file mode 100644
index 0000000..8f9aa2e
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.html
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<wicket:panel xmlns:wicket="http://wicket.apache.org">
+	<div wicket:id="container">
+		<div wicket:id="repeater"></div>
+	</div>
+</wicket:panel>
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.java
new file mode 100644
index 0000000..cfaa74c
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/repeater/AjaxListPanel.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.extensions.ajax.markup.html.repeater;
+
+import java.util.EmptyStackException;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.RepeatingView;
+
+/**
+ * An panel for an <it>ajaxified</it> list of components.
+ * <p>
+ * Allows to append and delete components without an update of a whole container.
+ * 
+ * @see #append(Component, AjaxRequestTarget)
+ * @see #delete(Component, AjaxRequestTarget)
+ * 
+ * @author svenmeier
+ */
+public class AjaxListPanel extends Panel
+{
+
+	private static final long serialVersionUID = 1L;
+	
+	private WebMarkupContainer container;
+
+	private RepeatingView repeater;
+
+	public AjaxListPanel(String id)
+	{
+		super(id);
+
+		this.container = new WebMarkupContainer("container");
+		this.container.setOutputMarkupId(true);
+		add(this.container);
+
+		this.repeater = new RepeatingView("repeater");
+		this.container.add(this.repeater);
+	}
+
+	/**
+	 * Get an id for a new child to be appended.
+	 * 
+	 * @return id
+	 * 
+	 * @see #append(Component, AjaxRequestTarget)
+	 */
+	public String newChildId() {
+		return repeater.newChildId();
+	}
+	
+	/**
+	 * Append a component.
+	 * 
+	 * @param component
+	 *            the component
+	 * @param target
+	 *            optional target
+	 * @return the component
+	 * 
+	 * @param T component type
+	 */
+	public <T extends Component> T append(T component, AjaxRequestTarget target)
+	{
+		this.repeater.add(component);
+		
+		if (target != null)
+		{
+			target.prependJavaScript(String.format("Wicket.DOM.add(Wicket.DOM.get('%s'), '<div id=\"%s\" />');",
+				container.getMarkupId(), component.getMarkupId()));
+			target.add(component);
+		}
+
+		return component;
+	}
+	
+	/**
+	 * Delete a component.
+	 * 
+	 * @param target
+	 *            optional target
+	 * @return the component
+	 * @throws EmptyStackException if empty
+	 * 
+	 * @param T component type
+	 */
+	public <T extends Component> T delete(T component, AjaxRequestTarget target) {
+
+		this.repeater.remove(component);
+		if (target != null)
+		{
+			target.appendJavaScript(String.format("Wicket.DOM.remove(Wicket.DOM.get('%s'));", component.getMarkupId()));
+		}
+		
+		return (T)component;
+	}
+}
\ No newline at end of file