PageSourceSearch

https://assets.webis.de/js/filter.js

js webis.de collected 2026-10-01 10:17:34 UTC 19,734 bytes, 535 lines download raw bytes

1////////////////////////////////////////////////////
2// Generic code for filter-based history manipulation
3////////////////////////////////////////////////////
4
5const historyNewStateThresholdMs = 1000;
6let lastHistoryChangeMs = new Date().getTime();
7
8function updateUrlQueryParameter(query) {
9  const url = new URL(document.location.href);
10  if (query.trim() === "") {
11    if (!url.searchParams.has("q")) {
12      return;
13    } else {
14      url.searchParams.delete("q");
15    }
16  } else {
17    if (url.searchParams.has("q") && url.searchParams.get("q") === query) {
18      return;
19    } else {
20      url.searchParams.set("q", query);
21    }
22  }
23
24  const now = new Date().getTime();
25  const msSinceLastHistoryChange = now - lastHistoryChangeMs;
26  lastHistoryChangeMs = now;
27
28  if (msSinceLastHistoryChange >= historyNewStateThresholdMs) {
29    history.pushState({ query: query }, "", url);
30  } else {
31    history.replaceState({ query: query }, "", url);
32  }
33}
34
35
36////////////////////////////////////////////////////
37// Generic code for filtering
38////////////////////////////////////////////////////
39
40function containQuery(attributes, queryWords) {
41  for (let q = 0; q < queryWords.length; ++q) {
42    let queryWord = queryWords[q].replace(/\+/g, " ");
43    let found = false;
44
45    const attributeSpecificatorPos = queryWord.indexOf(":");
46    if (attributeSpecificatorPos >= 0) {
47      let attribute = queryWord.substr(0, attributeSpecificatorPos);
48      if (attribute == "tags") { attribute = "keywords"; } // legacy attribute name
49      queryWord = queryWord.substr(attributeSpecificatorPos + 1);
50      if (attributes.hasOwnProperty(attribute)) {
51        const attributeValue = attributes[attribute];
52        if (attributeValue.indexOf(queryWord) >= 0) {
53          found = true;
54        }
55      }
56    } else {
57      for (let a in attributes) {
58        const attributeValue = attributes[a];
59        if (attributeValue.indexOf(queryWord) >= 0) {
60          found = true;
61          break;
62        }
63      }
64    }
65    if (!found) {
66      return false;
67    }
68  }
69  return true;
70};
71
72function getSubGroupHeader(group, element) {
73  const subGroupId = element.dataset['subGroup'];
74  if (subGroupId === undefined) {
75    return null;
76  } else {
77    const subGroupHeader = group.querySelector('[data-sub-group-header="' + subGroupId + '"]');
78    return subGroupHeader;
79  }
80}
81
82function filterByQuery(query, groups, elementSelector, updateUrlQueryParam = true) {
83  query = query.trim();
84  let filteredAll = true;
85  if (query === "") {
86    for (let g = 0; g < groups.length; ++g) {
87      groups[g].classList.remove("uk-hidden");
88      const elements = groups[g].querySelectorAll(elementSelector);
89      for (let e = 0; e < elements.length; ++e) {
90        elements[e].classList.remove("uk-hidden");
91      }
92    }
93    filteredAll = false;
94  } else {
95    const queryWords = normalize(query, protectCommata = false, protectQueryModifiers = true).split(/\s+/);
96    for (let g = 0; g < groups.length; ++g) {
97      const group = groups[g];
98      let filteredAllOfGroup = true;
99      let visibleSubGroupHeaders = [];
100      const elements = group.querySelectorAll(elementSelector);
101      for (let e = 0; e < elements.length; ++e) {
102        const element = elements[e];
103        const subGroupHeader = getSubGroupHeader(group, element);
104        if (subGroupHeader !== null && !subGroupHeader.classList.contains("uk-hidden")) {
105          element.classList.remove("uk-hidden");
106          filteredAllOfGroup = false;
107        } else {
108          const attributes = element.dataset;
109          if (containQuery(attributes, queryWords)) {
110            element.classList.remove("uk-hidden");
111            filteredAllOfGroup = false;
112            if (subGroupHeader !== null) { visibleSubGroupHeaders.push(subGroupHeader) };
113          } else {
114            element.classList.add("uk-hidden");
115          }
116        }
117      }
118
119      visibleSubGroupHeaders.forEach(
120          subGroupHeader => subGroupHeader.classList.remove("uk-hidden"));
121
122      if (filteredAllOfGroup) {
123        group.classList.add("uk-hidden");
124      } else {
125        group.classList.remove("uk-hidden");
126        filteredAll = false;
127      }
128    }
129  }
130
131  const filteredAllMessage = document.getElementById("filtered-all-message");
132  if (filteredAllMessage !== null) {
133    if (filteredAll) {
134      filteredAllMessage.classList.remove("uk-hidden");
135      filteredAllMessage.removeAttribute("aria-hidden");
136    } else {
137      filteredAllMessage.classList.add("uk-hidden");
138      filteredAllMessage.setAttribute("aria-hidden", "true");
139    }
140  }
141
142  if (updateUrlQueryParam) { // webis.de page
143    updateUrlQueryParameter(query);
144  }
145
146  if (typeof UIkit !== "undefined") {
147    // Force UIkit update to prevent glitches
148    UIkit.update();
149  }
150
151  return filteredAll;
152};
153
154function normalize(value, protectCommata = false, protectQueryModifiers = false) {
155  const regexBase = /[^\u{61}-\u{7a}\u{df}-\u{f6}\u{f8}-\u{ff}\u{100}-\u{17F}0-9\-,:+\s]/gu;
156  const regexCommata = /[,]/g;
157  const regexQueryModifiers = /[:+]/g;
158  const regexWhitespace = /[\s]/g;
159  
160  let tmp = value.toLowerCase().normalize("NFD").replace(/[\u{300}-\u{36f}]/gu, "").replace(regexBase, "");
161  if (!protectCommata) tmp = tmp.replace(regexCommata, "");
162  if (!protectQueryModifiers) tmp = tmp.replace(regexQueryModifiers, "");
163  tmp = tmp.replace(regexWhitespace, " ");
164
165  return tmp;
166};
167
168function removeHyphenationPossibilities(value) {
169  return value.replace(/&shy;/g, "");
170};
171
172/*
173 * groups: node list of group nodes for which the attributes should be populated
174 * elementSelector: query selector that specifies each element within a group to be populated
175 * dataAttributesPopulationFunction: a function that takes the DOM node of an element and sets the data-attributes of the respective node
176 */
177function populateDataAttributesForGroups(groups, elementSelector, dataAttributesPopulationFunction) {
178  for (let g = 0; g < groups.length; ++g) {
179    const elements = groups[g].querySelectorAll(elementSelector);
180    populateDataAttributesForElements(elements, dataAttributesPopulationFunction)
181  }
182}
183
184/*
185 * elements: node list of elements for which the attributes should be populated
186 * dataAttributesPopulationFunction: a function that takes the DOM node of an element and sets the data-attributes of the respective node
187 */
188function populateDataAttributesForElements(elements, dataAttributesPopulationFunction) {
189  for (let e = 0; e < elements.length; ++e) {
190    dataAttributesPopulationFunction(elements[e]);
191    // remove &shy; from all attributes (Chrome seems to insert them automatically)
192    const dataset = elements[e].dataset;
193    for (let a in dataset) {
194      dataset[a] = removeHyphenationPossibilities(dataset[a]);
195    }
196  }
197}
198
199/*
200 * node: the element for which to set the data-attributes
201 */
202function defaultDataAttributesPopulationFunction(node) {
203  const attributes = node.dataset;
204  for (let a in attributes) {
205    if (a == "author" || a == "tags" || a == "editor" || a == "artifacts" || a == "mentor") {
206      attributes[a] = normalize(attributes[a], protectCommata = true, protectQueryModifiers = false);
207    } else {
208      attributes[a] = normalize(attributes[a]);
209    }
210  }
211  attributes['text'] = normalize(node.textContent);
212  attributes['fields'] = Object.keys(attributes).join(" ");
213  attributes['links'] = Array.from(node.querySelectorAll("a")).map(link => link.getAttribute("href")).join(" ");
214  return attributes;
215}
216
217/*
218 * filterFunction: a function that takes a filter query and filters the list on the page accordingly
219 * filterField: the input field to initialize
220 */
221function initializeFilterField(filterFunction, filterField = document.getElementById("filter-field")) {
222  if (filterField !== null) {
223    // remove spurious "\"
224    if (document.location.search.indexOf("\\") > 0) {
225        document.location.search = document.location.search.replace(/\\/g, "");
226    }
227    let params = new URLSearchParams(document.location.search);
228
229    // Set up filter field
230    if ((params.has("q")) && (params.get("q") !== "")) {
231        const query = params.get("q");
232        filterField.value = query;
233    }
234    filterField.addEventListener("input", event => filterFunction(event.target.value));
235    filterFunction(filterField.value);
236    if (document.location.hash === "") { // only focus filterField if no fragment identifier present
237        filterField.focus();
238    }
239
240    // Update if query in URL changed (e.g., browser back button)
241    window.addEventListener("popstate", event => {
242      let params = new URL(document.location).searchParams;
243
244      if (params.has("q")) {
245        const query = params.get("q");
246        if (query !== filterField.value) {
247          filterField.value = query;
248          filterFunction(query);
249        }
250      } else {
251        filterField.value = "";
252        filterFunction("");
253      }
254    });
255  }
256}
257
258/*
259 * groups: the groups in which the elements should be filtered
260 * elementSelector: query selector that specifies each element within a group to be filtered
261 * updateHash: Whether to update the window.location.hash on filtering
262 */
263function makeFilterFunction(groups, elementSelector, updateUrlQueryParam = true) {
264  const filterFunction = (query) => {
265    return filterByQuery(query, groups, elementSelector, updateUrlQueryParam);
266  };
267  return filterFunction;
268}
269
270/*
271 * groups: the groups in which the elements should be filtered
272 * elementSelector: query selector that specifies each element within a group to be filtered
273 * updateHash: Whether to update the window.location.hash on filtering
274 * dataAttributesPopulationFunction: a function that takes the DOM node of an element and sets the data-attributes of the respective node
275 */
276function initWebisFiltering(groups, elementSelector, updateUrlQueryParam = true, dataAttributesPopulationFunction = defaultDataAttributesPopulationFunction) {
277  populateDataAttributesForGroups(groups, elementSelector, dataAttributesPopulationFunction);
278  const filterFunction = makeFilterFunction(groups, elementSelector, updateUrlQueryParam);
279  initializeFilterField(filterFunction);
280  return filterFunction;
281}
282
283/*
284 * Chrome does not apply the :target selector if the node with the ID was added after the hash changed. This function changes the has back and forth (if one was set) so that Chrome applies the :target selector correctly.
285 */
286function updateCssTargetSelector() {
287  // fix for chrome to jump to anchor that was just inserted:
288  const hash = window.location.hash;
289  if (hash !== "") {
290    window.location.hash = "";
291    window.location.hash = hash;
292  }
293};
294
295/*
296 * parentElement: DOM element to which the list should be added
297 * sourceUrl: URL of the page that contains the list to be added
298 * listSelector: query selector to select the list in the source document
299 * listCallback: function that is called on the list before it is added to the parent element
300 */
301function includeList(parentElement, sourceUrl, listSelector, listCallback) {
302  parentElement.innerText = "Loading...";
303
304  const request = new XMLHttpRequest();
305  request.onload = function() {
306    const list = this.response.documentElement.querySelector(listSelector);
307    listCallback(list);
308    parentElement.innerText = "";
309    parentElement.appendChild(list);
310    updateCssTargetSelector();
311  }
312  request.open("GET", sourceUrl);
313  request.responseType = "document";
314  request.send();
315};
316
317// update legacy 'filter:' option
318if (document.location.hash.startsWith("#filter:")) {
319    const query = decodeURIComponent(document.location.hash.substr(8));
320    
321    let newUrl = new URL(document.location);
322    newUrl.hash = "";
323    newUrl.searchParams.set("q", query);
324    history.replaceState({ query: query }, document.title, newUrl.href);
325}
326
327// update legacy '#?q=' option
328if (document.location.hash.startsWith("#?q=")) {
329    const query = decodeURIComponent(document.location.hash.substr(4));
330
331    let newUrl = new URL(document.location);
332    newUrl.hash = "";
333    newUrl.searchParams.set("q", query);
334    history.replaceState({ query: query }, document.title, newUrl.href);
335}
336
337////////////////////////////////////////////////////
338// Specific code for common web page layouts
339////////////////////////////////////////////////////
340
341function initWebisTableFiltering(tables = document.querySelectorAll(".targetable"), updateUrlQueryParam = true, dataAttributesPopulationFunction = defaultDataAttributesPopulationFunction) {
342  const elementSelector = "tbody tr";
343  extendedDataAttributesPopulationFunction = entry => {
344    const attributes = dataAttributesPopulationFunction(entry);
345
346    // add name of table
347    attributes['table'] = normalize(entry.closest('table').querySelector('thead').querySelector('tr').textContent.trim());
348
349    // add name of table part
350    const groupHeader = entry.querySelector('th[id]');
351    if (groupHeader !== null) {
352      attributes['subGroupHeader'] = groupHeader.id;
353    } else {
354      let predecessor = entry.previousElementSibling;
355      while (predecessor !== null && predecessor.querySelector('th[id]') == null) {
356        predecessor = predecessor.previousElementSibling;
357      }
358      if (predecessor !== null) {
359        attributes['subGroup'] = predecessor.querySelector('th[id]').id;
360      }
361    }
362
363    return attributes;
364  };
365  return initWebisFiltering(tables, elementSelector, updateUrlQueryParam, extendedDataAttributesPopulationFunction);
366}
367
368function initWebisListFiltering(lists = document.querySelectorAll(".webis-list"), updateUrlQueryParam = true) {
369  const elementSelector = ".entry";
370  return initWebisFiltering(lists, elementSelector, updateUrlQueryParam, defaultDataAttributesPopulationFunction);
371}
372
373function initWebisParagraphsFiltering(paragraphs = document.querySelectorAll(".webis-paragraphs"), updateUrlQueryParam = true) {
374  const elementSelector = "p";
375  return initWebisFiltering(paragraphs, elementSelector, updateUrlQueryParam, defaultDataAttributesPopulationFunction);
376}
377
378////////////////////////////////////////////////////
379// Specific code for data
380////////////////////////////////////////////////////
381
382/*
383 * node: the element for which to set the data-attributes
384 */
385function dataTableDataAttributesPopulationFunction(node) {
386  const attributes = defaultDataAttributesPopulationFunction(node);
387  attributes['name'] = normalize(node.children[1].textContent);
388  attributes['publisher'] = normalize(node.children[2].textContent);
389  attributes['year'] = normalize(node.children[3].textContent);
390  attributes['units'] = normalize(node.children[6].textContent);
391  attributes['task'] = normalize(node.children[7].textContent);
392  const access = Array.from(node.children[8].querySelectorAll("a[title]")).map(aTag => normalize(aTag.getAttribute("title")));
393  if (access.length > 0) {
394    attributes['access'] = access.join(',');
395  }
396
397  return attributes;
398}
399
400function initWebisDataFiltering(tables = document.querySelectorAll(".targetable"), updateUrlQueryParam = true) {
401  if (typeof initTableSorting === "function") { // tables.js included
402    initTableSorting(tables);
403  }
404  return initWebisTableFiltering(tables, updateUrlQueryParam, dataTableDataAttributesPopulationFunction);
405}
406
407////////////////////////////////////////////////////
408// Specific code for publications
409////////////////////////////////////////////////////
410
411// Show BibTeX on click
412function activateBibtexToggle(root = document) {
413  root.querySelectorAll('.bib-toggle').forEach(el => el.addEventListener("click", (event) => {
414    event.preventDefault();
415
416    const bibtexId = event.target.dataset.target;
417    const bibtex = document.getElementById(bibtexId);
418
419    bibtex.classList.toggle("uk-hidden");
420    const isHidden = bibtex.classList.contains("uk-hidden");
421    if (!isHidden) {
422      bibtex.focus();
423    }
424    bibtex.setAttribute("aria-hidden", isHidden ? "true" : "false");
425
426    bibtex.style.height = "5px";
427    bibtex.style.height = (bibtex.scrollHeight + 5) + "px";
428  }));
429};
430
431// Generate fragment identifier in URL and copy URL to clipboard on click
432function activateShareLink(root = document) {
433  root.querySelectorAll('.copylink').forEach(el => el.addEventListener("click", (event) => {
434    // Prevent page reload for links with empty href (as needed by uni-weimar.de pages)
435    event.preventDefault();
436
437    const bibentry = event.target.parentElement;
438    const bibid = bibentry.previousElementSibling.id;
439
440    const hash = "#" + bibid;
441    history.pushState({ target: bibid }, document.title, hash);
442
443    // Always copy URL when clicking copylink link, even when selecting the same bibentry
444    const urlWithoutFilter = window.location.href.replace(window.location.search, "");
445    copyStringToClipboard(urlWithoutFilter);
446
447    // Display "copied to clipboard" for 1 s after clicking copylink link
448    var copiedSpan = document.createElement("span");
449    const copiedText = document.createTextNode("copied to clipboard");
450    copiedSpan.appendChild(copiedText);
451    event.target.hidden = true;
452    event.target.insertAdjacentElement('afterend', copiedSpan);
453    setTimeout(function() { event.target.parentNode.removeChild(copiedSpan); event.target.hidden = false }, 1000);
454
455  }));
456}
457
458function copyStringToClipboard(str) {
459  var el = document.createElement('textarea');
460  
461  el.value = str;
462  el.setAttribute('readonly', '');
463  el.style = { position: 'absolute', left: '-9999px' };
464  
465  document.body.appendChild(el);
466  el.select();
467  document.execCommand('copy');
468  document.body.removeChild(el);
469}
470
471function initWebisPublicationsFiltering(groups = document.querySelectorAll(".year-entry"), updateUrlQueryParam = true) {
472  groups.forEach(group => activateBibtexToggle(group))
473  groups.forEach(group => activateShareLink(group))
474  if (typeof initBibHighlightOnCopyLink === "function") { // selection.js included
475    initBibHighlightOnCopyLink();
476  }
477  const elementSelector = ".bib-entry";
478  return initWebisFiltering(groups, elementSelector, updateUrlQueryParam, defaultDataAttributesPopulationFunction);
479}
480
481////////////////////////////////////////////////////
482// Specific code to include publications
483////////////////////////////////////////////////////
484
485/*
486 * parentElement: element to which the bibentries should be added
487 * query: filter query as used on the webis.de page
488 * yearHeadingSize: changes the h2 tags of the year heading to h<yearHeadingSize>
489 */
490function includeBibentries(parentElement, query = "", yearHeadingSize = 3) {
491  const sourceUrl = "https://webis.de/publications.html";
492  includeList(parentElement, sourceUrl, '.publications-list', bibList => {
493    const filterFunction = initWebisPublicationsFiltering(bibList.querySelectorAll(".year-entry"), false);
494    filterFunction(query);
495    bibList.classList.remove("uk-container", "uk-margin-medium");
496    if (yearHeadingSize === null) {
497      removeBibHeading(bibList);
498    } else if (yearHeadingSize !== 2) {
499      changeBibHeadingSize(bibList, yearHeadingSize);
500    }
501    if (typeof initBibHighlightOnCopyLink === "function") { // selection.js included
502      initBibHighlightOnCopyLink(bibList.querySelectorAll(".copylink"));
503    }
504  });
505}
506
507/*
508 * Removes all list headings and combine the groups into one
509 */
510function removeBibHeading(bibList) {
511  const group = document.createElement("div");
512  bibList.appendChild(group);
513
514  bibList.querySelectorAll(".year-entry > .bib-entry, .year-entry > a").forEach(node => {
515    group.appendChild(node);
516  });
517
518  bibList.querySelectorAll(".year-entry").forEach(year => {
519    year.remove();
520  });
521
522  group.classList.add("year-entry");
523}
524
525/*
526 * Changes the headings from h2 to h<headingSize>
527 */
528function changeBibHeadingSize(bibList, headingSize) {
529  bibList.querySelectorAll("h2").forEach(heading => {
530    heading.outerHTML = heading.outerHTML.replace(/^<h2/, "<h" + headingSize).replace(/h2>/, "h" + headingSize + ">");
531  });
532}
533
534
535

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.