1(function (window, document) { 2 "use strict"; 3 4 var DEBOUNCE_MS = 400; 5 var MIN_QUERY_LENGTH = 3; 6 7 function whenJQueryReady(callback) { 8 if (typeof window.jQuery !== "undefined") { 9 callback(window.jQuery); 10 return; 11 } 12 13 var attempts = 0; 14 var timer = window.setInterval(function () { 15 attempts += 1; 16 if (typeof window.jQuery !== "undefined") { 17 window.clearInterval(timer); 18 callback(window.jQuery); 19 } else if (attempts > 100) { 20 window.clearInterval(timer); 21 } 22 }, 50); 23 } 24 25 whenJQueryReady(function ($) { 26 function escapeHtml(value) { 27 return String(value == null ? "" : value) 28 .replace(/&/g, "&") 29 .replace(/</g, "<") 30 .replace(/>/g, ">") 31 .replace(/"/g, """) 32 .replace(/'/g, "'"); 33 } 34 35 function normalizeApiPath(path) { 36 if (!path) { 37 return ""; 38 } 39 40 var value = String(path).trim(); 41 if (!value) { 42 return ""; 43 } 44 45 if (value.indexOf("http://") === 0 || value.indexOf("https://") === 0) { 46 return value; 47 } 48 49 return value.charAt(0) === "/" ? value : "/" + value; 50 } 51 52 function debounce(fn, delay) { 53 var timer; 54 return function () { 55 var context = this; 56 var args = arguments; 57 clearTimeout(timer); 58 timer = setTimeout(function () { 59 fn.apply(context, args); 60 }, delay); 61 }; 62 } 63 64 function IbfGlobalSearch(rootElement) { 65 this.$root = $(rootElement); 66 this.$config = $("#ibf-global-search-config"); 67 this.autocompleteApiUrl = normalizeApiPath(this.$config.data("autocomplete-api-url")); 68 this.summaryApiUrl = normalizeApiPath(this.$config.data("summary-api-url")); 69 this.trackQueryApiUrl = normalizeApiPath( 70 this.$config.data("track-query-api-url") || this.$config.attr("data-track-query-api-url") 71 ); 72 this.trackClickApiUrl = normalizeApiPath( 73 this.$config.data("track-click-api-url") || this.$config.attr("data-track-click-api-url") 74 ); 75 this.resultsPageUrl = normalizeApiPath(this.$config.data("results-page-url") || this.$config.attr("data-results-page-url") || ""); 76 this.minQueryLength = Number(this.$config.data("min-query-length") || this.$config.attr("data-min-query-length")) || MIN_QUERY_LENGTH; 77 this.trackAnalytics = String(this.$config.data("track-analytics") || "").toLowerCase() === "true" 78 || String(this.$config.attr("data-track-analytics") || "").toLowerCase() === "true"; 79 this.query = $.trim(String(this.$config.data("query") || this.$config.attr("data-query") || "")); 80 this.queryId = $.trim(String(this.$config.data("query-id") || this.$config.attr("data-query-id") || "")); 81 this.resultCount = Number(this.$config.data("result-count") || this.$config.attr("data-result-count") || 0) || 0; 82 this.pageNumber = Number(this.$config.data("page") || this.$config.attr("data-page") || 1) || 1; 83 84 this.$form = this.$root.find("[data-ibf-gs-form]").first(); 85 if (!this.$form.length) { 86 this.$form = this.$root.is("form") ? this.$root : this.$root.find("form").first(); 87 } 88 89 this.$input = this.$root.find("[data-ibf-gs-input], input.search-input, input[name='quickSearch'], input[name='q']").first(); 90 this.$submit = this.$root.find("[data-ibf-gs-submit], button.custom-button, .search-btn").first(); 91 this.$notes = this.$root.find("[data-ibf-gs-notes], .add-notes").first(); 92 this.$synonymBox = this.$root.find("[data-ibf-gs-synonym-box]").first(); 93 this.$synonymText = this.$root.find("[data-ibf-gs-synonym-text]").first(); 94 this.$summaryBox = this.$root.find("[data-ibf-gs-summary-box]").first(); 95 this.$summaryBody = this.$root.find("[data-ibf-gs-summary-body]").first(); 96 this.$summaryLoading = this.$root.find("[data-ibf-gs-summary-loading]").first(); 97 this._requestToken = 0; 98 } 99 100 IbfGlobalSearch.prototype.init = function () { 101 if (this.trackAnalytics) { 102 this.initAnalytics(); 103 } 104 105 if (!this.$input.length) { 106 return; 107 } 108 109 this.hideNotes();
110 this.bindEvents(); 111 }; 112 113 IbfGlobalSearch.prototype.getDeviceType = function () { 114 var width = window.innerWidth || document.documentElement.clientWidth || 1024; 115 if (width < 768) { 116 return "mobile"; 117 } 118 if (width < 1024) { 119 return "tablet"; 120 } 121 return "desktop"; 122 }; 123 124 IbfGlobalSearch.prototype.getScreenSize = function () { 125 var width = window.screen && window.screen.width ? window.screen.width : window.innerWidth; 126 var height = window.screen && window.screen.height ? window.screen.height : window.innerHeight; 127 return width + "x" + height; 128 }; 129 130 IbfGlobalSearch.prototype.getSessionId = function () { 131 var key = "ibf-cludo-sid"; 132 try { 133 var existing = window.sessionStorage.getItem(key); 134 if (existing) { 135 return existing; 136 } 137 var created = "s-" + Date.now() + "-" + Math.random().toString(36).slice(2, 10); 138 window.sessionStorage.setItem(key, created); 139 return created; 140 } catch (e) { 141 return "s-" + Date.now(); 142 } 143 }; 144 145 IbfGlobalSearch.prototype.buildTrackingBase = function () { 146 return { 147 dt: this.getDeviceType(), 148 sz: this.getScreenSize(), 149 ua: navigator.userAgent || "", 150 brl: (navigator.language || navigator.userLanguage || ""), 151 hn: window.location.hostname || "", 152 refurl: window.location.href || "", 153 refpt: document.title || "", 154 sid: this.getSessionId(), 155 pn: String(this.pageNumber || 1) 156 }; 157 }; 158 159 IbfGlobalSearch.prototype.initAnalytics = function () { 160 var self = this; 161 162 if (this.query && this.queryId) { 163 this.trackSearchQuery(); 164 } 165 166 this.$root.off("click.ibfGsTrack").on("click.ibfGsTrack", "[data-ibf-gs-result-link]", function () { 167 var $link = $(this); 168 self.trackResultClick({ 169 clurl: $link.attr("data-clurl") || $link.attr("href") || "", 170 title: $link.attr("data-title") || $.trim($link.text()) || "", 171 cli: $link.attr("data-cli") || "" 172 }); 173 }); 174 }; 175 176 IbfGlobalSearch.prototype.trackSearchQuery = function () { 177 if (!this.trackQueryApiUrl || !this.query || !this.queryId) { 178 return; 179 } 180 181 var dedupeKey = "ibf-cludo-qid-" + this.queryId; 182 try { 183 if (window.sessionStorage.getItem(dedupeKey)) { 184 return; 185 } 186 window.sessionStorage.setItem(dedupeKey, "1"); 187 } catch (e) { 188 // continue without dedupe 189 } 190 191 var payload = $.extend(this.buildTrackingBase(), { 192 sw: this.query, 193 qid: this.queryId, 194 rc: String(this.resultCount) 195 }); 196 197 $.ajax({
198 url: this.trackQueryApiUrl, 199 method: "POST", 200 contentType: "application/json; charset=utf-8", 201 dataType: "json", 202 data: JSON.stringify(payload) 203 }); 204 }; 205 206 IbfGlobalSearch.prototype.trackResultClick = function (click) { 207 if (!this.trackClickApiUrl || !this.queryId || !click || !click.clurl) { 208 return; 209 } 210 211 var payload = $.extend(this.buildTrackingBase(), { 212 ls: "searchresult", 213 sw: this.query, 214 qid: this.queryId, 215 clurl: click.clurl, 216 cli: String(click.cli || ""), 217 title: click.title || "" 218 }); 219 220 $.ajax({ 221 url: this.trackClickApiUrl, 222 method: "POST", 223 contentType: "application/json; charset=utf-8", 224 dataType: "json", 225 data: JSON.stringify(payload) 226 }); 227 }; 228 229 IbfGlobalSearch.prototype.bindEvents = function () { 230 var self = this; 231 232 this.fetchAutocomplete = debounce(function (query) { 233 self.loadAutocomplete(query); 234 }, DEBOUNCE_MS); 235 236 this.fetchSummary = debounce(function (query) { 237 self.loadSummary(query); 238 }, DEBOUNCE_MS + 200); 239 240 this.$input.off(".ibfGs").on("input.ibfGs keyup.ibfGs", function () { 241 var query = $.trim(self.$input.val()); 242 if (query.length < self.minQueryLength) { 243 self.hideNotes(); 244 return; 245 } 246 247 self.fetchAutocomplete(query); 248 self.fetchSummary(query); 249 }); 250 251 if (this.$form.length) { 252 this.$form.off("submit.ibfGs").on("submit.ibfGs", function (event) { 253 event.preventDefault(); 254 self.goToResults($.trim(self.$input.val())); 255 return false; 256 }); 257 } 258 259 if (this.$submit.length) { 260 this.$submit.off("click.ibfGs").on("click.ibfGs", function (event) { 261 event.preventDefault(); 262 self.goToResults($.trim(self.$input.val())); 263 return false; 264 }); 265 } 266 267 this.$root.off("click.ibfGsSuggestion").on("click.ibfGsSuggestion", "[data-ibf-gs-suggestion]", function (event) { 268 event.preventDefault(); 269 var suggestion = $(this).attr("data-ibf-gs-suggestion") || $(this).text(); 270 self.$input.val(suggestion); 271 self.goToResults($.trim(suggestion)); 272 }); 273 }; 274 275 IbfGlobalSearch.prototype.goToResults = function (query) { 276 query = $.trim(query || ""); 277 if (!query) { 278 return; 279 } 280 281 var target = this.resultsPageUrl; 282 if (!target) { 283 target = "/search-result"; 284 } 285 286 var separator = target.indexOf("?") >= 0 ? "&" : "?"; 287 window.location.href = target + separator + "q=" + encodeURIComponent(query); 288 }; 289 290 IbfGlobalSearch.prototype.hideNotes = function () { 291 if (this.$notes.length) { 292 this.$notes.hide(); 293 } 294 if (this.$synonymBox.length) { 295 this.$synonymBox.hide(); 296 } 297 if (this.$summaryBox.length) { 298 this.$summaryBox.hide(); 299 } 300 if (this.$summaryBody.length) { 301 this.$summaryBody.empty(); 302 } 303 if (this.$synonymText.length) { 304 this.$synonymText.empty(); 305 } 306 if (this.$summaryLoading.length) { 307 this.$summaryLoading.hide(); 308 } 309 }; 310 311 IbfGlobalSearch.prototype.showNotesContainer = function () { 312 if (this.$notes.length) { 313 this.$notes.show(); 314 } 315 }; 316 317 IbfGlobalSearch.prototype.loadAutocomplete = function (query) { 318 var self = this; 319 if (!this.autocompleteApiUrl) { 320 return; 321 } 322 323 $.ajax({
324 url: this.autocompleteApiUrl, 325 method: "GET", 326 dataType: "json", 327 cache: false, 328 data: { 329 query: query, 330 results: 6 331 } 332 }).done(function (data) { 333 self.renderSynonyms(data); 334 }); 335 }; 336 337 IbfGlobalSearch.prototype.collectSuggestionTexts = function (data) { 338 var texts = []; 339 var suggestions = (data && (data.suggestions || data.Suggestions)) || []; 340 var results = (data && (data.results || data.Results)) || []; 341 var i; 342 343 for (i = 0; i < suggestions.length; i += 1) { 344 if (suggestions[i]) { 345 texts.push(String(suggestions[i])); 346 } 347 } 348 349 for (i = 0; i < results.length; i += 1) { 350 var title = results[i] && (results[i].title || results[i].Title); 351 if (title && texts.indexOf(title) === -1) { 352 texts.push(String(title)); 353 } 354 } 355 356 return texts; 357 }; 358 359 IbfGlobalSearch.prototype.renderSynonyms = function (data) { 360 if (!this.$synonymBox.length || !this.$synonymText.length) { 361 return; 362 } 363 364 var texts = this.collectSuggestionTexts(data); 365 var didYouMean = texts.length ? texts[0] : ""; 366 var synonymParts = texts.slice(1, 4); 367 368 if (!didYouMean && !synonymParts.length) { 369 this.$synonymBox.hide(); 370 this.updateNotesVisibility(); 371 return; 372 } 373 374 var html = ""; 375 if (didYouMean) { 376 html += 'Did you mean <a href="#" data-ibf-gs-suggestion="' + escapeHtml(didYouMean) + '">' + escapeHtml(didYouMean) + "</a>?"; 377 } 378 379 if (synonymParts.length) { 380 if (html) { 381 html += " "; 382 } 383 html += 'Also matching synonyms: <a href="#" data-ibf-gs-suggestion="' + escapeHtml(synonymParts.join(", ")) + '">' + escapeHtml(synonymParts.join(", ")) + "</a>"; 384 } 385 386 this.$synonymText.html(html); 387 this.$synonymBox.show(); 388 this.showNotesContainer(); 389 this.updateNotesVisibility(); 390 }; 391 392 IbfGlobalSearch.prototype.loadSummary = function (query) { 393 var self = this; 394 if (!this.summaryApiUrl || !this.$summaryBox.length) { 395 return; 396 } 397 398 var token = ++this._requestToken; 399 this.showNotesContainer(); 400 this.$summaryBox.show(); 401 if (this.$summaryLoading.length) { 402 this.$summaryLoading.show(); 403 } 404 if (this.$summaryBody.length) { 405 this.$summaryBody.empty(); 406 } 407 408 $.ajax({
409 url: this.summaryApiUrl, 410 method: "GET", 411 dataType: "json", 412 cache: false, 413 data: { 414 query: query 415 } 416 }).done(function (data) { 417 if (token !== self._requestToken) { 418 return; 419 } 420 421 var success = data && (data.success === true || data.Success === true); 422 var summary = data && (data.summary || data.Summary); 423 var synonyms = (data && (data.synonyms || data.Synonyms)) || []; 424 425 if (success && summary) { 426 self.$summaryBody.html(summary); 427 self.$summaryBox.show(); 428 self.showNotesContainer(); 429 } else { 430 self.$summaryBody.empty(); 431 self.$summaryBox.hide(); 432 } 433 434 if (synonyms.length) { 435 self.mergeSynonymsFromSummary(synonyms); 436 } 437 438 self.updateNotesVisibility(); 439 }).fail(function () { 440 if (token !== self._requestToken) { 441 return; 442 } 443 444 self.$summaryBody.empty(); 445 self.$summaryBox.hide(); 446 self.updateNotesVisibility(); 447 }).always(function () { 448 if (token === self._requestToken && self.$summaryLoading.length) { 449 self.$summaryLoading.hide(); 450 } 451 }); 452 }; 453 454 IbfGlobalSearch.prototype.mergeSynonymsFromSummary = function (synonyms) { 455 if (!this.$synonymText.length || !synonyms || !synonyms.length) { 456 return; 457 } 458 459 if ($.trim(this.$synonymText.text())) { 460 return; 461 } 462 463 var didYouMean = synonyms[0]; 464 var rest = synonyms.slice(1, 4); 465 var html = 'Did you mean <a href="#" data-ibf-gs-suggestion="' + escapeHtml(didYouMean) + '">' + escapeHtml(didYouMean) + "</a>?"; 466 if (rest.length) { 467 html += ' Also matching synonyms: <a href="#" data-ibf-gs-suggestion="' + escapeHtml(rest.join(", ")) + '">' + escapeHtml(rest.join(", ")) + "</a>"; 468 } 469 470 this.$synonymText.html(html); 471 this.$synonymBox.show(); 472 this.showNotesContainer(); 473 }; 474 475 IbfGlobalSearch.prototype.updateNotesVisibility = function () { 476 if (!this.$notes.length) { 477 return; 478 } 479 480 var synonymVisible = this.$synonymBox.length && this.$synonymBox.is(":visible"); 481 var summaryVisible = this.$summaryBox.length && this.$summaryBox.is(":visible"); 482 if (synonymVisible || summaryVisible) { 483 this.$notes.show(); 484 } else { 485 this.$notes.hide(); 486 } 487 }; 488 489 function prepareModals() { 490 // Remove duplicate quick-search modals (e.g. nav widget + standalone widget), 491 // keeping the one wired for global search. 492 var $modals = $("[id='quickSearch']"); 493 if ($modals.length > 1) { 494 var $keep = $modals.filter("[data-ibf-global-search]").first(); 495 if (!$keep.length) { 496 $keep = $modals.has("[data-ibf-gs-input]").first(); 497 } 498 if (!$keep.length) { 499 $keep = $modals.first(); 500 } 501 $modals.not($keep).remove(); 502 $modals = $keep; 503 } 504 505 // Keep a single config element. 506 var $configs = $("[id='ibf-global-search-config']"); 507 if ($configs.length > 1) { 508 $configs.not($configs.first()).remove(); 509 } 510 511 // Move the modal to <body> so Bootstrap's backdrop doesn't cover it when the 512 // modal is rendered inside the header/nav stacking context (faded/unclickable). 513 $modals.each(function () { 514 var $modal = $(this); 515 if (!$modal.parent().is("body")) { 516 $modal.appendTo("body"); 517 } 518 }); 519 } 520 521 function boot() { 522 prepareModals(); 523 524 $("[data-ibf-global-search], [data-ibf-global-search-results], #quickSearch").each(function () { 525 if ($(this).data("ibf-gs-initialized")) { 526 return; 527 } 528 529 $(this).data("ibf-gs-initialized", true); 530 var module = new IbfGlobalSearch(this);
531 module.init(); 532 }); 533 } 534 535 $(boot); 536 537 // Modal may be opened after initial bind; ensure handlers attach once DOM is ready. 538 $(document).on("shown.bs.modal", "#quickSearch", function () { 539 boot(); 540 var $input = $(this).find("[data-ibf-gs-input], input.search-input").first(); 541 if ($input.length) { 542 $input.trigger("focus"); 543 } 544 }); 545 }); 546})(window, document);
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.