Reference Source

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;