@bokeh/bokehjs
Version:
Interactive, novel data visualization
204 lines • 7.19 kB
JavaScript
import { div, empty, InlineStyleSheet, ClassList } from "../dom";
import { reversed } from "./array";
import { isBoolean, isString, isPlainObject } from "./types";
import { execute } from "./callbacks";
import menus_css, * as menus from "../../styles/legacy_menus.css";
import icons_css from "../../styles/icons.css";
import base_css from "../../styles/base.css";
import { Menu, DividerItem } from "../../models/ui/menus";
import { apply_icon } from "../../models/common/resolve";
export class ContextMenu {
items;
static __name__ = "ContextMenu";
el = div();
shadow_el;
_open = false;
get is_open() {
return this._open;
}
get can_open() {
return this.items.length != 0;
}
target;
orientation;
reversed;
labels;
prevent_hide;
extra_styles;
class_list;
constructor(items, options) {
this.items = items;
this.target = options.target;
this.orientation = options.orientation ?? "vertical";
this.reversed = options.reversed ?? false;
this.labels = options.labels ?? true;
this.prevent_hide = options.prevent_hide;
this.extra_styles = options.extra_styles ?? [];
this.shadow_el = this.el.attachShadow({ mode: "open" });
this.class_list = new ClassList(this.el.classList);
}
_item_click = (entry) => {
if (entry.action != null) {
if (isPlainObject(entry)) {
entry.action();
}
else {
void execute(entry.action, new Menu(), { item: entry });
}
}
this.hide();
};
_on_mousedown = (event) => {
if (event.composedPath().includes(this.el)) {
return;
}
if (this.prevent_hide?.(event) ?? false) {
return;
}
this.hide();
};
_on_keydown = (event) => {
if (event.key == "Escape") {
this.hide();
}
};
_on_blur = () => {
this.hide();
};
remove() {
this._unlisten();
this.el.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);
}
_position(at) {
const pos = (() => {
if ("left_of" in at) {
const { left, top } = at.left_of.getBoundingClientRect();
return { right: left, top };
}
if ("right_of" in at) {
const { top, right } = at.right_of.getBoundingClientRect();
return { left: right, top };
}
if ("below" in at) {
const { left, bottom } = at.below.getBoundingClientRect();
return { left, top: bottom };
}
if ("above" in at) {
const { left, top } = at.above.getBoundingClientRect();
return { left, bottom: top };
}
return at;
})();
const parent_el = this.el.offsetParent ?? document.body;
const origin = (() => {
const rect = parent_el.getBoundingClientRect();
const style = getComputedStyle(parent_el);
return {
left: rect.left - parseFloat(style.marginLeft),
right: rect.right + parseFloat(style.marginRight),
top: rect.top - parseFloat(style.marginTop),
bottom: rect.bottom + parseFloat(style.marginBottom),
};
})();
const { style } = this.el;
style.left = pos.left != null ? `${pos.left - origin.left}px` : "auto";
style.top = pos.top != null ? `${pos.top - origin.top}px` : "auto";
style.right = pos.right != null ? `${origin.right - pos.right}px` : "auto";
style.bottom = pos.bottom != null ? `${origin.bottom - pos.bottom}px` : "auto";
}
stylesheets() {
return [base_css, /*...super.stylesheets(), */ menus_css, icons_css, ...this.extra_styles];
}
empty() {
empty(this.shadow_el);
this.class_list.clear();
}
render() {
this.empty();
for (const style of this.stylesheets()) {
const stylesheet = isString(style) ? new InlineStyleSheet(style) : style;
stylesheet.install(this.shadow_el);
}
this.class_list.add(menus[this.orientation]);
const items = this.reversed ? reversed(this.items) : this.items;
for (const item of items) {
let el;
if (item == null || item instanceof DividerItem) {
el = div({ class: menus.divider });
}
else if (isBoolean(item.disabled) ? item.disabled : item.disabled?.() ?? false) {
continue;
}
else if (isPlainObject(item) && item.custom != null) {
el = item.custom;
}
else {
const icon_el = (() => {
if (item.icon != null) {
const el = div({ class: menus.menu_icon });
apply_icon(el, item.icon);
return el;
}
else {
return null;
}
})();
const checked = isBoolean(item.checked) ? item.checked : item.checked?.();
const active = checked ?? false ? menus.active : null;
const label = this.labels ? item.label : null;
el = div({ class: [active], title: item.tooltip, tabIndex: 0 }, icon_el, label);
if (isPlainObject(item)) {
if (item.class != null) {
el.classList.add(item.class);
}
if (item.content != null) {
el.append(item.content);
}
}
el.addEventListener("click", () => {
this._item_click(item);
});
el.addEventListener("keydown", (event) => {
if (event.key == "Enter") {
this._item_click(item);
}
});
}
this.shadow_el.appendChild(el);
}
}
show(at) {
if (this.items.length == 0) {
return;
}
this.render();
if (this.shadow_el.children.length == 0) {
return;
}
(this.target.shadowRoot ?? this.target).appendChild(this.el);
this._position(at ?? { left: 0, top: 0 });
this._listen();
this._open = true;
}
hide() {
if (this._open) {
this._open = false;
this._unlisten();
this.el.remove();
}
}
toggle(at) {
this._open ? this.hide() : this.show(at);
}
}
//# sourceMappingURL=menus.js.map