UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

391 lines 19 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime"; import { UIElement, UIElementView } from "./ui_element"; import * as p from "../../core/properties"; import { HasProps } from "../../core/has_props"; import { isArray } from "../../core/util/types"; import { keys } from "../../core/util/object"; import { map } from "../../core/util/iterator"; import { receivers_for_sender } from "../../core/signaling"; import { diagnostics } from "../../core/diagnostics"; import { assert } from "../../core/util/assert"; import { ValuePrinter, KindPrinter, OpaqueKindPrinter } from "./printers"; import examiner_css from "../../styles/examiner.css"; import pretty_css from "../../styles/pretty.css"; import icons_css from "../../styles/icons.css"; import { render, Component } from "preact"; import { signal, computed } from "@preact/signals"; import { Serializer } from "../../core/serialization/serializer"; import { Deserializer } from "../../core/serialization/deserializer"; function highlight(el) { for (const animation of el.getAnimations()) { animation.cancel(); } el.animate([ { backgroundColor: "#def189" }, { backgroundColor: "initial" }, ], { duration: 2000 }); } function emphasize(text, pattern) { const i = text.indexOf(pattern); if (i == -1) { return _jsx("span", { children: text }); } else { const j = i + pattern.length; const prefix = text.substring(0, i); const infix = text.substring(i, j); const suffix = text.substring(j); return (_jsxs(_Fragment, { children: [_jsx("span", { children: prefix }), _jsx("span", { class: "underline", children: infix }), _jsx("span", { children: suffix })] })); } } function compute_attrs(model) { 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.map(([base, attrs]) => [base, attrs.map((attr) => model.property(attr))]); } export class ExaminerView extends UIElementView { static __name__ = "ExaminerView"; stylesheets() { return [...super.stylesheets(), pretty_css, examiner_css, icons_css]; } render() { super.render(); const models_filter = signal(""); const props_filter = signal(""); const watches_filter = signal(""); const group_props = signal(true); const show_initial = signal(true); const show_internal = signal(true); const opaque_types = signal(false); const watched_props = signal(new Set()); function cls(...classes) { const transformed = classes .flatMap((cls) => isArray(cls) ? cls : [cls]) .filter((cls) => cls != null) .map((cls) => cls.trim()) .filter((cls) => cls.length != 0); return [...new Set(transformed)].join(" "); } function click(obj) { if (obj instanceof HasProps) { current_model.value = obj; } } function to_html(obj) { const printer = new ValuePrinter(click); return printer.to_html(obj); } class ModelItem extends Component { static __name__ = "ModelItem"; constructor(props) { super(props); this.state = { value_changed: new Date(), }; } _model_el = null; listener = ((obj) => { if (!(obj instanceof p.Property && this._model_el != null)) { return; } const { model } = this.props; for (const prop of model) { if (prop == obj) { this.setState({ value_changed: new Date() }); highlight(this._model_el); } } }).bind(this); componentDidMount() { diagnostics.connect(this.listener); } componentWillUnmount() { diagnostics.disconnect(this.listener); } render() { const { model } = this.props; const root = model.is_root ? _jsx("span", { class: "tag", children: "root" }) : null; const key_down = (event) => { if (event.key == "Enter") { click(model); } }; const active = current_model.value == model ? "active" : null; return (_jsxs("span", { class: cls("model-ref", active), tabIndex: 0, onKeyDown: key_down, ref: (el) => { this._model_el = el; }, children: [to_html(model), root] })); } } class ModelsToolbar extends Component { static __name__ = "ModelsToolbar"; render() { const key_up = (event) => { if (event.currentTarget instanceof HTMLInputElement) { models_filter.value = event.currentTarget.value; } }; return (_jsx("div", { class: "toolbar", children: _jsx("input", { class: "filter", type: "text", placeholder: "Filter", onKeyUp: key_up }) })); } } class PropsToolbar extends Component { static __name__ = "PropsToolbar"; render() { const key_up = (event) => { if (event.currentTarget instanceof HTMLInputElement) { props_filter.value = event.currentTarget.value; } }; return (_jsxs("div", { class: "toolbar", children: [_jsx("input", { class: "filter", type: "text", placeholder: "Filter", onKeyUp: key_up }), _jsxs("span", { class: "checkbox", children: [_jsx("input", { type: "checkbox", checked: group_props.value, onChange: (event) => group_props.value = event.currentTarget.checked }), _jsx("span", { children: "Group" })] }), _jsxs("span", { class: "checkbox", children: [_jsx("input", { type: "checkbox", checked: show_initial.value, onChange: (event) => show_initial.value = event.currentTarget.checked }), _jsx("span", { children: "Show initial" })] }), _jsxs("span", { class: "checkbox", children: [_jsx("input", { type: "checkbox", checked: show_internal.value, onChange: (event) => show_internal.value = event.currentTarget.checked }), _jsx("span", { children: "Show internal" })] }), _jsxs("span", { class: "checkbox", children: [_jsx("input", { type: "checkbox", checked: opaque_types.value, onChange: (event) => opaque_types.value = event.currentTarget.checked }), _jsx("span", { children: "Opaque types" })] })] })); } } class ModelsList extends Component { static __name__ = "ModelsList"; constructor(props) { super(props); } models_list = computed(() => { const pattern = models_filter.value; return this.props.models.filter((model) => model.constructor.__qualified__.includes(pattern)); }); render() { return (_jsxs("div", { class: "models-panel", children: [_jsx(ModelsToolbar, {}), _jsx("div", { class: "models-list", children: this.models_list.value.map((model) => _jsx(ModelItem, { model: model })) })] })); } } class PropEditor extends Component { static __name__ = "PropEditor"; constructor(props) { super(props); } get serializer() { const doc = this.props.prop.obj.document; assert(doc != null); const all_refs = new Map(map(doc.all_models, (model) => [model, model.ref()])); return new Serializer({ binary: false, include_defaults: false, references: all_refs }); } get deserializer() { const doc = this.props.prop.obj.document; assert(doc != null); const all_models = new Map(map(doc.all_models, (model) => [model.id, model])); return new Deserializer(doc.resolver, all_models, (obj) => obj.attach_document(doc)); } commit(value) { const parsed = JSON.parse(value); const decoded = this.deserializer.decode(parsed); const { prop } = this.props; prop.obj.setv({ [prop.attr]: decoded }); } input_el = null; render() { const { prop } = this.props; const value = this.serializer.encode(prop.get_value()); return (_jsxs(_Fragment, { children: [_jsx("input", { type: "text", ref: (el) => { this.input_el = el; }, onKeyUp: (event) => this.on_key_up(event), value: JSON.stringify(value) }), _jsx("div", { class: "btn btn-accept", onClick: () => this.accept_edit() }), _jsx("div", { class: "btn btn-cancel", onClick: () => this.cancel_edit() }), _jsx("div", { class: "btn btn-delete", onClick: () => this.delete_value() })] })); } accept_edit() { assert(this.input_el != null); this.commit(this.input_el.value); this.finalize(); } cancel_edit() { this.finalize(); } delete_value() { const { prop } = this.props; const value = prop.default_value(prop.obj); prop.obj.setv({ [prop.attr]: value }); this.finalize(); } finalize() { this.props.close(); } on_key_up(event) { switch (event.key) { case "Enter": { this.commit(event.currentTarget.value); break; } case "Escape": { this.cancel_edit(); break; } default: } } componentDidMount() { this.input_el?.focus(); } } class PropValue extends Component { static __name__ = "PropValue"; constructor(props) { super(props); this.state = { value_changed: new Date(), }; } _value_el = null; listener = ((obj) => { if (obj === this.props.prop && this._value_el != null) { this.setState({ value_changed: new Date() }); highlight(this._value_el); } }).bind(this); componentDidMount() { diagnostics.connect(this.listener); } componentWillUnmount() { diagnostics.disconnect(this.listener); } render() { const { prop } = this.props; return (_jsx("span", { class: "value", ref: (el) => { this._value_el = el; }, children: prop.is_unset ? _jsx("span", { children: "unset" }) : to_html(prop.get_value()) })); } } class PropItem extends Component { static __name__ = "PropItem"; constructor(props) { super(props); this.state = { editing: false }; } toggle_edit() { const { editing } = this.state; this.setState({ editing: !editing }); } render() { const { prop } = this.props; const connections = receivers_for_sender.get(prop.obj) ?? []; const listeners = connections.filter((connection) => connection.signal == prop.change).length; const watched = watched_props.value.has(prop); const watch_el = _jsx("input", { type: "checkbox", checked: watched, onChange: (event) => { const { checked } = event.currentTarget; const watched = watched_props.value; watched[checked ? "add" : "delete"](prop); watched_props.value = new Set(watched); } }); const dirty = prop.dirty ? "dirty" : null; const internal = prop.internal ? "internal" : null; const hidden = !prop.dirty && !show_initial.value || prop.internal && !show_internal.value ? "hidden" : null; const pattern = props_filter.value; const { attr } = prop; const kind_printer = opaque_types.value ? new OpaqueKindPrinter() : new KindPrinter(); const { editing } = this.state; return (_jsxs("div", { class: cls("prop", dirty, internal, hidden), children: [_jsxs("div", { class: "prop-attr", tabIndex: 0, children: [watch_el, emphasize(attr, pattern), prop.internal ? _jsx("span", { class: "tag", children: "internal" }) : null] }), _jsx("div", { class: "prop-conns", children: listeners != 0 ? _jsx("span", { class: "tag", children: `${listeners}` }) : null }), _jsx("div", { class: "prop-kind", children: kind_printer.to_html(prop.kind) }), _jsx("div", { class: cls("prop-value", editing ? "editing" : null), children: !editing ? _jsxs(_Fragment, { children: [_jsx(PropValue, { prop: prop }), _jsx("div", { class: "btn btn-edit", onClick: () => this.toggle_edit() })] }) : _jsx(PropEditor, { prop: prop, close: () => this.toggle_edit() }) })] })); } } class BaseItem extends Component { static __name__ = "BaseItem"; collapsed = signal(false); render() { const toggle = () => { this.collapsed.value = !this.collapsed.value; }; const { base, props } = this.props; const items = props.map((prop) => _jsx(PropItem, { prop: prop })); if (group_props.value) { return (_jsxs("div", { class: cls("branch", this.collapsed.value ? "collapsed" : null), children: [_jsxs("div", { class: "base", onClick: toggle, children: [_jsx("span", { class: "expander" }), "inherited from ", _jsx("span", { class: "monospace", children: base.__qualified__ })] }), items] })); } else { return _jsx(_Fragment, { children: items }); } } } class PropsList extends Component { static __name__ = "PropsList"; model_props = computed(() => { const model = current_model.value; return model != null ? compute_attrs(model) : []; }); props_list = computed(() => { const text = props_filter.value; return this.model_props.value.map(([base, props]) => [base, props.filter((prop) => prop.attr.includes(text))]); }); render() { return (_jsx("div", { class: "props-list", children: this.props_list.value.map(([base, props]) => _jsx(BaseItem, { base: base, props: props })) })); } } class PropsPanel extends Component { static __name__ = "PropsPanel"; render() { return (_jsxs("div", { class: "props-panel", children: [_jsx(PropsToolbar, {}), _jsx(PropsList, {})] })); } } class WatchesToolbar extends Component { static __name__ = "WatchesToolbar"; render() { const key_up = (event) => { if (event.currentTarget instanceof HTMLInputElement) { watches_filter.value = event.currentTarget.value; } }; return (_jsx("div", { class: "toolbar", children: _jsx("input", { class: "filter", type: "text", placeholder: "Filter", onKeyUp: key_up }) })); } } class WatchesList extends Component { static __name__ = "WatchesList"; remove_watch(prop) { const watched = watched_props.value; watched.delete(prop); watched_props.value = new Set(watched); } render() { const props = [...watched_props.value]; const entries = (() => { if (props.length == 0) { return _jsx("div", { class: "nothing", children: "No watched properties" }); } else { return props.map((prop) => (_jsxs("div", { class: cls("prop", prop.dirty ? "dirty" : null), children: [_jsx("div", { children: to_html(prop) }), _jsx("div", { children: _jsx(PropValue, { prop: prop }) }), _jsx("div", { class: "btn btn-delete", onClick: () => this.remove_watch(prop) })] }))); } })(); return _jsx("div", { class: "watches-list", children: entries }); } } class WatchesPanel extends Component { static __name__ = "WatchesPanel"; render() { return (_jsxs("div", { class: "watches-panel", children: [_jsx(WatchesToolbar, {}), _jsx(WatchesList, {})] })); } } class ExaminerPanel extends Component { static __name__ = "ExaminerPanel"; render() { return (_jsxs("div", { class: "examiner", children: [_jsx(ModelsList, { models: [...models] }), _jsxs("div", { class: "col", style: { width: "100%" }, children: [_jsx(WatchesPanel, {}), _jsx(PropsPanel, {})] })] })); } } const { target } = this.model; const models = (() => { if (target != null) { return target.references(); } else { return this.model.document?.all_models ?? new Set(); } })(); const current_model = signal([...models][0]); const examiner = _jsx(ExaminerPanel, {}); render(examiner, this.shadow_el); } } 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