import "./wizard.scss"; import * as React from "react"; import { Trans } from "@lingui/macro"; import { cssNames, prevDefault } from "../../utils"; import { Button } from "../button"; import { Stepper } from "../stepper"; import { SubTitle } from "../layout/sub-title"; import { Spinner } from "../spinner"; interface WizardCommonProps { data?: Partial; save?: (data: Partial, callback?: () => void) => void; reset?: () => void; done?: () => void; hideSteps?: boolean; } export interface WizardProps extends WizardCommonProps { className?: string; step?: number; title?: string; header?: React.ReactNode; onChange?: (step: number) => void; } interface State { step?: number; } export class Wizard extends React.Component { public state: State = { step: this.getValidStep(this.props.step) }; get steps() { const { className, title, step, header, onChange, children, ...commonProps } = this.props; const steps = React.Children.toArray(children) as WizardStepElem[]; return steps.filter(step => !step.props.skip).map((stepElem, i) => { const stepProps = stepElem.props; return React.cloneElement(stepElem, { step: i + 1, wizard: this, next: this.nextStep, prev: this.prevStep, first: this.firstStep, last: this.lastStep, isFirst: this.isFirstStep, isLast: this.isLastStep, ...commonProps, ...stepProps } as WizardStepProps) }); } get step() { return this.state.step; } set step(step: number) { step = this.getValidStep(step); if (step === this.step) return; this.setState({ step }, () => { if (this.props.onChange) { this.props.onChange(step); } }); } protected getValidStep(step: number) { return Math.min(Math.max(1, step), this.steps.length) || 1; } isFirstStep = () => this.step === 1; isLastStep = () => this.step === this.steps.length; firstStep = (): any => this.step = 1; nextStep = (): any => this.step++; prevStep = (): any => this.step--; lastStep = (): any => this.step = this.steps.length; render() { const { className, title, header, hideSteps } = this.props; const steps = this.steps.map(stepElem => ({ title: stepElem.props.title })) const step = React.cloneElement(this.steps[this.step - 1]); return (
{header} {title ? : null} {!hideSteps && steps.length > 1 ? : null}
{step}
); } } export interface WizardStepProps extends WizardCommonProps { wizard?: Wizard; title?: string; className?: string | object; contentClass?: string | object; customButtons?: React.ReactNode; // render custom buttons block in footer moreButtons?: React.ReactNode; // add more buttons to section in the footer loading?: boolean; // indicator of loading content for the step waiting?: boolean; // indicator of waiting response before going to next step disabledNext?: boolean; // disable next button flag, e.g when filling step is not finished hideNextBtn?: boolean; hideBackBtn?: boolean; step?: number; prevLabel?: React.ReactNode; // custom label for prev button nextLabel?: React.ReactNode; // custom label for next button next?: () => void | boolean | Promise; // custom action for next button prev?: () => void; // custom action for prev button first?: () => void; last?: () => void; isFirst?: () => boolean; isLast?: () => boolean; beforeContent?: React.ReactNode; afterContent?: React.ReactNode; noValidate?: boolean; // no validate form attribute skip?: boolean; // don't render the step scrollable?: boolean; } interface WizardStepState { waiting?: boolean; } type WizardStepElem = React.ReactElement; export class WizardStep extends React.Component { private form: HTMLFormElement; public state: WizardStepState = {}; private unmounting = false; static defaultProps: WizardStepProps = { scrollable: true, } componentWillUnmount() { this.unmounting = true; } prev = () => { const { isFirst, prev, done } = this.props; if (isFirst() && done) done(); else prev(); } next = () => { const next = this.props.next; const nextStep = this.props.wizard.nextStep; if (nextStep !== next) { const result = next(); if (result instanceof Promise) { this.setState({ waiting: true }); result.then(nextStep).finally(() => { if (this.unmounting) return; this.setState({ waiting: false }); }); } else if (typeof result === "boolean" && result) { nextStep(); } } else { nextStep(); } } submit = () => { if (!this.form.noValidate) { const valid = this.form.checkValidity(); if (!valid) return; } this.next(); } renderLoading() { return (
) } render() { const { step, isFirst, isLast, children, loading, customButtons, disabledNext, scrollable, hideNextBtn, hideBackBtn, beforeContent, afterContent, noValidate, skip, moreButtons, } = this.props; let { className, contentClass, nextLabel, prevLabel, waiting } = this.props; if (skip) { return; } waiting = (waiting !== undefined) ? waiting : this.state.waiting; className = cssNames(`WizardStep step${step}`, className); contentClass = cssNames("step-content", { scrollable }, contentClass); prevLabel = prevLabel || (isFirst() ? Cancel : Back); nextLabel = nextLabel || (isLast() ? Submit : Next); return (
this.form = e}> {beforeContent}
{loading ? this.renderLoading() : children}
{customButtons !== undefined ? customButtons : (
{moreButtons}
)} {afterContent}
) } }