All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dominokit.domino.ui.stepper.Stepper Maven / Gradle / Ivy

There is a newer version: 2.0.4
Show newest version
/*
 * Copyright © 2019 Dominokit
 *
 * 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
 *
 *     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.dominokit.domino.ui.stepper;

import static java.util.Objects.nonNull;
import static org.dominokit.domino.ui.stepper.StepperStyles.*;
import static org.jboss.elemento.Elements.span;

import elemental2.dom.HTMLDivElement;
import elemental2.dom.Node;
import java.util.ArrayList;
import java.util.List;
import org.dominokit.domino.ui.animations.Transition;
import org.dominokit.domino.ui.button.Button;
import org.dominokit.domino.ui.grid.flex.FlexDirection;
import org.dominokit.domino.ui.grid.flex.FlexItem;
import org.dominokit.domino.ui.grid.flex.FlexLayout;
import org.dominokit.domino.ui.grid.flex.FlexWrap;
import org.dominokit.domino.ui.icons.Icons;
import org.dominokit.domino.ui.mediaquery.MediaQuery;
import org.dominokit.domino.ui.style.Color;
import org.dominokit.domino.ui.utils.BaseDominoElement;
import org.dominokit.domino.ui.utils.DominoElement;
import org.jboss.elemento.IsElement;

/**
 * A Wizard like component that can have multiple {@link Step}s while only one step can be activated
 * at a time
 *
 * @see Steppers sample
 */
public class Stepper extends BaseDominoElement {

  private final FlexItem content;
  private final FlexItem stepContentFlexItem;
  private final FlexItem stepFooter;
  private FlexLayout root = FlexLayout.create();
  private int activeStepNumber = 1;
  private StepperDirection direction = StepperDirection.HORIZONTAL;
  private List steps = new ArrayList<>();
  private Step activeStep;
  private Color barColor;

  private Transition activateStepTransition;
  private Transition deactivateStepTransition;
  private int activateStepTransitionDuration;
  private int deactivateStepTransitionDuration;

  private boolean forceVertical = false;
  private StepperDirection originalDirection = StepperDirection.HORIZONTAL;

  private StepStateColors stepStateColors;

  private final List stepStateChangeListeners = new ArrayList<>();
  private final List completeListeners = new ArrayList<>();

  private StepNumberRenderer stepNumberRenderer =
      new StepNumberRenderer() {
        @Override
        public Node inactiveElement(Step step, StepStateColors stepStateColors) {
          return DominoElement.of(span())
              .css(stepStateColors.inactive().getBackground())
              .setTextContent(step.getStepNumber() + "")
              .element();
        }

        @Override
        public Node activeElement(Step step, StepStateColors stepStateColors) {
          return DominoElement.of(span())
              .css(stepStateColors.active().getBackground())
              .setTextContent(step.getStepNumber() + "")
              .element();
        }

        @Override
        public Node errorElement(Step step, StepStateColors stepStateColors) {
          return DominoElement.of(span())
              .css(stepStateColors.error().getBackground())
              .setTextContent(step.getStepNumber() + "")
              .element();
        }

        @Override
        public Node completedElement(Step step, StepStateColors stepStateColors) {
          return DominoElement.of(span())
              .appendChild(Icons.ALL.check_mdi().size18())
              .css(stepStateColors.completed().getBackground())
              .element();
        }

        @Override
        public Node disabledElement(Step step, StepStateColors stepStateColors) {
          return DominoElement.of(span())
              .appendChild(Icons.ALL.block_helper_mdi().size18())
              .css(stepStateColors.disabled().getBackground())
              .element();
        }
      };

  public static Stepper create() {
    return new Stepper();
  }

