From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
assets/js/concise/UtilityFunctions.js | 344 +++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 322 insertions(+), 22 deletions(-)
diff --git a/assets/js/concise/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
index fe2f543..d11cdbc 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,64 @@
};
}
+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;
+ }
+ // console.log('Prefixing input: ', input);
+ // console.log('With prefix: ', prefix);
+ // console.log('Wrapper: ', wrapper);
+ const oldId = input.id;
+ const newId = replace ? prefix : `${prefix}${input.name}`;
+ // console.log('Old ID: ', oldId);
+ // console.log('New ID: ', newId);
+ // 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
@@ -389,17 +559,23 @@
* @returns {Promise<unknown>}
*/
window.typeText = function(container, text, speed = 50) {
- container.classList.add('typeText');
return new Promise((resolve) => {
+ // Cancel any existing animation on this container
+ if (container._typeInterval) {
+ clearInterval(container._typeInterval);
+ delete container._typeInterval;
+ }
+
let index = 0;
container.textContent = '';
- const interval = setInterval(() => {
+ container._typeInterval = setInterval(() => {
if (index < text.length) {
container.textContent += text.charAt(index);
index++;
} else {
- clearInterval(interval);
+ clearInterval(container._typeInterval);
+ delete container._typeInterval;
resolve();
}
}, speed);
@@ -407,22 +583,29 @@
}
/**
- * Erases text like a keyboard would. TODO: erase a set word from existing text
+ * Erases text like a keyboard would.
* @param container
* @param speed
* @returns {Promise<unknown>}
*/
window.eraseText = function(container, speed = 10) {
return new Promise((resolve) => {
+ // Cancel any existing animation on this container
+ if (container._eraseInterval) {
+ clearInterval(container._eraseInterval);
+ delete container._eraseInterval;
+ }
+
let text = container.textContent;
let index = text.length;
- const interval = setInterval(() => {
+ container._eraseInterval = setInterval(() => {
if (index > 0) {
index--;
container.textContent = text.substring(0, index);
} else {
- clearInterval(interval);
+ clearInterval(container._eraseInterval);
+ delete container._eraseInterval;
resolve();
}
}, speed);
@@ -440,31 +623,47 @@
* @returns {Function} - Call this function to stop the loop
*/
window.typeLoop = function(container, text, typeSpeed = 50, eraseSpeed = 10, pauseAfterType = 1000, pauseAfterErase = 250) {
+ // Generate unique key for this container
+ const containerId = container.id || container.dataset.typeKey || `type-${Date.now()}`;
+ if (!container.dataset.typeKey) {
+ container.dataset.typeKey = containerId;
+ }
+
+ // Stop any existing loop immediately
+ if (container._stopTyping) {
+ container._stopTyping();
+ }
+
let isRunning = true;
async function loop() {
while (isRunning) {
- // Type the text
await window.typeText(container, text, typeSpeed);
-
- // Wait 1 second
+ if (!isRunning) break;
await new Promise(resolve => setTimeout(resolve, pauseAfterType));
-
- // Erase the text
+ if (!isRunning) break;
await window.eraseText(container, eraseSpeed);
-
- // Wait 0.25 seconds before next iteration
+ if (!isRunning) break;
await new Promise(resolve => setTimeout(resolve, pauseAfterErase));
}
}
- // Start the loop
- loop();
-
- // Return a function to stop the loop
- return function stopLoop() {
+ const stopLoop = function() {
isRunning = false;
+ if (container._typeInterval) {
+ clearInterval(container._typeInterval);
+ delete container._typeInterval;
+ }
+ if (container._eraseInterval) {
+ clearInterval(container._eraseInterval);
+ delete container._eraseInterval;
+ }
};
+
+ container._stopTyping = stopLoop;
+ loop(); // Start immediately
+
+ return stopLoop;
};
window.toCamelCase = function (string) {
@@ -684,23 +883,26 @@
* @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;
}
+window.sleep = async function (ms = 50) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+};
+
class DebouncedActions {
constructor() {
this.timeouts = new Map();
@@ -790,15 +992,113 @@
{ passive: true }
);
+
+window.previousBGSize = 'Small';
+window.bgSizes = {
+ Small: 500,
+ Med: 768,
+ Large: 1024
+};
+
+window.bgObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ let newSize = entry.target.dataset[`bg${window.previousBGSize}`];
+ entry.target.style.backgroundImage = `url(${newSize})`;
+ entry.target.dataset.bgImg = window.previousBGSize;
+ window.bgObserver.unobserve(entry.target);
+ }
+ })
+ },
+ {
+ root: null,
+ rootMargin: '0px 0px -100px 0px',
+ threshold: 0
+ });
+
+function updateBG() {
+ let current = window.innerWidth;
+ let newWidth = getBGWidth(current);
+
+ if (newWidth) {
+ window.previousBGSize = newWidth;
+ document.querySelectorAll('[data-bg-img]:not([data-bg-img="'+window.previousBGSize+'"])').forEach(img => {
+ window.bgObserver.observe(img);
+ });
+ }
+
+}
+function getBGWidth(width) {
+ let prev = window.previousBGSize;
+
+ let check = {
+ Small: ['Med','Large'],
+ Med: ['Large'],
+ Large: false
+ };
+
+ if (!check[prev]) {
+ return false;
+ }
+ let next = 'Small';
+
+ check[prev].forEach(w => {
+ if (width => window.bgSizes[w]) {
+ next = w;
+ }
+ });
+ return next;
+}
+
+updateBG();
+
// Debounced resize to recalc scrollable height
window.addEventListener('resize', () => {
window.debouncer.schedule('recalc-max-scroll', () => {
updateMaxScroll();
updateScrollProgress(window.scrollY || docEl.scrollTop || 0);
}, 20);
+ window.debouncer.schedule('bg-resize', () => {
+ updateBG();
+ });
});
// Initial setup
updateMaxScroll();
updateScrollProgress(lastY);
+
+
+window.decodeHTMLEntities = function(text) {
+ if (!window.decodeHelper) {
+ window.decodeHelper = document.createElement('textarea');
+ }
+
+ window.decodeHelper.innerHTML = text;
+ return window.decodeHelper.value;
+}
+
+
+window.focusNextElement = function() {
+ //add all elements we want to include in our selection
+ var focussableElements =
+ 'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
+ if (document.activeElement && document.activeElement.form) {
+ var focussable = Array.prototype.filter.call(
+ document.activeElement.form.querySelectorAll(focussableElements),
+ function (element) {
+ //check for visibility while always include the current activeElement
+ return (
+ element.offsetWidth > 0 ||
+ element.offsetHeight > 0 ||
+ element === document.activeElement
+ );
+ }
+ );
+ var index = focussable.indexOf(document.activeElement);
+ if (index > -1) {
+ var nextElement = focussable[index + 1] || focussable[0];
+ nextElement.focus();
+ }
+ }
+}
--
Gitblit v1.10.0