clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / tinyclient / fixi.js
fixi.js 17.9K
1// moxi/moxi.js2(()=>{3 let doc = document4 if(doc.__moxi_mo) return5 let liveFns = new Set(), pending = false,6 recompute = ()=>{7 if (pending) return8 pending = true9 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.length27 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 v39 },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 = doc45 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+(.+)$/), elts48 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)) return63 if (!fire(elt, "mx:init", {})) return64 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))) return82 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)) return96 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.documentElement100 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.js114(()=>{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) || defaultVal119 let dflt = (n, d)=>(window.fixiCfg ?? {})[n] ?? d120 let ignore = (elt)=>elt.closest("[fx-ignore]") != null121 let init = (elt)=>{122 let options = {}123 if (elt.__fixi || ignore(elt) || !send(elt, "init", {options})) return124 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) return148 if (/GET|DELETE/.test(cfg.method)){149 let params = new URLSearchParams(cfg.body)150 if (params.size)151 cfg.action += (/\?/.test(cfg.action) ? "&" : "?") + params152 cfg.body = null153 }154 reqs.add(cfg)155 try {156 if (cfg.confirm){157 let result = await cfg.confirm()158 if (!result) return159 }160 if (!send(elt, "before", {cfg, requests:reqs})) return161 cfg.response = await cfg.fetch(cfg.action, cfg)162 cfg.text = await cfg.response.text()163 if (!send(elt, "after", {cfg})) return164 } catch(error) {165 send(elt, "error", {cfg, error})166 return167 } 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.text178 else if(cfg.swap !== 'none') throw cfg.swap179 }180 if (cfg.transition)181 await cfg.transition(doSwap).finished182 else183 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)) return194 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.js206(()=>{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) break213 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 msg219 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' : '') + val223 else if (field === 'retry') msg.retry = parseInt(val) || null224 else if (field in msg) msg[field] = val225 }226 }227 }228 if (msg.data || msg.event || msg.id || msg.retry) yield msg229 }230 document.addEventListener("fx:config", (evt)=>{231 let cfg = evt.detail.cfg, realFetch = cfg.fetch232 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 response236 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 = null240 if (style === 'outerHTML') { anchor = document.createElement('i'); anchor.style.display = 'none'; target.after(anchor) }241 let et = anchor || target242 let onVis = null243 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.id251 if (msg.retry) sse.retry = msg.retry252 if (!send(et, 'message', {cfg, message:msg})) { sse.close(); break }253 let t = target, s = style, tx = false254 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.swap258 tx = e.transition259 } 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.data265 }266 if (tx && cfg.transition) await cfg.transition(swap).finished267 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) break275 if (!cfg.sseReconnect && !(cfg.ssePauseOnHidden && document.hidden)) break276 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) break280 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.js297(()=>{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)) return302 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.nodeValue309 return310 }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] = c315 let oc = o.firstChild, nc = n.firstChild, on, nn, m316 while (oc && nc){317 on = oc.nextSibling; nn = nc.nextSibling318 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 = nn325 }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 = html332 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.js341(()=>{342 let QS_DEFAULT = {GET: 1, HEAD: 1, DELETE: 1}343 let doc = document344345 let isEltIter = (x)=>{346 if (!x || typeof x !== "object" || x instanceof FormData || x instanceof Element) return false347 if (!(Symbol.iterator in x)) return false348 for (let i of x) return i instanceof Element349 return false350 }351352 let isRaw = (b)=>typeof b === "string"353 || b instanceof Blob354 || b instanceof URLSearchParams355 || b instanceof ArrayBuffer356357 let appendFields = (fd, el)=>{358 if (!el) return359 if (el instanceof HTMLFormElement) { for (let [k, v] of new FormData(el)) fd.append(k, v); return }360 if (!el.name) return361 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 }369370 let toFD = (body)=>{371 if (body instanceof FormData) return body372 let fd = new FormData()373 if (body == null) return fd374 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 fd383 }384385 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 out393 }394 return []395 }396397 let isJson = (b)=>b != null && typeof b === "object" && !(b instanceof FormData) && !(b instanceof Element) && !isEltIter(b) && !isRaw(b)398399 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 }404405 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 = ()=>p411 p.abort = ()=>ctl.abort()412 return p413 }414415 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("?") ? "&" : "?") + qs425 } else if (isRaw(body)) {426 init.body = body427 } 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 = fd434 }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 res443 }444445 let make = (method)=>(url, body, opts)=>{446 let ctl = new AbortController()447 return wrap(req(method, url, body, ctl, opts), ctl)448 }449450 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 = api454 for (let k of ["get", "head", "post", "put", "patch", "del"]) window[k] = api[k]455})()456;