221 lines
6.4 KiB
TypeScript
221 lines
6.4 KiB
TypeScript
import {Column, ColumnView} from "./column"
|
|
import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events"
|
|
import type * as p from "@bokehjs/core/properties"
|
|
import type {Attrs} from "@bokehjs/core/types"
|
|
import {build_views} from "@bokehjs/core/build_views"
|
|
import type {UIElementView} from "@bokehjs/models/ui/ui_element"
|
|
import {ColumnView as BkColumnView} from "@bokehjs/models/layouts/column"
|
|
|
|
@server_event("scroll_latest_event")
|
|
export class ScrollLatestEvent extends ModelEvent {
|
|
constructor(readonly model: Feed, readonly rerender: boolean) {
|
|
super()
|
|
this.origin = model
|
|
this.rerender = rerender
|
|
}
|
|
|
|
protected override get event_values(): Attrs {
|
|
return {model: this.origin, rerender: this.rerender}
|
|
}
|
|
|
|
static override from_values(values: object) {
|
|
const {model, rerender} = values as {model: Feed, rerender: boolean}
|
|
return new ScrollLatestEvent(model, rerender)
|
|
}
|
|
}
|
|
|
|
export class FeedView extends ColumnView {
|
|
declare model: Feed
|
|
_intersection_observer: IntersectionObserver
|
|
_last_visible: UIElementView | null
|
|
_rendered: boolean = false
|
|
_sync: boolean
|
|
_reference: number | null = null
|
|
_reference_view: UIElementView | null = null
|
|
|
|
override initialize(): void {
|
|
super.initialize()
|
|
this._sync = true
|
|
this._intersection_observer = new IntersectionObserver((entries) => {
|
|
const visible = [...this.model.visible_children]
|
|
const nodes = this.node_map
|
|
|
|
for (const entry of entries) {
|
|
const id = nodes.get(entry.target).id
|
|
if (entry.isIntersecting) {
|
|
if (!visible.includes(id)) {
|
|
visible.push(id)
|
|
}
|
|
} else if (visible.includes(id)) {
|
|
visible.splice(visible.indexOf(id), 1)
|
|
}
|
|
}
|
|
|
|
if (this._sync) {
|
|
this.model.visible_children = visible
|
|
}
|
|
|
|
if (visible.length > 0) {
|
|
const refs = this.child_models.map((model) => model.id)
|
|
const indices = visible.map((ref) => refs.indexOf(ref))
|
|
this._last_visible = this.child_views[Math.min(...indices)]
|
|
} else {
|
|
this._last_visible = null
|
|
}
|
|
}, {
|
|
root: this.el,
|
|
threshold: 0.01,
|
|
})
|
|
}
|
|
|
|
override connect_signals(): void {
|
|
super.connect_signals()
|
|
this.model.on_event(ScrollLatestEvent, (event: ScrollLatestEvent) => {
|
|
this.scroll_to_latest()
|
|
if (event.rerender) {
|
|
this._rendered = false
|
|
}
|
|
})
|
|
}
|
|
|
|
get node_map(): any {
|
|
const nodes = new Map()
|
|
for (const view of this.child_views) {
|
|
nodes.set(view.el, view.model)
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
override async update_children(): Promise<void> {
|
|
const last = this._last_visible
|
|
const scroll_top = this.el.scrollTop
|
|
this._reference_view = last
|
|
this._reference = last?.el.offsetTop || 0
|
|
this._sync = false
|
|
const created = await this.build_child_views()
|
|
const created_children = new Set(created)
|
|
const createdLength = created.length
|
|
const views_length = this.child_views.length
|
|
|
|
// Check whether we simply have to prepend or append items
|
|
// instead of removing and reordering them
|
|
const is_prepended = created.every((view, index) => view === this.child_views[index])
|
|
const is_appended = created.every((view, index) => view === this.child_views[views_length - createdLength + index])
|
|
const reorder = !(is_prepended || is_appended)
|
|
if (reorder) {
|
|
// First remove and then either reattach existing elements or render and
|
|
// attach new elements, so that the order of children is consistent, while
|
|
// avoiding expensive re-rendering of existing views.
|
|
for (const child_view of this.child_views) {
|
|
child_view.el.remove()
|
|
}
|
|
}
|
|
const prepend: Element[] = []
|
|
for (const child_view of this.child_views) {
|
|
const is_new = created_children.has(child_view)
|
|
const target = this.shadow_el
|
|
if (reorder) {
|
|
if (is_new) {
|
|
child_view.render_to(target)
|
|
} else {
|
|
target.append(child_view.el)
|
|
}
|
|
} else {
|
|
if (is_new) {
|
|
child_view.render()
|
|
if (is_appended) {
|
|
target.append(child_view.el)
|
|
} else if (is_prepended) {
|
|
prepend.push(child_view.el)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (is_prepended) {
|
|
this.shadow_el.prepend(...prepend)
|
|
}
|
|
this.r_after_render()
|
|
this._update_children()
|
|
this.invalidate_layout()
|
|
this._sync = true
|
|
|
|
// Ensure we adjust the scroll position in case we prepended items
|
|
if (is_prepended) {
|
|
requestAnimationFrame(() => {
|
|
const after_offset = this._reference_view?.el.offsetTop || 0
|
|
const offset = (after_offset-(this._reference || 0))
|
|
this.el.scrollTo({top: scroll_top + offset, behavior: "smooth"})
|
|
})
|
|
}
|
|
}
|
|
|
|
override async build_child_views(): Promise<UIElementView[]> {
|
|
const {created, removed} = await build_views(this._child_views, this.child_models, {parent: this})
|
|
|
|
const visible = this.model.visible_children
|
|
for (const view of removed) {
|
|
if (visible.includes(view.model.id)) {
|
|
visible.splice(visible.indexOf(view.model.id), 1)
|
|
}
|
|
this._resize_observer.unobserve(view.el)
|
|
this._intersection_observer.unobserve(view.el)
|
|
}
|
|
this.model.visible_children = [...visible]
|
|
|
|
for (const view of created) {
|
|
this._resize_observer.observe(view.el, {box: "border-box"})
|
|
this._intersection_observer.observe(view.el)
|
|
}
|
|
|
|
return created
|
|
}
|
|
|
|
override render(): void {
|
|
this._rendered = false
|
|
super.render()
|
|
}
|
|
|
|
override trigger_auto_scroll(): void {}
|
|
|
|
override after_render(): void {
|
|
BkColumnView.prototype.after_render.call(this)
|
|
requestAnimationFrame(() => {
|
|
if (this.model.scroll_position) {
|
|
this.scroll_to_position()
|
|
}
|
|
if (this.model.view_latest && !this._rendered) {
|
|
this.scroll_to_latest()
|
|
}
|
|
this.toggle_scroll_button()
|
|
this._rendered = true
|
|
})
|
|
}
|
|
}
|
|
|
|
export namespace Feed {
|
|
export type Attrs = p.AttrsOf<Props>
|
|
export type Props = Column.Props & {
|
|
visible_children: p.Property<string[]>
|
|
}
|
|
}
|
|
|
|
export interface Feed extends Feed.Attrs { }
|
|
|
|
export class Feed extends Column {
|
|
declare properties: Feed.Props
|
|
|
|
constructor(attrs?: Partial<Feed.Attrs>) {
|
|
super(attrs)
|
|
}
|
|
|
|
static override __module__ = "panel.models.feed"
|
|
|
|
static {
|
|
this.prototype.default_view = FeedView
|
|
|
|
this.define<Feed.Props>(({List, Str}) => ({
|
|
visible_children: [List(Str), []],
|
|
}))
|
|
}
|
|
}
|