feat(stepper,updater): add progress-aware stepper flows and updater countdown states
This commit is contained in:
@@ -5,8 +5,6 @@ import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
unsafeCSS,
|
||||
type CSSResult,
|
||||
cssManager,
|
||||
property,
|
||||
type TemplateResult,
|
||||
@@ -20,16 +18,33 @@ import { zIndexRegistry } from '../../00zindex.js';
|
||||
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
||||
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
|
||||
import '../../00group-feedback/dees-progressbar/dees-progressbar.js';
|
||||
import '../dees-tile/dees-tile.js';
|
||||
|
||||
export interface IStepProgressState {
|
||||
label?: string;
|
||||
percentage?: number;
|
||||
indeterminate?: boolean;
|
||||
showPercentage?: boolean;
|
||||
statusText?: string;
|
||||
terminalLines?: string[];
|
||||
statusRows?: number;
|
||||
}
|
||||
|
||||
export interface IStepProgress extends IStepProgressState {
|
||||
autoAdvance?: boolean;
|
||||
}
|
||||
|
||||
export interface IStep {
|
||||
title: string;
|
||||
content: TemplateResult;
|
||||
progressStep?: IStepProgress;
|
||||
menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
|
||||
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
||||
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
||||
validationFuncCalled?: boolean;
|
||||
abortController?: AbortController;
|
||||
progressStepState?: IStepProgressState;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -276,6 +291,14 @@ export class DeesStepper extends DeesElement {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.step-body .content.withProgressStep {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.progressStep {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* --- Footer: modal-style bottom buttons --- */
|
||||
.bottomButtons {
|
||||
display: flex;
|
||||
@@ -375,6 +398,7 @@ export class DeesStepper extends DeesElement {
|
||||
const isHidden =
|
||||
this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
|
||||
const isFirst = stepIndex === 0;
|
||||
const progressStepState = stepArg.progressStep ? this.getProgressStepState(stepArg) : null;
|
||||
return html`<dees-tile
|
||||
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
|
||||
>
|
||||
@@ -390,7 +414,20 @@ export class DeesStepper extends DeesElement {
|
||||
</div>
|
||||
<div class="step-body">
|
||||
<div class="title">${stepArg.title}</div>
|
||||
<div class="content">${stepArg.content}</div>
|
||||
${stepArg.progressStep && progressStepState ? html`
|
||||
<div class="progressStep">
|
||||
<dees-progressbar
|
||||
.label=${progressStepState.label ?? stepArg.title}
|
||||
.percentage=${progressStepState.percentage ?? 0}
|
||||
.indeterminate=${progressStepState.indeterminate ?? false}
|
||||
.showPercentage=${progressStepState.showPercentage ?? true}
|
||||
.statusText=${progressStepState.statusText ?? ''}
|
||||
.terminalLines=${progressStepState.terminalLines ?? []}
|
||||
.statusRows=${progressStepState.statusRows ?? 3}
|
||||
></dees-progressbar>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="content ${stepArg.progressStep ? 'withProgressStep' : ''}">${stepArg.content}</div>
|
||||
</div>
|
||||
<div slot="footer" class="bottomButtons">
|
||||
${isSelected && this.activeForm !== null && !this.activeFormValid
|
||||
@@ -426,22 +463,30 @@ export class DeesStepper extends DeesElement {
|
||||
public async firstUpdated() {
|
||||
await this.domtoolsPromise;
|
||||
await this.domtools.convenience.smartdelay.delayFor(0);
|
||||
if (!this.steps.length) {
|
||||
return;
|
||||
}
|
||||
this.prepareStepForActivation(this.steps[0]);
|
||||
this.selectedStep = this.steps[0];
|
||||
this.setScrollStatus();
|
||||
await this.updateComplete;
|
||||
await this.setScrollStatus();
|
||||
// Remove entrance class after initial animation completes
|
||||
await this.domtools.convenience.smartdelay.delayFor(350);
|
||||
this.shadowRoot!.querySelector('.step.entrance')?.classList.remove('entrance');
|
||||
}
|
||||
|
||||
public async updated() {
|
||||
this.setScrollStatus();
|
||||
public async updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||
if (!changedProperties.has('selectedStep') && !changedProperties.has('steps')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setScrollStatus();
|
||||
}
|
||||
|
||||
public scroller!: typeof domtools.plugins.SweetScroll.prototype;
|
||||
|
||||
public async setScrollStatus() {
|
||||
const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer') as HTMLElement;
|
||||
const firstStepElement = this.shadowRoot!.querySelector('.step') as HTMLElement;
|
||||
const selectedStepElement = this.shadowRoot!.querySelector('.selected') as HTMLElement;
|
||||
if (!selectedStepElement) {
|
||||
return;
|
||||
@@ -452,14 +497,11 @@ export class DeesStepper extends DeesElement {
|
||||
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
|
||||
}px`;
|
||||
}
|
||||
console.log('Setting scroll status');
|
||||
console.log(selectedStepElement);
|
||||
const scrollPosition =
|
||||
selectedStepElement.offsetTop -
|
||||
stepperContainer.offsetHeight / 2 +
|
||||
selectedStepElement.offsetHeight / 2;
|
||||
console.log(scrollPosition);
|
||||
const domtoolsInstance = await domtools.DomTools.setupDomTools();
|
||||
await domtools.DomTools.setupDomTools();
|
||||
if (!this.scroller) {
|
||||
this.scroller = new domtools.plugins.SweetScroll(
|
||||
{
|
||||
@@ -474,7 +516,11 @@ export class DeesStepper extends DeesElement {
|
||||
if (!this.selectedStep.validationFuncCalled && this.selectedStep.validationFunc) {
|
||||
this.selectedStep.abortController = new AbortController();
|
||||
this.selectedStep.validationFuncCalled = true;
|
||||
await this.selectedStep.validationFunc(this, selectedStepElement, this.selectedStep.abortController.signal);
|
||||
void this.runStepValidation(
|
||||
this.selectedStep,
|
||||
selectedStepElement,
|
||||
this.selectedStep.abortController.signal,
|
||||
);
|
||||
}
|
||||
this.scroller.to(scrollPosition);
|
||||
}
|
||||
@@ -492,6 +538,7 @@ export class DeesStepper extends DeesElement {
|
||||
currentStep.validationFuncCalled = false;
|
||||
const previousStep = this.steps[currentIndex - 1];
|
||||
previousStep.validationFuncCalled = false;
|
||||
this.prepareStepForActivation(previousStep);
|
||||
this.selectedStep = previousStep;
|
||||
await this.domtoolsPromise;
|
||||
await this.domtools.convenience.smartdelay.delayFor(100);
|
||||
@@ -511,9 +558,52 @@ export class DeesStepper extends DeesElement {
|
||||
currentStep.validationFuncCalled = false;
|
||||
const nextStep = this.steps[currentIndex + 1];
|
||||
nextStep.validationFuncCalled = false;
|
||||
this.prepareStepForActivation(nextStep);
|
||||
this.selectedStep = nextStep;
|
||||
}
|
||||
|
||||
public resetProgressStep(stepArg: IStep = this.selectedStep) {
|
||||
if (!stepArg?.progressStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public updateProgressStep(
|
||||
progressStateArg: Partial<IStepProgressState>,
|
||||
stepArg: IStep = this.selectedStep,
|
||||
) {
|
||||
if (!stepArg?.progressStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProgressState = this.getProgressStepState(stepArg);
|
||||
stepArg.progressStepState = {
|
||||
...currentProgressState,
|
||||
...progressStateArg,
|
||||
terminalLines: progressStateArg.terminalLines
|
||||
? [...progressStateArg.terminalLines]
|
||||
: [...(currentProgressState.terminalLines ?? [])],
|
||||
};
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public appendProgressStepLine(lineArg: string, stepArg: IStep = this.selectedStep) {
|
||||
if (!stepArg?.progressStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProgressState = this.getProgressStepState(stepArg);
|
||||
this.updateProgressStep(
|
||||
{
|
||||
terminalLines: [...(currentProgressState.terminalLines ?? []), lineArg],
|
||||
},
|
||||
stepArg,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the currently selected step for a <dees-form> in its content. When
|
||||
* found, subscribes to the form's RxJS changeSubject so the primary
|
||||
@@ -582,6 +672,74 @@ export class DeesStepper extends DeesElement {
|
||||
await optionArg.action(this);
|
||||
}
|
||||
|
||||
private getProgressStepState(stepArg: IStep): IStepProgressState {
|
||||
if (!stepArg.progressStep) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!stepArg.progressStepState) {
|
||||
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
|
||||
}
|
||||
|
||||
return stepArg.progressStepState;
|
||||
}
|
||||
|
||||
private createInitialProgressStepState(stepArg: IStep): IStepProgressState {
|
||||
return {
|
||||
label: stepArg.progressStep?.label ?? stepArg.title,
|
||||
percentage: stepArg.progressStep?.percentage ?? 0,
|
||||
indeterminate: stepArg.progressStep?.indeterminate ?? false,
|
||||
showPercentage: stepArg.progressStep?.showPercentage ?? true,
|
||||
statusText: stepArg.progressStep?.statusText ?? '',
|
||||
terminalLines: [...(stepArg.progressStep?.terminalLines ?? [])],
|
||||
statusRows: stepArg.progressStep?.statusRows ?? 3,
|
||||
};
|
||||
}
|
||||
|
||||
private prepareStepForActivation(stepArg?: IStep) {
|
||||
if (!stepArg?.progressStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
|
||||
}
|
||||
|
||||
private async runStepValidation(
|
||||
stepArg: IStep,
|
||||
selectedStepElement: HTMLElement,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await stepArg.validationFunc?.(this, selectedStepElement, signal);
|
||||
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stepArg.progressStep && stepArg.progressStep.autoAdvance !== false && this.selectedStep === stepArg) {
|
||||
this.goNext();
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stepArg.progressStep) {
|
||||
const errorText = error instanceof Error ? error.message : 'Unexpected error';
|
||||
this.appendProgressStepLine(`Error: ${errorText}`, stepArg);
|
||||
this.updateProgressStep(
|
||||
{
|
||||
indeterminate: false,
|
||||
statusText: errorText,
|
||||
},
|
||||
stepArg,
|
||||
);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently-open confirmation modal (if any). Prevents double-stacking when
|
||||
* the user clicks the backdrop or the Cancel button while a confirm modal
|
||||
@@ -657,6 +815,9 @@ export class DeesStepper extends DeesElement {
|
||||
public async destroy() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
const container = this.shadowRoot!.querySelector('.stepperContainer');
|
||||
if (this.selectedStep?.abortController) {
|
||||
this.selectedStep.abortController.abort();
|
||||
}
|
||||
container?.classList.add('predestroy');
|
||||
await domtools.convenience.smartdelay.delayFor(250);
|
||||
if (this.parentElement) {
|
||||
|
||||
Reference in New Issue
Block a user