  public Stepper() {
    init(this);
    stepStateColors =
        new StepStateColorsImpl(
            Color.GREY, Color.THEME, Color.RED, Color.GREEN, Color.GREY_LIGHTEN_2);
    barColor = Color.GREY;

    this.activateStepTransition = Transition.FADE_IN;
    this.deactivateStepTransition = Transition.FADE_OUT;
    this.activateStepTransitionDuration = 200;
    this.deactivateStepTransitionDuration = 200;

    root.css(direction.style, D_STEPPER).setWrap(FlexWrap.WRAP_TOP_TO_BOTTOM);

    stepContentFlexItem = FlexItem.create();
    stepFooter = FlexItem.create();
    content =
        FlexItem.create()
            .appendChild(
                FlexLayout.create()
                    .setDirection(FlexDirection.TOP_TO_BOTTOM)
                    .appendChild(stepContentFlexItem)
                    .appendChild(stepFooter));
    root.appendChild(content.setOrder(Integer.MAX_VALUE).css(STEP_CONTENT));

    setStepFooter(
        FlexLayout.create()
            .appendChild(
                FlexItem.create()
                    .appendChild(
                        Button.createPrimary(Icons.ALL.arrow_left_bold_mdi())
                            .setTextContent("Back")
                            .addClickListener(evt -> previous())))
            .appendChild(
                FlexItem.create()
                    .appendChild(
                        FlexItem.create()
                            .appendChild(
                                Button.createPrimary(Icons.ALL.arrow_right_bold_mdi())
                                    .setTextContent("Next")
                                    .addClickListener(evt -> next()))))
            .appendChild(
                FlexItem.create()
                    .appendChild(
                        FlexItem.create()
                            .appendChild(
                                Button.createPrimary(Icons.ALL.arrow_left_bold_mdi())
                                    .setTextContent("Complete")
                                    .addClickListener(evt -> completeActiveStep())))));

    setBarColor(barColor);

    MediaQuery.addOnSmallAndDownListener(
        () -> {
          forceVertical = true;
          originalDirection = this.direction;
          setDirection(this.direction);
        });
    MediaQuery.addOnMediumAndUpListener(
        () -> {
          forceVertical = false;
          setDirection(this.originalDirection);
        });
  }

  /**
   * @param stepNumberRenderer {@link StepNumberRenderer} to override the default one
   * @return same Stepper instance
   */
  public Stepper setStepNumberRenderer(StepNumberRenderer stepNumberRenderer) {
    if (nonNull(stepNumberRenderer)) {
      this.stepNumberRenderer = stepNumberRenderer;
      steps.forEach(Step::renderNumber);
    }
    return this;
  }

  /**
   * @param stepStateColors {@link StepStateColors} to override the default one
   * @return same Stepper instance
   */
  public Stepper setStepStateColors(StepStateColors stepStateColors) {
    if (nonNull(stepStateColors)) {
      this.stepStateColors = stepStateColors;
      steps.forEach(Step::renderNumber);
    }
    return this;
  }

  /**
   * @param footerElement {@link IsElement} that will be used a footer for all steps
   * @return same Stepper instance
   */
  public Stepper setStepFooter(IsElement footerElement) {
    return setStepFooter(footerElement.element());
  }

  /**
   * @param footerElement {@link Node} that will be used a footer for all steps
   * @return same Stepper instance
   */
  public Stepper setStepFooter(Node footerElement) {
    stepFooter.setContent(footerElement);
    return this;
  }

  /**
   * Marks the Stepper as completed and call the stepper complete handlers
   *
   * @return same Stepper instance
   */
  public Stepper complete() {
    completeListeners.forEach(listener -> listener.onComplete(this));
    return this;
  }

  /**
   * Marks the Stepper as completed and call the stepper complete handlers
   *
   * @param completeContent {@link IsElement} to show up in the stepper as a completed indicator of
   *     to finalize the stepper process
   * @return same Stepper instance
   */
  public Stepper complete(IsElement completeContent) {
    return complete(completeContent.element());
  }

