UNPKG

simplyflow

Version:

Flow based programming in javascript, with signals and effects

1,265 lines (1,260 loc) 41 kB
(() => { var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/state.mjs var state_exports = {}; __export(state_exports, { batch: () => batch, clockEffect: () => clockEffect, destroy: () => destroy, effect: () => effect, signal: () => signal, throttledEffect: () => throttledEffect, trace: () => trace, untracked: () => untracked }); var iterate = Symbol("iterate"); if (!Symbol.xRay) { Symbol.xRay = Symbol("xRay"); } if (!Symbol.Signal) { Symbol.Signal = Symbol("Signal"); } var signalHandler = { get: (target, property, receiver) => { if (property === Symbol.xRay) { return target; } if (property === Symbol.Signal) { return true; } const value = target?.[property]; notifyGet(receiver, property); if (typeof value === "function") { if (Array.isArray(target)) { return (...args) => { let l = target.length; let result = value.apply(receiver, args); if (l != target.length) { notifySet(receiver, makeContext("length", { was: l, now: target.length })); } return result; }; } else if (target instanceof Set || target instanceof Map) { return (...args) => { let s = target.size; let result = value.apply(target, args); if (s != target.size) { notifySet(receiver, makeContext("size", { was: s, now: target.size })); } if (["set", "add", "clear", "delete"].includes(property)) { notifySet(receiver, makeContext({ entries: {}, forEach: {}, has: {}, keys: {}, values: {}, [Symbol.iterator]: {} })); } return result; }; } else if (target instanceof HTMLElement || target instanceof Number || target instanceof String || target instanceof Boolean) { return value.bind(target); } else { return value.bind(receiver); } } if (value && typeof value == "object") { return signal(value); } return value; }, set: (target, property, value, receiver) => { value = value?.[Symbol.xRay] || value; let current = target[property]; if (current !== value) { target[property] = value; notifySet(receiver, makeContext(property, { was: current, now: value })); } if (typeof current === "undefined") { notifySet(receiver, makeContext(iterate, {})); } return true; }, has: (target, property) => { let receiver = signals.get(target); if (receiver) { notifyGet(receiver, property); } return Object.hasOwn(target, property); }, deleteProperty: (target, property) => { if (typeof target[property] !== "undefined") { let current = target[property]; delete target[property]; let receiver = signals.get(target); notifySet(receiver, makeContext(property, { delete: true, was: current })); } return true; }, defineProperty: (target, property, descriptor) => { if (typeof target[property] === "undefined") { let receiver = signals.get(target); notifySet(receiver, makeContext(iterate, {})); } return Object.defineProperty(target, property, descriptor); }, ownKeys: (target) => { let receiver = signals.get(target); notifyGet(receiver, iterate); return Reflect.ownKeys(target); } }; var signals = /* @__PURE__ */ new WeakMap(); function signal(v) { if (v[Symbol.Signal]) { let target = v[Symbol.xRay]; if (!signals.has(target)) { signals.set(target, v); } v = target; } else if (!signals.has(v)) { signals.set(v, new Proxy(v, signalHandler)); } return signals.get(v); } function trace(signal2, prop) { const listeners = getListeners(signal2, prop); return listeners.map((listener) => { return { effect: listener.effectType, fn: listener.effectFunction, signal: signals.get(listener.effectFunction) }; }); } var batchedListeners = /* @__PURE__ */ new Set(); var batchMode = 0; function notifySet(self, context = {}) { let listeners = []; context.forEach((change, property) => { let propListeners = getListeners(self, property); if (propListeners?.length) { for (let listener of propListeners) { addContext(listener, makeContext(property, change)); } listeners = listeners.concat(propListeners); } }); listeners = new Set(listeners.filter(Boolean)); if (listeners) { if (batchMode) { batchedListeners = batchedListeners.union(listeners); } else { const currentEffect = computeStack[computeStack.length - 1]; for (let listener of Array.from(listeners)) { if (listener != currentEffect && listener?.needsUpdate) { listener(); } clearContext(listener); } } } } function makeContext(property, change) { let context = /* @__PURE__ */ new Map(); if (typeof property === "object") { for (let prop in property) { context.set(prop, property[prop]); } } else { context.set(property, change); } return context; } function addContext(listener, context) { if (!listener.context) { listener.context = context; } else { context.forEach((change, property) => { listener.context.set(property, change); }); } listener.needsUpdate = true; } function clearContext(listener) { delete listener.context; delete listener.needsUpdate; } function notifyGet(self, property) { let currentCompute = computeStack[computeStack.length - 1]; if (currentCompute) { setListeners(self, property, currentCompute); } } var listenersMap = /* @__PURE__ */ new WeakMap(); var computeMap = /* @__PURE__ */ new WeakMap(); function getListeners(self, property) { let listeners = listenersMap.get(self); return listeners ? Array.from(listeners.get(property) || []) : []; } function setListeners(self, property, compute) { if (!listenersMap.has(self)) { listenersMap.set(self, /* @__PURE__ */ new Map()); } let listeners = listenersMap.get(self); if (!listeners.has(property)) { listeners.set(property, /* @__PURE__ */ new Set()); } listeners.get(property).add(compute); if (!computeMap.has(compute)) { computeMap.set(compute, /* @__PURE__ */ new Map()); } let connectedSignals = computeMap.get(compute); if (!connectedSignals.has(property)) { connectedSignals.set(property, /* @__PURE__ */ new Set()); } connectedSignals.get(property).add(self); } function clearListeners(compute) { let connectedSignals = computeMap.get(compute); if (connectedSignals) { connectedSignals.forEach((property) => { property.forEach((s) => { let listeners = listenersMap.get(s); if (listeners.has(property)) { listeners.get(property).delete(compute); } }); }); } } var computeStack = []; var effectStack = []; var effectMap = /* @__PURE__ */ new WeakMap(); var signalStack = []; function effect(fn) { if (effectStack.findIndex((f) => fn == f) !== -1) { throw new Error("Recursive update() call", { cause: fn }); } effectStack.push(fn); let connectedSignal = signals.get(fn); if (!connectedSignal) { connectedSignal = signal({ current: null }); signals.set(fn, connectedSignal); } const computeEffect = function computeEffect2() { if (signalStack.findIndex((s) => s == connectedSignal) !== -1) { throw new Error("Cyclical dependency in update() call", { cause: fn }); } clearListeners(computeEffect2); computeEffect2.effectFunction = fn; computeEffect2.effectType = effect; computeStack.push(computeEffect2); signalStack.push(connectedSignal); let result; try { result = fn(computeEffect2, computeStack, signalStack); } finally { computeStack.pop(); signalStack.pop(); if (result instanceof Promise) { result.then((result2) => { connectedSignal.current = result2; }); } else { connectedSignal.current = result; } } }; computeEffect.fn = fn; effectMap.set(connectedSignal, computeEffect); computeEffect(); return connectedSignal; } function destroy(connectedSignal) { const computeEffect = effectMap.get(connectedSignal)?.deref(); if (!computeEffect) { return; } clearListeners(computeEffect); let fn = computeEffect.fn; signals.remove(fn); effectMap.delete(connectedSignal); } function batch(fn) { batchMode++; let result; try { result = fn(); } finally { if (result instanceof Promise) { result.then(() => { batchMode--; if (!batchMode) { runBatchedListeners(); } }); } else { batchMode--; if (!batchMode) { runBatchedListeners(); } } } return result; } function runBatchedListeners() { let copyBatchedListeners = Array.from(batchedListeners); batchedListeners = /* @__PURE__ */ new Set(); const currentEffect = computeStack[computeStack.length - 1]; for (let listener of copyBatchedListeners) { if (listener != currentEffect && listener?.needsUpdate) { listener(); } clearContext(listener); } } function throttledEffect(fn, throttleTime) { if (effectStack.findIndex((f) => fn == f) !== -1) { throw new Error("Recursive update() call", { cause: fn }); } effectStack.push(fn); let connectedSignal = signals.get(fn); if (!connectedSignal) { connectedSignal = signal({ current: null }); signals.set(fn, connectedSignal); } let throttled = false; let hasChange = true; const computeEffect = function computeEffect2() { if (signalStack.findIndex((s) => s == connectedSignal) !== -1) { throw new Error("Cyclical dependency in update() call", { cause: fn }); } if (throttled && throttled > Date.now()) { hasChange = true; return; } clearListeners(computeEffect2); computeEffect2.effectFunction = fn; computeEffect2.effectType = throttledEffect; computeStack.push(computeEffect2); signalStack.push(connectedSignal); let result; try { result = fn(computeEffect2, computeStack, signalStack); } finally { hasChange = false; computeStack.pop(); signalStack.pop(); if (result instanceof Promise) { result.then((result2) => { connectedSignal.current = result2; }); } else { connectedSignal.current = result; } } throttled = Date.now() + throttleTime; globalThis.setTimeout(() => { if (hasChange) { computeEffect2(); } }, throttleTime); }; computeEffect(); return connectedSignal; } function clockEffect(fn, clock) { let connectedSignal = signals.get(fn); if (!connectedSignal) { connectedSignal = signal({ current: null }); signals.set(fn, connectedSignal); } let lastTick = -1; let hasChanged = true; const computeEffect = function computeEffect2() { if (lastTick < clock.time) { if (hasChanged) { clearListeners(computeEffect2); computeEffect2.effectFunction = fn; computeEffect2.effectType = clockEffect; computeStack.push(computeEffect2); lastTick = clock.time; let result; try { result = fn(computeEffect2, computeStack); } finally { computeStack.pop(); if (result instanceof Promise) { result.then((result2) => { connectedSignal.current = result2; }); } else { connectedSignal.current = result; } hasChanged = false; } } else { lastTick = clock.time; } } else { hasChanged = true; } }; computeEffect(); return connectedSignal; } function untracked(fn) { const remember = computeStack.slice(); computeStack = []; try { return fn(); } finally { computeStack = remember; } } // src/bind.mjs var SimplyBind = class { /** * @param Object options - a set of options for this instance, options may include: * - root (signal) (required) - the root data object that contains al signals that can be bound * - container (HTMLElement) - the dom element to use as the root for all bindings * - attribute (string) - the prefix for the field, list and map attributes, e.g. 'data-bind' * - transformers (object name:function) - a map of transformer names and functions * - defaultTransformers (object with field, list and map properties) */ constructor(options) { this.bindings = /* @__PURE__ */ new Map(); const defaultOptions = { container: document.body, attribute: "data-bind", transformers: {}, defaultTransformers: { field: [defaultFieldTransformer], list: [defaultListTransformer], map: [defaultMapTransformer] } }; if (!options?.root) { throw new Error("bind needs at least options.root set"); } this.options = Object.assign({}, defaultOptions, options); const attribute = this.options.attribute; const bindAttributes = [attribute + "-field", attribute + "-list", attribute + "-map"]; const bindSelector = `[${attribute}-field],[${attribute}-list],[${attribute}-map]`; const transformAttribute = attribute + "-transform"; const getBindingAttribute = (el) => { const foundAttribute = bindAttributes.find((attr2) => el.hasAttribute(attr2)); if (!foundAttribute) { console.error("No matching attribute found", el, attr); } return foundAttribute; }; const render = (el) => { this.bindings.set(el, throttledEffect(() => { if (!el.isConnected) { untrack(el, this.getBindingPath(el)); destroy(this.bindings.get(el)); return; } const context = { templates: el.querySelectorAll(":scope > template"), attribute: getBindingAttribute(el) }; context.path = this.getBindingPath(el); context.value = getValueByPath(this.options.root, context.path); context.element = el; track(el, context); runTransformers(context); }, 50)); }; const runTransformers = (context) => { let transformers; switch (context.attribute) { case this.options.attribute + "-field": transformers = this.options.defaultTransformers.field || []; break; case this.options.attribute + "-list": transformers = this.options.defaultTransformers.list || []; break; case this.options.attribute + "-map": transformers = this.options.defaultTransformers.map || []; break; } if (context.element.hasAttribute(transformAttribute)) { context.element.getAttribute(transformAttribute).split(" ").filter(Boolean).forEach((t) => { if (this.options.transformers[t]) { transformers.push(this.options.transformers[t]); } else { console.warn("No transformer with name " + t + " configured", { cause: context.element }); } }); } let next; for (let transformer of transformers) { next = /* @__PURE__ */ ((next2, transformer2) => { return (context2) => { return transformer2.call(this, context2, next2); }; })(next, transformer); } next(context); }; const applyBindings = (bindings2) => { for (let bindingEl of bindings2) { if (!this.bindings.get(bindingEl)) { render(bindingEl); } } }; const updateBindings = (changes) => { const selector = `[${attribute}-field],[${attribute}-list],[${attribute}-map]`; for (const change of changes) { if (change.type == "childList" && change.addedNodes) { for (let node of change.addedNodes) { if (node instanceof HTMLElement) { let bindings2 = Array.from(node.querySelectorAll(selector)); if (node.matches(selector)) { bindings2.unshift(node); } if (bindings2.length) { applyBindings(bindings2); } } } } } }; this.observer = new MutationObserver((changes) => { updateBindings(changes); }); this.observer.observe(this.options.container, { subtree: true, childList: true }); const bindings = this.options.container.querySelectorAll( ":is([" + this.options.attribute + "-field],[" + this.options.attribute + "-list],[" + this.options.attribute + "-map]):not(template)" ); if (bindings.length) { applyBindings(bindings); } } /** * Finds the first matching template and creates a new DocumentFragment * with the correct data bind attributes in it (prepends the current path) * @param Context context * @return DocumentFragment */ applyTemplate(context) { const path = context.path; const templates = context.templates; const list = context.list; const index = context.index; const parent = context.parent; const value = list ? list[index] : context.value; let template = this.findTemplate(templates, value); if (!template) { let result = new DocumentFragment(); result.innerHTML = "<!-- no matching template -->"; return result; } let clone = template.content.cloneNode(true); if (!clone.children?.length) { return clone; } if (clone.children.length > 1) { throw new Error("template must contain a single root node", { cause: template }); } const attribute = this.options.attribute; const attributes = [attribute + "-field", attribute + "-list", attribute + "-map"]; const bindings = clone.querySelectorAll(`[${attribute}-field],[${attribute}-list],[${attribute}-map]`); for (let binding of bindings) { const attr2 = attributes.find((attr3) => binding.hasAttribute(attr3)); const bind2 = binding.getAttribute(attr2); if (bind2.substring(0, ":root.".length) == ":root.") { binding.setAttribute(attr2, bind2.substring(":root.".length)); } else if (bind2 == ":value" && index != null) { binding.setAttribute(attr2, path + "." + index); } else if (index != null) { binding.setAttribute(attr2, path + "." + index + "." + bind2); } else { binding.setAttribute(attr2, parent + "." + bind2); } } if (typeof index !== "undefined") { clone.children[0].setAttribute(attribute + "-key", index); } Object.defineProperty( clone.children[0], "$bindTemplate", { value: template, enumerable: false, writable: true, configurable: true } ); return clone; } /** * Returns the path referenced in either the field, list or map attribute * @param HTMLElement el * @return string The path referenced, or void */ getBindingPath(el) { const attributes = [ this.options.attribute + "-field", this.options.attribute + "-list", this.options.attribute + "-map" ]; for (let attr2 of attributes) { if (el.hasAttribute(attr2)) { return el.getAttribute(attr2); } } } /** * Finds the first template from an array of templates that * matches the given value. */ findTemplate(templates, value) { const templateMatches = (t) => { let path = this.getBindingPath(t); let currentItem; if (path) { if (path.substr(0, 6) == ":root.") { currentItem = getValueByPath(this.options.root, path); } else { currentItem = getValueByPath(value, path); } } else { currentItem = value; } const strItem = "" + currentItem; let matches = t.getAttribute(this.options.attribute + "-match"); if (matches) { if (matches === ":empty" && !currentItem) { return t; } else if (matches === ":notempty" && currentItem) { return t; } if (strItem.match(matches)) { return t; } } if (!matches && currentItem !== null && currentItem !== void 0) { return t; } }; let template = Array.from(templates).find(templateMatches); let rel = template?.getAttribute("rel"); if (rel) { let replacement = document.querySelector("template#" + rel); if (!replacement) { throw new Error("Could not find template with id " + rel); } template = replacement; } return template; } destroy() { this.bindings.forEach((binding) => { destroy(binding); }); this.bindings = /* @__PURE__ */ new Map(); this.observer.disconnect(); } }; function bind(options) { return new SimplyBind(options); } var tracking = /* @__PURE__ */ new Map(); function track(el, context) { if (!tracking.has(context.path)) { tracking.set(context.path, [context]); } else { tracking.get(context.path).push(context); } } function untrack(el, path) { let list = tracking.get(path); list = list.filter((context) => context.element == el); tracking.set(path, list); } function matchValue(a, b) { if (a == ":empty" && !b) { return true; } if (b == ":empty" && !a) { return true; } if ("" + a == "" + b) { return true; } return false; } function getValueByPath(root, path) { let parts = path.split("."); let curr = root; let part, prevPart; while (parts.length && curr) { part = parts.shift(); if (part == ":key") { return prevPart; } else if (part == ":value") { return curr; } else if (part == ":root") { curr = root; } else { part = decodeURIComponent(part); curr = curr[part]; prevPart = part; } } return curr; } function defaultFieldTransformer(context) { const el = context.element; const templates = context.templates; const templatesCount = templates.length; const path = context.path; const value = context.value; const attribute = this.options.attribute; if (templates?.length) { transformLiteralByTemplates.call(this, context); } else { switch (el.tagName) { case "INPUT": transformInput.call(this, context); break; case "BUTTON": transformButton.call(this, context); break; case "SELECT": transformSelect.call(this, context); break; case "A": transformAnchor.call(this, context); break; case "IMG": transformImage.call(this, contet); break; case "IFRAME": transformIframe.call(this, context); break; case "META": transformMeta.call(this, context); break; case "TEMPLATE": break; default: transformElement.call(this, context); break; } } return context; } function defaultListTransformer(context) { const el = context.element; const templates = context.templates; const templatesCount = templates.length; const path = context.path; const value = context.value; const attribute = this.options.attribute; if (!Array.isArray(value)) { console.error("Value is not an array.", el, path, value); } else if (!templates?.length) { console.error("No templates found in", el); } else { transformArrayByTemplates.call(this, context); } return context; } function defaultMapTransformer(context) { const el = context.element; const templates = context.templates; const templatesCount = templates.length; const path = context.path; const value = context.value; const attribute = this.options.attribute; if (typeof value != "object") { console.error("Value is not an object.", el, path, value); } else if (!templates?.length) { console.error("No templates found in", el); } else { transformObjectByTemplates.call(this, context); } return context; } function transformArrayByTemplates(context) { const el = context.element; const templates = context.templates; const templatesCount = templates.length; const path = context.path; const value = context.value; const attribute = this.options.attribute; let items = el.querySelectorAll(":scope > [" + attribute + "-key]"); let lastKey = 0; let skipped = 0; context.list = value; for (let item of items) { let currentKey = parseInt(item.getAttribute(attribute + "-key")); if (currentKey > lastKey) { context.index = lastKey; el.insertBefore(this.applyTemplate(context), item); } else if (currentKey < lastKey) { item.remove(); } else { let bindings = Array.from(item.querySelectorAll(`[${attribute}]`)); if (item.matches(`[${attribute}]`)) { bindings.unshift(item); } let needsReplacement = bindings.find((b) => { let databind = b.getAttribute(attribute); return databind.substr(0, 5) !== ":root" && databind.substr(0, path.length) !== path; }); if (!needsReplacement) { if (item.$bindTemplate) { let newTemplate = this.findTemplate(templates, value[lastKey]); if (newTemplate != item.$bindTemplate) { needsReplacement = true; if (!newTemplate) { skipped++; } } } } if (needsReplacement) { context.index = lastKey; el.replaceChild(this.applyTemplate(context), item); } } lastKey++; if (lastKey >= value.length) { break; } } items = el.querySelectorAll(":scope > [" + attribute + "-key]"); let length = items.length + skipped; if (length > value.length) { while (length > value.length) { let child = el.querySelectorAll(":scope > :not(template)")?.[length - 1]; child?.remove(); length--; } } else if (length < value.length) { while (length < value.length) { context.index = length; el.appendChild(this.applyTemplate(context)); length++; } } } function transformObjectByTemplates(context) { const el = context.element; const templates = context.templates; const templatesCount = templates.length; const path = context.path; const value = context.value; const attribute = this.options.attribute; context.list = value; let items = Array.from(el.querySelectorAll(":scope > [" + attribute + "-key]")); for (let key in context.list) { context.index = key; let item = items.shift(); if (!item) { let clone = this.applyTemplate(context); el.appendChild(clone); continue; } if (item.getAttribute[attribute + "-key"] != key) { items.unshift(item); let outOfOrderItem = el.querySelector(":scope > [" + attribute + '-key="' + key + '"]'); if (!outOfOrderItem) { let clone = this.applyTemplate(context); el.insertBefore(clone, item); continue; } else { el.insertBefore(outOfOrderItem, item); item = outOfOrderItem; items = items.filter((i) => i != outOfOrderItem); } } let newTemplate = this.findTemplate(templates, value[key]); if (newTemplate != item.$bindTemplate) { let clone = this.applyTemplate(context); el.replaceChild(clone, item); } } while (items.length) { let item = items.shift(); item.remove(); } } function getParentPath(el, attribute) { const parentEl = el.parentElement?.closest(`[${attribute}-list],[${attribute}-map]`); if (!parentEl) { return ":root"; } if (parentEl.hasAttribute(`${attribute}-list`)) { return parentEl.getAttribute(`${attribute}-list`); } return parentEl.getAttribute(`${attribute}-map`); } function transformLiteralByTemplates(context) { const el = context.element; const templates = context.templates; const value = context.value; const attribute = this.options.attribute; const rendered = el.querySelector(":scope > :not(template)"); const template = this.findTemplate(templates, value); context.parent = getParentPath(el, attribute); if (rendered) { if (template) { if (rendered?.$bindTemplate != template) { const clone = this.applyTemplate(context); el.replaceChild(clone, rendered); } } else { el.removeChild(rendered); } } else if (template) { const clone = this.applyTemplate(context); el.appendChild(clone); } } function transformInput(context) { const el = context.element; let value = context.value; transformElement(context); if (typeof value == "undefined") { value = ""; } if (el.type == "checkbox" || el.type == "radio") { if (matchValue(el.value, value)) { el.checked = true; } else { el.checked = false; } } else if (!matchValue(el.value, value)) { el.value = "" + value; } } function transformButton(context) { const el = context.element; const value = context.value; transformElement(context); setProperties(el, value, "value"); } function transformSelect(context) { const el = context.element; let value = context.value; if (value === null) { value = ""; } if (typeof value != "object") { if (el.multiple) { if (Array.isArray(value)) { for (let option of el.options) { if (value.indexOf(option.value) === false) { option.selected = false; } else { option.selected = true; } } } } else { let option = el.options.find((o) => matchValue(o.value, value)); if (option) { option.selected = true; option.setAttribute("selected", true); } } } else { if (value.options) { setSelectOptions(el, value.options); } if (value.selected) { transformSelect(Object.asssign({}, context, { value: value.selected })); } setProperties(el, value, "name", "id", "selectedIndex", "className"); } } function addOption(select, option) { if (!option) { return; } if (typeof option !== "object") { select.options.add(new Option("" + option)); } else if (option.text) { select.options.add(new Option(option.text, option.value, option.defaultSelected, option.selected)); } else if (typeof option.value != "undefined") { select.options.add(new Option("" + option.value, option.value, option.defaultSelected, option.selected)); } } function setSelectOptions(select, options) { select.innerHTML = ""; if (Array.isArray(options)) { for (const option of options) { addOption(select, option); } } else if (options && typeof options == "object") { for (const option in options) { addOption(select, { text: options[option], value: option }); } } } function transformAnchor(context) { const el = context.element; const value = context.value; transformElement(context); setProperties(el, value, "title", "target", "href", "name", "newwindow", "nofollow"); } function transformImage(context) { const el = context.element; const value = context.value; transformElement(context); setProperties(el, value, "title", "alt", "src"); } function transformIframe(context) { const el = context.element; const value = context.value; transformElement(context); setProperties(el, value, "title", "src"); } function transformMeta(context) { const el = context.element; const value = context.value; transformElement(context); setProperties(el, value, "content"); } function transformElement(context) { const el = context.element; let value = context.value; if (typeof value == "undefined" || value == null) { value = ""; } let strValue = "" + value; if (typeof value != "object" || strValue.substring(0, 8) != "[object ") { el.innerHTML = strValue; return; } setProperties(el, value, "innerHTML", "title", "id", "className"); } function setProperties(el, data, ...properties) { if (!data || typeof data !== "object") { return; } for (const property of properties) { if (typeof data[property] === "undefined") { continue; } if (matchValue(el[property], data[property])) { continue; } if (data[property] === null) { el[property] = ""; } else { el[property] = "" + data[property]; } } } // src/model.mjs var model_exports = {}; __export(model_exports, { columns: () => columns, filter: () => filter, model: () => model, paging: () => paging, scroll: () => scroll, sort: () => sort }); var SimplyFlowModel = class { /** * Creates a new datamodel, with a state property that contains * all the data passed to this constructor * @param state Object with all the data for this model */ constructor(state) { this.state = signal(state); if (!this.state.options) { this.state.options = {}; } this.effects = [{ current: state.data }]; this.view = signal(state.data); } /** * Adds an effect to run whenever a signal it depends on * changes. this.state is the usual signal. * The `fn` function param is not itself an effect, but must return * and effect function. `fn` takes one param, which is the data signal. * This signal will always have at least a `current` property. * The result of the effect function is pushed on to the this.effects * list. And the last effect added is set as this.view */ addEffect(fn) { const dataSignal = this.effects[this.effects.length - 1]; this.view = fn.call(this, dataSignal); this.effects.push(this.view); } }; function model(options) { return new SimplyFlowModel(options); } function sort(options = {}) { return function(data) { this.state.options.sort = Object.assign({ direction: "asc", sortBy: null, sortFn: (a, b) => { const sort2 = this.state.options.sort; const sortBy = sort2.sortBy; if (!sort2.sortBy) { return 0; } const larger = sort2.direction == "asc" ? 1 : -1; const smaller = sort2.direction == "asc" ? -1 : 1; if (typeof a?.[sortBy] === "undefined") { if (typeof b?.[sortBy] === "undefined") { return 0; } return larger; } if (typeof b?.[sortBy] === "undefined") { return smaller; } if (a[sortBy] < b[sortBy]) { return smaller; } else if (a[sortBy] > b[sortBy]) { return larger; } else { return 0; } } }, options); return throttledEffect(() => { const sort2 = this.state.options.sort; if (sort2?.sortBy && sort2?.direction) { return data.current.toSorted(sort2?.sortFn); } return data.current; }, 50); }; } function paging(options = {}) { return function(data) { this.state.options.paging = Object.assign({ page: 1, pageSize: 20, max: 1 }, options); return throttledEffect(() => { return batch(() => { const paging2 = this.state.options.paging; if (!paging2.pageSize) { paging2.pageSize = 20; } paging2.max = Math.ceil(this.state.data.length / paging2.pageSize); paging2.page = Math.max(1, Math.min(paging2.max, paging2.page)); const start = (paging2.page - 1) * paging2.pageSize; const end = start + paging2.pageSize; return data.current.slice(start, end); }); }, 50); }; } function filter(options) { if (!options?.name || typeof options.name !== "string") { throw new Error("filter requires options.name to be a string"); } if (!options.matches || typeof options.matches !== "function") { throw new Error("filter requires options.matches to be a function"); } return function(data) { if (this.state.options[options.name]) { throw new Error("a filter with this name already exists on this model"); } this.state.options[options.name] = options; return throttledEffect(() => { if (this.state.options[options.name].enabled) { return data.current.filter(this.state.options[options.name].matches.bind(this)); } return data.current; }, 50); }; } function columns(options = {}) { if (!options || typeof options !== "object" || Object.keys(options).length === 0) { throw new Error("columns requires options to be an object with at least one property"); } return function(data) { this.state.options.columns = options; return throttledEffect(() => { return data.current.map((input) => { let result = {}; for (let key of Object.keys(this.state.options.columns)) { if (!this.state.options.columns[key]?.hidden) { result[key] = input[key]; } } return result; }); }, 50); }; } function scroll(options) { return function(data) { this.state.options.scroll = Object.assign({ offset: 0, rowHeight: 26, rowCount: 20, itemsPerRow: 1, size: data.current.length }, options); const scrollOptions = this.state.options.scroll; const scrollbar = scrollOptions.scrollbar || scrollOptions.container?.querySelector("[data-flow-scrollbar]"); if (scrollbar) { if (scrollOptions.container) { scrollOptions.container.addEventListener("scroll", (evt) => { scrollOptions.offset = Math.floor( scrollOptions.container.scrollTop / (scrollOptions.rowHeight * scrollOptions.itemsPerRow) ); }); } throttledEffect(() => { scrollOptions.size = data.current.length * scrollOptions.rowHeight; scrollbar.style.height = scrollOptions.size + "px"; }, 50); } return throttledEffect(() => { if (scrollOptions.container) { scrollOptions.rowCount = Math.ceil( scrollOptions.container.getBoundingClientRect().height / scrollOptions.rowHeight ); } scrollOptions.data = data.current; let start = Math.min(scrollOptions.offset, data.current.length - 1); let end = start + scrollOptions.rowCount; if (end > data.current.length) { end = data.current.length; start = end - scrollOptions.rowCount; } return data.current.slice(start, end); }, 50); }; } // src/flow.mjs if (!window.simply) { window.simply = {}; } Object.assign(window.simply, { bind, flow: model_exports, state: state_exports }); var flow_default = window.simply; })();