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

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

There is a newer version: 2.0.3
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.HTMLElement;
import elemental2.dom.Node;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.dominokit.domino.ui.animations.Animation;
import org.dominokit.domino.ui.forms.validations.ValidationResult;
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.style.Color;
import org.dominokit.domino.ui.utils.BaseDominoElement;
import org.dominokit.domino.ui.utils.DominoElement;
import org.dominokit.domino.ui.utils.HasValidation;
import org.jboss.elemento.IsElement;

/** A component that is a single step inside a {@link Stepper} */
public class Step extends BaseDominoElement implements HasValidation {

  private final DominoElement titleSpan;
  private final DominoElement descriptionSpan;
  private final DominoElement horizontalBarSpan;
  private final DominoElement verticalBarSpan;
  private final FlexItem errorMessagesFlexItem;
  private final FlexLayout header;
  private final DominoElement content;
  private Stepper stepper;
  private final FlexItem root = FlexItem.create().css(STEP_HEADER);

  private int stepNumber;
  private Color barColor = Color.GREY;

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

  private Stepper.StepState state;
  private Stepper.StepState nonErrorState;
  private FlexItem stepNumberFlexItem;
  private Stepper.StepState initialState;

  /**
   * @param title String title of the step
   * @return new Step instance
   */
  public static Step create(String title) {
    return new Step(title);
  }

  /**
   * @param title String title of the step
   * @param description String to describe the step that show up under the title
   * @return new Step instance
   */
  public static Step create(String title, String description) {
    return new Step(title, description);
  }

  /**
   * @param title String title of the step
   * @param description String to describe the step that show up under the title
   * @param initialState {@link Stepper.StepState} to be used by default as the step state
   * @return new Step instance
   */
  public static Step create(String title, String description, Stepper.StepState initialState) {
    return new Step(title, description, initialState);
  }

  /**
   * @param title String title of the step
   * @param description String to describe the step that show up under the title
   * @param initialState {@link Stepper.StepState} to be used by default as the step state
   */
  public Step(String title, String description, Stepper.StepState initialState) {
    init(this);
    this.initialState = initialState;
    this.state = Stepper.StepState.INACTIVE;
    this.nonErrorState = Stepper.StepState.INACTIVE;
    this.stepStateChangeListeners = new ArrayList<>();
    this.titleSpan = DominoElement.of(span());
    this.descriptionSpan = DominoElement.of(span()).hide();
    this.setDescription(description);
    this.horizontalBarSpan = DominoElement.of(span()).css(barColor.getBackground());
    this.verticalBarSpan = DominoElement.of(span()).css(barColor.getBackground());
    this.header = FlexLayout.create();
    this.content = DominoElement.div();

    this.errorMessagesFlexItem = FlexItem.create();
    this.stepNumberFlexItem = FlexItem.create();
    this.root.appendChild(
        header
            .appendChild(
                FlexItem.create()
                    .appendChild(
                        FlexLayout.create()
                            .css(STEP_NUMBER_CNTR)
                            .setDirection(FlexDirection.TOP_TO_BOTTOM)
                            .appendChild(stepNumberFlexItem.css(STEP_NUMBER))
                            .appendChild(
                                FlexItem.create()
                                    .setFlexGrow(1)
                                    .css(STEP_VERTICAL_BAR)
                                    .appendChild(verticalBarSpan))))
            .appendChild(
                FlexItem.create()
                    .css(STEP_TITLE_CNTR)
                    .setFlexGrow(1)
                    .appendChild(
                        FlexLayout.create()
                            .css(STEP_MAIN_TITLE_CNTR)
                            .setDirection(FlexDirection.TOP_TO_BOTTOM)
                            .appendChild(
                                FlexItem.create()
                                    .appendChild(
                                        FlexLayout.create()
                                            .appendChild(
                                                FlexItem.create()
                                                    .css(STEP_TITLE)
                                                    .appendChild(titleSpan.setTextContent(title)))
                                            .appendChild(
                                                FlexItem.create()
                                                    .css(STEP_HORIZONTAL_BAR)
                                                    .setFlexGrow(1)
                                                    .appendChild(horizontalBarSpan))))
                            .appendChild(
                                FlexItem.create()
                                    .setFlexGrow(1)
                                    .css(STEP_DESCRIPTION)
                                    .appendChild(descriptionSpan))
                            .appendChild(errorMessagesFlexItem.setFlexGrow(1).css(STEP_ERRORS)))));

    this.root.appendChild(header);
  }

