UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

405 lines 16.7 kB
import { UIElement, UIElementView } from "./ui_element"; import * as p from "../../core/properties"; import { HasProps } from "../../core/has_props"; import { div, span, input, empty } from "../../core/dom"; import { to_string } from "../../core/util/pretty"; import { Model } from "../../model"; import { isBoolean, isNumber, isString, isSymbol, isArray, isIterable, isObject, isPlainObject } from "../../core/util/types"; import { entries, keys } from "../../core/util/object"; import { clear } from "../../core/util/array"; import { interleave } from "../../core/util/iterator"; import { receivers_for_sender } from "../../core/signaling"; import { diagnostics } from "../../core/diagnostics"; import examiner_css from "../../styles/examiner.css"; import pretty_css, * as pretty from "../../styles/pretty.css"; export class HTMLPrinter { click; max_items; max_depth; static __name__ = "HTMLPrinter"; visited = new WeakSet(); depth = 0; constructor(click, max_items = 5, max_depth = 3) { this.click = click; this.max_items = max_items; this.max_depth = max_depth; } to_html(obj) { if (isObject(obj)) { if (this.visited.has(obj)) { return span("<circular>"); } else { this.visited.add(obj); } } if (obj == null) { return this.null(); } else if (isBoolean(obj)) { return this.boolean(obj); } else if (isNumber(obj)) { return this.number(obj); } else if (isString(obj)) { return this.string(obj); } else if (isSymbol(obj)) { return this.symbol(obj); } else if (obj instanceof Model) { return this.model(obj); } else if (obj instanceof p.Property) { return this.property(obj); } else if (isPlainObject(obj)) { return this.object(obj); } else if (isArray(obj)) { return this.array(obj); } else if (isIterable(obj)) { return this.iterable(obj); } else { return span(to_string(obj)); } } null() { return span({ class: pretty.nullish }, "null"); } token(val) { return span({ class: pretty.token }, val); } boolean(val) { return span({ class: pretty.boolean }, `${val}`); } number(val) { return span({ class: pretty.number }, `${val}`); } string(val) { const sq = val.includes("'"); const dq = val.includes('"'); const str = (() => { if (sq && dq) { return `\`${val.replace(/`/g, "\\`")}\``; } else if (dq) { return `'${val}'`; } else { return `"${val}"`; } })(); return span({ class: pretty.string }, str); } symbol(val) { return span({ class: pretty.symbol }, val.toString()); } array(obj) { const T = this.token; const items = []; let i = 0; for (const entry of obj) { items.push(this.to_html(entry)); if (i++ > this.max_items) { items.push(span("\u2026")); break; } } return span({ class: pretty.array }, T("["), ...interleave(items, () => T(", ")), T("]")); } iterable(obj) { const T = this.token; const tag = Object(obj)[Symbol.toStringTag] ?? "Object"; const items = this.array([...obj]); return span({ class: pretty.iterable }, `${tag}`, T("("), items, T(")")); } object(obj) { const T = this.token; const items = []; let i = 0; for (const [key, val] of entries(obj)) { items.push(span(`${key}`, T(": "), this.to_html(val))); if (i++ > this.max_items) { items.push(span("\u2026")); break; } } return span({ class: pretty.object }, T("{"), ...interleave(items, () => T(", ")), T("}")); } model(obj) { const T = this.token; const el = span({ class: pretty.model }, obj.constructor.__qualified__, T("("), this.to_html(obj.id), T(")")); const { click } = this; if (click != null) { el.classList.add("ref"); el.addEventListener("click", () => click(obj)); } return el; } property(obj) { const model = this.model(obj.obj); const attr = span({ class: pretty.attr }, obj.attr); return span(model, this.token("."), attr); } } export class ExaminerView extends UIElementView { static __name__ = "ExaminerView"; stylesheets() { return [...super.stylesheets(), pretty_css, examiner_css]; } prev_listener = null; watched_props = new Set(); render() { super.render(); if (this.prev_listener != null) { diagnostics.disconnect(this.prev_listener); } const models_list = []; const props_list = []; const watches_list = []; const animations = new WeakMap(); const listener = (obj) => { if (!(obj instanceof p.Property)) { return; } function highlight(el) { const prev = animations.get(el); if (prev != null) { prev.cancel(); } const anim = el.animate([ { backgroundColor: "#def189" }, { backgroundColor: "initial" }, ], { duration: 2000 }); animations.set(el, anim); } function update(prop, prop_el, value_el) { prop_el.classList.toggle("dirty", prop.dirty); empty(value_el); const value = prop.is_unset ? span("unset") : to_html(prop.get_value()); value_el.appendChild(value); highlight(value_el); } for (const [model, model_el] of models_list) { if (model == obj.obj) { highlight(model_el); } } for (const [prop, prop_el] of props_list) { if (prop == obj) { const [, , , value_el] = prop_el.children; update(prop, prop_el, value_el); break; } } for (const [prop, prop_el] of watches_list) { if (prop == obj) { const [, value_el] = prop_el.children; update(prop, prop_el, value_el); break; } } }; diagnostics.connect(listener); const models_tb_el = (() => { const filter_el = input({ class: "filter", type: "text", placeholder: "Filter" }); filter_el.addEventListener("keyup", () => { const text = filter_el.value; for (const [model, el] of models_list) { const show = model.constructor.__qualified__.includes(text); el.classList.toggle("hidden", !show); } }); return div({ class: "toolbar" }, filter_el); })(); const initial_cb_el = input({ type: "checkbox", checked: true }); const internal_cb_el = input({ type: "checkbox", checked: true }); const update_prop_visibility = () => { for (const [prop, prop_el] of props_list) { const show_initial = initial_cb_el.checked; const show_internal = internal_cb_el.checked; const hidden = !prop.dirty && !show_initial || prop.internal && !show_internal; prop_el.classList.toggle("hidden", hidden); } }; initial_cb_el.addEventListener("change", () => update_prop_visibility()); internal_cb_el.addEventListener("change", () => update_prop_visibility()); const props_tb_el = (() => { const filter_el = input({ class: "filter", type: "text", placeholder: "Filter" }); const group_el = span({ class: "checkbox" }, input({ type: "checkbox", checked: true }), span("Group")); const initial_el = span({ class: "checkbox" }, initial_cb_el, span("Initial?")); const internal_el = span({ class: "checkbox" }, internal_cb_el, span("Internal?")); filter_el.addEventListener("keyup", () => { const text = filter_el.value; for (const [prop, el] of props_list) { const show = prop.attr.includes(text); el.classList.toggle("hidden", !show); } }); return div({ class: "toolbar" }, filter_el, group_el, initial_el, internal_el); })(); const watches_tb_el = (() => { const filter_el = input({ class: "filter", type: "text", placeholder: "Filter" }); filter_el.addEventListener("keyup", () => { const text = filter_el.value; for (const [prop, el] of watches_list) { const show = prop.attr.includes(text); el.classList.toggle("hidden", !show); } }); return div({ class: "toolbar" }, filter_el); })(); const models_list_el = div({ class: "models-list" }); const props_list_el = div({ class: "props-list" }); const watches_list_el = div({ class: "watches-list" }); const models_panel_el = div({ class: "models-panel" }, models_tb_el, models_list_el); const props_panel_el = div({ class: "props-panel" }, props_tb_el, props_list_el); const watches_panel_el = div({ class: "watches-panel" }, watches_tb_el, watches_list_el); const column_el = div({ class: "col", style: { width: "100%" } }, watches_panel_el, props_panel_el); const examiner_el = div({ class: "examiner" }, models_panel_el, column_el); function click(obj) { if (obj instanceof Model) { render_props(obj); } } function to_html(obj) { const printer = new HTMLPrinter(click); return printer.to_html(obj); } const render_models = (models, doc) => { clear(models_list); empty(models_list_el); const roots = doc != null ? new Set(doc.roots()) : new Set(); for (const model of models) { const root = roots.has(model) ? span({ class: "tag" }, "root") : null; const ref_el = span({ class: "model-ref", tabIndex: 0 }, to_html(model), root); ref_el.addEventListener("keydown", (event) => { if (event.key == "Enter") { render_props(model); } }); models_list.push([model, ref_el]); models_list_el.appendChild(ref_el); } }; const render_props = (model) => { clear(props_list); empty(props_list_el); for (const [item, el] of models_list) { el.classList.toggle("active", model == item); } const bases = (() => { const bases = []; let proto = Object.getPrototypeOf(model); do { bases.push([proto.constructor, keys(proto._props)]); proto = Object.getPrototypeOf(proto); } while (proto.constructor != HasProps); bases.reverse(); const cumulative = []; for (const [, attrs] of bases) { attrs.splice(0, cumulative.length); cumulative.push(...attrs); } return bases; })(); const connections = receivers_for_sender.get(model) ?? []; for (const [base, attrs] of bases) { if (attrs.length == 0) { continue; } const expander_el = span({ class: ["expander"] }); const base_el = div({ class: "base" }, expander_el, "inherited from", " ", span({ class: "monospace" }, base.__qualified__)); props_list_el.appendChild(base_el); const props_group = []; for (const attr of attrs) { const prop = model.property(attr); const kind = prop.kind.toString(); const value = prop.is_unset ? span("unset") : to_html(prop.get_value()); const internal_el = prop.internal ? span({ class: "tag" }, "internal") : null; const listeners = connections.filter((connection) => connection.signal == prop.change).length; const listeners_el = listeners != 0 ? span({ class: "tag" }, `${listeners}`) : null; const watched = this.watched_props.has(prop); const watch_el = input({ type: "checkbox", checked: watched }); const attr_el = div({ class: "prop-attr", tabIndex: 0 }, watch_el, span({ class: "attr" }, attr), internal_el); const conns_el = div({ class: "prop-conns" }, listeners_el); const kind_el = div({ class: "prop-kind" }, kind); const value_el = div({ class: "prop-value" }, value); const dirty = prop.dirty ? "dirty" : null; const internal = prop.internal ? "internal" : null; const show_initial = initial_cb_el.checked; const show_internal = internal_cb_el.checked; const hidden = !prop.dirty && !show_initial || prop.internal && !show_internal ? "hidden" : null; const prop_el = div({ class: ["prop", dirty, internal, hidden] }, attr_el, conns_el, kind_el, value_el); props_group.push(prop_el); props_list.push([prop, prop_el]); props_list_el.appendChild(prop_el); watch_el.addEventListener("change", () => { this.watched_props[watch_el.checked ? "add" : "delete"](prop); render_watches(); }); } base_el.addEventListener("click", () => { expander_el.classList.toggle("closed"); for (const el of props_group) { el.classList.toggle("closed"); } }); } }; const render_watches = () => { clear(watches_list); empty(watches_list_el); if (this.watched_props.size == 0) { const empty_el = div({ class: "nothing" }, "No watched properties"); watches_list_el.appendChild(empty_el); } else { for (const prop of this.watched_props) { const attr_el = span(to_html(prop)); const value_el = span(prop.is_unset ? span("unset") : to_html(prop.get_value())); const prop_el = div({ class: ["prop", prop.dirty ? "dirty" : null] }, attr_el, value_el); watches_list.push([prop, prop_el]); watches_list_el.appendChild(prop_el); } } }; this.shadow_el.appendChild(examiner_el); const { target } = this.model; if (target != null) { const models = target.references(); const { document } = target; render_models(models, document); render_props(target); } else { const { document } = this.model; if (document != null) { render_models(document.all_models, document); const roots = document.roots(); if (roots.length != 0) { const [root] = roots; render_props(root); } } } render_watches(); } } export class Examiner extends UIElement { static __name__ = "Examiner"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = ExaminerView; this.define(({ Ref, Nullable }) => ({ target: [Nullable(Ref(HasProps)), null], })); } } //# sourceMappingURL=examiner.js.map