UNPKG

almostnojs

Version:

A minimalist, dependency-free JavaScript framework featuring tagged template rendering, DOM morphing, custom elements, state management, event handling, animations, and HTTP requests.

1,381 lines (1,367 loc) 59.2 kB
/* AlmostNo.js v1.3.0 Full */ (() => { var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/core.js var globalScope, core_default; var init_core = __esm({ "src/core.js"() { globalScope = typeof window !== "undefined" ? window : global; if (!globalScope.__AnJS__) { class AnJS extends Array { /** * Initialize AnJS * * @param {string | HTMLElement | NodeList} query - CSS selector or element. */ constructor(query) { super(); if (!query) return; if (query instanceof HTMLElement || query.nodeType === 1) this.push(query); else if (query instanceof NodeList || Array.isArray(query)) this.push(...query); else if (typeof query === "string") this.push(...document.querySelectorAll(query)); } /** * Iterate through elements * * @param {Function} fn - Callback function. * @returns {AnJS} - Returns self for chaining. */ each(fn) { this.forEach(fn); return this; } /** * Get elements by index or return all * * @param {number} [index] - The index of the element to retrieve. * @returns {HTMLElement | Array} - The specific element or an array of elements. */ get(index) { return index === void 0 ? this : this.at(index); } /** * Clone the first selected element * * @param {boolean} [deep=true] - Clone children. * @returns {HTMLElement | null} - Cloned element. */ clone(deep = true) { return this[0] ? this[0].cloneNode(deep) : null; } } globalScope.__AnJS__ = AnJS; } core_default = globalScope.__AnJS__; } }); // src/filtering.js var filtering_exports = {}; var init_filtering = __esm({ "src/filtering.js"() { init_core(); Object.assign(core_default.prototype, { /** * Filter elements based on a callback function or CSS selector * * @param {Function | string} callbackOrSelector - Callback function or CSS selector. * @returns {AnJS} - Returns a new instance of AnJS. */ filter(callbackOrSelector) { if (typeof callbackOrSelector === "function") return new core_default([...this].filter(callbackOrSelector)); return new core_default([...this].filter((el) => el.matches(callbackOrSelector))); }, /** * Find child elements by a CSS selector * * @param {string} selector - CSS selector to find child elements. * @returns {AnJS} - Returns a new instance of AnJS. */ find(selector) { return new core_default(this.flatMap((el) => [...el.querySelectorAll(selector)])); }, /** * Select the first element from the current selection * * @returns {AnJS} - Returns a new instance of AnJS. */ first() { return new core_default(this.length ? [this[0]] : []); }, /** * Select the last element from the current selection * * @returns {AnJS} - Returns a new instance of AnJS. */ last() { return new core_default(this.length ? [this[this.length - 1]] : []); }, /** * Select only elements with an even index * * @returns {AnJS} - Returns a new instance of AnJS. */ even() { return new core_default(this.filter((_, index) => !(index % 2))); }, /** * Select only elements with an odd index * * @returns {AnJS} - Returns a new instance of AnJS. */ odd() { return new core_default(this.filter((_, index) => index % 2)); } }); } }); // src/traversal.js var traversal_exports = {}; var init_traversal = __esm({ "src/traversal.js"() { init_core(); Object.assign(core_default.prototype, { /** * Select the next sibling element * * @returns {AnJS} - New AnJS instance with the next sibling. */ next() { return new core_default(this[0]?.nextElementSibling ? [this[0].nextElementSibling] : []); }, /** * Select the previous sibling element * * @returns {AnJS} - New AnJS instance with the previous sibling. */ prev() { return new core_default(this[0]?.previousElementSibling ? [this[0].previousElementSibling] : []); }, /** * Select the parent element * * @returns {AnJS} - New AnJS instance with the parent. */ parent() { return new core_default(this[0]?.parentElement ? [this[0].parentElement] : []); }, /** * Select child elements * * @returns {AnJS} - New AnJS instance with children. */ children() { return new core_default(this[0] ? [...this[0].children] : []); }, /** * Select all sibling elements * * @returns {AnJS} - New AnJS instance with all siblings except the current element. */ siblings() { const parent = this[0]?.parentElement; return new core_default(parent ? [...parent.children].filter((el) => el !== this[0]) : []); }, /** * Select the closest ancestor matching a selector * * @param {string} selector - CSS selector to match. * @returns {AnJS} - New AnJS instance with the closest matching ancestor. */ closest(selector) { return new core_default(this[0]?.closest(selector) ? [this[0].closest(selector)] : []); } }); } }); // src/state.js var state_exports = {}; __export(state_exports, { bindings: () => bindings, localBindings: () => localBindings }); function persistent(name, initial, persist) { return new Proxy(initial, { // Retrieve property get: (target, prop) => target[prop], // Update property & auto-save if persistence is enabled set: (target, prop, value) => { target[prop] = value; if (persist) { const storage = persist === "session" ? sessionStorage : localStorage; storage.setItem(name, JSON.stringify(target)); } return true; } }); } var bindings, localBindings, globalStates, attrBindings, boolAttrs; var init_state = __esm({ "src/state.js"() { init_core(); bindings = {}; localBindings = /* @__PURE__ */ new Map(); globalStates = typeof window !== "undefined" && (window.__AnJS_GLOBAL_STATES__ || (window.__AnJS_GLOBAL_STATES__ = {})) || {}; attrBindings = {}; boolAttrs = /* @__PURE__ */ new Set([ "disabled", "checked", "selected", "readonly", "multiple", "hidden", "autoplay", "controls", "loop", "muted" ]); core_default.prototype.global = function(name, initial, options = {}) { if (!name || typeof name !== "string") throw new Error("Global state must have a unique name."); if (!globalStates[name] && initial === void 0) throw new Error(`Global state "${name}" does not exist. Provide an initial state.`); if (options.persist) { const storage = options.persist === "session" ? sessionStorage : localStorage; const saved = storage.getItem(name); if (saved) initial = JSON.parse(saved); } if (!globalStates[name] && initial !== void 0) { globalStates[name] = $.state(persistent(name, initial, options.persist)); } return globalStates[name]; }; core_default.prototype.clearGlobal = function(name) { if (globalStates[name]) { delete globalStates[name]; localStorage.removeItem(name); sessionStorage.removeItem(name); } }; core_default.prototype.hasGlobal = function(name) { return !!globalStates[name]; }; core_default.prototype.state = function(initial = {}, options = {}) { var _a; const isGlobal = !!options.global; if (isGlobal) { if (!options.name) throw new Error("Global state must have a name."); initial = globalStates[_a = options.name] ?? (globalStates[_a] = initial); } const listeners = /* @__PURE__ */ new Map(); let batchQueue = null; function notify(prop, value) { const handlers = listeners.get(prop); if (handlers) handlers.forEach((fn) => fn(value, prop)); const wildcards = listeners.get("*"); if (wildcards) wildcards.forEach((fn) => fn(value, prop)); } const proxy = new Proxy(initial, { // Retrieve state property — also exposes onChange, onAny, patch as methods get: (target, prop) => { if (prop === "onChange") return (path, handler) => { if (!listeners.has(path)) listeners.set(path, /* @__PURE__ */ new Set()); listeners.get(path).add(handler); return () => listeners.get(path)?.delete(handler); }; if (prop === "onAny") return (handler) => { if (!listeners.has("*")) listeners.set("*", /* @__PURE__ */ new Set()); listeners.get("*").add(handler); return () => listeners.get("*")?.delete(handler); }; if (prop === "patch") return (changes) => { batchQueue = /* @__PURE__ */ new Set(); for (const [key, val] of Object.entries(changes)) { proxy[key] = val; } const changed = batchQueue; batchQueue = null; for (const key of changed) { notify(key, target[key]); } }; return target[prop]; }, // Update state property & trigger UI updates set: (target, prop, value) => { target[prop] = value; if (isGlobal) { Object.keys(bindings).forEach((bindKey) => { let [root, ...rest] = bindKey.split("."); if (!bindings[bindKey]) return; let nestedValue = rest.reduce((obj, key) => obj?.[key], globalStates[root]); bindings[bindKey].forEach((el) => el.textContent = nestedValue ?? ""); }); } bindings[prop]?.forEach((el) => el.textContent = value ?? ""); localBindings.get(proxy)?.[prop]?.forEach((el) => el.textContent = value ?? ""); attrBindings[prop]?.forEach(({ el, attr }) => { value == null ? el.removeAttribute(attr) : boolAttrs.has(attr.toLowerCase()) ? el.toggleAttribute(attr, !!value) : el.setAttribute(attr, value); }); if (batchQueue) { batchQueue.add(prop); } else { notify(prop, value); } return true; } }); localBindings.set(proxy, {}); this.bind(proxy); return proxy; }; core_default.prototype.bind = function(state, context = document) { context.querySelectorAll("[data-bind], [data-bind-this], [data-bind-attr]").forEach((el) => { var _a; if (el.dataset.bound) return; el.dataset.bound = "true"; const [attr, prop] = el.getAttribute("data-bind-attr")?.split(":") || [null, el.getAttribute("data-bind") || el.getAttribute("data-bind-this")]; const parts = prop?.split("."); const value = parts?.length > 1 ? parts.slice(1).reduce((o, k) => o?.[k], globalStates[parts[0]] ?? state) : state[prop] ?? globalStates[prop]; if (el.hasAttribute("data-bind")) { (bindings[prop] || (bindings[prop] = [])).push(el); el.textContent = value ?? ""; } else if (el.hasAttribute("data-bind-this")) { ((_a = localBindings.get(state))[prop] || (_a[prop] = [])).push(el); el.textContent = value ?? ""; } else { (attrBindings[prop] || (attrBindings[prop] = [])).push({ el, attr }); boolAttrs.has(attr.toLowerCase()) ? el.toggleAttribute(attr, !!value) : el.setAttribute(attr, value ?? ""); if (attr === "value" && ["INPUT", "TEXTAREA"].includes(el.tagName)) el.addEventListener("input", () => state[prop] = el.value); } }); this.autoEvents(state, context); }; core_default.prototype.autoEvents = function(state, context = document) { context.querySelectorAll("[data-on]").forEach((el) => { var _a; const [event, method] = el.getAttribute("data-on")?.split(":"); if (!el.dataset.boundEvent && typeof state[method] === "function") { el.dataset.boundEvent = "true"; const handler = (e) => state[method]?.(e, state); el.addEventListener(event, handler); if (!localBindings.has(state)) { localBindings.set(state, {}); } ((_a = localBindings.get(state))[event] || (_a[event] = [])).push({ el, event, handler }); } }); context.querySelectorAll("[data-action]").forEach((el) => { let action = el.dataset.action; if (!el.dataset.boundAction) { el.dataset.boundAction = "true"; el.addEventListener("click", (e) => { if (typeof state[action] === "function") return state[action](e, state); let [globalName, globalMethod] = (action || "").split("."); if (typeof globalStates[globalName]?.[globalMethod] === "function") { globalStates[globalName][globalMethod](e, globalStates[globalName]); } }); } }); }; core_default.prototype.unbind = function(state) { Object.values(localBindings.get(state)).forEach( (bindings2) => ( // Remove each event listener bindings2.forEach(({ el, event, handler }) => el.removeEventListener(event, handler)) ) ); [bindings, attrBindings].forEach((obj) => Object.keys(state).forEach((prop) => delete obj[prop])); localBindings.delete(state); }; } }); // src/component.js var component_exports = {}; __export(component_exports, { default: () => component_default, startObserver: () => startObserver }); function startObserver() { document.readyState !== "loading" ? components.observer() : document.addEventListener("DOMContentLoaded", () => components.observer()); } var components, component_default; var init_component = __esm({ "src/component.js"() { init_core(); components = { // Registry for components registry: {}, /** * Start observing the DOM for dynamically added components. * * @returns {void} */ observer() { new MutationObserver((mutations) => { mutations.forEach(({ addedNodes }) => { addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE && this.registry[node.tagName.toLowerCase()]) this.mount(node, node.tagName.toLowerCase()); }); }); }).observe(document.body, { childList: true, subtree: true }); }, /** * Register a new component. * * @param {string} name - Component tag name. * @param {Function} template - Rendering function. * @param {Function | Object} [stateOrHandlers] - State function or event handlers. * @param {Function} [handlers] - Optional event handlers. */ register(name, template, stateOrHandlers, handlers) { if (!stateOrHandlers && !handlers) return; let state = () => $.state({}); let finalHandlers = () => { }; if (typeof stateOrHandlers === "function") { try { state = typeof stateOrHandlers() === "object" ? stateOrHandlers : state; } catch { finalHandlers = stateOrHandlers; } } if (typeof handlers === "function") finalHandlers = handlers; this.registry[name.toLowerCase()] = { template, state, handlers: finalHandlers }; document.querySelectorAll(name.toLowerCase()).forEach((el) => this.mount(el, name)); }, /** * Mount a component dynamically. * * @param {HTMLElement} el - Element to replace. * @param {string} name - Component name. */ mount(el, name) { const { template, state, handlers } = this.registry[name.toLowerCase()]; if (el.dataset.__mounted) return; el.dataset.__mounted = "true"; const props = Object.fromEntries([...el.attributes].map((attr) => [attr.name, attr.value])); const componentState = core_default.prototype.state({ ...state(), ...props }); const rendered = this.render(template({ state: componentState, props }), componentState); el.replaceWith(rendered); $(rendered).bind(componentState); rendered.querySelectorAll(Object.keys(this.registry).join(",")).forEach((child) => this.mount(child, child.tagName.toLowerCase())); this.bind(rendered, handlers, componentState); }, /** * Render an HTML string into a DOM element. * * @param {string} html - Component HTML. * @param {Object} [state={}] - Optional state to bind. * @returns {HTMLElement} - Rendered DOM element. */ render(html2, state = {}) { const container = document.createElement("div"); container.innerHTML = html2.trim(); const element2 = container.firstElementChild || null; if (!element2) return null; if (state) $(element2).bind(state); return element2; }, /** * Attach event handlers to a mounted component. * * @param {HTMLElement} rendered - Rendered component element. * @param {Function | Object} handlers - Event handlers. * @param {Object} componentState - Component state. */ bind(rendered, handlers, componentState) { if (typeof handlers === "function") return handlers($(rendered), componentState); Object.entries(handlers).forEach(([event, actions]) => { $(rendered).on(event, "[data-action]", (e) => { const action = e.target.dataset.action; if (actions[action]) { actions[action](componentState, e); $(rendered).bind(componentState); } }); }); } }; core_default.prototype.component = function(name, template, stateOrHandlers, handlers) { components.register(name, template, stateOrHandlers, handlers); }; core_default.prototype.render = components.render; startObserver(); component_default = components; } }); // src/template.js function unsafeHTML(value) { if (typeof console !== "undefined") console.warn("[AnJS] unsafeHTML() is an escape hatch \u2014 convert this call site to use html`...` instead."); return new UnsafeHTML(value); } function html(strings, ...values) { return new TemplateResult(strings, values); } function render(result, container) { if (!(result instanceof TemplateResult)) { throw new TypeError(`render() expects a TemplateResult, got ${Array.isArray(result) ? "Array" : typeof result}. Wrap with html\`...\` first.`); } let instance = templateCache.get(container); if (!instance || instance.strings !== result.strings) { const markup = result.strings.reduce((acc, str, i) => { if (i >= result.values.length) return acc + str; const marker = markerFor(i); if (/=\s*$/.test(str)) return acc + str + '"' + marker + '"'; return acc + str + marker; }, ""); const tpl = document.createElement("template"); tpl.innerHTML = markup; const parts = []; walkTemplate(tpl.content, parts); container.innerHTML = ""; container.appendChild(tpl.content.cloneNode(true)); const liveParts = resolveParts(parts, container); instance = { strings: result.strings, parts: liveParts, values: result.values.map(() => EMPTY) }; templateCache.set(container, instance); } commitValues(instance, result.values); } function markerFor(index) { return `${MARKER_PREFIX}${index}${MARKER_SUFFIX}`; } function walkTemplate(root, parts, path = []) { let childIndex = 0; for (let node = root.firstChild; node; node = node.nextSibling) { if (node.nodeType === 8) { const text = node.data.trim(); if (text.startsWith("anjs-")) { const index = parseInt(text.slice(5), 10); if (!isNaN(index)) { parts.push({ type: "node", index, path: [...path, childIndex] }); } } } else if (node.nodeType === 1) { const attrs = [...node.attributes]; for (const attr of attrs) { const markerRe = /<!--anjs-(\d+)-->/g; const segments = attr.value.split(markerRe); if (segments.length <= 1) continue; const statics = []; const indices = []; for (let s = 0; s < segments.length; s++) { if (s % 2 === 0) { statics.push(segments[s]); } else { indices.push(parseInt(segments[s], 10)); } } for (let k = 0; k < indices.length; k++) { parts.push({ type: "attr", index: indices[k], path: [...path, childIndex], name: attr.name, statics, attrIndices: indices, slotIndex: k }); } node.setAttribute(attr.name, statics.join("")); } walkTemplate(node, parts, [...path, childIndex]); } childIndex++; } } function resolveParts(parts, container) { return parts.map((part) => { let node = container; for (const idx of part.path) { node = node.childNodes[idx]; if (!node) return null; } if (part.type === "node") { const text = document.createTextNode(""); node.parentNode.replaceChild(text, node); return { type: "node", index: part.index, node: text }; } const resolved = { type: "attr", index: part.index, node, name: part.name }; if (part.statics) { resolved.statics = part.statics; resolved.attrIndices = part.attrIndices; resolved.slotIndex = part.slotIndex; } return resolved; }).filter(Boolean); } function commitValues(instance, newValues) { for (const part of instance.parts) { const newVal = newValues[part.index]; const oldVal = instance.values[part.index]; if (newVal === oldVal) continue; if (part.type === "node") { if (newVal instanceof TemplateResult) { if (!part._container) { part._container = document.createElement("span"); part._container.style.display = "contents"; part.node.parentNode.replaceChild(part._container, part.node); } render(newVal, part._container); } else if (newVal instanceof UnsafeHTML) { if (!part._container) { part._container = document.createElement("span"); part._container.style.display = "contents"; part.node.parentNode.replaceChild(part._container, part.node); } part._container.innerHTML = newVal.value; } else if (Array.isArray(newVal)) { commitArray(part, newVal); } else if (newVal == null || newVal === false) { if (part._container) { const text = document.createTextNode(""); part._container.parentNode.replaceChild(text, part._container); part.node = text; delete part._container; delete part._items; } else { part.node.data = ""; } } else { if (part._container) { const text = document.createTextNode(String(newVal)); part._container.parentNode.replaceChild(text, part._container); part.node = text; delete part._container; } else { part.node.data = String(newVal); } } } else if (part.type === "attr") { if (part.statics) { const statics = part.statics; const indices = part.attrIndices; if (indices.length === 1 && statics[0] === "" && statics[1] === "" && typeof newVal === "boolean") { if (newVal) { part.node.setAttribute(part.name, ""); } else { part.node.removeAttribute(part.name); } } else { let assembled = statics[0]; for (let k = 0; k < indices.length; k++) { const v = newValues[indices[k]]; assembled += v == null || v === false ? "" : String(v); assembled += statics[k + 1]; } part.node.setAttribute(part.name, assembled); } } else { if (newVal === true) { part.node.setAttribute(part.name, ""); } else if (newVal === false || newVal == null) { part.node.removeAttribute(part.name); } else { part.node.setAttribute(part.name, String(newVal)); } } } } instance.values = [...newValues]; } function commitArray(part, items) { if (!part._items) { part._items = []; part._keys = []; if (!part._container) { part._container = document.createElement("span"); part._container.style.display = "contents"; part.node.parentNode.replaceChild(part._container, part.node); } } const container = part._container; const existing = part._items; const oldKeys = part._keys; const isKeyed = items.length > 0 && items[0] && items[0]._key !== void 0; if (isKeyed) { const oldMap = /* @__PURE__ */ new Map(); for (let i = 0; i < oldKeys.length; i++) { oldMap.set(oldKeys[i], existing[i]); } const newSlots = []; const newKeys = []; for (let i = 0; i < items.length; i++) { const item = items[i]; const key = item._key; newKeys.push(key); const slot = oldMap.get(key); if (slot) { if (item instanceof TemplateResult) { render(item, slot); } else { slot.textContent = String(item ?? ""); } newSlots.push(slot); oldMap.delete(key); } else { const newSlot = document.createElement("span"); newSlot.style.display = "contents"; if (item instanceof TemplateResult) { render(item, newSlot); } else { newSlot.textContent = String(item ?? ""); } newSlots.push(newSlot); } } for (const orphan of oldMap.values()) { orphan.remove(); } for (let i = 0; i < newSlots.length; i++) { const slot = newSlots[i]; const current = container.childNodes[i]; if (current !== slot) { container.insertBefore(slot, current || null); } } part._items = newSlots; part._keys = newKeys; return; } for (let i = 0; i < items.length; i++) { const item = items[i]; if (i < existing.length) { if (item instanceof TemplateResult) { render(item, existing[i]); } else { existing[i].textContent = String(item ?? ""); } } else { const slot = document.createElement("span"); slot.style.display = "contents"; if (item instanceof TemplateResult) { render(item, slot); } else { slot.textContent = String(item ?? ""); } container.appendChild(slot); existing.push(slot); } } while (existing.length > items.length) { const removed = existing.pop(); removed.remove(); } } var templateCache, EMPTY, TemplateResult, UnsafeHTML, MARKER_PREFIX, MARKER_SUFFIX; var init_template = __esm({ "src/template.js"() { templateCache = /* @__PURE__ */ new WeakMap(); EMPTY = Symbol("empty"); TemplateResult = class { constructor(strings, values) { this.strings = strings; this.values = values; } }; UnsafeHTML = class { constructor(value) { this.value = String(value ?? ""); } }; MARKER_PREFIX = "<!--anjs-"; MARKER_SUFFIX = "-->"; } }); // src/morph.js function morph(target, newHTML) { const template = document.createElement("template"); template.innerHTML = newHTML; reconcile(target, target.childNodes, template.content.childNodes); } function reconcile(parent, oldNodes, newNodes) { const newLen = newNodes.length; for (let i = 0; i < newLen; i++) { const oldChild = oldNodes[i]; const newChild = newNodes[i]; if (!oldChild) { parent.appendChild(newChild.cloneNode(true)); continue; } if (oldChild.nodeType === newChild.nodeType) { if (oldChild.nodeType === 3 || oldChild.nodeType === 8) { if (oldChild.data !== newChild.data) { oldChild.data = newChild.data; } continue; } if (oldChild.nodeType === 1 && oldChild.tagName === newChild.tagName) { syncAttributes(oldChild, newChild); reconcile(oldChild, oldChild.childNodes, newChild.childNodes); continue; } } parent.replaceChild(newChild.cloneNode(true), oldChild); } while (parent.childNodes.length > newLen) { parent.removeChild(parent.lastChild); } } function syncAttributes(oldEl, newEl) { for (const { name, value } of newEl.attributes) { if (oldEl.getAttribute(name) !== value) { oldEl.setAttribute(name, value); } } for (const { name } of [...oldEl.attributes]) { if (!newEl.hasAttribute(name)) { oldEl.removeAttribute(name); } } const tag = oldEl.tagName; const isFocused = document.activeElement === oldEl; if (tag === "INPUT" || tag === "TEXTAREA") { if (oldEl.value !== newEl.value && !isFocused) { oldEl.value = newEl.value || ""; } if (oldEl.type === "checkbox" || oldEl.type === "radio") { if (oldEl.checked !== newEl.checked && !isFocused) { oldEl.checked = newEl.checked; } } } if (tag === "SELECT" && !isFocused) { if (oldEl.value !== newEl.value) { oldEl.value = newEl.value; } } } var init_morph = __esm({ "src/morph.js"() { } }); // src/repeat.js function repeat(items, keyFn, templateFn) { const results = []; let index = 0; for (const item of items) { const result = templateFn(item, index); if (result && typeof result === "object") { result._key = keyFn(item, index); } results.push(result); index++; } return results; } var init_repeat = __esm({ "src/repeat.js"() { } }); // src/element.js var element_exports = {}; __export(element_exports, { AnJSElement: () => AnJSElement, html: () => html, registerComponent: () => registerComponent, repeat: () => repeat, unsafeHTML: () => unsafeHTML }); var registerComponent, AnJSElement; var init_element = __esm({ "src/element.js"() { init_template(); init_morph(); init_repeat(); registerComponent = (name, ComponentClass) => { if (!customElements.get(name)) customElements.define(name, ComponentClass); }; AnJSElement = class _AnJSElement extends HTMLElement { // Define which attributes to observe (subclasses should override this) static get observedAttributes() { return []; } /** * Update scheduling strategy — override in subclasses * * @returns {string} 'microtask' (default, fastest) or 'raf' (frame-coalesced, for streaming data) */ static get updateStrategy() { return "microtask"; } // Constructor initializes state constructor() { super(); this._updatePending = false; this._initialized = false; this._computedDefs = /* @__PURE__ */ new Map(); this._disposers = []; const useRaf = this.constructor.updateStrategy === "raf"; this._schedule = useRaf ? (fn) => requestAnimationFrame(fn) : (fn) => queueMicrotask(fn); this._setupUpdatePromise(); this.state = new Proxy($.state({}), { // Intercept property changes set: (target, prop, value) => { target[prop] = value; this._recompute(prop); if (!this._updatePending) { this._updatePending = true; this._schedule(() => { if (this._updatePending) { this._updatePending = false; this.update(); } }); } return true; } }); } // Lifecycle: Called when element is added to the DOM connectedCallback() { this.constructor.observedAttributes.forEach((attr) => { this.state[attr] = this.getAttribute(attr) ?? ""; }); this.update(); } // Lifecycle: Called when element is removed from the DOM disconnectedCallback() { this._updatePending = false; this.destroy(); for (const dispose of this._disposers) dispose(); this._disposers.length = 0; } // Lifecycle: Called when an observed attribute changes attributeChangedCallback(name, oldValue, newValue) { if (this.state[name] !== newValue) { this.state[name] = newValue; } } /** * Define a computed property that auto-recalculates when dependencies change * * @param {string} name - Computed property name (set on this.state) * @param {string[]} deps - Array of state property names to watch * @param {Function} fn - Compute function, receives dependency values as arguments */ computed(name, deps, fn) { this._computedDefs.set(name, { deps, fn }); const values = deps.map((d) => this.state[d]); this.state[name] = fn(...values); } /** * Recalculate computed properties when a dependency changes * * @param {string} changedProp - The property that just changed * @private */ _recompute(changedProp) { if (this._computing) return; for (const [name, { deps, fn }] of this._computedDefs) { if (deps.includes(changedProp)) { const values = deps.map((d) => this.state[d]); const newVal = fn(...values); if (this.state[name] !== newVal) { this._computing = true; this.state[name] = newVal; this._computing = false; } } } } /** * Register a disposer function for automatic cleanup on disconnect * * @param {Function} disposer - Cleanup function (e.g., returned by $.listen or state.onChange) * @returns {Function} - The same disposer, for convenience */ own(disposer) { this._disposers.push(disposer); return disposer; } /** * Create a fresh updateComplete promise (internal) * @private */ _setupUpdatePromise() { this.updateComplete = new Promise((resolve) => { this._resolveUpdate = resolve; }); } // Update DOM based on the current state update() { this._updatePending = false; const isFirst = !this._initialized; const result = this.render(); if (result instanceof TemplateResult) { render(result, this); } else if (typeof result === "string" && this._initialized) { morph(this, result); } else if (typeof result === "string") { this.innerHTML = result; } this._initialized = true; $.bind(this.state, this); if (isFirst) this.init(); this.updated(); if (this._resolveUpdate) this._resolveUpdate(); this._setupUpdatePromise(); } /** * Lifecycle hook — called once after the first render completes. * Override in subclasses for one-time setup that needs the DOM. */ init() { if (this.setup !== _AnJSElement.prototype.setup) this.setup(); } /** * @deprecated Use init() instead. Retained for backward compatibility. */ setup() { } /** * Lifecycle hook — called after every render completes. * Override in subclasses for post-render side effects. */ updated() { } /** * Lifecycle hook — called when element is removed from the DOM, * before auto-cleanup runs. Override for custom teardown. */ destroy() { } // Default render method (override in subclasses) render() { return `<p>${this.constructor.name} is not implemented yet.</p>`; } }; } }); // src/request.js var request_exports = {}; __export(request_exports, { default: () => request_default, http: () => http, request: () => request }); function mergeHeaders(customHeaders = {}, hasBody) { const headers = { "Accept": "application/json", ...customHeaders }; if (hasBody && !headers["Content-Type"]) headers["Content-Type"] = "application/json"; return headers; } function processBody(data, headers) { if (data instanceof FormData) { delete headers["Content-Type"]; return data; } const isFormEncoded = headers["Content-Type"] === "application/x-www-form-urlencoded"; return isFormEncoded ? new URLSearchParams(data).toString() : JSON.stringify(data); } function withTimeout(fetchPromise, timeout, url) { if (timeout === 0 || timeout == null) return fetchPromise; return Promise.race([ // Fetch promise fetchPromise, // Timeout promise new Promise( (_, reject) => ( // Reject with timeout error setTimeout(() => reject(new Error(`Request timed out: ${url}`)), timeout) ) ) ]); } function request(url, method = "GET", data = null, options = {}) { const { timeout = 5e3, signal } = options; const urlObj = new URL(url, window.location.origin); const hasBody = data && !["GET", "DELETE"].includes(method); const headers = mergeHeaders(options.headers, hasBody); const body = hasBody ? processBody(data, headers) : void 0; const fetchOptions = { method, headers, ...body && { body }, ...signal && { signal } }; const fetchPromise = fetch(urlObj.toString(), fetchOptions).then((response) => { if (!response.ok) throw new Error(`HTTP ${response.status} at ${url}`); const contentType = response.headers?.get("Content-Type") || ""; return contentType.includes("application/json") ? response.json() : response.text(); }); return withTimeout(fetchPromise, timeout, url); } var http, request_default; var init_request = __esm({ "src/request.js"() { init_core(); core_default.prototype.request = function(url, method = "GET", data = null, options = {}) { return request(url, method, data, options); }; http = { // Read operations (safe, idempotent) head: (url, options = {}) => request(url, "HEAD", null, options), get: (url, options = {}) => request(url, "GET", null, options), options: (url, options = {}) => request(url, "OPTIONS", null, options), // Write operations (modifying data) post: (url, data, options = {}) => request(url, "POST", data, options), put: (url, data, options = {}) => request(url, "PUT", data, options), patch: (url, data, options = {}) => request(url, "PATCH", data, options), delete: (url, options = {}) => request(url, "DELETE", null, options), // Utilities abortController: () => new AbortController() }; request_default = http; } }); // src/animate.js var animate_exports = {}; var init_animate = __esm({ "src/animate.js"() { init_core(); Object.assign(core_default.prototype, { /** * Animate elements with CSS transitions. * * @param {Object} styles - CSS properties to animate. * @param {number} [duration=400] - Duration in milliseconds. * @param {string} [easing="ease"] - Easing function. * @returns {AnJS} - The current AnJS instance for chaining. */ animate(styles, duration = 400, easing = "ease") { return this.each((el) => { el.style.transition = `all ${duration}ms ${easing}`; Object.assign(el.style, styles); if (duration > 0) setTimeout(() => el.style.transition = "", duration); }); }, /** * Fade elements to a specific opacity. * * @param {number} [opacity] - Target opacity (0 to 1). * @param {number} [duration=400] - Duration in milliseconds. * @returns {AnJS} - The current AnJS instance for chaining. */ fade(opacity = +(this[0]?.style.opacity === "0"), duration = 400) { return this.animate({ opacity }, duration); }, /** * Fade elements in or out. * * @param {number} [duration=400] - Duration in milliseconds. * @return {AnJS} - The current AnJS instance for chaining. */ fadeIn(duration) { return this.fade(1, duration); }, /** * Fade elements out. * * @param {number} [duration=400] - Duration in milliseconds. * @return {AnJS} - The current AnJS instance for chaining. */ fadeOut(duration) { return this.fade(0, duration); } }); } }); // src/prebuilt.js init_core(); // src/dom.js init_core(); Object.assign(core_default.prototype, { /** * Get or set text or HTML content * * @param {string} [value] - Content to set. * @param {boolean} [html=false] - Whether to set/get as HTML (true) or text (false). * @returns {string | AnJS} - Content if getting, or self for chaining if setting. */ content(value, html2 = false) { if (value === void 0) { if (!this[0]) return ""; return html2 ? this[0].innerHTML : this[0].textContent; } return this.each((el) => html2 ? el.innerHTML = value : el.textContent = value); }, /** * Get or set text content * * @param {string} [value] - Text content to set. * @return {string | AnJS} - Text content if getting, or self for chaining if setting. */ text(value) { return this.content(value, false); }, /** * Get or set HTML content * * @param {string} [value] - HTML content to set. * @return {string | AnJS} - HTML content if getting, or self for chaining if setting. */ html(value) { return this.content(value, true); }, /** * Get or set CSS styles * * @param {string} name - CSS property name. * @param {string} [value] - CSS value to set. * @returns {string | AnJS} - CSS value if getting, or self for chaining if setting. */ css(name, value) { if (value === void 0) return this[0]?.style.getPropertyValue(name) || ""; return this.each((el) => el.style[name] = value); }, /** * Add, remove, or toggle a class * * @param {string} name - Class name. * @param {boolean} [add] - Add (true), Remove (false), Toggle (undefined). * @returns {AnJS} - Self for chaining. */ class(name, add) { return this.each((el) => el.classList[add === void 0 ? "toggle" : add ? "add" : "remove"](name)); }, /** * Show or hide elements * * @param {boolean} show - Show (true), Hide (false). * @returns {AnJS} - Self for chaining. */ display(show) { return this.each((el) => el.style.display = show ? "" : "none"); }, /** * Show or hide elements * * @returns {AnJS} - Self for chaining. */ hide() { return this.display(false); }, /** * Show elements * * @returns {AnJS} - Self for chaining. */ show() { return this.display(true); }, /** * Remove elements from the DOM * * @returns {AnJS} - Self for chaining. */ remove() { return this.each((el) => el.remove()); }, /** * Empty elements (remove all children) * * @returns {AnJS} - Self for chaining. */ empty() { return this.each((el) => el.innerHTML = ""); }, /** * Insert content relative to the selected element(s) * * @param {string | HTMLElement | HTMLElement[]} content - HTML string, element, or array of elements. * @param {string} position - 'before', 'prepend', 'append', 'after'. * @returns {AnJS} - Chainable instance. */ insert(content, position = "before") { const positions = { before: "beforeBegin", prepend: "afterBegin", append: "beforeEnd", after: "afterEnd" }; if (!positions[position]) return this; return this.each((target) => { if (typeof content === "string") return target.insertAdjacentHTML(positions[position], content); (Array.isArray(content) ? content : [content]).forEach( (el) => ( // Insert element at specified position target.insertAdjacentElement(positions[position], el.cloneNode(true)) ) ); }); }, /** * Get or set a property * * @param {string} name - Property name. * @param {any} value - Property value (optional). * @returns {any | AnJS} - Property value if getting, or self for chaining if setting. */ prop(name, value) { if (arguments.length === 1) return this[0]?.[name]; return this.each((el) => el[name] = value); }, /** * Get or set the value of form elements * * @param {string} [value] - The value to set (if provided). * @returns {string | AnJS} - The current value if getting, or the AnJS instance if setting. */ val(value) { if (arguments.length === 0) return this[0]?.value; return this.each((el) => el.value = value); }, /** * Check if the first selected element has a class * * @param {string} className - Class name. * @returns {boolean} - True if the class exists, otherwise false. */ has(className) { return this[0]?.classList.contains(className) ?? false; }, /** * Focus on the first selected element * * @returns {AnJS} - Self for chaining. */ focus() { this[0]?.focus(); return this; }, /** * Remove focus from the first selected element * * @returns {AnJS} - Self for chaining. */ blur() { this[0]?.blur(); return this; } }); // src/attributes.js init_core(); Object.assign(core_default.prototype, { /** * Get or set an attribute on selected elements * * @param {string} name - Attribute name. * @param {string} [value] - Attribute value (if setting). * @returns {string | AnJS} - Attribute value if getting, AnJS instance if setting. */ attr(name, value) { if (value === void 0) return this[0]?.getAttribute(name); if (value === null) return this.each((el) => el.removeAttribute(name)); return this.each((el) => el.setAttribute(name, value)); }, /** * Get or set the id attribute * * @param {string} [value] - The id to set. * @returns {string | AnJS} - The id if getting, otherwise chainable. */ id(value) { return value === void 0 ? this.attr("id") : this.attr("id", value); }, /** * Remove an attribute from selected elements * * @param {string} name - Attribute name to remove. * @returns {AnJS} - Returns self for chaining. */ removeAttr(name) { return this.attr(name, null); }, /** * Serialize form data from the first selected element * * @returns {string} - URL-encoded form data string or an empty