  /**
   * @param title String title of the step
   * @param description String to describe the step that show up under the title
   */
  public Step(String title, String description) {
    this(title, description, Stepper.StepState.INACTIVE);
  }

  /** @param title String title of the step */
  public Step(String title) {
    this(title, null);
  }

  /**
   * @param title String new title for the step
   * @return same Step instance
   */
  public Step setTitle(String title) {
    this.titleSpan.setTextContent(title);
    return this;
  }

  /** @return String */
  public String getTitle() {
    return titleSpan.element().textContent;
  }

  /**
   * @param description String new description
   * @return same Step instance
   */
  public Step setDescription(String description) {
    if (nonNull(description) && !description.isEmpty()) {
      this.descriptionSpan.setTextContent(description);
      this.descriptionSpan.show();
    } else {
      this.descriptionSpan.setTextContent("");
      this.descriptionSpan.hide();
    }
    return this;
  }

  /** @return String */
  public String getDescription() {
    return this.descriptionSpan.getTextContent();
  }

  /** this will append the element to the step content element {@inheritDoc} */
  public Step appendChild(IsElement element) {
    this.content.appendChild(element);
    return this;
  }

  /** this will append the element to the step content element {@inheritDoc} */
  public Step appendChild(Node node) {
    this.content.appendChild(node);
    return this;
  }

  /**
   * Change the order of the step in the stepper
   *
   * @param stepNumber int
   * @return same Step instance
   */
  public Step setStepNumber(int stepNumber) {
    this.stepNumber = stepNumber;
    renderNumber();
    this.root.setOrder(stepNumber + stepNumber - 1);
    return this;
  }

  /** @return int step number in the stepper */
  public int getStepNumber() {
    return this.stepNumber;
  }

  /**
   * Make the step the current active step in the stepper, this show the step content and hide other
   * steps content, this will put the step in {@link Stepper.StepState#ACTIVE}
   *
   * @return same Step instance
   */
  Step activate() {
    if (stepper.getSteps().isEmpty()) {
      this.stepper.getStepContentFlexItem().setContent(this.content);
      setState(Stepper.StepState.ACTIVE);
    } else {
      this.content.hide();
      this.stepper.getStepContentFlexItem().setContent(this.content);
      Animation.create(this.content)
          .duration(stepper.getActivateStepTransitionDuration())
          .transition(stepper.getActivateStepTransition())
          .beforeStart(element -> this.content.show())
          .callback(
              element -> {
                setState(Stepper.StepState.ACTIVE);
              })
          .animate();
    }
    return this;
  }

  private void setState(Stepper.StepState state, boolean forceState) {
    if ((this.state != Stepper.StepState.DISABLED || forceState)
        && (getErrors().isEmpty() || forceState)) {
      Stepper.StepState oldState = this.state;
      this.removeCss(this.state.getStyle());
      this.state = state;
      this.css(this.state.getStyle());
      if (Stepper.StepState.ERROR != state) {
        this.nonErrorState = state;
      }
      if (nonNull(stepper)) {
        renderNumber();
        stepStateChangeListeners.forEach(
            listener -> listener.onStateChanged(oldState, this, this.stepper));
        stepper
            .getStepStateChangeListeners()
            .forEach(listener -> listener.onStateChanged(oldState, this, this.stepper));
      }
    }
  }

