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:17 UTC

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

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;
+		}
+	};
+
+
+}