Spaces:
Sleeping
Sleeping
File size: 4,101 Bytes
e66cfb4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | 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",
},
) |