Files
bqkc/_includes/js/browse-js.html
Nasir Anthony Montalvo 8ef34bd81d final beta edits
2026-01-26 00:36:55 -06:00

1057 lines
39 KiB
HTML

{% if site.data.theme.browse-child-objects == true %}
{%- assign items = site.data[site.metadata] | where_exp: 'item','item.objectid' -%}
{% else %}
{%- assign items = site.data[site.metadata] | where_exp: 'item','item.objectid and item.parentid == nil' -%}
{% endif %}
{%- assign fields = site.data.config-browse -%}
{% unless site.data.theme.faceted-search or site.data.theme.advanced-search %}
{% include js/browse-simple-js.html %}
{% else %}
<script>
/**
* BROWSE PAGE JAVASCRIPT - Less Janky Version
*
* Handles collection browsing with search, filtering, sorting, and URL state management.
* Supports both simple hash-based searches and advanced parameter-based searches.
*/
// =============================================================================
// INITIALIZATION & DATA SETUP
// =============================================================================
/**
* Initialize items array from Jekyll site data
*/
var items = [
{% for item in items %}
{ {% for f in fields %}{% if item[f.field] %}{{ f.field | escape | jsonify }}:{{ item[f.field] | strip | jsonify }}, {%- endif -%}{%- endfor -%}
{% if item.image_thumb %}"img": {{ item.image_thumb | relative_url | jsonify }}, {%- endif -%}
{% if item.display_template %}"display_template": {{ item.display_template | jsonify }}, {%- endif -%}
{% if item.format %}"format": {{ item.format | jsonify }}, {%- endif -%}
{% if item.image_alt_text %}"alt": {{ item.image_alt_text | escape | jsonify }}, {%- endif -%}
"title":{{ item.title | strip | escape | jsonify }},
{% if item.parentid %}"parent": {{ item.parentid | jsonify }}, {%- endif -%}
"id":{{ item.objectid | jsonify }} }{% unless forloop.last %},{% endunless %}{%- endfor -%}
];
// Valid field names for URL-based searches
const validFields = [{% for f in fields %}"{{ f.field }}",{% endfor %}"title","display_template"];
// Date field names - add your date fields here
const dateFields = ['date', 'created', 'published', 'modified', 'issued', 'date_created', 'date_published'];
// Cache DOM elements
var loadingIcon, filterTextBox, browseItemsDiv;
{% include helpers/get-icon.js %}
// =============================================================================
// CARD GENERATION
// =============================================================================
/**
* Creates HTML card for displaying a collection item
*/
function makeCard(obj) {
const placeholder = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3C/svg%3E";
const itemHref = `{{ '/items/' | relative_url }}${ obj.parent ? obj.parent + ".html#" + obj.id : obj.id + ".html"}`;
let card = '<div class="item col-lg-4 col-md-6 mb-2"><div class="card">';
// Image or icon
if(obj.img) {
card += `<a href="${itemHref}"><img class="card-img-top lazyload" src="${placeholder}" data-src="${obj.img}" alt="${obj.alt || obj.title}"></a>`;
}
// Title
card += `<div class="card-body text-center"><h3 class="card-title h4"><a href="${itemHref}" class="text-dark">${obj.title}</a></h3>`;
// Icon for items without images
if(!obj.img) {
const thumbIcon = getIcon(obj.display_template, obj.format, "thumb");
if(thumbIcon) card += `<p><a href="${itemHref}">${thumbIcon}</a></p>`;
}
// Field data
card += '<p class="card-text">';
{% for f in fields %}{% unless f.hidden == 'true' %}
if(obj[{{ f.field | jsonify }}]){
{% if f.display_name %}card += '<strong>{{ f.display_name }}:</strong> ';{% endif %}
{% if f.btn == 'true' %}
const btns = obj[{{ f.field | jsonify }}].split(";");
btns.forEach(btn => {
if(btn.trim()) {
card += `<a class="btn btn-sm btn-secondary m-1 text-wrap" href="{{ page.url | relative_url }}#{{ f.field }}:${encodeURIComponent(btn.trim())}">${btn.trim()}</a>`;
}
});
{% else %}
card += obj[{{ f.field | jsonify }}];
{% endif %}
{% unless forloop.last %}card += '<br>';{% endunless %}
}
{% endunless %}{% endfor %}
card += '</p>';
// Media type badge
if(obj.display_template) {
const mediaIcon = getIcon(obj.display_template, obj.format, "hidden");
card += `<p class="card-text"><small><a class="btn btn-sm btn-outline-secondary" href="{{ page.url | relative_url }}#display_template:${encodeURIComponent(obj.display_template)}">${obj.display_template.toUpperCase().replace("_"," ")} ${mediaIcon}</a></small></p>`;
}
card += `<hr><a href="${itemHref}" class="btn btn-sm btn-light" title="link to ${obj.title}">View Full Record</a></div></div></div>`;
return card;
}
// =============================================================================
// DATE UTILITIES
// =============================================================================
/**
* Check if a field name represents a date field
*/
function isDateField(fieldName) {
return dateFields.includes(fieldName.toLowerCase());
}
/**
* Normalize date string to consistent format for comparison
* Handles: yyyy, yyyy-mm, yyyy-mm-dd, mm/dd/yyyy
*/
function normalizeDateString(dateStr) {
if (!dateStr) return null;
const cleaned = dateStr.trim();
// Year only (4 digits)
if (/^\d{4}$/.test(cleaned)) {
return { normalized: cleaned, type: 'year', year: parseInt(cleaned) };
}
// Year-Month format yyyy-mm
const yearMonthMatch = cleaned.match(/^(\d{4})-(\d{1,2})$/);
if (yearMonthMatch) {
const [, year, month] = yearMonthMatch;
return {
normalized: `${year}-${month.padStart(2, '0')}`,
type: 'year-month',
year: parseInt(year),
month: parseInt(month)
};
}
// ISO format yyyy-mm-dd
const isoMatch = cleaned.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if (isoMatch) {
const [, year, month, day] = isoMatch;
return {
normalized: `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`,
type: 'full',
year: parseInt(year),
month: parseInt(month),
day: parseInt(day)
};
}
// US format mm/dd/yyyy
const usMatch = cleaned.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (usMatch) {
const [, month, day, year] = usMatch;
return {
normalized: `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`,
type: 'full',
year: parseInt(year),
month: parseInt(month),
day: parseInt(day)
};
}
return null;
}
/**
* Check if a date falls within a range (handles partial dates)
*/
function isDateInRange(itemDate, startDate, endDate) {
const itemDateObj = normalizeDateString(itemDate);
if (!itemDateObj) return false;
const startDateObj = startDate ? normalizeDateString(startDate) : null;
const endDateObj = endDate ? normalizeDateString(endDate) : null;
// Year-only comparison
if (itemDateObj.type === 'year') {
let inRange = true;
if (startDateObj) inRange = inRange && itemDateObj.year >= startDateObj.year;
if (endDateObj) inRange = inRange && itemDateObj.year <= endDateObj.year;
return inRange;
}
// Year-Month comparison
if (itemDateObj.type === 'year-month') {
let inRange = true;
if (startDateObj) {
// Convert start date to year-month format for comparison
let startNormalized;
if (startDateObj.type === 'year') {
startNormalized = `${startDateObj.year}-01`;
} else if (startDateObj.type === 'year-month') {
startNormalized = startDateObj.normalized;
} else {
startNormalized = startDateObj.normalized.substring(0, 7); // Extract yyyy-mm from yyyy-mm-dd
}
inRange = inRange && itemDateObj.normalized >= startNormalized;
}
if (endDateObj) {
// Convert end date to year-month format for comparison
let endNormalized;
if (endDateObj.type === 'year') {
endNormalized = `${endDateObj.year}-12`;
} else if (endDateObj.type === 'year-month') {
endNormalized = endDateObj.normalized;
} else {
endNormalized = endDateObj.normalized.substring(0, 7); // Extract yyyy-mm from yyyy-mm-dd
}
inRange = inRange && itemDateObj.normalized <= endNormalized;
}
return inRange;
}
// Full date comparison
if (itemDateObj.type === 'full') {
let inRange = true;
if (startDateObj) {
let startNormalized;
if (startDateObj.type === 'year') {
startNormalized = `${startDateObj.year}-01-01`;
} else if (startDateObj.type === 'year-month') {
startNormalized = `${startDateObj.normalized}-01`;
} else {
startNormalized = startDateObj.normalized;
}
inRange = inRange && itemDateObj.normalized >= startNormalized;
}
if (endDateObj) {
let endNormalized;
if (endDateObj.type === 'year') {
endNormalized = `${endDateObj.year}-12-31`;
} else if (endDateObj.type === 'year-month') {
// Get last day of the month
const year = endDateObj.year;
const month = endDateObj.month;
const lastDay = new Date(year, month, 0).getDate();
endNormalized = `${endDateObj.normalized}-${lastDay.toString().padStart(2, '0')}`;
} else {
endNormalized = endDateObj.normalized;
}
inRange = inRange && itemDateObj.normalized <= endNormalized;
}
return inRange;
}
return false;
}
// =============================================================================
// SEARCH & FILTER FUNCTIONS
// =============================================================================
/**
* Check if item matches search criteria (handles semicolon-separated values)
*/
function itemMatches(item, field, query, startDate, endDate) {
// Date field search
if (isDateField(field) && (startDate || endDate)) {
if (!item[field]) return false;
const fieldValues = item[field].toString().split(";");
return fieldValues.some(value => isDateInRange(value.trim(), startDate, endDate));
}
// Text search
if (query && query !== "") {
const q = query.trim().toUpperCase();
// Search all fields
if (field === "all") {
for (let k in item) {
if (k !== "img" && item[k] && item[k].toString().toUpperCase().includes(q)) {
return true;
}
}
return false;
}
// Search specific field
if (item[field]) {
const fieldValues = item[field].toString().split(";");
return fieldValues.some(value => value.trim().toUpperCase().includes(q));
}
}
return false;
}
/**
* Basic filter - searches single field with single query
*/
function filterItems(arr, query, field = "all", startDate = "", endDate = "") {
loadingIcon.classList.remove("d-none");
let filteredItems = arr;
// Apply filter if we have search criteria
if (query || startDate || endDate) {
filteredItems = arr.filter(item => itemMatches(item, field, query, startDate, endDate));
}
updateDisplay(filteredItems, query, field, startDate, endDate);
loadingIcon.classList.add("d-none");
}
/**
* Advanced filter - handles multiple search criteria with boolean logic
*/
function advancedFilterItems(arr, searchCriteria) {
loadingIcon.classList.remove("d-none");
const filteredItems = arr.filter(item => {
if (!searchCriteria?.length) return true;
let result = true;
for (let i = 0; i < searchCriteria.length; i++) {
const {boolean, field, value, startDate, endDate} = searchCriteria[i];
// Skip empty criteria
if (!value && !startDate && !endDate) continue;
const matches = itemMatches(item, field, value, startDate, endDate);
// Apply boolean logic
switch(boolean) {
case 'OR': result = result || matches; break;
case 'NOT': result = result && !matches; break;
default: result = result && matches; break; // AND or first item
}
}
return result;
});
updateDisplay(filteredItems);
displayActiveFilters(searchCriteria);
loadingIcon.classList.add("d-none");
}
/**
* Update the display with filtered results
*/
function updateDisplay(filteredItems, query = "", field = "", startDate = "", endDate = "") {
// Use DocumentFragment for better performance when adding many cards
const fragment = document.createDocumentFragment();
const tempDiv = document.createElement('div');
// Generate all cards at once
const cardsHTML = filteredItems.map(item => makeCard(item)).join('');
tempDiv.innerHTML = cardsHTML;
// Move all cards to fragment
while (tempDiv.firstChild) {
fragment.appendChild(tempDiv.firstChild);
}
// Single DOM update
browseItemsDiv.innerHTML = '';
browseItemsDiv.appendChild(fragment);
// Update count
document.querySelector("#numberOf").innerHTML = `${filteredItems.length} of {{ items | size }} items`;
// Focus active input (only if needed)
if (query || startDate || endDate) {
const activeInput = document.querySelector('#filterTextBox:not([style*="display: none"]), #startDate, #endDate');
if (activeInput && document.activeElement !== activeInput) {
activeInput.focus();
}
}
// Show filter indicators for basic search
if (query || startDate || endDate) {
const searchCriteria = [{
boolean: 'AND',
field: field,
value: isDateField(field) ? `${startDate || ''} - ${endDate || ''}`.trim().replace(/^-\s*|-\s*$/g, '') : query,
startDate: startDate,
endDate: endDate
}];
displayActiveFilters(searchCriteria);
} else if (arguments.length <= 1) { // Only clear if not called from advancedFilterItems
displayActiveFilters([]);
}
}
// =============================================================================
// UI INTERACTION HANDLERS
// =============================================================================
/**
* Toggle between text and date inputs based on field selection
*/
function toggleDateInputs(fieldName, container) {
const textInput = container.querySelector('.search-input, #filterTextBox');
const dateInputs = container.querySelector('.date-range-inputs, #dateRangeInputs');
if (!textInput || !dateInputs) return;
if (isDateField(fieldName)) {
textInput.style.display = 'none';
dateInputs.style.display = 'block';
dateInputs.classList.add('active');
} else {
textInput.style.display = 'block';
dateInputs.style.display = 'none';
dateInputs.classList.remove('active');
}
}
/**
* Handle basic search form submission
*/
function submitFilter() {
const query = filterTextBox.value;
const fieldSelect = document.querySelector("#fieldSelect");
const field = fieldSelect ? fieldSelect.value : "all";
if (isDateField(field)) {
const startDate = document.querySelector("#startDate").value;
const endDate = document.querySelector("#endDate").value;
if (!startDate && !endDate) return false;
// Use URL parameters for date searches
const params = new URLSearchParams();
params.set('f0', field);
params.set('b0', 'AND');
if (startDate) params.set('start0', startDate);
if (endDate) params.set('end0', endDate);
params.set('v0', `${startDate || ''} - ${endDate || ''}`.trim().replace(/^-\s*|-\s*$/g, ''));
window.location = window.location.pathname + "?" + params.toString();
} else {
// Use hash for text searches
const hash = field === "all" ? query : `${field}:${query}`;
window.location.hash = encodeURIComponent(hash);
}
return false;
}
/**
* Reset all filters and return to base URL
*/
function resetFilter() {
// Clear UI
filterTextBox.value = "";
{% unless site.data.theme.faceted-search == false %}document.querySelector("#fieldSelect").value = "all";{% endunless%}
// Clear URL completely
if (window.location.hash || window.location.search) {
window.location = window.location.pathname;
return false;
}
// Reset advanced search form
const searchRows = document.getElementById('searchRows');
if (searchRows) {
searchRows.innerHTML = '';
for (let i = 0; i < 3; i++) addSearchRow();
}
filterItems(items, "", "all");
displayActiveFilters([]);
return false;
}
// =============================================================================
// URL STATE MANAGEMENT
// =============================================================================
/**
* Handle hash-based searches (simple searches and facet links)
*/
function handleHashChange() {
const hash = decodeURIComponent(location.hash.substr(1));
// ignore back to top hash
if (hash == 'maincontent') return;
// Don't interfere with parameter-based searches
if (!hash && window.location.search) return;
// Clear parameters if using hash
if (hash && window.location.search) {
window.location = window.location.pathname + "#" + hash;
return;
}
// No hash - show all items
if (!hash) {
filterTextBox.value = "";
{% unless site.data.theme.faceted-search == false %}document.querySelector("#fieldSelect").value = "all";{% endunless%}
filterItems(items, "", "all");
updateAdvancedSearchModal([]);
return;
}
// Parse field:query format
let field = 'all';
let query = hash;
if (hash.includes(":")) {
const [potentialField, hashQuery] = hash.split(":");
if (validFields.includes(potentialField)) {
field = potentialField;
query = hashQuery;
} else {
// Invalid field - search everything with just the query part
field = 'all';
query = hashQuery; // Use only the part after the colon, not the full hash
}
}
// Update UI and filter
filterTextBox.value = query;
{% unless site.data.theme.faceted-search == false %}
document.querySelector("#fieldSelect").value = field;{% endunless%}
filterItems(items, query, field);
// Update advanced search modal
if (query) {
updateAdvancedSearchModal([{boolean: 'AND', field: field, value: query}], true);
}
}
// =============================================================================
// ADVANCED SEARCH MODAL
// =============================================================================
/**
* Update advanced search modal to reflect current search state
*/
function updateAdvancedSearchModal(searchCriteria, forceUpdate = false) {
const searchRows = document.getElementById('searchRows');
if (!searchRows) return;
// Add default empty rows if no criteria
if (!searchCriteria.length && searchRows.children.length === 0) {
for (let i = 0; i < 3; i++) addSearchRow();
return;
}
// Rebuild rows from criteria
if (searchCriteria.length && (searchRows.children.length === 0 || forceUpdate)) {
searchRows.innerHTML = '';
searchCriteria.forEach(criteria => addSearchRow(criteria));
addSearchRow(); // Extra empty row
}
}
/**
* Add a search row to the advanced search modal
*/
function addSearchRow(values = {}) {
const template = document.getElementById('searchRowTemplate');
const searchRows = document.getElementById('searchRows');
const newRow = template.content.cloneNode(true);
const row = newRow.querySelector('.search-row');
// Set initial values
if (values.boolean) row.querySelector('.boolean-operator').value = values.boolean;
if (values.field) {
row.querySelector('.field-select').value = values.field;
toggleDateInputs(values.field, row);
}
if (values.value && !isDateField(values.field)) {
row.querySelector('.search-input').value = values.value;
}
if (values.startDate) {
const startInput = row.querySelector('.start-date');
if (startInput) startInput.value = values.startDate;
}
if (values.endDate) {
const endInput = row.querySelector('.end-date');
if (endInput) endInput.value = values.endDate;
}
// Hide boolean operator for first row
if (searchRows.children.length === 0) {
row.querySelector('.boolean-operator').style.visibility = 'hidden';
}
// Event handlers
row.querySelector('.field-select').addEventListener('change', e => toggleDateInputs(e.target.value, row));
row.querySelector('.remove-row').addEventListener('click', () => {
row.remove();
if (searchRows.children.length === 1) {
searchRows.querySelector('.boolean-operator').style.visibility = 'hidden';
}
});
// Enter key handlers
['.search-input', '.start-date', '.end-date'].forEach(selector => {
const input = row.querySelector(selector);
if (input) {
input.addEventListener('keypress', e => {
if (e.key === 'Enter') {
e.preventDefault();
submitAdvancedSearch();
}
});
}
});
searchRows.appendChild(row);
}
/**
* Submit advanced search form
*/
function submitAdvancedSearch(event) {
if (event?.preventDefault) event.preventDefault();
const form = document.getElementById('advancedSearchForm');
const rows = form.querySelectorAll('.search-row');
const params = new URLSearchParams();
// Build parameters from form rows
rows.forEach((row, index) => {
const boolean = index === 0 ? 'AND' : row.querySelector('.boolean-operator').value;
const field = row.querySelector('.field-select').value;
if (isDateField(field)) {
const startDate = row.querySelector('.start-date').value.trim();
const endDate = row.querySelector('.end-date').value.trim();
if (startDate || endDate) {
params.set(`b${index}`, boolean);
params.set(`f${index}`, field);
params.set(`v${index}`, `${startDate || ''} - ${endDate || ''}`.trim().replace(/^-\s*|-\s*$/g, ''));
if (startDate) params.set(`start${index}`, startDate);
if (endDate) params.set(`end${index}`, endDate);
}
} else {
const value = row.querySelector('.search-input').value.trim();
if (value) {
params.set(`b${index}`, boolean);
params.set(`f${index}`, field);
params.set(`v${index}`, value);
}
}
});
// Preserve sort
const activeSort = document.querySelector(".browse-sort-item.active");
if (activeSort) params.set('sort', activeSort.dataset.filter);
window.location = window.location.pathname + "?" + params.toString();
// Close modal
try {
const modal = bootstrap.Modal.getInstance(document.getElementById('advancedSearchModal'));
modal?.hide();
} catch (e) {
console.error('Could not close modal:', e);
}
return false;
}
// =============================================================================
// ACTIVE FILTER INDICATORS
// =============================================================================
/**
* Display active filter buttons below search box
*/
function displayActiveFilters(searchCriteria) {
const activeFiltersDiv = document.getElementById('activeFilters');
if (!activeFiltersDiv) return;
activeFiltersDiv.innerHTML = '';
if (!searchCriteria?.length) return;
const container = document.createElement('div');
container.className = 'd-flex flex-wrap align-items-center';
// Create filter buttons
searchCriteria.forEach((criteria, index) => {
if (!criteria.value) return;
const button = document.createElement('button');
button.className = `btn btn-sm btn-outline-${criteria.boolean === 'NOT' ? 'danger' : criteria.boolean === 'OR' ? 'warning' : 'success'} me-2 mb-1`;
button.type = 'button';
// Get field display name
let fieldName = criteria.field === 'all' ? 'All Fields' :
'display_template' === criteria.field ? 'Content Type' :
{% for f in fields %}'{{f.field}}' === criteria.field ? '{% if f.display_name %}{{f.display_name}}{% else %}{{f.field | capitalize }}{% endif %}' : {% endfor %}
criteria.field.charAt(0).toUpperCase() + criteria.field.slice(1).replace(/_/g, ' ');
// Format display value for dates
let displayValue = criteria.value;
if (isDateField(criteria.field) && (criteria.startDate || criteria.endDate)) {
if (criteria.startDate && criteria.endDate) {
displayValue = `${criteria.startDate} to ${criteria.endDate}`;
} else if (criteria.startDate) {
displayValue = `from ${criteria.startDate}`;
} else if (criteria.endDate) {
displayValue = `until ${criteria.endDate}`;
}
}
const showBoolean = searchCriteria.length > 1 && criteria.boolean !== 'AND';
button.innerHTML = `
<span class="me-1">${showBoolean ? criteria.boolean + ' ' : ''}${fieldName}: "${displayValue}"</span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
`;
button.addEventListener('click', () => {
if (window.location.hash && !window.location.search) {
resetFilter();
} else {
removeActiveFilter(index);
}
});
container.appendChild(button);
});
// Add "Add Field" button
const addButton = document.createElement('button');
addButton.type = 'button';
addButton.className = 'btn btn-outline-secondary btn-sm mb-1{% if site.data.theme.advanced-search == false %} d-none{% endif %}';
addButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"></path>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"></path>
</svg>
Add Field
`;
addButton.addEventListener('click', () => {
new bootstrap.Modal(document.getElementById('advancedSearchModal')).show();
});
container.appendChild(addButton);
activeFiltersDiv.appendChild(container);
}
/**
* Remove a specific active filter by rebuilding URL without it
*/
function removeActiveFilter(filterIndex) {
if (window.location.hash && !window.location.search) {
resetFilter();
return;
}
const urlParams = new URLSearchParams(window.location.search);
const newParams = new URLSearchParams();
let newIndex = 0;
// Rebuild parameters excluding the removed filter
let index = 0;
while (urlParams.has(`f${index}`)) {
if (index !== filterIndex) {
newParams.set(`b${newIndex}`, urlParams.get(`b${index}`));
newParams.set(`f${newIndex}`, urlParams.get(`f${index}`));
newParams.set(`v${newIndex}`, urlParams.get(`v${index}`));
if (urlParams.has(`start${index}`)) newParams.set(`start${newIndex}`, urlParams.get(`start${index}`));
if (urlParams.has(`end${index}`)) newParams.set(`end${newIndex}`, urlParams.get(`end${index}`));
newIndex++;
}
index++;
}
// Preserve sort
if (urlParams.has('sort')) newParams.set('sort', urlParams.get('sort'));
// Navigate to new URL or reset if no filters left
if (newIndex === 0) {
resetFilter();
} else {
window.location = window.location.pathname + '?' + newParams.toString();
}
}
// =============================================================================
// SORTING UTILITIES
// =============================================================================
/**
* Fisher-Yates shuffle for random sorting
*/
function shuffle(array) {
let m = array.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[array[m], array[i]] = [array[i], array[m]];
}
return array;
}
/**
* Sort items by specified field
*/
function sorting(jsonObject, keyToSortBy) {
jsonObject.sort((a, b) => {
let x = a[keyToSortBy];
let y = b[keyToSortBy];
if (typeof x === 'string') x = x.toUpperCase();
if (typeof y === 'string') y = y.toUpperCase();
return (x == null) ? 1 : (y == null) ? -1 : (x < y) ? -1 : (x > y) ? 1 : 0;
});
}
// =============================================================================
// INITIALIZATION & EVENT HANDLERS
// =============================================================================
/**
* Initialize page on load
*/
window.addEventListener('load', function() {
// Cache DOM elements
loadingIcon = document.querySelector("#loadingIcon");
filterTextBox = document.querySelector('#filterTextBox');
browseItemsDiv = document.querySelector("#browseItems");
// Initialize default sort
{% if site.data.theme.default-sort-field %}
const defaultSort = "{{ site.data.theme.default-sort-field }}".toLowerCase();
const matchingSort = document.querySelector(`[data-filter="${defaultSort}"]`);
if (matchingSort) {
sorting(items, defaultSort);
matchingSort.classList.add("active");
document.getElementById("sortFilter").innerHTML = matchingSort.textContent;
} else {
shuffle(items);
}
{% else %}
shuffle(items);
{% endif %}
// Parse URL and initialize search state
const urlParams = new URLSearchParams(window.location.search);
// Handle sort parameter
if (urlParams.has('sort')) {
const sortValue = urlParams.get('sort');
document.querySelectorAll('.browse-sort-item').forEach(option => {
option.classList.remove('active');
if (option.dataset.filter === sortValue) {
option.classList.add('active');
document.getElementById('sortFilter').innerHTML = option.textContent;
sortValue !== 'random' ? sorting(items, sortValue) : shuffle(items);
}
});
}
// Handle advanced search parameters
if (urlParams.toString()) {
const searchRows = document.getElementById('searchRows');
if (searchRows) searchRows.innerHTML = '';
let index = 0;
const searchCriteria = [];
// Parse URL parameters into search criteria
while (urlParams.has(`f${index}`)) {
const criteria = {
boolean: urlParams.get(`b${index}`),
field: urlParams.get(`f${index}`),
value: urlParams.get(`v${index}`)
};
if (urlParams.has(`start${index}`)) criteria.startDate = urlParams.get(`start${index}`);
if (urlParams.has(`end${index}`)) criteria.endDate = urlParams.get(`end${index}`);
searchCriteria.push(criteria);
if (searchRows) addSearchRow(criteria);
index++;
}
// Add extra empty row and apply filters
if (searchRows && searchCriteria.length > 0) addSearchRow();
advancedFilterItems(items, searchCriteria);
}
// Handle hash-based search
else if (window.location.hash) {
handleHashChange();
}
// Default: show all items
else {
const searchRows = document.getElementById('searchRows');
if (searchRows) {
searchRows.innerHTML = '';
for (let i = 0; i < 3; i++) addSearchRow();
}
filterItems(items, "", "all");
}
// Set up event handlers
setupEventHandlers();
});
/**
* Set up all event handlers
*/
function setupEventHandlers() {
// Hash change handler
window.addEventListener("hashchange", handleHashChange);
// Form submission handlers
document.getElementById('browseFilter').addEventListener('submit', e => {
e.preventDefault();
submitFilter();
return false;
});
const advancedSearchForm = document.getElementById('advancedSearchForm');
if (advancedSearchForm) {
advancedSearchForm.addEventListener('submit', e => {
e.preventDefault();
submitAdvancedSearch(e);
return false;
});
}
{% unless site.data.theme.faceted-search == false %}
// Field selection handlers
document.querySelector("#fieldSelect").addEventListener("change", function(e) {
const field = e.target.value;
if (field === 'advanced') {
e.target.value = 'all';
new bootstrap.Modal(document.getElementById('advancedSearchModal')).show();
return;
}
toggleDateInputs(field, document.querySelector('#browseFilter'));
document.querySelector("#selectedField").textContent = e.target.options[e.target.selectedIndex].text;
});
// Mobile dropdown handler
document.addEventListener('click', function(e) {
if (e.target.matches('.dropdown-item[data-field]')) {
const field = e.target.getAttribute('data-field');
const fieldSelect = document.getElementById('fieldSelect');
const selectedField = document.getElementById('selectedField');
if (field === 'advanced') {
new bootstrap.Modal(document.getElementById('advancedSearchModal')).show();
return;
}
if (fieldSelect) {
fieldSelect.value = field;
fieldSelect.dispatchEvent(new Event('change'));
}
if (selectedField) selectedField.textContent = e.target.textContent;
}
});{% endunless %}
// Sort option handlers
document.querySelectorAll(".browse-sort-item").forEach(button => {
button.addEventListener("click", event => {
const field = button.dataset.filter;
const displayName = button.textContent;
// Update active sort option
document.querySelectorAll(".browse-sort-item").forEach(opt => opt.classList.remove("active"));
button.classList.add("active");
document.getElementById("sortFilter").innerHTML = displayName;
// Apply sort
field !== 'random' ? sorting(items, field) : shuffle(items);
// Handle current search state
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString()) {
// Advanced search active - update URL with sort parameter
const searchCriteria = [];
let index = 0;
while (urlParams.has(`f${index}`)) {
const criteria = {
boolean: urlParams.get(`b${index}`),
field: urlParams.get(`f${index}`),
value: urlParams.get(`v${index}`)
};
if (urlParams.has(`start${index}`)) criteria.startDate = urlParams.get(`start${index}`);
if (urlParams.has(`end${index}`)) criteria.endDate = urlParams.get(`end${index}`);
searchCriteria.push(criteria);
index++;
}
const newParams = new URLSearchParams();
searchCriteria.forEach((criteria, i) => {
newParams.set(`b${i}`, criteria.boolean);
newParams.set(`f${i}`, criteria.field);
newParams.set(`v${i}`, criteria.value);
if (criteria.startDate) newParams.set(`start${i}`, criteria.startDate);
if (criteria.endDate) newParams.set(`end${i}`, criteria.endDate);
});
newParams.set('sort', field);
window.history.pushState({}, '', window.location.pathname + '?' + newParams.toString());
advancedFilterItems(items, searchCriteria);
} else {
// Basic search - apply sort and current filter
const query = filterTextBox.value;
{% if site.data.theme.faceted-search == false %}
const searchField = 'all';
{% else %}
const searchField = document.querySelector("#fieldSelect").value;{% endif %}
filterItems(items, query, searchField);
}
});
});
// Advanced search modal handler
const advancedSearchModal = document.getElementById('advancedSearchModal');
if (advancedSearchModal) {
advancedSearchModal.addEventListener('show.bs.modal', function() {
// Reflect hash-based search in modal
if (window.location.hash && !window.location.search) {
const hash = decodeURIComponent(location.hash.substr(1));
if (!hash) return;
let field = 'all';
let query = hash;
if (hash.includes(":")) {
const [potentialField, hashQuery] = hash.split(":");
if (validFields.includes(potentialField)) {
field = potentialField;
query = hashQuery;
}
}
updateAdvancedSearchModal([{boolean: 'AND', field: field, value: query}]);
}
});
}
}
// Initialize tooltips for the "Content Type" dropdown
document.addEventListener('DOMContentLoaded', function () {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
});
</script>
{% endunless %}