VisiOmatic web client

source

control/Sidebar.js

/**
 #	This file part of:	VisiOmatic
 * @file Responsive sidebar.

 * @author Emmanuel Bertin <bertin@cfht.hawaii.edu>
 * @copyright (c) 2015-2023 CNRS/IAP/CFHT/SorbonneU.
   Adapted from the leaflet-sidebar plugin by Tobias Bieniek
   (original copyright notice follows).

The MIT License (MIT)

Copyright (c) 2013 Tobias Bieniek

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import {Browser, Control, DomEvent, DomUtil, Evented, Util} from 'leaflet';


export const Sidebar = Control.extend( /** @lends Sidebar */ {
	includes: Evented && Evented.prototype,

	options: {
		title: 'Toggle advanced menu',
		position: 'left',
		collapsed: true,
		forceSeparateButton: false
	},

	/**
	 * Create a new sidebar.

	 * @extends leaflet.Control
	 * @memberof module:control/Sidebar.js
	 * @constructs
	 * @param {object} [options] - Options.

	 * @param {string} [options.title='Toggle advanced menu']
	   Title of the panel.

	 * @param {'left'|'right'} [options.position='left']
	   Position of the sidebar.

	 * @param {boolean} [options.collapsed=true]
	   Sidebar initially in closed position?

	 * @param {boolean} [options.forceSeparateButton=false]
	   Sidebar button separate from zoom control?

	 * @returns {Sidebar} Instance of a sidebar.
	 */
	initialize: function (options) {

		Util.setOptions(this, options);

		// Create sidebar
		this._sidebar = DomUtil.create('div', 'leaflet-container sidebar');
		if (this.options.collapsed) {
			DomUtil.addClass(this._sidebar, 'collapsed');
		} else {
			DomUtil.addClass(this._sidebar, 'closed');
		}
		// Attach .sidebar-left/right class
		DomUtil.addClass(this._sidebar, 'sidebar-' + this.options.position);

		// Attach touch styling if necessary
		if (Browser.touch) {
			DomUtil.addClass(this._sidebar, 'leaflet-touch');
		}

		// Create containers for tabs and their contents (panes)
		this._tabs = DomUtil.create('div', 'sidebar-tabs', this._sidebar);
		this._tabitems = [];

		this._container = DomUtil.create('div', 'sidebar-content', this._sidebar);
		this._panes = [];
		this._closeButtons = [];
	},

	/**
	 * Add this sidebar to the specified map.
	 *
	 * @param {L.Map} map
	   Leaflet map the control is to be added to.
	 * @returns {Sidebar}
	   The sidebar (this).
	 */
	addTo: function (map) {
		const	className = 'leaflet-control-zoom-sidebar',
			parent = map._controlContainer;
		var	buttonContainer;
	
		// Create sidebar
		DomUtil.addClass(map._container, 'sidebar-map');
		parent.insertBefore(this._sidebar, parent.firstChild);
		DomEvent
			.disableClickPropagation(this._sidebar)
			.disableScrollPropagation(this._sidebar);

		this._map = map;

		// Create sidebar toggle button
		if (map.zoomControl && !this.options.forceSeparateButton) {
			buttonContainer = map.zoomControl._container;
		} else {
			buttonContainer = DomUtil.create('div', 'leaflet-bar');
		}
		
		this._toggleButton = this._addButton(this.options.title,
		  className + (this.options.collapsed ? ' collapsed' : ''), buttonContainer);

		return this;
	},

	/**
	 * Add sidebar tab list.
	 *
	 * @returns {object} Tab list element.
	 */
	addTabList: function () {
		this._tablist = DomUtil.create('ul', '', this._tabs);
		this._tablist.setAttribute('role', 'tablist');
		return this._tablist;
	},

	/**
	 * Add sidebar tab and its pane.
	 *
	 * @param {string} id
	   Element id of the tab pane (and part of the tab id).
	 * @param {string} className
	   Class name of the tab.
	 * @param {string} [title]
	   Title of the tab.
	 * @param {object} content
	   Element containing the tab content.
	 * @param {string} [sideClass] 
	   Class name of an additional element which may be opened or close in
	   parallel to the tab.
	 * @returns {object} The newly created tab pane.
	 */
	addTab: function (id, className, title, content, sideClass) {
		const	tablist = this._tablist ? this._tablist : this.addTabList(),
			item = DomUtil.create('li', '', tablist),
			button = DomUtil.create('a', className, item);

		item.setAttribute('role', 'tab');
		item._sidebar = this;
		button.href = '#' + id;
		button.id = id + '-toggle';
		button.title = title;
		DomEvent.on(button, 'click', L.DomEvent.preventDefault);
		DomEvent.on(button, 'click', this._onClick, item);
		item.sideClass = sideClass;
		this._tabitems.push(item);

		// Sidebar pane
		const	pane = DomUtil.create('div', 'sidebar-pane', this._container),
			header = DomUtil.create('h1', 'sidebar-header', pane);

		header.innerHTML = title;

		const	closeButton = DomUtil.create('div', 'sidebar-close', header),
			closeIcon = DomUtil.create('div', 'sidebar-close-icon', closeButton);
		this._closeButtons.push(closeButton);
		DomEvent.on(closeButton, 'click', this._onCloseClick, this);
		pane.id = id;
		pane.sideClass = sideClass;
		pane.appendChild(content);
		this._panes.push(pane);
		return pane;
	},

	/**
	 * Remove this sidebar from the map.
	 *
	 * @param {L.Map} map
	   Leaflet map the control had been added to.
	 * @returns {Sidebar}
	   The sidebar (this).
	 */
	removeFrom: function (map) {

		this._map = null;

		for (var i = this._tabitems.length - 1; i >= 0; i--) {
			var	child = this._tabitems[i];
			DomEvent.off(child.querySelector('a'), 'click', this._onClick);
		}

		for (var i = this._closeButtons.length - 1; i >= 0; i--) {
			var	child = this._closeButtons[i];
			DomEvent.off(child, 'click', this._onCloseClick, this);
		}

		return this;
	},

	/**
	 * Open sidebar (if necessary) and show the specified tab.
	 *
	 * @param {string} id - The id of the tab to show (without the # character)
	 * @returns {Sidebar}
	   The sidebar (this).
	 */
	open: function (id) {

		// hide old active contents and show new content
		for (var i = this._panes.length - 1; i >= 0; i--) {
			var	child = this._panes[i];
			if (child.id === id) {
				DomUtil.addClass(child, 'active');
				if (child.sideClass) {
					DomUtil.addClass(this._sidebar, child.sideClass);
				}
			} else if (DomUtil.hasClass(child, 'active')) {
				DomUtil.removeClass(child, 'active');
				if (child.sideClass) {
					DomUtil.removeClass(this._sidebar, child.sideClass);
				}
			}
		}

		// remove old active highlights and set new highlight
		for (var i = this._tabitems.length - 1; i >= 0; i--) {
			var	child = this._tabitems[i];
			if (child.querySelector('a').hash === '#' + id) {
				DomUtil.addClass(child, 'active');
			} else if (DomUtil.hasClass(child, 'active')) {
				DomUtil.removeClass(child, 'active');
			}
		}

		this.fire('content', {id: id});

		// open sidebar (if necessary)
		if (DomUtil.hasClass(this._sidebar, 'closed')) {
			this.fire('opening');
			DomUtil.removeClass(this._sidebar, 'closed');
		}

		return this;
	},

	/**
	 * Close the sidebar (if necessary).
	 *
	 * @returns {Sidebar}
	   The sidebar (this).
	 */
	close: function () {
		// remove old active highlights
		for (var i = this._tabitems.length - 1; i >= 0; i--) {
			var child = this._tabitems[i];
			if (DomUtil.hasClass(child, 'active')) {
				DomUtil.removeClass(child, 'active');
				if (child.sideClass) {
					DomUtil.removeClass(this._sidebar, child.sideClass);
				}
			}
		}

		// close sidebar
		if (!DomUtil.hasClass(this._sidebar, 'closed')) {
			this.fire('closing');
			DomUtil.addClass(this._sidebar, 'closed');
		}

		return this;
	},

	/**
	 * Collapse/Expand the sidebar.
	 */
	toggle: function () {
		this.close();
		if (DomUtil.hasClass(this._sidebar, 'collapsed')) {
			DomUtil.addClass(this._sidebar, 'closed');
			this.fire('expanding');
			DomUtil.removeClass(this._sidebar, 'collapsed');
			DomUtil.removeClass(this._toggleButton, 'collapsed');
		} else {
			DomUtil.removeClass(this._sidebar, 'closed');
			this.fire('collapsing');
			DomUtil.addClass(this._sidebar, 'collapsed');
			DomUtil.addClass(this._toggleButton, 'collapsed');
		}
	},

	/**
	 * Actions performed when clicking on the sidebar toggle button.
	 * @private
	 */
	_onClick: function () {
		if (DomUtil.hasClass(this, 'active')) {
			this._sidebar.close();
		} else if (!DomUtil.hasClass(this, 'disabled')) {
			this._sidebar.open(this.querySelector('a').hash.slice(1));
		}
	},

	/**
	 * Actions performed when clicking on the sidebar close button.
	 * @private
	 */
	_onCloseClick: function () {
		this.close();
	},

	/**
	 * Add a button
	 *
	 * @private
	 * @param {string} [title]
	   Title of the button.
	 * @param {string} className
	   Class name of the button.
	 * @param {object} container
	   Element containing the button.
	 * @returns {object} New link (button) element.
	 */
	_addButton: function (title, className, container) {
		const	link = DomUtil.create('a', className, container);
		link.href = '#';
		link.title = title;

		DomEvent
			.addListener(link, 'click', DomEvent.stopPropagation)
			.addListener(link, 'click', DomEvent.preventDefault)
			.addListener(link, 'click', this.toggle, this);

		return link;
	}

});

/**
 * Instantiate a sidebar.
 *
 * @function
 * @param {object} [options] - Options: see {@link Sidebar}
 * @returns {Sidebar} Instance of a sidebar.

 * @example
 * const sb = sidebar({position: right}).addTo(map);
 */
export const sidebar = function (map, options) {
	return new Sidebar(map, options);
};