about summary refs log tree commit diff stats
path: root/src/html
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-08-19 10:02:54 +0200
committerbptato <nincsnevem662@gmail.com>2023-08-20 15:39:07 +0200
commit65ad7f9fc8b69140c050006fbe9ea1644bc283d8 (patch)
treeb2e8b654c31ba6fe66992014c3eefeb0b731b3e2 /src/html
parent01dfb3abe8d3a282a127aafd34e55fff1ff9d2df (diff)
downloadchawan-65ad7f9fc8b69140c050006fbe9ea1644bc283d8.tar.gz
javascript: update Events, misc fixes & additions
Events: just implement the interfaces, no events are triggered yet.

JS changes:
* add LegacyUnforgeable
* make consts enumerable
* fix crash in isInstanceOf
* fix destructor warnings
* refactor registerType

As a result, peakmem is now 1G+ on 1.6.14. It stays ~750M on 2.0.0. Hmm.
Well, better upgrade to 2.0.0 I guess.
Diffstat (limited to 'src/html')
-rw-r--r--src/html/dom.nim13
-rw-r--r--src/html/event.nim225
2 files changed, 236 insertions, 2 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 8afb68e6..89fd7748 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -83,7 +83,7 @@ type
   Location = ref object
     window: Window
 
-  Window* = ref object
+  Window* = ref object of EventTarget
     attrs*: WindowAttributes
     console* {.jsget.}: console
     navigator* {.jsget.}: Navigator
@@ -1784,6 +1784,17 @@ func formmethod*(element: Element): FormMethod =
 
   return FORM_METHOD_GET
 
+# Forward declaration hack
+isDefaultPassive = func (eventTarget: EventTarget): bool =
+  if eventTarget of Window:
+    return true
+  if not (eventTarget of Node):
+    return false
+  let node = Node(eventTarget)
+  return EventTarget(node.document) == eventTarget or
+    EventTarget(node.document.html) == eventTarget or
+    EventTarget(node.document.body) == eventTarget
+
 proc parseColor(element: Element, s: string): RGBAColor =
   let cval = parseComponentValue(newStringStream(s))
   #TODO TODO TODO return element style
diff --git a/src/html/event.nim b/src/html/event.nim
index d862b40c..433e9597 100644
--- a/src/html/event.nim
+++ b/src/html/event.nim
@@ -1,4 +1,9 @@
+import math
+import times
+
+import bindings/quickjs
 import js/javascript
+import utils/opt
 
 type
   EventPhase = enum
@@ -7,25 +12,243 @@ type
     AT_TARGET = 2u16
     BUBBLING_PHASE = 3u16
 
-  Event* = ref object
+  EventFlag = enum
+    FLAG_STOP_PROPAGATION
+    FLAG_STOP_IMMEDIATE_PROPAGATION
+    FLAG_CANCELED
+    FLAG_IN_PASSIVE_LISTENER
+    FLAG_COMPOSED
+    FLAG_INITIALIZED
+    FLAG_DISPATCH
+
+  Event* = ref object of RootObj
     ctype {.jsget: "type".}: string
     target {.jsget.}: EventTarget
     currentTarget {.jsget.}: EventTarget
     eventPhase {.jsget.}: uint16
     bubbles {.jsget.}: bool
     cancelable {.jsget.}: bool
+    #TODO DOMHighResTimeStamp?
+    timeStamp {.jsget.}: float64
+    flags: set[EventFlag]
+    isTrusted {.jsufget.}: bool
+
+  CustomEvent* = ref object of Event
+    ctx: JSContext #TODO get rid of this
+    detail {.jsget.}: JSValue
 
   EventTarget* = ref object of RootObj
+    eventListeners*: seq[EventListener]
 
   EventHandler* = JSValue
 
+  EventListenerCallback = proc (event: Event): Err[JSError]
+
+  EventListener = ref object
+    ctype: string
+    callback: EventListenerCallback
+    capture: bool
+    passive: Opt[bool]
+    once: bool
+    #TODO AbortSignal
+    removed: bool
+
 jsDestructor(Event)
+jsDestructor(CustomEvent)
 jsDestructor(EventTarget)
 
