Files
Automaaval/dist/zacatraz/_internal/panel/models/html.ts
T
2026-03-14 21:48:05 +00:00

287 lines
8.2 KiB
TypeScript

import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events"
import type * as p from "@bokehjs/core/properties"
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"
function searchAllDOMs(node: Element | ShadowRoot, selector: string): (Element | ShadowRoot)[] {
let found: (Element | ShadowRoot)[] = []
if (node instanceof Element && node.matches(selector)) {
found.push(node)
}
node.children && Array.from(node.children).forEach(child => {
found = found.concat(searchAllDOMs(child, selector))
})
if (node instanceof Element && node.shadowRoot) {
found = found.concat(searchAllDOMs(node.shadowRoot, selector))
}
return found
}
@server_event("html_stream")
export class HTMLStreamEvent extends ModelEvent {
constructor(readonly model: HTML, readonly patch: string, readonly start: number) {
super()
this.patch = patch
this.start = start
this.origin = model
}
protected override get event_values(): Attrs {
return {model: this.origin, patch: this.patch, start: this.start}
}
static override from_values(values: object) {
const {model, patch, start} = values as {model: HTML, patch: string, start: number}
return new HTMLStreamEvent(model, patch, start)
}
}
export class DOMEvent extends ModelEvent {
constructor(readonly node: string, readonly data: unknown) {
super()
}
protected override get event_values(): Attrs {
return {model: this.origin, node: this.node, data: this.data}
}
static {
this.prototype.event_name = "dom_event"
}
}
export function html_decode(input: string): string | null {
const doc = new DOMParser().parseFromString(input, "text/html")
return doc.documentElement.textContent
}
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)
}
}
}
function throttle(func: Function, limit: number): any {
let lastFunc: ReturnType<typeof setTimeout> | undefined
let lastRan: number
return function(...args: any) {
// @ts-ignore
const context = this
if (!lastRan) {
func.apply(context, args)
lastRan = Date.now()
} else {
clearTimeout(lastFunc)
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args)
lastRan = Date.now()
}
}, limit - (Date.now() - lastRan))
}
}
}
export class HTMLView extends PanelMarkupView {
declare model: HTML
_buffer: string | null = null
protected readonly _event_listeners: Map<string, Map<string, (event: Event) => void>> = new Map()
override connect_signals(): void {
super.connect_signals()
const {text, visible, events} = this.model.properties
this.on_change(text, () => {
this._buffer = null
const html = this.process_tex()
this.set_html(html)
})
this.on_change(visible, () => {
if (this.model.visible) {
this.container.style.visibility = "visible"
}
})
this.on_change(events, () => {
this._remove_event_listeners()
this._setup_event_listeners()
})
const set_text = throttle(() => {
const text = this._buffer
this._buffer = null
this.model.setv({text}, {silent: true})
const html = this.process_tex()
this.set_html(html)
}, 10)
this.model.on_event(HTMLStreamEvent, (event: HTMLStreamEvent) => {
const beginning = this._buffer == null ? this.model.text : this._buffer
this._buffer = beginning.slice(0, event.start)+event.patch
set_text()
})
}
protected rerender() {
this.render()
this.invalidate_layout()
}
set_html(html: string | null): void {
if (html === null) {
return
}
this.container.innerHTML = html
if (this.model.run_scripts) {
run_scripts(this.container)
}
this._setup_event_listeners()
for (const anchor of this.container.querySelectorAll("a")) {
const link = anchor.getAttribute("href")
if (link && link.startsWith("#")) {
anchor.addEventListener("click", () => {
const found = searchAllDOMs(document.body, link)
if ((found.length > 0) && found[0] instanceof Element) {
found[0].scrollIntoView()
}
})
if (!this.root.has_finished() && this.model.document && window.location.hash === link) {
this.model.document.on_event("document_ready", () => {
anchor.scrollIntoView()
setTimeout(() => anchor.scrollIntoView(), 5)
})
}
}
}
}
override render(): void {
super.render()
this.container.style.visibility = "hidden"
this.shadow_el.appendChild(this.container)
if (this.provider.status == "failed" || this.provider.status == "loaded") {
this._has_finished = true
}
const html = this.process_tex()
this.watch_stylesheets()
this.set_html(html)
}
override style_redraw(): void {
if (this.model.visible) {
this.container.style.visibility = "visible"
}
}
override process_tex(): string {
const decoded = html_decode(this.model.text)
const text = decoded ?? this.model.text
if (this.model.disable_math || !this.contains_tex(text)) {
return text
}
const tex_parts = this.provider.MathJax.find_tex(text)
const processed_text: string[] = []
let last_index: number | undefined = 0
for (const part of tex_parts) {
processed_text.push(text.slice(last_index, part.start.n))
processed_text.push(this.provider.MathJax.tex2svg(part.math, {display: part.display}).outerHTML)
last_index = part.end.n
}
if (last_index! < text.length) {
processed_text.push(text.slice(last_index))
}
return processed_text.join("")
}
private contains_tex(html: string): boolean {
if (!this.provider.MathJax) {
return false
}
return this.provider.MathJax.find_tex(html).length > 0
}
private _remove_event_listeners(): void {
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, event_callback] of callbacks) {
el.removeEventListener(event_name, event_callback)
}
}
this._event_listeners.clear()
}
private _setup_event_listeners(): void {
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 event_names) {
const callback = (event: Event) => {
this.model.trigger_event(new DOMEvent(node, serializeEvent(event)))
}
el.addEventListener(event_name, callback)
let callbacks = this._event_listeners.get(node)
if (callbacks === undefined) {
this._event_listeners.set(node, callbacks = new Map())
}
callbacks.set(event_name, callback)
}
}
}
}
export namespace HTML {
export type Attrs = p.AttrsOf<Props>
export type Props = Markup.Props & {
events: p.Property<Dict<string[]>>
run_scripts: p.Property<boolean>
}
}
export interface HTML extends HTML.Attrs {}
export class HTML extends Markup {
declare properties: HTML.Props
constructor(attrs?: Partial<HTML.Attrs>) {
super(attrs)
}
static override __module__ = "panel.models.markup"
static {
this.prototype.default_view = HTMLView
this.define<HTML.Props>(({Bool, Str, List, Dict}) => ({
events: [ Dict(List(Str)), {} ],
run_scripts: [ Bool, true ],
}))
}
}