Skip to content

Commit

Permalink
Rewrite panel/models/reactive_html.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpap committed Apr 4, 2024
1 parent 6e3da22 commit 6f59502
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 265 deletions.
34 changes: 16 additions & 18 deletions panel/models/comm_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {Model} from "@bokehjs/model"
import {Message} from "@bokehjs/protocol/message"
import {Receiver} from "@bokehjs/protocol/receiver"
import type {Patch, DocumentChangedEvent} from "@bokehjs/document"
import {isArray, isPlainObject} from "@bokehjs/core/util/types"
import {values, size} from "@bokehjs/core/util/object"

export const comm_settings: any = {
debounce: true,
Expand Down Expand Up @@ -101,28 +103,24 @@ export class CommManager extends Model {
}
}

protected _extract_buffers(value: any, buffers: ArrayBuffer[]): any {
let extracted: any
if (value instanceof Array) {
extracted = []
protected _extract_buffers(value: unknown, buffers: ArrayBuffer[]): void {
if (isArray(value)) {
for (const val of value) {
extracted.push(this._extract_buffers(val, buffers))
this._extract_buffers(val, buffers)
}
} else if (value instanceof Object) {
extracted = {}
for (const key in value) {
if (key === "buffer" && value[key] instanceof ArrayBuffer) {
const id = Object.keys(buffers).length
extracted = {id}
buffers.push(value[key])
break
} else if (isPlainObject(value)) {
if (size(value) == 1 && value.buffer instanceof ArrayBuffer) {
const {buffer} = value
delete value.buffer
const id = buffers.length
value.id = id
buffers.push(buffer)
} else {
for (const val of values(value)) {
this._extract_buffers(val, buffers)
}
extracted[key] = this._extract_buffers(value[key], buffers)
}
} else {
extracted = value
}
return extracted
}

process_events() {
Expand All @@ -133,7 +131,7 @@ export class CommManager extends Model {
this._event_buffer = []
const message = {...Message.create("PATCH-DOC", {}, patch)}
const buffers: ArrayBuffer[] = []
message.content = this._extract_buffers(message.content, buffers)
this._extract_buffers(message.content, buffers)
this._client_comm.send(message, {}, buffers)
for (const view of this.ns.shared_views.get(this.plot_id)) {
if (view !== this && view.document != null) {
Expand Down
65 changes: 34 additions & 31 deletions panel/models/html.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {ModelEvent} from "@bokehjs/core/bokeh_events"
import type * as p from "@bokehjs/core/properties"
import type {Attrs} from "@bokehjs/core/types"
import type {Attrs, Dict} from "@bokehjs/core/types"
import {entries} from "@bokehjs/core/util/object"
import {Markup} from "@bokehjs/models/widgets/markup"
import {PanelMarkupView} from "./layout"
import {serializeEvent} from "./event-to-object"

export class DOMEvent extends ModelEvent {
constructor(readonly node: string, readonly data: any) {
constructor(readonly node: string, readonly data: unknown) {
super()
}

Expand All @@ -19,27 +20,29 @@ export class DOMEvent extends ModelEvent {
}
}

export function htmlDecode(input: string): string | null {
export function html_decode(input: string): string | null {
const doc = new DOMParser().parseFromString(input, "text/html")
return doc.documentElement.textContent
}

export function runScripts(node: any): void {
Array.from(node.querySelectorAll("script")).forEach((oldScript: any) => {
const newScript = document.createElement("script")
Array.from(oldScript.attributes)
.forEach((attr: any) => newScript.setAttribute(attr.name, attr.value))
newScript.appendChild(document.createTextNode(oldScript.innerHTML))
if (oldScript.parentNode) {
oldScript.parentNode.replaceChild(newScript, oldScript)
export function run_scripts(node: Element): void {
for (const old_script of node.querySelectorAll("script")) {
const new_script = document.createElement("script")
for (const attr of old_script.attributes) {
new_script.setAttribute(attr.name, attr.value)
}
})
new_script.append(document.createTextNode(old_script.innerHTML))
const parent_node = old_script.parentNode
if (parent_node != null) {
parent_node.replaceChild(new_script, old_script)
}
}
}

export class HTMLView extends PanelMarkupView {
declare model: HTML

_event_listeners: any = {}
protected readonly _event_listeners: Map<string, Map<string, (event: Event) => void>> = new Map()

override connect_signals(): void {
super.connect_signals()
Expand Down Expand Up @@ -69,7 +72,7 @@ export class HTMLView extends PanelMarkupView {
if (html !== null) {
this.container.innerHTML = html
if (this.model.run_scripts) {
runScripts(this.container)
run_scripts(this.container)
}
this._setup_event_listeners()
}
Expand All @@ -96,8 +99,8 @@ export class HTMLView extends PanelMarkupView {
}

override process_tex(): string {
const decoded = htmlDecode(this.model.text)
const text = decoded || this.model.text
const decoded = html_decode(this.model.text)
const text = decoded ?? this.model.text
if (this.model.disable_math || !this.contains_tex(text)) {
return text
}
Expand Down Expand Up @@ -129,36 +132,36 @@ export class HTMLView extends PanelMarkupView {
}

private _remove_event_listeners(): void {
for (const node in this._event_listeners) {
const el: any = document.getElementById(node)
for (const [node, callbacks] of this._event_listeners) {
const el = document.getElementById(node)
if (el == null) {
console.warn(`DOM node '${node}' could not be found. Cannot subscribe to DOM events.`)
continue
}
for (const event_name in this._event_listeners[node]) {
const event_callback = this._event_listeners[node][event_name]
for (const [event_name, event_callback] of callbacks) {
el.removeEventListener(event_name, event_callback)
}
}
this._event_listeners = {}
this._event_listeners.clear()
}

private _setup_event_listeners(): void {
for (const node in this.model.events) {
const el: any = document.getElementById(node)
for (const [node, event_names] of entries(this.model.events)) {
const el = document.getElementById(node)
if (el == null) {
console.warn(`DOM node '${node}' could not be found. Cannot subscribe to DOM events.`)
continue
}
for (const event_name of this.model.events[node]) {
const callback = (event: any) => {
for (const event_name of event_names) {
const callback = (event: Event) => {
this.model.trigger_event(new DOMEvent(node, serializeEvent(event)))
}
el.addEventListener(event_name, callback)
if (!(node in this._event_listeners)) {
this._event_listeners[node] = {}
let callbacks = this._event_listeners.get(node)
if (callbacks === undefined) {
this._event_listeners.set(node, callbacks = new Map())
}
this._event_listeners[node][event_name] = callback
callbacks.set(event_name, callback)
}
}
}
Expand All @@ -168,7 +171,7 @@ export namespace HTML {
export type Attrs = p.AttrsOf<Props>

export type Props = Markup.Props & {
events: p.Property<any>
events: p.Property<Dict<string[]>>
run_scripts: p.Property<boolean>
}
}
Expand All @@ -186,8 +189,8 @@ export class HTML extends Markup {

static {
this.prototype.default_view = HTMLView
this.define<HTML.Props>(({Any, Bool}) => ({
events: [ Any, {} ],
this.define<HTML.Props>(({Bool, Str, List, Dict}) => ({
events: [ Dict(List(Str)), {} ],
run_scripts: [ Bool, true ],
}))
}
Expand Down

0 comments on commit 6f59502

Please sign in to comment.