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