@bokeh/bokehjs
Version:
Interactive, novel data visualization
259 lines • 8.87 kB
JavaScript
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_views() {
return [...super.children_views(), ...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