about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2025-05-05 19:30:20 +0200
committerbptato <nincsnevem662@gmail.com>2025-05-05 19:33:02 +0200
commitee6033b9024ca836019e7212ff18b24c915e2d6f (patch)
tree10c23c6acdd16ca355f53b0a88676985691e1cb9
parent32ab5acba853a9940e49250c7a27bcb18fe8af75 (diff)
downloadchawan-ee6033b9024ca836019e7212ff18b24c915e2d6f.tar.gz
event: implement capturing phase, misc improvements
Fixes acid3 tests 31, 32
-rw-r--r--src/html/dom.nim19
-rw-r--r--src/html/event.nim44
-rw-r--r--src/html/xmlhttprequest.nim15
-rw-r--r--src/server/buffer.nim48
-rw-r--r--test/net/xhr.html5
5 files changed, 90 insertions, 41 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim
index bb2c4982..90a2e164 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -2819,6 +2819,12 @@ proc blur(ctx: JSContext; element: Element) {.jsfunc.} =
     if element.document.focus == element:
       element.document.setFocus(nil)
 
+proc click(ctx: JSContext; element: Element) {.jsfunc.} =
+  #TODO should also trigger click on inputs.
+  let event = newEvent(satClick.toAtom(), element, bubbles = true,
+    cancelable = true)
+  discard ctx.dispatch(element, event)
+
 proc scrollTo(element: Element) {.jsfunc.} =
   discard #TODO maybe in app mode?
 
@@ -2851,9 +2857,10 @@ func findAutoFocus*(document: Document): Element =
 proc fireEvent*(window: Window; event: Event; target: EventTarget) =
   discard window.jsctx.dispatch(target, event)
 
-proc fireEvent*(window: Window; name: StaticAtom; target: EventTarget) =
-  let event = newEvent(name.toAtom(), target)
-  event.isTrusted = true
+proc fireEvent*(window: Window; name: StaticAtom; target: EventTarget;
+    bubbles, cancelable, trusted: bool) =
+  let event = newEvent(name.toAtom(), target, bubbles, cancelable)
+  event.isTrusted = trusted
   window.fireEvent(event, target)
 
 proc parseColor(element: Element; s: string): ARGBColor =
@@ -4357,7 +4364,8 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
           image.invalidate()
           #TODO fire error on error
           if window.settings.scripting != smFalse:
-            window.fireEvent(satLoad, image)
+            window.fireEvent(satLoad, image, bubbles = false,
+              cancelable = false, trusted = true)
         )
       )
     window.pendingResources.add(p)
@@ -5803,7 +5811,8 @@ proc createEvent(ctx: JSContext; document: Document; atom: CAtom):
   of satCustomevent:
     return ok(ctx.newCustomEvent(satUempty.toAtom()))
   of satEvent, satEvents, satSvgevents:
-    return ok(newEvent(satUempty.toAtom(), nil))
+    return ok(newEvent(satUempty.toAtom(), nil,
+      bubbles = false, cancelable = false))
   of satUievent, satUievents:
     return ok(newUIEvent(satUempty.toAtom()))
   else:
diff --git a/src/html/event.nim b/src/html/event.nim
index 132ea926..7a4d030d 100644
--- a/src/html/event.nim
+++ b/src/html/event.nim
@@ -37,10 +37,10 @@ type
     eventPhase {.jsget.}: uint16
     bubbles {.jsget.}: bool
     cancelable {.jsget.}: bool
-    #TODO DOMHighResTimeStamp?
-    timeStamp {.jsget.}: float64
     flags*: set[EventFlag]
     isTrusted* {.jsufget.}: bool
+    #TODO DOMHighResTimeStamp?
+    timeStamp {.jsget.}: float64
 
   CustomEvent* = ref object of Event
     detail {.jsget.}: JSValue
@@ -131,11 +131,14 @@ proc newEvent(ctx: JSContext; ctype: CAtom; eventInitDict = EventInit()):
   event.innerEventCreationSteps(eventInitDict)
   return event
 
