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



                       
              
               
                
                    
              
                

    





                          
                   








                                   
                                   
                                  
                                         


                                

                                
                          




                                        

                                      
                                       
 
                         
 

                                                           


                                    





                      
                   
                         

                         


                                                            








                                       

                                                          
                               



                                                                      



                                             

                        
                                                                          
                                




                                                   
                                                                           


                   
                         

   












                                                                        


                                                      








































                                                                           
                                                                          

                                                   
                                     










































































                                                                               
                                                                              


                                                                         
                 














                                                                  
                                      
                                        
                                                  
                                                
                               
import math
import times

import bindings/quickjs
import js/dict
import js/error
import js/fromjs
import js/javascript
import js/tojs
import types/opt

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

  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

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

  CustomEventInit = object of EventInit
    detail: JSValue

# Event
proc innerEventCreationSteps(event: Event, ctx: JSContext,
    eventInitDict: EventInit) =
  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())
  event.bubbles = eventInitDict.bubbles
  event.cancelable = eventInitDict.cancelable
  if eventInitDict.composed:
    event.flags.incl(FLAG_COMPOSED)

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

proc newEvent*(ctx: JSContext, ctype: string, target: EventTarget): Event =
  return Event(
    ctype: ctype,
    target: target,
    currentTarget: target
  )

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 = CustomEventInit()): JSResult[CustomEvent] {.jsctor.} =
  let event = CustomEvent()
  event.innerEventCreationSteps(ctx, eventInitDict)
  event.detail = 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, ctype: string,
    callback: EventListenerCallback, options = JS_UNDEFINED) {.jsfunc.} =
  let (capture, once, passive) = flattenMore(ctx, options)
  let listener = EventListener(
    ctype: ctype,
    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)