/ client_code / ChipsInput / __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 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 ""