UNPKG

@financial-times/o-stepped-progress

Version:

Track progress through a multi-step process, such as a form

211 lines (190 loc) 5.91 kB
import SteppedProgressStep from './stepped-progress-step.js'; /** * Component class names. * * @access private * @type {object} */ const classNames = { step: 'o-stepped-progress__step' }; /** * Represents a stepped progress component. */ class SteppedProgress { /** * Class constructor. * * @access public * @param {HTMLElement} steppedProgressElement - The component element in the DOM. * @param {object} [options={}] - An options object for configuring the component. */ constructor (steppedProgressElement, options) { this.steppedProgressElement = steppedProgressElement; this.options = Object.assign({}, { // TODO }, options || SteppedProgress.getDataAttributes(steppedProgressElement)); this._constructSteps(); } /** * Get an array of steps. * * @access public * @returns {Array<SteppedProgressStep>} Returns an array of steps. */ getSteps() { return [...this._steps]; } /** * Get an array of steps with a "completed" status. * * @access public * @returns {Array<SteppedProgressStep>} Returns an array of steps. */ getCompletedSteps() { return this._steps.filter(step => step.isComplete()); } /** * Get whether a step exists at a given index (0-based). * * @access public * @param {number} index - The index to check. * @returns {boolean} Returns whether a step exists at a given index. */ hasStepAtIndex(index) { return Boolean(this._steps[index]); } /** * Get the step at a given index (0-based). * * @access public * @param {number} index - The index of the step to get. * @returns {SteppedProgressStep} Returns the step at the given index. * @throws {Error} Will throw an error if there is no step at the given index. Use {@link SteppedProgress#hasStepAtIndex} to check. */ getStepAtIndex(index) { if (!this.hasStepAtIndex(index)) { throw new Error(`No step at index: ${index}`); } return this._steps[index]; } /** * Get the step which has the "current" state. If there are multiple steps with this state then * the last one will be returned. * * @access public * @returns {SteppedProgressStep} Returns the current step. */ getCurrentStep() { return this._steps.filter(step => step.isCurrent()).pop(); } /** * Get the last step in the stepped progress. * * @access public * @returns {SteppedProgressStep} Returns the last step. */ getLastStep() { return this._steps[this._steps.length - 1]; } /** * Get whether all steps have the "completed" state. * * @access public * @returns {boolean} Returns whether all steps are completed. */ isComplete() { return this._steps.every(step => step.isComplete()); } /** * Get the next future step (a step which does not have the "current", "complete", or "error" * states). If no such step exists, the last step will be returned. * * @access public * @returns {SteppedProgressStep} Returns the next step. */ getNextStep() { if (!this.isComplete()) { return this._steps.find(step => step.isFuture()) || this.getLastStep(); } return this.getLastStep(); } /** * Mark the current step as "complete" and then mark the next step as "current". If all steps * have the "complete" state then this method does nothing. * * @access public * @returns {void} */ progress() { if (!this.isComplete()) { const currentStep = this.getCurrentStep(); if (currentStep) { currentStep.markAsComplete(); } } if (!this.isComplete()) { this.getNextStep().markAsCurrent(); } } /** * Construct step instances and store them on the `_steps` property. * * @access private * @returns {void} */ _constructSteps() { const elements = this.steppedProgressElement.querySelectorAll(`.${classNames.step}`); this._steps = [...elements].map(element => new SteppedProgressStep(element, this)); } /** * Get the data attributes from the stepped progress element. If the component is being set up * declaratively, this method is used to extract the data attributes from the DOM. * * @access public * @param {HTMLElement} steppedProgressElement - The component element in the DOM * @returns {object} Returns an options object constructed from the DOM. */ static getDataAttributes(steppedProgressElement) { if (!(steppedProgressElement instanceof HTMLElement)) { return {}; } return Object.keys(steppedProgressElement.dataset).reduce((options, key) => { // Ignore data-o-component if (key === 'oComponent') { return options; } // Build a concise key and get the option value const shortKey = key.replace(/^oSteppedProgress(w)(w+)$/, (m, m1, m2) => m1.toLowerCase() + m2); const value = steppedProgressElement.dataset[key]; // Try parsing the value as JSON, otherwise just set it as a string try { options[shortKey] = JSON.parse(value.replace(/'/g, '"')); } catch (error) { options[shortKey] = value; } return options; }, {}); } /** * Initialise stepped progress component. * * @access public * @param {(HTMLElement | string)} rootElement - The root element to intialise the component in, or a CSS selector for the root element * @param {object} [options={}] - An options object for configuring the component * @returns {(SteppedProgress|Array<SteppedProgress>)} Returns a stepped progress instance, or an array of instances. */ static init(rootElement, options) { if (!rootElement) { rootElement = document.body; } if (!(rootElement instanceof HTMLElement)) { rootElement = document.querySelector(rootElement); } if (rootElement instanceof HTMLElement && rootElement.matches('[data-o-component=o-stepped-progress]')) { return new SteppedProgress(rootElement, options); } return Array.from(rootElement.querySelectorAll('[data-o-component="o-stepped-progress"]'), rootEl => new SteppedProgress(rootEl, options)); } } export default SteppedProgress;