  private void setState(Stepper.StepState state) {
    setState(state, false);
  }

  /** renders the step number based on the step state */
  void renderNumber() {
    if (nonNull(stepper)) {
      this.stepNumberFlexItem.setContent(
          this.state.render(this, stepper.getStepStateColors(), stepper.getStepNumberRenderer()));
    }
  }

  /**
   * deactivate the step and call the provided handler, this will put the step in {@link
   * Stepper.StepState#INACTIVE}
   *
   * @param handler Consumer of Step
   * @return same Step instance
   */
  Step deactivate(Consumer handler) {
    Animation.create(this.content)
        .duration(stepper.getDeactivateStepTransitionDuration())
        .transition(stepper.getDeactivateStepTransition())
        .callback(
            element -> {
              if (Stepper.StepState.COMPLETED != state) {
                setState(Stepper.StepState.INACTIVE);
              }
              handler.accept(this);
            })
        .animate();

    return this;
  }

  /**
   * Mark the step as completed
   *
   * @return same Step instance
   */
  public Step complete() {
    this.clearInvalid();
    int stepIndex = stepper.getSteps().indexOf(this);
    if (stepIndex < (stepper.getSteps().size() - 1)
        && stepIndex <= stepper.getSteps().indexOf(stepper.getActiveStep())) {
      stepper.next();
    }
    setState(Stepper.StepState.COMPLETED);
    return this;
  }

  /**
   * Revert the Step to its initial state, if initial state is not specified in the construction,
   * this will be active state for first step and inactive for other steps
   */
  void reset() {
    setState(this.initialState, true);
  }

  /** This will put the step in {@link Stepper.StepState#DISABLED} {@inheritDoc} */
  @Override
  public Step disable() {
    return setDisabled();
  }

  /**
   * This will not make the step active but will enable the step so it is clickable and can be
   * stepped into with the stepper and by default will put the step in the {@link
   * Stepper.StepState#INACTIVE} {@inheritDoc}
   */
  @Override
  public Step enable() {
    return setEnabled(Stepper.StepState.INACTIVE);
  }

  /**
   * This will put the step in {@link Stepper.StepState#DISABLED}
   *
   * @return the same Step instance
   */
  public Step setDisabled() {
    setState(Stepper.StepState.DISABLED);
    return this;
  }

  /**
   * This will put the step in {@link Stepper.StepState#ACTIVE}
   *
   * @return the same Step instance
   */
  public Step setActive() {
    if (nonNull(this.stepper)) {
      this.stepper.activateStep(this);
    }
    return this;
  }

  /**
   * This will not make the step active unless specified,but will enable the step so it is clickable
   * and can be stepped into with the stepper and will put the step in the provided state
   *
   * @param targetState {@link Stepper.StepState}
   * @return the same Step instance
   */
  public Step setEnabled(Stepper.StepState targetState) {
    setState(targetState, true);
    return this;
  }

  /** @param stepper {@link Stepper} the step belongs to */
  void setStepper(Stepper stepper) {
    this.stepper = stepper;
    renderNumber();
  }

  /** @param barColor {@link Color} of the bar connecting the step with the next step */
  void setBarColor(Color barColor) {
    this.horizontalBarSpan.removeCss(this.barColor.getBackground());
    this.verticalBarSpan.removeCss(this.barColor.getBackground());
    this.barColor = barColor;
    this.horizontalBarSpan.css(this.barColor.getBackground());
    this.verticalBarSpan.css(this.barColor.getBackground());
  }

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

  /** if the step is invalid it will be put in the {@link Stepper.StepState#ERROR} {@inheritDoc} */
  @Override
  public ValidationResult validate() {
    clearInvalid();
    for (Validator validator : validators) {
      ValidationResult result = validator.isValid();
      if (!result.isValid()) {
        invalidate(result.getErrorMessage());
        return result;
      }
    }
    return ValidationResult.valid();
  }

