about summary refs log blame commit diff stats
path: root/src/html/event.nim
blob: 67131e63ff27cb1ea8cde6fa85458d857866d336 (plain) (tree)
1
2
3
4
5
6
7
8
9
               
                  
                
 
                 

                      

                          
                       

                       
                       
                    
                

    





                          
                   






                              

                                
                                   
                                  
                                         


                                

                                
                          


                                    
                             

                                      
                                       
 
                         
 
                                 
 
                             
                 
                                                             
                                    
                 
                         

                     
 
                   
                         

                         
                          



                                                                        
 
    
                               






                                       
       
                                                                       
                               


                                                                      


                                             
                                

                        
                                                                         
                                
                                 
                                              

                  
                                                          


                   
                         

   
                                                                       
                                





                              
                                                                    
                
                                 

                                               


                                                      






                                                             
                                    

                                                 
                                        
 
                                                         



                                                       
                                                                  

                                   

                                                              

                                                
                                    
 
                                                       



                                             
                             

                                                     
                                 

                                             
                                 

             
                                                                     
                                      
                           
                                              
                                     


                     
                                                           
                                 
 
                                                     

                                                            
                                               





                                               



                                                                      


                                       
                                                              
                                                          
                                          



                                                                               
               
                                                                             



                                              
                               
                                           

                                                          








                                                                        
                                                                        



                             
        

                                                            
               
                                       

                             
                                                                            
                                                                
                                      
                                       

               

                                                                              

                                      

                                      
                                                      






                                                      
                                                   


                    
                           





                                     
                           
                          





                                                              
                
                           

                                 
                                                                              



                                                                          

                                                          
                 


                     
                                        
   
                                               
      
 
                                                                  
                                                  



                                                                 
                                             
 































                                                                             
                                                                    








                                                                           
                                      
                                        
                                                  
                                                
                               
import std/math
import std/options
import std/times

import html/catom
import html/script
import js/domexception
import monoucha/fromjs
import monoucha/javascript
import monoucha/jserror
import monoucha/jstypes
import monoucha/jsutils
import monoucha/quickjs
import monoucha/tojs
import types/opt

type
  EventPhase = enum
    NONE = 0u16
    CAPTURING_PHASE = 1u16
    AT_TARGET = 2u16
    BUBBLING_PHASE = 3u16

  EventFlag* = enum
    efStopPropagation
    efStopImmediatePropagation
    efCanceled
    efInPassiveListener
    efComposed
    efInitialized
    efDispatch

  Event* = ref object of RootObj
    ctype* {.jsget: "type".}: CAtom
    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
    detail {.jsget.}: JSValue

  EventTarget* = ref object of RootObj
    eventListeners*: seq[EventListener]

  EventHandler* = JSValue

  EventListenerCallback = JSValue

  EventListener* = ref object
    ctype*: CAtom
    # if callback is undefined, the listener has been removed
    callback*: EventListenerCallback
    capture: bool
    passive: Option[bool]
    once: bool
    #TODO AbortSignal

jsDestructor(Event)
jsDestructor(CustomEvent)
jsDestructor(EventTarget)

# Forward declaration hack
var isDefaultPassive*: proc(target: EventTarget): bool {.nimcall,
  noSideEffect.} = nil
var getParent*: proc(ctx: JSContext; target: EventTarget, event: Event):
  EventTarget {.nimcall.}

type
  EventInit* = object of JSDict
    bubbles: bool
    cancelable: bool
    composed: bool

  CustomEventInit = object of EventInit
    detail: JSValue

# Event
proc innerEventCreationSteps*(event: Event; eventInitDict: EventInit) =
  event.flags = {efInitialized}
  #TODO this is probably incorrect?
  # I think it measures the time since the first fork. not sure though
  event.timeStamp = round(cpuTime())
  event.bubbles = eventInitDict.bubbles
  event.cancelable = eventInitDict.cancelable
  if eventInitDict.composed:
    event.flags.incl(efComposed)