+# Forward declaration hack
+var isDefaultPassive*: proc (eventTarget: EventTarget): bool
+
+# Event
+proc innerEventCreationSteps(event: Event, ctx: JSContext,
+    eventInitDict: JSValue) =
+  event.flags = {FLAG_INITIALIZED}
+  #TODO this is probably incorrect?
+  # I think it measures the time since the first fork. not sure though
+  event.timeStamp = round(cpuTime())
+  if not JS_IsUndefined(eventInitDict):
+    template set(name: static string, value: var bool) =
+      let prop = JS_GetPropertyStr(ctx, eventInitDict, name)
+      let jsVal = fromJS[bool](ctx, prop)
+      if jsVal.isSome:
+        value = jsVal.get
+    set "bubbles", event.bubbles
+    set "cancelable", event.cancelable
+    var composed: bool
+    set "composed", composed
+    if composed:
+      event.flags.incl(FLAG_COMPOSED)
+
+#TODO eventInitDict type
+proc newEvent(ctx: JSContext, ctype: string, eventInitDict = JS_UNDEFINED):
+    Result[Event, JSError] {.jsctor.} =
+  if not JS_IsUndefined(eventInitDict) and not JS_IsObject(eventInitDict):
+    return err(newTypeError("eventInitDict must be an object"))
+  let event = Event()
+  event.innerEventCreationSteps(ctx, eventInitDict)
+  event.ctype = ctype
+  return ok(event)
+
+proc initialize(this: Event, ctype: string, bubbles, cancelable: bool) =
+  this.flags.incl(FLAG_INITIALIZED)
+  this.isTrusted = false
+  this.target = nil
+  this.ctype = ctype
+  this.bubbles = bubbles
+  this.cancelable = cancelable
+
+proc initEvent(this: Event, ctype: string, bubbles, cancelable: bool)
+    {.jsfunc.} =
+  if FLAG_DISPATCH notin this.flags:
+    this.initialize(ctype, bubbles, cancelable)
+
 func srcElement(this: Event): EventTarget {.jsfget.} =
   return this.target
 