  /**
   * Marks the Stepper as completed and call the stepper complete handlers
   *
   * @param completeContent {@link Node} to show up in the stepper as a completed indicator of to
   *     finalize the stepper process
   * @return same Stepper instance
   */
  public Stepper complete(Node completeContent) {
    complete();
    content.setContent(completeContent);
    getActiveStep().css("complete-content");
    return this;
  }

  /** @return the current active {@link Step} */
  public Step getCurrentStep() {
    return activeStep;
  }

  /**
   * Complete the current active step, if there is more steps this will also move the stepper to the
   * next enabled non-completed step
   *
   * @return same Stepper instance
   */
  public Stepper completeActiveStep() {
    this.activeStep.complete();
    return this;
  }

  /**
   * @param step {@link Step} to be added to this Stepper instance
   * @return same Stepper instance
   */
  public Stepper appendChild(Step step) {
    if (!steps.contains(step)) {
      step.setStepper(this);
    }
    if (!steps.isEmpty()) {
      steps.get(steps.size() - 1).removeCss(LAST_STEP);
      steps.get(steps.size() - 1).setFlexGrow(1);
    } else {
      this.activeStep = step;
      step.activate();
      step.setFlexGrow(1);
    }
    steps.add(step);
    step.setStepNumber(steps.indexOf(step) + 1);
    root.appendChild(step);
    step.css(LAST_STEP);

    return this;
  }

  /**
   * @param direction {@link StepperDirection}
   * @return same Stepper instance
   */
  public Stepper setDirection(StepperDirection direction) {

    if (forceVertical && StepperDirection.VERTICAL != direction) {
      this.originalDirection = this.direction;
      setDirection(StepperDirection.VERTICAL);
    } else {
      if (direction != this.direction) {
        root.removeCss(this.direction.style);
        root.css(direction.style);
      }
      root.setDirection(direction.flexDirection);
      this.direction = direction;
      if (StepperDirection.VERTICAL == direction) {
        this.content.setOrder(this.activeStepNumber + 1);
      } else {
        this.content.setOrder(Integer.MAX_VALUE);
      }
    }
    return this;
  }

  /**
   * Move the stepper to the next enabled non-completed step
   *
   * @return same Stepper instance
   */
  public Stepper next() {
    int activeStepIndex = this.steps.indexOf(activeStep);
    if (activeStepIndex < (this.steps.size() - 1)) {
      activateStep(getNextActiveStep());
    }
    return this;
  }

  /**
   * @param stepToActivate {@link Step} to be activated, the step should not disabled
   * @return same Stepper instance
   */
  public Stepper activateStep(Step stepToActivate) {
    if (StepState.DISABLED != stepToActivate.getState()) {
      if (!stepToActivate.equals(activeStep)) {
        this.activeStepNumber = (steps.indexOf(stepToActivate) * 2) + 1;
        this.activeStep.deactivate(
            step -> {
              this.activeStep = stepToActivate;
              this.activeStep.activate();
              if (StepperDirection.VERTICAL == this.direction) {
                this.content.setOrder(this.activeStepNumber + 1);
              }
            });
      }
    }

    return this;
  }

  /**
   * @param index int index of the step to be activated, the step should not disabled
   * @return same Stepper instance
   */
  public Stepper activateStep(int index) {
    if (index >= 0 && index < steps.size()) {
      activateStep(steps.get(index));
    }

    return this;
  }

  private Step getNextActiveStep() {
    int currentStepIndex = this.steps.indexOf(activeStep);
    for (int i = currentStepIndex + 1; i < this.steps.size(); i++) {
      Step nextStep = steps.get(i);
      if (StepState.DISABLED != nextStep.getState()) {
        return nextStep;
      }
    }
    return activeStep;
  }

