From 7a9054bb3f033c98067b3196378311dae54c5fbf Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 20 Jan 2026 01:31:53 +0000
Subject: [PATCH] =OperationQueue refactor to the JVBase/managers/queue namespace

---
 assets/js/concise/UtilityFunctions.js |  160 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 156 insertions(+), 4 deletions(-)

diff --git a/assets/js/concise/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
index d80a661..2381056 100644
--- a/assets/js/concise/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -152,6 +152,118 @@
 	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
@@ -283,6 +395,46 @@
 	};
 }
 
+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, replace = false) {
+	let newId = replace ? prefix : `${prefix}${input.name}`;
+	if (input.labels.length > 0) {
+		input.labels?.forEach(label => {
+			label.htmlFor = newId;
+		});
+	} else {
+		if (input.nextElementSibling?.tagName === 'LABEL') {
+			input.nextElementSibling.htmlFor = newId;
+		}else if (input.previousElementSibling?.tagName === 'LABEL') {
+			input.previousElementSibling.htmlFor = newId;
+		} else {
+			let label = input.parentElement.querySelector(`label[for="${input.id}"]`);
+			if (label) {
+				label.htmlFor = newId;
+			}
+		}
+	}
+
+	input.id = newId;
+}
+
 /**
  * Makes first letter uppercase
  * @param string
@@ -713,23 +865,23 @@
  * @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();

--
Gitblit v1.10.0