SidePanel.js

import Collection from 'ol/Collection';
import Control from 'ol/control/Control';

import { CLASS_CONTROL, CLASS_UNSELECTABLE } from 'ol/css';

import el from './el';
import SidePanelPane from './SidePanelPane';
import SidePanelTab from './SidePanelTab';

import './SidePanel.css';

/**
 * Top-level {@link external:Control|Control} for ol-side-panel - provides a responsive tabbed left side panel for OpenLayers.
 * @observableProperty {SidePanelPane} activePane - The currently active pane - or null if no pane is active.
 * @extends external:Control
 * @example
 * import { SidePanel } from 'ol-side-panel';
 *
 * const sidePanel = new SidePanel();
 *
 * map.addControl(sidePanel);
 *
 * const layersPane = sidePanel.definePane({
 *   paneId: 'layers',
 *   name: "Layers",
 *   icon: '<i class="bi bi-layers-half"></i>'
 * });
 *
 * const layersGreeting = document.createElement('p');
 * layersGreeting.innerHTML = "Hi there layers!";
 *
 * layersPane.addWidgetElement(layersGreeting);
 */
class SidePanel extends Control {

  constructor() {

    super({
      element: document.createElement('div'),
      activePane: null,
    });

    const { element } = this;
    element.className = `ol-side-panel ${CLASS_UNSELECTABLE} ${CLASS_CONTROL}`;

    window.nextOlSidePanelPaneUniqId = window.nextOlSidePanelPaneUniqId || 0;

    /**
     * @type {external:Collection<SidePanelPane>}
     * @private
     */
    this.panes = new Collection();

    element.appendChild(el('div', { class: 'ol-side-panel-tabs', role: 'tablist' }, (tabList) => {
      this.panes.on('add', (evt) => {
        const pane = evt.element;

        tabList.appendChild(SidePanelTab.fromPane(pane).element_);

        pane.on('change:active', () => {
          if (!pane.get('active') && pane === this.get('activePane')) {
            this.updateActivePane_(null);
          } else if (pane.get('active')) {
            this.updateActivePane_(pane);
          }
        });

      });
      this.panes.on('remove', (evt) => {
        tabList.removeChild(evt.element.tabRef_.element_);
      });

      tabList.appendChild(el('div', { class: 'ol-side-panel-tabs-separator' }));

    }));

    element.appendChild(el('div', { class: 'ol-side-panel-content' }, (sidePanelContent) => {
      this.panes.on('add', evt => sidePanelContent.appendChild(evt.element.element_));
      this.panes.on('remove', (evt) => {
        sidePanelContent.removeChild(evt.element.element_);
        if (evt.element === this.get('activePane')) {
          this.updateActivePane_(null);
        }
      });
    }));

    this.on('change:activePane', () => {
      if (this.get('activePane')) {
        this.getMap().getTargetElement().className = this.getMap().getTargetElement().className.replace(/\bol-side-panel-collapsed\b/g, '');
      } else if (this.getMap().getTargetElement().className.search(/\bol-side-panel-collapsed\b/) === -1) {
        this.getMap().getTargetElement().className += ' ol-side-panel-collapsed';
      }
    });

  }

  /**
   * Defines a new pane.
   * @param {Object} args - pane args
   * @param {String} args.paneId - Locally unique identifier for the pane - used to refer to the pane within an
   *  instance of the ol-side-panel control.
   * @param {String|external:Element} args.name - Name of the pane - User-facing name of this pane.
   * @param {String|external:Element} args.icon - Icon of the pane - User-facing icon for this pane.
   * @param {Number} args.weight - Weight of the pane - controls the order of
   *  the pane tabs with lower numbers appearing closer to the top and higher numbers appearing farther
   *  down. Panes with a weight >= 100 will have their icons appear at the bottom of the tab bar.
   * @returns {SidePanelPane} A reference to the newly defined pane.
   */
  definePane({
    paneId, name, icon, weight,
  }) {
    /* eslint-disable-next-line no-plusplus */
    const paneUniqId = window.nextOlSidePanelPaneUniqId++;
    const pane = new SidePanelPane({
      paneId, paneUniqId, name, icon, weight,
    });
    this.panes.push(pane);
    return pane;
  }

  /**
   * Get the pane with the specified identifier.
   * @param {String} paneId - The identifier of the pane to get - this is the same identifier that was
   *  passed into {@link SidePanel#definePane|definePane}.
   * @returns {SidePanelPane} A reference to the pane.
   */
  getPaneById(paneId) {
    return this.panes.getArray().find(p => p.paneId_ === paneId);
  }

  /**
   * Remove the pane with the specified identifier.
   * @param {String} paneId - The identifier of the pane to remove - this is the same identifier that was
   *  passed into {@link SidePanel#definePane|definePane}.
   */
  removePaneById(paneId) {
    this.removePane_(this.getPaneById(paneId));
  }

  /**
   * Remove the pane with the specified identifier.
   * @param {String} paneId - The identifier of the pane to remove - this is the same identifier that was
   *  passed into {@link SidePanel#definePane|definePane}.
   * @private
   */
  removePane_(pane) {
    this.panes.remove(pane);
  }

  /**
   * Add an HTML element to the named pane as a new {@link SidePanelWidget}.
   * @param {String} paneId The identifier of the pane to add the widget to.
   * @param {external:Element} widgetElement
   * @returns {SidePanelWidget} A reference to the newly added widget.
   */
  addWidgetElement(paneId, widgetElement) {
    return this.getPane(paneId).addWidgetElement(widgetElement);
  }

  updateActivePane_(pane) {
    this.set('activePane', pane);
    this.panes.getArray().filter(p => p !== pane).forEach(p => p.set('active', false));
  }

  /**
   * @param {external:Map} map - The OpenLayers map instance this control is being added to.
   */
  setMap(map) {
    const oldMap = this.getMap();
    if (map === oldMap) {
      return;
    }

    const removeClassFromOldMapTarget = (oldMapTarget) => {
      if (oldMapTarget) {
        oldMapTarget.className.replace(/\bmap-with-ol-side-panel\b/g, '');
      }
    };

    const addClassToNewMapTarget = (newMapTarget) => {
      if (newMapTarget) {

        /* eslint-disable-next-line no-param-reassign */
        newMapTarget.className += ' map-with-ol-side-panel ol-side-panel-collapsed';
      }
    };

    if (oldMap) {
      removeClassFromOldMapTarget(oldMap.getTargetElement());
      // Cleanup the old event listener
      if (this.onMapTargetChange) {
        oldMap.un('change:target', this.onMapTargetChange.listener);
        this.onMapTargetChange = null;
      }
    }
    super.setMap(map);

    if (map) {
      addClassToNewMapTarget(map.getTargetElement());

      const handleMapTargetChange = (evt) => {
        removeClassFromOldMapTarget(evt.oldValue);
        addClassToNewMapTarget(map.getTargetElement());
      };

      this.onMapTargetChange = map.getInteractions().on('change:target', handleMapTargetChange);
    }
  }

}

export default SidePanel;