fusee-framework 2.7.1 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -32
- package/framework/actions/index.js +12 -0
- package/framework/auto-import-preset.js +18 -5
- package/framework/auto-imports.d.ts +30 -27
- package/framework/core/compiler.js +322 -310
- package/framework/core/h.js +632 -588
- package/framework/di/index.js +9 -0
- package/framework/index.js +10 -7
- package/framework/plugins/index.js +8 -0
- package/framework/store/index.js +12 -0
- package/framework/types/actions.d.ts +52 -50
- package/framework/types/di-entry.d.ts +9 -0
- package/framework/types/index.d.ts +112 -118
- package/package.json +19 -1
- package/framework/core/comet-js/comet-config.js +0 -14
- package/framework/core/comet-js/comet-fusee.d.ts +0 -10
- package/framework/core/comet-js/comet-fusee.js +0 -114
- package/framework/core/comet-js/comet-methods.js +0 -32
- package/framework/core/comet-js/comet.d.ts +0 -70
- package/framework/core/comet-js/comet.js +0 -534
- package/framework/frel/frel.d.ts +0 -152
- package/framework/frel/frel.js +0 -757
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
export type CometTriggerSpec = {
|
|
2
|
-
event?: string | null
|
|
3
|
-
poll?: number
|
|
4
|
-
once?: boolean
|
|
5
|
-
changed?: boolean
|
|
6
|
-
delay?: number
|
|
7
|
-
throttle?: number
|
|
8
|
-
from?: string | null
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type CometRequestConfig = {
|
|
12
|
-
method: string
|
|
13
|
-
url: string | null
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type CometOobSwap = {
|
|
17
|
-
el: Element
|
|
18
|
-
mode: string
|
|
19
|
-
targetSelector: string | null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type CometConfig = {
|
|
23
|
-
defaultSwap: string
|
|
24
|
-
attrPrefix: string
|
|
25
|
-
requestingClass: string
|
|
26
|
-
timeout: number
|
|
27
|
-
hydrator: ((node: Node, detail: any) => void) | null
|
|
28
|
-
cleanup: ((target: Element, mode: string) => void) | null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type CometApi = {
|
|
32
|
-
init(root: Element): void
|
|
33
|
-
process(element: Element): void
|
|
34
|
-
config: CometConfig
|
|
35
|
-
parseTriggerSpec(raw: string | null): CometTriggerSpec[]
|
|
36
|
-
getRequestConfig(element: Element): CometRequestConfig | null
|
|
37
|
-
gatherParams(element: Element): URLSearchParams
|
|
38
|
-
swap(target: Element, html: string, mode: string): Node[]
|
|
39
|
-
extractOOBSwaps(html: string, document: Document): {
|
|
40
|
-
mainHTML: string
|
|
41
|
-
oobSwaps: CometOobSwap[]
|
|
42
|
-
}
|
|
43
|
-
setHydrator(fn: ((node: Node, detail: any) => void) | null): void
|
|
44
|
-
setCleanup(fn: ((target: Element, mode: string) => void) | null): void
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export declare const config: CometConfig
|
|
48
|
-
export declare const ATTR: Record<string, string>
|
|
49
|
-
|
|
50
|
-
export function parseTriggerSpec(raw: string | null): CometTriggerSpec[]
|
|
51
|
-
export function getRequestConfig(element: Element): CometRequestConfig | null
|
|
52
|
-
export function gatherParams(element: Element): URLSearchParams
|
|
53
|
-
export function swap(target: Element, html: string, mode: string): Node[]
|
|
54
|
-
export function extractOOBSwaps(html: string, document: Document): {
|
|
55
|
-
mainHTML: string
|
|
56
|
-
oobSwaps: CometOobSwap[]
|
|
57
|
-
}
|
|
58
|
-
export function performRequest(element: Element, triggerEvent: Event | null): Promise<void>
|
|
59
|
-
export function wireElement(element: Element): void
|
|
60
|
-
export function process(element: Element): void
|
|
61
|
-
export function init(root: Element): void
|
|
62
|
-
export function stopObserving(): void
|
|
63
|
-
export function setHydrator(fn: ((node: Node, detail: any) => void) | null): void
|
|
64
|
-
export function setCleanup(fn: ((target: Element, mode: string) => void) | null): void
|
|
65
|
-
|
|
66
|
-
declare global {
|
|
67
|
-
interface Window {
|
|
68
|
-
Comet?: CometApi
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,534 +0,0 @@
|
|
|
1
|
-
import { config } from "./comet-config.js";
|
|
2
|
-
import { ATTR, METHOD_ATTRS, DEFAULT_TRIGGER_BY_TAG } from "./comet-methods.js";
|
|
3
|
-
|
|
4
|
-
export { config, ATTR };
|
|
5
|
-
|
|
6
|
-
// --- Event dispatch ---------------------------------------------------
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Dispatches a `comet:<name>` CustomEvent on el, bubbling, cancelable.
|
|
10
|
-
* Returns the event so callers can check `.defaultPrevented`.
|
|
11
|
-
*/
|
|
12
|
-
function fire(el, name, detail) {
|
|
13
|
-
const event = new CustomEvent(`comet:${name}`, {
|
|
14
|
-
detail,
|
|
15
|
-
bubbles: true,
|
|
16
|
-
cancelable: true,
|
|
17
|
-
})
|
|
18
|
-
el.dispatchEvent(event)
|
|
19
|
-
return event
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// --- Trigger spec parsing ---------------------------------------------
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Parses a comet-trigger value into an array of trigger specs.
|
|
26
|
-
* Supports comma-separated multiple triggers, "every Xs" polling,
|
|
27
|
-
* "load" (fire once on init), and modifiers: delay:Xms, throttle:Xms,
|
|
28
|
-
* changed, once, from:<selector>.
|
|
29
|
-
*
|
|
30
|
-
* "keyup changed delay:500ms" -> { event: 'keyup', changed: true, delay: 500 }
|
|
31
|
-
* "every 2s" -> { poll: 2000 }
|
|
32
|
-
* "load" -> { event: 'load' } (handled specially: fires immediately)
|
|
33
|
-
*/
|
|
34
|
-
export function parseTriggerSpec(raw) {
|
|
35
|
-
if (!raw || !raw.trim()) return [{ event: null }]
|
|
36
|
-
return raw.split(',').map(part => parseSingleTrigger(part.trim())).filter(Boolean)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parseSingleTrigger(part) {
|
|
40
|
-
if (!part) return null
|
|
41
|
-
const tokens = part.split(/\s+/)
|
|
42
|
-
const first = tokens[0]
|
|
43
|
-
|
|
44
|
-
if (first === 'every') {
|
|
45
|
-
const ms = parseDuration(tokens[1]) ?? 1000
|
|
46
|
-
return { poll: ms }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const spec = { event: first, once: false, changed: false, delay: 0, throttle: 0, from: null }
|
|
50
|
-
for (const tok of tokens.slice(1)) {
|
|
51
|
-
if (tok === 'once') spec.once = true
|
|
52
|
-
else if (tok === 'changed') spec.changed = true
|
|
53
|
-
else if (tok.startsWith('delay:')) spec.delay = parseDuration(tok.slice(6)) ?? 0
|
|
54
|
-
else if (tok.startsWith('throttle:')) spec.throttle = parseDuration(tok.slice(9)) ?? 0
|
|
55
|
-
else if (tok.startsWith('from:')) spec.from = tok.slice(5)
|
|
56
|
-
}
|
|
57
|
-
return spec
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parseDuration(s) {
|
|
61
|
-
if (!s) return null
|
|
62
|
-
const m = /^(\d+(?:\.\d+)?)(ms|s)?$/.exec(s.trim())
|
|
63
|
-
if (!m) return null
|
|
64
|
-
const n = parseFloat(m[1])
|
|
65
|
-
return m[2] === 's' ? n * 1000 : n
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// --- Request config -----------------------------------------------------
|
|
69
|
-
|
|
70
|
-
/** Returns { method, url } from whichever comet-<method> attribute is present, or null. */
|
|
71
|
-
export function getRequestConfig(el) {
|
|
72
|
-
for (const [method, attr] of METHOD_ATTRS) {
|
|
73
|
-
if (el.hasAttribute(attr)) {
|
|
74
|
-
return { method, url: el.getAttribute(attr) }
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return null
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function resolveDefaultEvent(el) {
|
|
81
|
-
return DEFAULT_TRIGGER_BY_TAG[el.tagName] || 'click'
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// --- Param gathering ------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Gathers request parameters for el: form fields (if el is a form, or
|
|
88
|
-
* the triggering element is inside one and no comet-get/post is on the
|
|
89
|
-
* form itself — kept simple in Phase 1: only el's OWN form, if el IS a
|
|
90
|
-
* form), comet-vals (JSON), comet-include (selector, gathers name/value
|
|
91
|
-
* pairs from matches), and el's own name/value if it has both.
|
|
92
|
-
*/
|
|
93
|
-
export function gatherParams(el) {
|
|
94
|
-
const params = new URLSearchParams()
|
|
95
|
-
|
|
96
|
-
if (el.tagName === 'FORM') {
|
|
97
|
-
const fd = new FormData(el)
|
|
98
|
-
for (const [k, v] of fd.entries()) {
|
|
99
|
-
if (typeof v === 'string') params.append(k, v)
|
|
100
|
-
}
|
|
101
|
-
} else if (el.name && el.value !== undefined) {
|
|
102
|
-
params.append(el.name, el.value)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const includeSel = el.getAttribute(ATTR.include)
|
|
106
|
-
if (includeSel) {
|
|
107
|
-
const root = el.ownerDocument
|
|
108
|
-
root.querySelectorAll(includeSel).forEach(node => {
|
|
109
|
-
if (node.name && node.value !== undefined) {
|
|
110
|
-
params.append(node.name, node.value)
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const valsRaw = el.getAttribute(ATTR.vals)
|
|
116
|
-
if (valsRaw) {
|
|
117
|
-
try {
|
|
118
|
-
const vals = JSON.parse(valsRaw)
|
|
119
|
-
for (const [k, v] of Object.entries(vals)) {
|
|
120
|
-
params.append(k, String(v))
|
|
121
|
-
}
|
|
122
|
-
} catch (e) {
|
|
123
|
-
console.warn(`[comet] invalid comet-vals JSON on`, el, e)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return params
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// --- Swap ------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Performs the actual DOM swap. `mode` is one of: innerHTML, outerHTML,
|
|
134
|
-
* beforebegin, afterbegin, beforeend, afterend, delete, none.
|
|
135
|
-
* Returns the list of newly-inserted top-level nodes (for re-processing),
|
|
136
|
-
* or [] for delete/none/innerHTML-of-nothing-new-at-top-level cases where
|
|
137
|
-
* re-processing should instead happen on `target` itself (innerHTML) or
|
|
138
|
-
* nothing (delete/none).
|
|
139
|
-
*/
|
|
140
|
-
export function swap(target, html, mode) {
|
|
141
|
-
switch (mode) {
|
|
142
|
-
case 'none':
|
|
143
|
-
return []
|
|
144
|
-
case 'delete':
|
|
145
|
-
target.remove()
|
|
146
|
-
return []
|
|
147
|
-
case 'outerHTML': {
|
|
148
|
-
const parent = target.parentNode
|
|
149
|
-
const tpl = target.ownerDocument.createElement('template')
|
|
150
|
-
tpl.innerHTML = html
|
|
151
|
-
const nodes = Array.from(tpl.content.childNodes)
|
|
152
|
-
parent.replaceChild(tpl.content, target)
|
|
153
|
-
return nodes
|
|
154
|
-
}
|
|
155
|
-
case 'beforebegin':
|
|
156
|
-
case 'afterbegin':
|
|
157
|
-
case 'beforeend':
|
|
158
|
-
case 'afterend': {
|
|
159
|
-
const tpl = target.ownerDocument.createElement('template')
|
|
160
|
-
tpl.innerHTML = html
|
|
161
|
-
const nodes = Array.from(tpl.content.childNodes)
|
|
162
|
-
target.insertAdjacentHTML(mode, html)
|
|
163
|
-
return nodes
|
|
164
|
-
}
|
|
165
|
-
case 'innerHTML':
|
|
166
|
-
default:
|
|
167
|
-
target.innerHTML = html
|
|
168
|
-
return [target]
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Finds every element in `html` carrying comet-swap-oob, removes it from
|
|
174
|
-
* the main fragment, and returns { mainHTML, oobSwaps: [{el, mode, targetSelector}] }
|
|
175
|
-
* so the caller can perform the OOB swaps separately from the main one.
|
|
176
|
-
*
|
|
177
|
-
* comet-swap-oob="true" -> mode "outerHTML", target "#<the element's own id>"
|
|
178
|
-
* comet-swap-oob="innerHTML" -> mode "innerHTML", target "#<the element's own id>"
|
|
179
|
-
* comet-swap-oob="innerHTML:#foo" -> mode "innerHTML", target "#foo"
|
|
180
|
-
*/
|
|
181
|
-
export function extractOOBSwaps(html, doc) {
|
|
182
|
-
const tpl = doc.createElement('template')
|
|
183
|
-
tpl.innerHTML = html
|
|
184
|
-
|
|
185
|
-
const oobEls = Array.from(tpl.content.querySelectorAll(`[${ATTR.swapOob}]`))
|
|
186
|
-
const oobSwaps = []
|
|
187
|
-
|
|
188
|
-
for (const el of oobEls) {
|
|
189
|
-
const raw = el.getAttribute(ATTR.swapOob)
|
|
190
|
-
let mode = 'outerHTML'
|
|
191
|
-
let targetSelector = el.id ? `#${el.id}` : null
|
|
192
|
-
|
|
193
|
-
if (raw && raw !== 'true') {
|
|
194
|
-
const parts = raw.split(':')
|
|
195
|
-
mode = parts[0]
|
|
196
|
-
if (parts[1]) targetSelector = parts[1]
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
el.removeAttribute(ATTR.swapOob)
|
|
200
|
-
el.remove()
|
|
201
|
-
oobSwaps.push({ el, mode, targetSelector })
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { mainHTML: tpl.innerHTML, oobSwaps }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// --- Indicator / confirm ------------------------------------------------
|
|
208
|
-
|
|
209
|
-
function getIndicatorEl(el) {
|
|
210
|
-
const sel = el.getAttribute(ATTR.indicator)
|
|
211
|
-
if (!sel) return el
|
|
212
|
-
return el.ownerDocument.querySelector(sel) || el
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// --- Core request pipeline ---------------------------------------------
|
|
216
|
-
|
|
217
|
-
const inFlightPollers = new WeakMap() // element -> interval id, for cleanup
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Fires the request configured on `el`, using `triggerEvent` (may be
|
|
221
|
-
* null for programmatic/poll-driven calls) for header/context info.
|
|
222
|
-
*/
|
|
223
|
-
export async function performRequest(el, triggerEvent) {
|
|
224
|
-
const reqConfig = getRequestConfig(el)
|
|
225
|
-
if (!reqConfig) return
|
|
226
|
-
|
|
227
|
-
const confirmMsg = el.getAttribute(ATTR.confirm)
|
|
228
|
-
if (confirmMsg && typeof el.ownerDocument.defaultView?.confirm === 'function') {
|
|
229
|
-
if (!el.ownerDocument.defaultView.confirm(confirmMsg)) return
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const targetSel = el.getAttribute(ATTR.target)
|
|
233
|
-
let target = targetSel ? el.ownerDocument.querySelector(targetSel) : el
|
|
234
|
-
if (!target) target = el
|
|
235
|
-
|
|
236
|
-
const swapMode = el.getAttribute(ATTR.swap) || config.defaultSwap
|
|
237
|
-
|
|
238
|
-
const params = gatherParams(el)
|
|
239
|
-
|
|
240
|
-
const requestDetail = {
|
|
241
|
-
method: reqConfig.method,
|
|
242
|
-
url: reqConfig.url,
|
|
243
|
-
params,
|
|
244
|
-
headers: {
|
|
245
|
-
'Comet-Request': 'true',
|
|
246
|
-
'Comet-Trigger': triggerEvent ? triggerEvent.type : '',
|
|
247
|
-
'Comet-Target': targetSel || '',
|
|
248
|
-
},
|
|
249
|
-
target,
|
|
250
|
-
swapMode,
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const configEvent = fire(el, 'configRequest', requestDetail)
|
|
254
|
-
if (configEvent.defaultPrevented) return
|
|
255
|
-
|
|
256
|
-
const beforeEvent = fire(el, 'beforeRequest', requestDetail)
|
|
257
|
-
if (beforeEvent.defaultPrevented) return
|
|
258
|
-
|
|
259
|
-
const indicatorEl = getIndicatorEl(el)
|
|
260
|
-
indicatorEl.classList.add(config.requestingClass)
|
|
261
|
-
|
|
262
|
-
let url = requestDetail.url
|
|
263
|
-
let fetchOpts = {
|
|
264
|
-
method: requestDetail.method.toUpperCase(),
|
|
265
|
-
headers: requestDetail.headers,
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (requestDetail.method === 'get') {
|
|
269
|
-
const qs = params.toString()
|
|
270
|
-
if (qs) url += (url.includes('?') ? '&' : '?') + qs
|
|
271
|
-
} else {
|
|
272
|
-
fetchOpts.body = params
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let controller = null
|
|
276
|
-
if (config.timeout > 0) {
|
|
277
|
-
controller = new AbortController()
|
|
278
|
-
fetchOpts.signal = controller.signal
|
|
279
|
-
setTimeout(() => controller.abort(), config.timeout)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
let response
|
|
283
|
-
try {
|
|
284
|
-
response = await fetch(url, fetchOpts)
|
|
285
|
-
} catch (err) {
|
|
286
|
-
indicatorEl.classList.remove(config.requestingClass)
|
|
287
|
-
fire(el, 'sendError', { ...requestDetail, error: err })
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
indicatorEl.classList.remove(config.requestingClass)
|
|
292
|
-
|
|
293
|
-
fire(el, 'afterRequest', { ...requestDetail, response })
|
|
294
|
-
|
|
295
|
-
if (!response.ok) {
|
|
296
|
-
const errEvent = fire(el, 'responseError', { ...requestDetail, response })
|
|
297
|
-
if (errEvent.defaultPrevented) return
|
|
298
|
-
console.warn(`[comet] request to ${url} failed with status ${response.status}`)
|
|
299
|
-
return
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
await handleResponse(el, response, requestDetail)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function handleResponse(el, response, requestDetail) {
|
|
306
|
-
const doc = el.ownerDocument
|
|
307
|
-
|
|
308
|
-
const redirect = response.headers.get('Comet-Redirect')
|
|
309
|
-
if (redirect) {
|
|
310
|
-
doc.defaultView.location.assign(redirect)
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
let target = requestDetail.target
|
|
315
|
-
const retarget = response.headers.get('Comet-Retarget')
|
|
316
|
-
if (retarget) {
|
|
317
|
-
target = doc.querySelector(retarget) || target
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
let swapMode = requestDetail.swapMode
|
|
321
|
-
const reswap = response.headers.get('Comet-Reswap')
|
|
322
|
-
if (reswap) swapMode = reswap
|
|
323
|
-
|
|
324
|
-
const html = await response.text()
|
|
325
|
-
|
|
326
|
-
const { mainHTML, oobSwaps } = extractOOBSwaps(html, doc)
|
|
327
|
-
|
|
328
|
-
const swapEvent = fire(el, 'beforeSwap', { ...requestDetail, target, swapMode, html: mainHTML })
|
|
329
|
-
if (swapEvent.defaultPrevented) return
|
|
330
|
-
|
|
331
|
-
cleanupTarget(target, swapMode, el, requestDetail)
|
|
332
|
-
|
|
333
|
-
const newNodes = swap(target, mainHTML, swapMode)
|
|
334
|
-
for (const node of newNodes) {
|
|
335
|
-
hydrateNode(node, el, requestDetail)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
for (const { el: oobEl, mode, targetSelector } of oobSwaps) {
|
|
339
|
-
if (!targetSelector) continue
|
|
340
|
-
const oobTarget = doc.querySelector(targetSelector)
|
|
341
|
-
if (!oobTarget) continue
|
|
342
|
-
|
|
343
|
-
cleanupTarget(oobTarget, mode, el, requestDetail)
|
|
344
|
-
|
|
345
|
-
const oobHTML = mode === 'outerHTML' ? oobEl.outerHTML : oobEl.innerHTML
|
|
346
|
-
const oobNewNodes = swap(oobTarget, oobHTML, mode)
|
|
347
|
-
for (const node of oobNewNodes) {
|
|
348
|
-
hydrateNode(node, el, requestDetail)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
fire(el, 'afterSwap', { ...requestDetail, target, swapMode })
|
|
353
|
-
|
|
354
|
-
const pushUrl = response.headers.get('Comet-Push-Url') || el.getAttribute(ATTR.pushUrl)
|
|
355
|
-
if (pushUrl && pushUrl !== 'false') {
|
|
356
|
-
const url = pushUrl === 'true' ? requestDetail.url : pushUrl
|
|
357
|
-
doc.defaultView.history.pushState({ comet: true }, '', url)
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function cleanupTarget(target, mode, el, detail) {
|
|
362
|
-
if (!target) return
|
|
363
|
-
fire(target, 'cleanup', { ...detail, target, mode, triggerEl: el })
|
|
364
|
-
if (typeof config.cleanup === 'function') {
|
|
365
|
-
try {
|
|
366
|
-
config.cleanup(target, mode)
|
|
367
|
-
} catch (err) {
|
|
368
|
-
console.error('[comet] error in cleanup hook:', err)
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function hydrateNode(node, el, detail) {
|
|
374
|
-
if (node.nodeType === 1) {
|
|
375
|
-
if (typeof config.hydrator === 'function') {
|
|
376
|
-
try {
|
|
377
|
-
config.hydrator(node, { ...detail, triggerEl: el })
|
|
378
|
-
} catch (err) {
|
|
379
|
-
console.error('[comet] error in hydrator hook:', err)
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
fire(node, 'hydrate', { ...detail, node, triggerEl: el })
|
|
383
|
-
process(node)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** Registers a custom hydrator hook function. */
|
|
388
|
-
export function setHydrator(fn) {
|
|
389
|
-
config.hydrator = fn
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Registers a custom cleanup hook function. */
|
|
393
|
-
export function setCleanup(fn) {
|
|
394
|
-
config.cleanup = fn
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// --- Wiring --------------------------------------------------------------
|
|
398
|
-
|
|
399
|
-
const wired = new WeakSet()
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Wires a single element that has a comet-<method> attribute: attaches
|
|
403
|
-
* its trigger listener(s), or starts its poller, or fires immediately
|
|
404
|
-
* for a "load" trigger. Idempotent — calling this twice on the same
|
|
405
|
-
* element is a no-op the second time.
|
|
406
|
-
*/
|
|
407
|
-
export function wireElement(el) {
|
|
408
|
-
if (wired.has(el)) return
|
|
409
|
-
wired.add(el)
|
|
410
|
-
|
|
411
|
-
const reqConfig = getRequestConfig(el)
|
|
412
|
-
if (!reqConfig) return
|
|
413
|
-
|
|
414
|
-
const specs = parseTriggerSpec(el.getAttribute(ATTR.trigger))
|
|
415
|
-
|
|
416
|
-
for (const spec of specs) {
|
|
417
|
-
if (spec.poll !== undefined) {
|
|
418
|
-
startPolling(el, spec.poll)
|
|
419
|
-
continue
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const eventName = spec.event || resolveDefaultEvent(el)
|
|
423
|
-
|
|
424
|
-
if (eventName === 'load') {
|
|
425
|
-
queueMicrotask(() => performRequest(el, null))
|
|
426
|
-
continue
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
attachTriggerListener(el, eventName, spec)
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function attachTriggerListener(el, eventName, spec) {
|
|
434
|
-
let lastValue = el.value
|
|
435
|
-
let throttling = false
|
|
436
|
-
let debounceTimer = null
|
|
437
|
-
|
|
438
|
-
const handler = (evt) => {
|
|
439
|
-
if (spec.from && evt.target !== el && !(evt.target.closest && evt.target.closest(spec.from))) {
|
|
440
|
-
return
|
|
441
|
-
}
|
|
442
|
-
if (spec.changed && el.value === lastValue) return
|
|
443
|
-
lastValue = el.value
|
|
444
|
-
|
|
445
|
-
if (eventName === 'submit') evt.preventDefault()
|
|
446
|
-
|
|
447
|
-
const run = () => {
|
|
448
|
-
if (spec.throttle > 0) {
|
|
449
|
-
if (throttling) return
|
|
450
|
-
throttling = true
|
|
451
|
-
setTimeout(() => { throttling = false }, spec.throttle)
|
|
452
|
-
}
|
|
453
|
-
performRequest(el, evt)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (spec.delay > 0) {
|
|
457
|
-
clearTimeout(debounceTimer)
|
|
458
|
-
debounceTimer = setTimeout(run, spec.delay)
|
|
459
|
-
} else {
|
|
460
|
-
run()
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (spec.once) el.removeEventListener(eventName, handler)
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
el.addEventListener(eventName, handler)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function startPolling(el, intervalMs) {
|
|
470
|
-
if (inFlightPollers.has(el)) return
|
|
471
|
-
const id = setInterval(() => {
|
|
472
|
-
if (!el.isConnected) {
|
|
473
|
-
clearInterval(id)
|
|
474
|
-
inFlightPollers.delete(el)
|
|
475
|
-
return
|
|
476
|
-
}
|
|
477
|
-
performRequest(el, null)
|
|
478
|
-
}, intervalMs)
|
|
479
|
-
inFlightPollers.set(el, id)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// --- Public scan/init API -------------------------------------------------
|
|
483
|
-
|
|
484
|
-
const REQUEST_ATTR_SELECTOR = METHOD_ATTRS.map(([, attr]) => `[${attr}]`).join(',')
|
|
485
|
-
|
|
486
|
-
/** Wires a single element and, separately, recurses into its descendants. Safe to call on already-wired elements/subtrees. */
|
|
487
|
-
export function process(el) {
|
|
488
|
-
if (el.nodeType !== 1) return
|
|
489
|
-
if (el.matches && el.matches(REQUEST_ATTR_SELECTOR)) {
|
|
490
|
-
wireElement(el)
|
|
491
|
-
}
|
|
492
|
-
if (el.querySelectorAll) {
|
|
493
|
-
el.querySelectorAll(REQUEST_ATTR_SELECTOR).forEach(wireElement)
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
let observer = null
|
|
498
|
-
|
|
499
|
-
/** Scans root for comet-* elements and wires them, then starts a MutationObserver on root for future additions. */
|
|
500
|
-
export function init(root) {
|
|
501
|
-
if (!root) return
|
|
502
|
-
process(root)
|
|
503
|
-
|
|
504
|
-
if (typeof MutationObserver === 'undefined') return
|
|
505
|
-
if (observer) observer.disconnect()
|
|
506
|
-
|
|
507
|
-
observer = new MutationObserver(mutations => {
|
|
508
|
-
for (const m of mutations) {
|
|
509
|
-
m.addedNodes.forEach(node => {
|
|
510
|
-
if (node.nodeType === 1) process(node)
|
|
511
|
-
})
|
|
512
|
-
}
|
|
513
|
-
})
|
|
514
|
-
observer.observe(root, { childList: true, subtree: true })
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
export function stopObserving() {
|
|
518
|
-
if (observer) {
|
|
519
|
-
observer.disconnect()
|
|
520
|
-
observer = null
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// --- Auto-init in a real browser environment ------------------------------
|
|
525
|
-
|
|
526
|
-
if (typeof window !== 'undefined' && typeof document !== 'undefined' && !window.__COMET_NO_AUTO_INIT__) {
|
|
527
|
-
const start = () => init(document.body)
|
|
528
|
-
if (document.readyState === 'loading') {
|
|
529
|
-
document.addEventListener('DOMContentLoaded', start)
|
|
530
|
-
} else {
|
|
531
|
-
start()
|
|
532
|
-
}
|
|
533
|
-
window.Comet = { init, process, config, parseTriggerSpec, getRequestConfig, gatherParams, swap, extractOOBSwaps, setHydrator, setCleanup }
|
|
534
|
-
}
|