1/*global VuFind */ 2 3VuFind.register('search', function search() { 4 let jsRecordListSelector = '.js-result-list'; 5 let paginationLinksSelector = '.js-pagination a,.js-pagination-simple a'; 6 let scrollElementSelector = '.search-stats'; 7 let searchStatsSelector = '.js-search-stats'; 8 let searchFormSelector = 'form.searchForm'; 9 let resultsControlFormSelector = '.search-controls form'; 10 let sortFormSelector = resultsControlFormSelector + '.search-sort'; 11 let limitFormSelector = resultsControlFormSelector + '.search-result-limit'; 12 let viewTypeSelector = '.view-buttons a'; 13 let selectAllSelector = '.checkbox-select-all'; 14 15 // Forward declaration 16 let loadResults = function loadResultsForward() {}; 17 18 /** 19 * Get the URL without any parameters 20 * @param {string} url URL to get base from 21 * @returns {string} Base URL without query parameters 22 */ 23 function getBaseUrl(url) { 24 const parts = url.split('?'); 25 return parts[0]; 26 } 27 28 /** 29 * Initialize pagination. 30 */ 31 function initPagination() { 32 document.querySelectorAll(paginationLinksSelector).forEach((element) => { 33 if (!element.dataset.ajaxPagination) { 34 element.dataset.ajaxPagination = true; 35 element.addEventListener('click', function handleClick(event) { 36 event.preventDefault(); 37 const href = this.getAttribute('href'); 38 loadResults(href, true); 39 }); 40 } 41 }); 42 } 43 44 /** 45 * Initialize result controls that are not refreshed via AJAX. 46 * 47 * Note that view type links are updated in updateResultControls, but using them 48 * will cause a reload since page contents may change. 49 */ 50 function initResultControls() { 51 document.querySelectorAll(resultsControlFormSelector).forEach((form) => { 52 if (!form.dataset.ajaxPagination) { 53 form.dataset.ajaxPagination = true; 54 form.querySelectorAll('.jumpMenu').forEach(jumpMenu => { 55 // Disable original jump menu function: 56 jumpMenu.classList.remove('jumpMenu'); 57 jumpMenu.addEventListener('change', function handleSubmit(event) { 58 event.preventDefault(); 59 // Build a URL from form action and fields and load results: 60 let urlParts = form.getAttribute('action').split('?', 2); 61 const query = new URLSearchParams(urlParts.length > 1 ? urlParts[1] : ''); 62 63 /** 64 * Add data from an element to query 65 * @param {HTMLElement|RadioNodeList} el Element or RadioNodeList to add values from 66 */ 67 function _addToQuery(el) { 68 if ('radio' === el.type && !el.checked) { 69 return; 70 } 71 if (el.name.endsWith('[]')) { 72 query.append(el.name, el.value); 73 } else { 74 query.set(el.name, el.value); 75 } 76 } 77 78 Object.entries(form.elements).forEach(([, element]) => { 79 // Chrome represents multiple hidden 'filter[]' fields as a RadioNodeList: 80 if (element instanceof RadioNodeList) { 81 Object.entries(element).forEach(([, setElement]) => { 82 _addToQuery(setElement); 83 }); 84 } else { 85 _addToQuery(element); 86 } 87 }); 88 // Remove page so that any change resets it: 89 query.delete('page'); 90 const url = urlParts[0] + '?' + query.toString(); 91 loadResults(url, true); 92 }); 93 }); 94 } 95 }); 96 } 97 98 /** 99 * Prepend a hidden field to a form. 100 * @param {?Element} form Form to prepend element into or null 101 * @param {string} name Name for the input element 102 * @param {string} value Value for the input element 103 */ 104 function prependHiddenField(form, name, value) { 105 if (!form) { 106 return; 107 } 108 const input = document.createElement('input'); 109 input.type = "hidden"; 110 input.name = name; 111 input.value = value; 112 form.prepend(input); 113 } 114 115 /** 116 * Handle a hidden field. 117 * 118 * Adds, updates or removes the field as necessary. 119 * @param {string} formSelector Selector for searching the form 120 * @param {string} fieldName Name of the field to search from the form 121 * @param {?string} value Value to set or null to remove the field 122 */ 123 function handleHiddenField(formSelector, fieldName, value) { 124 let form = document.querySelector(formSelector); 125 if (!form) { 126 return; 127 } 128 let field = form.querySelector("input[name=" + fieldName + "]"); 129 if (field) { 130 if (value) { 131 field.value = value; 132 } else { 133 field.remove(); 134 } 135 } else if (value) { 136 prependHiddenField(form, fieldName, value); 137 } 138 } 139 140 /** 141 * Update value of a select field 142 * @param {?Element} select Select element to update 143 * @param {?string} value Value to set 144 */ 145 function updateSelectValue(select, value) { 146 if (!select) { 147 return; 148 } 149 if (select.value !== value) { 150 if (value) { 151 select.value = value; 152 } else { 153 const defaultValue = select.querySelector('option[data-default]'); 154 if (defaultValue) { 155 select.value = defaultValue.value; 156 } 157 } 158 } 159 } 160 161 /** 162 * Update URLs of result controls (sort, limit, view type) 163 * 164 * We will deliberately avoid replacing the controls for accessibility, so we need 165 * to ensure that they contain current URL parameters. 166 * @param {string} pageUrl Current page URL to use for updating sort and limit controls 167 */ 168 function updateResultControls(pageUrl) { 169 const parts = pageUrl.split('?', 2); 170 const params = new URLSearchParams(parts.length > 1 ? parts[1] : ''); 171 const sort = params.get('sort'); 172 const limit = params.get('limit'); 173 174 // Update hidden fields of the search form: 175 handleHiddenField(searchFormSelector, 'limit', limit); 176 handleHiddenField(searchFormSelector, 'sort', sort); 177 178 // Update hidden fields of search control forms: 179 handleHiddenField(sortFormSelector, 'limit', limit); 180 handleHiddenField(limitFormSelector, 'sort', sort); 181 182 // Update currently selected values (required for history traversal to show correct values): 183 updateSelectValue(document.querySelector(sortFormSelector + ' select'), sort); 184 updateSelectValue(document.querySelector(limitFormSelector + ' select'), limit); 185 186 // Update view type links: 187 document.querySelectorAll(viewTypeSelector).forEach((element) => { 188 const url = element.getAttribute('href'); 189 const urlParts = url.split('?', 2);
190 const urlParams = new URLSearchParams(urlParts.length > 1 ? urlParts[1] : ''); 191 if (sort) { 192 urlParams.set('sort', sort); 193 } else { 194 urlParams.delete('sort'); 195 } 196 if (limit) { 197 urlParams.set('limit', limit); 198 } else { 199 urlParams.delete('limit'); 200 } 201 element.setAttribute('href', urlParts[0] + '?' + urlParams.toString()); 202 }); 203 204 // Reset "select all" checkbox: 205 document.querySelectorAll(selectAllSelector).forEach((el) => el.checked = false); 206 } 207 208 /** 209 * Update URLs of links pointing to the same results page 210 * 211 * Updates links pointing to this page to ensure that they contain current URL 212 * parameters (e.g. sort and limit). Any link that only contains query parameters 213 * is considered. 214 * @param {string} pageUrl Current page URL to use for updating links 215 */ 216 function updateResultLinks(pageUrl) { 217 let urlParts = pageUrl.split('?', 2); 218 const params = new URLSearchParams(urlParts.length > 1 ? urlParts[1] : ''); 219 const sort = params.get('sort'); 220 const limit = params.get('limit'); 221 222 document.querySelectorAll('a').forEach((a) => { 223 // Use original href attribute since a.href returns an absolute URL: 224 const href = a.getAttribute('href'); 225 if (!href || !href.startsWith('?')) { 226 return true; 227 } 228 const hrefParams = new URLSearchParams(href.substr(1)); 229 if (null === sort) { 230 hrefParams.delete('sort'); 231 } else { 232 hrefParams.set('sort', sort); 233 } 234 if (null === limit) { 235 hrefParams.delete('limit'); 236 } else { 237 hrefParams.set('limit', limit); 238 } 239 a.href = '?' + hrefParams.toString(); 240 }); 241 } 242 243 /** 244 * Scroll view port to results 245 * @param {string} _style Scroll behavior ('smooth' (default), 'instant' or 'auto') 246 */ 247 function scrollToResults(_style) { 248 const scrollEl = document.querySelector(scrollElementSelector); 249 if (scrollEl && window.scrollY > scrollEl.offsetTop) { 250 const style = typeof _style !== 'undefined' ? _style : 'smooth'; 251 scrollEl.scrollIntoView({behavior: style}); 252 } 253 } 254 255 /** 256 * Show an error message 257 * @param {string} error Error message to show 258 */ 259 function showError(error) { 260 let errorMsg = document.createElement('div'); 261 errorMsg.classList = 'alert alert-danger'; 262 errorMsg.textContent = error; 263 const recordList = document.querySelector(jsRecordListSelector); 264 recordList.replaceChildren(errorMsg); 265 } 266 267 /** 268 * Load results and update associated elements. 269 * @param {string} pageUrl Current page URL to load results 270 * @param {boolean} addToHistory Add current search into history 271 */ 272 loadResults = function loadResultsReal(pageUrl, addToHistory) { 273 const recordList = document.querySelector(jsRecordListSelector); 274 const backend = recordList.dataset.backend; 275 if (typeof backend === 'undefined') { 276 showError('ERROR: data-backend not set for record list'); 277 return; 278 } 279 if (recordList.classList.contains('loading')) { 280 return; 281 } 282 recordList.classList.add('loading'); 283 const history = recordList.dataset.history; 284 285 recordList.prepend(VuFind.loadingOverlay()); 286 scrollToResults(); 287 const searchStats = document.querySelector(searchStatsSelector); 288 const statsKey = searchStats.dataset.key; 289 290 const queryParams = new URLSearchParams('method=getSearchResults'); 291 queryParams.set('source', backend); 292 if (typeof history !== 'undefined') { 293 queryParams.set('history', history); 294 } 295 queryParams.set('statsKey', statsKey); 296 let pageUrlParts = pageUrl.split('?'); 297 if (typeof pageUrlParts[1] !== 'undefined') { 298 queryParams.set('querystring', pageUrlParts[1]); 299 if (addToHistory) { 300 window.history.pushState( 301 { 302 url: getBaseUrl(window.location.href) + '?' + pageUrlParts[1] 303 }, 304 '', 305 '?' + pageUrlParts[1] 306 ); 307 } 308 } 309 updateResultControls(pageUrl); 310 updateResultLinks(pageUrl); 311 312 VuFind.emit('results-load', { 313 url: pageUrl, 314 addToHistory: addToHistory 315 }); 316 317 fetch(VuFind.path + '/AJAX/JSON?' + queryParams.toString()) 318 .then((response) => { 319 response.json() 320 .then((result) => { 321 if (result.error) { 322 throw result.error; 323 } 324 if (!response.ok) { 325 throw result.data; 326 } 327 // We expect to get the results list in elements, but reset it to hide spinner just in case: 328 recordList.textContent = '';
329 Object.entries(result.data.elements).forEach(([elementSelector, contents]) => { 330 document.querySelectorAll(elementSelector).forEach((element) => { 331 VuFind.setElementContents( 332 element, 333 contents.content, 334 contents.attrs, 335 contents.target ? (contents.target + 'HTML') : '' 336 ); 337 }); 338 }); 339 VuFind.initResultScripts(jsRecordListSelector); 340 initPagination(); 341 342 VuFind.emit('results-loaded', { 343 url: pageUrl, 344 addToHistory: addToHistory, 345 data: result 346 }); 347 348 recordList.classList.remove('loading'); 349 }) 350 .catch((error) => { 351 // This error message is from the server, so no need to prefix it 352 showError(error); 353 recordList.classList.remove('loading'); 354 }); 355 }) 356 .catch((error) => { 357 showError(VuFind.translate('error_occurred') + ' - ' + error); 358 recordList.classList.remove('loading'); 359 }); 360 }; 361 362 /** 363 * Handle history state change event and load results accordingly. 364 * @param {Event} event Event to use for loading results 365 */ 366 function historyStateListener(event) { 367 if (event.state && event.state.url && getBaseUrl(window.location.href) === getBaseUrl(event.state.url)) { 368 event.preventDefault(); 369 loadResults(event.state.url, false); 370 } 371 } 372 373 /** 374 * Initialize JS pagination if enabled 375 */ 376 function init() { 377 if (document.querySelector(jsRecordListSelector)) { 378 initPagination(); 379 initResultControls(); 380 window.history.replaceState({url: window.location.href}, '', window.location.href); 381 window.addEventListener('popstate', historyStateListener); 382 } 383 } 384 385 return { 386 init: init, 387 scrollToResults 388 }; 389});
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.