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(/­/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 ­ 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.