1/* Shared catalog autocomplete for the site's search fields. 2 * 3 * The suggestion widget used to be wired to a single element by ID 4 * (#newCatalogSearchText), so every other search box on the site â the sticky header, the 5 * mobile search layer, the 404 page â had no autocomplete at all. This exposes the widget 6 * as a reusable initialiser and binds it to all of them. 7 * 8 * Fields are matched by name, not id: header.php declares id="srch" twice (desktop + mobile 9 * layer) and 404.php a third time, so an id-based selector can only ever reach the first. 10 * 11 * Requires jQuery + jQuery UI, both loaded site-wide from header.php. 12 */ 13(function ($) { 14 'use strict'; 15 16 if (!$ || !$.fn || !$.fn.autocomplete) return; 17 18 var ENDPOINT = 'ajax_catalog_autocomplete_results/'; 19 /** Suggestion labels are long ("ת××× ×ת ××××× - â¦"); this is the narrowest readable menu. */ 20 var MIN_MENU_WIDTH = 360; 21 /** Breathing room kept between the menu and the viewport edges. */ 22 var VIEWPORT_MARGIN = 12; 23 24 /** z-index of the nearest fixed/sticky ancestor, or null when the field is in normal flow. */ 25 function stickyAncestorZIndex(el) { 26 for (var node = el.parentElement; node && node !== document.body; node = node.parentElement) { 27 var cs = window.getComputedStyle(node); 28 if (cs.position === 'fixed' || cs.position === 'sticky') { 29 var z = parseInt(cs.zIndex, 10); 30 return isNaN(z) ? null : z; 31 } 32 } 33 return null; 34 } 35 36 /** 37 * Build the shared autocomplete options. 38 * 39 * @param {function} [extraData] Returns additional request params (the catalog field sends 40 * its active filters; everywhere else sends nothing extra). 41 */ 42 function options(extraData) { 43 return { 44 minLength: 2, 45 source: function (request, response) { 46 var data = { 47 term: request.term, 48 lang: typeof CURRENTLANG !== 'undefined' ? CURRENTLANG : '' 49 }; 50 if (typeof extraData === 'function') { 51 var extra = extraData() || {}; 52 for (var k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) data[k] = extra[k]; 53 } 54 $.ajax({ 55 url: (typeof HOMEURL !== 'undefined' ? HOMEURL : '/') + ENDPOINT, 56 dataType: 'json', 57 data: data, 58 success: function (items) { 59 response($.map(items, function (item) { 60 return { 61 type: item.type, 62 typeText: item.typeText, 63 label: item.value, 64 value: item.value, 65 url: item.url 66 }; 67 })); 68 }, 69 // Never leave the widget spinning on a failed request. 70 error: function () { response([]); } 71 }); 72 }, 73 open: function () { 74 var $input = $(this); 75 var $widget = $input.autocomplete('widget'); 76 77 // Narrow fields (the sticky header's is ~160px) truncate every suggestion, so the 78 // menu gets a comfortable minimum width instead of matching the input. It grows 79 // away from the field's start edge â leftwards in RTL â so that edge stays 80 // aligned with the input and only the far edge moves. 81 var inputWidth = $input.outerWidth(); 82 var maxWidth = $(window).width() - (VIEWPORT_MARGIN * 2); 83 var width = Math.min(Math.max(MIN_MENU_WIDTH, inputWidth), maxWidth); 84 85 var offset = $input.offset(); 86 var rtl = (window.getComputedStyle(this).direction === 'rtl'); 87 var left = rtl ? (offset.left + inputWidth - width) : offset.left; 88 89 // Keep it on screen on narrow viewports. 90 var scrollX = $(window).scrollLeft(); 91 left = Math.max(scrollX + VIEWPORT_MARGIN, 92 Math.min(left, scrollX + $(window).width() - VIEWPORT_MARGIN - width)); 93 94 var css = { 'width': width + 'px', 'transform': 'none' }; 95 // The sticky header sits at a very high z-index, so a default-stacked menu has 96 // its first rows hidden behind it. Lift the menu above whichever fixed/sticky 97 // ancestor the field lives in; fields in normal flow keep the default stacking. 98 var z = stickyAncestorZIndex(this); 99 if (z !== null) css['z-index'] = z + 1; 100 101 $widget.css(css).offset({ top: $widget.offset().top, left: left }); 102 }, 103 select: function (event, ui) { 104 $(this).val($('<div/>').html(ui.item.value).text()); 105 if (ui.item.url) window.location.href = ui.item.url; 106 else event.preventDefault(); 107 } 108 }; 109 } 110 111 /** Apply the widget, including the shared suggestion markup. */ 112 function init(input, extraData) { 113 var $input = $(input); 114 if (!$input.length || $input.data('uiAutocomplete')) return $input; // never double-bind 115 $input.autocomplete(options(extraData)); 116 $input.data('ui-autocomplete')._renderItem = function (ul, item) { 117 return $('<li>') 118 .append('<a class="' + item.type + '"><span>' + item.typeText + '</span> - ' + item.label + '</a>') 119 .appendTo(ul); 120 }; 121 return $input; 122 } 123 124 window.cilInitSearchAutocomplete = init; 125 126 $(function () { 127 // Every site search box except the catalog one, which common.js wires itself because it 128 // also carries filter state and its own required-field error handling. 129 $('input[name="srch"]').each(function () { 130 if (this.id === 'newCatalogSearchText') return; 131 init(this); 132 }); 133 }); 134 135})(window.jQuery);
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.