diff --git a/docs/components_page/__init__.py b/docs/components_page/__init__.py index 489bb8ea..3342769a 100644 --- a/docs/components_page/__init__.py +++ b/docs/components_page/__init__.py @@ -3,7 +3,7 @@ import dash import dash_bootstrap_components as dbc -from dash import html +from dash import ClientsideFunction, Input, Output, html from jinja2 import Environment, FileSystemLoader from .components.table.simple import table_body, table_header # noqa @@ -18,6 +18,22 @@ LOREM = (COMPONENTS / "modal" / "lorem.txt").read_text().strip() +CLIENTSIDE_CALLBACK = """ +if (!window.dash_clientside) { + window.dash_clientside = {}; +} +window.dash_clientside.clientside = { + make_draggable: function(id) { + setTimeout(function() { + var el = document.getElementById(id) + window.console.log(el) + dragula([el]) + }, 1) + return window.dash_clientside.no_update + } +} +""" + HIGHLIGHT_JS_CSS = ( "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.1/" "build/styles/monokai-sublime.min.css" @@ -149,6 +165,7 @@ def register_apps(): ) app = dash.Dash( external_stylesheets=["/static/loading.css"], + external_scripts=["/static/js/clientside.js"], requests_pathname_prefix=requests_pathname_prefix, suppress_callback_exceptions=True, serve_locally=SERVE_LOCALLY, @@ -171,6 +188,17 @@ def register_apps(): ) else: app.layout = parse(app, **kwargs) + + app.clientside_callback( + ClientsideFunction( + namespace="clientside", function_name="scrollAfterLoad" + ), + # id won't actually be updated, we just want the callback to run + # once Dash has initialised and hydrated the page + Output("url", "id"), + Input("url", "hash"), + ) + if slug == "index": routes["/docs/components"] = app else: diff --git a/docs/components_page/markdown_parser.py b/docs/components_page/markdown_parser.py index e3d83449..e99a8e84 100644 --- a/docs/components_page/markdown_parser.py +++ b/docs/components_page/markdown_parser.py @@ -27,6 +27,10 @@ PROP_NAME_PATTERN = re.compile(r"""(\n- )(\w+?) \(""") NESTED_PROP_NAME_PATTERN = re.compile(r"""(\n\s+- )(\w+?) \(""") +HEADING_TEMPLATE = """ + {heading}# +""" + def parse(app, markdown_path, extra_env_vars=None): extra_env_vars = extra_env_vars or {} @@ -46,7 +50,8 @@ def parse(app, markdown_path, extra_env_vars=None): markdown_blocks = SPLIT_PATTERN.split(raw) markdown_blocks = [ - dcc.Markdown(block.strip()) for block in markdown_blocks + dcc.Markdown(_preprocess_markdown(block), dangerously_allow_html=True) + for block in markdown_blocks ] examples_docs = EXAMPLE_DOC_PATTERN.findall(raw) @@ -55,7 +60,24 @@ def parse(app, markdown_path, extra_env_vars=None): ] content.extend(_interleave(markdown_blocks, examples_docs)) - return html.Div(content, key=str(markdown_path)) + return html.Div([dcc.Location(id="url")] + content, key=str(markdown_path)) + + +def _preprocess_markdown(text): + text = text.strip() + lines = text.split("\n") + new_lines = [] + for line in lines: + if line.startswith("#"): + level, heading = line.split(" ", 1) + level = level.count("#") + id_ = heading.replace(" ", "-").lower() + new_lines.append( + HEADING_TEMPLATE.format(level=level, id_=id_, heading=heading) + ) + else: + new_lines.append(line) + return "\n".join(new_lines) def _parse_block(block, app, extra_env_vars): diff --git a/docs/static/docs.css b/docs/static/docs.css index fd3d7673..3d9c92ef 100644 --- a/docs/static/docs.css +++ b/docs/static/docs.css @@ -479,3 +479,16 @@ span.hljs-meta { .superhero-demo .dropdown-divider { border-top: 1px solid rgba(0, 0, 0, 0.15); } + +.anchor-link { + color: var(--bs-secondary); + opacity: 0.5; + margin-left: 0.5rem; + text-decoration: none; + transition: color 0.15s ease-in-out, opacity 0.15s ease-in-out; +} + +.anchor-link:hover, :hover>.anchor-link { + color: var(--bs-primary); + opacity: 1; +} diff --git a/docs/static/js/clientside.js b/docs/static/js/clientside.js new file mode 100644 index 00000000..1e5dd27c --- /dev/null +++ b/docs/static/js/clientside.js @@ -0,0 +1,22 @@ +const delays = [100, 100, 300, 500, 1000, 2000]; + +function scrollWithRetry(id, idx) { + const targetElement = document.getElementById(id); + if (targetElement) { + targetElement.scrollIntoView(); + } else if (idx < delays.length) { + setTimeout(() => scrollWithRetry(id, idx + 1), delays[idx]); + } +} + +if (!window.dash_clientside) { + window.dash_clientside = {}; +} +window.dash_clientside.clientside = { + scrollAfterLoad: function(hash) { + if (hash) { + scrollWithRetry(hash.slice(1), 0); + } + return window.dash_clientside.no_update; + } +};