All files / event / mod.ts

98.68% Branches 75/76
100.00% Functions 4/4
100.00% Lines 134/134
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
 
x7
x7
 
 
 
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
 
 
 
 
 
 
 
x7
x7
x7
x7
x7
x7
x7
x7
x65
x65
x65
x7
x7
x79
x1
x1
x84
x79
x31
x31
 
 
x78
x79
x9
x10
x10
x10
x14
x10
x1
x1
x10
x9
 
 
x79
 
x88
x37
x37
x88
x70
x70
x88
x14
x14
x14
 
 
x74
x74
x78
 
x78
x4
x4
x4
x4
x4
x4
x4
x4
x4
 
x78
x78
x37
x37
x37
x42
x8
x8
x8
x78
x74
 
x88
x2
x2
x2
x5
x2
x2
 
x88
x1
x1
x1
x50
x47
x47
x3
x3
x3
x3
x3
x3
x1
x1
 
x88
x1
x1
x1
x15
x15
x15
x15
x1
x1
 
 
x74
x143
x4
x4
x143
x1
x1
x143
x1
x1
x142
x74
 
 
x88
x88
x88
x88
x79
x7
 
 
 
 
 
 
 
x7


























































































I










































































// Imports
import { type Cache, type Callback, type Context, type Directive, Phase } from "@mizu/internal/engine"
import { keyboard } from "./keyboard.ts"
export type * from "@mizu/internal/engine"

/** `@event` typings. */
export const typings = {
  modifiers: {
    throttle: { type: Date, default: 250 },
    debounce: { type: Date, default: 250 },
    keys: { type: String },
    prevent: { type: Boolean },
    stop: { type: Boolean },
    self: { type: Boolean },
    attach: { type: String, allowed: ["element", "window", "document"] },
    passive: { type: Boolean },
    once: { type: Boolean },
    capture: { type: Boolean },
  },
} as const

/**
 * `@event` directive.
 *
 * @internal `_event` Force the directive to use specified event name rather than the attribute name, provided that a single attribute was passed.
 * @internal `_callback` Force the directive to use specified callback rather than the attribute value.
 */
export const _event = {
  name: /^@(?<event>)/,
  prefix: "@",
  phase: Phase.INTERACTIVITY,
  default: "null",
  typings,
  multiple: true,
  init(renderer) {
    if (!renderer.cache(this.name)) {
      renderer.cache<Cache<typeof this>>(this.name, new WeakMap())
    }
  },
  async execute(renderer, element, { cache, attributes, context, state }) {
    if (!renderer.isHtmlElement(element)) {
      return
    }
    const parsed = attributes.map((attribute) => renderer.parseAttribute(attribute, this.typings, { prefix: this.prefix, modifiers: true }))
    if ((arguments[2]._event) && (arguments[2].attributes.length === 1)) {
      parsed[0].name = arguments[2]._event
    }

    // Handle shorthand listeners attachment
    const shorthands = parsed.filter(({ name }) => !name.length)
    if (shorthands.length) {
      for (const shorthand of shorthands) {
        const [attribute] = parsed.splice(parsed.indexOf(shorthand), 1)
        const value = await renderer.evaluate(element, attribute.value, { context, state })
        if (typeof value === "object") {
          parsed.unshift(...Object.entries(value ?? {}).map(([name, value]) => ({ ...attribute, name, value })))
        } else {
          renderer.warn(`[${this.name}] empty shorthand expects an object but got ${typeof value}, ignoring`, element)
        }
      }
    }

    // Attach listeners
    for (const { name: event, value: expression, modifiers, attribute, ephemeral } of parsed) {
      // Ensure listener is not duplicated
      if (!cache.has(element)) {
        cache.set(element, new WeakMap())
      }
      if (!cache.get(element)!.has(attribute)) {
        cache.get(element)!.set(attribute, new Map())
      }
      if (cache.get(element)!.get(attribute)!.has(event)) {
        Object.assign(cache.get(element)!.get(attribute)!.get(event)!, { context, state, expression })
        continue
      }

      // Create callback
      const _callback = arguments[2]._callback
      let callback = function (event: Event) {
        const registered = cache.get(element)?.get(attribute)?.get(event.type)
        // Ignore and remove expired listeners
        if (((!ephemeral) && (!element.hasAttribute(attribute.name))) || (registered && (registered.target !== element) && (!element.isConnected))) {
          if (registered) {
            registered.target.removeEventListener(event.type, registered.listener)
          }
          cache.get(element)?.get(attribute)?.delete(event.type)
          if (!cache.get(element)?.get(attribute)?.size) {
            cache.get(element)?.delete(attribute)
          }
          return
        }
        // Execute callback
        const { context: _context, state: _state, expression: _expression } = registered ?? { context, state, expression }
        if (_callback) {
          _callback(event, { attribute, expression: _expression })
          return
        }
        if (typeof (_expression as string | Callback) === "function") {
          ;(_expression as unknown as Callback)(event)
          return
        }
        renderer.evaluate(element, `${_expression || _event.default}`, { context: _context, state: { ..._state, $event: event }, args: [event] })
      } as Callback
      // Apply keyboard modifiers to callback
      if (modifiers.keys) {
        const check = keyboard(modifiers.keys)
        callback = ((callback) =>
          function (event: KeyboardEvent) {
            return check(event) ? callback(...arguments) : false
          })(callback)
      }
      // Apply throttle modifier to callback
      if (modifiers.throttle) {
        let throttled = false
        callback = ((callback) =>
          function () {
            if (throttled) {
              return false
            }
            throttled = true
            try {
              return callback(...arguments)
            } finally {
              setTimeout(() => throttled = false, modifiers.throttle)
            }
          })(callback)
      }
      // Apply debounce modifier to callback
      if (modifiers.debounce) {
        let timeout = undefined as ReturnType<typeof setTimeout> | undefined
        callback = ((callback) =>
          function () {
            const args = arguments
            clearTimeout(timeout)
            timeout = setTimeout(() => callback(...args), modifiers.debounce)
            return false
          })(callback)
      }

      // Create listener
      const listener = function (event: Event) {
        if (modifiers.prevent) {
          event.preventDefault()
        }
        if (modifiers.stop) {
          event.stopPropagation()
        }
        if (modifiers.self && event.target !== element) {
          return
        }
        return callback(event)
      }

      // Attach listener
      const target = { window: renderer.window, document: renderer.document }[modifiers.attach as string] ?? element
      target.addEventListener(event, listener, { passive: modifiers.passive, once: modifiers.once, capture: modifiers.capture })
      cache.get(element)?.get(attribute)?.set(event, { target, listener, context, state, expression })
    }
  },
} as const satisfies Directive<{
  Name: RegExp
  Cache: WeakMap<HTMLElement, WeakMap<Attr, Map<string, { target: EventTarget; listener: EventListener; context: Context; state: Record<PropertyKey, unknown>; expression: string }>>>
  Typings: typeof typings
  Default: true
}>

/** Default exports. */
export default _event