| | |
| | | return false; |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | /** |
| | | * Load and instantiate HTML templates as lightweight components |
| | | */ |
| | | class TemplateRegistry { |
| | | constructor() { |
| | | this.templates = new Map(); // name -> <template> |
| | | this.definitions = new Map(); // name -> component definition |
| | | } |
| | | |
| | | /** |
| | | * Collect all <template class="name"> elements |
| | | */ |
| | | registerAll(root = document) { |
| | | root.querySelectorAll('template').forEach(tpl => { |
| | | tpl.classList.forEach(name => { |
| | | if (!this.templates.has(name)) { |
| | | this.templates.set(name, tpl); |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Define component behavior |
| | | */ |
| | | define(name, definition = {}, context = null) { |
| | | this.definitions.set(name, { |
| | | refs: definition.refs || null, |
| | | manyRefs: definition.manyRefs || null, |
| | | setup: definition.setup || null, |
| | | context: context |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Create a component instance |
| | | */ |
| | | create(name, data = {}) { |
| | | const tpl = this.templates.get(name); |
| | | if (!tpl) { |
| | | console.warn(`[TemplateRegistry] Template "${name}" not found`); |
| | | return null; |
| | | } |
| | | |
| | | const element = tpl.content.cloneNode(true).firstElementChild; |
| | | if (!element) return null; |
| | | |
| | | const def = this.definitions.get(name); |
| | | const refs = def?.refs |
| | | ? this.#collectRefs(element, def.refs) |
| | | : {}; |
| | | const manyRefs = def?.manyRefs |
| | | ? this.#collectRefs(element, def.manyRefs, false) |
| | | : {}; |
| | | |
| | | def?.setup?.({ |
| | | el: element, |
| | | refs, |
| | | manyRefs, |
| | | data |
| | | }); |
| | | |
| | | return element; |
| | | } |
| | | |
| | | /** |
| | | * Resolve refs declared in component definition |
| | | */ |
| | | #collectRefs(root, refMap, single = true) { |
| | | const refs = {}; |
| | | |
| | | for (const [key, value] of Object.entries(refMap)) { |
| | | let selector; |
| | | let required = false; |
| | | |
| | | if (typeof value === 'string') { |
| | | selector = value; |
| | | } else { |
| | | selector = value.selector; |
| | | required = !!value.required; |
| | | } |
| | | |
| | | const found = (single) ? root.querySelector(selector) : root.querySelectorAll(selector); |
| | | |
| | | if (required) { |
| | | if (single && !found) { |
| | | console.warn(`[TemplateRegistry] Required ref "${key}" not found: ${selector}`); |
| | | } |
| | | |
| | | if (!single && found.length === 0) { |
| | | console.warn(`[TemplateRegistry] Required manyRef "${key}" not found: ${selector}`); |
| | | } |
| | | } |
| | | |
| | | refs[key] = single ? found : Array.from(found); |
| | | } |
| | | |
| | | return refs; |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | window.jvbTemplates = new TemplateRegistry(); |
| | | |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | window.jvbTemplates.registerAll(); |
| | | }); |
| | | |
| | | /** |
| | | * Gets a clone of an icon element if it exists for efficient DOM manipulation |
| | | * @param icon |
| | |
| | | }; |
| | | } |
| | | |
| | | window.chunkIt = async function(items, renderCallback, placementCallback, size = 10) { |
| | | const chunks = []; |
| | | for (let i = 0; i <items.length; i += size) { |
| | | chunks.push(items.slice(i, i + size)); |
| | | } |
| | | |
| | | for (const chunk of chunks) { |
| | | const fragment = document.createDocumentFragment(); |
| | | chunk.forEach(item => { |
| | | const element = renderCallback(item); |
| | | if (element) fragment.append(element); |
| | | }); |
| | | |
| | | placementCallback(fragment); |
| | | await new Promise(resolve => requestAnimationFrame(resolve)); |
| | | } |
| | | } |
| | | |
| | | window.prefixInput = function(input, prefix, wrapper = null, replace = false, name = false) { |
| | | if (!input) { |
| | | console.warn('prefixInput called with null/undefined input'); |
| | | return; |
| | | } |
| | | const oldId = input.id; |
| | | const newId = replace ? prefix : `${prefix}${input.name}`; |
| | | |
| | | // Search for label within wrapper if provided, otherwise use existing logic |
| | | let label = null; |
| | | |
| | | if (wrapper) { |
| | | // Most reliable: search within wrapper by old ID |
| | | label = wrapper.querySelector(`label[for="${oldId}"]`); |
| | | } else if (input.labels && input.labels.length > 0) { |
| | | // Fallback to input.labels if no wrapper provided |
| | | label = input.labels[0]; |
| | | } else if (input.previousElementSibling?.tagName === 'LABEL') { |
| | | label = input.previousElementSibling; |
| | | } else if (input.nextElementSibling?.tagName === 'LABEL') { |
| | | label = input.nextElementSibling; |
| | | } else { |
| | | // Final fallback: search up the tree |
| | | label = input.closest('[data-field]')?.querySelector(`label[for="${oldId}"]`); |
| | | } |
| | | |
| | | if (label) { |
| | | label.htmlFor = newId; |
| | | } |
| | | |
| | | input.id = newId; |
| | | if (name) { |
| | | input.name = newId; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Makes first letter uppercase |
| | | * @param string |
| | |
| | | * @param {HTMLElement|null} parent |
| | | * @returns {object} |
| | | */ |
| | | window.uiFromSelectors = function(selectors, parent = null) { |
| | | window.uiFromSelectors = function(selectors, parent = null, all = false) { |
| | | let ui = {}; |
| | | for (let [key, selector] of Object.entries(selectors)) { |
| | | if (typeof selector === 'object') { |
| | | ui[key] = window.uiFromSelectors(selector, parent); |
| | | }else { |
| | | if (!parent) { |
| | | ui[key] = document.querySelector(selector); |
| | | ui[key] = (all) ? document.querySelectorAll(selector) : document.querySelector(selector); |
| | | } else { |
| | | ui[key] = parent.querySelector(selector); |
| | | ui[key] = (all) ? parent.querySelectorAll(selector) : parent.querySelector(selector); |
| | | } |
| | | |
| | | } |
| | | } |
| | | return ui; |
| | | } |
| | | |
| | | |
| | | class DebouncedActions { |
| | | constructor() { |
| | | this.timeouts = new Map(); |
| | |
| | | updateMaxScroll(); |
| | | updateScrollProgress(lastY); |
| | | |
| | | |
| | | |
| | | window.decodeHTMLEntities = function(text) { |
| | | if (!window.decodeHelper) { |
| | | window.decodeHelper = document.createElement('textarea'); |
| | | } |
| | | |
| | | window.decodeHelper.innerHTML = text; |
| | | return window.decodeHelper.value; |
| | | } |