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/TaxonomySelector.js | 309 ++++++++++++++++++++++++++++++++------------------
1 files changed, 197 insertions(+), 112 deletions(-)
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 5244f02..bdbe06b 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -14,6 +14,7 @@
this.activeField = null;
this.isInitializing = true;
+ this.lazyInit = false;
this.messageText = {}
this.init();
}
@@ -21,6 +22,7 @@
init() {
this.initStore();
this.initElements();
+ this.defineTemplates();
this.initModal();
this.scanExistingFields();
this.initListeners();
@@ -62,6 +64,88 @@
this.store.subscribe(this.handleStoreEvent.bind(this));
}
+ defineTemplates() {
+ const T = window.jvbTemplates;
+ const terms = this;
+
+ T.define('emptyState');
+ T.define('selectedTerm', {
+ refs: {
+ name: '.item-name',
+ btn: 'button',
+ },
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ el.dataset.taxonomy = data.taxonomy;
+ if (refs.name) refs.name.textContent = data.path;
+ if (refs.button) refs.button.title = `Remove ${data.name}`;
+ }
+ });
+ T.define('termListItem', {
+ refs: {
+ checkbox: 'input',
+ label: 'label',
+ name: 'span, .term-name'
+ },
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+
+ let field = terms.currentField();
+ let isSelected = terms.selectedTerms.get(terms.activeField).has(data.id);
+ let limitReached = field.limit > 0 && terms.selectedTerms.get(terms.activeField).size >= field.limit;
+
+ if (refs.checkbox) {
+ refs.checkbox.dataset.id = data.id;
+ refs.checkbox.id = `${field.element.id}-${data.id}`;
+ refs.checkbox.name = `${field.element.id}-${field.taxonomy}-select`;
+ refs.checkbox.value = data.id;
+ refs.checkbox.disabled = !isSelected && limitReached;
+ refs.checkbox.checked = isSelected;
+ }
+ if (refs.label) {
+ refs.label.htmlFor = `${field.element.id}-${data.id}`;
+ refs.label.title = data.path??data.name;
+ refs.label.dataset.path = data.path;
+ }
+ if (refs.name) {
+ refs.name.textContent = data.show ? data.path : data.name;
+ }
+
+ if (data.hasChildren) {
+ let temp = {
+ plural: field.plural,
+ name: data.name
+ };
+ const toggle = window.jvbTemplates.create('termChildrenToggle', temp);
+ el.append(toggle);
+ }
+ }
+ });
+
+ T.define('termChildrenToggle', {
+ setup({el, refs, manyRefs, data}) {
+ el.ariaLabel = `View ${data.plural} nested under ${data.name}`;
+ }
+ });
+
+ T.define('termBreadcrumb', {
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ el.textContent = data.name;
+ el.title = data.name;
+ }
+ });
+
+ T.define('autocompleteItem', {
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ el.textContent = data.path||data.name;
+ el.title = `Select ${data.name}`;
+ }
+ });
+
+
+ }
/******************************************************************
ELEMENTS
******************************************************************/
@@ -148,19 +232,23 @@
}
handleClick(e) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
const fieldId = this.getFieldId(e.target) || this.activeField;
const field = this.fields.get(fieldId);
if (!fieldId || !field) return;
- const autoComplete = window.targetCheck(e, '.item.autocomplete');
+ const autocomplete = window.targetCheck(e, '.item.autocomplete');
- if (autoComplete) {
- let termId = parseInt(autoComplete.dataset.id);
+ if (autocomplete) {
+ let termId = parseInt(autocomplete.dataset.id);
this.addSelected(termId, fieldId);
- this.scheduleHideDropdown(fieldId);
+ this.scheduleHideDropdown(fieldId, 6000);
if (field.ui.search) {
field.ui.search.value = '';
}
+ return;
}
const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
@@ -207,12 +295,14 @@
if (pathLevel) {
const termId = parseInt(pathLevel.dataset.id)??0;
this.navigateTo(termId);
+ return;
}
const dropdown = window.targetCheck(e, this.selectors.field.dropdown);
if (dropdown) {
// reset the timer for hiding the dropdown
this.scheduleHideDropdown(fieldId);
+ return;
}
const clearSearch = window.targetCheck(e, this.selectors.search.clear);
@@ -240,7 +330,7 @@
}
handleChange(e) {
- if (!this.container.contains(e.target)) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
return;
}
if (!['checkbox', 'button'].includes(e.target.type)) return;
@@ -257,6 +347,9 @@
}
//For search in modal or field autocomplete
handleInput(e) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
let fieldId = this.getFieldId(e.target)??this.activeField;
if (!fieldId) return;
const field = this.fields.get(fieldId);
@@ -315,9 +408,13 @@
}
handleFocus(e) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
const fieldId = this.getFieldId(e.target);
+ if (!fieldId) return;
const field = this.fields.get(fieldId);
- if (!fieldId || !field) return;
+ if (!field) return;
if (!field.hasAutocomplete && !field.hasSearch) return;
window.debouncer.cancel(`${fieldId}-search-results`);
@@ -329,16 +426,20 @@
//Hide autocomplete dropdown on blur
handleBlur(e) {
+ if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+ return;
+ }
const fieldId = this.getFieldId(e.target);
+ if (!fieldId) return;
const field = this.fields.get(fieldId);
- if (!fieldId || ! field) return;
+ if (!field) return;
if (!field.hasAutocomplete || this.container.open) return;
if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return;
this.scheduleHideDropdown(fieldId);
}
- scheduleHideDropdown(fieldId){
+ scheduleHideDropdown(fieldId, delay = 1500){
const field = this.fields.get(fieldId);
if (!field) return;
@@ -352,7 +453,7 @@
field.ui.dropdown.wrapper.hidden = true;
}
},
- 1500
+ delay
);
}
@@ -455,6 +556,10 @@
closeModal() {
const field = this.fields.get(this.activeField);
if (!field) return;
+
+
+ this.updateFieldValue(this.activeField);
+
this.observer.unobserve(this.ui.terms.sentinel);
window.removeChildren(this.ui.terms.list);
@@ -522,24 +627,26 @@
if (!field) return;
if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
- const item = window.getTemplate('selectedTerm');
- if (!item) return;
+ this.ui.selected.append(this.getSelectedTermUI(term));
+ }
- item.dataset.id = termId;
- item.dataset.taxonomy = field.taxonomy;
- item.querySelector('.item-name').textContent = term.path;
- item.querySelector('button').title = `Remove ${term.name}`;
-
- this.ui.selected.append(item);
+ getSelectedTermUI(term, showPath = true) {
+ return window.jvbTemplates.create('selectedTerm', term);
}
/******************************************************************
FIELDS
******************************************************************/
scanExistingFields(container = document.body) {
- container.querySelectorAll('[data-type="selector"]').forEach(
+ container.querySelectorAll('[data-type="selector"], [data-field-type="selector"]').forEach(
selector => {
try {
- this.registerField(selector);
+ if (selector.dataset.lazy) {
+ this.lazyInit = true;
+ } else {
+ // Register field if not already registered
+ // registerField will check if already registered and return early if so
+ this.registerField(selector);
+ }
} catch (error) {
this.error.log(error, {
component: 'TaxonomySelector',
@@ -549,12 +656,41 @@
}
}
);
+ if (this.lazyInit) {
+ this.initObserver(container);
+ }
+ }
+
+ unregisterFields(container) {
+ container.querySelectorAll('[data-type="selector"],[data-field-type="selector"]').forEach(
+ selector=> {
+ this.fields.delete(selector.dataset.fieldId);
+ }
+ );
+ }
+ initObserver(container){
+ this.lazyObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting && entry.target.dataset.lazy) {
+ delete entry.target.dataset.lazy;
+ this.registerField(entry.target);
+ this.lazyObserver.unobserve(entry.target);
+ }
+ });
+ }, {rootMargin: '50px'});
+
+ container.querySelectorAll('[data-type="selector"][data-lazy], [data-field-type="selector"][data-lazy]').forEach(field => {
+ this.lazyObserver.observe(field);
+ });
}
registerField(element, options = {}) {
+ if (element.dataset.fieldId && this.fields.has(element.dataset.fieldId)) {
+ return element.dataset.fieldId; // Already registered
+ }
+
let input = element.querySelector('input[type="hidden"]');
if (!input && !Object.hasOwn(element.dataset, 'filter')) {
- console.warn('TaxonomySelector: No hidden input found for field', element);
return;
}
@@ -578,7 +714,6 @@
autocomplete: Object.hasOwn(button.dataset, 'autocomplete'),
creatable: Object.hasOwn(button.dataset, 'creatable')
};
- if (Object.keys(options).length === 0) return;
} else if (Object.hasOwn(options, 'toggle')) {
button = document.querySelector(options.toggle);
selectors.toggle = options.toggle;
@@ -629,7 +764,18 @@
if (this.isInitializing) {
this.batchFetch.add(config.taxonomy);
}
- this.updateFieldUI(fieldId);
+
+ if (element.offsetParent !== null) {
+ this.updateFieldUI(fieldId);
+ } else {
+ // Defer until visible
+ requestIdleCallback(() => {
+ if (element.offsetParent !== null) {
+ this.updateFieldUI(fieldId);
+ }
+ }, {timeout: 2000});
+
+ }
return fieldId;
}
@@ -695,7 +841,8 @@
const field = this.fields.get(fieldId);
if (!field) return;
let selected = Array.from(this.selectedTerms.get(fieldId));
- field.ui.value.value = selected.join(',');
+ field.ui.value.value = selected.join(',')??'';
+ field.ui.value.dispatchEvent(new Event('change', { bubbles: true }));
}
checkLimits(fieldId) {
@@ -757,6 +904,7 @@
if (this.ui.terms.sentinel) {
this.observer.unobserve(this.ui.terms.sentinel);
}
+ return;
}
this.setCreateButton(true);
@@ -772,76 +920,20 @@
const currentParent = this.store.filters.parent??0;
this.ui.nav.back.hidden = currentParent === 0;
- const fragment = document.createDocumentFragment();
- terms.forEach(term => {
- const element = this.createTermElement({
- show: showPath,
- ... term
- });
- if (element) {
- fragment.append(element);
- }
- });
+ window.chunkIt(
+ terms,
+ (term) => this.createTermElement({show:showPath, ... term}),
+ (fragment) => this.ui.terms.list.append(fragment),
+ 10
+ ).then(()=>{});
if (terms.length > 0) {
this.setMessage(false);
}
-
- this.ui.terms.list.append(fragment);
}
createTermElement(term) {
if (!term || !term.name) return null;
-
- const item = window.getTemplate('termListItem');
- item.dataset.id = term.id;
-
- const isSelected = this.selectedTerms.get(this.activeField).has(term.id);
- let [
- checkbox,
- label,
- nameSpan
- ] = [
- item.querySelector('input'),
- item.querySelector('label'),
- item.querySelector('span, .term-name')
- ];
-
- let field = this.currentField();
- let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit;
- if (checkbox && label && nameSpan) {
- [
- checkbox.dataset.id,
- checkbox.id,
- checkbox.name,
- checkbox.value,
- checkbox.disabled,
- checkbox.checked,
- label.htmlFor,
- label.title,
- label.dataset.path,
- nameSpan.textContent
- ] = [
- term.id,
- `${field.element.id}-${term.id}`,
- `${field.element.id}-${field.taxonomy}-select`,
- term.id,
- !isSelected && limitReached,
- isSelected,
- `${field.element.id}-${term.id}`,
- term.path??term.name,
- term.path,
- term.show ? term.path : term.name
- ];
- if (term.hasChildren) {
- const toggle = window.getTemplate('termChildrenToggle');
- if (toggle) {
- toggle.ariaLabel = `View ${field.plural} nested under ${term.name}`;
- item.append(toggle);
- }
- }
- }
-
- return item;
+ return window.jvbTemplates.create('termListItem', term);
}
showAutocompleteTerms() {
@@ -856,12 +948,12 @@
if (terms.length === 0) {
this.setMessage(true, `No ${field.plural} found.`, false);
} else {
- terms.forEach(term => {
- const item = this.createAutocompleteTerm(term);
- if (item) {
- dropdown.append(item);
- }
- });
+ window.chunkIt(
+ terms,
+ (term) => this.createAutocompleteTerm(term),
+ (fragment) => dropdown.append(fragment)
+ ).then(()=>{});
+
this.setMessage(false);
}
this.setCreateButton(true);
@@ -870,14 +962,9 @@
field.ui.dropdown.wrapper.hidden = false;
}
}
+
createAutocompleteTerm(term) {
- const item = window.getTemplate('autocompleteItem');
- if (!item) return;
-
- item.dataset.id = term.id;
- item.textContent = term.path || term.name;
-
- return item;
+ return window.jvbTemplates.create('autocompleteItem', term);
}
/******************************************************************
UI
@@ -886,16 +973,12 @@
const term = this.store.get(termId);
const field = this.fields.get(fieldId);
if (!term || !field) return;
+
//if the term already exists in the selected items, bail early
if (field.ui.selected && field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
- const item = window.getTemplate('selectedTerm');
- if (!item) return;
- item.dataset.id = termId;
- item.dataset.taxonomy = field.taxonomy;
- item.querySelector('.item-name').textContent = term.path;
- item.querySelector('button').title = `Remove ${term.name}`;
+ let item = this.getSelectedTermUI(term);
if (field.ui.selected) {
field.ui.selected.append(item);
@@ -926,13 +1009,7 @@
// Add new breadcrumb
const term = this.store.get(termId);
if (!term) return;
-
- const crumb = window.getTemplate('termBreadcrumb');
- if (!crumb) return;
-
- crumb.dataset.id = termId;
- crumb.textContent = term.name;
- crumb.title = term.name;
+ const crumb = window.jvbTemplates.create('termBreadcrumb', term);
nav.append(crumb);
}
@@ -955,6 +1032,13 @@
/******************************************************************
UTILITY
******************************************************************/
+ checkRendered(collection, term) {
+ if (!collection) return;
+ if (!Object.hasOwn(collection, term.taxonomy)) {
+ collection[term.taxonomy] = new Map();
+ }
+ return collection[term.taxonomy].has(term.id);
+ }
currentField() {
return this.fields.get(this.activeField)??false;
}
@@ -1222,6 +1306,7 @@
this.observer?.unobserve(this.ui.terms.sentinel);
}
this.observer?.disconnect();
+ this.lazyObserver?.disconnect();
// Remove event listeners
document.removeEventListener('click', this.clickHandler);
--
Gitblit v1.10.0