/ client_code / Tabs / __init__.py
__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  
  8  from functools import partial
  9  
 10  import anvil.js
 11  from anvil import HtmlPanel as _HtmlPanel
 12  from anvil import Link as _Link
 13  
 14  from ..utils._component_helpers import (
 15      _get_color,
 16      _get_rgb,
 17      _html_injector,
 18      _spacing_property,
 19      _supports_relative_colors,
 20  )
 21  from ._anvil_designer import TabsTemplate
 22  
 23  __version__ = "3.1.0"
 24  
 25  _html_injector.css(
 26      """
 27  .ae-tabs-container.anvil-role-card {
 28      border-bottom-left-radius: 0px;
 29      border-bottom-right-radius: 0px;
 30  }
 31  .ae-tabs {
 32      position: relative;
 33      overflow-x: auto;
 34      overflow-y: hidden;
 35      height: auto;
 36      width: 100%;
 37      background-color: var(--background, inherit);
 38      margin: 0 auto;
 39      white-space: nowrap;
 40      padding: 0;
 41      display: flex;
 42      z-index: 1;
 43  }
 44  .ae-tabs .ae-tab {
 45      flex-grow: 1;
 46      display: inline-block;
 47      text-align: center;
 48      line-height: 48px;
 49      height: 48px;
 50      padding: 0;
 51      margin: 0;
 52      text-transform: uppercase
 53  }
 54  .ae-tabs .ae-tab a {
 55      color: rgb(var(--color) / 0.7);
 56      display: block;
 57      width: 100%;
 58      height: 100%;
 59      padding: 0 24px;
 60      font-size: 14px;
 61      overflow: hidden;
 62      -webkit-transition: color .28s ease, background-color .28s ease;
 63      transition: color .28s ease, background-color .28s ease
 64  }
 65  .ae-tabs .ae-tab a > div {
 66      text-overflow: ellipsis;
 67      white-space: nowrap;
 68  }
 69  .ae-tabs .ae-tab a:focus,.ae-tabs .ae-tab a:focus.ae-tab-active {
 70      --fallback-bg: var(--color) / 0.2;
 71      background-color: rgb(var(--active-bg, var(--fallback-bg)));
 72      outline: none
 73  }
 74  .ae-tabs .ae-tab a:hover,.ae-tabs .ae-tab a.ae-tab-active {
 75      background-color: transparent;
 76      color: rgb(var(--color));
 77  }
 78  .ae-tabs .ae-tab a:hover,.ae-tabs .ae-tab a.ae-tab-active {
 79      background-color: rgb(var(--active-bg));
 80  }
 81  .ae-tabs .ae-tab-indicator {
 82      position: absolute;
 83      bottom: 0;
 84      height: 3px;
 85      background-color: rgb(var(--color) / 0.4);
 86      will-change: left, right;
 87  }
 88  
 89  @supports (color: rgb(from white r g b / 0.2)) {
 90      .ae-tabs .ae-tab a:hover,.ae-tabs .ae-tab a.ae-tab-active {
 91          color: var(--color);
 92      }
 93      .ae-tabs .ae-tab a {
 94          color: rgb(from var(--color) r g b / 0.7);
 95      }
 96      .ae-tabs .ae-tab a:focus,.ae-tabs .ae-tab a:focus.ae-tab-active {
 97          --fallback-bg: rgb(from var(--color) r g b / 0.2);
 98          background-color: var(--active-bg, var(--fallback-bg));
 99      }
100      .ae-tabs .ae-tab a:hover,.ae-tabs .ae-tab a.ae-tab-active {
101          background-color: var(--active-bg);
102      }
103      .ae-tabs .ae-tab-indicator {
104          background-color: rgb(from var(--color) r g b / 0.4);
105      }
106  }
107  """
108  )
109  
110  if _supports_relative_colors():
111      _get_rgb = _get_color
112  
113  _defaults = {
114      "align": "left",
115      "tab_titles": [],
116      "active_tab_index": 0,
117      "active_background": "",
118      "spacing_above": "none",
119      "spacing_below": "none",
120      "foreground": "",
121      "background": "",
122      "role": None,
123      "visible": True,
124      "bold": False,
125      "italic": False,
126      "font": None,
127      "font_size": None,
128  }
129  
130  from anvil.js import window
131  
132  ResizeObserver = window.get("ResizeObserver")
133  if ResizeObserver is None:
134  
135      class ResizeObserver:
136          def __init__(self, *args):
137              pass
138  
139          def observe(self, node):
140              pass
141  
142          def disconnect(self):
143              pass
144  
145  
146  def _apply_to_links(prop):
147      def getter(self):
148          return self._props[prop]
149  
150      def setter(self, value):
151          self._props[prop] = value
152          for link in self.get_components():
153              setattr(link, prop, value)
154  
155      return property(getter, setter)
156  
157  
158  class Tabs(TabsTemplate):
159      def __init__(self, **properties):
160          #### set up dom nodes
161          self._shown = False
162          dom_node = self._dom_node = anvil.js.get_dom_node(self)
163          dom_node.style.padding = "0"
164          dom_node.classList.add("ae-tabs-container")
165  
166          self._tabs_node = self.dom_nodes["ae-tabs"]
167          self._indicator = self.dom_nodes["ae-tab-indicator"]
168  
169          props = self._props = _defaults | properties
170  
171          # annoying font_size property
172          if isinstance(props["font_size"], str) and props["font_size"].isdigit():
173              props["font_size"] = int(props["font_size"])
174  
175          self._prev = props["active_tab_index"] or 0
176  
177          props_to_init = {
178              "tab_titles": props["tab_titles"],
179              "spacing_above": props["spacing_above"],
180              "spacing_below": props["spacing_below"],
181              "foreground": props["foreground"],
182              "background": props["background"],
183              "active_background": props["active_background"],
184              "role": props["role"],
185              "visible": props["visible"],
186          }
187  
188          self.init_components(**props_to_init)
189          self._ro = ResizeObserver(lambda *e: self._set_indicator())
190  
191      def _on_show(self, **event_args):
192          if self._shown:
193              return
194          self._ro.observe(self._dom_node)
195  
196      def _on_hide(self, **event_args):
197          self._shown = False
198          self._ro.disconnect()
199  
200      def _raise_tab_click(self, sender, tab_index, **event_args):
201          self._set_indicator(tab_index)
202          self.raise_event(
203              "tab_click",
204              tab_index=tab_index,
205              tab_title=sender.text,
206              tab_component=sender,
207          )
208  
209      def _set_indicator(self, tab_index=None):
210          animate = tab_index is not None
211          tab_index = tab_index if tab_index is not None else self._prev
212  
213          for i, node in enumerate(self._link_nodes):
214              node.classList.toggle("ae-tab-active", i == tab_index)
215  
216          left, right = (0, 90) if tab_index <= self._prev else (90, 0)
217          if animate:
218              self._indicator.style.transition = (
219                  f"left 300ms ease-out {left}ms, right 300ms ease-out {right}ms"
220              )
221          else:
222              self._indicator.style.transition = ""
223  
224          self._prev = tab_index
225          link_node = self._link_nodes[tab_index]
226          left = link_node.offsetLeft
227          right = (
228              self._tabs_node.offsetWidth - link_node.offsetLeft - link_node.offsetWidth
229          )
230          self._indicator.style.left = f"{left}px"
231          self._indicator.style.right = f"{right}px"
232  
233      @property
234      def tab_titles(self):
235          return self._props["tab_titles"]
236  
237      @tab_titles.setter
238      def tab_titles(self, tab_list):
239          self._props["tab_titles"] = tab_list or []
240          self.clear()
241          self._link_nodes = []
242          for i, text in enumerate(tab_list):
243              link = _Link(
244                  text=text,
245                  spacing_above="none",
246                  spacing_below="none",
247                  align=self.align,
248                  bold=self.bold,
249                  italic=self.italic,
250                  font=self.font,
251                  font_size=self.font_size,
252              )
253              link.set_event_handler("click", partial(self._raise_tab_click, tab_index=i))
254              self._link_nodes.append(anvil.js.get_dom_node(link))
255              self.add_component(link)
256          self._set_indicator()
257  
258      @property
259      def active_tab_index(self):
260          return self._prev
261  
262      @active_tab_index.setter
263      def active_tab_index(self, index):
264          self._set_indicator(index or 0)
265  
266      @property
267      def active_background(self):
268          return self._props["active_background"]
269  
270      @active_background.setter
271      def active_background(self, value):
272          self._props["active_background"] = value
273          self._dom_node.style.setProperty("--active-bg", value and _get_rgb(value))
274  
275      @property
276      def foreground(self):
277          return self._props["foreground"]
278  
279      @foreground.setter
280      def foreground(self, value):
281          self._props["foreground"] = value
282          self._dom_node.style.setProperty("--color", _get_rgb(value))
283  
284      @property
285      def background(self):
286          return self._props["background"]
287  
288      @background.setter
289      def background(self, value):
290          self._props["background"] = value
291          self._dom_node.style.setProperty("--background", value and _get_color(value))
292  
293      align = _apply_to_links("align")
294      bold = _apply_to_links("bold")
295      font = _apply_to_links("font")
296      font_size = _apply_to_links("font_size")
297      italic = _apply_to_links("italic")
298  
299      role = _HtmlPanel.role
300      visible = _HtmlPanel.visible
301      tag = _HtmlPanel.tag
302      spacing_above = _spacing_property("above")
303      spacing_below = _spacing_property("below")