#TODO eventInitDict type
proc newEvent(ctx: JSContext; ctype: CAtom; eventInitDict = EventInit()):
    JSResult[Event] {.jsctor.} =
  let event = Event(ctype: ctype)
  event.innerEventCreationSteps(eventInitDict)
  return ok(event)

proc newEvent*(ctype: CAtom; target: EventTarget): Event =
  return Event(
    ctype: ctype,
    target: target,
    currentTarget: target
  )

proc initialize(this: Event; ctype: CAtom; bubbles, cancelable: bool) =
  this.flags.incl(efInitialized)
  this.isTrusted = false
  this.target = nil
  this.ctype = ctype
  this.bubbles = bubbles
  this.cancelable = cancelable

proc initEvent(this: Event; ctype: CAtom; bubbles, cancelable: bool)
    {.jsfunc.} =
  if efDispatch 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(efStopPropagation)

func cancelBubble(this: Event): bool {.jsfget.} =
  return efStopPropagation in this.flags

proc cancelBubble(this: Event; cancel: bool) {.jsfset.} =
  if cancel:
    this.stopPropagation()

proc stopImmediatePropagation(this: Event) {.jsfunc.} =
  this.flags.incl({efStopPropagation, efStopImmediatePropagation})

proc setCanceledFlag(this: Event) =
  if this.cancelable and efInPassiveListener notin this.flags:
    this.flags.incl(efCanceled)

proc returnValue(this: Event): bool {.jsfget.} =
  return efCanceled notin this.flags

proc returnValue(this: Event; value: bool) {.jsfset.} =
  if not value:
    this.setCanceledFlag()

proc preventDefault(this: Event) {.jsfunc.} =
  this.flags.incl(efCanceled)

func defaultPrevented(this: Event): bool {.jsfget.} =
  return efCanceled in this.flags

func composed(this: Event): bool {.jsfget.} =
  return efComposed in this.flags

# CustomEvent
proc newCustomEvent(ctype: CAtom; eventInitDict = CustomEventInit()):
    JSResult[CustomEvent] {.jsctor.} =
  let event = CustomEvent()
  event.innerEventCreationSteps(eventInitDict)
  event.detail = eventInitDict.detail
  event.ctype = ctype
  return ok(event)

proc finalize(rt: JSRuntime; this: CustomEvent) {.jsfin.} =
  JS_FreeValueRT(rt, this.detail)

proc initCustomEvent(this: CustomEvent; ctype: CAtom;
    bubbles, cancelable: bool; detail: JSValue) {.jsfunc.} =
  if efDispatch notin this.flags:
    this.initialize(ctype, bubbles, cancelable)
    this.detail = detail

# EventTarget
proc newEventTarget(): EventTarget {.jsctor.} =
  return EventTarget()

proc defaultPassiveValue(ctx: JSContext; ctype: CAtom;
    eventTarget: EventTarget): bool =
  const check = [satTouchstart, satTouchmove, satWheel, satMousewheel]
  if ctx.toStaticAtom(ctype) in check:
    return true
  return eventTarget.isDefaultPassive()

proc findEventListener(eventTarget: EventTarget; ctype: CAtom;
    callback: EventListenerCallback; capture: bool): int =
  for i, it in eventTarget.eventListeners:
    if it.ctype == ctype and it.callback == callback and it.capture == capture:
      return i
  return -1

# EventListener
proc invoke(ctx: JSContext; listener: EventListener; event: Event): JSValue =
  #TODO make this standards compliant
  if JS_IsNull(listener.callback):
    return JS_UNDEFINED
  let jsTarget = ctx.toJS(event.currentTarget)
  let jsEvent = ctx.toJS(event)
  if JS_IsFunction(ctx, listener.callback):
    let ret = JS_Call(ctx, listener.callback, jsTarget, 1,
      jsEvent.toJSValueArray())
    JS_FreeValue(ctx, jsTarget)
    JS_FreeValue(ctx, jsEvent)
    return ret
  assert JS_IsObject(listener.callback)
  let handler = JS_GetPropertyStr(ctx, listener.callback, "handleEvent")
  if JS_IsException(handler):
    JS_FreeValue(ctx, jsTarget)
    JS_FreeValue(ctx, jsEvent)
    return handler
  let ret = JS_Call(ctx, handler, jsTarget, 1, jsEvent.toJSValueArray())
  JS_FreeValue(ctx, jsTarget)
  JS_FreeValue(ctx, jsEvent)
  return ret

