553 lines
16 KiB
JavaScript
553 lines
16 KiB
JavaScript
// sigpro 1.2.13
|
|
const isFunc = f => typeof f === "function"
|
|
const isObj = o => o && typeof o === "object"
|
|
const isArr = Array.isArray
|
|
const doc = typeof document !== "undefined" ? document : null
|
|
const ensureNode = n => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n)))
|
|
|
|
let activeEffect = null
|
|
let activeOwner = null
|
|
let isFlushing = false
|
|
let batchDepth = 0
|
|
const effectQueue = new Set()
|
|
const proxyCache = new WeakMap()
|
|
const ITER = Symbol('iter')
|
|
const MOUNTED_NODES = new WeakMap()
|
|
|
|
const dispose = eff => {
|
|
if (!eff || eff._disposed) return
|
|
eff._disposed = true
|
|
const stack = [eff]
|
|
while (stack.length) {
|
|
const e = stack.pop()
|
|
if (e._cleanups) {
|
|
e._cleanups.forEach(fn => fn())
|
|
e._cleanups.clear()
|
|
}
|
|
if (e._children) {
|
|
e._children.forEach(child => stack.push(child))
|
|
e._children.clear()
|
|
}
|
|
if (e._deps) {
|
|
e._deps.forEach(depSet => depSet.delete(e))
|
|
e._deps.clear()
|
|
}
|
|
}
|
|
}
|
|
|
|
const onMount = fn => {
|
|
if (activeOwner) (activeOwner._mounts ||= []).push(fn)
|
|
}
|
|
|
|
const onUnmount = fn => {
|
|
if (activeOwner) (activeOwner._cleanups ||= new Set()).add(fn)
|
|
}
|
|
|
|
const untrack = fn => {
|
|
const p = activeEffect
|
|
activeEffect = null
|
|
try { return fn() } finally { activeEffect = p }
|
|
}
|
|
|
|
const createEffect = (fn, isComputed = false) => {
|
|
const effect = () => {
|
|
if (effect._disposed) return
|
|
if (effect._deps) effect._deps.forEach(s => s.delete(effect))
|
|
if (effect._cleanups) {
|
|
effect._cleanups.forEach(c => c())
|
|
effect._cleanups.clear()
|
|
}
|
|
const prevEffect = activeEffect
|
|
const prevOwner = activeOwner
|
|
activeEffect = activeOwner = effect
|
|
try {
|
|
return effect._result = fn()
|
|
} catch (e) {
|
|
console.error("[SigPro]", e)
|
|
} finally {
|
|
activeEffect = prevEffect
|
|
activeOwner = prevOwner
|
|
}
|
|
}
|
|
effect._deps = effect._cleanups = effect._children = null
|
|
effect._disposed = false
|
|
effect._isComputed = isComputed
|
|
effect._depth = activeEffect ? activeEffect._depth + 1 : 0
|
|
effect._mounts = []
|
|
effect._parent = activeOwner
|
|
if (activeOwner) (activeOwner._children ||= new Set()).add(effect)
|
|
return effect
|
|
}
|
|
|
|
const flush = () => {
|
|
if (isFlushing) return
|
|
isFlushing = true
|
|
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth)
|
|
effectQueue.clear()
|
|
for (const e of sorted) if (!e._disposed) e()
|
|
isFlushing = false
|
|
}
|
|
|
|
const Batch = fn => {
|
|
batchDepth++
|
|
try {
|
|
return fn()
|
|
} finally {
|
|
batchDepth--
|
|
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
|
|
flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
const trackUpdate = (subs, trigger = false) => {
|
|
if (!trigger && activeEffect && !activeEffect._disposed) {
|
|
subs.add(activeEffect)
|
|
; (activeEffect._deps ||= new Set()).add(subs)
|
|
} else if (trigger && subs.size > 0) {
|
|
let hasQueue = false
|
|
for (const e of subs) {
|
|
if (e === activeEffect || e._disposed) continue
|
|
if (e._isComputed) {
|
|
e._dirty = true
|
|
if (e._subs) trackUpdate(e._subs, true)
|
|
} else {
|
|
effectQueue.add(e)
|
|
hasQueue = true
|
|
}
|
|
}
|
|
if (hasQueue && !isFlushing && batchDepth === 0) queueMicrotask(flush)
|
|
}
|
|
}
|
|
|
|
const $ = (val, key = null) => {
|
|
const subs = new Set()
|
|
if (isFunc(val)) {
|
|
let cache
|
|
const computed = () => {
|
|
if (computed._dirty) {
|
|
const prev = activeEffect
|
|
activeEffect = computed
|
|
try {
|
|
const next = val()
|
|
if (!Object.is(cache, next)) {
|
|
cache = next
|
|
trackUpdate(subs, true)
|
|
}
|
|
} finally {
|
|
activeEffect = prev
|
|
}
|
|
computed._dirty = false
|
|
}
|
|
trackUpdate(subs)
|
|
return cache
|
|
}
|
|
computed._isComputed = true
|
|
computed._subs = subs
|
|
computed._dirty = true
|
|
computed._deps = null
|
|
computed._disposed = false
|
|
computed.stop = () => { }
|
|
if (activeOwner) onUnmount(computed.stop)
|
|
return computed
|
|
}
|
|
if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val } catch (e) { }
|
|
return (...args) => {
|
|
if (args.length) {
|
|
const next = isFunc(args[0]) ? args[0](val) : args[0]
|
|
if (!Object.is(val, next)) {
|
|
val = next
|
|
if (key) localStorage.setItem(key, JSON.stringify(val))
|
|
trackUpdate(subs, true)
|
|
}
|
|
}
|
|
trackUpdate(subs)
|
|
return val
|
|
}
|
|
}
|
|
|
|
const $$ = (target) => {
|
|
if (!isObj(target)) return target
|
|
|
|
let proxy = proxyCache.get(target)
|
|
if (proxy) return proxy
|
|
|
|
const subsMap = new Map()
|
|
const getSubs = (k) => {
|
|
let s = subsMap.get(k)
|
|
if (!s) subsMap.set(k, (s = new Set()))
|
|
return s
|
|
}
|
|
|
|
proxy = new Proxy(target, {
|
|
get(t, k, receiver) {
|
|
if (typeof k !== 'symbol') trackUpdate(getSubs(k))
|
|
return $$(Reflect.get(t, k, receiver))
|
|
},
|
|
set(t, k, v, receiver) {
|
|
const isNew = !Reflect.has(t, k)
|
|
const oldV = Reflect.get(t, k, receiver)
|
|
const result = Reflect.set(t, k, v, receiver)
|
|
|
|
if (result && !Object.is(oldV, v)) {
|
|
trackUpdate(getSubs(k), true)
|
|
if (isNew) trackUpdate(getSubs(ITER), true)
|
|
}
|
|
return result
|
|
},
|
|
deleteProperty(t, k) {
|
|
const result = Reflect.deleteProperty(t, k)
|
|
if (result) {
|
|
trackUpdate(getSubs(k), true)
|
|
trackUpdate(getSubs(ITER), true)
|
|
}
|
|
return result
|
|
},
|
|
ownKeys(t) {
|
|
trackUpdate(getSubs(ITER))
|
|
return Reflect.ownKeys(t)
|
|
}
|
|
})
|
|
|
|
proxyCache.set(target, proxy)
|
|
return proxy
|
|
}
|
|
|
|
const Watch = (sources, cb) => {
|
|
if (cb === undefined) {
|
|
const effect = createEffect(sources)
|
|
effect()
|
|
return () => dispose(effect)
|
|
}
|
|
const effect = createEffect(() => {
|
|
const vals = Array.isArray(sources) ? sources.map(s => s()) : sources()
|
|
untrack(() => cb(vals))
|
|
})
|
|
effect()
|
|
return () => dispose(effect)
|
|
}
|
|
|
|
const cleanupNode = node => {
|
|
if (node._cleanups) {
|
|
node._cleanups.forEach(fn => fn())
|
|
node._cleanups.clear()
|
|
}
|
|
if (node._ownerEffect) dispose(node._ownerEffect)
|
|
if (node.childNodes) node.childNodes.forEach(cleanupNode)
|
|
}
|
|
|
|
const DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i
|
|
const isDangerousAttr = key => key === 'src' || key === 'href' || key.startsWith('on')
|
|
|
|
const validateAttr = (key, val) => {
|
|
if (val == null || val === false) return null
|
|
if (isDangerousAttr(key)) {
|
|
const sVal = String(val)
|
|
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
|
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`)
|
|
return '#'
|
|
}
|
|
}
|
|
return val
|
|
}
|
|
|
|
const Tag = (tag, props = {}, children = []) => {
|
|
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
|
children = props
|
|
props = {}
|
|
}
|
|
if (isFunc(tag)) {
|
|
const ctx = { _mounts: [], _cleanups: new Set() }
|
|
const effect = createEffect(() => {
|
|
const result = tag(props, {
|
|
children,
|
|
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
|
})
|
|
effect._result = result
|
|
return result
|
|
})
|
|
effect()
|
|
|
|
const result = effect._result
|
|
if (result == null) return null
|
|
|
|
const node = (result instanceof Node || (isArr(result) && result.every(n => n instanceof Node)))
|
|
? result
|
|
: doc.createTextNode(String(result))
|
|
|
|
const attach = n => {
|
|
if (isObj(n) && !n._isRuntime) {
|
|
n._mounts = effect._mounts || []
|
|
n._cleanups = effect._cleanups || new Set()
|
|
n._ownerEffect = effect
|
|
}
|
|
}
|
|
|
|
isArr(node) ? node.forEach(attach) : attach(node)
|
|
return node
|
|
}
|
|
const isSVG = /^(svg|path|circle|rect|line|poly(line|gon)|g|defs|text(path)?|tspan|use|symbol|image|marker|ellipse)$/i.test(tag);
|
|
const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag)
|
|
el._cleanups = new Set()
|
|
|
|
for (let k in props) {
|
|
if (!props.hasOwnProperty(k)) continue
|
|
let v = props[k]
|
|
if (k === "ref") {
|
|
isFunc(v) ? v(el) : (v.current = el)
|
|
continue
|
|
}
|
|
if (isSVG && k.startsWith("xlink:")) {
|
|
const ns = "http://www.w3.org/1999/xlink"
|
|
val == null ? el.removeAttributeNS(ns, k.slice(6)) : el.setAttributeNS(ns, k.slice(6), val)
|
|
continue
|
|
}
|
|
if (k.startsWith("on")) {
|
|
const ev = k.slice(2).toLowerCase()
|
|
el.addEventListener(ev, v)
|
|
const off = () => el.removeEventListener(ev, v)
|
|
el._cleanups.add(off)
|
|
onUnmount(off)
|
|
} else if (isFunc(v)) {
|
|
const effect = createEffect(() => {
|
|
const val = validateAttr(k, v())
|
|
if (k === "class") el.className = val || ""
|
|
else if (val == null) el.removeAttribute(k)
|
|
else if (k in el && !isSVG) el[k] = val
|
|
else el.setAttribute(k, val === true ? "" : val)
|
|
})
|
|
effect()
|
|
el._cleanups.add(() => dispose(effect))
|
|
onUnmount(() => dispose(effect))
|
|
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
|
const evType = k === "checked" ? "change" : "input"
|
|
el.addEventListener(evType, ev => v(ev.target[k]))
|
|
}
|
|
} else {
|
|
const val = validateAttr(k, v)
|
|
if (val != null) {
|
|
if (k in el && !isSVG) el[k] = val
|
|
else el.setAttribute(k, val === true ? "" : val)
|
|
}
|
|
}
|
|
}
|
|
|
|
const append = c => {
|
|
if (isArr(c)) return c.forEach(append)
|
|
if (isFunc(c)) {
|
|
const anchor = doc.createTextNode("")
|
|
el.appendChild(anchor)
|
|
let currentNodes = []
|
|
const effect = createEffect(() => {
|
|
const res = c()
|
|
const next = (isArr(res) ? res : [res]).map(ensureNode)
|
|
currentNodes.forEach(n => {
|
|
if (n._isRuntime) n.destroy()
|
|
else cleanupNode(n)
|
|
if (n.parentNode) n.remove()
|
|
})
|
|
let ref = anchor
|
|
for (let i = next.length - 1; i >= 0; i--) {
|
|
const node = next[i]
|
|
if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref)
|
|
if (node._mounts) node._mounts.forEach(fn => fn())
|
|
ref = node
|
|
}
|
|
currentNodes = next
|
|
})
|
|
effect()
|
|
el._cleanups.add(() => dispose(effect))
|
|
onUnmount(() => dispose(effect))
|
|
} else {
|
|
const node = ensureNode(c)
|
|
el.appendChild(node)
|
|
if (node._mounts) node._mounts.forEach(fn => fn())
|
|
}
|
|
}
|
|
append(children)
|
|
return el
|
|
}
|
|
|
|
const Render = renderFn => {
|
|
const cleanups = new Set()
|
|
const mounts = []
|
|
const previousOwner = activeOwner
|
|
const previousEffect = activeEffect
|
|
const container = doc.createElement("div")
|
|
container.style.display = "contents"
|
|
container.setAttribute("role", "presentation")
|
|
activeOwner = { _cleanups: cleanups, _mounts: mounts }
|
|
activeEffect = null
|
|
|
|
const processResult = result => {
|
|
if (!result) return
|
|
if (result._isRuntime) {
|
|
cleanups.add(result.destroy)
|
|
container.appendChild(result.container)
|
|
} else if (isArr(result)) {
|
|
result.forEach(processResult)
|
|
} else {
|
|
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)))
|
|
}
|
|
}
|
|
|
|
try {
|
|
processResult(renderFn({ onCleanup: fn => cleanups.add(fn) }))
|
|
} finally {
|
|
activeOwner = previousOwner
|
|
activeEffect = previousEffect
|
|
}
|
|
|
|
mounts.forEach(fn => fn())
|
|
return {
|
|
_isRuntime: true,
|
|
container,
|
|
destroy: () => {
|
|
cleanups.forEach(fn => fn())
|
|
cleanupNode(container)
|
|
container.remove()
|
|
}
|
|
}
|
|
}
|
|
|
|
const If = (cond, ifYes, ifNot = null) => {
|
|
const anchor = doc.createTextNode("")
|
|
const root = Tag("div", { style: "display:contents" }, [anchor])
|
|
let currentView = null
|
|
|
|
Watch(
|
|
() => !!(isFunc(cond) ? cond() : cond),
|
|
show => {
|
|
if (currentView) {
|
|
currentView.destroy()
|
|
currentView = null
|
|
}
|
|
|
|
const content = show ? ifYes : ifNot
|
|
if (content) {
|
|
currentView = Render(() => isFunc(content) ? content() : content)
|
|
root.insertBefore(currentView.container, anchor)
|
|
}
|
|
}
|
|
)
|
|
|
|
onUnmount(() => currentView?.destroy())
|
|
return root
|
|
}
|
|
|
|
const For = (src, itemFn, keyFn) => {
|
|
const anchor = doc.createTextNode("")
|
|
const root = Tag("div", { style: "display:contents" }, [anchor])
|
|
let cache = new Map()
|
|
Watch(() => (isFunc(src) ? src() : src) || [], items => {
|
|
const nextCache = new Map()
|
|
const nextOrder = []
|
|
const newItems = items || []
|
|
for (let i = 0; i < newItems.length; i++) {
|
|
const item = newItems[i]
|
|
const key = keyFn ? keyFn(item, i) : (item?.id ?? i)
|
|
let view = cache.get(key)
|
|
if (!view) view = Render(() => itemFn(item, i))
|
|
else cache.delete(key)
|
|
nextCache.set(key, view)
|
|
nextOrder.push(view)
|
|
}
|
|
cache.forEach(view => view.destroy())
|
|
let lastRef = anchor
|
|
for (let i = nextOrder.length - 1; i >= 0; i--) {
|
|
const view = nextOrder[i]
|
|
const node = view.container
|
|
if (node.nextSibling !== lastRef) root.insertBefore(node, lastRef)
|
|
lastRef = node
|
|
}
|
|
cache = nextCache
|
|
})
|
|
return root
|
|
}
|
|
|
|
const Router = routes => {
|
|
const getHash = () => window.location.hash.slice(1) || "/"
|
|
const path = $(getHash())
|
|
const handler = () => path(getHash())
|
|
window.addEventListener("hashchange", handler)
|
|
onUnmount(() => window.removeEventListener("hashchange", handler))
|
|
const hook = Tag("div", { class: "router-hook" })
|
|
let currentView = null
|
|
Watch([path], () => {
|
|
const cur = path()
|
|
const route = routes.find(r => {
|
|
const p1 = r.path.split("/").filter(Boolean)
|
|
const p2 = cur.split("/").filter(Boolean)
|
|
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i])
|
|
}) || routes.find(r => r.path === "*")
|
|
if (route) {
|
|
currentView?.destroy()
|
|
const params = {}
|
|
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
|
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]
|
|
})
|
|
Router.params(params)
|
|
currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component)
|
|
hook.replaceChildren(currentView.container)
|
|
}
|
|
})
|
|
return hook
|
|
}
|
|
Router.params = $({})
|
|
Router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
|
|
Router.back = () => window.history.back()
|
|
Router.path = () => window.location.hash.replace(/^#/, "") || "/"
|
|
|
|
const Anim = (show, render, { enter, leave } = {}) => {
|
|
const wrap = Tag('div', { style: 'display:contents' })
|
|
let view = null
|
|
|
|
const wait = (el, cb) => {
|
|
let done = false
|
|
const finish = () => !done && (done = true, cb())
|
|
if (!el) return finish()
|
|
'transitionend animationend'.split(' ').map(e => el.addEventListener(e, finish, { once: true }))
|
|
setTimeout(finish, 500)
|
|
}
|
|
|
|
Watch(show, on => {
|
|
if (on && !view) {
|
|
const el = (view = Render(render)).container.firstChild
|
|
wrap.appendChild(view.container)
|
|
if (enter && el) {
|
|
el.classList.add(enter); el.clientTop
|
|
el.classList.add(enter + '-active')
|
|
wait(el, () => el.classList.remove(enter, enter + '-active'))
|
|
}
|
|
} else if (!on && view) {
|
|
const el = view.container.firstChild
|
|
const del = () => (view?.destroy(), view = null)
|
|
leave && el ? (el.classList.add(leave), wait(el, del)) : del()
|
|
}
|
|
})
|
|
|
|
return onUnmount(() => view?.destroy()), wrap
|
|
}
|
|
|
|
const Mount = (comp, target) => {
|
|
const t = typeof target === "string" ? doc.querySelector(target) : target
|
|
if (!t) return
|
|
if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy()
|
|
const inst = Render(isFunc(comp) ? comp : () => comp)
|
|
t.replaceChildren(inst.container)
|
|
MOUNTED_NODES.set(t, inst)
|
|
return inst
|
|
}
|
|
|
|
const SigPro = Object.freeze({ $, $$, Watch, Tag, If, For, Router, Mount, onMount, onUnmount, Anim, Batch })
|
|
|
|
if (typeof window !== "undefined") {
|
|
Object.assign(window, SigPro)
|
|
"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li dl dt dd a em strong pre code small sub sup b i u mark blockquote cite abbr time del ins kbd var form label input textarea select button option optgroup fieldset legend datalist output progress meter details summary dialog img svg video audio canvas figure figcaption picture source table thead tbody tfoot tr th td caption colgroup col iframe object embed template slot"
|
|
.split(" ").forEach(t => {
|
|
const name = t[0].toUpperCase() + t.slice(1)
|
|
window[name] = (p, c) => Tag(t, p, c)
|
|
})
|
|
}
|
|
|
|
export { $, $$, Watch, Tag, If, For, Router, Mount, onMount, onUnmount, Anim, Batch } |