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 |
}; |