Reference Source

coral-spectrum/coral-component-shell/src/scripts/ShellHelp.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-search';
import {AnchorList} from '../../../coral-component-list';
import '../../../coral-component-wait';
import {commons, i18n} from '../../../coral-utils';
import help from '../templates/help';
import helpResult from '../templates/helpResult';
import helpSearchError from '../templates/helpSearchError';
import noHelpResults from '../templates/noHelpResults';

const CLASSNAMES = ['_coral-Menu', '_coral-AnchorList', '_coral-Shell-help'];

/**
 @class Coral.Shell.Help
 @classdesc A Shell Help component
 @htmltag coral-shell-help
 @extends {HTMLElement}
 @extends {BaseComponent}
 */
class ShellHelp extends BaseComponent(HTMLElement) {
  /** @ignore */
  constructor() {
    super();

    // Prepare templates
    this._elements = {};
    help.call(this._elements, {commons, i18n});

    // Events
    this._delegateEvents({
      'coral-search:clear': '_showItems',
      'coral-search:submit': '_performSearch'
    });

    // Item handling
    this.items._startHandlingItems(true);
  }

  /**
   The item collection.

   @type {Collection}
   @readonly
   */
  get items() {
    // Construct the collection on first request:
    if (!this._items) {
      this._items = new Collection({
        host: this,
        itemTagName: 'coral-shell-help-item',
        itemBaseTagName: 'a',
        container: this._elements.items
      });
    }

    return this._items;
  }

  /**
   The search field placeholder.

   @type {String}
   @default ""
   @htmlattribute placeholder
   */
  get placeholder() {
    return this._elements.search.placeholder;
  }

  set placeholder(value) {
    this._elements.search.placeholder = value;
  }

  /** @private */
  _moveItems() {
    this.setAttribute('id', this.id || commons.getUID());

    const selector = `#${this.id} > a[is="coral-shell-help-item"], coral-shell-help-separator`;
    Array.prototype.forEach.call(this.querySelectorAll(selector), (item) => {
      this._elements.items.appendChild(item);
    });
  }

  /** @private */
  _performSearch(event) {
    event.stopPropagation();

    // Show loading
    this._elements.items.hidden = true;
    this._showLoading();
    this._elements.resultMessage.hidden = true;
    this._elements.results.hidden = true;

    // Trigger event
    const searchTerm = this._elements.search.value;
    this.trigger('coral-shell-help:search', {
      value: searchTerm
    });
  }

  /** @private */
  _showItems(event) {
    event.stopPropagation();

    // Hide search results
    this._elements.results.hidden = true;

    // Hide loading
    this._hideLoading();

    // Hide no-results
    this._elements.resultMessage.hidden = true;

    // Show items
    this._elements.items.hidden = false;
  }

  /** @private */
  _clearTimeout(timeoutName) {
    if (this[timeoutName]) {
      window.clearTimeout(this[timeoutName]);
      this[timeoutName] = undefined;
    }
  }

  /** @private */
  _showMessage(elementName, message) {
    const el = this._elements[elementName];
    const timeoutName = `_${elementName}Timeout`;

    // Show message element
    el.hidden = false;

    // Add message text after 150ms delay to give screen readers enough
    // time to recognize the live region and respond to the text update
    this._clearTimeout(timeoutName);
    this[timeoutName] = window.setTimeout(() => el.appendChild(message), 150);
  }

  /** @private */
  _showLoading() {
    if (!this._elements.loading.hidden) {
      return;
    }

    if (this._elements.loading.contains(this._elements.loadingMessage)) {
      this._elements.loadingMessage = this._elements.loading.removeChild(this._elements.loadingMessage);
    }

    this._showMessage('loading', this._elements.loadingMessage);
  }

  /** @private */
  _hideLoading() {
    if (this._elements.loading.hidden) {
      return;
    }

    this._elements.loading.hidden = true;

    // clear the timeout
    this._clearTimeout('_loadingTimeout');
    if (this._elements.loading.contains(this._elements.loadingMessage)) {
      this._elements.loadingMessage = this._elements.loading.removeChild(this._elements.loadingMessage);
    }
  }

  /**
   Indicate to the user that an error has occurred
   */
  showError() {
    // Hide loading
    this._hideLoading();

    this._elements.resultMessage.innerHTML = '';

    // Show the error message
    this._showMessage('resultMessage', helpSearchError.call(this._elements, {i18n}));
  }

  /**
   Show a set of search results.

   @param {Array.<ShellHelpResult>} results
   A set of search result objects.
   @param {Number} total
   The total number of results.
   @param {String} allResultsURL
   The URL at which all results will be displayed.
   */
  showResults(results, total, allResultsURL) {
    // Hide loading
    this._hideLoading();

    // clear setTimeout
    if (this._showResultsTimeout) {
      window.clearTimeout(this._showResultsTimeout);
      this._showResultsTimeout = undefined;
    }

    if (!results || total === 0) {
      // Clear existing result message
      this._elements.resultMessage.innerHTML = '';
      // Indicate to the user that no results were found
      this._showMessage('resultMessage', noHelpResults.call(this._elements, {i18n}));
    } else {
      // Clear existing results
      this._elements.results.innerHTML = '';
      // Populate results
      results.forEach((result) => {
        // Tweak: make the space between bullets larger with a non-breaking space
        const separator = ' & ';
        const description = result.tags.join(separator);

        const item = new AnchorList.Item().set({
          href: result.href,
          target: result.target
        });

        item.classList.add('_coral-Shell-help-result-item');

        item.content = helpResult.call(this._elements, {
          title: result.title,
          description: description
        }).firstElementChild;

        this._elements.results.appendChild(item);
      });

      // Show results
      this._elements.results.hidden = false;

      // Show total
      if (total > 1) {
        const seeAllItem = new AnchorList.Item().set({
          href: allResultsURL,
          content: {
            innerHTML: i18n.get('See all {0} results', total)
          },
          target: '_blank'
        });

        // Look like a link
        seeAllItem.content.classList.add('coral-Link');

        this._elements.results.appendChild(seeAllItem);
      }
    }
  }

  /** @ignore */
  static get observedAttributes() {
    return super.observedAttributes.concat(['placeholder']);
  }

  /** @ignore */
  render() {
    super.render();

    this.classList.add(...CLASSNAMES);

    // Move the items into the right place
    this._moveItems();

    const contentWrapper = this.querySelector('[handle="contentWrapper"]');

    // Support cloneNode
    if (contentWrapper) {
      this._elements.contentWrapper = contentWrapper;

      ['search', 'result', 'items', 'results', 'resultMessage', 'loading'].forEach((handle) => {
        this._elements[handle] = this.querySelector(`[handle="${handle}"]`);
      });
      this._items._container = this._elements.items;
    } else {
      this.appendChild(this._elements.contentWrapper);
    }
  }

  /**
   A search result object.

   @typedef {Object} ShellHelpResult

   @property {String} title
   The title of the search result.
   @property {String} href
   The URL of the search result.
   @property {String} target
   This property specifies where to display the search result. Use this property only if the href property is present.
   @property {Array.<String>} tags
   A set of tags associated with the search result.
   */

  /**
   Triggered when the user submits a search term

   @event Coral.Shell.Help#coral-shell-help:search

   @param {Object} event
   Event object.
   @param {HTMLElement} event.detail.value
   The user-provided input value aka the search-term
   */
}

export default ShellHelp;