Spaces:
Sleeping
Sleeping
| 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", | |
| }, | |
| ) |