Java tutorial
/* * Copyright 2015-2016 Red Hat, Inc, and individual contributors. * * Licensed 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 * * https://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.jboss.hal.ballroom.wizard; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import com.google.common.collect.Iterables; import com.google.gwt.core.client.GWT; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.web.bindery.event.shared.HandlerRegistration; import com.google.web.bindery.event.shared.HandlerRegistrations; import elemental2.dom.HTMLButtonElement; import elemental2.dom.HTMLDivElement; import elemental2.dom.HTMLElement; import elemental2.dom.HTMLLIElement; import elemental2.dom.HTMLParagraphElement; import org.jboss.gwt.elemento.core.Elements; import org.jboss.hal.ballroom.Attachable; import org.jboss.hal.ballroom.PatternFly; import org.jboss.hal.ballroom.dialog.Modal.ModalOptions; import org.jboss.hal.resources.Constants; import org.jboss.hal.resources.Ids; import org.jboss.hal.resources.UIConstants; import static elemental2.dom.DomGlobal.document; import static org.jboss.gwt.elemento.core.Elements.*; import static org.jboss.gwt.elemento.core.EventType.bind; import static org.jboss.gwt.elemento.core.EventType.click; import static org.jboss.hal.ballroom.dialog.Modal.$; import static org.jboss.hal.resources.CSS.*; import static org.jboss.hal.resources.UIConstants.*; /** * General purpose wizard relying on a context for the common data and an enum representing the states of the different * steps. * * @param <C> The context * @param <S> The state enum */ public class Wizard<C, S extends Enum<S>> { private static final Constants CONSTANTS = GWT.create(Constants.class); private static final String SELECTOR_ID = HASH + Ids.HAL_WIZARD; private static final HTMLElement root; private static final HTMLElement titleElement; private static final HTMLElement closeIcon; private static final HTMLElement stepsNames; private static final HTMLElement mainContainer; private static final HTMLElement blankSlate; private static final HTMLButtonElement cancelButton; private static final HTMLButtonElement backButton; private static final HTMLButtonElement nextButton; private static final HTMLElement nextText; private static final HTMLElement nextIcon; private static boolean open; static { root = div().css(modal).id(Ids.HAL_WIZARD).attr(ROLE, "wizard") //NON-NLS .attr(TABINDEX, "-1").aria("labeledby", Ids.HAL_WIZARD_TITLE) .add(div().css(modalDialog, modalLg, wizardPf) .add(div().css(modalContent).add(div().css(modalHeader) .add(closeIcon = button().css(close).aria(UIConstants.LABEL, CONSTANTS.close()) .add(span().css(pfIcon("close")).aria(HIDDEN, TRUE)).get()) .add(titleElement = h(4).css(modalTitle).id(Ids.HAL_WIZARD_TITLE).get())) .add(div().css(modalBody, wizardPfBody, clearfix) .add(div().css(wizardPfSteps) .add(stepsNames = ul().css(wizardPfStepsIndicator).get())) .add(div().css(wizardPfRow) .add(mainContainer = div().css(wizardPfMain, wizardHalNoSidebar) .add(blankSlate = div().css(blankSlatePf).get()).get()))) .add(div().css(modalFooter, wizardPfFooter) .add(cancelButton = button().css(btn, btnDefault, btnCancel) .textContent(CONSTANTS.cancel()).get()) .add(backButton = button().css(btn, btnDefault) .add(span().css(fontAwesome("angle-left"))) .add(span().textContent(CONSTANTS.back())).get()) .add(nextButton = button().css(btn, btnPrimary) .add(nextText = span().textContent(CONSTANTS.next()).get()) .add(nextIcon = span().css(fontAwesome("angle-right")).get()) .get())))) .get(); document.body.appendChild(root); initEventHandler(); } public static boolean isOpen() { return open; } private static void initEventHandler() { $(SELECTOR_ID).on(UIConstants.SHOWN_MODAL, () -> Wizard.open = true); $(SELECTOR_ID).on(UIConstants.HIDDEN_MODAL, () -> Wizard.open = false); } private static void reset() { Elements.removeChildrenFrom(stepsNames); elemental2.dom.NodeList<elemental2.dom.Element> contents = mainContainer .querySelectorAll("." + wizardPfContents); Elements.stream(contents).forEach(mainContainer::removeChild); Elements.setVisible(blankSlate, false); } // ------------------------------------------------------ wizard instance private final C context; private final LinkedHashMap<S, WizardStep<C, S>> steps; private final LinkedHashMap<S, HTMLElement> stepElements; private final Map<S, HTMLElement> stepIndicators; private final HandlerRegistration handlerRegistration; private S initialState; private BackFunction<C, S> back; private NextFunction<C, S> next; private EnumSet<S> lastStates; private FinishCallback<C, S> finishCallback; private CancelCallback<C> cancelCallback; private boolean showsError; private boolean stayOpenAfterFinish; private boolean finishCanClose; private S state; private Wizard(Builder<C, S> builder) { this.context = builder.context; this.steps = new LinkedHashMap<>(builder.steps); this.steps.values().forEach(step -> step.init(this)); this.stepElements = new LinkedHashMap<>(); this.stepIndicators = new HashMap<>(); this.initialState = builder.initialState == null ? steps.keySet().iterator().next() : builder.initialState; this.back = builder.back; this.next = builder.next; this.lastStates = builder.lastStates == null ? EnumSet.of(Iterables.getLast(steps.keySet())) : builder.lastStates; this.finishCallback = builder.finishCallback; this.cancelCallback = builder.cancelCallback; this.showsError = false; this.stayOpenAfterFinish = builder.stayOpenAfterFinish; this.finishCanClose = false; reset(); Wizard.titleElement.textContent = builder.title; handlerRegistration = HandlerRegistrations.compose(bind(closeIcon, click, event -> onCancel()), bind(cancelButton, click, event -> onCancel()), bind(backButton, click, event -> onBack()), bind(nextButton, click, event -> onNext())); } // ------------------------------------------------------ public API /** * Opens the wizard and reset the state, context and UI. If you override this method please make sure to call * {@code super.show()} <em>before</em> you access or modify the context. */ public void show() { if (stepsNames.childElementCount == 0) { initSteps(); } for (WizardStep<C, S> step : steps.values()) { step.reset(context); } state = initialState; if (Wizard.open) { throw new IllegalStateException( "Another wizard is still open. Only one wizard can be open at a time. Please close the other wizard!"); } $(SELECTOR_ID).modal(ModalOptions.create(true)); $(SELECTOR_ID).modal("show"); PatternFly.initComponents(SELECTOR_ID); pushState(state); } public void showProgress(String title, SafeHtml text) { blankSlate.classList.remove(wizardPfComplete); blankSlate.classList.add(wizardPfProcess); Elements.removeChildrenFrom(blankSlate); blankSlate.appendChild(div().css(spinner, spinnerLg, blankSlatePfIcon).get()); blankSlate.appendChild(h(3).css(blankSlatePfMainAction).textContent(title).get()); blankSlate.appendChild(p().css(blankSlatePfSecondaryAction).innerHtml(text).get()); stepElements.values().forEach(element -> Elements.setVisible(element, false)); Elements.setVisible(blankSlate, true); backButton.disabled = true; nextButton.disabled = true; } public void showSuccess(String title, SafeHtml text) { showSuccess(title, text, null, null, true); } public void showSuccess(String title, SafeHtml text, boolean lastStep) { showSuccess(title, text, null, null, lastStep); } public void showSuccess(String title, SafeHtml text, CloseAction<C> closeAction) { showSuccess(title, text, null, null, closeAction, true); } public void showSuccess(String title, SafeHtml text, CloseAction<C> closeAction, boolean lastStep) { showSuccess(title, text, null, null, closeAction, lastStep); } public void showSuccess(String title, SafeHtml text, String successButton, SuccessAction<C> successAction) { showSuccess(title, text, successButton, successAction, true); } public void showSuccess(String title, SafeHtml text, String successButton, SuccessAction<C> successAction, boolean lastStep) { showSuccess(title, text, successButton, successAction, null, lastStep); } public void showSuccess(String title, SafeHtml text, String successButton, SuccessAction<C> successAction, CloseAction<C> closeAction, boolean lastStep) { blankSlate.classList.remove(wizardPfProcess); blankSlate.classList.add(wizardPfComplete); Elements.removeChildrenFrom(blankSlate); blankSlate.appendChild(div().css(wizardPfSuccessIcon).add(span().css(glyphicon("ok-circle"))).get()); blankSlate.appendChild(h(3).css(blankSlatePfMainAction).textContent(title).get()); blankSlate.appendChild(p().css(blankSlatePfSecondaryAction).innerHtml(text).get()); if (successButton != null && successAction != null) { blankSlate.appendChild( button().css(btn, btnLg, btnPrimary).textContent(successButton).on(click, event -> { successAction.execute(context); close(); }).get()); } stepElements.values().forEach(element -> Elements.setVisible(element, false)); Elements.setVisible(blankSlate, true); cancelButton.disabled = lastStep; backButton.disabled = lastStep; nextButton.disabled = false; if (lastStep) { nextText.textContent = CONSTANTS.close(); Elements.setVisible(nextIcon, false); } if (closeAction != null) { nextButton.onclick = event -> { closeAction.execute(context); return null; }; } else { nextButton.onclick = null; } finishCanClose = lastStep; } public void showError(String title, SafeHtml text) { showError(title, text, null, true); } public void showError(String title, SafeHtml text, boolean lastStep) { showError(title, text, null, lastStep); } public void showError(String title, SafeHtml text, String error) { showError(title, text, error, true); } public void showError(String title, SafeHtml text, String error, boolean lastStep) { blankSlate.classList.remove(wizardPfProcess); blankSlate.classList.add(wizardPfComplete); Elements.removeChildrenFrom(blankSlate); blankSlate.appendChild(div().css(wizardPfErrorIcon).add(span().css(glyphicon("remove-circle"))).get()); blankSlate.appendChild(h(3).css(blankSlatePfMainAction).textContent(title).get()); HTMLParagraphElement p = p().css(blankSlatePfSecondaryAction).innerHtml(text).get(); blankSlate.appendChild(p); if (error != null) { String id = Ids.uniqueId(); p.appendChild(a(HASH + id).css(marginLeft5).data(UIConstants.TOGGLE, UIConstants.COLLAPSE) .aria(UIConstants.EXPANDED, UIConstants.FALSE).aria(UIConstants.CONTROLS, id) .textContent(CONSTANTS.details()).get()); p.appendChild(div().css(collapse).id(id).aria(UIConstants.EXPANDED, UIConstants.FALSE) .add(pre().css(wizardHalErrorText).textContent(error)).get()); } stepElements.values().forEach(element -> Elements.setVisible(element, false)); Elements.setVisible(blankSlate, true); cancelButton.disabled = lastStep; backButton.disabled = lastStep; nextButton.disabled = !lastStep; if (lastStep) { nextText.textContent = CONSTANTS.close(); Elements.setVisible(nextIcon, false); } finishCanClose = lastStep; showsError = true; } public C getContext() { return context; } // ------------------------------------------------------ workflow @SuppressWarnings("unchecked") private void onCancel() { if (currentStep() instanceof AsyncStep) { ((AsyncStep<C>) currentStep()).onCancel(context, this::cancel); } else { if (currentStep().onCancel(context)) { cancel(); } } } private void cancel() { if (cancelCallback != null) { cancelCallback.onCancel(context); } close(); } @SuppressWarnings("unchecked") private void onBack() { if (currentStep() instanceof AsyncStep) { ((AsyncStep<C>) currentStep()).onBack(context, this::back); } else { if (currentStep().onBack(context)) { back(); } } finishCanClose = false; } private void back() { if (showsError) { pushState(state); } else { S previousState = back.back(context, state); if (previousState != null) { pushState(previousState); } } } @SuppressWarnings("unchecked") private void onNext() { if (finishCanClose) { // we're on the last step and have either seen a success or error message close(); } else { if (currentStep() instanceof AsyncStep) { ((AsyncStep<C>) currentStep()).onNext(context, this::next); } else { if (currentStep().onNext(context)) { next(); } } } } private void next() { S nextState = next.next(context, state); if (nextState != null) { pushState(nextState); } else { finish(); } } private void finish() { if (finishCallback != null) { finishCallback.onFinish(this, context); } if (!stayOpenAfterFinish) { close(); } } /** * Sets the current state to the specified state and updates the UI to reflect the current state. * * @param state the next state */ private void pushState(S state) { this.state = state; this.showsError = false; stepIndicators.forEach((s, element) -> { if (s == state) { element.classList.add(active); } else { element.classList.remove(active); } }); Elements.setVisible(blankSlate, false); stepElements.forEach((s, element) -> Elements.setVisible(element, s == state)); currentStep().onShow(context); cancelButton.disabled = false; backButton.disabled = state == initialState; nextButton.disabled = false; nextText.textContent = lastStates.contains(state) ? CONSTANTS.finish() : CONSTANTS.next(); Elements.setVisible(nextIcon, !lastStates.contains(state)); } private WizardStep<C, S> currentStep() { return steps.get(state); } // ------------------------------------------------------ private methods private void initSteps() { int index = 1; for (Map.Entry<S, WizardStep<C, S>> entry : steps.entrySet()) { S status = entry.getKey(); WizardStep<C, S> step = entry.getValue(); HTMLLIElement li = li().css(wizardPfStep) .add(a().add(span().css(wizardPfStepNumber).textContent(String.valueOf(index))) .add(span().css(wizardPfStepTitle).textContent(step.title))) .get(); stepIndicators.put(status, li); stepsNames.appendChild(li); HTMLDivElement wrapper = div().css(wizardPfContents).add(step).get(); step.attachables.forEach(Attachable::attach); Elements.setVisible(wrapper, false); mainContainer.appendChild(wrapper); stepElements.put(status, wrapper); index++; } } private void close() { handlerRegistration.removeHandler(); steps.values().forEach(step -> step.attachables.forEach(Attachable::detach)); $(SELECTOR_ID).modal("hide"); } // ------------------------------------------------------ inner classes @FunctionalInterface public interface BackFunction<C, S extends Enum<S>> { S back(C context, S currentState); } @FunctionalInterface public interface NextFunction<C, S extends Enum<S>> { S next(C context, S currentState); } /** * An action executed when the user clicks on the success button of the success page. */ @FunctionalInterface public interface SuccessAction<C> { void execute(C context); } /** * An action executed when the user clicks on the close button of the success page. */ @FunctionalInterface public interface CloseAction<C> { void execute(C context); } /** * A callback executed when the user finishes last step. * * @param <C> */ @FunctionalInterface public interface FinishCallback<C, S extends Enum<S>> { void onFinish(Wizard<C, S> wizard, C context); } /** * A callback executed whenever the user cancels the wizard. * * @param <C> */ @FunctionalInterface public interface CancelCallback<C> { void onCancel(C context); } // ------------------------------------------------------ wizard builder public static class Builder<C, S extends Enum<S>> { private final String title; private final C context; private final LinkedHashMap<S, WizardStep<C, S>> steps; private S initialState; private BackFunction<C, S> back; private NextFunction<C, S> next; private EnumSet<S> lastStates; private FinishCallback<C, S> finishCallback; private CancelCallback<C> cancelCallback; private boolean stayOpenAfterFinish; public Builder(String title, C context) { this.title = title; this.context = context; this.steps = new LinkedHashMap<>(); this.initialState = null; this.back = null; this.next = null; this.lastStates = null; this.finishCallback = null; this.cancelCallback = null; this.stayOpenAfterFinish = false; } public Builder<C, S> addStep(S state, WizardStep<C, S> step) { steps.put(state, step); return this; } public Builder<C, S> onBack(BackFunction<C, S> back) { this.back = back; return this; } public Builder<C, S> onNext(NextFunction<C, S> next) { this.next = next; return this; } public Builder<C, S> onFinish(FinishCallback<C, S> finishCallback) { this.finishCallback = finishCallback; return this; } public Builder<C, S> onCancel(CancelCallback<C> cancelCallback) { this.cancelCallback = cancelCallback; return this; } public Builder<C, S> stayOpenAfterFinish() { this.stayOpenAfterFinish = true; return this; } public Wizard<C, S> build() { if (steps.isEmpty()) { throw new IllegalStateException("No steps found for wizard '" + title + "'"); } if (back == null) { throw new IllegalStateException("No back function defined for wizard '" + title + "'"); } if (next == null) { throw new IllegalStateException("No next function defined for wizard '" + title + "'"); } return new Wizard<>(this); } } }