Source code for panel.layout.accordion

from __future__ import annotations

from typing import (
    TYPE_CHECKING, Callable, ClassVar, List, Mapping,
)

import param

from bokeh.models import Column as BkColumn, CustomJS

from ..reactive import Reactive
from .base import NamedListPanel
from .card import Card

if TYPE_CHECKING:
    from bokeh.model import Model

    from ..viewable import Viewable


[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 _direction: ClassVar[str | None] = 'vertical' _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', 'min_width', 'max_width' ] 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 _process_property_change(self, props): props.pop('children', None) return super()._process_property_change(props) 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, old_models = [], [] if len(self._names) != len(self): raise ValueError( 'Accordion names do not match objects, ensure that the ' 'Accordion.objects are not modified directly. Found ' f'{len(self._names)} names, expected {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) del self._panels[id(obj)] 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)): child_params = dict(params, title=name) child_params.update(self._apply_style(i)) if id(pane) in self._panels: card = self._panels[id(pane)] card.param.update(**child_params) else: card = Card( pane, css_classes=['accordion'], header_css_classes=['accordion-header'], **child_params ) card.param.watch(self._set_active, ['collapsed']) self._panels[id(pane)] = card if ref in card._models: panel = card._models[ref][0] old_models.append(panel) 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 as e: if e.layout is not None and e.layout is not self: raise e e.layout = None 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, old_models def _compute_sizing_mode(self, children, props): children = [subchild for child in children for subchild in child.children[1:]] return super()._compute_sizing_mode(children, props) 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) # Public API
[docs] def select( self, selector: type | Callable[[Viewable], bool] | None = None ) -> List[Viewable]: selected = Reactive.select(self, selector) if self._panels: for card in self._panels.values(): selected += card.select(selector) return selected return selected