Skip to content

Commit

Permalink
Support anchor links in HTML model (#7258)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Sep 12, 2024
1 parent a9162e1 commit d90097b
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 5 deletions.
39 changes: 34 additions & 5 deletions panel/models/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ 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) {
Expand Down Expand Up @@ -127,12 +141,27 @@ export class HTMLView extends PanelMarkupView {
}

set_html(html: string | null): void {
if (html !== null) {
this.container.innerHTML = html
if (this.model.run_scripts) {
run_scripts(this.container)
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", () => setTimeout(() => anchor.scrollIntoView(), 5))
}
}
this._setup_event_listeners()
}
}

Expand Down
38 changes: 38 additions & 0 deletions panel/tests/ui/pane/test_markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from playwright.sync_api import expect

from panel.layout import Row
from panel.models import HTML
from panel.pane import Markdown
from panel.tests.util import serve_component, wait_until
Expand Down Expand Up @@ -115,3 +116,40 @@ def test_html_model_no_stylesheet(page):
header_element = page.locator('h1:has-text("Header")')
assert header_element.is_visible()
assert header_element.text_content() == "Header"

def test_anchor_scroll(page):
md = ''
for tag in ['tag1', 'tag2', 'tag3']:
md += f'# {tag}\n\n'
md += f'{tag} content\n' * 50

content = Markdown(md)
link = Markdown('<a id="link1" href="#tag1">Link1</a><a id="link3" href="#tag3">Link</a>')

serve_component(page, Row(link, content))

expect(page.locator('#tag1')).to_be_in_viewport()
expect(page.locator('#tag3')).not_to_be_in_viewport()

page.locator('#link3').click()

expect(page.locator('#tag1')).not_to_be_in_viewport()
expect(page.locator('#tag3')).to_be_in_viewport()

page.locator('#link1').click()

expect(page.locator('#tag1')).to_be_in_viewport()
expect(page.locator('#tag3')).not_to_be_in_viewport()

def test_anchor_scroll_on_init(page):
md = ''
for tag in ['tag1', 'tag2', 'tag3']:
md += f'# {tag}\n\n'
md += f'{tag} content\n' * 50

content = Markdown(md)

serve_component(page, content, suffix='#tag3')

expect(page.locator('#tag1')).not_to_be_in_viewport()
expect(page.locator('#tag3')).to_be_in_viewport()

0 comments on commit d90097b

Please sign in to comment.