  /** if the step is invalid it will be put in the {@link Stepper.StepState#ERROR} {@inheritDoc} */
  @Override
  public List validateAll() {
    clearInvalid();
    List validationResults =
        validators.stream().map(Validator::isValid).collect(Collectors.toList());
    List errorMessages =
        validationResults.stream()
            .filter(validationResult -> !validationResult.isValid())
            .map(ValidationResult::getErrorMessage)
            .collect(Collectors.toList());

    if (!errorMessages.isEmpty()) {
      invalidate(errorMessages);
    }
    return validationResults;
  }

  /** {@inheritDoc} */
  @Override
  public Step addValidator(Validator validator) {
    if (nonNull(validator)) {
      validators.add(validator);
    }
    return this;
  }

  /** {@inheritDoc} */
  @Override
  public Step removeValidator(Validator validator) {
    if (nonNull(validator)) {
      validators.remove(validator);
    }
    return this;
  }

  /** {@inheritDoc} */
  @Override
  public boolean hasValidator(Validator validator) {
    return validators.contains(validator);
  }

  /** this will be put the step in the {@link Stepper.StepState#ERROR} {@inheritDoc} */
  @Override
  public Step invalidate(String errorMessage) {
    return invalidate(Collections.singletonList(errorMessage));
  }

  /** this will be put the step in the {@link Stepper.StepState#ERROR} {@inheritDoc} */
  @Override
  public Step invalidate(List errorMessages) {
    clearInvalid();
    if (nonNull(errorMessages) && !errorMessages.isEmpty()) {
      setState(Stepper.StepState.ERROR, true);
      errorMessagesFlexItem
          .css(STEP_INVALID)
          .apply(
              self ->
                  errorMessages.forEach(
                      errorMessage -> self.appendChild(span().textContent(errorMessage))));
      this.errors.addAll(errorMessages);
    } else {
      setState(this.nonErrorState);
    }
    return this;
  }

  /** {@inheritDoc} */
  @Override
  public List getErrors() {
    return new ArrayList<>(errors);
  }

  /**
   * This will clear the step error and change it back to the state it had before it was
   * invalidation {@inheritDoc}
   */
  @Override
  public Step clearInvalid() {
    if (!errors.isEmpty()) {
      setState(nonErrorState);
      this.errors.clear();
      this.errorMessagesFlexItem.clearElement();
      this.removeCss(STEP_INVALID);
    }
    return this;
  }

  /** @return the {@link Stepper} this step belongs to */
  public Stepper getStepper() {
    return stepper;
  }

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

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

  /**
   * @param flexGrow int Flex grow of the flex item that is used as the root element for this step
   */
  void setFlexGrow(int flexGrow) {
    root.setFlexGrow(flexGrow);
  }

  /** @return List of all {@link Stepper.StepStateChangeListener} added to this step */
  public List getStepStateChangeListeners() {
    return stepStateChangeListeners;
  }

  /** @return int index of the step within the stepper */
  public int getIndex() {
    return this.stepper.getSteps().indexOf(this);
  }

  /** @return boolean, true if the step index is 0 */
  public boolean isFirstStep() {
    return getIndex() == 0;
  }

  /** @return boolean, true if the step is last step in the stepper */
  public boolean isLastStep() {
    return getIndex() == stepper.getSteps().size() - 1;
  }

  /** @return the current {@link Stepper.StepState} of the step */
  public Stepper.StepState getState() {
    return this.state;
  }

  /** @return boolean, true if the step state is {@link Stepper.StepState#ACTIVE} */
  public boolean isActive() {
    return Stepper.StepState.ACTIVE == this.state;
  }

  /** @return the {@link Stepper.StepState} this step was initially constructed with */
  public Stepper.StepState getInitialState() {
    return initialState;
  }

  /**
   * @param initialState {@link Stepper.StepState} to be used as default initial state for the step
   */
  public void setInitialState(Stepper.StepState initialState) {
    this.initialState = initialState;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy