coral-spectrum/coral-component-wizardview/src/scripts/WizardView.js
/**
* 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;