import { isString, isFunction } from '../core/util'; import { on, createEl, addClass, setStyle, preventDefault } from '../core/util/dom'; import Point from '../geo/Point'; import type { Geometry } from '../geometry'; import type { Map } from '../map'; import UIComponent, { UIComponentOptionsType } from './UIComponent'; /** * @property {Object} options * @property {Boolean} [options.autoPan=false] - set it to false if you don't want the map to do panning animation to fit the opened menu. * @property {Number} [options.width=160] - default width * @property {Number} [options.maxHeight=0] - default max-height * @property {String|HTMLElement} [options.custom=false] - set it to true if you want a customized menu, customized html codes or a HTMLElement is set to items. * @property {Object[]|String|HTMLElement} options.items - html code or a html element is options.custom is true. Or a menu items array, containing: item objects, "-" as a splitor line * @memberOf ui.Menu * @instance */ const defaultOptions: MenuOptionsType = { 'containerClass': 'maptalks-menu', 'animation': null, 'animationDelay': 10, 'animationOnHide': false, 'autoPan': false, 'width': 160, 'maxHeight': 0, 'custom': false, 'items': [] }; /** * @classdesc * Class for context menu, useful for interactions with right clicks on the map. * @category ui * @extends ui.UIComponent * @memberOf ui */ class Menu extends UIComponent { options: MenuOptionsType; /** * Menu items is set to options.items or by setItems method.
*
* Normally items is a object array, containing:
* 1. item object: {'item': 'This is a menu text', 'click': function() {alert('oops! You clicked!');)}}
* 2. minus string "-", which will draw a splitor line on the menu.
*
* If options.custom is set to true, the menu is considered as a customized one. Then items is the customized html codes or HTMLElement.
* @param {Object} options - options defined in [ui.Menu]{@link ui.Menu#options} */ constructor(options: MenuOptionsType) { super(options); } // TODO: obtain class in super //@internal _getClassName() { return 'Menu'; } addTo(owner: Geometry | Map) { if (owner._menu && owner._menu !== this) { owner.removeMenu(); } owner._menu = this; this._owner = owner; return UIComponent.prototype.addTo.apply(this, [owner]); } /** * Set the items of the menu. * @param {Object[]|String|HTMLElement} items - items of the menu * return {ui.Menu} this * @example * menu.setItems([ * //return false to prevent event propagation * {'item': 'Query', 'click': function() {alert('Query Clicked!'); return false;}}, * '-', * {'item': 'Edit', 'click': function() {alert('Edit Clicked!')}}, * {'item': 'About', 'click': function() {alert('About Clicked!')}} * ]); */ setItems(items: Array) { this.options['items'] = items; return this; } /** * Get items of the menu. * @return {Object[]|String|HTMLElement} - items of the menu */ getItems() { return this.options['items'] || []; } /** * Create the menu DOM. * @protected * @return {HTMLElement} menu's DOM */ buildOn(): HTMLElement { let dom: HTMLElement; if (this.options['custom']) { if (isString(this.options['items'])) { const container = createEl('div'); container.innerHTML = this.options['items']; this._appendCustomClass(container); dom = container; } else { dom = this.options['items'] as any; } } else { dom = createEl('div'); if (this.options['containerClass']) { addClass(dom, this.options['containerClass']); } dom.style.width = this._getMenuWidth() + 'px'; /*const arrow = createEl('em'); addClass(arrow, 'maptalks-ico');*/ const menuItems = this._createMenuItemDom(); // dom.appendChild(arrow); dom.appendChild(menuItems); on(dom, 'contextmenu', preventDefault); this._appendCustomClass(dom); } if (dom) { this._bindDomEvents(dom, 'off'); this._bindDomEvents(dom, 'on'); } return dom; } /** * Offset of the menu DOM to fit the click position. * @return {Point} offset * @private */ getOffset() { if (!this.getMap()) { return null; } const mapSize = this.getMap().getSize(), p = this.getMap().viewPointToContainerPoint(this._getViewPoint()), size = this.getSize(); let dx = 0, dy = 0; if (p.x + size['width'] > mapSize['width']) { dx = -size['width']; } if (p.y + size['height'] > mapSize['height']) { dy = -size['height']; } return new Point(dx, dy); } getTransformOrigin() { const p = this.getOffset()._multi(-1); return p.x + 'px ' + p.y + 'px'; } getEvents() { return { '_zoomstart _zoomend _movestart _dblclick _click': this._removePrevDOM }; } //@internal _createMenuItemDom() { const me = this; const map = this.getMap(); const ul = createEl('ul'); addClass(ul, 'maptalks-menu-items'); const items = this.getItems(); function onMenuClick(index) { return function (e) { const param = map._parseEvent(e, 'click'); param['target'] = me; param['owner'] = me._owner; param['index'] = index; const result = this._callback(param); if (result === false) { return; } me.hide(); if (me._owner) { me._owner.fire('closemenu'); } }; } let item, itemDOM; for (let i = 0, len = items.length; i < len; i++) { item = items[i]; if (item === '-' || item === '_') { itemDOM = createEl('li'); addClass(itemDOM, 'maptalks-menu-splitter'); } else { itemDOM = createEl('li'); let itemTitle = item['item']; if (isFunction(itemTitle)) { itemTitle = itemTitle({ 'owner': this._owner, 'index': i }); } itemDOM.innerHTML = itemTitle; itemDOM._callback = item['click']; on(itemDOM, 'click', (onMenuClick)(i)); } ul.appendChild(itemDOM); } const maxHeight = this.options['maxHeight'] || 0; if (maxHeight > 0) { setStyle(ul, 'max-height: ' + maxHeight + 'px; overflow-y: auto;'); } return ul; } //@internal _getMenuWidth() { const defaultWidth = 160; const width = this.options['width'] || defaultWidth; return width; } } Menu.mergeOptions(defaultOptions); export default Menu; export type MenuItem = { name?: string; click?: () => void; } export type MenuOptionsType = { containerClass?: string; animationDelay?: number; animationOnHide?: boolean; autoPan?: boolean; width?: number; maxHeight?: number; custom?: boolean; items?: Array; } & UIComponentOptionsType;