import dash_mantine_components as dmc from dash import html, dcc, Input, Output, State, clientside_callback, ALL _TOC_CB_REGISTERED = False def _indent_px_by_level(level: int, base_px: int = 14) -> int: try: lvl = int(level or 1) except Exception: lvl = 1 if lvl <= 1: return 0 # H1 elif lvl == 2: return base_px # H2 else: return base_px * 2 # H3 以上 def make_toc( titles, *, top_offset: int = 80, col_span=None, visible_from: str = "md", ): global _TOC_CB_REGISTERED if not _TOC_CB_REGISTERED: clientside_callback( """ function(n_list, id_list, topOffset) { if (!n_list || !Array.isArray(n_list) || !id_list) { return window.dash_clientside.no_update; } const ctx = dash_clientside.callback_context; if (!ctx.triggered.length) { return window.dash_clientside.no_update; } const propId = ctx.triggered[0].prop_id; const m = propId.match(/\\[(\\d+)\\]\\.n_clicks$/); let idx = -1; if (m) { idx = parseInt(m[1]); } else { idx = n_list.findIndex(v => v); } if (idx < 0 || !id_list[idx]) { return window.dash_clientside.no_update; } const targetId = id_list[idx].target; const el = document.getElementById(targetId); if (el) { const y = el.getBoundingClientRect().top + window.scrollY - (parseInt(topOffset) || 0); window.scrollTo({ top: y, behavior: 'smooth' }); } return new Array(n_list.length).fill(null); } """, Output({"type": "toc-btn", "target": ALL}, "n_clicks"), Input({"type": "toc-btn", "target": ALL}, "n_clicks"), State({"type": "toc-btn", "target": ALL}, "id"), State({"type": "toc-offset"}, "data"), prevent_initial_call=True, ) _TOC_CB_REGISTERED = True if titles and isinstance(titles, (list, tuple)) and titles[0]: first_label = titles[0].get("label") or "" else: first_label = "" if first_label: toc_title = f"目錄({first_label})" iter_titles = titles[1:] else: toc_title = "目錄" iter_titles = titles buttons = [] for t in iter_titles: lvl = int(t.get("level", 1)) pad_left = _indent_px_by_level(lvl, base_px=14) wrapper = html.Div( dmc.Button( t["label"], id={"type": "toc-btn", "target": t["id"]}, variant="subtle", fullWidth=True, fw=400, className="toc-button", style={ "justifyContent": "flex-start", "textAlign": "left", "fontWeight": 400, "fontSize": "16px", "width": "100%", "padding": "6px 0", "display": "flex", }, ), style={"paddingLeft": f"{pad_left}px"}, **{ "data-level": str(lvl), "data-target": t["id"], }, className="toc-item", ) buttons.append(wrapper) scroll_box = html.Div( dmc.Stack( children=[ dmc.Title(toc_title, order=3), dcc.Store(id={"type": "toc-offset"}, data=int(top_offset)), *buttons, ], gap="xs", ), className="toc-scroll", id="toc-root", **{"data-toc-offset": int(top_offset)}, ) return dmc.Stack( [scroll_box], gap="xs", className="toc-container", style={ "textAlign": "left", }, )