|
Line 0
Link Here
|
| 0 |
- |
1 |
/** |
|
|
2 |
* Additional filters library for Koha DataTables |
| 3 |
* |
| 4 |
* Provides boolean data attribute-based filter controls that integrate |
| 5 |
* seamlessly with kohaTable's additional_filters parameter. |
| 6 |
* |
| 7 |
* Template Usage: |
| 8 |
* [% INCLUDE 'additional-filters.inc' |
| 9 |
* filters = [ |
| 10 |
* { id = 'filter-expired', label_show = t('Include expired'), label_hide = t('Exclude expired') }, |
| 11 |
* { id = 'filter-cancelled', label_show = t('Include cancelled'), label_hide = t('Exclude cancelled') }, |
| 12 |
* ] |
| 13 |
* filter_class = 'bookings' # Optional, defaults to 'filters' |
| 14 |
* %] |
| 15 |
* |
| 16 |
* JavaScript Usage: |
| 17 |
* @example |
| 18 |
* const additional_filters = AdditionalFilters.init(['filter-expired', 'filter-cancelled']) |
| 19 |
* .onChange((filters, { anyFiltersApplied }) => { |
| 20 |
* table.column('status').visible(anyFiltersApplied); |
| 21 |
* }) |
| 22 |
* .build({ |
| 23 |
* status: ({ filters, isNotApplied }) => |
| 24 |
* isNotApplied(filters['filter-expired']) ? { '!=': 'expired' } : undefined |
| 25 |
* }); |
| 26 |
*/ |
| 27 |
|
| 28 |
window.AdditionalFilters = { |
| 29 |
/** |
| 30 |
* Initialize filter controls and attach event listeners |
| 31 |
* @param {string[]|Object} filterIds - Array of full element IDs or options object |
| 32 |
* @param {Function} [onFilterChange] - Callback when filters change |
| 33 |
* @param {Object} [options] - Configuration options |
| 34 |
* @param {string} [options.event='click'] - Event type to listen for |
| 35 |
* @param {string} [options.attribute='filtered'] - Data attribute name |
| 36 |
* @param {string} [options.closest='a'] - Element selector for event delegation |
| 37 |
* @param {boolean} [options.strict=true] - Log warnings for missing elements |
| 38 |
* @returns {AdditionalFiltersAPI} Chainable API object |
| 39 |
*/ |
| 40 |
init: function (filterIds, onFilterChange, options = {}) { |
| 41 |
if (typeof filterIds === "object" && !Array.isArray(filterIds)) { |
| 42 |
options = filterIds; |
| 43 |
filterIds = options.filterIds || []; |
| 44 |
onFilterChange = options.onFilterChange || onFilterChange; |
| 45 |
} |
| 46 |
|
| 47 |
const config = { |
| 48 |
event: options.event || "click", |
| 49 |
attribute: options.attribute || "filtered", |
| 50 |
closest: options.closest || "a", |
| 51 |
...options, |
| 52 |
}; |
| 53 |
|
| 54 |
const filters = {}; |
| 55 |
|
| 56 |
const isApplied = filter => |
| 57 |
filter?.hasAttribute(`data-${config.attribute}`); |
| 58 |
const isNotApplied = filter => !isApplied(filter); |
| 59 |
|
| 60 |
function attachFilter(elementId) { |
| 61 |
const element = document.getElementById(elementId); |
| 62 |
if (element) { |
| 63 |
filters[elementId] = element; |
| 64 |
element.addEventListener(config.event, handleFilter); |
| 65 |
} else if (config.strict !== false) { |
| 66 |
console.debug( |
| 67 |
`AdditionalFilters: Element not found with ID '${elementId}'` |
| 68 |
); |
| 69 |
} |
| 70 |
} |
| 71 |
|
| 72 |
function handleFilter(e) { |
| 73 |
const target = e.target; |
| 74 |
const filter = target.closest(config.closest); |
| 75 |
if (!filter) return; |
| 76 |
|
| 77 |
if (isApplied(filter)) { |
| 78 |
filter.removeAttribute(`data-${config.attribute}`); |
| 79 |
} else { |
| 80 |
filter.setAttribute(`data-${config.attribute}`, ""); |
| 81 |
} |
| 82 |
|
| 83 |
if (changeCallback) { |
| 84 |
changeCallback(filters, { |
| 85 |
anyFiltersApplied: Object.values(filters).some(isApplied), |
| 86 |
anyFiltersNotApplied: |
| 87 |
Object.values(filters).some(isNotApplied), |
| 88 |
isApplied, |
| 89 |
isNotApplied, |
| 90 |
}); |
| 91 |
} |
| 92 |
} |
| 93 |
|
| 94 |
filterIds.forEach(attachFilter); |
| 95 |
|
| 96 |
let filterDefinitions = {}; |
| 97 |
let changeCallback = onFilterChange; |
| 98 |
|
| 99 |
const api = { |
| 100 |
filters: filters, |
| 101 |
isApplied: isApplied, |
| 102 |
isNotApplied: isNotApplied, |
| 103 |
config: config, |
| 104 |
|
| 105 |
/** |
| 106 |
* Re-scan DOM for missing filter elements |
| 107 |
* @returns {AdditionalFiltersAPI} Chainable API |
| 108 |
*/ |
| 109 |
refresh: function () { |
| 110 |
filterIds.forEach(filterId => { |
| 111 |
if (!filters[filterId]) { |
| 112 |
attachFilter(filterId); |
| 113 |
} |
| 114 |
}); |
| 115 |
return api; |
| 116 |
}, |
| 117 |
|
| 118 |
/** |
| 119 |
* Set or update the filter change callback |
| 120 |
* @param {Function} callback - Called when filters change |
| 121 |
* @param {Object} callback.filters - Filter element map |
| 122 |
* @param {Object} callback.helpers - Helper functions and state |
| 123 |
* @param {boolean} callback.helpers.anyFiltersApplied - True if any filter is applied |
| 124 |
* @param {boolean} callback.helpers.anyFiltersNotApplied - True if any filter is not applied |
| 125 |
* @param {Function} callback.helpers.isApplied - Check if filter is applied |
| 126 |
* @param {Function} callback.helpers.isNotApplied - Check if filter is not applied |
| 127 |
* @returns {AdditionalFiltersAPI} Chainable API |
| 128 |
*/ |
| 129 |
onChange: function (callback) { |
| 130 |
changeCallback = callback; |
| 131 |
return api; |
| 132 |
}, |
| 133 |
|
| 134 |
/** |
| 135 |
* Clean up event listeners and references |
| 136 |
* @returns {void} |
| 137 |
*/ |
| 138 |
destroy: function () { |
| 139 |
Object.values(filters).forEach(filter => { |
| 140 |
if (filter) { |
| 141 |
filter.removeEventListener(config.event, handleFilter); |
| 142 |
} |
| 143 |
}); |
| 144 |
Object.keys(filters).forEach(key => delete filters[key]); |
| 145 |
changeCallback = null; |
| 146 |
}, |
| 147 |
|
| 148 |
/** |
| 149 |
* Add filter definitions for API parameters |
| 150 |
* @param {Object} definitions - Map of API parameters to generator functions |
| 151 |
* @returns {AdditionalFiltersAPI} Chainable API |
| 152 |
*/ |
| 153 |
withFilters: function (definitions) { |
| 154 |
filterDefinitions = { ...filterDefinitions, ...definitions }; |
| 155 |
return api; |
| 156 |
}, |
| 157 |
|
| 158 |
/** |
| 159 |
* Generate additional_filters object for kohaTable |
| 160 |
* @param {Object} [definitions] - Filter definitions to use |
| 161 |
* @returns {Object} additional_filters object for kohaTable |
| 162 |
*/ |
| 163 |
getAdditionalFilters: function (definitions) { |
| 164 |
const filtersToUse = definitions || filterDefinitions; |
| 165 |
const additionalFilters = {}; |
| 166 |
|
| 167 |
for (const [apiParam, generator] of Object.entries( |
| 168 |
filtersToUse |
| 169 |
)) { |
| 170 |
additionalFilters[apiParam] = () => { |
| 171 |
return generator({ |
| 172 |
filters, |
| 173 |
isApplied, |
| 174 |
isNotApplied, |
| 175 |
}); |
| 176 |
}; |
| 177 |
} |
| 178 |
|
| 179 |
return additionalFilters; |
| 180 |
}, |
| 181 |
|
| 182 |
/** |
| 183 |
* Set filter definitions and return additional_filters object |
| 184 |
* @param {Object} [definitions] - Filter definitions |
| 185 |
* @returns {Object} additional_filters object for kohaTable |
| 186 |
*/ |
| 187 |
build: function (definitions) { |
| 188 |
if (definitions) { |
| 189 |
filterDefinitions = { |
| 190 |
...filterDefinitions, |
| 191 |
...definitions, |
| 192 |
}; |
| 193 |
} |
| 194 |
return this.getAdditionalFilters(); |
| 195 |
}, |
| 196 |
}; |
| 197 |
|
| 198 |
return api; |
| 199 |
}, |
| 200 |
|
| 201 |
/** |
| 202 |
* Initialize filters when DOM is ready |
| 203 |
* @param {string[]|Object} filterIds - Array of full element IDs or options object |
| 204 |
* @param {Function} [onFilterChange] - Callback when filters change |
| 205 |
* @param {Object} [options] - Configuration options |
| 206 |
* @param {boolean} [options.allowEmpty] - Resolve even if no elements found |
| 207 |
* @returns {Promise<AdditionalFiltersAPI>} Promise resolving to API object |
| 208 |
*/ |
| 209 |
ready: function (filterIds, onFilterChange, options = {}) { |
| 210 |
return new Promise(resolve => { |
| 211 |
const tryInit = () => { |
| 212 |
const helper = this.init(filterIds, onFilterChange, options); |
| 213 |
if ( |
| 214 |
Object.keys(helper.filters).length > 0 || |
| 215 |
options.allowEmpty |
| 216 |
) { |
| 217 |
resolve(helper); |
| 218 |
} else { |
| 219 |
setTimeout(tryInit, 50); |
| 220 |
} |
| 221 |
}; |
| 222 |
|
| 223 |
if (document.readyState === "loading") { |
| 224 |
document.addEventListener("DOMContentLoaded", tryInit); |
| 225 |
} else { |
| 226 |
tryInit(); |
| 227 |
} |
| 228 |
}); |
| 229 |
}, |
| 230 |
}; |