-proc newEvent*(ctype: CAtom; target: EventTarget): Event =
+proc newEvent*(ctype: CAtom; target: EventTarget; bubbles, cancelable: bool):
+    Event =
   return Event(
     ctype: ctype,
     target: target,
-    currentTarget: target
+    currentTarget: target,
+    bubbles: bubbles,
+    cancelable: cancelable
   )
 
 proc initialize(this: Event; ctype: CAtom; bubbles, cancelable: bool) =
@@ -352,8 +355,7 @@ proc invoke(ctx: JSContext; listener: EventListener; event: Event): JSValue =
     # Apparently it's a bad idea to call a function that can then delete
     # the reference it was called from.
     let callback = JS_DupValue(ctx, listener.callback)
-    let ret = JS_Call(ctx, callback, jsTarget, 1,
-      jsEvent.toJSValueArray())
+    let ret = JS_Call(ctx, callback, jsTarget, 1, jsEvent.toJSValueArray())
     JS_FreeValue(ctx, callback)
     JS_FreeValue(ctx, jsTarget)
     JS_FreeValue(ctx, jsEvent)
@@ -517,13 +519,13 @@ proc hasEventListener*(eventTarget: EventTarget; ctype: CAtom): bool =
   return false
 
 proc dispatchEvent0(ctx: JSContext; event: Event; currentTarget: EventTarget;
-    stop, canceled: var bool) =
+    stop, canceled: var bool; capture: bool) =
   event.currentTarget = currentTarget
   var els = currentTarget.eventListeners # copy intentionally
   for el in els:
     if JS_IsUndefined(el.callback):
       continue # removed, presumably by a previous handler
-    if el.ctype == event.ctype:
+    if el.ctype == event.ctype and el.capture == capture:
       let e = ctx.invoke(el, event)
       if JS_IsException(e):
         ctx.logException()
@@ -536,15 +538,31 @@ proc dispatchEvent0(ctx: JSContext; event: Event; currentTarget: EventTarget;
         break
 
 proc dispatch*(ctx: JSContext; target: EventTarget; event: Event): bool =
-  #TODO this is far from being compliant
   var canceled = false
   var stop = false
   event.flags.incl(efDispatch)
   event.target = target
-  var target = target
-  while target != nil and not stop:
-    ctx.dispatchEvent0(event, target, stop, canceled)
-    target = ctx.getParentImpl(target, event)
+  var it = target
+  var targets: seq[EventTarget] = @[]
+  while it != nil:
+    targets.add(it)
+    it = ctx.getParentImpl(it, event)
+  event.eventPhase = 1
+  for i in countdown(targets.high, 1):
+    if stop:
+      break
+    let it = targets[i]
+    ctx.dispatchEvent0(event, it, stop, canceled, capture = true)
+  event.eventPhase = 2
+  ctx.dispatchEvent0(event, target, stop, canceled, capture = true)
+  ctx.dispatchEvent0(event, target, stop, canceled, capture = false)
+  if event.bubbles:
+    event.eventPhase = 3
+    for target in targets:
+      if stop:
+        break
+      ctx.dispatchEvent0(event, target, stop, canceled, capture = false)
+  event.eventPhase = 0
   event.flags.excl(efDispatch)
   return canceled
 
diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim
index 47a6bef9..1879bb29 100644
--- a/src/html/xmlhttprequest.nim
+++ b/src/html/xmlhttprequest.nim
@@ -118,6 +118,10 @@ proc parseMethod(s: string): DOMResult[HttpMethod] =
   else:
     errDOMException("Invalid method", "SyntaxError")
 
+proc fireReadyStateChangeEvent(window: Window; target: EventTarget) =
+  window.fireEvent(satReadystatechange, target, bubbles = false,
+    cancelable = false, trusted = true)
+
 proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string;
     misc: varargs[JSValueConst]): Err[DOMException] {.jsfunc.} =
   let httpMethod = ?parseMethod(httpMethod)
@@ -156,7 +160,7 @@ proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string;
   #TODO response object, received bytes
   if this.readyState != xhrsOpened:
     this.readyState = xhrsOpened
