PageSourceSearch

https://www.ibf.org.sg/Mvc/Scripts/IbfGlobalSearch/IbfGlobalSearch.js

js ibf.org.sg collected 2026-10-02 10:28:02 UTC 20,758 bytes, 546 lines download raw bytes

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, "&lt;")
30                .replace(/>/g, "&gt;")
31                .replace(/"/g, "&quot;")
32                .replace(/'/g, "&#039;");
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.