// FilterPanel.js - Handles UI for filtering feed content
|
import cache from '../utils/cache';
|
|
import { debounce } from '../utils/formatters';
|
class FilterPanel {
|
constructor(container, options = {}) {
|
this.container = container;
|
this.options = {
|
taxonomies: [],
|
contentTypes: [],
|
onChange: null,
|
taxonomyFor: {},
|
...options
|
};
|
|
// Store references to key elements
|
this.selected = {};
|
this.selectedFiltersContainer = container.querySelector('.selected-items');
|
this.matchAll = container.querySelector('.toggle-text');
|
this.filterToggles = container.querySelectorAll('.filter-toggle');
|
this.filtersContainer = container.querySelector('.filters');
|
this.filterDropdowns = container.querySelectorAll('.filter-dropdown');
|
this.clearFiltersButton = container.querySelector('.clear-filters');
|
this.contentTypeInputs = container.querySelectorAll('input[name="content_type"]');
|
this.orderInputs = container.querySelectorAll('input[name="order"]');
|
|
// Local cache for terms
|
this.termCache = {
|
terms: new Map(),
|
timestamp: Date.now()
|
};
|
|
// Initialize
|
this.bindEvents();
|
}
|
|
/**
|
* Initialize filter dropdowns
|
*/
|
initSelectors() {
|
this.selectors = this.container.querySelectorAll('.jvb-selector');
|
|
this.selectorInstances = {};
|
let type = this.getCurrentContentType();
|
this.selectors.forEach(selector => {
|
|
if(selector.dataset.for.includes(type)){
|
const taxonomy = selector.dataset.taxonomy;
|
const options = selector.querySelector('.selected-items');
|
|
this.selectorInstances[taxonomy] = new TaxonomySelector(selector, {
|
multiple: true,
|
feed: true,
|
onSuccess: () => this.updateSelected(taxonomy)
|
});
|
}
|
|
});
|
}
|
|
updateSelected(taxonomy){
|
// selected = this.selectorInstances[taxonomy].selected;
|
let selected = this.selectorInstances[taxonomy].selectedItems;
|
|
}
|
/**
|
* New method to bind content type events
|
*/
|
bindContentTypeEvents() {
|
const contentTypeInputs = this.container.querySelectorAll('input[name="content_type"]');
|
|
contentTypeInputs.forEach(input => {
|
// Remove existing event listeners to prevent duplicates
|
input.removeEventListener('change', this._handleContentTypeChange);
|
|
// Store event handler as property so we can remove it later
|
this._handleContentTypeChange = (e) => {
|
if (input.checked) {
|
const contentType = input.value;
|
|
// First, directly update taxonomy and order filters UI
|
this.updateTaxonomyFilters(contentType);
|
this.updateOrderFilters(contentType);
|
|
this.handleFormChange();
|
}
|
};
|
|
// Add the event listener
|
input.addEventListener('change', this._handleContentTypeChange);
|
});
|
}
|
|
/**
|
* Replace the existing bindEvents method in FilterPanel
|
*/
|
bindEvents() {
|
// Form change events
|
this.container.addEventListener('submit', e => e.preventDefault());
|
this.container.addEventListener('change', this.handleFormChanges.bind(this));
|
this.container.addEventListener('click', this.handleToggleClick.bind(this));
|
|
// Add global keydown listener for Escape key
|
document.addEventListener('keydown', (e) => {
|
if (e.key === 'Escape') {
|
this.closeAllDropdowns();
|
}
|
});
|
|
// Dialog close events
|
this.filterDropdowns.forEach(dialog => {
|
dialog.addEventListener('close', this.handleDialogClose.bind(this));
|
dialog.addEventListener('click', (e) => {
|
// Check if click was directly on the dialog element (backdrop)
|
// and not on any of its children
|
if (e.target === dialog) {
|
dialog.close();
|
}
|
});
|
// Set up search inputs
|
const search = dialog.querySelector('input[type="search"]');
|
if (search) {
|
search.addEventListener('input', debounce(e => {
|
const taxonomy = dialog.dataset.taxonomy;
|
const query = e.target.value;
|
const container = dialog.querySelector('.options-container');
|
this.handleSearch(taxonomy, query, container);
|
}, 300));
|
}
|
});
|
|
// Clear filters button
|
if (this.clearFiltersButton) {
|
this.clearFiltersButton.addEventListener('click', this.clearFilters.bind(this));
|
}
|
}
|
/**
|
* Handle form change events
|
*/
|
handleFormChanges(e) {
|
const target = e.target;
|
|
// Skip changes in dialogs (handled separately)
|
if (target.closest('dialog')) return;
|
|
// Handle content type changes
|
if (target.name === 'content_type' && target.checked) {
|
const contentType = target.value;
|
this.updateTaxonomyFilters(contentType);
|
this.updateOrderFilters(contentType);
|
}
|
|
// Handle order type changes
|
if (target.name === 'order') {
|
this.handleOrderChange(target);
|
}
|
|
// For all changes, trigger form update
|
this.handleFormChange();
|
}
|
handleToggleClick(e) {
|
const toggle = e.target.closest('.filter-toggle');
|
if (!toggle) return;
|
|
e.preventDefault();
|
|
const wasExpanded = toggle.getAttribute('aria-expanded') === 'true';
|
this.closeAllDropdowns();
|
|
const taxonomyFilter = toggle.closest('[data-taxonomy]');
|
if (!taxonomyFilter) return;
|
|
const taxonomy = taxonomyFilter.dataset.taxonomy;
|
const dropdown = this.container.querySelector(
|
`.filter-dropdown[data-taxonomy="${taxonomy}"]`
|
);
|
|
if (!dropdown) return;
|
|
toggle.setAttribute('aria-expanded', !wasExpanded);
|
dropdown.showModal();
|
|
if (!wasExpanded) {
|
const search = dropdown.querySelector('input[type="search"]');
|
if (search) search.focus();
|
|
const options = dropdown.querySelector('.options-container');
|
if (options && options.children.length === 1) {
|
this.loadTaxonomyOptions(taxonomy, options);
|
}
|
}
|
}
|
|
/**
|
* Handle dialog close events
|
*/
|
handleDialogClose(e) {
|
|
const dialog = e.target;
|
const taxonomy = dialog.dataset.taxonomy;
|
const selected = this.selectorInstances[taxonomy].selectedItems;
|
if (Object.keys(selected).length > 0) {
|
this.updateFilters(taxonomy, selected);
|
}
|
}
|
|
/**
|
* Handle order type changes
|
*/
|
handleOrderChange(target) {
|
const orderDirection = this.container.querySelector('.order-direction');
|
|
if (target.value === 'random') {
|
orderDirection.hidden = true;
|
// Reset direction to default
|
this.container.querySelector('#order-desc').checked = true;
|
} else if (target.value === 'title') {
|
this.container.querySelector('#order-asc').checked = true;
|
orderDirection.hidden = false;
|
} else {
|
orderDirection.hidden = false;
|
}
|
}
|
|
/**
|
* Handle search input in taxonomy dialogs
|
*/
|
handleSearch(taxonomy, query, container) {
|
if (!container) return;
|
|
// Clear existing options
|
container.innerHTML = '';
|
container.setAttribute('data-loading', 'true');
|
|
// Debounced search function
|
if (this.searchDebounce) {
|
clearTimeout(this.searchDebounce);
|
}
|
|
this.searchDebounce = setTimeout(async () => {
|
try {
|
// Get terms - either from cache or API
|
let terms;
|
|
if (query) {
|
// Create cache key for search
|
const cacheKey = `terms_${taxonomy}_search_${query}`;
|
terms = cache.get(cacheKey);
|
|
if (!terms) {
|
// For search, always fetch from API if not cached
|
const response = await fetch(
|
`${window.feedSettings.apiUrl}terms/${taxonomy}?search=${encodeURIComponent(query)}`,
|
{
|
headers: {
|
'X-WP-Nonce': window.feedSettings.nonce
|
}
|
}
|
);
|
|
if (!response.ok) {
|
throw new Error('Failed to fetch terms');
|
}
|
|
const data = await response.json();
|
terms = data.terms;
|
|
// Cache search results for 5 minutes
|
cache.set(cacheKey, terms, 300000);
|
}
|
} else if (this.termCache.terms.has(taxonomy)) {
|
// Use cached terms if available
|
terms = this.termCache.terms.get(taxonomy);
|
} else {
|
// Fetch from API
|
const response = await fetch(
|
`${window.feedSettings.apiUrl}terms/${taxonomy}`,
|
{
|
headers: {
|
'X-WP-Nonce': window.feedSettings.nonce
|
}
|
}
|
);
|
|
if (!response.ok) {
|
throw new Error('Failed to fetch terms');
|
}
|
|
const data = await response.json();
|
terms = data.terms;
|
|
// Update cache
|
this.termCache.terms.set(taxonomy, terms);
|
this.termCache.timestamp = Date.now();
|
}
|
|
// Render terms
|
this.renderTermOptions(container, terms);
|
|
} catch (error) {
|
console.error('Error fetching terms:', error);
|
container.innerHTML = '<div class="error">Failed to load options</div>';
|
} finally {
|
container.removeAttribute('data-loading');
|
}
|
}, 300);
|
}
|
|
/**
|
* Handle form change events
|
*/
|
handleFormChange() {
|
// Get current form data
|
const formData = this.getFormData();
|
|
// Update URL
|
this.updateURL(formData);
|
|
// Call change handler if available
|
if (typeof this.options.onChange === 'function') {
|
this.options.onChange(formData);
|
}
|
}
|
|
/**
|
* Get current form data
|
*/
|
getFormData() {
|
if (!this.container) return {};
|
|
const formData = new FormData(this.container);
|
|
// Get content type - explicitly check the checked radio button
|
const contentTypeInput = this.container.querySelector('input[name="content_type"]:checked');
|
const contentType = contentTypeInput ? contentTypeInput.value : this.options.defaultContent;
|
|
const filters = {
|
content: contentType,
|
order: formData.get('order') || 'date',
|
direction: formData.get('direction') || 'desc',
|
match: formData.get('match') || 'any',
|
};
|
|
|
for (const [taxonomy, selector] of Object.entries(this.selectorInstances)) {
|
if(Object.keys(selector.selectedItems).length > 0){
|
filters[taxonomy] = Object.keys(selector.selectedItems);
|
}
|
}
|
|
|
// Add favourites flag if present
|
if (formData.get('favourites_only')) {
|
filters.favouritesOnly = true;
|
}
|
|
return filters;
|
}
|
|
/**
|
* Update taxonomy filters based on content type
|
*/
|
updateTaxonomyFilters(contentType) {
|
if (!contentType) {
|
contentType = this.options.defaultContent || 'tattoo';
|
}
|
// Show/hide taxonomy filters based on content type
|
const filters = this.container.querySelectorAll('.taxonomy-filter');
|
|
filters.forEach(filter => {
|
const applicableTypes = filter.dataset.for.split(',');
|
const isVisible = applicableTypes.includes(contentType);
|
filter.hidden = !isVisible;
|
|
// Update content type for visible selectors
|
if (isVisible) {
|
const taxonomy = filter.dataset.taxonomy;
|
if (this.taxonomySelectors && this.taxonomySelectors[taxonomy]) {
|
this.taxonomySelectors[taxonomy].setContentType(contentType);
|
}
|
}
|
});
|
|
// Remove selected filters for hidden taxonomies
|
const hiddenFilters = this.container.querySelectorAll('.taxonomy-filter[hidden]');
|
hiddenFilters.forEach(filter => {
|
const taxonomy = filter.dataset.taxonomy;
|
const selectedItems = this.selectedFiltersContainer.querySelectorAll(
|
`.selected-item[data-taxonomy="${taxonomy}"]`
|
);
|
|
if (selectedItems.length > 0) {
|
selectedItems.forEach(item => item.remove());
|
}
|
});
|
this.initSelectors();
|
// Update clear filters button visibility
|
this.updateClearFiltersButton();
|
}
|
|
/**
|
* Get current content type
|
*/
|
getCurrentContentType() {
|
const contentTypeInput = this.container.querySelector('input[name="content_type"]:checked');
|
return contentTypeInput ? contentTypeInput.value : this.options.defaultContent;
|
}
|
|
/**
|
* Update order filters based on content type
|
*/
|
updateOrderFilters(contentType) {
|
if (!contentType) return;
|
|
const orderFilters = this.container.querySelectorAll('input[name="order"]');
|
|
orderFilters.forEach(filter => {
|
const applicableTypes = filter.dataset.for;
|
|
if (applicableTypes) {
|
const types = applicableTypes.split(',');
|
filter.hidden = !types.includes(contentType);
|
|
if (filter.hidden && filter.checked) {
|
// Reset to default
|
this.container.querySelector('input#order-date').checked = true;
|
this.container.querySelector('input[name="direction"][value="desc"]').checked = true;
|
}
|
}
|
});
|
}
|
|
/**
|
* Close all open dropdowns
|
*/
|
closeAllDropdowns() {
|
this.filterToggles.forEach(toggle => {
|
toggle.setAttribute('aria-expanded', 'false');
|
});
|
|
this.filterDropdowns.forEach(dialog => {
|
if (dialog.open) {
|
dialog.close();
|
}
|
});
|
}
|
|
/**
|
* Update URL parameters based on filters
|
*/
|
updateURL(filters) {
|
const params = new URLSearchParams();
|
|
// Add content type
|
if (filters.content && filters.content !== 'all') {
|
params.set('f_content', filters.content);
|
}
|
|
// Add order and direction
|
if (filters.order) {
|
params.set('f_order', filters.order);
|
if (filters.direction) {
|
params.set('f_direction', filters.direction);
|
}
|
}
|
|
// Add taxonomy filters
|
|
for (const [key, value] of Object.entries(filters)) {
|
if (Array.isArray(value) && value.length > 0) {
|
value.forEach(v => params.append('f_'+key, v));
|
}
|
}
|
|
// Add favourites filter
|
if (filters.favouritesOnly) {
|
params.set('f_favourites', '1');
|
}
|
// if(filters.match){
|
// params.set('f_match', filters.match);
|
// }
|
|
// Update URL without reloading page
|
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
|
history.pushState({ filters }, '', newUrl);
|
}
|
|
/**
|
* Update filters for a taxonomy
|
*/
|
updateFilters(taxonomy, values) {
|
this.selected[taxonomy] = Object.keys(values);
|
// Remove existing selected items for this taxonomy
|
const existingItems = this.selectedFiltersContainer.querySelectorAll(
|
`.selected-item[data-taxonomy="${taxonomy}"]`
|
);
|
existingItems.forEach(item => item.remove());
|
|
for (const [id, name] of Object.entries(values)) {
|
const filterTag = this.createFilterTag(taxonomy, id, name);
|
this.selectedFiltersContainer.appendChild(filterTag);
|
}
|
|
// Update clear filters button visibility
|
this.updateClearFiltersButton();
|
|
// Trigger form change
|
this.handleFormChange();
|
}
|
|
/**
|
* Create a filter tag element
|
*/
|
createFilterTag(taxonomy, id, name) {
|
const tag = document.createElement('span');
|
tag.className = 'selected-item';
|
tag.dataset.taxonomy = taxonomy;
|
tag.dataset.id = id;
|
|
const icon = feedSettings?.icons?.[taxonomy] || '';
|
|
tag.innerHTML = `
|
${icon}
|
<span class="filter-name">${this.escapeHtml(name)}</span>
|
<button type="button" aria-label="Remove ${name}" class="remove-item">
|
${window.feedSettings?.icons?.close || '×'}
|
</button>
|
`;
|
|
// Add click handler for remove button
|
tag.querySelector('.remove-item').addEventListener('click', () => {
|
// Uncheck checkbox if it exists
|
let taxonomy = tag.dataset.taxonomy;
|
delete this.selectorInstances[taxonomy].selectedItems[id];
|
|
// Remove tag
|
tag.remove();
|
|
// Update clear filters button visibility
|
this.updateClearFiltersButton();
|
|
// Trigger form change
|
this.handleFormChange();
|
});
|
|
return tag;
|
}
|
|
/**
|
* Update clear filters button visibility
|
*/
|
updateClearFiltersButton() {
|
if (!this.clearFiltersButton) return;
|
|
let filters = this.selectedFiltersContainer.children.length;
|
|
const hasFilters = filters > 0;
|
|
const hasMultiple = filters > 1;
|
|
this.clearFiltersButton.hidden = !hasFilters;
|
|
this.filtersContainer.classList.toggle('has-filters', hasFilters);
|
|
this.matchAll.hidden = !hasMultiple;
|
}
|
|
/**
|
* Clear all filters
|
*/
|
clearFilters() {
|
// Reset form
|
if (this.container) {
|
this.container.reset();
|
}
|
|
// Clear selected items
|
if (this.selectedFiltersContainer) {
|
this.selectedFiltersContainer.innerHTML = '';
|
}
|
|
// Uncheck all checkboxes in dropdowns
|
this.filterDropdowns.forEach(dialog => {
|
dialog.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
checkbox.checked = false;
|
});
|
});
|
|
// Update clear filters button visibility
|
this.updateClearFiltersButton();
|
|
// Trigger form change
|
this.handleFormChange();
|
}
|
|
/**
|
* Load taxonomy options
|
*/
|
async loadTaxonomyOptions(taxonomy, container, page = 1) {
|
if (!container) return;
|
|
container.setAttribute('data-loading', 'true');
|
|
try {
|
// Check cache first (if first page and no search)
|
if (page === 1 && this.termCache.terms.has(taxonomy)) {
|
this.renderTermOptions(container, this.termCache.terms.get(taxonomy));
|
return;
|
}
|
|
// Create cache key
|
const cacheKey = `terms_${taxonomy}_page_${page}`;
|
let terms = cache.get(cacheKey);
|
|
if (!terms) {
|
// Fetch from API
|
const response = await fetch(
|
`${window.feedSettings.apiUrl}terms/${taxonomy}?page=${page}`,
|
{
|
headers: {
|
'X-WP-Nonce': window.feedSettings.nonce
|
}
|
}
|
);
|
|
if (!response.ok) {
|
throw new Error('Failed to fetch terms');
|
}
|
|
const data = await response.json();
|
terms = data.terms;
|
|
// Cache for 5 minutes
|
cache.set(cacheKey, terms, 300000);
|
|
// Update local cache if first page
|
if (page === 1) {
|
this.termCache.terms.set(taxonomy, terms);
|
this.termCache.timestamp = Date.now();
|
}
|
}
|
|
// Render terms
|
this.renderTermOptions(container, terms, page > 1);
|
|
} catch (error) {
|
console.error('Error fetching terms:', error);
|
if (container.children.length === 0) {
|
container.innerHTML = '<div class="error">Failed to load options</div>';
|
}
|
} finally {
|
container.removeAttribute('data-loading');
|
}
|
}
|
|
/**
|
* Render term options in a dropdown
|
*/
|
renderTermOptions(container, terms = [], append = false) {
|
if (!container) return;
|
|
const termsArray = Array.isArray(terms) ? terms : Object.values(terms);
|
|
// Get the relevant taxonomy
|
const taxonomy = container.closest('.filter-dropdown')?.dataset.taxonomy;
|
if (!taxonomy) return;
|
|
// Get selected values
|
const selectedValues = this.getSelectedValues(taxonomy);
|
|
// Create HTML
|
const html = termsArray.map(term => {
|
const isChecked = selectedValues.includes(term.id.toString());
|
const displayName = term.display_name || term.name;
|
const inputId = `${taxonomy}-${term.id}`;
|
|
return `
|
<input type="checkbox"
|
id="${inputId}"
|
name="${taxonomy}"
|
value="${term.id}"
|
data-umami-event="toggle_taxonomy"
|
data-umami-taxonomy="${taxonomy}"
|
data-umami-term="${term.name}"
|
data-umami-term-id="${term.id}"
|
data-umami-action="${isChecked ? 'remove' : 'add'}"
|
${isChecked ? 'checked' : ''}>
|
<label for="${inputId}" class="filter-option">
|
<span>${this.escapeHtml(displayName)}</span>
|
${term.count ? `<span class="count">(${term.count})</span>` : ''}
|
</label>
|
`;
|
}).join('');
|
|
// Insert HTML
|
if (append) {
|
container.insertAdjacentHTML('beforeend', html);
|
} else {
|
container.innerHTML = html;
|
}
|
}
|
|
/**
|
* Get selected values for a taxonomy
|
*/
|
getSelectedValues(taxonomy) {
|
const selectedItems = this.selectedFiltersContainer.querySelectorAll(
|
`.selected-item[data-taxonomy="${taxonomy}"]`
|
);
|
|
return Array.from(selectedItems).map(item => item.dataset.id);
|
}
|
|
/**
|
* Get term details from cache
|
*/
|
getTermDetails(taxonomy, id) {
|
if (!this.termCache.terms.has(taxonomy)) return null;
|
|
const terms = this.termCache.terms.get(taxonomy);
|
return terms[id] || null;
|
}
|
|
/**
|
* Load form state from URL parameters
|
*/
|
loadFromURL() {
|
const params = new URLSearchParams(window.location.search);
|
|
// Reset form state
|
if (this.container) {
|
this.container.reset();
|
}
|
|
// Clear selected filters
|
if (this.selectedFiltersContainer) {
|
this.selectedFiltersContainer.innerHTML = '';
|
}
|
|
// Set content type
|
const contentType = params.get('f_content');
|
if (contentType) {
|
const contentTypeInput = this.container.querySelector(
|
`input[name="content_type"][value="${contentType}"]`
|
);
|
if (contentTypeInput) {
|
contentTypeInput.checked = true;
|
// Update visible taxonomies based on content type
|
this.updateTaxonomyFilters(contentType);
|
}
|
}
|
|
// Set order and direction
|
const order = params.get('f_order');
|
if (order) {
|
const orderInput = this.container.querySelector(`input[name="order"][value="${order}"]`);
|
if (orderInput) {
|
orderInput.checked = true;
|
this.updateOrderFilters(contentType || this.options.defaultContent);
|
}
|
}
|
|
const direction = params.get('f_direction');
|
if (direction) {
|
const directionInput = this.container.querySelector(`input[name="direction"][value="${direction}"]`);
|
if (directionInput) {
|
directionInput.checked = true;
|
}
|
}
|
|
// Set favourites filter
|
if (params.get('f_favourites') === '1') {
|
const favouritesCheckbox = this.container.querySelector('input[name="favourites_only"]');
|
if (favouritesCheckbox) {
|
favouritesCheckbox.checked = true;
|
}
|
}
|
|
// Find and set taxonomy filters
|
const taxonomyPromises = {};
|
|
// Process all potential taxonomy parameters
|
for (const [key, value] of params.entries()) {
|
if (key.startsWith('f_') && key !== 'f_content' &&
|
key !== 'f_order' && key !== 'f_direction' &&
|
key !== 'f_favourites' && key !== 'f_match') {
|
|
const taxName = key.replace('f_', '');
|
const termId = value;
|
|
// Create a promise to fetch term details and create a filter tag
|
if(!Object.hasOwnProperty(taxName)){
|
taxonomyPromises[taxName] = [];
|
}
|
taxonomyPromises[taxName].push(termId);
|
}
|
}
|
//Check all terms at once
|
this.fetchTermDetails(taxonomyPromises).then(()=>{
|
this.updateClearFiltersButton();
|
});
|
}
|
|
async fetchTermDetails(terms) {
|
for(const [taxonomy, termIds] of Object.entries(terms)){
|
|
termIds.forEach(termId => {
|
if(this.termCache.terms.has(taxonomy) && this.termCache.terms.get(taxonomy)[termId]){
|
//add to Selected from cache.
|
|
let name = this.termCache.terms.get(taxonomy)[termId].name;
|
const filterTag = this.createFilterTag(taxonomy, termId, name);
|
this.selectedFiltersContainer.appendChild(filterTag);
|
//remove any terms we already have
|
terms[taxonomy] = terms[taxonomy].filter(function(item) {
|
return item !== termId;
|
});
|
}
|
});
|
}
|
let params = new URLSearchParams(terms);
|
|
// Otherwise fetch the term details
|
try {
|
// Format the URL - might need to adjust based on your API structure
|
const url = `${window.feedSettings?.apiUrl}terms/check?`+params.toString();
|
|
const response = await fetch(url, {
|
headers: {
|
'X-WP-Nonce': window.feedSettings?.nonce || ''
|
}
|
});
|
|
if (!response.ok) {
|
throw new Error('Failed to fetch term details');
|
}
|
|
const termData = await response.json();
|
|
for(const [taxonomy, termIds] of Object.entries(termData.terms)){
|
if(!this.termCache.terms.has(taxonomy)){
|
this.termCache.terms.set(taxonomy, new Map());
|
}
|
|
for(const [termId, termData] of Object.entries(termIds)){
|
const filterTag = this.createFilterTag(taxonomy, termId, termData.name);
|
this.selectedFiltersContainer.appendChild(filterTag);
|
|
this.termCache.terms.get(taxonomy).set(parseInt(termId), termData);
|
}
|
|
}
|
|
|
return true;
|
} catch (error) {
|
console.error(`Error fetching term details for ${taxonomy}/${termId}:`, error);
|
return null;
|
}
|
}
|
/**
|
* Safe HTML escaping
|
*/
|
escapeHtml(text) {
|
if (!text) return '';
|
|
return text
|
.replace(/&/g, "&")
|
.replace(/</g, "<")
|
.replace(/>/g, ">")
|
.replace(/"/g, """)
|
.replace(/'/g, "'");
|
}
|
|
/**
|
* Set up focus traps for modals
|
*/
|
setupFocusTraps() {
|
this.filterDropdowns.forEach(dialog => {
|
dialog.addEventListener('show', () => {
|
// Store current focus to restore later
|
this._previouslyFocused = document.activeElement;
|
|
// Focus first focusable element
|
const focusable = dialog.querySelectorAll(
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
);
|
if (focusable.length) {
|
focusable[0].focus();
|
}
|
|
// Trap focus in modal
|
this.trapFocus(dialog);
|
});
|
|
dialog.addEventListener('close', () => {
|
// Restore focus
|
if (this._previouslyFocused) {
|
this._previouslyFocused.focus();
|
}
|
});
|
});
|
}
|
|
/**
|
* Trap focus within an element
|
*/
|
trapFocus(element) {
|
const focusableElements = element.querySelectorAll(
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
);
|
if (!focusableElements.length) return;
|
|
const firstFocusable = focusableElements[0];
|
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
const trapHandler = function(e) {
|
if (e.key !== 'Tab') return;
|
|
if (e.shiftKey && document.activeElement === firstFocusable) {
|
lastFocusable.focus();
|
e.preventDefault();
|
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
|
firstFocusable.focus();
|
e.preventDefault();
|
}
|
};
|
|
// Store the handler to remove it later
|
element._trapHandler = trapHandler;
|
element.addEventListener('keydown', trapHandler);
|
}
|
}
|
|
export default FilterPanel;
|