__init__.py
  1  # SPDX-License-Identifier: MIT
  2  #
  3  # Copyright (c) 2021 The Anvil Extras project team members listed at
  4  # https://github.com/anvilistas/anvil-extras/graphs/contributors
  5  #
  6  # This software is published at https://github.com/anvilistas/anvil-extras
  7  from anvil import HtmlPanel as _HtmlPanel
  8  from anvil.js import get_dom_node
  9  
 10  from ...utils._component_helpers import _add_roles, _remove_roles
 11  from ._anvil_designer import OptionTemplate
 12  
 13  __version__ = "3.1.0"
 14  
 15  
 16  class Divider(_HtmlPanel):
 17      _anvil_events_ = ["click"]
 18      is_divider = True
 19  
 20      def __init__(self):
 21          get_dom_node(self).dataset["divider"] = ""
 22          self.key = ""
 23          self.value = None
 24  
 25      @property
 26      def selected(self):
 27          return False
 28  
 29      @selected.setter
 30      def selected(self, val):
 31          pass
 32  
 33  
 34  class Option(OptionTemplate):
 35      is_divider = False
 36  
 37      def __init__(self, **properties):
 38          self._props = properties
 39          self._dom_node = get_dom_node(self)
 40          self._icon_node = get_dom_node(self.icon_checked)
 41          self._icon_node.style.visibility = "hidden"
 42          self.label.role = "ae-ms-option-label"
 43          self.label_sub.role = "ae-ms-option-subtext"
 44          self.role = ["ae-ms-option"]
 45  
 46          self.init_components(**properties)
 47          self._dom_node.addEventListener("focus", self._on_focus)
 48          self._dom_node.addEventListener("blur", self._on_blur)
 49  
 50      @property
 51      def selected(self):
 52          return self._props.get("selected", False)
 53  
 54      @selected.setter
 55      def selected(self, val):
 56          if self.disabled:
 57              return
 58          self._props["selected"] = val
 59          self._icon_node.style.visibility = "visible" if val else "hidden"
 60  
 61      @property
 62      def active(self):
 63          return "ae-ms-option-active" in self.role
 64  
 65      @active.setter
 66      def active(self, val):
 67          if val:
 68              _add_roles(self, "ae-ms-option-active")
 69              self._dom_node.scrollIntoView({"block": "nearest"})
 70          else:
 71              _remove_roles(self, "ae-ms-option-active")
 72  
 73      @property
 74      def disabled(self):
 75          return self._props.get("disabled", False)
 76  
 77      @disabled.setter
 78      def disabled(self, val):
 79          self._props["disabled"] = val
 80          if val:
 81              self._dom_node.dataset["disabled"] = ""
 82          else:
 83              del self._dom_node.dataset["disabled"]
 84  
 85      def focus(self):
 86          self._dom_node.focus()
 87  
 88      def click(self):
 89          self.raise_event("click")
 90  
 91      def _on_focus(self, e):
 92          if not self.disabled:
 93              self.active = True
 94  
 95      def _on_blur(self, e):
 96          self.active = False
 97  
 98      def _on_click(self, **event_args):
 99          self.selected = not self.selected
100  
101      def _on_hide(self, **event_args):
102          self.active = False
103  
104      def _on_show(self, **event_args):
105          if self.disabled:
106              # and option isn't dynamic so we can do this on the show event and not worry about it
107              self._dom_node.parentElement.style.cursor = "not-allowed"
108  
109      @classmethod
110      def from_str(cls, item: str, idx: str) -> tuple:
111          key = value = item
112          if item == "---":
113              return Divider()
114          else:
115              return cls(idx=idx, key=key, value=value)
116  
117      @classmethod
118      def from_tuple(cls, item: tuple, idx: int) -> tuple:
119          key, value = item
120          if not isinstance(key, str):
121              raise TypeError(
122                  f"expected a tuple of the form str, value in items at idx {idx}"
123              )
124          return cls(idx=idx, key=key, value=value)
125  
126      @classmethod
127      def from_dict(cls, item: dict, idx: int) -> tuple:
128          sentinel = object()
129  
130          # if they only set a key and not a value then use the key as the value
131          value = item.get("value", sentinel)
132          if value is sentinel:
133              value = item.get("key")
134  
135          title = item.get("title", "")
136          icon = item.get("icon", "")
137          subtext = item.get("subtext", "")
138          disabled = not item.get("enabled", True)
139          key = item.get("key")
140  
141          return cls(
142              idx=idx,
143              key=key,
144              value=value,
145              icon=icon,
146              subtext=subtext,
147              title=title,
148              disabled=disabled,
149          )
150  
151      @classmethod
152      def from_items(cls, items):
153          options = []
154  
155          for idx, item in enumerate(items):
156              if isinstance(item, str):
157                  option = cls.from_str(item, idx)
158              elif isinstance(item, (tuple, list)):
159                  option = cls.from_tuple(item, idx)
160              elif isinstance(item, dict):
161                  option = cls.from_dict(item, idx)
162              else:
163                  raise TypeError(f"Invalid item at index {idx} (got type {type(item)})")
164  
165              options.append(option)
166  
167          return options