""" layout/page/page_callbacks.py ------------------------------ 頁面級 Dash callback。 callback 定義在 module 層級,import 即生效。 register_page_callbacks() 保留為空殼供相容呼叫。 """ from __future__ import annotations import hashlib, json from dash import callback, Input, Output, State, ALL, ctx, no_update from ..renderer.renderer import render_doc from .sections import get_section_blocks from .loader import build_toc_titles from ..toc import make_toc # render_doc 結果快取(key = section blocks 的 hash) _render_cache: dict[str, object] = {} def _cached_render(blocks: list) -> object: """同樣的 blocks 只渲染一次。""" try: key = hashlib.md5( json.dumps(blocks, ensure_ascii=False, sort_keys=True).encode() ).hexdigest() except Exception: return render_doc(blocks) if key not in _render_cache: _render_cache[key] = render_doc(blocks) return _render_cache[key] # ------------------------------------------------------------------ # Callback 1:切換 section → 更新內容 + TOC + Grid 欄寬 # ------------------------------------------------------------------ @callback( Output("section-content-wrapper", "children"), Output("section-toc-wrapper", "children"), Output("main-content-col", "span"), Output("toc-col", "span"), Output("toc-col", "style"), Output("page-active-section-store", "data"), Input({"type": "section-btn", "sid": ALL}, "n_clicks"), State("page-sections-store", "data"), State("page-raw-toc-store", "data"), prevent_initial_call=True, ) def _cb_switch_section(_, sections, raw_toc): if not sections: return (no_update,) * 6 trig = ctx.triggered_id if not trig: return (no_update,) * 6 section_id = trig.get("sid") current_blocks = get_section_blocks(sections, section_id) content = _cached_render(current_blocks) if section_id == "sec-all": toc_titles = build_toc_titles(current_blocks, raw_toc or []) toc_component = ( make_toc( titles=toc_titles, top_offset=90, col_span={"xs": 0, "sm": 0, "md": 3, "lg": 3, "xl": 3}, visible_from="md", ) if toc_titles else None ) return ( content, toc_component, {"base": 12, "md": 9}, {"base": 12, "md": 3}, {"display": "block"}, section_id, ) return ( content, None, {"base": 12, "md": 12}, {"base": 12, "md": 0}, {"display": "none"}, section_id, ) # ------------------------------------------------------------------ # Callback 2:同步 section 按鈕的 active 樣式 # ------------------------------------------------------------------ @callback( Output({"type": "section-btn", "sid": ALL}, "variant"), Output({"type": "section-btn", "sid": ALL}, "color"), Input("page-active-section-store", "data"), State("page-sections-store", "data"), ) def _cb_highlight_section_buttons(active_sid, sections): if not sections: return [], [] variants, colors = [], [] for s in sections: sid = s.get("id") or "" is_active = sid == active_sid variants.append("filled" if is_active else "light") colors.append("blue" if is_active else "gray") return variants, colors # ------------------------------------------------------------------ # 相容 shim:讓舊的 register_page_callbacks() 呼叫不報錯 # ------------------------------------------------------------------ def register_page_callbacks() -> None: """Callbacks 在 import 時已全部掛載,此函式保留供舊版呼叫。""" pass