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.