Mochi0622's picture
initial deploy
e66cfb4
Raw
History Blame Contribute Delete
4.1 kB
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",
},
)