UNPKG

react-joyride

Version:

Create guided tours for your apps

327 lines (262 loc) 7.96 kB
import { Props as FloaterProps } from 'react-floater'; import is from 'is-lite'; import { ACTIONS, LIFECYCLE, STATUS } from '~/literals'; import { Origin, State, Status, Step, StoreHelpers, StoreOptions } from '~/types'; import { hasValidKeys, objectKeys, omit } from './helpers'; type StateWithContinuous = State & { continuous: boolean }; type Listener = (state: State) => void; type PopperData = Parameters<NonNullable<FloaterProps['getPopper']>>[0]; const defaultState: State = { action: 'init', controlled: false, index: 0, lifecycle: LIFECYCLE.INIT, origin: null, size: 0, status: STATUS.IDLE, }; const validKeys = objectKeys(omit(defaultState, 'controlled', 'size')); class Store { private beaconPopper: PopperData | null; private tooltipPopper: PopperData | null; private data: Map<string, any> = new Map(); private listener: Listener | null; private store: Map<string, any> = new Map(); constructor(options?: StoreOptions) { const { continuous = false, stepIndex, steps = [] } = options ?? {}; this.setState( { action: ACTIONS.INIT, controlled: is.number(stepIndex), continuous, index: is.number(stepIndex) ? stepIndex : 0, lifecycle: LIFECYCLE.INIT, origin: null, status: steps.length ? STATUS.READY : STATUS.IDLE, }, true, ); this.beaconPopper = null; this.tooltipPopper = null; this.listener = null; this.setSteps(steps); } public getState(): State { if (!this.store.size) { return { ...defaultState }; } return { action: this.store.get('action') || '', controlled: this.store.get('controlled') || false, index: parseInt(this.store.get('index'), 10), lifecycle: this.store.get('lifecycle') || '', origin: this.store.get('origin') || null, size: this.store.get('size') || 0, status: (this.store.get('status') as Status) || '', }; } private getNextState(state: Partial<State>, force: boolean = false): State { const { action, controlled, index, size, status } = this.getState(); const newIndex = is.number(state.index) ? state.index : index; const nextIndex = controlled && !force ? index : Math.min(Math.max(newIndex, 0), size); return { action: state.action ?? action, controlled, index: nextIndex, lifecycle: state.lifecycle ?? LIFECYCLE.INIT, origin: state.origin ?? null, size: state.size ?? size, status: nextIndex === size ? STATUS.FINISHED : (state.status ?? status), }; } private getSteps(): Array<Step> { const steps = this.data.get('steps'); return Array.isArray(steps) ? steps : []; } private hasUpdatedState(oldState: State): boolean { const before = JSON.stringify(oldState); const after = JSON.stringify(this.getState()); return before !== after; } private setState(nextState: Partial<StateWithContinuous>, initial: boolean = false) { const state = this.getState(); const { action, index, lifecycle, origin = null, size, status, } = { ...state, ...nextState, }; this.store.set('action', action); this.store.set('index', index); this.store.set('lifecycle', lifecycle); this.store.set('origin', origin); this.store.set('size', size); this.store.set('status', status); if (initial) { this.store.set('controlled', nextState.controlled); this.store.set('continuous', nextState.continuous); } if (this.listener && this.hasUpdatedState(state)) { this.listener(this.getState()); } } public addListener = (listener: Listener) => { this.listener = listener; }; public setSteps = (steps: Array<Step>) => { const { size, status } = this.getState(); const state = { size: steps.length, status, }; this.data.set('steps', steps); if (status === STATUS.WAITING && !size && steps.length) { state.status = STATUS.RUNNING; } this.setState(state); }; public getHelpers(): StoreHelpers { return { close: this.close, go: this.go, info: this.info, next: this.next, open: this.open, prev: this.prev, reset: this.reset, skip: this.skip, }; } public getPopper = (name: 'beacon' | 'tooltip'): PopperData | null => { if (name === 'beacon') { return this.beaconPopper; } return this.tooltipPopper; }; public setPopper = (name: 'beacon' | 'tooltip', popper: PopperData) => { if (name === 'beacon') { this.beaconPopper = popper; } else { this.tooltipPopper = popper; } }; public cleanupPoppers = () => { this.beaconPopper = null; this.tooltipPopper = null; }; public close = (origin: Origin | null = null) => { const { index, status } = this.getState(); if (status !== STATUS.RUNNING) { return; } this.setState({ ...this.getNextState({ action: ACTIONS.CLOSE, index: index + 1, origin }), }); }; public go = (nextIndex: number) => { const { controlled, status } = this.getState(); if (controlled || status !== STATUS.RUNNING) { return; } const step = this.getSteps()[nextIndex]; this.setState({ ...this.getNextState({ action: ACTIONS.GO, index: nextIndex }), status: step ? status : STATUS.FINISHED, }); }; public info = (): State => this.getState(); public next = () => { const { index, status } = this.getState(); if (status !== STATUS.RUNNING) { return; } this.setState(this.getNextState({ action: ACTIONS.NEXT, index: index + 1 })); }; public open = () => { const { status } = this.getState(); if (status !== STATUS.RUNNING) { return; } this.setState({ ...this.getNextState({ action: ACTIONS.UPDATE, lifecycle: LIFECYCLE.TOOLTIP }), }); }; public prev = () => { const { index, status } = this.getState(); if (status !== STATUS.RUNNING) { return; } this.setState({ ...this.getNextState({ action: ACTIONS.PREV, index: index - 1 }), }); }; public reset = (restart = false) => { const { controlled } = this.getState(); if (controlled) { return; } this.setState({ ...this.getNextState({ action: ACTIONS.RESET, index: 0 }), status: restart ? STATUS.RUNNING : STATUS.READY, }); }; public skip = () => { const { status } = this.getState(); if (status !== STATUS.RUNNING) { return; } this.setState({ action: ACTIONS.SKIP, lifecycle: LIFECYCLE.INIT, status: STATUS.SKIPPED, }); }; public start = (nextIndex?: number) => { const { index, size } = this.getState(); this.setState({ ...this.getNextState( { action: ACTIONS.START, index: is.number(nextIndex) ? nextIndex : index, }, true, ), status: size ? STATUS.RUNNING : STATUS.WAITING, }); }; public stop = (advance = false) => { const { index, status } = this.getState(); if (([STATUS.FINISHED, STATUS.SKIPPED] as Array<Status>).includes(status)) { return; } this.setState({ ...this.getNextState({ action: ACTIONS.STOP, index: index + (advance ? 1 : 0) }), status: STATUS.PAUSED, }); }; public update = (state: Partial<State>) => { if (!hasValidKeys(state, validKeys)) { throw new Error(`State is not valid. Valid keys: ${validKeys.join(', ')}`); } this.setState({ ...this.getNextState( { ...this.getState(), ...state, action: state.action ?? ACTIONS.UPDATE, origin: state.origin ?? null, }, true, ), }); }; } export type StoreInstance = ReturnType<typeof createStore>; export default function createStore(options?: StoreOptions) { return new Store(options); }