1048 lines
38 KiB
HTML
1048 lines
38 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}]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
{% endunless %} |