+#TODO shadow DOM etc.
+func composedPath(this: Event): seq[EventTarget] {.jsfunc.} =
+  if this.currentTarget == nil:
+    return @[]
+  return @[this.currentTarget]
+
+proc stopPropagation(this: Event) {.jsfunc.} =
+  this.flags.incl(FLAG_STOP_PROPAGATION)
+
+func cancelBubble(this: Event): bool {.jsfget.} =
+  return FLAG_STOP_PROPAGATION in this.flags
+
+proc cancelBubble(this: Event, cancel: bool) {.jsfset.} =
+  if cancel:
+    this.stopPropagation()
+
+proc stopImmediatePropagation(this: Event) {.jsfunc.} =
+  this.flags.incl({FLAG_STOP_PROPAGATION, FLAG_STOP_IMMEDIATE_PROPAGATION})
+
+proc setCanceledFlag(this: Event) =
+  if this.cancelable and FLAG_IN_PASSIVE_LISTENER notin this.flags:
+    this.flags.incl(FLAG_CANCELED)
+
+proc returnValue(this: Event): bool {.jsfget.} =
+  return FLAG_CANCELED notin this.flags
+
+proc returnValue(this: Event, value: bool) {.jsfset.} =
+  if not value:
+    this.setCanceledFlag()
+
+proc preventDefault(this: Event) {.jsfunc.} =
+  this.flags.incl(FLAG_CANCELED)
+
+func defaultPrevented(this: Event): bool {.jsfget.} =
+  return FLAG_CANCELED in this.flags
+
+func composed(this: Event): bool {.jsfget.} =
+  return FLAG_COMPOSED in this.flags
+
+# CustomEvent
+proc newCustomEvent(ctx: JSContext, ctype: string,
+    eventInitDict = JS_UNDEFINED): Result[CustomEvent, JSError] {.jsctor.} =
+  if not JS_IsUndefined(eventInitDict) and not JS_IsObject(eventInitDict):
+    return err(newTypeError("eventInitDict must be an object"))
+  let event = CustomEvent()
+  event.innerEventCreationSteps(ctx, eventInitDict)
+  event.detail = JS_GetPropertyStr(ctx, eventInitDict, "detail")
+  event.ctx = ctx
+  event.ctype = ctype
+  return ok(event)
+
+proc finalize(this: CustomEvent) {.jsfin.} =
+  JS_FreeValue(this.ctx, this.detail)
+
+proc initCustomEvent(ctx: JSContext, this: CustomEvent, ctype: string,
+    bubbles, cancelable: bool, detail: JSValue) {.jsfunc.} =
+  if FLAG_DISPATCH notin this.flags:
+    this.initialize(ctype, bubbles, cancelable)
+    this.ctx = ctx
+    this.detail = detail
+
+# EventTarget
+proc newEventTarget(): EventTarget {.jsctor.} =
+  return EventTarget()
+
+proc defaultPassiveValue(ctype: string, eventTarget: EventTarget): bool =
+  if ctype in ["touchstart", "touchmove", "wheel", "mousewheel"]:
+    return true
+  return eventTarget.isDefaultPassive()
+
+proc findEventListener(eventTarget: EventTarget, ctype: string,
+    callback: EventListenerCallback, capture: bool): int =
+  for i in 0 ..< eventTarget.eventListeners.len:
+    let it = eventTarget.eventListeners[i]
+    if it.ctype == ctype and it.callback == callback and it.capture == capture:
+      return i
+  return -1
+
+# shared
+proc addAnEventListener(eventTarget: EventTarget, listener: EventListener) =
+  #TODO signals
+  if listener.callback == nil:
+    return
+  if listener.passive.isNone:
+    listener.passive = opt(defaultPassiveValue(listener.ctype, eventTarget))
+  if eventTarget.findEventListener(listener.ctype, listener.callback,
+      listener.capture) == -1: # dedup
+    eventTarget.eventListeners.add(listener)
+  #TODO signals
+
+proc removeAnEventListener(eventTarget: EventTarget, i: int) =
+  eventTarget.eventListeners[i].removed = true
+  eventTarget.eventListeners.delete(i)
+
+proc flatten(ctx: JSContext, options: JSValue): bool =
+  if JS_IsBool(options):
+    return fromJS[bool](ctx, options).get(false)
+  if JS_IsObject(options):
+    let x = JS_GetPropertyStr(ctx, options, "capture")
+    return fromJS[bool](ctx, x).get(false)
+  return false
+
+proc flattenMore(ctx: JSContext, options: JSValue):
+    tuple[
+      capture: bool,
+      once: bool,
+      passive: Opt[bool]
+      #TODO signals
+    ] =
+  if JS_IsUndefined(options):
+    return
+  let capture = flatten(ctx, options)
+  var once = false
+  var passive: Opt[bool]
+  if JS_IsObject(options):
+    once = fromJS[bool](ctx, JS_GetPropertyStr(ctx, options, "once"))
+      .get(false)
+    let x = fromJS[bool](ctx, JS_GetPropertyStr(ctx, options, "passive"))
+    if x.isSome:
+      passive = opt(x.get)
+  return (capture, once, passive)
+
+proc addEventListener(ctx: JSContext, eventTarget: EventTarget,
+    callback: EventListenerCallback, options = JS_UNDEFINED) {.jsfunc.} =
+  let (capture, once, passive) = flattenMore(ctx, options)
+  let listener = EventListener(
+    capture: capture,
+    passive: passive,
+    once: once,
+    callback: callback
+  )
+  eventTarget.addAnEventListener(listener)
+
+proc removeEventListener(ctx: JSContext, eventTarget: EventTarget,
+    ctype: string, callback: EventListenerCallback,
+    options = JS_UNDEFINED) {.jsfunc.} =
+  let capture = flatten(ctx, options)
+  let i = eventTarget.findEventListener(ctype, callback, capture)
+  if i != -1:
+    eventTarget.removeAnEventListener(i)
+
 proc addEventModule*(ctx: JSContext) =
   let eventCID = ctx.registerType(Event)
+  ctx.registerType(CustomEvent, parent = eventCID)
   ctx.defineConsts(eventCID, EventPhase, uint16)
   ctx.registerType(EventTarget)