UNPKG

@adobe/coral-spectrum

Version:

Coral Spectrum is a JavaScript library of Web Components following Spectrum design patterns.

389 lines (320 loc) 11.3 kB
/** * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ import {BaseComponent} from '../../../coral-base-component'; import {Collection} from '../../../coral-collection'; import '../../../coral-component-steplist'; import '../../../coral-component-panelstack'; import {commons} from '../../../coral-utils'; import {Decorator} from '../../../coral-decorator'; const CLASSNAME = '_coral-WizardView'; /** @class Coral.WizardView @classdesc A WizardView component is the wrapping container used to create the typical Wizard pattern. This is intended to be used with a {@link StepList} and a {@link PanelStack}. @htmltag coral-wizardview @extends {HTMLElement} @extends {BaseComponent} */ const WizardView = Decorator(class extends BaseComponent(HTMLElement) { /** @ignore */ constructor() { super(); this._delegateEvents({ 'capture:click coral-steplist[coral-wizardview-steplist] > coral-step': '_onStepClick', 'coral-steplist:change coral-steplist[coral-wizardview-steplist]': '_onStepListChange', 'click [coral-wizardview-previous]': '_onPreviousClick', 'click [coral-wizardview-next]': '_onNextClick' }); // Init the collection mutation observer this.stepLists._startHandlingItems(true); this.panelStacks._startHandlingItems(true); // Disable tracking for specific elements that are attached to the component. this._observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { // Sync added nodes for (let i = 0 ; i < mutation.addedNodes.length ; i++) { const addedNode = mutation.addedNodes[i]; if (addedNode.setAttribute && ( addedNode.hasAttribute('coral-wizardview-next') || addedNode.hasAttribute('coral-wizardview-previous') || addedNode.hasAttribute('coral-wizardview-steplist') || addedNode.hasAttribute('coral-wizardview-panelstack') )) { addedNode.setAttribute('tracking', 'off'); } } }); }); this._observer.observe(this, { childList: true, subtree: true }); } /** The set of controlled PanelStacks. Each PanelStack must have the <code>coral-wizardview-panelstack</code> attribute. @type {Collection} @readonly */ get panelStacks() { // Construct the collection on first request: if (!this._panelStacks) { this._panelStacks = new Collection({ host: this, itemTagName: 'coral-panelstack', // allows panelstack to be nested itemSelector: ':scope > coral-panelstack[coral-wizardview-panelstack]', onlyHandleChildren: true, onItemAdded: this._onItemAdded }); } return this._panelStacks; } /** The set of controlling StepLists. Each StepList must have the <code>coral-wizardview-steplist</code> attribute. @type {Collection} @readonly */ get stepLists() { // Construct the collection on first request: if (!this._stepLists) { this._stepLists = new Collection({ host: this, itemTagName: 'coral-steplist', // allows steplist to be nested itemSelector: ':scope > coral-steplist[coral-wizardview-steplist]', onlyHandleChildren: true, onItemAdded: this._onItemAdded }); } return this._stepLists; } /** Called by the Collection when an item is added @private */ _onItemAdded(item) { this._selectItemByIndex(item, this._getSelectedIndex()); } _onStepClick(event) { this._trackEvent('click', 'coral-wizardview-steplist-step', event, event.matchedTarget); } /** Handles the next button click. @private */ _onNextClick(event) { // we stop propagation in case the wizard views are nested event.stopPropagation(); this.next(); const stepList = this.stepLists.first(); const step = stepList.items.getAll()[this._getSelectedIndex()]; this._trackEvent('click', 'coral-wizardview-next', event, step); } /** Handles the previous button click. @private */ _onPreviousClick(event) { // we stop propagation in case the wizard views are nested event.stopPropagation(); this.previous(); const stepList = this.stepLists.first(); const step = stepList.items.getAll()[this._getSelectedIndex()]; this._trackEvent('click', 'coral-wizardview-previous', event, step); } /** Detects a change in the StepList and triggers an event. @private */ _onStepListChange(event) { // Stop propagation of the events to support nested panels event.stopPropagation(); // Get the step number const index = event.target.items.getAll().indexOf(event.detail.selection); // Sync the other StepLists this._selectStep(index); this.trigger('coral-wizardview:change', { selection: event.detail.selection, oldSelection: event.detail.oldSelection }); this._trackEvent('change', 'coral-wizardview', event); } /** @private */ _getSelectedIndex() { const stepList = this.stepLists.first(); if (!stepList) { return -1; } let stepIndex = -1; if (stepList.items) { stepIndex = stepList.items.getAll().indexOf(stepList.selectedItem); } else { // Manually get the selected step const steps = stepList.querySelectorAll('coral-step'); // Find the last selected step for (let i = steps.length - 1 ; i >= 0 ; i--) { if (steps[i].hasAttribute('selected')) { stepIndex = i; break; } } } return stepIndex; } /** Select the step according to the provided index. @param {*} component The StepList or PanelStack to select the step on. @param {Number} index The index of the step that should be selected. @private */ _selectItemByIndex(component, index) { let item = null; // we need to set an id to be able to find direct children component.id = component.id || commons.getUID(); // if collection api is available we use it to find the correct item if (component.items) { // Get the corresponding item item = component.items.getAll()[index]; } // Resort to querying manually on immediately children else if (component.tagName === 'CORAL-STEPLIST') { // @polyfill IE - we use id since :scope is not supported item = component.querySelectorAll(`#${component.id} > coral-step`)[index]; } else if (component.tagName === 'CORAL-PANELSTACK') { // @polyfill IE - we use id since :scope is not supported item = component.querySelectorAll(`#${component.id} > coral-panel`)[index]; } if (item) { // we only select if not select to avoid mutations if (!item.hasAttribute('selected')) { item.setAttribute('selected', ''); } } // if we did not find an item to select, it means that the "index" is not available in the component, therefore we // need to deselect all items else { // we use the component id to be able to find direct children if (component.tagName === 'CORAL-STEPLIST') { // @polyfill IE - we use id since :scope is not supported item = component.querySelector(`#${component.id} > coral-step[selected]`); } else if (component.tagName === 'CORAL-PANELSTACK') { // @polyfill IE - we use id since :scope is not supported item = component.querySelector(`#${component.id} > coral-panel[selected]`); } if (item) { item.removeAttribute('selected'); } } } /** @private */ _selectStep(index) { // we apply the selection to all available steplists this.stepLists.getAll().forEach((stepList) => { this._selectItemByIndex(stepList, index); }); // we apply the selection to all available panelstacks this.panelStacks.getAll().forEach((panelStack) => { this._selectItemByIndex(panelStack, index); }); } /** Sets the correct selected item in every PanelStack. @private */ _syncPanelStackSelection(defaultIndex) { // Find out which step we're on by checking the first StepList let index = this._getSelectedIndex(); if (index === -1) { if (typeof defaultIndex !== 'undefined') { index = defaultIndex; } else { // No panel selected return; } } this.panelStacks.getAll().forEach((panelStack) => { this._selectItemByIndex(panelStack, index); }); } /** Selects the correct step in every StepList. @private */ _syncStepListSelection(defaultIndex) { // Find out which step we're on by checking the first StepList let index = this._getSelectedIndex(); if (index === -1) { if (typeof defaultIndex !== 'undefined') { index = defaultIndex; } else { // No step selected return; } } this.stepLists.getAll().forEach((stepList) => { this._selectItemByIndex(stepList, index); }); } /** Shows the next step. If the WizardView is already in the last step nothing will happen. @emits {coral-wizardview:change} */ next() { const stepList = this.stepLists.first(); if (!stepList) { return; } // Change to the next step stepList.next(); // Select the step everywhere this._selectStep(stepList.items.getAll().indexOf(stepList.selectedItem)); } /** Shows the previous step. If the WizardView is already in the first step nothing will happen. @emits {coral-wizardview:change} */ previous() { const stepList = this.stepLists.first(); if (!stepList) { return; } // Change to the previous step stepList.previous(); // Select the step everywhere this._selectStep(stepList.items.getAll().indexOf(stepList.selectedItem)); } /** @ignore */ render() { super.render(); this.classList.add(CLASSNAME); this._syncStepListSelection(0); this._syncPanelStackSelection(0); // Disable tracking for specific elements that are attached to the component. const selector = '[coral-wizardview-next],[coral-wizardview-previous],[coral-wizardview-steplist],[coral-wizardview-panelstack]'; const items = this.querySelectorAll(selector); for (let i = 0 ; i < items.length ; i++) { items[i].setAttribute('tracking', 'off'); } } /** Triggered when the {@link WizardView} selected step list item has changed. @typedef {CustomEvent} coral-wizardview:change @property {Step} event.detail.selection The new selected step list item. @property {Step} event.detail.oldSelection The prior selected step list item. */ }); export default WizardView;