  /**
   * Move the stepper back tp the previous step that is not disabled
   *
   * @return same Stepper instance
   */
  public Stepper previous() {
    int activeStepIndex = this.steps.indexOf(activeStep);
    if (activeStepIndex > 0) {
      Step prevActiveStep = getPrevActiveStep();
      if (!prevActiveStep.equals(activeStep)) {
        this.activeStep.deactivate(
            step -> {
              this.activeStep = prevActiveStep;
              this.activeStep.activate();
              this.activeStepNumber = (steps.indexOf(prevActiveStep) * 2) + 1;
              if (StepperDirection.VERTICAL == this.direction) {
                this.content.setOrder(this.activeStepNumber + 1);
              }
            });
      }
    }
    return this;
  }

  private Step getPrevActiveStep() {
    int currentStepIndex = this.steps.indexOf(activeStep);
    for (int i = currentStepIndex - 1; i >= 0; i--) {
      Step prevStep = steps.get(i);
      if (StepState.DISABLED != prevStep.getState()) {
        return prevStep;
      }
    }
    return activeStep;
  }

  /**
   * @param color {@link Color} the color of the bar connecting a step with the next step
   * @return same Stepper instance
   */
  public Stepper setBarColor(Color color) {
    steps.forEach(step -> step.setBarColor(color));
    this.content.styler(style -> style.setBorderColor(color.getHex()));
    this.barColor = color;
    return this;
  }

  /**
   * Adds a listener that listen to state changes of Stepper steps
   *
   * @param listener {@link StepStateChangeListener}
   * @return same Stepper instance
   */
  public Stepper addStateChangeListener(StepStateChangeListener listener) {
    if (nonNull(listener)) {
      this.stepStateChangeListeners.add(listener);
    }
    return this;
  }

  /**
   * @param listener {@link StepStateChangeListener}
   * @return same Stepper instance
   */
  public Stepper removeStateChangeListener(StepStateChangeListener listener) {
    if (nonNull(listener)) {
      this.stepStateChangeListeners.remove(listener);
    }
    return this;
  }

  /** @return List of all {@link StepStateChangeListener}s of this Stepper */
  public List getStepStateChangeListeners() {
    return stepStateChangeListeners;
  }

  /**
   * @param listener {@link StepperCompleteListener}
   * @return same Stepper instance
   */
  public Stepper addCompleteListener(StepperCompleteListener listener) {
    if (nonNull(listener)) {
      this.completeListeners.add(listener);
    }
    return this;
  }

  /**
   * @param listener {@link StepperCompleteListener}
   * @return same Stepper instance
   */
  public Stepper removeCompleteListener(StepperCompleteListener listener) {
    if (nonNull(listener)) {
      this.completeListeners.remove(listener);
    }
    return this;
  }

  /** @return a List of {@link StepperCompleteListener}s */
  public List getCompleteListeners() {
    return completeListeners;
  }

  /** @return the animation {@link Transition} currently used for activating a step */
  public Transition getActivateStepTransition() {
    return activateStepTransition;
  }

  /**
   * @param activateStepTransition {@link Transition} for the animation of activating a step
   * @return same Stepper instance
   */
  public Stepper setActivateStepTransition(Transition activateStepTransition) {
    if (nonNull(activateStepTransition)) {
      this.activateStepTransition = activateStepTransition;
    }
    return this;
  }

  /** @return the animation {@link Transition} currently used for deactivating a step */
  public Transition getDeactivateStepTransition() {
    return deactivateStepTransition;
  }

  /**
   * @param deactivateStepTransition {@link Transition} for the animation of deactivating a step
   * @return same Stepper instance
   */
  public Stepper setDeactivateStepTransition(Transition deactivateStepTransition) {
    if (nonNull(deactivateStepTransition)) {
      this.deactivateStepTransition = deactivateStepTransition;
    }
    return this;
  }

  /** @return int duration in milli-seconds for activating a step animation */
  public int getActivateStepTransitionDuration() {
    return activateStepTransitionDuration;
  }

