__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.js import get_dom_node 9 from anvil.js.window import setTimeout 10 11 from ...popover import pop 12 from ..Option import Option 13 from ._anvil_designer import DropDownTemplate 14 15 __version__ = "3.1.0" 16 17 18 class DropDown(DropDownTemplate): 19 def __init__(self, **properties): 20 self._dom_node = get_dom_node(self) 21 self._dom_node.style.height = "100%" 22 self._props = properties 23 self.dd_node = self.dom_nodes["ae-ms-dd"] 24 self.options_node = self.dom_nodes["ae-ms-options"] 25 self.init_components(**properties) 26 self.select_all_btn.role = "ae-ms-select-btn" 27 self.deselect_all_btn.role = "ae-ms-select-btn" 28 self.filter_box.role = "ae-ms-filter" 29 self.dd_node.addEventListener("keydown", self._on_keydown) 30 self._no_options = Option() 31 self._no_options.visible = False 32 self.options_panel.add_component(self._no_options) 33 34 @property 35 def options(self): 36 return self._props.get("options", []) 37 38 @options.setter 39 def options(self, val): 40 val = val or [] 41 self._props["options"] = val 42 self.options_panel.clear() 43 for opt in val: 44 self.options_panel.add_component(opt) 45 opt.add_event_handler("click", self._on_option_clicked) 46 47 self.options_panel.add_component(self._no_options) 48 self._no_options.visible = False 49 50 @property 51 def enable_filtering(self): 52 return self._props.get("enable_filtering", False) 53 54 @enable_filtering.setter 55 def enable_filtering(self, val): 56 self._props["enable_filtering"] = val 57 self.filter_box.visible = val 58 59 @property 60 def enable_select_all(self): 61 return self._props.get("enable_select_all", False) 62 63 @enable_select_all.setter 64 def enable_select_all(self, val): 65 self._props["enable_select_all"] = val 66 self.select_all_flow.visible = self.multiple and val 67 68 @property 69 def multiple(self): 70 return self._props.get("multiple", False) 71 72 @multiple.setter 73 def multiple(self, val): 74 self._props["multiple"] = val 75 if val: 76 self.enable_select_all = self.enable_select_all 77 78 def _close(self): 79 pop(self.popper, "hide") 80 81 def _on_filter_show(self, **event_args): 82 # because of weird way we are hacking the show events in popovers 83 setTimeout(self.filter_box.focus) 84 85 def _on_filter_hide(self, **event_args): 86 self.filter_box.text = "" 87 for option in self.options: 88 option.visible = True 89 90 self._no_options.visible = False 91 92 def _on_filter_change(self, **event_args): 93 term = self.filter_box.text or "" 94 term = term.lower() 95 num_results = 0 96 for option in self.options: 97 if option.is_divider: 98 option.visible = not term 99 else: 100 visible = term in option.key.lower() or term in option.subtext.lower() 101 option.visible = visible 102 num_results += visible 103 104 active_idx = self._get_active_idx() 105 if active_idx != -1: 106 self.options[active_idx].active = False 107 108 no_options = not num_results and term 109 self._no_options.visible = no_options 110 if no_options: 111 self._no_options.label.text = f"No results matched {term!r}" 112 113 if not term: 114 return 115 116 first_idx = self._get_next_idx(-1) 117 if first_idx != -1: 118 self.options[first_idx].active = True 119 120 def _on_filter_enter(self, **e): 121 active_idx = self._get_active_idx() 122 if active_idx != -1: 123 self.options[active_idx].click() 124 125 def _on_select_all(self, **event_args): 126 for option in self.options: 127 option.selected = True 128 self._on_focus() 129 self.raise_event("change") 130 131 def _on_deselect_all(self, **event_args): 132 for option in self.options: 133 option.selected = False 134 self._on_focus() 135 self.raise_event("change") 136 137 def _on_show(self, **event_args): 138 if self.enable_filtering: 139 return 140 141 def focus(): 142 self.options_node.tabIndex = 0 143 self.options_node.focus() 144 self.options_node.tabIndex = -1 145 146 setTimeout(focus) 147 148 def _on_focus(self, *args, **kws): 149 setTimeout(self.filter_box.focus) 150 151 def _get_active_idx(self): 152 for i, opt in enumerate(self.options): 153 if opt.is_divider: 154 continue 155 if opt.active: 156 return i 157 return -1 158 159 def _get_next_idx(self, active_idx, dir=1, pred=None): 160 num_options = len(self.options) 161 if active_idx == -1 and dir == -1: 162 active_idx = 0 163 164 nxt_idx = (active_idx + dir) % num_options 165 166 for i in range(nxt_idx, dir * num_options + nxt_idx, dir): 167 idx = i % num_options 168 nxt = self.options[idx] 169 if nxt.is_divider: 170 continue 171 if not nxt.visible or nxt.disabled: 172 continue 173 if pred is None: 174 return idx 175 cond = pred(nxt) 176 if cond: 177 return idx 178 179 return -1 180 181 def _on_keydown(self, e): 182 key = e.key 183 if key == "Tab": 184 key = "ArrowUp" if e.shiftKey else "ArrowDown" 185 186 is_input = e.target.nodeName == "INPUT" 187 188 if key == "Escape": 189 self._close() 190 get_dom_node(self.popper).firstElementChild.focus() 191 # TODO focus the popper 192 193 elif key == " ": 194 if not is_input: 195 e.preventDefault() 196 197 elif key == "Enter": 198 e.preventDefault() 199 self._on_filter_enter() 200 201 elif key == "ArrowDown" or key == "ArrowUp": 202 e.preventDefault() 203 204 dir = 1 if key == "ArrowDown" else -1 205 active_idx = self._get_active_idx() 206 if active_idx != -1: 207 self.options[active_idx].active = False 208 next_idx = self._get_next_idx(active_idx, dir) 209 if next_idx != -1: 210 self.options[next_idx].active = True 211 212 elif key.isalpha(): 213 if is_input: 214 return 215 key = key.lower() 216 active_idx = self._get_active_idx() 217 next_idx = self._get_next_idx( 218 active_idx, dir=1, pred=lambda opt: opt.key.lower().startswith(key) 219 ) 220 if next_idx != -1: 221 if active_idx != -1: 222 self.options[active_idx].active = False 223 self.options[next_idx].active = True 224 225 def _on_option_clicked(self, sender, **e): 226 multiple = self.multiple 227 for opt in self.options: 228 if not multiple: 229 opt.selected = opt is sender 230 opt.active = opt is sender and not sender.disabled 231 232 if sender.disabled: 233 return 234 235 self.raise_event("change") 236 237 if not self.multiple: 238 self._close()