Source: src/js/views/maps/ToolbarView.js

'use strict';

define(
  [
    'jquery',
    'underscore',
    'backbone',
    'text!templates/maps/toolbar.html',
    'models/maps/Map',
    'common/IconUtilities',
    // Sub-views - TODO: import these as needed
    'views/maps/LayersPanelView',
    'views/maps/DrawToolView',
    'views/maps/HelpPanelView',
    'views/maps/viewfinder/ViewfinderView',
  ],
  function (
    $,
    _,
    Backbone,
    Template,
    Map,
    IconUtilities,
    // Sub-views
    LayersPanelView,
    DrawTool,
    HelpPanel,
    ViewfinderView,
  ) {

    /**
    * @class ToolbarView
    * @classdesc The map toolbar view is a side bar that contains information about a map,
    * including the available layers, plus UI for changing the settings of a map.
    * @classcategory Views/Maps
    * @name ToolbarView
    * @extends Backbone.View
    * @screenshot views/maps/ToolbarView.png
    * @since 2.18.0
    * @constructs
    */
    var ToolbarView = Backbone.View.extend(
      /** @lends ToolbarView.prototype */{

        /**
        * The type of View this is
        * @type {string}
        */
        type: 'ToolbarView',

        /**
        * The HTML classes to use for this view's element
        * @type {string}
        */
        className: 'toolbar',

        /**
        * The model that this view uses
        * @type {Map}
        */
        model: null,

        /**
         * The primary HTML template for this view. The template must have two element,
         * one with the contentContainer class, and one with the linksContainer class.
         * See {@link ToolbarView#classes}.
         * @type {Underscore.template}
         */
        template: _.template(Template),

        /**
         * The classes of the sub-elements that combined to create a toolbar view.
         *
         * @name ToolbarView#classes
         * @type {Object}
         * @property {string} open The class to add to the view when the toolbar is open
         * (and the content is visible)
         * @property {string} contentContainer The element that contains all containers
         * for the toolbar section content. This element must be part of this view's
         * template.
         * @property {string} linksContainer The container for all of the section links
         * (i.e. tabs)
         * @property {string} link A section link
         * @property {string} linkTitle The section link title
         * @property {string} linkIcon The section link icon
         * @property {string} linkActive The class to add to a link when its content is
         * active
         * @property {string} content A section's content. This element will be the
         * container for the view associated with this section.
         * @property {string} contentActive A class added to a content container when it
         * is the active section
         */
        classes: {
          open: 'toolbar--open',
          contentContainer: 'toolbar__all-content',
          linksContainer: 'toolbar__links',
          link: 'toolbar__link',
          linkTitle: 'toolbar__link-title',
          linkIcon: 'toolbar__link-icon',
          linkActive: 'toolbar__link--active',
          content: 'toolbar__content',
          contentActive: 'toolbar__content--active'
        },

        /**
         * A string that represents an icon. Can be either the name of the Font Awesome
         * 3.2 icon OR an SVG string for an icon with all the following properties: 1)
         * Uses viewBox attribute and not width/height; 2) Sets fill or stroke to
         * "currentColor" in the svg element, no styles included elsewhere, 3) Has the
         * required xmlns attribute
         *
         * @typedef {string} MapIconString
         *
         * @see {@link https://fontawesome.com/v3.2/icons/}
         *
         * @example
         * '<svg viewBox="0 0 400 110" fill="currentColor"><path d="M0 0h300v100H0z"/></svg>'
         * @example
         * 'map-marker'
         */

        /**
         * Options/settings that are used to create a toolbar section and its associated
         * link/tab.
         *
         * @typedef {Object} SectionOption
         * @property {string} label The name of this section to show to the user.
         * @property {MapIconString} icon The icon to show in the link (tab) for this
         * section
         * @property {Backbone.View} [view] The view that renders the content of the
         * toolbar section.
         * @property {object} [viewOptions] Any additional options to pass to the content
         * view. By default, the label, icon, and Map model will be passed to the view as
         * 'label', 'icon', and 'model', respectively. To pass a specific attribute from
         * the Map model, use a string with the syntax 'model.desiredAttribute'. For
         * example, 'model.layers' will be converted to view.model.get('layers')
         * @property {function} [action] A function to call when the link/tab is clicked.
         * This can be provided instead of a view and viewOptions, in which case no
         * toolbar section will be created. The function will be passed the view and the
         * Map model as arguments.
         * @property {function} [isVisible] A function that determines whether this 
         * section should be visible in the toolbar.
         */

        /**
         * The sections displayed in the toolbar will be created based on the options set
         * in this array.
         * 
         * @type {SectionOption[]}
         */
        sectionOptions: [
          {
            label: 'Layers',
            icon: '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="m3.2 7.3 8.6 4.6a.5.5 0 0 0 .4 0l8.6-4.6a.4.4 0 0 0 0-.8L12.1 3a.5.5 0 0 0-.4 0L3.3 6.5a.4.4 0 0 0 0 .8Z"/><path d="M20.7 10.7 19 9.9l-6.7 3.6a.5.5 0 0 1-.4 0L5 9.9l-1.8.8a.5.5 0 0 0 0 .8l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.8Z"/><path d="m20.7 15.1-1.5-.7-7 3.8a.5.5 0 0 1-.4 0l-7-3.8-1.5.7a.5.5 0 0 0 0 .9l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.9Z"/></svg>',
            view: LayersPanelView,
            isVisible(model) {
              return model.get('showLayerList');
            },
          },
          {
            label: 'Home',
            icon: 'home',
            action: function (view, model) {
              model.flyHome();
            },
            isVisible(model) {
              return model.get("showHomeButton");
            },
          },
          // We can enable to the draw tool once we have a use case for it
          // {
          //   label: 'Draw',
          //   icon: 'pencil',
          //   view: DrawTool,
          //   viewOptions: {}
          // },
          {
            label: 'Viewfinder',
            icon: 'search',
            view: ViewfinderView,
            action(view, model) {
              const sectionEl = this;
              view.defaultActivationAction(sectionEl);
              sectionEl.sectionView.focusInput();
            },
            isVisible(model) {
              return MetacatUI.mapKey && model.get("showViewfinder");
            },
          },
          {
            label: 'Help',
            icon: 'question-sign',
            view: HelpPanel,
            viewOptions: {
              showFeedback: 'model.showFeedback',
              feedbackText: 'model.feedbackText',
              showNavHelp: 'model.showNavHelp',
            },
            isVisible(model) {
              return model.get("showNavHelp") || model.get("showFeedback");
            },
          }
        ],

        /**
         * Whether or not the toolbar menu is opened. This will get updated when the user
         * interacts with the toolbar links.
         * @type {Boolean}
         */
        isOpen: false,

        /**
        * Executed when a new ToolbarView is created
        * @param {Object} [options] - A literal object with options to pass to the view
        */
        initialize: function (options) {

          try {
            // Get all the options and apply them to this view
            if (typeof options == 'object') {
              for (const [key, value] of Object.entries(options)) {
                this[key] = value;
              }
            }
            if (!this.model || !(this.model instanceof Map)) {
              this.model = new Map();
            }

            if (this.model.get('toolbarOpen') === true) {
              this.isOpen = true;
            }

            // Check whether each section should be shown, defaulting to true.
            this.sections = this.sectionOptions.filter(section => {
              return typeof section.isVisible === 'function'
                ? section.isVisible(this.model)
                : true;
            });
          } catch (e) {
            console.log('Error initializing a ToolbarView', e);
          }

        },

        /**
        * Renders this view
        * @return {ToolbarView} Returns the rendered view element
        */
        render: function () {

          try {

            // Save a reference to this view
            var view = this;

            // Insert the template into the view
            this.$el.html(this.template({}));

            // Ensure the view's main element has the given class name
            this.el.classList.add(this.className);

            // Select and save a reference to the elements that will contain the
            // links/tabs and the section content.
            this.contentContainer = document.querySelector(
              '.' + view.classes.contentContainer
            )
            this.linksContainer = document.querySelector(
              '.' + view.classes.linksContainer
            )

            // sectionElements will store the section link, section content element, and
            // the status of the given section (whether it is active or not)
            this.sectionElements = [];

            // For each section configured in the view's sections property, create a link
            // and render the content. Set a listener for when the link is clicked. 
            this.sections.forEach(function (sectionOption) {
              // Render the link and content elements
              var linkEl = view.renderSectionLink(sectionOption)
              var action = sectionOption.action
              let contentEl = null;
              let sectionView;
              if (sectionOption.view) {
                const { contentContainer, sectionContent } = view.renderSectionContent(sectionOption)
                contentEl = contentContainer;
                sectionView = sectionContent;
              }
              // Set the section to false to start
              var isActive = false
              // Save a reference to these elements and their status. sectionEl is an
              // object that has type SectionElement (documented in comments below)
              var sectionEl = { linkEl, contentEl, isActive, action, sectionView };
              view.sectionElements.push(sectionEl)
              // Attach the link and content to the view
              if (contentEl) {
                view.contentContainer.appendChild(contentEl);
              }
              view.linksContainer.appendChild(linkEl);
              // Add a listener that shows the section when the link is clicked
              linkEl.addEventListener('click', function () {
                view.handleLinkClick(sectionEl)
              });
            })

            // Set the toolbar to open, depending on what is initially set in view.isOpen.
            // Set the first section to active if the toolbar is open.
            if (this.isOpen) {
              this.el.classList.add(this.classes.open)
              view.handleLinkClick(this.sectionElements[0])
            }

            return this

          }
          catch (error) {
            console.log(
              'There was an error rendering a ToolbarView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * A reference to all of the elements required to make up a toolbar section: the
         * section content and the section link (i.e. tab); as well as the status of the
         * section: active or in active.
         *
         * @typedef {Object} SectionElement
         * @property {HTMLElement} contentEl The element that contains the toolbar
         * section's content (the content rendered by the associated view)
         * @property {HTMLElement} linkEl The element that acts as a link to show the
         * section's content, and open/close the toolbar.
         * @property {Boolean} isActive True if this is the active section, false
         * otherwise.
         * @property {Backbone.View} sectionView The associated Backbone.View instance. 
         */

        /**
         * Executed when any one of the tabs/links are clicked. Opens the toolbar if it's
         * closed, closes it if the active section is clicked, and otherwise activates the
         * clicked section content.
         * @param {SectionElement} sectionEl
         */
        handleLinkClick: function (sectionEl) {
          try {
            var toolbarOpen = this.isOpen;
            var sectionActive = sectionEl.isActive;
            if (toolbarOpen && sectionActive) {
              this.close()
              return
            }
            if (!toolbarOpen && sectionEl.contentEl) {
              this.open()
            }
            if (!sectionActive) {
              if (sectionEl.contentEl) {
                this.inactivateAllSections()
              }
              this.activateSection(sectionEl)
            }
          }
          catch (error) {
            console.log(
              'There was an error handling a toolbar link click in a ToolbarView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Creates a link/tab for a given toolbar section
         * @param {SectionOption} sectionOption The label and icon that are set in the
         * Section Option are used to create the link content
         * @returns {HTMLElement} Returns the link element
         */
        renderSectionLink: function (sectionOption) {
          try {

            // Create a container, label
            const link = document.createElement('div')
            const title = document.createElement('div')
            // Create the icon
            const icon = this.createIcon(sectionOption.icon)

            // Add the relevant classes
            link.classList.add(this.classes.link)
            title.classList.add(this.classes.linkTitle)
            // Add the label text
            title.textContent = sectionOption.label

            link.append(icon, title)

            return link
          }
          catch (error) {
            console.log(
              'There was an error rendering a section link in a ToolbarView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Given the name of a Font Awesome 3.2 icon, or an SVG string, creates an icon
         * element with the appropriate classes for the tool bar link (tab)
         * @param {MapIconString} iconString The string to use to create the icon
         * @returns {HTMLElement} Returns either an <i> element with a Font Awesome icon,
         * or and SVG with a custom icon
         */
        createIcon: function (iconString) {
          try {
            // The icon element we will create and return. By default, return an empty span
            // element.
            let icon = document.createElement('span');

            // iconString must be string
            if (typeof iconString === 'string') {
              // If the icon is an SVG element
              if (IconUtilities.isSVG(iconString)) {
                icon = new DOMParser()
                  .parseFromString(iconString, 'image/svg+xml')
                  .documentElement;
                // If the icon is not an SVG, assume it's the name for a Font Awesome icon
              } else {
                icon = document.createElement('i')
                icon.className = 'icon-' + iconString;
              }

            }
            icon.classList.add(this.classes.linkIcon);
            return icon
          }
          catch (error) {
            console.log(
              'There was an error  in a ToolbarView' +
              '. Error details: ' + error
            );
            return document.createElement('span')
          }
        },

        /**
         * @typedef {Object} SectionContentReturnType
         * @property {HTMLElement} contentContainer - The content container HTML
         * element.
         * @property {Backbone.View} sectionContent - The Backbone.View instance
         */

        /**
         * Creates a container for a toolbar section's content, then rendered the
         * specified view in that container.
         * @param {SectionOption} sectionOption The view and view options that are set in
         * the Section Option are used to create the content container
         * @returns {SectionContentReturnType} The content container with the
         * rendered view, and the Backbone.View itself.
         */
        renderSectionContent: function (sectionOption) {
          try {
            const view = this
            // Create the container for the toolbar section content
            var contentContainer = document.createElement('div')
            // Add the class that identifies a toolbar section's content
            contentContainer.classList.add(this.classes.content)
            // Render the toolbar section view
            // Merge the icon and label with the other section options
            var viewOptions = Object.assign(
              {
                label: sectionOption.label,
                icon: sectionOption.icon,
                model: this.model
              },
              sectionOption.viewOptions
            )
            // Convert any values in the form of 'model.someAttribute' to the model
            // attribute that is specified.
            for (const [key, value] of Object.entries(viewOptions)) {
              if (typeof value === 'string' && value.startsWith('model.')) {
                const attr = value.replace(/^model\./, '')
                viewOptions[key] = view.model.get(attr)
              }
            }
            var sectionContent = new sectionOption.view(viewOptions)
            contentContainer.appendChild(sectionContent.el)
            sectionContent.render()
            return { contentContainer, sectionContent }
          }
          catch (error) {
            console.log('Error rendering ToolbarView section', error);
          }
        },

        /**
         * Opens the toolbar and displays the content of the active toolbar section
         */
        open: function () {
          try {
            this.isOpen = true
            this.el.classList.add(this.classes.open)
          }
          catch (error) {
            console.log(
              'There was an error opening a ToolbarView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Closes the toolbar. Also inactivates all sections.
         */
        close: function () {
          try {
            this.isOpen = false
            this.el.classList.remove(this.classes.open)
            // Ensure that no section is active when the toolbar is closed
            this.inactivateAllSections()
          }
          catch (error) {
            console.log(
              'There was an error closing a ToolbarView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Display the content of a given section
         * @param {SectionElement} sectionEl The section to activate
         */
        activateSection: function (sectionEl) {
          if (!sectionEl) return;
          try {
            if (sectionEl.action && typeof sectionEl.action === 'function') {
              const view = this;
              const model = this.model;
              sectionEl.action(view, model)
            } else {
              this.defaultActivationAction(sectionEl);
            }
          }
          catch (error) {
            console.log('Failed to show a section in a ToolbarView', error);
          }
        },

        /**
         * The default action for a section being activated. 
         * @param {SectionElement} sectionEl The section to activate
         */
        defaultActivationAction(sectionEl) {
          sectionEl.isActive = true;
          sectionEl.contentEl.classList.add(this.classes.contentActive)
          sectionEl.linkEl.classList.add(this.classes.linkActive)
        },

        /**
         * Hide the content of a section
         * @param {SectionElement} sectionEl The section to inactivate
         */
        inactivateSection: function (sectionEl) {
          try {
            sectionEl.isActive = false;
            if (sectionEl.contentEl) {
              sectionEl.contentEl.classList.remove(this.classes.contentActive)
              sectionEl.linkEl.classList.remove(this.classes.linkActive)
            }
          }
          catch (error) {
            console.log(
              'There was an error showing a toolbar section in a ToolbarView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Hide all of the sections in a toolbar view
         */
        inactivateAllSections: function () {
          try {
            var view = this;
            this.sectionElements.forEach(function (sectionEl) {
              view.inactivateSection(sectionEl)
            })
          }
          catch (error) {
            console.log(
              'There was an error hiding toolbar sections in a ToolbarView' +
              '. Error details: ' + error
            );
          }
        },

      }
    );

    return ToolbarView;

  }
);