UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

534 lines 20.4 kB
import { logger } from "../../core/logging"; import { div, a } from "../../core/dom"; import { build_views, remove_views } from "../../core/build_views"; import { UIElement, UIElementView } from "../ui/ui_element"; import { Logo, Location, ToolName } from "../../core/enums"; import { every, sort_by, includes, intersection, split, clear } from "../../core/util/array"; import { join, enumerate } from "../../core/util/iterator"; import { typed_keys, values, entries } from "../../core/util/object"; import { isArray } from "../../core/util/types"; import { Tool } from "./tool"; import { ToolProxy } from "./tool_proxy"; import { ToolGroup } from "./tool_group"; import { ToolButton } from "./tool_button"; import { GestureTool } from "./gestures/gesture_tool"; import { InspectTool } from "./inspectors/inspect_tool"; import { ActionTool } from "./actions/action_tool"; import { HelpTool } from "./actions/help_tool"; import { Menu, DividerItem } from "../ui/menus"; import { ContextMenu } from "../../core/util/menus"; import { Signal0 } from "../../core/signaling"; import { version } from "../../version"; import toolbars_css, * as toolbars from "../../styles/toolbar.css"; import logos_css, * as logos from "../../styles/logo.css"; import icons_css from "../../styles/icons.css"; export class ToolbarView extends UIElementView { static __name__ = "ToolbarView"; _tool_button_views = new Map(); _tool_buttons; _items = []; get tool_buttons() { return this._tool_buttons.flat(); } get tool_button_views() { return this.tool_buttons.map((tb) => this._tool_button_views.get(tb)).filter((view) => view != null); } _overflow_menu; _overflow_el; get overflow_el() { return this._overflow_el; } _visible = null; get visible() { return !this.model.visible ? false : (!this.model.autohide || (this._visible ?? false)); } *children() { yield* super.children(); yield* this._tool_button_views.values(); } has_finished() { if (!super.has_finished()) { return false; } for (const child_view of this._tool_button_views.values()) { if (!child_view.has_finished()) { return false; } } return true; } initialize() { super.initialize(); const { location } = this.model; const reversed = location == "left" || location == "above"; const orientation = this.model.horizontal ? "vertical" : "horizontal"; this._overflow_menu = new ContextMenu([], { target: this.el, orientation, reversed, prevent_hide: (event) => { return event.composedPath().includes(this._overflow_el); }, }); } async lazy_initialize() { await super.lazy_initialize(); await this._build_tool_button_views(); } connect_signals() { super.connect_signals(); const { buttons, tools, location, autohide, group, group_types } = this.model.properties; this.on_change([buttons, tools, group, group_types], async () => { await this._build_tool_button_views(); this.rerender(); }); this.on_change(location, () => { this.rerender(); }); this.on_change(autohide, () => { this._on_visible_change(); }); this.on_transitive_change(tools, () => { this.rerender(); }, { signal: (obj) => obj.properties.visible.change, }); } stylesheets() { return [...super.stylesheets(), toolbars_css, logos_css, icons_css]; } remove() { remove_views(this._tool_button_views); this._destroy_proxies(); super.remove(); } // Manually keep track of view constructed ToolProxy models, because models don't // have any sensible life cycle management (at least from views' perspective). _our_proxies = []; _destroy_proxies() { for (const proxy of this._our_proxies) { proxy.destroy(); } clear(this._our_proxies); } /** * Group similar tools into tool proxies. */ _group_tools(tools) { const { group_types } = this.model; const grouped = new Map(); for (const [tool, i] of enumerate(tools)) { const group_type = group_types.find((type) => Tool.is_alias_of(tool, type)); if (group_type != null && tool.group !== false) { const key = tool.group === true ? tool.type : `${tool.type}_${tool.group}`; const group = grouped.get(key); if (group != null) { group.push(tool); } else { grouped.set(key, [tool]); } } else { // The key doesn't matter, just use something unique. grouped.set(`${i}`, [tool]); } } return Array.from(grouped.values(), (group) => { if (group.length > 1) { const proxy = new ToolGroup({ tools: group }); this._our_proxies.push(proxy); return proxy; } else { return group[0]; } }); } async _build_tool_button_views() { this._destroy_proxies(); this._tool_buttons = (() => { const { buttons } = this.model; if (buttons == "auto") { const tool_bars = [ ...values(this.model.gestures).map((gesture) => gesture.tools), this.model.actions, this.model.inspectors, this.model.auxiliaries, ]; const { group } = this.model; const button_bars = tool_bars.map((bar) => { const grouped = group ? this._group_tools(bar) : bar; return grouped.map((tool) => tool.tool_button()); }); return button_bars; } else { return split(buttons, null); } })(); await build_views(this._tool_button_views, this._tool_buttons.flat(), { parent: this }); } set_visibility(visible) { if (visible != this._visible) { this._visible = visible; this._on_visible_change(); } } _on_visible_change() { this.el.classList.toggle(toolbars.hidden, !this.visible); } _after_resize() { super._after_resize(); this._after_render(); } _menu_at() { switch (this.model.location) { case "right": return { left_of: this._overflow_el }; case "left": return { right_of: this._overflow_el }; case "above": return { below: this._overflow_el }; case "below": return { above: this._overflow_el }; } } toggle_menu() { this._overflow_menu.toggle(this._menu_at()); } render() { super.render(); this.el.classList.add(toolbars[this.model.location]); this.el.classList.toggle(toolbars.inner, this.model.inner); this._on_visible_change(); const { horizontal } = this.model; this._overflow_el = div({ class: toolbars.tool_overflow, tabIndex: 0 }, horizontal ? "⋮" : "⋯"); this._overflow_el.addEventListener("click", (_event) => { this.toggle_menu(); }); this._overflow_el.addEventListener("keydown", (event) => { if (event.key == "Enter") { this.toggle_menu(); } }); this._items = []; if (this.model.logo != null) { const gray = this.model.logo === "grey" ? logos.grey : null; const logo_el = a({ href: "https://bokeh.org/", target: "_blank", class: [logos.logo, logos.logo_small, gray], title: `Bokeh ${version}` }); this._items.push(logo_el); this.shadow_el.appendChild(logo_el); } for (const [, button_view] of this._tool_button_views) { button_view.render(); } const bars = this._tool_buttons.map((group) => { return group .filter((button) => button.tool.visible) .map((button) => this._tool_button_views.get(button)) .filter((view) => view != null) .map((view) => view.el); }).filter((bar) => bar.length != 0); const divider = () => div({ class: toolbars.divider }); for (const el of join(bars, divider)) { this._items.push(el); } this.shadow_el.append(...this._items); } _after_render() { super._after_render(); clear(this._overflow_menu.items); if (this.shadow_el.contains(this._overflow_el)) { this.shadow_el.removeChild(this._overflow_el); } for (const el of this._items) { if (!this.shadow_el.contains(el)) { this.shadow_el.append(el); } } const { horizontal } = this.model; const overflow_size = 15; const { bbox } = this; const overflow_cls = horizontal ? toolbars.right : toolbars.above; let size = 0; let overflowed = false; for (const el of this._items) { if (overflowed) { this.shadow_el.removeChild(el); this._overflow_menu.items.push({ custom: el, class: overflow_cls }); } else { const { width, height } = el.getBoundingClientRect(); size += horizontal ? width : height; overflowed = horizontal ? size > bbox.width - overflow_size : size > bbox.height - overflow_size; if (overflowed) { this.shadow_el.removeChild(el); this.shadow_el.appendChild(this._overflow_el); this._overflow_menu.items.push({ custom: el, class: overflow_cls }); } } } if (this._overflow_menu.is_open) { this._overflow_menu.show(this._menu_at()); } for (const tb_view of this.tool_button_views) { tb_view.update_bbox(); } } toggle_auto_scroll(force) { if (this.model.active_scroll != "auto") { return; } for (const tool of this.model.tools) { if (tool.event_types.includes("scroll")) { tool.active = force ?? !tool.active; break; } } } } import { Struct, Ref, Nullable, List, Or } from "../../core/kinds"; const GestureToolLike = Or(Ref(GestureTool), Ref((ToolProxy))); const GestureEntry = Struct({ tools: List(GestureToolLike), active: Nullable(GestureToolLike), }); const GesturesMap = Struct({ pan: GestureEntry, scroll: GestureEntry, pinch: GestureEntry, rotate: GestureEntry, move: GestureEntry, tap: GestureEntry, doubletap: GestureEntry, press: GestureEntry, pressup: GestureEntry, multi: GestureEntry, }); export const Inspection = Tool; function create_gesture_map() { return { pan: { tools: [], active: null }, scroll: { tools: [], active: null }, pinch: { tools: [], active: null }, rotate: { tools: [], active: null }, move: { tools: [], active: null }, tap: { tools: [], active: null }, doubletap: { tools: [], active: null }, press: { tools: [], active: null }, pressup: { tools: [], active: null }, multi: { tools: [], active: null }, }; } export class Toolbar extends UIElement { static __name__ = "Toolbar"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = ToolbarView; this.define(({ Bool, List, Or, Ref, Nullable, Auto }) => ({ tools: [List(Or(Ref(Tool), Ref(ToolProxy))), []], logo: [Nullable(Logo), "normal"], autohide: [Bool, false], group: [Bool, true], group_types: [List(ToolName), ["hover"]], active_drag: [Nullable(Or(GestureToolLike, Auto)), "auto"], active_inspect: [Nullable(Or(Ref(Inspection), List(Ref(Inspection)), Ref(ToolProxy), Auto)), "auto"], active_scroll: [Nullable(Or(GestureToolLike, Auto)), "auto"], active_tap: [Nullable(Or(GestureToolLike, Auto)), "auto"], active_multi: [Nullable(Or(GestureToolLike, Auto)), "auto"], })); this.internal(({ List, Bool, Ref, Or, Null, Auto }) => { return { buttons: [Or(List(Or(Ref(ToolButton), Null)), Auto), "auto"], location: [Location, "right"], inner: [Bool, false], gestures: [GesturesMap, create_gesture_map], actions: [List(Or(Ref(ActionTool), Ref(ToolProxy))), []], inspectors: [List(Or(Ref(InspectTool), Ref(ToolProxy))), []], auxiliaries: [List(Or(Ref(Tool), Ref(ToolProxy))), []], help: [List(Or(Ref(HelpTool), Ref(ToolProxy))), []], }; }); } active_changed; get horizontal() { return this.location == "above" || this.location == "below"; } get vertical() { return this.location == "left" || this.location == "right"; } connect_signals() { super.connect_signals(); const { tools, active_drag, active_inspect, active_scroll, active_tap, active_multi } = this.properties; this.on_change([tools, active_drag, active_inspect, active_scroll, active_tap, active_multi], () => { this._init_tools(); this._activate_tools(); }); } initialize() { super.initialize(); this.active_changed = new Signal0(this, "active_changed"); this._init_tools(); this._activate_tools(); } _init_tools() { const visited = new Set(); function isa(tool, type) { const is = tool.underlying instanceof type; if (is) { visited.add(tool); } return is; } const new_inspectors = this.tools.filter(t => isa(t, InspectTool)); this.inspectors = new_inspectors; const new_help = this.tools.filter(t => isa(t, HelpTool)); this.help = new_help; const new_actions = this.tools.filter(t => isa(t, ActionTool)); this.actions = new_actions; const new_gestures = create_gesture_map(); for (const tool of this.tools) { if (isa(tool, GestureTool)) { new_gestures[tool.event_role].tools.push(tool); } } for (const et of typed_keys(new_gestures)) { const gesture = this.gestures[et]; gesture.tools = sort_by(new_gestures[et].tools, (tool) => tool.default_order); if (gesture.active != null && every(gesture.tools, (tool) => tool.id != gesture.active?.id)) { gesture.active = null; } } const new_auxiliaries = this.tools.filter((tool) => !visited.has(tool)); this.auxiliaries = new_auxiliaries; } _activate_tools() { if (this.active_inspect == "auto") { // do nothing as all tools are active be default } else if (this.active_inspect == null) { for (const inspector of this.inspectors) { inspector.active = false; } } else if (isArray(this.active_inspect)) { const active_inspect = intersection(this.active_inspect, this.inspectors); if (active_inspect.length != this.active_inspect.length) { this.active_inspect = active_inspect; } for (const inspector of this.inspectors) { if (!includes(this.active_inspect, inspector)) { inspector.active = false; } } } else { let found = false; for (const inspector of this.inspectors) { if (inspector != this.active_inspect) { inspector.active = false; } else { found = true; } } if (!found) { this.active_inspect = null; } } const _activate_gesture = (tool) => { if (tool.active) { // tool was activated by a proxy, but we need to finish configuration manually this._active_change(tool); } else { tool.active = true; } }; // Connecting signals has to be done before changing the active state of the tools. for (const gesture of values(this.gestures)) { for (const tool of gesture.tools) { // XXX: connect once this.connect(tool.properties.active.change, () => { this._active_change(tool); this.active_changed.emit(); }); } } function _get_active_attr(et) { switch (et) { case "tap": return "active_tap"; case "pan": return "active_drag"; case "pinch": case "scroll": return "active_scroll"; case "multi": return "active_multi"; default: return null; } } function _supports_auto(et, tool) { return et == "tap" || et == "pan" || tool.supports_auto(); } const is_active_gesture = (active_tool) => { return this.tools.includes(active_tool) || (active_tool instanceof Tool && this.tools.some((tool) => tool instanceof ToolProxy && tool.tools.includes(active_tool))); }; for (const [event_role, gesture] of entries(this.gestures)) { const et = event_role; const active_attr = _get_active_attr(et); if (active_attr != null) { const active_tool = this[active_attr]; if (active_tool == "auto") { if (gesture.tools.length != 0) { const [tool] = gesture.tools; if (_supports_auto(et, tool)) { _activate_gesture(tool); } } } else if (active_tool != null) { if (is_active_gesture(active_tool)) { _activate_gesture(active_tool); } else { this[active_attr] = null; } } else { this.gestures[et].active = null; for (const tool of this.gestures[et].tools) { tool.active = false; } } } } this.active_changed.emit(); } _active_change(tool) { const { event_types } = tool; for (const et of event_types) { if (tool.active) { const currently_active_tool = this.gestures[et].active; if (currently_active_tool != null && tool != currently_active_tool) { logger.debug(`Toolbar: deactivating tool: ${currently_active_tool} for event type '${et}'`); currently_active_tool.active = false; } this.gestures[et].active = tool; logger.debug(`Toolbar: activating tool: ${tool} for event type '${et}'`); } else { this.gestures[et].active = null; } } } to_menu() { const groups = [ ...values(this.gestures).map((gesture) => gesture.tools), this.actions, this.inspectors, this.auxiliaries, ]; const entries = groups .filter((group) => group.length != 0) .map((group) => group.map((tool) => tool.menu_item())); const items = [...join(entries, () => new DividerItem())]; return new Menu({ items }); } } //# sourceMappingURL=toolbar.js.map