  /**
   * @param activateStepTransitionDuration int duration in milli-seconds for activating a step
   *     animation
   * @return same Stepper instance
   */
  public Stepper setActivateStepTransitionDuration(int activateStepTransitionDuration) {
    this.activateStepTransitionDuration = activateStepTransitionDuration;
    return this;
  }

  /** @return int duration in milli-seconds for deactivating a step animation */
  public int getDeactivateStepTransitionDuration() {
    return deactivateStepTransitionDuration;
  }

  /**
   * @param deactivateStepTransitionDuration int duration in milli-seconds for deactivating a step
   *     animation
   * @return same Stepper instance
   */
  public Stepper setDeactivateStepTransitionDuration(int deactivateStepTransitionDuration) {
    this.deactivateStepTransitionDuration = deactivateStepTransitionDuration;
    return this;
  }

  /** {@inheritDoc} */
  @Override
  public HTMLDivElement element() {
    return root.element();
  }

  /** @return List of all {@link Step}s */
  public List getSteps() {
    return steps;
  }

  /** @return the {@link FlexItem} that wraps the active step content */
  public FlexItem getStepContentFlexItem() {
    return stepContentFlexItem;
  }

  /** @return the {@link StepStateColors} */
  public StepStateColors getStepStateColors() {
    return this.stepStateColors;
  }

  /** @return the {@link StepNumberRenderer} */
  public StepNumberRenderer getStepNumberRenderer() {
    return this.stepNumberRenderer;
  }

  /** @return the current active {@link Step} */
  public Step getActiveStep() {
    return this.activeStep;
  }

  /**
   * Activates he first step and reset all steps to the initial state
   *
   * @return
   */
  public Stepper reset() {
    steps.forEach(Step::reset);
    activateStep(0);
    return this;
  }

  /** An enum of possible Stepper directions */
  public enum StepperDirection {
    /** The steps in the Stepper header will be aligned Horizontally */
    HORIZONTAL(FlexDirection.LEFT_TO_RIGHT, HORIZONTAL_STEPPER),
    /** The steps in the Stepper header will be aligned Veritically */
    VERTICAL(FlexDirection.TOP_TO_BOTTOM, VERTICAL_STEPPER);

    private FlexDirection flexDirection;
    private String style;

    /**
     * @param flexDirection {@link FlexDirection} of the Stepper Flex layout
     * @param style String css class name for the direction
     */
    StepperDirection(FlexDirection flexDirection, String style) {
      this.flexDirection = flexDirection;
      this.style = style;
    }

    /** @return the FlexDirection */
    public FlexDirection getFlexDirection() {
      return flexDirection;
    }

    /** @return String css class name */
    public String getStyle() {
      return style;
    }
  }

  /** A function to implement listeners to be called whenever the step state is changed */
  public interface StepStateChangeListener {
    /**
     * @param oldState {@link StepState}
     * @param step {@link Step} new state can be obtained from the this step instance
     * @param stepper {@link Stepper} the step belongs to
     */
    void onStateChanged(StepState oldState, Step step, Stepper stepper);
  }

  /**
   * An interface that can implemented to provide different colors for different {@link Step} states
   * other than the default colors
   */
  public interface StepStateColors {
    /** @return the {@link Color} for inactive steps */
    Color inactive();

    /** @return the {@link Color} for active steps */
    Color active();

    /** @return the {@link Color} for steps that has validation errors */
    Color error();

    /** @return the {@link Color} for completed steps */
    Color completed();

    /** @return the {@link Color} for disabled steps */
    Color disabled();
  }

  private static final class StepStateColorsImpl implements StepStateColors {
    private Color inactive;
    private Color active;
    private Color error;
    private Color completed;
    private Color disabled;

    public StepStateColorsImpl(
        Color inactive, Color active, Color error, Color completed, Color disabled) {
      this.inactive = inactive;
      this.active = active;
      this.error = error;
      this.completed = completed;
      this.disabled = disabled;
    }