# shared
proc addAnEventListener(ctx: JSContext; target: EventTarget;
    listener: EventListener) =
  #TODO signals
  if JS_IsUndefined(listener.callback):
    return
  if listener.passive.isNone:
    listener.passive = some(ctx.defaultPassiveValue(listener.ctype, target))
  if target.findEventListener(listener.ctype, listener.callback,
      listener.capture) == -1: # dedup
    target.eventListeners.add(listener)
  #TODO signals

proc removeAnEventListener(eventTarget: EventTarget; ctx: JSContext; i: int) =
  let listener = eventTarget.eventListeners[i]
  JS_FreeValue(ctx, listener.callback)
  listener.callback = JS_UNDEFINED
  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: Option[bool]
      #TODO signals
    ] =
  if JS_IsUndefined(options):
    return
  let capture = flatten(ctx, options)
  var once = false
  var passive: Option[bool]
  if JS_IsObject(options):
    let jsOnce = JS_GetPropertyStr(ctx, options, "once")
    once = fromJS[bool](ctx, jsOnce).get(false)
    JS_FreeValue(ctx, jsOnce)
    let jsPassive = JS_GetPropertyStr(ctx, options, "passive")
    let x = fromJS[bool](ctx, jsPassive)
    JS_FreeValue(ctx, jsPassive)
    if x.isSome:
      passive = some(x.get)
  return (capture, once, passive)

proc addEventListener*(ctx: JSContext; eventTarget: EventTarget; ctype: CAtom;
    callback: EventListenerCallback; options = JS_UNDEFINED): Err[JSError]
    {.jsfunc.} =
  if not JS_IsObject(callback) and not JS_IsNull(callback):
    return errTypeError("callback is not an object")
  let (capture, once, passive) = flattenMore(ctx, options)
  let listener = EventListener(
    ctype: ctype,
    capture: capture,
    passive: passive,
    once: once,
    callback: JS_DupValue(ctx, callback)
  )
  ctx.addAnEventListener(eventTarget, listener)
  ok()

proc removeEventListener(ctx: JSContext; eventTarget: EventTarget;
    ctype: CAtom; callback: EventListenerCallback;
    options = JS_UNDEFINED) {.jsfunc.} =
  let capture = flatten(ctx, options)
  let i = eventTarget.findEventListener(ctype, callback, capture)
  if i != -1:
    eventTarget.removeAnEventListener(ctx, i)

proc dispatchEvent0(ctx: JSContext; event: Event; currentTarget: EventTarget;
    stop, canceled: var 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:
      let e = ctx.invoke(el, event)
      if JS_IsException(e):
        ctx.logException()
      JS_FreeValue(ctx, e)
      if efStopImmediatePropagation in event.flags:
        stop = true
        break
      if efStopPropagation in event.flags:
        stop = true
      if efCanceled in event.flags:
        canceled = true

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)
  var target = target
  while target != nil and not stop:
    ctx.dispatchEvent0(event, target, stop, canceled)
    target = ctx.getParent(target, event)
  event.flags.excl(efDispatch)
  return canceled

proc dispatchEvent(ctx: JSContext; this: EventTarget; event: Event):
    DOMResult[bool] {.jsfunc.} =
  if efDispatch in event.flags:
    return errDOMException("Event's dispatch flag is already set",
      "InvalidStateError")
  if efInitialized notin event.flags:
    return errDOMException("Event is not initialized", "InvalidStateError")
  event.isTrusted = false
  return ok(ctx.dispatch(this, event))

proc addEventModule*(ctx: JSContext) =
  let eventCID = ctx.registerType(Event)
  ctx.registerType(CustomEvent, parent = eventCID)
  ctx.defineConsts(eventCID, EventPhase, uint16)
  ctx.registerType(EventTarget)