/ client_code / augment.py
augment.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 as _partial 9 from functools import wraps as _wraps 10 11 import anvil as _anvil 12 from anvil import Component as _Component 13 from anvil import DataGrid as _DataGrid 14 from anvil import js as _js 15 from anvil.js.window import WeakMap as _WeakMap 16 from anvil.js.window import jQuery as _S 17 18 from .utils._deprecated import deprecated 19 20 __version__ = "3.1.0" 21 22 __all__ = ["add_event", "add_event_handler", "set_event_handler", "trigger"] 23 24 _Callable = type(lambda: None) 25 _prefix = "x-augmented-" 26 27 # because Skulpt doesn't have a WeakMap and js WeakMap preserves identity of methods 28 _wm = _WeakMap() 29 30 31 def _weak_cache_event(fn): 32 @_wraps(fn) 33 def wrapper(instance, event: str): 34 event_map = _wm.get(instance) 35 if event_map is None: 36 _wm.set(instance, {}) 37 event_map = _wm.get(instance) 38 if event not in event_map: 39 event_map[event] = fn(instance, event) 40 return event_map[event] 41 42 return wrapper 43 44 45 # use cache so we don't add the same event to the component multiple times 46 # we only need to add the event once and use anvil architecture to raise the event 47 @_weak_cache_event 48 def add_event(component: _Component, event: str) -> None: 49 """component: (instantiated) anvil component 50 event: str - any jquery event string 51 """ 52 if not isinstance(event, str): 53 raise TypeError("event must be type str and not " + type(event)) 54 55 _add_event(component, event) 56 57 if _has_native_event(component, event): 58 return 59 60 def handler(e): 61 handleObj = e.get("handleObj") 62 if handleObj is None: 63 type = e.type 64 else: 65 type = handleObj.get("origType") or e.type 66 event_args = {"event_type": type, "original_event": e} 67 if event.startswith("key"): 68 event_args |= { 69 "key": e.key, 70 "key_code": e.keyCode, 71 "shift_key": e.shiftKey, 72 "alt_key": e.altKey, 73 "meta_key": e.metaKey, 74 "ctrl_key": e.ctrlKey, 75 } 76 if component.raise_event(event, **event_args): 77 e.preventDefault() 78 79 js_event_name = "mouseenter mouseleave" if event == "hover" else event 80 _get_jquery_for_component(component).on(js_event_name, handler) 81 82 83 def set_event_handler(component: _Component, event: str, func: _Callable) -> None: 84 """uses anvil's set_event_handler for any jquery event""" 85 add_event(component, event) 86 component.set_event_handler(event, func) 87 88 89 def add_event_handler(component: _Component, event: str, func: _Callable) -> None: 90 """uses anvil's add_event_handler for any jquery event""" 91 add_event(component, event) 92 component.add_event_handler(event, func) 93 94 95 def remove_event_handler(component: _Component, event: str, func: _Callable) -> None: 96 """equivalent to anvil's remove_event_handler""" 97 component.remove_event_handler(event, func) 98 99 100 @deprecated( 101 "trigger('writeback') is no longer supported\nYou can now trigger a writeback using component.raise_event('x-anvil-write-back-<property>')" 102 ) 103 def _trigger_writeback(self): 104 return 105 106 107 def trigger(self: _Component, event: str): 108 """trigger an event on a component, self is an anvil component, event is a str or a dictionary 109 if event is a dictionary it should include an 'event' key e.g. {'event': 'keypress', 'which': 13} 110 """ 111 if event == "writeback": 112 return _trigger_writeback(self) 113 if isinstance(event, dict): 114 event = _S.Event(event["event"], event) 115 event = "mouseenter mouseleave" if event == "hover" else event 116 _get_jquery_for_component(self).trigger(event) 117 118 119 _Component.trigger = trigger 120 121 122 def _get_jquery_for_component(component): 123 if isinstance(component, _anvil.Button): 124 return _S(_js.get_dom_node(component).firstElementChild) 125 elif isinstance(component, _anvil.FileLoader): 126 return _S(_js.get_dom_node(component)).find("form") 127 elif isinstance(component, (_anvil.CheckBox, _anvil.RadioButton)): 128 return _S(_js.get_dom_node(component)).find("input") 129 else: 130 return _S(_js.get_dom_node(component)) 131 132 133 def _noop(**e): 134 pass 135 136 137 _remap = set() 138 _native = set() 139 140 141 def _add_event(self, event_name): 142 key = (type(self), event_name) 143 if key in _native or key in _remap: 144 return 145 try: 146 self.add_event_handler(event_name, _noop) 147 except ValueError: 148 _remap.add(key) 149 else: 150 _native.add(key) 151 self.remove_event_handler(event_name, _noop) 152 153 154 @_weak_cache_event 155 def _get_handler(fn, event): 156 @_wraps(fn) 157 def wrap_handler(*args, **kws): 158 kws["event_name"] = event 159 return fn(*args, **kws) 160 161 return wrap_handler 162 163 164 def wrap_event_method(method): 165 old_method = getattr(_Component, method) 166 167 @_wraps(old_method) 168 def wrapped(self, event_name, *args, **kws): 169 key = (type(self), event_name) 170 if key not in _remap: 171 return old_method(self, event_name, *args, **kws) 172 173 remapped = _prefix + event_name 174 175 if len(args) == 1 and callable(args[0]): 176 args = [_get_handler(args[0], event_name)] 177 178 return old_method(self, remapped, *args, **kws) 179 180 setattr(_Component, method, wrapped) 181 182 183 for method in [ 184 "raise_event", 185 "add_event_handler", 186 "set_event_handler", 187 "remove_event_handler", 188 ]: 189 wrap_event_method(method) 190 191 192 def _has_native_event(self, event): 193 key = (type(self), event) 194 return key in _native 195 196 197 old_data_grid_event_handler = _DataGrid.set_event_handler 198 199 200 def datagrid_set_event_handler(self, event, handler): 201 if event == "pagination_click": 202 _set_pagination_handlers(self, handler) 203 else: 204 old_data_grid_event_handler(self, event, handler) 205 206 207 _DataGrid.set_event_handler = datagrid_set_event_handler 208 209 210 def _prevent_disabled(js_event): 211 if js_event.currentTarget.classList.contains("disabled"): 212 js_event.stopPropagation() 213 214 215 def _wrap_js_event(handler): 216 def wrapper(e): 217 handler() 218 219 return wrapper 220 221 222 def _set_pagination_handlers(data_grid, handler): 223 grid_dom = _js.get_dom_node(data_grid) 224 for name in ["first", "last", "previous", "next"]: 225 btn = grid_dom.querySelector(f".{name}-page") 226 # use True so that we capture this event before the anvil click event 227 btn.addEventListener("click", _prevent_disabled, True) 228 btn.addEventListener( 229 "click", 230 _wrap_js_event( 231 _partial( 232 handler, 233 sender=data_grid, 234 button=name, 235 event_name="pagination_click", 236 ) 237 ), 238 ) 239 # note we don't tidy this up - we should probably call removeEventListener 240 # but this will be called from code and is unlikely that the user will call this function twice 241 242 243 if __name__ == "__main__": 244 _ = _anvil.ColumnPanel() 245 _.set_event_handler( 246 "show", 247 lambda **e: _anvil.Notification( 248 "oops AnvilAugment is a dependency", timeout=None 249 ).show(), 250 ) 251 _anvil.open_form(_) 252 253 _ = None