291 lines
12 KiB
JavaScript
291 lines
12 KiB
JavaScript
// ===========================================
|
|
// REUSABLE DATATABLE COLUMN SEARCH UTILITY
|
|
// ===========================================
|
|
/**
|
|
* Initialize column-specific search functionality for DataTable
|
|
* @param {Object} config - Configuration object
|
|
* @param {string} config.tableId - DataTable selector (e.g., '#PRItemTable')
|
|
* @param {Object} config.dataTable - DataTable instance
|
|
* @param {Array} config.searchableColumns - Array of column configurations
|
|
* @param {string} config.containerId - Container ID for search inputs (optional)
|
|
* @param {string} config.searchInputClass - CSS class for search inputs (optional)
|
|
*/
|
|
function initializeColumnSearch(config) {
|
|
const {
|
|
tableId,
|
|
dataTable,
|
|
searchableColumns,
|
|
containerId = null,
|
|
searchInputClass = 'column-search-input'
|
|
} = config;
|
|
|
|
// Create unique class name based on table ID to avoid conflicts
|
|
const uniqueSearchClass = searchInputClass + '-' + tableId.replace('#', '');
|
|
|
|
// Create search container if not provided
|
|
let searchContainer;
|
|
if (containerId) {
|
|
searchContainer = $('#' + containerId);
|
|
} else {
|
|
// Create default search container above the table with unique ID
|
|
const containerClass = 'column-search-container-' + tableId.replace('#', '');
|
|
|
|
// Remove existing search container for this table if it exists
|
|
$('.' + containerClass).remove();
|
|
|
|
searchContainer = $('<div class="column-search-container ' + containerClass + ' mb-3"></div>');
|
|
$(tableId).parent().prepend(searchContainer);
|
|
}
|
|
|
|
// Clear existing search inputs
|
|
searchContainer.empty();
|
|
|
|
// Unbind any existing events for this specific table's search inputs
|
|
$('.' + uniqueSearchClass).off('keyup change');
|
|
$('.clear-column-search-' + tableId.replace('#', '')).off('click');
|
|
|
|
// Create search inputs for each specified column
|
|
searchableColumns.forEach(function (columnConfig) {
|
|
const columnIndex = columnConfig.columnIndex;
|
|
const columnName = columnConfig.columnName;
|
|
const placeholder = columnConfig.placeholder;
|
|
const searchType = columnConfig.searchType || 'text';
|
|
const searchMode = columnConfig.searchMode || 'contains';
|
|
const selectOptions = columnConfig.selectOptions || null;
|
|
const width = columnConfig.width || 'auto';
|
|
|
|
// Create input wrapper
|
|
const inputWrapper = $('<div class="d-inline-block me-2 mb-2" style="width: ' + width + ';"><label class="form-label small">' + columnName + ':</label></div>');
|
|
|
|
let searchInput;
|
|
|
|
// Create different input types based on searchType
|
|
switch (searchType) {
|
|
case 'select':
|
|
searchInput = $('<select class="' + uniqueSearchClass + ' form-select form-select-sm" data-column="' + columnIndex + '" data-column-name="' + columnName + '" data-search-mode="' + searchMode + '" data-table-id="' + tableId + '"><option value="">All ' + columnName + '</option></select>');
|
|
|
|
// Add options if provided
|
|
if (selectOptions && Array.isArray(selectOptions)) {
|
|
selectOptions.forEach(function (option) {
|
|
searchInput.append('<option value="' + option.value + '">' + option.text + '</option>');
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'date':
|
|
searchInput = $('<input type="date" class="' + uniqueSearchClass + ' form-control form-control-sm" data-column="' + columnIndex + '" data-column-name="' + columnName + '" data-search-mode="' + searchMode + '" data-table-id="' + tableId + '" placeholder="' + (placeholder || 'Search ' + columnName + '...') + '">');
|
|
break;
|
|
|
|
case 'number':
|
|
searchInput = $('<input type="number" class="' + uniqueSearchClass + ' form-control form-control-sm" data-column="' + columnIndex + '" data-column-name="' + columnName + '" data-search-mode="' + searchMode + '" data-table-id="' + tableId + '" placeholder="' + (placeholder || 'Search ' + columnName + '...') + '">');
|
|
break;
|
|
|
|
default: // text
|
|
searchInput = $('<input type="text" class="' + uniqueSearchClass + ' form-control form-control-sm" data-column="' + columnIndex + '" data-column-name="' + columnName + '" data-search-mode="' + searchMode + '" data-table-id="' + tableId + '" placeholder="' + (placeholder || 'Search ' + columnName + '...') + '">');
|
|
}
|
|
|
|
inputWrapper.append(searchInput);
|
|
searchContainer.append(inputWrapper);
|
|
});
|
|
|
|
// Add clear all button with unique class
|
|
const clearButtonClass = 'clear-column-search-' + tableId.replace('#', '');
|
|
const clearButton = $('<div class="d-inline-block me-2 mb-2"><label class="form-label small"> </label><button type="button" class="btn btn-outline-secondary btn-sm d-block ' + clearButtonClass + '">Clear All</button></div>');
|
|
searchContainer.append(clearButton);
|
|
|
|
// Bind search events with unique class
|
|
bindColumnSearchEvents(dataTable, uniqueSearchClass, tableId);
|
|
}
|
|
|
|
/**
|
|
* Bind search events to column search inputs
|
|
* @param {Object} dataTable - DataTable instance
|
|
* @param {string} uniqueSearchClass - Unique CSS class for search inputs
|
|
* @param {string} tableId - Table ID for unique identification
|
|
*/
|
|
function bindColumnSearchEvents(dataTable, uniqueSearchClass, tableId) {
|
|
// Individual column search
|
|
$('.' + uniqueSearchClass).off('keyup change').on('keyup change', function () {
|
|
const columnIndex = $(this).data('column');
|
|
const searchValue = $(this).val();
|
|
const searchMode = $(this).data('search-mode') || 'contains';
|
|
const inputTableId = $(this).data('table-id');
|
|
|
|
// Only process if this event is for the correct table
|
|
if (inputTableId !== tableId) {
|
|
return;
|
|
}
|
|
|
|
// Apply column-specific search with regex based on search mode
|
|
let searchRegex = '';
|
|
|
|
if (searchValue) {
|
|
switch (searchMode) {
|
|
case 'exact':
|
|
// Exact match - search for the exact value
|
|
searchRegex = '^' + escapeRegex(searchValue) + '$';
|
|
break;
|
|
case 'starts':
|
|
// Starts with - search for values that start with the input
|
|
searchRegex = '^' + escapeRegex(searchValue);
|
|
break;
|
|
case 'ends':
|
|
// Ends with - search for values that end with the input
|
|
searchRegex = escapeRegex(searchValue) + '$';
|
|
break;
|
|
default: // 'contains'
|
|
// Contains - default DataTable behavior (no regex needed)
|
|
searchRegex = searchValue;
|
|
}
|
|
}
|
|
|
|
// Apply search with regex if needed
|
|
if (searchMode !== 'contains' && searchValue) {
|
|
dataTable.column(columnIndex).search(searchRegex, true, false).draw();
|
|
} else {
|
|
dataTable.column(columnIndex).search(searchValue).draw();
|
|
}
|
|
|
|
// Update URL parameters (optional)
|
|
updateSearchParams($(this).data('column-name'), searchValue, tableId);
|
|
});
|
|
|
|
// Clear all searches with unique class
|
|
const clearButtonClass = 'clear-column-search-' + tableId.replace('#', '');
|
|
$('.' + clearButtonClass).off('click').on('click', function () {
|
|
$('.' + uniqueSearchClass).val('');
|
|
|
|
// Clear all column searches
|
|
dataTable.columns().search('').draw();
|
|
|
|
// Clear URL parameters (optional)
|
|
clearAllSearchParams(tableId);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Escape special regex characters
|
|
* @param {string} string - String to escape
|
|
* @returns {string} Escaped string
|
|
*/
|
|
function escapeRegex(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
/**
|
|
* Populate select options dynamically from table data
|
|
* @param {Object} dataTable - DataTable instance
|
|
* @param {number} columnIndex - Column index to extract unique values
|
|
* @param {string} selectSelector - jQuery selector for the select element
|
|
*/
|
|
function populateSelectOptions(dataTable, columnIndex, selectSelector) {
|
|
// Get unique values from the column
|
|
const uniqueValues = dataTable.column(columnIndex).data().unique().sort();
|
|
const selectElement = $(selectSelector);
|
|
|
|
// Clear existing options (except the first "All" option)
|
|
selectElement.find('option:not(:first)').remove();
|
|
|
|
// Add unique values as options
|
|
uniqueValues.each(function (value) {
|
|
if (value && value.toString().trim() !== '') {
|
|
selectElement.append('<option value="' + value + '">' + value + '</option>');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update URL search parameters (optional feature)
|
|
* @param {string} paramName - Parameter name
|
|
* @param {string} paramValue - Parameter value
|
|
* @param {string} tableId - Table ID for unique parameter names
|
|
*/
|
|
function updateSearchParams(paramName, paramValue, tableId) {
|
|
if (typeof URLSearchParams === 'undefined') return;
|
|
|
|
try {
|
|
const url = new URL(window.location);
|
|
const paramKey = 'search_' + tableId.replace('#', '') + '_' + paramName;
|
|
|
|
if (paramValue && paramValue.trim() !== '') {
|
|
url.searchParams.set(paramKey, paramValue);
|
|
} else {
|
|
url.searchParams.delete(paramKey);
|
|
}
|
|
|
|
// Wrap in try-catch to handle potential extension interference
|
|
window.history.replaceState({}, '', url);
|
|
} catch (error) {
|
|
// Silently ignore extension-related errors
|
|
if (!error.message.includes('listener indicated an asynchronous response')) {
|
|
console.warn('Error updating URL parameters:', error);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Clear all search parameters from URL for a specific table
|
|
* @param {string} tableId - Table ID to clear parameters for
|
|
*/
|
|
function clearAllSearchParams(tableId) {
|
|
if (typeof URLSearchParams === 'undefined') return;
|
|
|
|
const url = new URL(window.location);
|
|
const keysToDelete = [];
|
|
const tablePrefix = 'search_' + tableId.replace('#', '') + '_';
|
|
|
|
for (const key of url.searchParams.keys()) {
|
|
if (key.indexOf(tablePrefix) === 0) {
|
|
keysToDelete.push(key);
|
|
}
|
|
}
|
|
|
|
keysToDelete.forEach(function (key) {
|
|
url.searchParams.delete(key);
|
|
});
|
|
window.history.replaceState({}, '', url);
|
|
}
|
|
|
|
/**
|
|
* Restore search values from URL parameters for a specific table
|
|
* @param {string} uniqueSearchClass - Unique CSS class for search inputs
|
|
* @param {string} tableId - Table ID to restore parameters for
|
|
*/
|
|
function restoreSearchFromURL(uniqueSearchClass, tableId) {
|
|
if (typeof URLSearchParams === 'undefined') return;
|
|
|
|
const url = new URL(window.location);
|
|
const tablePrefix = 'search_' + tableId.replace('#', '') + '_';
|
|
|
|
$('.' + uniqueSearchClass).each(function () {
|
|
const columnName = $(this).data('column-name');
|
|
const inputTableId = $(this).data('table-id');
|
|
|
|
// Only restore for the correct table
|
|
if (inputTableId !== tableId) {
|
|
return;
|
|
}
|
|
|
|
const searchValue = url.searchParams.get(tablePrefix + columnName);
|
|
|
|
if (searchValue) {
|
|
$(this).val(searchValue).trigger('change');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clean up search functionality for a specific table (call this when destroying/reinitializing tables)
|
|
* @param {string} tableId - Table ID to clean up
|
|
*/
|
|
function cleanupColumnSearch(tableId) {
|
|
const uniqueSearchClass = 'column-search-input-' + tableId.replace('#', '');
|
|
const clearButtonClass = 'clear-column-search-' + tableId.replace('#', '');
|
|
const containerClass = 'column-search-container-' + tableId.replace('#', '');
|
|
|
|
// Unbind events
|
|
$('.' + uniqueSearchClass).off('keyup change');
|
|
$('.' + clearButtonClass).off('click');
|
|
|
|
// Remove search container
|
|
$('.' + containerClass).remove();
|
|
} |