Source code for domonic.events

"""
domonic.events
==============

DOM-style event classes and dispatch machinery for domonic.

This module provides ``EventTarget`` plus a broad set of web-platform-flavoured
event classes so DOM nodes, windows, animations, and helper objects can share a
common event model.
"""

from __future__ import annotations

import inspect
import time
from typing import Any, Callable, ClassVar

from domonic.constants.keyboard import Code, Key, KeyCode, KeyLocation, normalize_code, normalize_key


[docs] class EventListener: """Interface-style base for listener objects with ``handleEvent()``.""" def handleEvent(self, event: "Event") -> Any: raise NotImplementedError
[docs] class EventListenerOptions(dict): """Dictionary-like helper for DOM listener options. Supports the common ``capture``, ``once``, ``passive``, and ``signal`` fields accepted by ``addEventListener()``. """ def __init__( self, capture: bool = False, once: bool = False, passive: bool = False, signal: Any = None, ) -> None: super().__init__(capture=capture, once=once, passive=passive, signal=signal)
[docs] class EventTarget: """ DOM-style event target base class. Extend ``EventTarget`` to give an object support for ``addEventListener()``, ``removeEventListener()``, and ``dispatchEvent()`` with DOM-like capture, target, and bubble semantics where appropriate. """ def __init__(self, *args, **kwargs) -> None: # Initialize a dictionary to store event listeners. self.listeners: dict[str, list[Callable[..., Any]]] = {} self._listener_options: dict[str, list[dict[str, Any]]] = {}
[docs] def hasEventListener(self, eventType: str) -> bool: """ Check if an event listener for the given event type exists. Args: eventType (str): The type of the event. Returns: bool: True if listeners for the event type exist, otherwise False. """ return bool(self.listeners.get(eventType))
@staticmethod def _normalize_listener_options( options: bool | dict[str, Any] | None = None, **kwargs: Any ) -> dict[str, Any]: normalized: dict[str, Any] = {"capture": False, "once": False, "passive": False, "signal": None} if isinstance(options, bool): normalized["capture"] = options elif isinstance(options, dict): normalized["capture"] = bool(options.get("capture", False)) normalized["once"] = bool(options.get("once", False)) normalized["passive"] = bool(options.get("passive", False)) normalized["signal"] = options.get("signal") if "use_capture" in kwargs: normalized["capture"] = bool(kwargs["use_capture"]) if "capture" in kwargs: normalized["capture"] = bool(kwargs["capture"]) if "once" in kwargs: normalized["once"] = bool(kwargs["once"]) if "passive" in kwargs: normalized["passive"] = bool(kwargs["passive"]) if "signal" in kwargs: normalized["signal"] = kwargs["signal"] return normalized def _get_event_path(self, target: Any) -> list[Any]: path: list[Any] = [] current = target seen: set[int] = set() while current is not None and id(current) not in seen: path.append(current) seen.add(id(current)) if hasattr(current, "parentNode") and getattr(current, "parentNode", None) is not None: current = current.parentNode continue owner_document = getattr(current, "ownerDocument", None) if owner_document is not None and owner_document is not current: current = owner_document continue default_view = getattr(current, "defaultView", None) if default_view is not None and default_view is not current: current = default_view continue break return path def _invoke_listeners(self, current_target: Any, event: "Event", capture: bool) -> None: event_type = event.type listeners = list(getattr(current_target, "_listener_options", {}).get(event_type, [])) event.currentTarget = current_target event.srcElement = event.target event.eventPhase = Event.AT_TARGET if current_target is event.target else ( Event.CAPTURING_PHASE if capture else Event.BUBBLING_PHASE ) for listener in listeners: if listener["capture"] != capture: continue callback = listener["callback"] event._in_passive_listener = listener["passive"] if hasattr(callback, "handleEvent"): result = callback.handleEvent(event) else: result = callback(event) if result is False: event.preventDefault() event._in_passive_listener = False if listener["once"]: current_target.removeEventListener(event_type, callback, listener["capture"]) if event._immediate_propagation_stopped: return if capture is False: handler = getattr(current_target, f"on{event_type}", None) if callable(handler): result = handler(event) if result is False: event.preventDefault()
[docs] def addEventListener( self, eventType: str, callback: Callable[..., Any], options: bool | dict[str, Any] | None = None, *args, **kwargs, ) -> None: """ Add an event listener for the given event type. Args: eventType (str): The type of the event to listen for. callback (Callable): The callback function to be executed when the event occurs. """ if eventType not in self.listeners: self.listeners[eventType] = [] self._listener_options[eventType] = [] listener_options = self._normalize_listener_options(options, **kwargs) signal = listener_options.get("signal") if signal is not None: if getattr(signal, "aborted", False): return for listener in self._listener_options[eventType]: if listener["callback"] is callback and listener["capture"] == listener_options["capture"]: return self.listeners[eventType].append(callback) self._listener_options[eventType].append({"callback": callback, **listener_options}) if signal is not None and hasattr(signal, "addEventListener"): def _remove_on_abort(event: Any, target=self, ev_type=eventType, cb=callback, capture=listener_options["capture"]): target.removeEventListener(ev_type, cb, {"capture": capture}) signal.addEventListener("abort", _remove_on_abort, {"once": True})
[docs] def removeEventListener( self, eventType: str, callback: Callable[..., Any], options: bool | dict[str, Any] | None = None ) -> None: """ Remove an event listener for the given event type. Args: eventType (str): The type of the event. callback (Callable): The callback function to be removed. """ if eventType not in self.listeners: return capture = self._normalize_listener_options(options)["capture"] callbacks = self.listeners[eventType] listeners = self._listener_options.get(eventType, []) for index, listener in enumerate(listeners): if listener["callback"] is callback and listener["capture"] == capture: listeners.pop(index) try: callbacks.remove(callback) except ValueError: pass break if not listeners: self._listener_options.pop(eventType, None) self.listeners.pop(eventType, None)
[docs] def dispatchEvent(self, event: Any) -> bool: """ Dispatch the specified event to all registered event listeners. Args: event (Dict): The event object to be dispatched. Returns: bool: True if the event was successfully dispatched, otherwise False. """ if not isinstance(event, Event): event = Event(event.get("type", ""), event) event.target = self event.currentTarget = self event.srcElement = self event.cancelBubble = False event._propagation_stopped = False event._immediate_propagation_stopped = False event._in_passive_listener = False path = self._get_event_path(self) event._path = path capture_targets = list(reversed(path[1:])) bubble_targets = path[1:] if event.bubbles else [] try: for target in capture_targets: self._invoke_listeners(target, event, capture=True) if event._propagation_stopped: return not event.defaultPrevented self._invoke_listeners(self, event, capture=True) if not event._immediate_propagation_stopped: self._invoke_listeners(self, event, capture=False) if event._propagation_stopped: return not event.defaultPrevented for target in bubble_targets: self._invoke_listeners(target, event, capture=False) if event._propagation_stopped: break return not event.defaultPrevented finally: event.eventPhase = Event.NONE event.currentTarget = None event._in_passive_listener = False
[docs] async def dispatchEventAsync(self, event: Any) -> bool: """ Dispatch the specified event asynchronously to all registered event listeners. Args: event (Dict): The event object to be dispatched. Returns: bool: True if the event was successfully dispatched, otherwise False. **Usage:** To dispatch an event asynchronously, use the ``await`` keyword when calling this method. **Example:** .. code-block:: python event_data = {"message": "Hello, world!"} async_event = {"type": "async_event", "data": event_data} await target.dispatchEventAsync(async_event) """ if not isinstance(event, Event): event = Event(event.get("type", ""), event) event.target = self event.currentTarget = self event.srcElement = self event.cancelBubble = False event._propagation_stopped = False event._immediate_propagation_stopped = False event._in_passive_listener = False async def call_listener(callback: Callable[..., Any], current_target: Any, capture: bool) -> Any: event.currentTarget = current_target event.srcElement = event.target event.eventPhase = Event.AT_TARGET if current_target is event.target else ( Event.CAPTURING_PHASE if capture else Event.BUBBLING_PHASE ) if hasattr(callback, "handleEvent"): result = callback.handleEvent(event) else: result = callback(event) if inspect.isawaitable(result): result = await result return result async def invoke(current_target: Any, capture: bool) -> None: listeners = list(getattr(current_target, "_listener_options", {}).get(event.type, [])) for listener in listeners: if listener["capture"] != capture: continue event._in_passive_listener = listener["passive"] result = await call_listener(listener["callback"], current_target, capture) if result is False: event.preventDefault() event._in_passive_listener = False if listener["once"]: current_target.removeEventListener(event.type, listener["callback"], listener["capture"]) if event._immediate_propagation_stopped: return if capture is False: handler = getattr(current_target, f"on{event.type}", None) if callable(handler): result = handler(event) if inspect.isawaitable(result): result = await result if result is False: event.preventDefault() path = self._get_event_path(self) event._path = path try: for target in reversed(path[1:]): await invoke(target, True) if event._propagation_stopped: return not event.defaultPrevented await invoke(self, True) if not event._immediate_propagation_stopped: await invoke(self, False) if event._propagation_stopped or not event.bubbles: return not event.defaultPrevented for target in path[1:]: await invoke(target, False) if event._propagation_stopped: break return not event.defaultPrevented finally: event.eventPhase = Event.NONE event.currentTarget = None event._in_passive_listener = False
EventDispatcher = EventTarget #: legacy alias
[docs] class Event: """Event class represents events and their properties.""" # Constants for event types EMPTIED: str = "emptied" #: ABORT: str = "abort" #: AFTERPRINT: str = "afterprint" #: BEFOREPRINT: str = "beforeprint" #: BEFOREUNLOAD: str = "beforeunload" #: CANPLAY: str = "canplay" #: CANPLAYTHROUGH: str = "canplaythrough" #: CHANGE: str = "change" #: DURATIONCHANGE: str = "durationchange" #: ENDED: str = "ended" #: ERROR: str = "error" #: FULLSCREENCHANGE: str = "fullscreenchange" #: FULLSCREENERROR: str = "fullscreenerror" #: INPUT: str = "input" #: INVALID: str = "invalid" #: LOAD: str = "load" #: LOADEDDATA: str = "loadeddata" #: LOADEDMETADATA: str = "loadedmetadata" #: MESSAGE: str = "message" #: OFFLINE: str = "offline" #: ONLINE: str = "online" #: OPEN: str = "open" #: PAUSE: str = "pause" #: PLAY: str = "play" #: PLAYING: str = "playing" #: PROGRESS: str = "progress" #: RATECHANGE: str = "ratechange" #: READYSTATECHANGE: str = "readystatechange" #: RESIZE: str = "resize" #: RESET: str = "reset" #: SCROLL: str = "scroll" #: SEARCH: str = "search" #: SEEKED: str = "seeked" #: SEEKING: str = "seeking" #: SELECT: str = "select" #: SHOW: str = "show" #: STALLED: str = "stalled" #: SUBMIT: str = "submit" #: SUSPEND: str = "suspend" #: TOGGLE: str = "toggle" #: UNLOAD: str = "unload" #: VOLUMECHANGE: str = "volumechange" #: WAITING: str = "waiting" #: NONE: int = 0 CAPTURING_PHASE: int = 1 AT_TARGET: int = 2 BUBBLING_PHASE: int = 3 def __str__(self) -> str: return self.type + ":" + str(self.timeStamp) def __init__(self, _type: str = "", options: dict[str, Any] | None = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.type: str = _type self.bubbles: bool = options.get("bubbles", False) self.cancelable: bool = options.get("cancelable", False) self._cancelBubble: bool = False self.composed: bool = options.get("composed", False) self.currentTarget: object = options.get("currentTarget", None) self.defaultPrevented: bool = options.get("defaultPrevented", False) self.eventPhase: int = options.get("eventPhase", Event.NONE) self.explicitOriginalTarget: object = options.get("explicitOriginalTarget", None) self.isTrusted: bool = options.get("isTrusted", False) self.originalTarget: object = options.get("originalTarget", None) self._returnValue: bool = True self.srcElement: object = options.get("srcElement", None) self.target: object = options.get("target", None) self.timeStamp: float = time.time_ns() / 1_000_000 self._propagation_stopped: bool = False self._immediate_propagation_stopped: bool = False self._in_passive_listener: bool = False self._path: list[Any] | None = None self.cancelBubble = options.get("cancelBubble", False) self.returnValue = options.get("returnValue", True) @property def cancelBubble(self) -> bool: return self._cancelBubble @cancelBubble.setter def cancelBubble(self, value: bool) -> None: self._cancelBubble = bool(value) if self._cancelBubble: self._propagation_stopped = True @property def returnValue(self) -> bool: return self._returnValue @returnValue.setter def returnValue(self, value: bool) -> None: self._returnValue = bool(value) self.defaultPrevented = not self._returnValue
[docs] def composedPath(self): """ Returns a list of the event's path, from the root to the target. """ if self._path is not None: return list(self._path) path = [] current_target = self.target while current_target is not None: path.append(current_target) if hasattr(current_target, "parentNode"): current_target = current_target.parentNode else: break # Include the window as the final item in the path. if path: last = path[-1] default_view = getattr(last, "defaultView", None) if default_view is not None and default_view not in path: path.append(default_view) return path
[docs] def initEvent( self, _type: str | None = None, bubbles: bool = True, cancelable: bool = True, *args, **kwargs ) -> "Event": """Initialize the event.""" self.type = _type or self.type self.bubbles = bubbles self.cancelable = cancelable self.cancelBubble = False self.defaultPrevented = False self.currentTarget = None self.eventPhase = Event.NONE self._propagation_stopped = False self._immediate_propagation_stopped = False self._path = None return self
[docs] def stopPropagation(self): """[prevents further propagation of the current event in the capturing and bubbling phases]""" self.cancelBubble = True # Set the cancelBubble flag to stop propagation self._propagation_stopped = True
[docs] def msConvertURL(self, url): """ Converts the provided URL to a format recognized by Internet Explorer. Args: url (str): The URL to be converted. Returns: str: The converted URL. """ if url.startswith('http'): # Example conversion for HTTP/HTTPS URLs in Internet Explorer return f'javascript:window.open("{url}");' else: # Handle other URL formats as needed return url
[docs] def preventDefault(self) -> None: """ Prevents the default action associated with the event, if cancelable. This method is used to signal that the event should not trigger its default behavior. Returns: None """ if self.cancelable and not self._in_passive_listener: self.defaultPrevented = True self.returnValue = False
[docs] def stopImmediatePropagation(self) -> None: """ Prevents further propagation of the current event and immediately stops other event listeners in the same phase from being invoked. This method is used to stop the event's propagation immediately and ensure that no other listeners in the same phase are invoked. Returns: None """ self.cancelBubble = True self._propagation_stopped = True self._immediate_propagation_stopped = True
[docs] class AbortSignal(EventTarget): """Signal object used to communicate cancellation.""" def __init__(self) -> None: super().__init__() self.aborted: bool = False self.reason: Any = None self.onabort = None def _signal_abort(self, reason: Any = None) -> None: if self.aborted: return self.aborted = True self.reason = reason self.dispatchEvent(Event(Event.ABORT)) def throwIfAborted(self) -> None: if self.aborted: raise RuntimeError(self.reason if self.reason is not None else "Signal already aborted")
[docs] class AbortController: """Controller used to abort work associated with an AbortSignal.""" def __init__(self) -> None: self.signal = AbortSignal() def abort(self, reason: Any = None) -> None: self.signal._signal_abort(reason)
[docs] class UIEvent(Event): """UIEvent is a specialized event class for user interface events.""" def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: """ Initialize a UIEvent. Args: _type (str): The type of the UIEvent. options (dict, optional): Additional options for the event. Defaults to None. Returns: None """ options = options or kwargs # If options is None, use kwargs self.canBubble = options.get("canBubble", None) self.cancelable = options.get("cancelable", None) self.detail = options.get("detail", None) self.view = options.get("view", None) self.layerX = options.get("layerX", None) self.layerY = options.get("layerY", None) self.sourceCapabilities = options.get("sourceCapabilities", None) super().__init__(_type, options, *args, **kwargs)
[docs] def initUIEvent(self, _type: str, canBubble: bool, cancelable: bool, view, detail) -> "UIEvent": """ Initialize a UIEvent with specific parameters. Args: _type (str): The type of the UIEvent. canBubble (bool): Specifies whether the event should bubble. cancelable (bool): Specifies whether the event is cancelable. view: The associated view or window. detail: Additional event-specific detail. Returns: UIEvent: The initialized UIEvent object. """ self.initEvent(_type, canBubble, cancelable) self.canBubble = canBubble self.view = view self.detail = detail return self
[docs] class MouseEvent(UIEvent): """mouse events""" CLICK: str = "click" #: CONTEXTMENU: str = "contextmenu" #: DBLCLICK: str = "dblclick" #: MOUSEDOWN: str = "mousedown" #: MOUSEENTER: str = "mouseenter" #: MOUSELEAVE: str = "mouseleave" #: MOUSEMOVE: str = "mousemove" #: MOUSEOVER: str = "mouseover" #: MOUSEOUT: str = "mouseout" #: MOUSEUP: str = "mouseup" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs self.canBubble = options.get("canBubble", None) self.cancelable = options.get("cancelable", None) self.x = options.get("x", 0) self.y = options.get("y", 0) self._clientX = options.get("clientX", 0) self._clientY = options.get("clientY", 0) self._altKey: bool = options.get("altKey", False) self._ctrlKey: bool = options.get("ctrlKey", False) self._shiftKey: bool = options.get("shiftKey", False) self._metaKey: bool = options.get("metaKey", False) self._button = None self._buttons = [] super().__init__(_type, options, *args, **kwargs) def initMouseEvent( self, _type: str = None, canBubble: bool = True, cancelable: bool = True, view=None, detail=None, screenX: float = 0, screenY: float = 0, clientX: float = 0, clientY: float = 0, ctrlKey: bool = False, altKey: bool = False, shiftKey: bool = False, metaKey: bool = False, button=None, relatedTarget=None, from_json={}, *args, **kwargs ) -> "MouseEvent": # print('initMouseEvent') self.initEvent(_type or self.type, canBubble, cancelable) self.canBubble = canBubble self.view = view self.detail = detail self.screenX = screenX self.screenY = screenY self.x = clientX self.y = clientY self._clientX = clientX self._clientY = clientY self._ctrlKey = ctrlKey self._altKey = altKey self._shiftKey = shiftKey self._metaKey = metaKey self._button = button self._buttons = [] if button is None else [button] self.relatedTarget = relatedTarget # TODO - parse from_json - so can relay return self @property def clientX(self): return self._clientX @property def clientY(self): return self._clientY @property def altKey(self): return self._altKey @property def ctrlKey(self): return self._ctrlKey @property def shiftKey(self): return self._shiftKey @property def metaKey(self): return self._metaKey @property def button(self): return self._button @property def buttons(self): return self._buttons @property def which(self): return self._button # MOUSE_EVENT
[docs] def getModifierState(self, keyArg: str): """Returns an array containing target ranges that will be affected by the insertion/deletion""" lookup = { "Alt": self.altKey, "Control": self.ctrlKey, "Meta": self.metaKey, "Shift": self.shiftKey, } return lookup.get(keyArg, False)
# MovementX Returns the horizontal coordinate of the mouse pointer relative to the position of the last mousemove event MouseEvent # MovementY Returns the vertical coordinate of the mouse pointer relative to the position of the last mousemove event MouseEvent # offsetX Returns the horizontal coordinate of the mouse pointer relative to the position of the edge of the target element MouseEvent # offsetY Returns the vertical coordinate of the mouse pointer relative to the position of the edge of the target element MouseEvent # pageX Returns the horizontal coordinate of the mouse pointer, relative to the document, when the mouse event was triggered MouseEvent # pageY Returns the vertical coordinate of the mouse pointer, relative to the document, when the mouse event was triggered MouseEvent # region MouseEvent # relatedTarget Returns the element related to the element that triggered the mouse event MouseEvent, FocusEvent def _infer_char_code(key: str) -> int: if len(key) == 1: return ord(key) return 0 def _infer_legacy_keycode(key: str) -> int: inferred = KeyCode.from_key(key) return inferred if inferred is not None else 0 def _infer_code_from_key_and_location(key: str, location: int) -> str: key_to_code = { Key.ENTER: Code.NUMPAD_ENTER if location == KeyLocation.NUMPAD else Code.ENTER, Key.TAB: Code.TAB, Key.SPACE: Code.SPACE, Key.ESCAPE: Code.ESCAPE, Key.BACKSPACE: Code.BACKSPACE, Key.DELETE: Code.DELETE, Key.INSERT: Code.INSERT, Key.HOME: Code.HOME, Key.END: Code.END, Key.PAGE_UP: Code.PAGE_UP, Key.PAGE_DOWN: Code.PAGE_DOWN, Key.ARROW_LEFT: Code.ARROW_LEFT, Key.ARROW_RIGHT: Code.ARROW_RIGHT, Key.ARROW_UP: Code.ARROW_UP, Key.ARROW_DOWN: Code.ARROW_DOWN, Key.SHIFT: Code.SHIFT_RIGHT if location == KeyLocation.RIGHT else Code.SHIFT_LEFT, Key.CONTROL: Code.CONTROL_RIGHT if location == KeyLocation.RIGHT else Code.CONTROL_LEFT, Key.ALT: Code.ALT_RIGHT if location == KeyLocation.RIGHT else Code.ALT_LEFT, Key.META: Code.META_RIGHT if location == KeyLocation.RIGHT else Code.META_LEFT, Key.CAPS_LOCK: Code.CAPS_LOCK, Key.NUM_LOCK: Code.NUM_LOCK, Key.SCROLL_LOCK: Code.SCROLL_LOCK, Key.CONTEXT_MENU: Code.CONTEXT_MENU, } if key in key_to_code: return key_to_code[key] normalized_code = normalize_code(key) if normalized_code: return normalized_code return ""
[docs] class KeyboardEvent(UIEvent): """keyboard events""" KEYDOWN: str = "keydown" #: KEYPRESS: str = "keypress" #: KEYUP: str = "keyup" #: DOM_KEY_LOCATION_STANDARD: int = KeyLocation.STANDARD #: DOM_KEY_LOCATION_LEFT: int = KeyLocation.LEFT #: DOM_KEY_LOCATION_RIGHT: int = KeyLocation.RIGHT #: DOM_KEY_LOCATION_NUMPAD: int = KeyLocation.NUMPAD #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.canBubble = options.get("canBubble", None) self.cancelable = options.get("cancelable", None) self._altKey: bool = options.get("altKey", False) self._ctrlKey: bool = options.get("ctrlKey", False) self._shiftKey: bool = options.get("shiftKey", False) self._metaKey: bool = options.get("metaKey", False) self.location = options.get("location", self.DOM_KEY_LOCATION_STANDARD) self.repeat = bool(options.get("repeat", False)) self.isComposing = bool(options.get("isComposing", False)) self._modifier_states = { "Alt": self._altKey, "Control": self._ctrlKey, "Meta": self._metaKey, "Shift": self._shiftKey, "CapsLock": bool(options.get("capsLock", False)), "NumLock": bool(options.get("numLock", False)), "ScrollLock": bool(options.get("scrollLock", False)), "AltGraph": bool(options.get("altGraph", False)), } raw_key = options.get("key", "") raw_code = options.get("code", "") raw_key_code = options.get("keyCode", None) self.key = normalize_key(raw_key) self.code = normalize_code(raw_code) or KeyCode.to_code(raw_key_code) or _infer_code_from_key_and_location( self.key, self.location ) self.keyCode = int(raw_key_code) if raw_key_code not in (None, "") else _infer_legacy_keycode(self.key) self.charCode = int(options.get("charCode", _infer_char_code(self.key))) super().__init__(_type, options, *args, **kwargs) def initKeyboardEvent( self, typeArg: str, canBubbleArg: bool, cancelableArg: bool, viewArg, charArg, keyArg, locationArg, modifiersListArg, repeat, ) -> "KeyboardEvent": self.initEvent(typeArg, canBubbleArg, cancelableArg) self.canBubbleArg = canBubbleArg self.cancelableArg = cancelableArg self.view = viewArg self.viewArg = viewArg self.charCode = charArg self.key = normalize_key(keyArg) self.location = locationArg self.locationArg = locationArg self.modifiersListArg = modifiersListArg modifiers = {modifier.strip().lower() for modifier in str(modifiersListArg).split() if modifier} self._altKey = "alt" in modifiers self._ctrlKey = "control" in modifiers or "ctrl" in modifiers self._metaKey = "meta" in modifiers self._shiftKey = "shift" in modifiers self._modifier_states.update( { "Alt": self._altKey, "Control": self._ctrlKey, "Meta": self._metaKey, "Shift": self._shiftKey, } ) self.code = _infer_code_from_key_and_location(self.key, self.location) self.keyCode = _infer_legacy_keycode(self.key) self.repeat = repeat return self @property def altKey(self): return self._altKey @property def ctrlKey(self): return self._ctrlKey @property def shiftKey(self): return self._shiftKey @property def metaKey(self): return self._metaKey @property def unicode(self): return self.key def getModifierState(self, keyArg: str) -> bool: return self._modifier_states.get(keyArg, False)
# @property # def keyCode(self): # return self.keyCode # @property # def charCode(self): # return self.charCode # @property # def code(self): # return self.code # @property # def key(self): # return self.key # def isComposing(self, *args, **kwargs): # pass # KeyboardEvent # isComposing Returns whether the state of the event is composing or not InputEvent, KeyboardEvent # repeat Returns whether a key is being hold down repeatedly, or not KeyboardEvent # location Returns the location of a key on the keyboard or device KeyboardEvent
[docs] class CompositionEvent(UIEvent): """CompositionEvent""" START: str = "compositionstart" END: str = "compositionend" UPDATE: str = "compositionupdate" def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.data = options.get("data", None) self.locale = options.get("locale", None) super().__init__(_type, options, *args, **kwargs)
[docs] class FocusEvent(UIEvent): """FocusEvent""" BLUR: str = "blur" #: FOCUS: str = "focus" #: FOCUSIN: str = "focusin" #: FOCUSOUT: str = "focusout" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.relatedTarget = options.get("relatedTarget", None) super().__init__(_type, options, *args, **kwargs)
[docs] class TouchEvent(UIEvent): """TouchEvent""" TOUCHCANCEL: str = "touchcancel" #: TOUCHEND: str = "touchend" #: TOUCHMOVE: str = "touchmove" #: TOUCHSTART: str = "touchstart" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.shiftKey = options.get("shiftKey", False) self.altKey = options.get("altKey", False) self.changedTouches = options.get("changedTouches", []) self.ctrlKey = options.get("ctrlKey", False) self.metaKey = options.get("metaKey", False) self.shiftKey = options.get("shiftKey", False) self.targetTouches = options.get("targetTouches", []) self.touches = options.get("touches", []) super().__init__(_type, options, *args, **kwargs)
[docs] class WheelEvent(UIEvent): """WheelEvent""" MOUSEWHEEL: str = "mousewheel" # DEPRECATED - USE WHEEL #: WHEEL: str = "wheel" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.deltaX = options.get("deltaX", 0) self.deltaY = options.get("deltaY", 0) self.deltaZ = options.get("deltaZ", 0) self.deltaMode = options.get("deltaMode", 0) super().__init__(_type, options, *args, **kwargs)
[docs] class AnimationEvent(Event): """AnimationEvent""" ANIMATIONEND: str = "animationend" #: ANIMATIONITERATION: str = "animationiteration" #: ANIMATIONSTART: str = "animationstart" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.animationName = options.get("animationName", None) """ Returns the name of the animation """ self.elapsedTime = options.get("elapsedTime", None) """ Returns the number of seconds an animation has been running """ self.pseudoElement = options.get("pseudoElement", None) """ Returns the name of the pseudo-element of the animation """ super().__init__(_type, options, *args, **kwargs)
[docs] class ClipboardEvent(Event): """ClipboardEvent""" COPY: str = "copy" #: CUT: str = "cut" #: PASTE: str = "paste" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.clipboardData = options.get("clipboardData", None) """ Returns an object containing the data affected by the clipboard operation """ super().__init__(_type, options, *args, **kwargs)
[docs] class ErrorEvent(Event): """ErrorEvent""" ERROR: str = "error" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.message: str = options.get("message", "") self.filename = options.get("filename", None) self.lineno = options.get("lineno", 0) self.colno = options.get("colno", 0) self.error = options.get("error", None) super().__init__(_type, options, *args, **kwargs)
[docs] class CloseEvent(Event): """CloseEvent""" CLOSE: str = "close" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.code = options.get("code", 0) self.reason = options.get("reason", "") self.wasClean = bool(options.get("wasClean", False)) super().__init__(_type, options, *args, **kwargs)
[docs] class SubmitEvent(Event): """SubmitEvent""" SUBMIT: str = "submit" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.submitter = options.get("submitter", None) super().__init__(_type, options, *args, **kwargs)
[docs] class PointerEvent(MouseEvent): """PointerEvent""" POINTER: str = "pointer" #: POINTERCANCEL: str = "pointercancel" #: POINTERDOWN: str = "pointerdown" #: POINTERENTER: str = "pointerenter" #: POINTERLEAVE: str = "pointerleave" #: POINTERMOVE: str = "pointermove" #: POINTEROUT: str = "pointerout" #: POINTEROVER: str = "pointerover" #: POINTERUP: str = "pointerup" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.pointerId: float = options.get("pointerId", 0) self.width: float = options.get("width", 0) self.height: float = options.get("height", 0) self.pressure: float = options.get("pressure", 0) self.tangentialPressure: float = options.get("tangentialPressure", 0) self.tiltX: float = options.get("tiltX", 0) self.tiltY: float = options.get("tiltY", 0) self.twist: float = options.get("twist", 0) self.pointerType: str = options.get("pointerType", "") self.isPrimary: bool = options.get("isPrimary", False) self._coalescedEvents = list(options.get("coalescedEvents", [])) self._predictedEvents = list(options.get("predictedEvents", [])) super().__init__(_type, options, *args, **kwargs) def getCoalescedEvents(self): return list(self._coalescedEvents) def getPredictedEvents(self): return list(self._predictedEvents)
# def getCurrentPoint(self, element): # """ Returns the current coordinates of the specified element relative to the viewport. """ # pass # def getIntermediatePoints(self, element): # """ Returns the coordinates of all the intermediate points of the pointer along the path of the pointer. """ # pass
[docs] class BeforeUnloadEvent(Event): BEFOREUNLOAD: ClassVar[str] = Event.BEFOREUNLOAD #: """ BeforeUnloadEvent """ def __init__(self, _type: str, options: dict[str, Any] | None = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self._beforeunload_return_value = options.get("returnValue", "") super().__init__(_type, options, *args, **kwargs) @property def returnValue(self) -> Any: return self._beforeunload_return_value @returnValue.setter def returnValue(self, value: Any) -> None: self._beforeunload_return_value = "" if value is None else value self.defaultPrevented = value not in ("", True, False, None)
[docs] class SVGEvent(Event): """SVGEvent""" ABORT: str = "abort" #: LOAD: str = "load" #: LOADEDDATA: str = "loadeddata" #: LOADEDMETADATA: str = "loadedmetadata" #: LOADSTART: str = "loadstart" #: PROGRESS: str = "progress" #: SCROLL: str = "scroll" #: UNLOAD: str = "unload" #: ERROR: str = "error" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs super().__init__(_type, options, *args, **kwargs)
# def initEvent(self, eventTypeArg, canBubbleArg, cancelableArg): # pass
[docs] class TimerEvent(Event): TIMER: str = "timer" #: TIMER_COMPLETE: str = "timercomplete" #: """ TimerEvent """ def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs super().__init__(_type, options, *args, **kwargs)
# def initTimerEvent(self, type, bubbles, cancelable, detail): # """ initTimerEvent() """ # pass
[docs] class DragEvent(Event): """DragEvent""" DRAG: str = "drag" #: END: str = "dragend" #: ENTER: str = "dragenter" #: EXIT: str = "dragexit" #: LEAVE: str = "dragleave" #: OVER: str = "dragover" #: START: str = "dragstart" #: DROP: str = "drop" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.dataTransfer = options.get("dataTransfer", None) """ Returns the data that is dragged/dropped """ super().__init__(_type, options, *args, **kwargs)
[docs] class HashChangeEvent(Event): """HashChangeEvent""" CHANGE: str = "hashchange" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.newURL = options.get("newURL", "") self.oldURL = options.get("oldURL", "") super().__init__(_type, options, *args, **kwargs)
[docs] class InputEvent(UIEvent): """InputEvent""" CHANGE: str = "change" #: SELECT: str = "select" #: INPUT: str = "input" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.data = options.get("data", None) """ Returns the inserted characters """ self.dataTransfer = options.get("dataTransfer", None) """ Returns an object containing information about the inserted/deleted data """ self.inputType = options.get("inputType", None) """ Returns the type of the change (i.e "inserting" or "deleting") """ self.isComposing = options.get("isComposing", None) """ Returns whether the state of the event is composing or not """ super().__init__(_type, options, *args, **kwargs)
[docs] def getTargetRanges(self): """Returns an array containing target ranges that will be affected by the insertion/deletion""" if hasattr(self, "_targetRanges"): return list(self._targetRanges) if isinstance(getattr(self, "target", None), object): target = self.target if hasattr(target, "getSelection"): selection = target.getSelection() if selection is not None: return [selection.getRangeAt(i) for i in range(selection.rangeCount)] return []
[docs] class PageTransitionEvent(Event): """PageTransitionEvent""" PAGEHIDE: str = "pagehide" #: PAGESHOW: str = "pageshow" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.persisted = options.get("persisted", None) """ Returns whether the webpage was cached by the browser """ super().__init__(_type, options, *args, **kwargs)
[docs] class PopStateEvent(Event): """PopStateEvent""" POPSTATE: str = "popstate" #: # ONPOPSTATE = "onpopstate" #:?? def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.state = options.get("state", None) """ Returns an object containing a copy of the history entries """ super().__init__(_type, options, *args, **kwargs)
[docs] class StorageEvent(Event): """StorageEvent""" STORAGE: str = "storage" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.key = options.get("key", None) """ Returns the key of the changed storage item """ self.newValue = options.get("newValue", None) """ Returns the new value of the changed storage item """ self.oldValue = options.get("oldValue", None) """ Returns the old value of the changed storage item """ self.storageArea = options.get("storageArea", None) """ Returns an object representing the affected storage object """ self.url = options.get("url", None) """ Returns the URL of the changed item's document """ super().__init__(_type, options, *args, **kwargs)
[docs] class TransitionEvent(Event): """TransitionEvent""" TRANSITIONEND: str = "transitionend" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.propertyName = options.get("propertyName", None) """ Returns the name of the transition""" self.elapsedTime = options.get("elapsedTime", None) """ Returns the number of seconds a transition has been running """ self.pseudoElement = options.get("pseudoElement", None) """ Returns the name of the pseudo-element of the transition """ super().__init__(_type, options, *args, **kwargs)
[docs] class ProgressEvent(Event): """ProgressEvent""" LOADSTART: str = "loadstart" #: PROGRESS: str = "progress" #: ABORT: str = "abort" #: ERROR: str = "error" #: LOAD: str = "load" #: LOADED: str = "loaded" #: LOADEND: str = "loadend" #: TIMEOUT: str = "timeout" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.lengthComputable: bool = options.get("lengthComputable", False) self.loaded: int = options.get("loaded", 0) self.total: int = options.get("total", 0) super().__init__(_type, options, *args, **kwargs)
[docs] class CustomEvent(Event): """CustomEvent""" def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.detail = options.get("detail", None) super().__init__(_type, options, *args, **kwargs) def initCustomEvent( self, _type: str, bubbles: bool = True, cancelable: bool = True, detail: Any = None ) -> "CustomEvent": self.initEvent(_type, bubbles, cancelable) self.detail = detail return self
[docs] class GamePadEvent(Event): """GamePadEvent""" START: str = "gamepadconnected" #: STOP: str = "gamepaddisconnected" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.gamepad = options.get("gamepad", None) super().__init__(_type, options, *args, **kwargs)
# TODO - tests and service worker API
[docs] class FetchEvent(Event): """FetchEvent""" FETCH: str = "fetch" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.clientId = options.get("clientId", None) """ Returns the client ID of the fetch request """ self.request = options.get("request", None) """ Returns the request object """ self._responded_with = options.get("response", None) self._pending_promises: list[Any] = [] super().__init__(_type, options, *args, **kwargs) @property def isReload(self): if self.request is None: return False return getattr(self.request, "url", None) == getattr(self.request, "referrer", object()) @property def replacesClientId(self): if self.request is None: return False return self.clientId != getattr(self.request, "clientId", None) @property def resultingClientId(self): if self.request is None: return self.clientId return self.clientId if self.replacesClientId else getattr(self.request, "clientId", None)
[docs] def respondWith(self, response): """Returns a promise that resolves to the response object""" self._responded_with = response return response
[docs] def waitUntil(self, promise): """Returns a promise that resolves when the response is available""" self._pending_promises.append(promise) return promise
[docs] class ExtendableEvent(Event): """ExtendableEvent""" # CAPTURING_PHASE = 1 # AT_TARGET = 2 # BUBBLING_PHASE = 3 def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.extendable = options.get("extendable", True) """ Returns whether the event is extendable or not """ self._pending_promises: list[Any] = [] """ Returns the time stamp of the event """ super().__init__(_type, options, *args, **kwargs) def waitUntil(self, promise: Any): self._pending_promises.append(promise) return promise
[docs] class SyncEvent(ExtendableEvent): """SyncEvent""" SYNC: str = "sync" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.tag = options.get("tag", None) """ Returns the tag of the sync event """ self.lastChance = options.get("lastChance", None) """ Returns whether the sync event is the last chance or not """ super().__init__(_type, options, *args, **kwargs)
[docs] class SecurityPolicyViolationEvent(ExtendableEvent): """SecurityPolicyViolationEvent""" SECURITY_POLICY_VIOLATION: str = "securitypolicyviolation" #: def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.blockedURI = options.get("blockedURI", None) """ Returns the blocked URI """ self.violatedDirective = options.get("violatedDirective", None) """ Returns the violated directive """ self.originalPolicy = options.get("originalPolicy", None) """ Returns the original policy """ self.isFrameAncestor = options.get("isFrameAncestor", None) """ Returns whether the frame is an ancestor of the frame that violated the policy """ self.isMainFrame = options.get("isMainFrame", None) """ Returns whether the frame is the main frame """ self.frame = options.get("frame", None) """ Returns the frame that violated the policy """ super().__init__(_type, options, *args, **kwargs)
[docs] class DOMContentLoadedEvent(Event): """DOMContentLoadedEvent""" DOMCONTENTLOADED: str = "DOMContentLoaded" #: # LOAD: str = "load" #: already on event # BEFOREUNLOAD: str = "beforeunload" #: already on event # UNLOAD: str = "unload" #: already on event # readystatechange = "readystatechange" #: ?? where does this one belong. Have added it to event def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.document = options.get("document", None) """ Returns the document that was loaded """ super().__init__(_type, options, *args, **kwargs)
# class InstallEvent() # class DeviceMotionEvent(Event): # """ DeviceMotionEvent """ # def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: # self.acceleration = None # """ Returns the acceleration of the device """ # self.accelerationIncludingGravity = None # """ Returns the acceleration of the device, including gravity """ # self.rotationRate = None # """ Returns the rotation rate of the device """ # self.interval = None # """ Returns the time interval between the previous and the current event """ # super().__init__(_type, options, *args, **kwargs) # class DeviceOrientationEvent(Event): # """ DeviceOrientationEvent """ # def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: # self.absolute = None # """ Returns true if the orientation is absolute """ # self.alpha = None # """ Returns the orientation of the device in degrees, relative to the Earth's coordinate system """ # self.beta = None # """ Returns the orientation of the device in degrees, relative to the Earth's coordinate system """ # self.gamma = None # """ Returns the orientation of the device in degrees, relative to the Earth's coordinate system """ # self.interval = None # """ Returns the time interval between the previous and the current event """ # super().__init__(_type, options, *args, **kwargs) # class DeviceLightEvent(Event): # """ DeviceLightEvent """ # def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: # self.value = None # """ Returns the value of the ambient light sensor """ # super().__init__(_type, options, *args, **kwargs) # class DeviceProximityEvent(Event): # """ DeviceProximityEvent """ # def __init__(self, _type: str, options: dict = None, *args, **kwargs) -> None: # self.value = None # """ Returns the value of the proximity sensor """ # self.min = None # """ Returns the minimum value of the proximity sensor """ # self.max = None # """ Returns the maximum value of the proximity sensor """ # super().__init__(_type, options, *args, **kwargs)
[docs] class TweenEvent(Event): """TweenEvent""" START: str = "onStart" #: STOP: str = "onStop" #: RESET: str = "onReset" #: PAUSE: str = "onPause" #: UNPAUSE: str = "onUnPause" #: UPDATE_START: str = "onUpdateStart" #: UPDATE_END: str = "onUpdateEnd" #: COMPLETE: str = "onComplete" #: TIMER: str = "onTimer" #: _source = None @property def source(self): return self._source @source.setter def source(self, source): self._source = source def __init__(self, _type, source=None, bubbles=False, cancelable=False): # super.__init__(self, type, bubbles, cancelable) super().__init__(_type) # TODO - self.source = source
[docs] class PromiseRejectionEvent(Event): # TODO - put with the promise? """PromiseRejectionEvent""" UNHANDLED: str = "unhandledrejection" #: HANDLED: str = "rejectionhandled" #: def __init__(self, _type, options=None, *args, **kwargs): options = options or kwargs self.promise = options.get("promise", None) """ Returns the promise that was rejected """ self.reason = options.get("reason", None) """ Returns the reason of the rejection """ self.isRejected = options.get("isRejected", None) """ Returns whether the promise was rejected or not """ super().__init__(_type, options, *args, **kwargs)
[docs] class MessageEvent(Event): """MessageEvent""" MESSAGE: str = "message" #: CONNECT: str = "connect" #: DISCONNECT: str = "disconnect" #: def __init__(self, _type, options: dict = None, *args, **kwargs) -> None: options = options or kwargs # if options is none use kwargs self.data = options.get("data", None) """ Returns the data of the message """ self.origin = options.get("origin", None) """ Returns the origin of the message """ self.lastEventId = options.get("lastEventId", None) """ Returns the last event id of the message """ self.source = options.get("source", None) """ Returns the source of the message """ self.ports = options.get("ports", []) """ Returns the ports of the message """ super().__init__(_type, options, *args, **kwargs)
class GlobalEventHandler: _handler_names = ( "onabort", "onblur", "oncancel", "oncanplay", "oncanplaythrough", "onchange", "onclick", "onclose", "oncontextmenu", "oncuechange", "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragexit", "ondragleave", "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied", "onended", "onerror", "onfocus", "ongotpointercapture", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", "onload", "onloadeddata", "onloadedmetadata", "onloadend", "onloadstart", "onlostpointercapture", "onmouseenter", "onmouseleave", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onpause", "onplay", "onplaying", "onpointercancel", "onpointerdown", "onpointerenter", "onpointerleave", "onpointermove", "onpointerout", "onpointerover", "onpointerup", "onprogress", "onratechange", "onreset", "onresize", "onscroll", "onseeked", "onseeking", "onselect", "onselectionchange", "onselectstart", "onshow", "onstalled", "onsubmit", "onsuspend", "ontimeupdate", "onvolumechange", "onwaiting", "onwheel", "onanimationcancel", "onanimationend", "onanimationiteration", "onauxclick", "onformdata", "onmousedown", "ontouchcancel", "ontouchstart", "ontransitioncancel", "ontransitionend", ) class WindowEventHandler: _handler_names = ( "onabort", "onafterprint", "onbeforeprint", "onbeforeunload", "onblur", "oncanplay", "oncanplaythrough", "onchange", "onclick", "oncontextmenu", "oncopy", "oncuechange", "oncut", "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied", "onended", "onerror", "onfocus", "onhashchange", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", "onload", "onloadeddata", "onloadedmetadata", "onloadstart", "onmessage", "onmousedown", "onmouseenter", "onmouseleave", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel", "onoffline", "ononline", "onpagehide", "onpageshow", "onpaste", "onpopstate", "onresize", "onscroll", "onstorage", "onsubmit", "onunload", ) def __init__(self, window): super().__init__() self.window = window def _make_default_event_handler(name: str): def handler(self, event): self._last_event = event callback = getattr(self, f"_{name}_callback", None) if callable(callback): return callback(event) return event handler.__name__ = name return handler def _install_default_event_handlers(*classes) -> None: for cls in classes: for name in getattr(cls, "_handler_names", ()): setattr(cls, name, _make_default_event_handler(name)) _install_default_event_handlers(GlobalEventHandler, WindowEventHandler)