tinyrelayfile
tinyrelay/tinyclient/fixi.js/

tinyclient/fixi.js raw
1// moxi/moxi.js
2(()=>{
3 let doc = document
4 if(doc.__moxi_mo) return
5 let liveFns = new Set(), pending = false,
6 recompute = ()=>{
7 if (pending) return
8 pending = true
9 queueMicrotask(()=>{liveFns.forEach(f=>f()); setTimeout(()=>pending = false)})
10 }
11 doc.__moxi_mo = new MutationObserver(recs=>{
12 recs.forEach(r=>r.type == "childList" && r.addedNodes.forEach(n=>process(n)))
13 recompute()
14 })
15 let AF = async function(){}.constructor, HARGS = ["q", "wait", "trigger", "debounce"],
16 fire = (elt, type, detail, bub)=>elt.dispatchEvent(new CustomEvent(type, {detail, cancelable:1, bubbles:bub??1, composed:1})),
17 el = (e,n,h,o)=>e.addEventListener(n,h,o),
18 DB = Symbol(),
19 mkDb = ()=>{let last = 0, j; return ms=>new Promise((r,rj)=>{j?.(DB); j = rj; let id = ++last; setTimeout(()=>id == last && (j = null, r()), ms)})},
20 mkWait = ctx=>x=>new Promise(r=>typeof x == "number" ? setTimeout(r,x) : el(ctx,x,r,{once:1})),
21 ignore = elt=>elt.closest("[mx-ignore]"),
22 one = x=>x?[x]:[],
23 POS = {before:"beforebegin",after:"afterend",start:"afterbegin",end:"beforeend"},
24 proxy = elts=>new Proxy({}, {
25 get:(_,p)=>{
26 if (p == "count") return elts.length
27 if (p == "arr") return ()=>elts.slice()
28 if (p == Symbol.iterator) return ()=>elts.values()
29 if (p == "trigger") return (t,d,b)=>elts.forEach(e=>fire(e,t,d,b))
30 if (p == "insert") return (pos,s)=>elts.forEach(e=>e.insertAdjacentHTML(POS[pos],s))
31 if (p == "take") return (cls,from)=>{
32 for (let e of typeof from == "string" ? doc.querySelectorAll(from) : from) e.classList.remove(cls)
33 for (let e of elts) e.classList.add(cls)
34 }
35 let v = elts[0]?.[p]
36 if (v?.call) return (...a)=>elts.map(e=>e[p](...a))[0]
37 if (v && typeof v == "object") return proxy(elts.map(e=>e[p]))
38 return v
39 },
40 set:(_,p,v)=>(elts.forEach(e=>e[p]=v),recompute(),true)
41 }),
42 mkq = ctx=>sel=>{
43 if (typeof sel != "string") return proxy(sel.nodeType ? [sel] : [...sel])
44 let im = sel.match(/^(.+)\s+in\s+(.+)$/), root = doc
45 if (im){ sel = im[1]; root = im[2] == "this" ? ctx : doc.querySelector(im[2]) }
46 if (!root) return proxy([])
47 let m = sel.match(/^(next|prev|closest|first|last)\s+(.+)$/), elts
48 if (m){
49 let [,d,s] = m, cdp = e=>ctx.compareDocumentPosition(e)
50 if (d == "closest") elts = one(ctx.closest(s))
51 else {
52 let all = [...root.querySelectorAll(s)]
53 if (d == "first") elts = all.slice(0,1)
54 else if (d == "last") elts = all.slice(-1)
55 else if (d == "next") elts = one(all.find(e=>cdp(e) & 4))
56 else elts = one(all.reverse().find(e=>cdp(e) & 2))
57 }
58 } else elts = [...root.querySelectorAll(sel)]
59 return proxy(elts)
60 },
61 init = elt=>{
62 if (elt.__moxi || ignore(elt)) return
63 if (!fire(elt, "mx:init", {})) return
64 elt.__moxi = {}
65 let q = mkq(elt), wait = mkWait(elt), trigger = fire.bind(0,elt), liveRuns = []
66 for (let a of elt.attributes){
67 if (a.name == "live"){
68 let fn = new AF(...HARGS, a.value),
69 debounce = mkDb(),
70 run = ()=>elt.isConnected ? fn.call(elt, q, wait, trigger, debounce) : liveFns.delete(run)
71 liveFns.add(run)
72 liveRuns.push(run)
73 } else if (a.name.startsWith("on-")){
74 let [name, ...mods] = a.name.slice(3).split("."),
75 has = m=>mods.includes(m), h = has("halt"), debounce = mkDb()
76 if (has("cc")) name = name.replace(/-([a-z])/g, (_,c)=>c.toUpperCase())
77 let target = has("outside") ? doc : elt,
78 opts = {capture: has("capture"), passive: has("passive")},
79 fn = new AF("event", ...HARGS, `with(event?.detail||{}){${a.value}}`),
80 handler = elt.__moxi[name] = evt=>{
81 if (evt && (has("self") && evt.target != elt || has("outside") && elt.contains(evt.target))) return
82 if (h || has("prevent")) evt?.preventDefault()
83 if (h || has("stop")) evt?.stopPropagation()
84 if (has("once")) target.removeEventListener(name, handler, opts)
85 return fn.call(elt, evt, q, wait, trigger, debounce).catch(e=>{if(e!=DB) throw e})
86 }
87 if (name == "init") handler()
88 else el(target, name, handler, opts)
89 }
90 }
91 liveRuns.forEach(r=>r())
92 fire(elt, "mx:inited", {}, false)
93 },
94 process = n=>{
95 if (n.nodeType != 1 || ignore(n)) return
96 let r = doc.evaluate("descendant-or-self::*[@live or @*[starts-with(name(),'on-')]]", n, null, 7, null)
97 for (let i = 0; i < r.snapshotLength; i++) init(r.snapshotItem(i))
98 },
99 gt = globalThis, de = doc.documentElement
100 gt.q = mkq(de)
101 gt.wait = mkWait(de)
102 gt.transition = fn=>doc.startViewTransition ? doc.startViewTransition(fn) : fn()
103 el(doc, "mx:process", evt=>process(evt.target))
104 el(doc, "refresh", recompute)
105 el(doc, "DOMContentLoaded", ()=>{
106 doc.__moxi_mo.observe(de, {childList:1, subtree:1, attributes:1, characterData:1})
107 el(doc, "input", recompute, true)
108 el(doc, "change", recompute, true)
109 process(doc.body)
110 })
111})()
112;
113// fixi/fixi.js
114(()=>{
115 if(document.__fixi_mo) return;
116 document.__fixi_mo = new MutationObserver((recs)=>recs.forEach((r)=>r.type === "childList" && r.addedNodes.forEach((n)=>process(n))))
117 let send = (elt, type, detail, bub)=>elt.dispatchEvent(new CustomEvent("fx:" + type, {detail, cancelable:true, bubbles:bub !== false, composed:true}))
118 let attr = (elt, name, defaultVal)=>elt.getAttribute(name) || defaultVal
119 let dflt = (n, d)=>(window.fixiCfg ?? {})[n] ?? d
120 let ignore = (elt)=>elt.closest("[fx-ignore]") != null
121 let init = (elt)=>{
122 let options = {}
123 if (elt.__fixi || ignore(elt) || !send(elt, "init", {options})) return
124 elt.__fixi = async(evt)=>{
125 let reqs = elt.__fixi.requests ||= new Set()
126 let form = elt.form || elt.closest("form")
127 let body = new FormData(form ?? undefined, evt.submitter)
128 if (elt.name && !evt.submitter && (!form || (elt.form === form && elt.type === 'submit'))) body.append(elt.name, elt.value)
129 let ac = new AbortController()
130 let cfg = {
131 trigger:evt,
132 action:attr(elt, "fx-action"),
133 method:attr(elt, "fx-method", "GET").toUpperCase(),
134 target:document.querySelector(attr(elt, "fx-target")) ?? elt,
135 swap:attr(elt, "fx-swap", dflt("swap", "outerHTML")),
136 body,
137 drop:reqs.size,
138 headers:{"FX-Request":"true", ...window.fixiCfg?.headers},
139 abort:ac.abort.bind(ac),
140 signal:ac.signal,
141 preventTrigger:true,
142 transition:dflt("transition", document.startViewTransition?.bind(document)),
143 fetch:fetch.bind(window)
144 }
145 let go = send(elt, "config", {cfg, requests:reqs})
146 if (cfg.preventTrigger) evt.preventDefault()
147 if (!go || cfg.drop) return
148 if (/GET|DELETE/.test(cfg.method)){
149 let params = new URLSearchParams(cfg.body)
150 if (params.size)
151 cfg.action += (/\?/.test(cfg.action) ? "&" : "?") + params
152 cfg.body = null
153 }
154 reqs.add(cfg)
155 try {
156 if (cfg.confirm){
157 let result = await cfg.confirm()
158 if (!result) return
159 }
160 if (!send(elt, "before", {cfg, requests:reqs})) return
161 cfg.response = await cfg.fetch(cfg.action, cfg)
162 cfg.text = await cfg.response.text()
163 if (!send(elt, "after", {cfg})) return
164 } catch(error) {
165 send(elt, "error", {cfg, error})
166 return
167 } finally {
168 reqs.delete(cfg)
169 send(elt, "finally", {cfg})
170 }
171 let doSwap = ()=>{
172 if (cfg.swap instanceof Function)
173 return cfg.swap(cfg)
174 else if (/(before|after)(begin|end)/.test(cfg.swap))
175 cfg.target.insertAdjacentHTML(cfg.swap, cfg.text)
176 else if(cfg.swap in cfg.target)
177 cfg.target[cfg.swap] = cfg.text
178 else if(cfg.swap !== 'none') throw cfg.swap
179 }
180 if (cfg.transition)
181 await cfg.transition(doSwap).finished
182 else
183 await doSwap()
184 send(elt, "swapped", {cfg})
185 if (!document.contains(elt)) send(document, "swapped", {cfg})
186 }
187 elt.__fixi.evt = attr(elt, "fx-trigger", elt.matches("form") ? "submit" : elt.matches("input:not([type=button]),select,textarea") ? "change" : "click")
188 elt.addEventListener(elt.__fixi.evt, elt.__fixi, options)
189 send(elt, "inited", {}, false)
190 }
191 let process = (n)=>{
192 if (n.matches){
193 if (ignore(n)) return
194 if (n.matches("[fx-action]")) init(n)
195 }
196 if(n.querySelectorAll) n.querySelectorAll("[fx-action]").forEach(init)
197 }
198 document.addEventListener("fx:process", (evt)=>process(evt.target))
199 document.addEventListener("DOMContentLoaded", ()=>{
200 document.__fixi_mo.observe(document.documentElement, {childList:true, subtree:true})
201 process(document.body)
202 })
203})()
204;
205// ssexi/ssexi.js
206(()=>{
207 let send = (elt, type, detail)=>elt.dispatchEvent(new CustomEvent("fx:sse:" + type, {detail, cancelable:true, bubbles:true, composed:true}))
208 async function* parseSSE(reader) {
209 let buf = '', dec = new TextDecoder(), msg = {data:'', event:'', id:'', retry:null}
210 while (true) {
211 let {done, value} = await reader.read()
212 if (done) break
213 buf += dec.decode(value, {stream:true})
214 let lines = buf.split(/\r\n|\r|\n/)
215 buf = lines.pop()
216 for (let line of lines) {
217 if (!line) {
218 if (msg.data || msg.event || msg.id || msg.retry) yield msg
219 msg = {data:'', event:'', id:'', retry:null}
220 } else if (line[0] !== ':') {
221 let i = line.indexOf(':'), field = i > 0 ? line.slice(0, i) : line, val = i > 0 ? line.slice(i+1).replace(/^ /, '') : ''
222 if (field === 'data') msg.data += (msg.data ? '\n' : '') + val
223 else if (field === 'retry') msg.retry = parseInt(val) || null
224 else if (field in msg) msg[field] = val
225 }
226 }
227 }
228 if (msg.data || msg.event || msg.id || msg.retry) yield msg
229 }
230 document.addEventListener("fx:config", (evt)=>{
231 let cfg = evt.detail.cfg, realFetch = cfg.fetch
232 cfg.headers.Accept ??= "text/html, text/event-stream"
233 cfg.fetch = async(url, options)=>{
234 let response = await realFetch(url, options)
235 if (!response.headers.get('Content-Type')?.includes('text/event-stream')) return response
236 let style = cfg.sseSwap || cfg.swap, sse = cfg.sse = {lastEventId:'', retry:null, reader:response.body.getReader(), closed:false}
237 sse.close = ()=>{ sse.closed = true; sse.reader?.cancel() }
238 if (!send(cfg.target, 'open', {cfg, response})) { cfg.swap = 'none'; return new Response('') }
239 let target = cfg.target, anchor = null
240 if (style === 'outerHTML') { anchor = document.createElement('i'); anchor.style.display = 'none'; target.after(anchor) }
241 let et = anchor || target
242 let onVis = null
243 if (cfg.sseDisconnectOnHidden) onVis = ()=>document.hidden && sse.close()
244 else if (cfg.ssePauseOnHidden) onVis = ()=>document.hidden && sse.reader?.cancel()
245 if (onVis) document.addEventListener('visibilitychange', onVis)
246 try {
247 while (!sse.closed) {
248 try {
249 for await (let msg of parseSSE(sse.reader)) {
250 if (msg.id) sse.lastEventId = msg.id
251 if (msg.retry) sse.retry = msg.retry
252 if (!send(et, 'message', {cfg, message:msg})) { sse.close(); break }
253 let t = target, s = style, tx = false
254 if (msg.event && msg.event[0] === '{') {
255 let e = JSON.parse(msg.event)
256 if (e.target) t = document.querySelector(e.target)
257 if (e.swap) s = e.swap
258 tx = e.transition
259 } else if (msg.event) { send(et, msg.event, {cfg, message:msg}); t = null }
260 if (t && msg.data) {
261 let swap = ()=>{
262 if (s instanceof Function) s(t, msg.data)
263 else if (/(before|after)(begin|end)/i.test(s)) t.insertAdjacentHTML(s, msg.data)
264 else if (s in t) t[s] = msg.data
265 }
266 if (tx && cfg.transition) await cfg.transition(swap).finished
267 else swap()
268 send(t.isConnected ? t : et, 'swapped', {cfg, message:msg})
269 if (t === target && anchor) { target = anchor; style = 'beforebegin' }
270 }
271 }
272 send(et, 'close', {cfg})
273 } catch(error) { send(et, 'error', {cfg, error}) }
274 if (sse.closed) break
275 if (!cfg.sseReconnect && !(cfg.ssePauseOnHidden && document.hidden)) break
276 if (cfg.ssePauseOnHidden && document.hidden) {
277 await new Promise(r=>{let h=()=>{if(!document.hidden||sse.closed){document.removeEventListener('visibilitychange',h);r()}};document.addEventListener('visibilitychange',h)})
278 } else await new Promise(r=>setTimeout(r, sse.retry || 3000))
279 if (sse.closed) break
280 try {
281 let hdrs = new Headers(options.headers)
282 if (sse.lastEventId) hdrs.set('Last-Event-ID', sse.lastEventId)
283 sse.reader = (await realFetch(url, {...options, headers: hdrs})).body.getReader()
284 } catch(error) { send(et, 'error', {cfg, error}); continue }
285 }
286 } finally {
287 anchor?.remove()
288 if (onVis) document.removeEventListener('visibilitychange', onVis)
289 }
290 cfg.swap = 'none'
291 return new Response('')
292 }
293 })
294})()
295;
296// paxi/paxi.js
297(()=>{
298 let mx = (o, n, ids)=>{
299 if (o.nodeType !== n.nodeType || o.nodeName !== n.nodeName){
300 n.querySelectorAll?.("[id]").forEach((ne)=>{
301 if (!n.contains(ne)) return
302 let oe = ids[ne.id]
303 if (oe){ delete ids[ne.id]; mx(oe, ne, ids); ne.replaceWith(oe) }
304 })
305 return o.replaceWith(n)
306 }
307 if (o.nodeType === 3 || o.nodeType === 8){
308 if (o.nodeValue !== n.nodeValue) o.nodeValue = n.nodeValue
309 return
310 }
311 for (let a of [...o.attributes]) if (!n.hasAttribute(a.name)) o.removeAttribute(a.name)
312 for (let a of n.attributes) if (o.getAttribute(a.name) !== a.value) o.setAttribute(a.name, a.value)
313 let oIds = {}
314 for (let c of o.children) if (c.id) oIds[c.id] = c
315 let oc = o.firstChild, nc = n.firstChild, on, nn, m
316 while (oc && nc){
317 on = oc.nextSibling; nn = nc.nextSibling
318 if (nc.id){
319 m = oIds[nc.id]
320 if (m && m !== oc){ o.insertBefore(m, oc); mx(m, nc, ids); nc = nn; continue }
321 if (!m){ o.insertBefore(nc, oc); nc = nn; continue }
322 }
323 mx(oc, nc, ids)
324 oc = on; nc = nn
325 }
326 while (oc){ on = oc.nextSibling; oc.remove(); oc = on }
327 while (nc){ nn = nc.nextSibling; o.appendChild(nc); nc = nn }
328 }
329 window.morph = (target, html)=>{
330 let t = document.createElement("template")
331 t.innerHTML = html
332 let ids = {}
333 target.querySelectorAll("[id]").forEach((e)=>ids[e.id] = e)
334 mx(target, t.content.firstElementChild, ids)
335 }
336 document.addEventListener("fx:config", (e)=>{
337 if (e.detail.cfg.swap === "morph") e.detail.cfg.swap = (cfg)=>morph(cfg.target, cfg.text)
338 })
339})();
340// rexi/rexi.js
341(()=>{
342 let QS_DEFAULT = {GET: 1, HEAD: 1, DELETE: 1}
343 let doc = document
344
345 let isEltIter = (x)=>{
346 if (!x || typeof x !== "object" || x instanceof FormData || x instanceof Element) return false
347 if (!(Symbol.iterator in x)) return false
348 for (let i of x) return i instanceof Element
349 return false
350 }
351
352 let isRaw = (b)=>typeof b === "string"
353 || b instanceof Blob
354 || b instanceof URLSearchParams
355 || b instanceof ArrayBuffer
356
357 let appendFields = (fd, el)=>{
358 if (!el) return
359 if (el instanceof HTMLFormElement) { for (let [k, v] of new FormData(el)) fd.append(k, v); return }
360 if (!el.name) return
361 if (el instanceof HTMLInputElement) {
362 if (el.type === "file") { for (let f of el.files) fd.append(el.name, f) }
363 else if (el.type === "checkbox" || el.type === "radio") { if (el.checked) fd.append(el.name, el.value) }
364 else fd.append(el.name, el.value)
365 } else if (el instanceof HTMLSelectElement && el.multiple) {
366 for (let o of el.selectedOptions) fd.append(el.name, o.value)
367 } else if ("value" in el) fd.append(el.name, el.value)
368 }
369
370 let toFD = (body)=>{
371 if (body instanceof FormData) return body
372 let fd = new FormData()
373 if (body == null) return fd
374 if (body instanceof URLSearchParams) { for (let [k, v] of body) fd.append(k, v); return fd }
375 if (typeof body === "string") { for (let [k, v] of new URLSearchParams(body)) fd.append(k, v); return fd }
376 if (body instanceof Element) appendFields(fd, body)
377 else if (isEltIter(body)) { for (let el of body) appendFields(fd, el) }
378 else for (let [k, v] of Object.entries(body)) {
379 if (Array.isArray(v)) { for (let x of v) fd.append(k, x) }
380 else if (v != null) fd.append(k, v)
381 }
382 return fd
383 }
384
385 let resolve = (inc)=>{
386 if (inc == null) return []
387 if (typeof inc === "string") return [...doc.querySelectorAll(inc)]
388 if (inc instanceof Element) return [inc]
389 if (Symbol.iterator in Object(inc)) {
390 let out = []
391 for (let i of inc) for (let e of resolve(i)) out.push(e)
392 return out
393 }
394 return []
395 }
396
397 let isJson = (b)=>b != null && typeof b === "object" && !(b instanceof FormData) && !(b instanceof Element) && !isEltIter(b) && !isRaw(b)
398
399 let toQS = (fd)=>{
400 let p = new URLSearchParams()
401 for (let [k, v] of fd) if (!(v instanceof Blob)) p.append(k, v)
402 return p.toString()
403 }
404
405 let wrap = (p, ctl)=>{
406 p.json = ()=>p.then((r)=>r.json())
407 p.text = ()=>p.then((r)=>r.text())
408 p.blob = ()=>p.then((r)=>r.blob())
409 p.html = ()=>p.then((r)=>r.text()).then((t)=>{let tpl = doc.createElement("template"); tpl.innerHTML = t; return tpl.content})
410 p.raw = ()=>p
411 p.abort = ()=>ctl.abort()
412 return p
413 }
414
415 let req = async (method, url, body, ctl, {timeout, signal, include, send, headers = {}, ...rest} = {})=>{
416 if (signal) signal.addEventListener("abort", ()=>ctl.abort(), {once: true})
417 if (timeout) setTimeout(()=>ctl.abort(), timeout)
418 let asQuery = (send === "query") || (send !== "body" && QS_DEFAULT[method])
419 let init = {method, headers: {...headers}, ...rest, signal: ctl.signal}
420 if (asQuery) {
421 let fd = toFD(body)
422 for (let el of resolve(include)) appendFields(fd, el)
423 let qs = toQS(fd)
424 if (qs) url += (url.includes("?") ? "&" : "?") + qs
425 } else if (isRaw(body)) {
426 init.body = body
427 } else if (include == null && isJson(body)) {
428 init.body = JSON.stringify(body)
429 init.headers["Content-Type"] ??= "application/json"
430 } else if (body != null || include != null) {
431 let fd = toFD(body)
432 for (let el of resolve(include)) appendFields(fd, el)
433 init.body = fd
434 }
435 let cfg = {url, init}
436 if (!doc.dispatchEvent(new CustomEvent("rexi:before", {detail: {cfg}, cancelable: true}))) {
437 throw new DOMException("Request cancelled by rexi:before listener", "AbortError")
438 }
439 let res = await fetch(cfg.url, cfg.init)
440 doc.dispatchEvent(new CustomEvent("rexi:after", {detail: {cfg, response: res}}))
441 if (!res.ok) throw Object.assign(new Error(`${method} ${cfg.url} -> ${res.status}`), {status: res.status, response: res})
442 return res
443 }
444
445 let make = (method)=>(url, body, opts)=>{
446 let ctl = new AbortController()
447 return wrap(req(method, url, body, ctl, opts), ctl)
448 }
449
450 let api = {}
451 for (let n of ["get", "head", "post", "put", "patch"]) api[n] = make(n.toUpperCase())
452 api.del = make("DELETE")
453 window.rexi = api
454 for (let k of ["get", "head", "post", "put", "patch", "del"]) window[k] = api[k]
455})()
456;

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs