diff --git a/src/lib/boarding-types.ts b/src/lib/boarding-types.ts index 1775e05..9acca4f 100644 --- a/src/lib/boarding-types.ts +++ b/src/lib/boarding-types.ts @@ -1,7 +1,7 @@ import HighlightElement, { HighlightElementHybridOptions, } from "./core/highlight-element"; -import { OverlayTopLevelOptions } from "./core/overlay"; +import {BoardingExitReason, OverlayTopLevelOptions} from "./core/overlay"; import { PopoverHybridOptions, PopoverStepLevelOptions, @@ -68,8 +68,19 @@ export interface BoardingOptions * Simple event that triggers for boarding.start() */ onStart?: (element: HighlightElement) => void; + /** + * Event that will trigger when boarding is finished, after everything is cleaned up + */ + onFinish?: (reason: BoardingExitReason) => void; + /** + * Event that will trigger when a step is entered + */ + onStep?: (step: number) => void; } +/** Identifier for different navigation actions relevant to an event */ +export type StepNavigationInitiator = "next" | "prev" | "init" | "reset"; + export interface BoardingStepDefinition extends HighlightElementHybridOptions { /** * Query selector representing the DOM Element @@ -77,12 +88,17 @@ export interface BoardingStepDefinition extends HighlightElementHybridOptions { element: string | HTMLElement; /** - * A method that will run very early for the element to-be highlighted. The method will run right before `onNext` (or `onPrevious` when going backwards) + * A method that will run very early for the element to-be highlighted. + * The method will run right before `onNext` (or `onPrevious` when going backwards). + * If returning a promise, it will wait for it to resolve before moving into the step. * - * Note: This method won't run for the first step when starting * @param initiator either "next", "prev" or "init" */ - prepareElement?: (initiator: "next" | "prev" | "init") => void; + prepareElement?: (initiator: StepNavigationInitiator) => void; + /** + * Event that will trigger when leaving a step + */ + cleanupElement?: (initiator: StepNavigationInitiator) => void; /** * Options representing popover for this step */ diff --git a/src/lib/boarding.ts b/src/lib/boarding.ts index 3759f66..39cc180 100644 --- a/src/lib/boarding.ts +++ b/src/lib/boarding.ts @@ -24,10 +24,13 @@ type HighlightSelector = BoardingStepDefinition | string | HTMLElement; enum MovementType { Start, Highlight, + CleanupNext, PrepareNext, Next, + CleanupPrevious, PreparePrevious, Previous, + CleanupReset, } type Movement = @@ -35,7 +38,9 @@ type Movement = movement: | MovementType.Start | MovementType.Next + | MovementType.CleanupNext | MovementType.PrepareNext + | MovementType.CleanupPrevious | MovementType.PreparePrevious | MovementType.Previous; index: number; @@ -43,6 +48,11 @@ type Movement = | { movement: MovementType.Highlight; selector: HighlightSelector; + } + | { + movement: MovementType.CleanupReset; + immediate: boolean; + exitReason: BoardingExitReason; }; /** @@ -64,6 +74,7 @@ class Boarding { private currentMovePrevented: Movement | false; private overlay: Overlay; + private stepCleanupRequired: boolean = false; constructor(options?: BoardingOptions) { const { @@ -137,6 +148,8 @@ class Boarding { if (!this.steps || this.steps.length === 0) { throw new Error("There are no steps defined to iterate"); } + + this.stepCleanupRequired = true; this.steps[index].prepareElement?.("init"); if (this.currentMovePrevented) { return; @@ -161,6 +174,7 @@ class Boarding { ? selector : { element: selector }; + this.stepCleanupRequired = true; stepDefinition.prepareElement?.("init"); if (this.currentMovePrevented) { return; @@ -205,8 +219,8 @@ class Boarding { * If preventMove was called, you can use this method to continue where the movement was stopped. * It's a smart method that chooses the correct method from: `next`, `previous`, `start` and `highlight` */ - public async continue() { - // setTimout foces the continue to always be executed async from the original (this is necessary, so a user can't make the mistake of calling preventMove and continue synchronously which would cause issues) + public continue() { + // setTimout foces the continue to always be executed from the original (this is necessary, so a user can't make the mistake of calling preventMove and continue synchronously which would cause issues) setTimeout(() => { if (this.currentMovePrevented === this.lastMovementRequested) { // reset, we are continuing @@ -220,18 +234,27 @@ class Boarding { case MovementType.Highlight: this.handleHighlight(this.lastMovementRequested.selector); break; + case MovementType.CleanupNext: + this.next(); + break; case MovementType.PrepareNext: this.handleNext(); break; case MovementType.Next: this.moveNext(); break; + case MovementType.CleanupPrevious: + this.previous(); + break; case MovementType.PreparePrevious: this.handlePrevious(); break; case MovementType.Previous: this.movePrevious(); break; + case MovementType.CleanupReset: + this.reset(this.lastMovementRequested.immediate, this.lastMovementRequested.exitReason); + break; } } else { console.warn( @@ -257,12 +280,28 @@ class Boarding { return; } + if (this.stepCleanupRequired && ( + this.lastMovementRequested?.movement !== MovementType.CleanupNext && + this.lastMovementRequested?.movement !== MovementType.PrepareNext)) { + this.lastMovementRequested = { + movement: MovementType.CleanupNext, + index: this.currentStep, + }; + + this.stepCleanupRequired = false; + this.steps[this.currentStep]?.cleanupElement?.("next"); + if (this.currentMovePrevented) { + return; + } + } + this.lastMovementRequested = { movement: MovementType.PrepareNext, index: this.currentStep, }; // call prepareElement for coming element if available + this.stepCleanupRequired = true; this.steps[this.currentStep + 1]?.prepareElement?.("next"); // check if prepareElement wants to stop if (this.currentMovePrevented) { @@ -281,12 +320,28 @@ class Boarding { return; } + if (this.stepCleanupRequired && ( + this.lastMovementRequested?.movement !== MovementType.CleanupPrevious && + this.lastMovementRequested?.movement !== MovementType.PreparePrevious)) { + this.lastMovementRequested = { + movement: MovementType.CleanupPrevious, + index: this.currentStep, + }; + + this.stepCleanupRequired = false; + this.steps[this.currentStep]?.cleanupElement?.("prev"); + if (this.currentMovePrevented) { + return; + } + } + this.lastMovementRequested = { movement: MovementType.PreparePrevious, index: this.currentStep, }; // call prepareElement for coming element if available + this.stepCleanupRequired = true; this.steps[this.currentStep - 1]?.prepareElement?.("prev"); // check if prepareElement wants to stop if (this.currentMovePrevented) { @@ -316,6 +371,21 @@ class Boarding { * @param exitReason report the reason reset is called to `onReset`. This string has no other functionallity and is purely of informational purpose inside `onReset` */ public reset(immediate = false, exitReason: BoardingExitReason = "cancel") { + if (this.stepCleanupRequired && + this.lastMovementRequested?.movement !== MovementType.CleanupReset) { + this.lastMovementRequested = { + movement: MovementType.CleanupReset, + immediate: immediate, + exitReason: exitReason, + }; + + this.stepCleanupRequired = false; + this.steps[this.currentStep]?.cleanupElement?.("reset"); + if (this.currentMovePrevented) { + return; + } + } + this.currentStep = 0; this.isActivated = false; this.overlay.clear(immediate, exitReason); @@ -328,6 +398,7 @@ class Boarding { // reset step tracking this.lastMovementRequested = undefined; this.currentMovePrevented = false; + this.options.onFinish?.(exitReason); } /** @@ -456,6 +527,7 @@ class Boarding { this.setStrictClickHandlingRules(nextElem); this.overlay.highlight(nextElem); this.currentStep += 1; + this.options.onStep?.(this.currentStep); } /** @@ -471,6 +543,7 @@ class Boarding { this.setStrictClickHandlingRules(previousElem); this.overlay.highlight(previousElem); this.currentStep -= 1; + this.options.onStep?.(this.currentStep); } /** @@ -483,6 +556,7 @@ class Boarding { this.isActivated = true; this.setStrictClickHandlingRules(element); this.overlay.highlight(element); + this.options.onStep?.(this.currentStep); } /** @@ -697,7 +771,7 @@ class Boarding { this.previous(); }, onCloseClick: () => { - this.reset(false, "cancel"); + this.reset(false, "close-button"); }, }); } diff --git a/src/lib/core/overlay.ts b/src/lib/core/overlay.ts index 7a87640..28316d6 100644 --- a/src/lib/core/overlay.ts +++ b/src/lib/core/overlay.ts @@ -19,8 +19,8 @@ type OverlaySupportedSharedOptions = Pick< "animate" | "padding" | "radius" >; -/** Identifier for different exist reasons that causing onReset, such as cancel or finish */ -export type BoardingExitReason = "cancel" | "finish"; +/** Identifier for different exit reasons that causing onReset, such as cancel or finish */ +export type BoardingExitReason = "cancel" | "close-button" | "finish"; /** The options of overlay that will come from the top-level */ export interface OverlayTopLevelOptions { @@ -133,6 +133,7 @@ class Overlay { /** * Removes the overlay and cancel any listeners * @param immediate immediately unmount overlay or animate out + * @param exitReason the reason for clearing the overlay, such as cancel or finish */ public clear(immediate = false, exitReason: BoardingExitReason) { // Callback for when overlay is about to be reset