///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 3 of the License. // // This community edition is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General // Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, see http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.web.dialog; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.markup.html.form.AjaxButton; import org.apache.wicket.feedback.ComponentFeedbackMessageFilter; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.projectforge.web.core.NavTopPanel; import org.projectforge.web.wicket.CsrfTokenHandler; import org.projectforge.web.wicket.WicketUtils; import org.projectforge.web.wicket.bootstrap.GridBuilder; import org.projectforge.web.wicket.components.SingleButtonPanel; import org.projectforge.web.wicket.flowlayout.MyComponentsRepeater; import de.micromata.wicket.ajax.AjaxCallback; import de.micromata.wicket.ajax.AjaxFormSubmitCallback; /** * Base component for the ProjectForge modal dialogs.<br/> * This dialog is modal.<br/> * * @author Johannes Unterstein (j.unterstein@micromata.de) * @author Kai Reinhard (k.reinhard@micromata.de) * */ public abstract class ModalDialog extends Panel { private static final long serialVersionUID = 4235521713603821639L; protected GridBuilder gridBuilder; protected final WebMarkupContainer mainContainer, mainSubContainer, gridContentContainer, buttonBarContainer; private boolean escapeKeyEnabled = true; private String closeButtonLabel; private SingleButtonPanel closeButtonPanel; private boolean showCancelButton; private boolean bigWindow; private boolean draggable = true; private Boolean resizable; private boolean lazyBinding; private WebMarkupContainer titleContainer; private Label titleLabel; protected Form< ? > form; protected FeedbackPanel formFeedback; private final IModel<Boolean> useCloseHandler = Model.of(false); private final AjaxEventBehavior closeBehavior; /** * If true, a GridBuilder is automatically available. */ protected boolean autoGenerateGridBuilder = true; /** * List to create action buttons in the desired order before creating the RepeatingView. */ protected MyComponentsRepeater<Component> actionButtons; /** * Cross site request forgery token. */ protected CsrfTokenHandler csrfTokenHandler; /** * @param id */ @SuppressWarnings("serial") public ModalDialog(final String id) { super(id); actionButtons = new MyComponentsRepeater<Component>("actionButtons"); mainContainer = new WebMarkupContainer("mainContainer"); add(mainContainer.setOutputMarkupId(true)); mainContainer.add(mainSubContainer = new WebMarkupContainer("mainSubContainer")); gridContentContainer = new WebMarkupContainer("gridContent"); gridContentContainer.setOutputMarkupId(true); buttonBarContainer = new WebMarkupContainer("buttonBar"); buttonBarContainer.setOutputMarkupId(true); closeBehavior = new AjaxEventBehavior("hidden.bs.modal") { @Override protected void onEvent(final AjaxRequestTarget target) { handleCloseEvent(target); } @Override protected void updateAjaxAttributes(final AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); attributes.setEventPropagation(AjaxRequestAttributes.EventPropagation.BUBBLE); } }; } /** * @see org.apache.wicket.Component#onInitialize() */ @Override protected void onInitialize() { super.onInitialize(); if (bigWindow == true) { mainSubContainer.add(AttributeModifier.append("class", "big-modal")); } if (useCloseHandler.getObject() == true) { mainContainer.add(closeBehavior); } } /** * Sets also draggable to false. Appends css class big-modal. */ public ModalDialog setBigWindow() { bigWindow = true; draggable = false; return this; } /** * Only the div panel of the modal dialog is rendered without buttons and content. Default is false. * @return this for chaining. */ public ModalDialog setLazyBinding() { this.lazyBinding = true; mainSubContainer.setVisible(false); return this; } public void bind(final AjaxRequestTarget target) { actionButtons.render(); mainSubContainer.setVisible(true); target.appendJavaScript(getJavaScriptAction()); } /** * @return true if no lazy binding was used or bind() was already called. */ public boolean isBound() { return mainSubContainer.isVisible(); } /** * @param draggable the draggable to set (default is true). * @return this for chaining. */ public ModalDialog setDraggable(final boolean draggable) { this.draggable = draggable; return this; } /** * @param resizable the resizable to set (default is true for bigWindows, otherwise false). * @return this for chaining. */ public ModalDialog setResizable(final boolean resizable) { this.resizable = resizable; return this; } /** * Display the cancel button. * @return this for chaining. */ public ModalDialog setShowCancelButton() { this.showCancelButton = true; return this; } /** * @param escapeKeyEnabled the keyboard to set (default is true). * @return this for chaining. */ public ModalDialog setEscapeKeyEnabled(final boolean escapeKeyEnabled) { this.escapeKeyEnabled = escapeKeyEnabled; return this; } /** * Close is used as default: * @param closeButtonLabel the closeButtonLabel to set * @return this for chaining. */ public ModalDialog setCloseButtonLabel(final String closeButtonLabel) { this.closeButtonLabel = closeButtonLabel; return this; } /** * Should be called directly after {@link #init()}. * @param tooltipTitle * @param tooltipContent * @see WicketUtils#addTooltip(Component, IModel, IModel) */ public ModalDialog setCloseButtonTooltip(final IModel<String> tooltipTitle, final IModel<String> tooltipContent) { WicketUtils.addTooltip(this.closeButtonPanel.getButton(), tooltipTitle, tooltipContent); return this; } public ModalDialog wantsNotificationOnClose() { setUseCloseHandler(true); return this; } public String getMainContainerMarkupId() { return mainContainer.getMarkupId(true); } @Override public void renderHead(final IHeaderResponse response) { super.renderHead(response); if (lazyBinding == false) { final String script = getJavaScriptAction(); response.render(OnDomReadyHeaderItem.forScript(script)); } } private String getJavaScriptAction() { final StringBuffer script = new StringBuffer(); script.append("$('#").append(getMainContainerMarkupId()).append("').modal({keyboard: ").append(escapeKeyEnabled) .append(", show: false });"); final boolean isResizable = (resizable == null && bigWindow == true) || Boolean.TRUE.equals(resizable) == true; if (draggable == true || isResizable == true) { script.append(" $('#").append(getMainContainerMarkupId()).append("')"); } if (draggable == true) { script.append(".draggable()"); } if (isResizable) { script.append(".resizable({ alsoResize: '#") .append(getMainContainerMarkupId()) // max-height of .modal-body is 600px, need to enlarge this setting for resizing. .append( ", .modal-body', resize: function( event, ui ) {$('.modal-body').css('max-height', '4000px');}, minWidth: 300, minHeight: 200 })"); } if (useCloseHandler.getObject()) { script.append(";$('#").append(getMainContainerMarkupId()).append("').on('hidden', function () { ") .append(" Wicket.Ajax.ajax({'u':'").append(closeBehavior.getCallbackUrl()).append("','c':'").append(getMainContainerMarkupId()) .append("'});").append("})"); } return script.toString(); } public ModalDialog open(final AjaxRequestTarget target) { target.appendJavaScript("$('#" + getMainContainerMarkupId() + "').modal('show');"); return this; } public void close(final AjaxRequestTarget target) { csrfTokenHandler.onSubmit(); target.appendJavaScript("$('#" + getMainContainerMarkupId() + "').modal('hide');"); } /** * Add the content to the AjaxRequestTarget if the content is changed. * @param target * @return this for chaining. */ public ModalDialog addContent(final AjaxRequestTarget target) { target.add(gridContentContainer); return this; } /** * Add the button bar to the AjaxRequestTarget if the buttons or their visibility are changed. * @param target * @return this for chaining. */ public ModalDialog addButtonBar(final AjaxRequestTarget target) { target.add(buttonBarContainer); return this; } /** * @param target * @return this for chaining. */ public ModalDialog addTitleLabel(final AjaxRequestTarget target) { target.add(titleLabel); return this; } public abstract void init(); /** * @param title * @return this for chaining. */ public ModalDialog setTitle(final String title) { return setTitle(Model.of(title)); } /** * @param title * @return this for chaining. */ public ModalDialog setTitle(final IModel<String> title) { titleContainer = new WebMarkupContainer("titleContainer"); mainSubContainer.add(titleContainer.setOutputMarkupId(true)); titleContainer.add(titleLabel = new Label("titleText", title)); titleLabel.setOutputMarkupId(true); return this; } /** * The gridContentContainer is cleared (all child elements are removed). This is useful for Ajax dialogs with dynamic content (see * {@link NavTopPanel} for an example). * @return */ public ModalDialog clearContent() { gridContentContainer.removeAll(); if (autoGenerateGridBuilder == true) { gridBuilder = new GridBuilder(gridContentContainer, "flowform"); } initFeedback(gridContentContainer); return this; } @SuppressWarnings("serial") protected void init(final Form< ? > form) { this.form = form; csrfTokenHandler = new CsrfTokenHandler(form); mainSubContainer.add(form); form.add(gridContentContainer); form.add(buttonBarContainer); if (showCancelButton == true) { final SingleButtonPanel cancelButton = appendNewAjaxActionButton(new AjaxCallback() { @Override public void callback(final AjaxRequestTarget target) { csrfTokenHandler.onSubmit(); onCancelButtonSubmit(target); close(target); } }, getString("cancel"), SingleButtonPanel.CANCEL); cancelButton.getButton().setDefaultFormProcessing(false); } closeButtonPanel = appendNewAjaxActionButton(new AjaxFormSubmitCallback() { @Override public void callback(final AjaxRequestTarget target) { csrfTokenHandler.onSubmit(); if (onCloseButtonSubmit(target)) { close(target); } } @Override public void onError(final AjaxRequestTarget target, final Form< ? > form) { csrfTokenHandler.onSubmit(); ModalDialog.this.onError(target, form); } }, closeButtonLabel != null ? closeButtonLabel : getString("close"), SingleButtonPanel.NORMAL); buttonBarContainer.add(actionButtons.getRepeatingView()); form.setDefaultButton(closeButtonPanel.getButton()); if (autoGenerateGridBuilder == true) { gridBuilder = new GridBuilder(gridContentContainer, "flowform"); } initFeedback(gridContentContainer); } private void initFeedback(final WebMarkupContainer container) { if (formFeedback == null) { formFeedback = new FeedbackPanel("formFeedback", new ComponentFeedbackMessageFilter(form)); formFeedback.setOutputMarkupId(true); formFeedback.setOutputMarkupPlaceholderTag(true); } container.add(formFeedback); } protected void ajaxError(final String error, final AjaxRequestTarget target) { csrfTokenHandler.onSubmit(); form.error(error); target.add(formFeedback); } /** * Called if {@link #wantsNotificationOnClose()} was chosen and the dialog is closed (by pressing esc, clicking outside or clicking the * upper right cross). * @param target */ protected void handleCloseEvent(final AjaxRequestTarget target) { csrfTokenHandler.onSubmit(); } /** * Called if user hit the cancel button. * @param target */ protected void onCancelButtonSubmit(final AjaxRequestTarget target) { } /** * Called if user hit the close button. * * @param target * * @return true if the dialog can be close, false if errors occured. */ protected boolean onCloseButtonSubmit(final AjaxRequestTarget target) { return true; } protected void onError(final AjaxRequestTarget target, final Form< ? > form) { } /** * @see org.apache.wicket.Component#onBeforeRender() */ @Override protected void onBeforeRender() { super.onBeforeRender(); if (lazyBinding == false) { actionButtons.render(); } } public String getFormId() { return "form"; } public SingleButtonPanel appendNewAjaxActionButton(final AjaxCallback ajaxCallback, final String label, final String... classnames) { final SingleButtonPanel result = addNewAjaxActionButton(ajaxCallback, label, classnames); this.actionButtons.add(result); return result; } public SingleButtonPanel prependNewAjaxActionButton(final AjaxCallback ajaxCallback, final String label, final String... classnames) { return insertNewAjaxActionButton(ajaxCallback, 0, label, classnames); } /** * @param ajaxCallback * @param position 0 is the first position. * @param label * @param classnames * @return */ public SingleButtonPanel insertNewAjaxActionButton(final AjaxCallback ajaxCallback, final int position, final String label, final String... classnames) { final SingleButtonPanel result = addNewAjaxActionButton(ajaxCallback, label, classnames); this.actionButtons.add(position, result); return result; } private SingleButtonPanel addNewAjaxActionButton(final AjaxCallback ajaxCallback, final String label, final String... classnames) { final AjaxButton button = new AjaxButton("button", form) { private static final long serialVersionUID = -5306532706450731336L; @Override protected void onSubmit(final AjaxRequestTarget target, final Form< ? > form) { csrfTokenHandler.onSubmit(); ajaxCallback.callback(target); } @Override protected void onError(final AjaxRequestTarget target, final Form< ? > form) { if (ajaxCallback instanceof AjaxFormSubmitCallback) { ((AjaxFormSubmitCallback) ajaxCallback).onError(target, form); } } }; final SingleButtonPanel buttonPanel = new SingleButtonPanel(this.actionButtons.newChildId(), button, label, classnames); buttonPanel.add(button); return buttonPanel; } /** * @return the mainContainer */ public WebMarkupContainer getMainContainer() { return mainContainer; } /** * Sets whether the close handler is used or not. Default is false. * * @param useCloseHandler True if close handler should be used * @return This */ public final ModalDialog setUseCloseHandler(final boolean useCloseHandler) { this.useCloseHandler.setObject(useCloseHandler); return this; } }