__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 anvil import HtmlPanel as _HtmlPanel 9 from anvil.js import get_dom_node as _get_dom_node 10 from anvil.js.window import navigator 11 12 from ..Chip import Chip 13 from ..utils._component_helpers import _get_color, _html_injector, _spacing_property 14 from ._anvil_designer import ChipsInputTemplate 15 16 __version__ = "3.1.0" 17 18 _primary = _get_color(None) 19 20 _html_injector.css( 21 """ 22 .ae-chips-input input { 23 box-shadow: none !important; 24 border: none !important; 25 padding: 7px 0 !important; 26 margin-bottom: 0 !important; 27 flex: 1; 28 min-width: 50px; 29 } 30 .ae-chips-input{ 31 display: flex; 32 flex-wrap: wrap; 33 gap: 8px; 34 border-bottom: 1px solid; 35 align-items: center; 36 padding-bottom: 4px; 37 } 38 39 """ 40 ) 41 42 _defaults = { 43 "primary_placeholder": "", 44 "secondary_placeholder": "", 45 "chips": [], 46 "visible": True, 47 "spacing_above": "small", 48 "spacing_below": "small", 49 } 50 51 52 is_android = "Android" in navigator.userAgent 53 54 55 class ChipsInput(ChipsInputTemplate): 56 def __init__(self, **properties): 57 self._chips = [] 58 self._deleting = False 59 self._placeholder = self._placeholder_0 = self._placeholder_1 = "" 60 61 input_node = _get_dom_node(self.chip_input) 62 input_node.addEventListener("keydown", self._chip_input_key_down) 63 64 dom_node = self._dom_node = _get_dom_node(self) 65 dom_node.classList.add("ae-chips-input") 66 self.temp_chip.remove_from_parent() 67 68 properties = _defaults | properties 69 self.init_components(**properties) 70 71 @property 72 def primary_placeholder(self): 73 return self._placeholder_0 74 75 @primary_placeholder.setter 76 def primary_placeholder(self, value): 77 self._placeholder_0 = value 78 if not len(self._chips): 79 self.chip_input.placeholder = value 80 self._placeholder = value 81 82 @property 83 def secondary_placeholder(self): 84 return self._placeholder_1 85 86 @secondary_placeholder.setter 87 def secondary_placeholder(self, value): 88 self._placeholder_1 = value 89 if len(self._chips): 90 self.chip_input.placeholder = value 91 self._placeholder = value 92 93 @property 94 def chips(self): 95 # make sure chips is immutable 96 return tuple(self._chips) 97 98 @chips.setter 99 def chips(self, value): 100 value = value or [] 101 if list(value) == self._chips: 102 return 103 self._chips = [] 104 self.clear(slot="chips") 105 106 seen = set() 107 for chip_text in value: 108 if chip_text in seen or not chip_text: 109 continue 110 chip = Chip(text=chip_text, spacing_above="none", spacing_below="none") 111 self.add_component(chip, slot="chips") 112 chip.set_event_handler("close_click", self._chip_close_click) 113 self._chips.append(chip_text) 114 seen.add(chip_text) 115 116 self._reset_placeholder() 117 118 visible = _HtmlPanel.visible 119 tag = _HtmlPanel.tag 120 spacing_above = _spacing_property("above") 121 spacing_below = _spacing_property("below") 122 123 ###### PRIVATE METHODS AND PROPS ###### 124 125 @property 126 def _last_chip(self): 127 """throws an error if we have no chips, when used must be wrapped in try/except""" 128 components = self.get_components() 129 components.remove(self.chip_input) 130 return components[-1] 131 132 def _reset_placeholder(self): 133 new_placeholder = self._placeholder_1 if self._chips else self._placeholder_0 134 if new_placeholder != self._placeholder: 135 self.chip_input.placeholder = self._placeholder = new_placeholder 136 137 def _reset_deleting(self, val): 138 try: 139 self._deleting = val 140 self._set_focus(self._last_chip, val) 141 except IndexError: 142 pass 143 144 def _chip_input_pressed_enter(self, **event_args): 145 """This method is called when the user presses Enter in this text box""" 146 chip_text = self.chip_input.text 147 if chip_text and chip_text not in self._chips: 148 chip = Chip(text=chip_text, spacing_above="none", spacing_below="none") 149 self.add_component(chip, slot="chips") 150 chip.set_event_handler("close_click", self._chip_close_click) 151 self._chips.append(chip_text) 152 self.chip_input.text = "" 153 self._reset_placeholder() 154 155 self.raise_event("chips_changed") 156 self.raise_event("chip_added", chip=chip_text) 157 158 def _chip_input_key_down(self, js_event): 159 """This method is called when on the user key down in this text box""" 160 try: 161 if not self.chip_input.text and js_event.key == "Backspace": 162 if not self._deleting: 163 self._reset_deleting(True) 164 return 165 _last_chip = self._last_chip 166 self._chips.pop() 167 chip_text = _last_chip.text 168 _last_chip.remove_from_parent() 169 self._reset_placeholder() 170 171 self.raise_event("chips_changed") 172 self.raise_event("chip_removed", chip=chip_text) 173 174 self._set_focus(self._last_chip, True) 175 176 elif self._deleting: 177 self._reset_deleting(False) 178 if js_event.key == "Tab": 179 js_event.preventDefault() 180 181 elif is_android and js_event.key == "Tab": 182 js_event.preventDefault() 183 self._chip_input_pressed_enter() 184 185 except IndexError: 186 pass 187 188 def _chip_input_focus(self, **event_args): 189 """This method is called when the TextBox gets focus""" 190 self._dom_node.style.borderBottom = f"1px solid {_primary}" 191 192 def _chip_input_lost_focus(self, **event_args): 193 """This method is called when the TextBox loses focus""" 194 self._dom_node.style.borderBottom = "1px solid" 195 self._reset_deleting(False) 196 197 def _chip_close_click(self, sender, **event_args): 198 chips = self._chips 199 chip_text = sender.text 200 chips.remove(chip_text) 201 sender.remove_from_parent() 202 self.raise_event("chips_changed") 203 self.raise_event("chip_removed", chip=chip_text) 204 205 @staticmethod 206 def _set_focus(chip, val): 207 chip.background = _primary if val else "" 208 chip.foreground = "#fff" if val else ""