Source code for panel.layout.accordion

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar, Mapping

import param

from bokeh.models import Column as BkColumn, CustomJS

from .base import NamedListPanel
from .card import Card

if TYPE_CHECKING:
    from bokeh.model import Model


[docs]class Accordion(NamedListPanel): """ The `Accordion` layout is a type of `Card` layout that allows switching between multiple objects by clicking on the corresponding card header. The labels for each card will default to the `name` parameter of the card’s contents, but may also be defined explicitly as part of a tuple. Like `Column` and `Row`, `Accordion` has a list-like API that allows interactively updating and modifying the cards using the methods `append`, `extend`, `clear`, `insert`, `pop`, `remove` and `__setitem__`. Reference: https://panel.holoviz.org/reference/layouts/Accordion.html :Example: >>> pn.Accordion(some_pane_with_a_name, ("Plot", some_plot)) """ active_header_background = param.String(default='#ccc', doc=""" Color for currently active headers.""") active = param.List(default=[], doc=""" List of indexes of active cards.""") header_color = param.String(doc=""" A valid CSS color to apply to the expand button.""") header_background = param.String(doc=""" A valid CSS color for the header background.""") toggle = param.Boolean(default=False, doc=""" Whether to toggle between active cards or allow multiple cards""") _bokeh_model = BkColumn _rename: ClassVar[Mapping[str, str | None]] = {'active': None, 'active_header_background': None, 'header_background': None, 'objects': 'children', 'dynamic': None, 'toggle': None, 'header_color': None} _toggle = """ for (var child of accordion.children) { if ((child.id !== cb_obj.id) && (child.collapsed == cb_obj.collapsed) && !cb_obj.collapsed) { child.collapsed = !cb_obj.collapsed } } """ _synced_properties = [ 'active_header_background', 'header_background', 'width', 'sizing_mode', 'width_policy', 'height_policy', 'header_color' ] def __init__(self, *objects, **params): super().__init__(*objects, **params) self._updating_active = False self.param.watch(self._update_active, ['active']) self.param.watch(self._update_cards, self._synced_properties) def _get_objects(self, model, old_objects, doc, root, comm=None): """ Returns new child models for the layout while reusing unchanged models and cleaning up any dropped objects. """ from panel.pane.base import RerenderError, panel new_models = [] if len(self._names) != len(self): raise ValueError('Accordion names do not match objects, ensure ' 'that the Tabs.objects are not modified ' 'directly. Found %d names, expected %d.' % (len(self._names), len(self))) for i, (name, pane) in enumerate(zip(self._names, self)): pane = panel(pane, name=name) self.objects[i] = pane for obj in old_objects: if obj not in self.objects: self._panels[id(obj)]._cleanup(root) params = {k: v for k, v in self.param.values().items() if k in self._synced_properties} ref = root.ref['id'] current_objects = list(self) self._updating_active = True for i, (name, pane) in enumerate(zip(self._names, self)): params.update(self._apply_style(i)) if id(pane) in self._panels: card = self._panels[id(pane)] else: card = Card( pane, title=name, css_classes=['accordion'], header_css_classes=['accordion-header'], margin=self.margin ) card.param.watch(self._set_active, ['collapsed']) self._panels[id(pane)] = card card.param.update(**params) if ref in card._models: panel = card._models[ref][0] else: try: panel = card._get_model(doc, root, model, comm) if self.toggle: cb = CustomJS(args={'accordion': model}, code=self._toggle) panel.js_on_change('collapsed', cb) except RerenderError: return self._get_objects(model, current_objects[:i], doc, root, comm) new_models.append(panel) self._updating_active = False self._set_active() self._update_cards() self._update_active() return new_models def _cleanup(self, root: Model | None = None) -> None: for panel in self._panels.values(): panel._cleanup(root) super()._cleanup(root) def _apply_style(self, i): if i == 0: margin = (5, 5, 0, 5) elif i == (len(self)-1): margin = (0, 5, 5, 5) else: margin = (0, 5, 0, 5) return dict(margin=margin, collapsed = i not in self.active) def _set_active(self, *events): if self._updating_active: return self._updating_active = True try: if self.toggle and events and not events[0].new: active = [list(self._panels.values()).index(events[0].obj)] else: active = [] for i, pane in enumerate(self.objects): if id(pane) not in self._panels: continue elif not self._panels[id(pane)].collapsed: active.append(i) if not self.toggle or active: self.active = active finally: self._updating_active = False def _update_active(self, *events): if self._updating_active: return self._updating_active = True try: for i, pane in enumerate(self.objects): if id(pane) not in self._panels: continue self._panels[id(pane)].collapsed = i not in self.active finally: self._updating_active = False def _update_cards(self, *events): params = {k: v for k, v in self.param.values().items() if k in self._synced_properties} for panel in self._panels.values(): panel.param.update(**params)