__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")