-    global.fireEvent(satReadystatechange, this)
+    global.fireReadyStateChangeEvent(this)
   return ok()
 
 proc checkOpened(this: XMLHttpRequest): DOMResult[void] =
@@ -207,6 +211,7 @@ proc fireProgressEvent(window: Window; target: EventTarget; name: StaticAtom;
     total: length,
     lengthComputable: length != 0
   ))
+  event.isTrusted = true
   window.fireEvent(event, target)
 
 proc errorSteps(window: Window; this: XMLHttpRequest; name: StaticAtom) =
@@ -214,7 +219,7 @@ proc errorSteps(window: Window; this: XMLHttpRequest; name: StaticAtom) =
   this.response = makeNetworkError()
   this.flags.excl(xhrfSend)
   if xhrfSync notin this.flags:
-    window.fireEvent(satReadystatechange, this)
+    window.fireReadyStateChangeEvent(this)
     if xhrfUploadComplete notin this.flags:
       this.flags.incl(xhrfUploadComplete)
       if xhrfUploadListener in this.flags:
@@ -260,7 +265,7 @@ proc onReadXHR(response: Response) =
     this.received.setLen(olen + n)
   if this.readyState == xhrsHeadersReceived:
     this.readyState = xhrsLoading
-  window.fireEvent(satReadystatechange, this)
+    window.fireReadyStateChangeEvent(this)
   window.fireProgressEvent(this, satProgress, int64(this.received.len),
     opaque.len)
 
@@ -275,7 +280,7 @@ proc onFinishXHR(response: Response; success: bool) =
       window.fireProgressEvent(this, satProgress, recvLen, opaque.len)
       this.readyState = xhrsDone
       this.flags.excl(xhrfSend)
-      window.fireEvent(satReadystatechange, this)
+      window.fireReadyStateChangeEvent(this)
       window.fireProgressEvent(this, satLoad, recvLen, opaque.len)
       window.fireProgressEvent(this, satLoadend, recvLen, opaque.len)
   else:
@@ -348,7 +353,7 @@ proc send(ctx: JSContext; this: XMLHttpRequest; body: JSValueConst = JS_NULL):
       let response = res.get
       this.response = response
       this.readyState = xhrsHeadersReceived
-      window.fireEvent(satReadystatechange, this)
+      window.fireReadyStateChangeEvent(this)
       if this.readyState != xhrsHeadersReceived:
         return
       let len = max(response.getContentLength(), 0)
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 6f3e004f..1c3f9939 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -975,16 +975,14 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
 
 proc dispatchDOMContentLoadedEvent(buffer: Buffer) =
   let window = buffer.window
-  let event = newEvent(satDOMContentLoaded.toAtom(), buffer.document)
-  event.isTrusted = true
-  window.fireEvent(event, buffer.document)
+  window.fireEvent(satDOMContentLoaded, buffer.document, bubbles = false,
+    cancelable = false, trusted = true)
   buffer.maybeReshape()
 
 proc dispatchLoadEvent(buffer: Buffer) =
   let window = buffer.window
-  let event = newEvent(satLoad.toAtom(), window)
-  event.isTrusted = true
-  window.fireEvent(event, window)
+  window.fireEvent(satLoad, window, bubbles = false, cancelable = false,
+    trusted = true)
   buffer.maybeReshape()
 
 proc finishLoad(buffer: Buffer; data: InputData): EmptyPromise =
@@ -1312,13 +1310,21 @@ proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): Request {.proxy.} =
       if buffer.config.scripting != smFalse:
         let window = buffer.window
         if input.inputType == itFile:
-          window.fireEvent(satInput, input)
+          window.fireEvent(satInput, input, bubbles = true, cancelable = true,
+            trusted = true)
         else:
           let inputEvent = newInputEvent(satInput.toAtom(),
-            InputEventInit(data: some(s), inputType: "insertText"))
+            InputEventInit(
+              data: some(s),
+              inputType: "insertText",
+              bubbles: true,
+              cancelable: true
+            )
+          )
           inputEvent.isTrusted = true
           window.fireEvent(inputEvent, input)
