// =========================================== // 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 = $('
'); $(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 = $('
'); let searchInput; // Create different input types based on searchType switch (searchType) { case 'select': searchInput = $(''); // Add options if provided if (selectOptions && Array.isArray(selectOptions)) { selectOptions.forEach(function (option) { searchInput.append(''); }); } break; case 'date': searchInput = $(''); break; case 'number': searchInput = $(''); break; default: // text searchInput = $(''); } inputWrapper.append(searchInput); searchContainer.append(inputWrapper); }); // Add clear all button with unique class const clearButtonClass = 'clear-column-search-' + tableId.replace('#', ''); const clearButton = $('
'); 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(''); } }); } /** * 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(); }