PageSourceSearch

https://kupo.itenos.de/themes/custom/theme-itenos2/js/column-filter.js?v=1.x

js itenos.de collected 2026-10-01 15:56:06 UTC 15,296 bytes, 512 lines download raw bytes

1/**
2 * column-filter.js
3 * CSP-friendly, small column filter helper for DataTables 2.x (text, select, date)
4 *
5 */
6
7(function (window, $) {
8  'use strict';
9  if (!$ || !$.fn || !$.fn.DataTable) {
10    console.error('column-filter: jQuery/DataTables not found.');
11    return;
12  }
13
14  const DEBUG = false; // set true to see console.debug traces
15  const tableRegistry = new WeakMap();
16
17  function dlog() {
18    if (!DEBUG) return;
19    console.debug.apply(console, arguments);
20  }
21
22  function debounce(fn, wait) {
23    let timer = null;
24    return function debounced() {
25      const ctx = this;
26      const args = arguments;
27      if (timer) {
28        clearTimeout(timer);
29      }
30      timer = setTimeout(function () {
31        timer = null;
32        fn.apply(ctx, args);
33      }, wait);
34    };
35  }
36
37  function escRegex(s) {
38    return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39  }
40
41  function translateLabel(label) {
42    try {
43      if (typeof window.Drupal !== 'undefined' && typeof window.Drupal.t === 'function') {
44        return window.Drupal.t(label);
45      }
46    } catch (e) {
47    }
48    return label;
49  }
50
51  function attachClearButton(widget, onClear, label) {
52    const wrap = $('<div class="column-filter-wrap"></div>');
53    widget.wrap(wrap);
54
55    const btn = $('<button type="button" class="column-filter-clear" aria-label=""></button>');
56    btn.attr('aria-label', label || 'Clear filter');
57
58    widget.after(btn);
59
60    btn.on('click', function (e) {
61      e.preventDefault();
62      e.stopPropagation(); // prevent header sort toggling when clicking clear
63      widget.val('');
64      widget.trigger('change');
65      widget.trigger('keyup');
66      if (typeof onClear === 'function') onClear();
67      widget.trigger('focus');
68    });
69
70    // prevent clicks on the widget from bubbling up to the th (which triggers sort)
71    widget.on('click mousedown mouseup focus', function (e) {
72      e.stopPropagation();
73    });
74  }
75
76  function normalizeSelectOptions(raw) {
77    if (!Array.isArray(raw)) return [];
78
79    return raw.map(function (opt) {
80      let value, label;
81
82      if (typeof opt === 'object' && opt !== null) {
83        value = ('value' in opt) ? String(opt.value) : '';
84        label = ('label' in opt) ? String(opt.label) : value;
85
86        if (!value) {
87          value = JSON.stringify(opt);
88        }
89      } else {
90        value = String(opt);
91        label = String(opt);
92      }
93
94      return {
95        value: value,
96        label: label
97      };
98    });
99  }
100
101  function createFilterElement(api, colIndex, cfg) {
102    const column = api.column(colIndex);
103
104    if (!column || !column.header()) {
105      console.warn('column-filter: invalid column index', colIndex);
106      return null;
107    }
108
109    const th = $(column.header());
110
111    let container = th.find('.column-filter-container');
112    if (!container.length) {
113      container = $('<div class="column-filter-container"></div>');
114      th.append(container);
115    } else {
116      container.empty();
117    }
118
119    const defaultLabel = cfg && cfg.filter_default_label
120      ? (typeof cfg.filter_default_label === 'function'
121        ? cfg.filter_default_label()
122        : translateLabel(cfg.filter_default_label))
123      : '';
124
125    if ((cfg && Array.isArray(cfg.data)) || (cfg && cfg.filter_type === 'select')) {
126      const select = $('<select class="column-filter-select"></select>');
127      select.append($('<option>').attr('value', '').text(defaultLabel || '-'));
128
129      const normalizedOptions = normalizeSelectOptions(cfg && cfg.data ? cfg.data : []);
130      normalizedOptions.forEach(function (opt) {
131        select.append($('<option>').attr('value', opt.value).text(opt.label));
132      });
133
134      container.append(select);
135      return select;
136    }
137
138    if (cfg && cfg.filter_type === 'date') {
139      const input = $('<input type="date" class="column-filter-date" />');
140      if (cfg.min) input.attr('min', cfg.min);
141      if (cfg.max) input.attr('max', cfg.max);
142
143      input.on('click', function () {
144        if (typeof this.showPicker === 'function') {
145          try { this.showPicker(); } catch (e) {}
146        }
147      });
148
149      container.append(input);
150      return input;
151    }
152
153    const input = $('<input type="text" class="column-filter-text" />')
154      .attr('placeholder', defaultLabel || '');
155    container.append(input);
156    return input;
157  }
158
159  function rebuildSelectOptions(widget, cfg, options, selectedValue) {
160    if (!widget || !widget.length || !widget.is('select')) return;
161
162    const defaultLabel = cfg && cfg.filter_default_label
163      ? (typeof cfg.filter_default_label === 'function'
164        ? cfg.filter_default_label()
165        : translateLabel(cfg.filter_default_label))
166      : '';
167
168    widget.empty();
169    widget.append($('<option>').attr('value', '').text(defaultLabel || '-'));
170
171    normalizeSelectOptions(options).forEach(function (opt) {
172      widget.append($('<option>').attr('value', opt.value).text(opt.label));
173    });
174
175    if (typeof selectedValue !== 'undefined' && selectedValue !== null) {
176      widget.val(String(selectedValue));
177    } else {
178      widget.val('');
179    }
180  }
181
182  function formatDateToGerman(value) {
183    const s = String(value || '').trim();
184    const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
185
186    if (!m) return s;
187
188    return m[3] + '.' + m[2] + '.' + m[1];
189  }
190
191  function applyFilter(api, colIndex, value, cfg) {
192    if (!api || typeof api.column !== 'function') return;
193
194    if (value === null || value === undefined || value === '') {
195      api.column(colIndex).search('');
196      return;
197    }
198
199    // detect server-side processing for this table
200    const isServerSide = !!(api.settings && api.settings()[0] && api.settings()[0].oInit && api.settings()[0].oInit.serverSide);
201
202    if ((cfg && Array.isArray(cfg.data)) || (cfg && cfg.filter_type === 'select')) {
203      if (isServerSide) {
204        api.column(colIndex).search(String(value), false, false);
205      } else {
206        const v = '^' + escRegex(value) + '$';
207        api.column(colIndex).search(v, true, false);
208      }
209      return;
210    }
211
212    if (cfg && cfg.filter_type === 'date') {
213      const rawVal = String(value).trim();
214      const val = formatDateToGerman(rawVal);
215
216      if (isServerSide) {
217        api.column(colIndex).search(val, false, false);
218      } else {
219        api.column(colIndex).search(val ? '^' + escRegex(val) + '$' : '', true, false);
220      }
221      return;
222    }
223
224    api.column(colIndex).search(value, false, true);
225  }
226
227  function toApiInstance(oTable) {
228    let api = oTable;
229    try {
230      if (oTable && typeof oTable.column !== 'function') {
231        const jq = $(oTable);
232        if (jq && jq.length && typeof jq.DataTable === 'function') {
233          api = jq.DataTable();
234        } else if (jq && jq.length && typeof jq.dataTable === 'function') {
235          api = jq.dataTable();
236        }
237      }
238    } catch (e) {
239      api = null;
240    }
241    return api;
242  }
243
244  function getRegisteredFilter(api, columnNumber) {
245    const reg = tableRegistry.get(api);
246    if (!reg) return null;
247
248    if (reg.widgetsByColumn && reg.widgetsByColumn[columnNumber]) {
249      return reg.widgetsByColumn[columnNumber];
250    }
251
252    if (Array.isArray(reg.widgets)) {
253      for (let i = 0; i < reg.widgets.length; i++) {
254        if (reg.widgets[i] && reg.widgets[i].configuredCol === columnNumber) {
255          return reg.widgets[i];
256        }
257      }
258    }
259
260    return null;
261  }
262
263  function exResetAllFilters(oTable) {
264    const api = toApiInstance(oTable);
265    if (!api || typeof api.columns !== 'function') {
266      console.error('column-filter: invalid DataTable API instance passed to exResetAllFilters');
267      return;
268    }
269
270    api.columns().search('');
271
272    const entry = tableRegistry.get(api);
273    if (entry && entry.widgets && entry.widgets.length) {
274      entry.widgets.forEach(function (item) {
275        if (!item || !item.widget) return;
276        try {
277          item.widget.val('');
278          item.widget.trigger('change');
279          item.widget.trigger('keyup');
280        } catch (e) {
281
282        }
283      });
284    }
285
286    api.draw();
287  }
288
289  function setFilterValue(oTable, columnNumber, value, trigger) {
290    const api = toApiInstance(oTable);
291    if (!api || typeof api.column !== 'function') {
292      console.error('column-filter: invalid DataTable API instance passed to setFilterValue');
293      return false;
294    }
295
296    const item = getRegisteredFilter(api, columnNumber);
297    if (!item || !item.widget || !item.widget.length) {
298      console.warn('column-filter: no widget found for column', columnNumber);
299      return false;
300    }
301
302    item.widget.val(value == null ? '' : String(value));
303
304    if (trigger !== false) {
305      item.widget.trigger('change');
306    }
307
308    return true;
309  }
310
311  function updateSelectFilterOptions(oTable, columnNumber, options, opts) {
312    const api = toApiInstance(oTable);
313    if (!api || typeof api.column !== 'function') {
314      console.error('column-filter: invalid DataTable API instance passed to updateSelectFilterOptions');
315      return false;
316    }
317
318    const item = getRegisteredFilter(api, columnNumber);
319    if (!item || !item.widget || !item.widget.length) {
320      console.warn('column-filter: no widget found for column', columnNumber);
321      return false;
322    }
323
324    if (!item.widget.is('select')) {
325      console.warn('column-filter: widget for column is not a select filter', columnNumber);
326      return false;
327    }
328
329    opts = opts || {};
330
331    const preserveSelection = opts.preserveSelection !== false;
332    const triggerFilter = opts.triggerFilter === true;
333    const selectedValue = Object.prototype.hasOwnProperty.call(opts, 'selectedValue')
334      ? opts.selectedValue
335      : undefined;
336
337    const currentValue = item.widget.val();
338    const normalized = normalizeSelectOptions(options);
339
340    // keep runtime cfg in sync
341    item.cfg.data = Array.isArray(options) ? options.slice() : [];
342
343    let nextValue = '';
344
345    if (typeof selectedValue !== 'undefined') {
346      nextValue = selectedValue;
347    } else if (preserveSelection) {
348      const exists = normalized.some(function (opt) {
349        return opt.value === String(currentValue);
350      });
351      nextValue = exists ? currentValue : '';
352    }
353
354    rebuildSelectOptions(item.widget, item.cfg, options, nextValue);
355
356    if (triggerFilter) {
357      item.widget.trigger('change');
358    }
359
360    return true;
361  }
362
363  function syncFilterVisibility(api, colIndex) {
364    const column = api.column(colIndex);
365    if (!column || !column.header()) return;
366
367    const th = $(column.header());
368    const container = th.find('.column-filter-container');
369    container.toggle(column.visible());
370  }
371
372  function init(oTable, filters) {
373    const api = toApiInstance(oTable);
374    if (!api || typeof api.column !== 'function') {
375      console.error('column-filter: invalid DataTable API instance passed to init');
376      return;
377    }
378
379    const existing = tableRegistry.get(api);
380
381    if (existing) {
382      // clear old registry and old DOM containers on re-init
383      existing.filters = filters || [];
384      existing.widgets = [];
385      existing.widgetsByColumn = {};
386      tableRegistry.set(api, existing);
387
388      $(api.table().header()).find('.column-filter-container').remove();
389    } else {
390      tableRegistry.set(api, {
391        widgets: [],
392        widgetsByColumn: {},
393        filters: filters || []
394      });
395    }
396
397    const reg = tableRegistry.get(api);
398    // create / ensure widgets for each configured filter
399    (filters || []).forEach(function (cfg) {
400      const configuredCol = cfg.column_number;
401      // create element using configured index as a hint; createFilterElement will warn if invalid
402      const widget = createFilterElement(api, configuredCol, cfg);
403      if (!widget) return;
404
405      const item = {
406        configuredCol: configuredCol,
407        widget: widget,
408        cfg: cfg
409      };
410
411      reg.widgets.push(item);
412      reg.widgetsByColumn[configuredCol] = item;
413
414      attachClearButton(widget, function () {
415        dlog('clear clicked for col', configuredCol, 'cfg', cfg);
416        applyFilter(api, configuredCol, '', cfg);
417        api.draw();
418      }, translateLabel(cfg && cfg.clear_label ? cfg.clear_label : 'Clear filter'));
419
420      const delay = Number.isInteger(cfg.filter_delay) ? cfg.filter_delay : 300;
421
422      if (widget.is('select')) {
423        widget.on('change', function () {
424          const val = $(this).val();
425          dlog('select change', configuredCol, val, cfg);
426          applyFilter(api, configuredCol, val, cfg);
427          api.draw();
428        });
429      } else if (widget.hasClass('column-filter-date')) {
430        widget.on('change', function () {
431          const val = $(this).val();
432          dlog('date change', configuredCol, val, cfg);
433          applyFilter(api, configuredCol, val, cfg);
434          api.draw();
435        });
436      } else {
437        const apply = debounce(function () {
438          const val = widget.val();
439          dlog('text apply', configuredCol, val, cfg);
440          applyFilter(api, configuredCol, val, cfg);
441          api.draw();
442        }, delay);
443        widget.on('input change keyup', apply);
444      }
445
446      if (typeof cfg.filter_default !== 'undefined' && cfg.filter_default !== '') {
447        widget.val(cfg.filter_default).trigger('change');
448      }
449
450      // initial sync for visibility
451      syncFilterVisibility(api, configuredCol);
452    });
453
454    const $table = $(api.table().node());
455
456    // remove previous handlers for this table namespace, then rebind
457    $table.off('.columnFilter');
458
459    $table.on('column-visibility.dt.columnFilter', function (e, settings, column, state) {
460      try {
461        syncFilterVisibility(api, column);
462      } catch (err) {
463        dlog('column-visibility handler error', err);
464      }
465    });
466
467    //  move existing containers to match new header locations
468    $table.on('column-reorder.dt.columnFilter', function () {
469      try {
470        const reg = tableRegistry.get(api);
471        if (!reg || !reg.widgets) return;
472        dlog('column-reorder: repositioning widgets');
473
474        reg.widgets.forEach(function (item) {
475          try {
476            const widget = item.widget;
477            const container = widget.closest('.column-filter-container');
478            if (!container || !container.length) return;
479
480            const origCfgCol = item.configuredCol;
481            let targetTh = null;
482            try {
483              const hdr = api.column(origCfgCol).header();
484              if (hdr) targetTh = $(hdr);
485            } catch (e) {
486              targetTh = null;
487            }
488            if (!targetTh || !targetTh.length) {
489              targetTh = widget.closest('th');
490            }
491
492            targetTh.find('.column-filter-container').remove();
493            targetTh.append(container);
494            syncFilterVisibility(api, origCfgCol);
495          } catch (e) {
496            dlog('error repositioning widget', e);
497          }
498        });
499      } catch (err) {
500        dlog('column-reorder handler error', err);
501      }
502    });
503  }
504
505  window.columnFilter = window.columnFilter || {};
506  window.columnFilter.init = init;
507  window.columnFilter.exResetAllFilters = exResetAllFilters;
508  window.columnFilter.setFilterValue = setFilterValue;
509  window.columnFilter.updateSelectFilterOptions = updateSelectFilterOptions;
510  window.columnFilterInit = init;
511
512})(window, window.jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.