-        buffer.window.fireEvent(satChange, input)
+        buffer.window.fireEvent(satChange, input, bubbles = true,
+          cancelable = true, trusted = true)
       buffer.maybeReshape()
       return buffer.implicitSubmit(input)
     of TAG_TEXTAREA:
@@ -1326,7 +1332,8 @@ proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): Request {.proxy.} =
       textarea.value = s
       textarea.invalidate()
       if buffer.config.scripting != smFalse:
-        buffer.window.fireEvent(satChange, textarea)
+        buffer.window.fireEvent(satChange, textarea, bubbles = true,
+          cancelable = true, trusted = true)
       buffer.maybeReshape()
     else: discard
   return nil
@@ -1398,7 +1405,8 @@ proc click(buffer: Buffer; option: HTMLOptionElement): ClickResult =
     if select.attrb(satMultiple):
       option.setSelected(not option.selected)
       if buffer.config.scripting != smFalse:
-        buffer.window.fireEvent(satChange, select)
+        buffer.window.fireEvent(satChange, select, bubbles = true,
+          cancelable = true, trusted = true)
       buffer.maybeReshape()
       return ClickResult()
     return buffer.click(select)
@@ -1489,8 +1497,10 @@ proc click(buffer: Buffer; input: HTMLInputElement): ClickResult =
     input.setChecked(not input.checked)
     if buffer.config.scripting != smFalse:
       # Note: not an InputEvent.
-      buffer.window.fireEvent(satInput, input)
-      buffer.window.fireEvent(satChange, input)
+      buffer.window.fireEvent(satInput, input, bubbles = true,
+        cancelable = true, trusted = true)
+      buffer.window.fireEvent(satChange, input, bubbles = true,
+        cancelable = true, trusted = true)
     buffer.maybeReshape()
     return ClickResult()
   of itRadio:
@@ -1498,8 +1508,10 @@ proc click(buffer: Buffer; input: HTMLInputElement): ClickResult =
     input.setChecked(true)
     if not wasChecked and buffer.config.scripting != smFalse:
       # See above.
-      buffer.window.fireEvent(satInput, input)
-      buffer.window.fireEvent(satChange, input)
+      buffer.window.fireEvent(satInput, input, bubbles = true,
+        cancelable = true, trusted = true)
+      buffer.window.fireEvent(satChange, input, bubbles = true,
+        cancelable = true, trusted = true)
     buffer.maybeReshape()
     return ClickResult()
   of itReset:
@@ -1559,7 +1571,8 @@ proc click*(buffer: Buffer; cursorx, cursory: int): ClickResult {.proxy.} =
     let element = buffer.getCursorElement(cursorx, cursory)
     if element != nil:
       let window = buffer.window
-      let event = newEvent(satClick.toAtom(), element)
+      let event = newEvent(satClick.toAtom(), element, bubbles = true,
+        cancelable = true)
       event.isTrusted = true
       canceled = window.jsctx.dispatch(element, event)
       buffer.maybeReshape()
@@ -1580,7 +1593,8 @@ proc select*(buffer: Buffer; selected: int): ClickResult {.proxy.} =
       if index != selected:
         select.setSelectedIndex(selected)
         if buffer.config.scripting != smFalse:
-          buffer.window.fireEvent(satChange, select)
+          buffer.window.fireEvent(satChange, select, bubbles = true,
+            cancelable = true, trusted = true)
     buffer.restoreFocus()
     buffer.maybeReshape()
   return ClickResult()
diff --git a/test/net/xhr.html b/test/net/xhr.html
index c9845d18..83a07d60 100644
--- a/test/net/xhr.html
+++ b/test/net/xhr.html
@@ -6,8 +6,11 @@
 const x = new XMLHttpRequest();
 assert(x.onreadystatechange === null);
 let changed = false;
-function myFunction() {
+function myFunction(event) {
 	changed = true;
+	assert(!event.bubbles);
+	assert(!event.cancelable);
+	assert(event.isTrusted);
 }
 x.onreadystatechange = myFunction;
 assertEquals(myFunction, x.onreadystatechange);