/ 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