UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

260 lines 8.87 kB
import { UIElement, UIElementView } from "../ui_element"; import { MenuItem } from "./menu_item"; import { DividerItem } from "./divider_item"; import { apply_icon } from "../../common/resolve"; import { isFunction } from "../../../core/util/types"; import { div, px } from "../../../core/dom"; import { Or, Ref, Null } from "../../../core/kinds"; import { build_views, remove_views } from "../../../core/build_views"; import { reversed as reverse } from "../../../core/util/array"; import { execute } from "../../../core/util/callbacks"; import menus_css, * as menus from "../../../styles/menus.css"; import icons_css from "../../../styles/icons.css"; function to_val(val) { return isFunction(val) ? val() : val; } export const MenuItemLike = Or(Ref(MenuItem), Ref(DividerItem), Null); export class MenuView extends UIElementView { static __name__ = "MenuView"; _menu_views = new Map(); *children() { yield* super.children(); yield* this._menu_views.values(); } _menu_items = []; get menu_items() { const items = this._menu_items; const { reversed } = this.model; return reversed ? reverse(items) : items; } _compute_menu_items() { return this.model.items; } _update_menu_items() { this._menu_items = this._compute_menu_items(); } get is_empty() { return this.menu_items.length == 0; } initialize() { super.initialize(); this._update_menu_items(); } async lazy_initialize() { await super.lazy_initialize(); const menus = this.menu_items .filter((item) => item instanceof MenuItem) .map((item) => item.menu) .filter((menu) => menu != null); await build_views(this._menu_views, menus, { parent: this }); } connect_signals() { super.connect_signals(); const { items } = this.model.properties; this.on_change(items, () => this._update_menu_items()); } prevent_hide; _open = false; get is_open() { return this._open; } _item_click = (item) => { if (!to_val(item.disabled)) { const { action } = item; if (action != null) { void execute(action, this.model, { item }); } this.hide(); } }; _on_mousedown = (event) => { if (event.composedPath().includes(this.el)) { return; } if (this.prevent_hide?.(event) ?? false) { return; } this.hide(); }; _on_keydown = (event) => { switch (event.key) { case "Escape": { this.hide(); break; } default: } }; _on_blur = () => { this.hide(); }; remove() { this._unlisten(); remove_views(this._menu_views); super.remove(); } _listen() { document.addEventListener("mousedown", this._on_mousedown); document.addEventListener("keydown", this._on_keydown); window.addEventListener("blur", this._on_blur); } _unlisten() { document.removeEventListener("mousedown", this._on_mousedown); document.removeEventListener("keydown", this._on_keydown); window.removeEventListener("blur", this._on_blur); } stylesheets() { return [...super.stylesheets(), menus_css, icons_css]; } render() { super.render(); const items = this.menu_items; const entries = []; if (items.length == 0) { return; } for (const item of items) { if (item instanceof MenuItem) { const check_el = div({ class: menus.check }); const label_el = div({ class: menus.label }, item.label); const shortcut_el = div({ class: menus.shortcut }, item.shortcut); const chevron_el = div({ class: menus.chevron }); const icon_el = (() => { const { icon } = item; if (icon != null) { const icon_el = div({ class: menus.icon }); apply_icon(icon_el, icon); return icon_el; } else { return null; } })(); const item_el = div({ class: menus.item, title: item.tooltip, tabIndex: 0 }, check_el, icon_el, label_el, shortcut_el, chevron_el); const has_menu = item.menu != null && !this._menu_views.get(item.menu).is_empty; item_el.classList.toggle(menus.menu, has_menu); item_el.classList.toggle(menus.disabled, to_val(item.disabled)); if (item.checked != null) { item_el.classList.add(menus.checkable); item_el.classList.toggle(menus.checked, to_val(item.checked)); } const show_submenu = (item) => { if (item.menu != null) { const menu_view = this._menu_views.get(item.menu); menu_view._show_submenu(item_el); } }; const hide_submenu = (item) => { if (item.menu != null) { const menu_view = this._menu_views.get(item.menu); menu_view.hide(); } }; function is_target(event) { const { currentTarget, target } = event; return currentTarget instanceof Node && target instanceof Node && currentTarget.contains(target); } item_el.addEventListener("click", (event) => { if (is_target(event)) { this._item_click(item); } else { this.hide(); } }); item_el.addEventListener("keydown", (event) => { // TODO https://github.com/bokeh/bokeh/issues/14241 switch (event.key) { case "Enter": { this._item_click(item); break; } case "ArrowDown": { break; } case "ArrowUp": { break; } case "ArrowLeft": { break; } case "ArrowRight": { break; } default: } }); const { menu } = item; if (menu != null) { item_el.addEventListener("pointerenter", () => { show_submenu(item); }); item_el.addEventListener("pointerleave", () => { hide_submenu(item); }); } this.shadow_el.append(item_el); entries.push({ item, el: item_el }); } else { const item_el = div({ class: menus.divider }); this.shadow_el.append(item_el); } } } _show_submenu(target) { if (this.is_empty) { this.hide(); return; } this.render(); target.append(this.el); const { style } = this.el; style.left = "100%"; style.top = "0"; this._listen(); this._open = true; } show(at) { if (this.is_empty) { this.hide(); return false; } const { parent } = this; if (parent == null) { // TODO position: fixed this.hide(); return false; } this.render(); const target = parent.el.shadowRoot ?? parent.el; target.append(this.el); const { style } = this.el; style.left = px(at.x); style.top = px(at.y); this._listen(); this._open = true; return true; } hide() { if (this._open) { this._open = false; this._unlisten(); this.el.remove(); } } } export class Menu extends UIElement { static __name__ = "Menu"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = MenuView; this.define(({ Bool, List }) => ({ items: [List(MenuItemLike), []], reversed: [Bool, false], })); } } //# sourceMappingURL=menu.js.map