    public void setInactive(Color inactive) {
      this.inactive = inactive;
    }

    public void setActive(Color active) {
      this.active = active;
    }

    public void setError(Color error) {
      this.error = error;
    }

    public void setCompleted(Color completed) {
      this.completed = completed;
    }

    public void setDisabled(Color disabled) {
      this.disabled = disabled;
    }

    @Override
    public Color inactive() {
      return inactive;
    }

    @Override
    public Color active() {
      return active;
    }

    @Override
    public Color error() {
      return error;
    }

    @Override
    public Color completed() {
      return completed;
    }

    @Override
    public Color disabled() {
      return disabled;
    }
  }

  /** An interface to provide a different implementation for rendering steps numbers */
  public interface StepNumberRenderer {
    /**
     * Renders the number for inactive steps
     *
     * @param step {@link Step} we are rendering the number for
     * @param stepStateColors {@link StepStateColors}
     * @return the {@link Node} to be used as the step number element
     */
    Node inactiveElement(Step step, StepStateColors stepStateColors);

    /**
     * Renders the number for active steps
     *
     * @param step {@link Step} we are rendering the number for
     * @param stepStateColors {@link StepStateColors}
     * @return the {@link Node} to be used as the step number element
     */
    Node activeElement(Step step, StepStateColors stepStateColors);

    /**
     * Renders the number for steps with errors
     *
     * @param step {@link Step} we are rendering the number for
     * @param stepStateColors {@link StepStateColors}
     * @return the {@link Node} to be used as the step number element
     */
    Node errorElement(Step step, StepStateColors stepStateColors);

    /**
     * Renders the number for completed steps
     *
     * @param step {@link Step} we are rendering the number for
     * @param stepStateColors {@link StepStateColors}
     * @return the {@link Node} to be used as the step number element
     */
    Node completedElement(Step step, StepStateColors stepStateColors);

    /**
     * Renders the number for disabled steps
     *
     * @param step {@link Step} we are rendering the number for
     * @param stepStateColors {@link StepStateColors}
     * @return the {@link Node} to be used as the step number element
     */
    Node disabledElement(Step step, StepStateColors stepStateColors);
  }

  /** An enum to list the {@link Step} possible states */
  public enum StepState {
    ACTIVE(STEP_ACTIVE),
    INACTIVE(STEP_INACTIVE),
    ERROR(STEP_ERROR),
    COMPLETED(STEP_COMPLETED),
    DISABLED(STEP_DISABLED);

    private String style;

    /**
     * @param style String css class name for the state, the css class name will be applied to the
     *     step when it is in the state
     */
    StepState(String style) {
      this.style = style;
    }

    /**
     * This method will be called whenever the Step state is changed, the method will use the {@link
     * StepNumberRenderer} to render the step number based on the new step state
     *
     * @param step {@link Step} that has it is state changed
     * @param colors {@link StepStateColors}
     * @param stepNumberRenderer {@link StepNumberRenderer}
     * @return the {@link Node} to be used as the step number element
     */
    public Node render(Step step, StepStateColors colors, StepNumberRenderer stepNumberRenderer) {
      switch (step.getState()) {
        case ACTIVE:
          return stepNumberRenderer.activeElement(step, colors);
        case ERROR:
          return stepNumberRenderer.errorElement(step, colors);
        case COMPLETED:
          return stepNumberRenderer.completedElement(step, colors);
        case DISABLED:
          return stepNumberRenderer.disabledElement(step, colors);
        case INACTIVE:
        default:
          return stepNumberRenderer.inactiveElement(step, colors);
      }
    }

    /** @return String css class name */
    public String getStyle() {
      return style;
    }
  }

  /** A function to implement logic that will be called when a step is marked as completed */
  public interface StepperCompleteListener {
    /** @param stepper {@link Stepper} that the completed step belongs to */
    void onComplete(Stepper stepper);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy