You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by iv...@apache.org on 2019/05/10 20:42:16 UTC

[wicket] branch modal-dialog created (now 0460768)

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

ivaynberg pushed a change to branch modal-dialog
in repository https://gitbox.apache.org/repos/asf/wicket.git.


      at 0460768  WICKET-6666 initial checkin of new ModalDialog

This branch includes the following new commits:

     new 0460768  WICKET-6666 initial checkin of new ModalDialog

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[wicket] 01/01: WICKET-6666 initial checkin of new ModalDialog

Posted by iv...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ivaynberg pushed a commit to branch modal-dialog
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 0460768cc29dfeb78f74284ed5b7ac98d85885ba
Author: Igor Vaynberg <ig...@42lines.net>
AuthorDate: Fri May 10 13:41:39 2019 -0700

    WICKET-6666 initial checkin of new ModalDialog
---
 .../examples/ajax/builtin/AjaxApplication.java     |   2 +
 .../apache/wicket/examples/ajax/builtin/Index.html |   2 +
 .../ajax/builtin/modal/ModalDialogPage.html        |  19 +
 .../ajax/builtin/modal/ModalDialogPage.java        |  77 ++++
 .../ajax/markup/html/modal/ModalDialog-skin.css    |  24 ++
 .../ajax/markup/html/modal/ModalDialog.css         |  43 +++
 .../ajax/markup/html/modal/ModalDialog.html        |   7 +
 .../ajax/markup/html/modal/ModalDialog.java        | 240 ++++++++++++
 .../ajax/markup/html/modal/ModalDialog.js          | 424 +++++++++++++++++++++
 .../markup/html/modal/ModalDialogReferences.java   |  38 ++
 10 files changed, 876 insertions(+)

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 82caa41..a5ce9ac 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;
@@ -65,6 +66,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..1fbe9d5
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.html
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<wicket:extend xmlns:wicket="http://wicket.apache.org">
+
+	<div wicket:id="dialog1"></div>
+	
+	<a wicket:id="openDialog1">Open Dialog 1</a>
+	
+	<wicket:fragment wicket:id="modalFragment1">
+	
+		<p>
+			This is a modal dialog
+		</p>
+		<p>
+			<a wicket:id="close" class="x-modal-close">Close</a>
+		</p>
+	
+	</wicket:fragment>
+
+</wicket:extend>
\ 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..ff1974c
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/ajax/builtin/modal/ModalDialogPage.java
@@ -0,0 +1,77 @@
+/*
+ * 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.ajax.AjaxRequestTarget;
+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.ModalDialogReferences;
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.html.panel.Fragment;
+
+/**
+ * @author Igor Vaynberg (ivaynberg)
+ */
+public class ModalDialogPage extends BasePage {
+
+	public ModalDialogPage() {
+
+		ModalDialog dialog1 = new ModalDialog("dialog1");
+		queue(dialog1);
+
+		queue(new AjaxLink<Void>("openDialog1") {
+			@Override
+			public void onClick(AjaxRequestTarget target) {
+				dialog1.open(target, new ModalFragment1(ModalDialog.CONTENT_ID) {
+					@Override
+					protected void onClose(AjaxRequestTarget target) {
+						dialog1.close(target);
+					}
+				});
+			}
+		});
+
+	}
+
+	@Override
+	public void renderHead(IHeaderResponse response) {
+		super.renderHead(response);
+		// include default modal skin css
+		response.render(CssHeaderItem.forReference(ModalDialogReferences.CSS_SKIN));
+	}
+
+	private abstract class ModalFragment1 extends Fragment {
+		public ModalFragment1(String id) {
+			super(id, "modalFragment1", ModalDialogPage.this);
+
+			queue(new AjaxLink<Void>("close") {
+
+				@Override
+				public void onClick(AjaxRequestTarget target) {
+					onClose(target);
+				}
+
+			});
+		}
+
+		protected abstract void onClose(AjaxRequestTarget target);
+
+	}
+
+}
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog-skin.css b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog-skin.css
new file mode 100644
index 0000000..4d6c78f
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog-skin.css
@@ -0,0 +1,24 @@
+body.modal-dialog-open {
+	
+}
+
+body.modal-dialog-no-scroll {
+}
+
+.modal-dialog-overlay {
+	background: rgba(0, 0, 0, 0.2);
+}
+
+.modal-dialog {
+	max-width: 800px;
+	background: white;
+	box-shadow: 0 0 60px 10px rgba(0, 0, 0, 0.7);
+}
+
+.modal-dialog-scroll-area {
+	overflow: auto;
+}
+
+.modal-dialog-content {
+	padding: 20px;
+}
\ No newline at end of file
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.css b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.css
new file mode 100644
index 0000000..317ff82
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.css
@@ -0,0 +1,43 @@
+body.modal-dialog-open {
+	
+}
+
+body.modal-dialog-no-scroll {
+	overflow: hidden;
+	height: 100%;
+	width: 100%;
+}
+
+body.modal-dialog-open-ios {
+	position: fixed;
+}
+
+.modal-dialog-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	margin: 0;
+	padding: 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
+	z-index: 1000000;
+}
+
+.modal-dialog {
+	position: relative;
+	width: 100%;
+	margin: auto;
+	overflow: hidden;
+	z-index: 1000010;
+}
+
+.modal-dialog-scroll-area {
+	overflow: auto;
+}
+
+.modal-dialog-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..c2aacd1
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.html
@@ -0,0 +1,7 @@
+<wicket:panel xmlns:wicket>
+	<div wicket:id="container">
+		<form wicket:id="form">
+				<div wicket:id="content"></div>
+		</form>
+	</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..a9451d9
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.java
@@ -0,0 +1,240 @@
+package org.apache.wicket.extensions.ajax.markup.html.modal;
+
+import java.io.Serializable;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+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.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.panel.EmptyPanel;
+import org.apache.wicket.markup.html.panel.Panel;
+
+import com.github.openjson.JSONStringer;
+
+/**
+ * Presents a modal dialog to the user. See open and close methods.
+ * 
+ * @author Igor Vaynberg (ivaynberg)
+ */
+public class ModalDialog extends Panel
+{
+
+	public static final String CONTENT_ID = "content";
+
+	private final WebMarkupContainer container;
+	private final WebMarkupContainer contentContainer;
+	private boolean open = false;
+	private transient boolean openedInThisRequest = false;
+	private Options options;
+
+	public ModalDialog(String id)
+	{
+		super(id);
+		setOutputMarkupId(true);
+
+		// Container controls the overall visibility of the modal form innards. In its initial state
+		// this is set to
+		// hidden so only the external tag renders. When the modal is openedInThisRequest it is
+		// first repainted with
+		// this shown to
+		// insert the markup into the dom, and then the modal javascript is invoked to rip out this
+		// tag out of the dom
+		// and make it modal.
+		container = new WebMarkupContainer("container");
+		container.setOutputMarkupId(true);
+		container.setVisible(false);
+		add(container);
+
+		// We need this here in case the modal itself is placed inside a form. If that is the case
+		// and the content
+		// contains a form that form's markup form tag will be rendered as div because as far as
+		// wicket is concerned it
+		// is part of the same dom as the page and nested forms are forbidden. By overriding
+		// isRootForm() to true we are
+		// forcing this form - which will be ripped out of the dom along with the content form if
+		// there is one to always
+		// render its tag as 'form' instead of 'div'.
+		var form = new Form<Void>("form")
+		{
+			@Override
+			public boolean isRootForm()
+			{
+				return true;
+			}
+		};
+		form.setOutputMarkupId(true);
+		container.add(form);
+
+		contentContainer = form;
+		contentContainer.add(new EmptyPanel(CONTENT_ID));
+	}
+
+	@Override
+	public void renderHead(IHeaderResponse response)
+	{
+		super.renderHead(response);
+		response.render(JavaScriptHeaderItem.forReference(ModalDialogReferences.JS));
+
+		// if the page is refreshed and the window was open in the previous request we need to
+		// re-open it
+		if (open == true && openedInThisRequest == false)
+		{
+			response.render(OnDomReadyHeaderItem.forScript(getOpenJavascript()));
+		}
+	}
+
+	public ModalDialog open(AjaxRequestTarget target, WebMarkupContainer content)
+	{
+		open(target, null, content);
+		return this;
+	}
+
+	public ModalDialog open(AjaxRequestTarget target, Options options, WebMarkupContainer content)
+	{
+
+		if (!content.getId().equals(CONTENT_ID))
+		{
+			throw new IllegalArgumentException(
+				"Content must have wicket id set to ModalDialog.CONTENT_ID");
+		}
+
+		contentContainer.replace(content);
+
+		container.setVisible(true);
+
+		open = true;
+		openedInThisRequest = true;
+
+		target.add(this);
+
+		this.options = options;
+
+		target.prependJavaScript(getOpenJavascript());
+		return this;
+	}
+
+	protected String getOpenJavascript()
+	{
+		Options options = this.options;
+
+		if (options == null)
+		{
+			options = new Options();
+		}
+
+		if (options.validate == null)
+		{
+			options.validate = Application.get().usesDevelopmentConfig();
+		}
+
+		String optionsJson = options.toJson();
+		String javascript = String.format("window.wicket.modal.open('%s', %s);",
+			container.getMarkupId(), optionsJson);
+		// wrapping in timeout removes the execution out of wicket's ajax update workflow making
+		// errors non-fatal and
+		// errors easier to debug due to simplified stack trace
+		javascript = String.format("window.setTimeout(function() { %s }, 0);", javascript);
+		return javascript;
+
+	}
+
+	public ModalDialog close(AjaxRequestTarget target)
+	{
+		open = false;
+		this.options = null;
+		String javascript = String.format("window.wicket.modal.close('%s');",
+			container.getMarkupId());
+		javascript = String.format("window.setTimeout(function() { %s }, 0);", javascript);
+		target.prependJavaScript(javascript);
+		container.setVisible(false);
+		contentContainer.replace(new EmptyPanel(CONTENT_ID));
+		target.add(this);
+		return this;
+	}
+
+	@Override
+	protected void onDetach()
+	{
+		super.onDetach();
+		openedInThisRequest = false;
+	}
+
+	public static class Options implements Serializable
+	{
+		private Boolean validate;
+		private String console;
+		private String maxWidth;
+		private String maxHeight;
+
+		public Boolean getValidate()
+		{
+			return validate;
+		}
+
+		public void setValidate(Boolean validate)
+		{
+			this.validate = validate;
+		}
+
+		public String getConsole()
+		{
+			return console;
+		}
+
+		public void setConsole(String console)
+		{
+			this.console = console;
+		}
+
+		public String getMaxWidth()
+		{
+			return maxWidth;
+		}
+
+		public void setMaxWidth(String maxWidth)
+		{
+			this.maxWidth = maxWidth;
+		}
+
+		public String getMaxHeight()
+		{
+			return maxHeight;
+		}
+
+		public void setMaxHeight(String maxHeight)
+		{
+			this.maxHeight = maxHeight;
+		}
+
+		public String toJson()
+		{
+			var json = new JSONWriter();
+
+			json.object();
+			json.value("validate", validate);
+			json.value("console", console);
+			json.value("maxWidth", maxWidth);
+			json.value("maxHeight", maxHeight);
+			json.endObject();
+			return json.toString();
+		}
+
+		private static class JSONWriter extends JSONStringer
+		{
+			public JSONWriter value(String key, Object value)
+			{
+				if (value != null)
+				{
+					key(key).value(value);
+				}
+				return this;
+			}
+		}
+
+
+	}
+
+}
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.js b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.js
new file mode 100644
index 0000000..4c67530
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialog.js
@@ -0,0 +1,424 @@
+/*
+ * 
+ * FEATURES
+ * - When modal is closed focus is restored to the element that had it before the modal was opened
+ * - Focus is trapped inside the modal when using tab/shift-tab
+ * - Focus is set on the first focusable element in the modal when it is opened
+ * - On Escape or click outside the modal a button with class x-modal-close will be clicked
+ * - Secondary close buttons can be added and marked with x-modal-close-secondary. Clicking these buttons forwards the
+ *   click to the primary x-modal-close button
+ * - Aria support
+ *   - Various aria attributes added to the modal making it behave as a dialog to screen readers
+ *   - aria-labelledby will be added if the modal content contains an element with x-modal-title class
+ *   - adia-describedby will be added if the modal content contains an element with x-modal-description class
+ * 
+ * ENTRY POINTS
+ * - window.wicket.modal.open: function(element, options)
+ *   - element: string|dom|jquery - dom element that will be body of modal
+ *   - options: object, see description below
+ * - window.wicket.modal.close: function(element)
+ *   - element: string|dom|jquery - dom element that was specified as body of modal
+ *  
+ * OPTIONS
+ * validate: boolean
+ *  - when modal is opened several checks will be performed
+ *  - error when modal content does not contain an element with x-modal-close class
+ *  - warning when modal content does not contain an element with modal-description class
+ *  - error when modal does not contain any focusable elements
+ * console: object
+ *  - an object used for reporting validation errors
+ *    - must have error(object...) method
+ *    - must have warn(object...) method
+ * 
+ * ROADMAP
+ * - Set max height of content as 80% of screen, also provide option later
+ * - Open full screen on small screens - css fix only via media queries?
+ * - Support for simultaneously opened modals - testing to make sure it works ok or do we need to implement stack tracking
+ * 
+ */
+;
+(function($, window, document, console, undefined) {
+	'use strict';
+
+	if (window.wicket && window.wicket.modal) {
+		return;
+	}
+
+	var DATA_KEY = "modal-dialog-data";
+	var OVERLAY_SELECTOR = ".modal-dialog-overlay";
+	var CONTAINER_SELECTOR = ".modal-dialog";
+	var SCROLL_SELECTOR=".modal-dialog-scroll-area";
+	var CONTENT_SELECTOR = ".modal-dialog-content";
+	var CLOSE_SELECTOR = ".x-modal-close";
+	var SECONDARY_CLOSE_SELECTOR = ".x-modal-close-secondary";
+	
+	//
+	// UTILITY METHODS
+	//
+
+	/** Retreives id of the element, creates one if none */
+	var getOrCreateIdCounter = 0;
+	function getOrCreateId(element) {
+		if (!element.attr("id")) {
+			element.attr("id", "modal-autoid-" + (getOrCreateIdCounter++));
+		}
+		return element.attr("id");
+	}
+
+	/**
+	 * Resolves a value to a dom node, useful when parsing arguments passed to
+	 * functions
+	 */
+	function resolveDomNode(element) {
+		if ((typeof element) === "string") {
+			return $(document.getElementById(element));
+		} else if (element.tagName) {
+			return $(element);
+		} else if (element instanceof $) {
+			return element;
+		}
+		throw new Error("Cannot resolve value: " + element + " to dom node");
+	}
+
+	/** 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");
+	}
+
+	/**
+	 * Finds all elements inside the container that can receive focus via the
+	 * tab key
+	 */
+	function findTabbable(container) {
+		return findFocusable(container).not("*[tabindex=-1]");
+	}
+
+	/** Focuses the first element inside the modal */
+	function focusDefaultFocusable(container) {
+		var matches = findFocusable(container);
+		var first = matches.not(".modal-dialog-close").first();
+		if (first.length > 0) {
+			first.focus();
+		} else {
+			matches.first().focus();
+		}
+	}
+
+	/**
+	 * Finds and clicks the close button inside the modal. Returns true if
+	 * button was found.
+	 */
+	function findAndClickCloseButton(container) {
+		var matches = container.find(CLOSE_SELECTOR).filter(":visible");
+		if (matches.length > 0) {
+			matches.first().click();
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	//
+	// BEHAVIORS
+	//
+	// Behaviors are event listeners that get called from open and close
+	// methods, they allow various aspects of code such as focus management and
+	// aria attribute management to be decoupled from each other making the
+	// overall code cleaner and easier to maintain.
+	//
+	// The structure of a behavior is an object with the following properties:
+	// initialize: function()
+	// - called before first modal is opened
+	// destroy: function()
+	// - called after last modal is closed
+	// prepare: function(overlayElement, contentElement, data)
+	// - called after overlay dom is constructed, but before it is inserted
+	// - into main dom
+	// open: function(overlayElement, contentElement, data)
+	// - called after overlayElement is inserted into main dom
+	// close: function(overlayElement, contentElement, data)
+	// - called after overlayElement is removed from main dom
+
+	//Scroll settings to remember for ios scroll to top issue. Currently, ios allows body 
+	//scrolling unless body is set to position: fixed, which causes the window to scroll to top.
+    var scrollTop;
+
+	/** Behavior that appends a css class to body as long as any modal is open */
+	var appendBodyClassBehavior = {
+
+		initialize : function() {
+			var body = $("body");
+			body.addClass("modal-dialog-open modal-dialog-no-scroll");
+
+			scrollTop = $(window).scrollTop();
+
+            if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) {
+                body.addClass("modal-dialog-open-ios");
+            }
+		},
+		terminate : function() {
+			$("body").removeClass("modal-dialog-open modal-dialog-no-scroll modal-dialog-open-ios");
+            if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) {
+            	$(window).scrollTop(scrollTop);
+            }
+		}
+	};
+
+	/**
+	 * Behavior that memorizes the focussed element when dialog is opened, and
+	 * returns focus to it when dialog is closed
+	 */
+	var returnFocusOnCloseBehavior = {
+		open : function(overlay, element, data) {
+			data.opener = document.activeElement;
+			if (data.options.validate) {
+				if (!data.opener || $(data.opener).is("body")) {
+					data.options.console.error("Error saving focused element when opening the modal, it is either none or body: ",
+							data.opener);
+				}
+			}
+		},
+		close : function(overlay, element, data) {
+			if (data.opener) {
+				try {
+					data.opener.focus();
+				} catch (error) {
+					if (data.options.validate) {
+						data.options.console.error(
+								"Error restoring focus after modal is closed. Attempted to set focus to element, but got an exception",
+								data.opener, error);
+					}
+					throw error;
+				}
+			}
+		}
+	}
+
+	/** Takes care of adding any necessary aria-related attributes to the dialog */
+	var addAriaAttributesBehavior = {
+		prepare : function(overlay, element, data) {
+			var content = overlay.find(CONTENT_SELECTOR);
+			var attrs = {
+				"role" : "dialog",
+				"aria-modal" : "true"
+			};
+
+			var title = element.find(".x-modal-title").first();
+			if (title.length > 0) {
+				attrs["aria-labelledby"] = getOrCreateId(title);
+			} else if (data.options.validate) {
+				data.options.console.error("No .x-modal-title element present in modal content: ", element.get(0));
+			}
+
+			var description = element.find(".x-modal-description").first();
+			if (description.length > 0) {
+				attrs["aria-describedby"] = getOrCreateId(description);
+			} else if (data.options.validate) {
+				data.options.console.warn("No .x-modal-description element present in modal content: ", element.get(0));
+			}
+
+			content.attr(attrs);
+		}
+	}
+
+	/** Closes the modal if the overlay is clicked or an escape key is pressed */
+	var closeOnOverlayClickOrEscapeBehavior = {
+		prepare : function(overlay, element, data) {
+			if (data.options.closeOnClickOutside) {
+				overlay.on("click.modal-dialog", function (event) {
+					if ($(event.target).closest(CONTENT_SELECTOR).length === 0) {
+						// clicked outside modal's content
+						findAndClickCloseButton(element);
+					}
+				});
+			}
+			if (data.options.closeOnEscape) {
+				overlay.on("keydown", function (event) {
+					if (event.which == 27) {
+						event.preventDefault();
+						event.stopPropagation();
+						findAndClickCloseButton(element);
+					}
+				});
+			}
+		},
+		open : function(overlay, element, data) {
+			if (data.options.validate && (data.options.closeOnClickOutside || data.options.closeOnEscape)) {
+				if (element.find(CLOSE_SELECTOR).filter(":visible").length === 0) {
+					data.options.console.error("Modal Dialog content does not contain a clickable element with class .x-modal-close."
+							+ " Clicking outside the modal or pressing ESC will have no effect");
+				}
+			}
+		}
+	};
+
+	/** Detects clicks on secondary close buttons (SENODARY_CLOSE_SELECTOR) and forwards the click to the primary close button */
+	var secondaryCloseButtonBehavior = {
+		prepare: function(overlay, element, data) {
+			overlay.on("click", SECONDARY_CLOSE_SELECTOR, function(event) {
+				event.preventDefault();
+				event.stopPropagation();
+				findAndClickCloseButton(element);
+			});
+		}
+	}
+
+	/** Traps focus inside the modal window. */
+	var trapFocusInsideModalBehavior = {
+		prepare : function(overlay, element, data) {
+			overlay.on("keydown", function(e) {
+				if (e.which === 9) { // tab
+					var container = $(e.target).closest(CONTENT_SELECTOR);
+					var focusables = findTabbable(container);
+					var firstFocusable = focusables.get(0);
+					var lastFocusable = focusables.get(focusables.length - 1);
+
+					if (!e.shiftKey && e.target === lastFocusable) {
+						e.preventDefault();
+						firstFocusable.focus();
+					}
+					if (e.shiftKey && e.target === firstFocusable) {
+						e.preventDefault();
+						lastFocusable.focus();
+					}
+				}
+			});
+
+			overlay.on("DOMNodeRemoved.modal-dialog", function(e) {
+				// handles focus transitions when nodes are removed, for example
+				// a node that has focus is removed via an ajax update
+				window.setTimeout(function() {
+					// needs to run in timeout because the event will get called
+					// with the node that is being removed as active
+					var active = $(document.activeElement);
+					if (active.closest(CONTENT_SELECTOR).length === 0) {
+						// focus has been moved to something outside the modal,
+						// refocus
+						focusDefaultFocusable(element);
+					}
+				}, 0);
+			});
+		},
+		close : function(overlay, element, data) {
+			overlay.off("DOMNodeRemoved.modal-dialog");
+		}
+
+	};
+
+	var focusDefaultOnOpeningBehavior = {
+		open : function(overlay, element, data) {
+			focusDefaultFocusable(element);
+		}
+	};
+
+	var sizingBehavior = {
+		prepare : function(overlay, element, data) {
+			if (data.options.maxWidth) {
+				overlay.find(CONTAINER_SELECTOR).css({
+					maxWidth : data.options.maxWidth
+				});
+			}
+			if(data.options.maxHeight) {
+				overlay.find(SCROLL_SELECTOR).css({
+					maxHeight: data.options.maxHeight
+				});
+			}
+		}
+	};
+
+	var defaultOptions = {
+		validate : false,
+		console : window.console,
+		maxWidth : null,
+		maxHeight: null, //"80vh"
+		closeOnClickOutside: false,
+		closeOnEscape: true
+	};
+
+	var behaviors = [ appendBodyClassBehavior, returnFocusOnCloseBehavior, closeOnOverlayClickOrEscapeBehavior, addAriaAttributesBehavior,
+			trapFocusInsideModalBehavior, focusDefaultOnOpeningBehavior, sizingBehavior, secondaryCloseButtonBehavior ];
+
+	//
+	// Entry Methods
+	//
+
+	window.wicket = window.wicket || {};
+	var ns = window.wicket.modal = {};
+
+	ns.open = function(element, options) {
+		options = $.extend({}, defaultOptions, options);
+		element = resolveDomNode(element);
+
+		var data = {
+			element : element,
+			options : options,
+		};
+
+		element.data(DATA_KEY, data);
+
+		var firstDialogOpened = $(document).find(OVERLAY_SELECTOR).length === 0;
+
+		if (firstDialogOpened) {
+			for (var i = 0; i < behaviors.length; i++) {
+				if (behaviors[i].initialize) {
+					behaviors[i].initialize();
+				}
+			}
+		}
+
+		data.contentParent = element.parent();
+
+		data.overlay = $(""//
+				+ "<div class='modal-dialog-overlay'>" //
+				+ "  <div class='modal-dialog'>" //
+				+ "    <div class='modal-dialog-scroll-area'>" //
+				+ "      <div class='modal-dialog-content' tabindex='0'>" //
+				+ "      </div>" //
+				+ "    </div>" //
+				+ "  </div>" //
+				+ "</div>");
+
+		for (var i = 0; i < behaviors.length; i++) {
+			if (behaviors[i].prepare) {
+				behaviors[i].prepare(data.overlay, element, data);
+			}
+		}
+
+		$("body").append(data.overlay);
+
+		element.appendTo(data.overlay.find(CONTENT_SELECTOR));
+
+		for (var i = 0; i < behaviors.length; i++) {
+			if (behaviors[i].open) {
+				behaviors[i].open(data.overlay, element, data);
+			}
+		}
+
+	}
+
+	ns.close = function(element) {
+		element = resolveDomNode(element);
+		var data = element.data(DATA_KEY);
+
+		for (var i = 0; i < behaviors.length; i++) {
+			if (behaviors[i].close) {
+				behaviors[i].close(data.overlay, element, data);
+			}
+		}
+
+		element.removeData(DATA_KEY);
+		element.appendTo(data.contentParent);
+		data.overlay.remove();
+
+		var lastDialogClosed = $(document).find("modal-dialog-overlay").length === 0;
+		if (lastDialogClosed) {
+			for (var i = 0; i < behaviors.length; i++) {
+				if (behaviors[i].terminate) {
+					behaviors[i].terminate(element);
+				}
+			}
+		}
+	}
+
+}(jQuery, window, document, console, undefined));
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialogReferences.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialogReferences.java
new file mode 100644
index 0000000..8be2792
--- /dev/null
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/ModalDialogReferences.java
@@ -0,0 +1,38 @@
+package org.apache.wicket.extensions.ajax.markup.html.modal;
+
+import java.util.List;
+
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.request.resource.CssResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.resource.JQueryPluginResourceReference;
+
+/**
+ * References for {@link ModalDialog}
+ * 
+ * @author Igor Vaynberg (ivaynberg)
+ */
+public class ModalDialogReferences
+{
+	public static final ResourceReference CSS = new CssResourceReference(
+		ModalDialogReferences.class, "ModalDialog.css");
+
+	public static final ResourceReference CSS_SKIN = new CssResourceReference(
+		ModalDialogReferences.class, "ModalDialog-skin.css");
+
+
+	public static final ResourceReference JS = new JQueryPluginResourceReference(
+		ModalDialogReferences.class, "ModalDialog.js")
+	{
+		@Override
+		public List<HeaderItem> getDependencies()
+		{
+			List<HeaderItem> deps = super.getDependencies();
+			deps.add(CssHeaderItem.forReference(CSS));
+			return deps;
+		}
+	};
+
+
+}