1/** 2 * Melett Component: Search. 3 * 4 * @package Melett\Scripts\Components 5 */ 6 7import gtmSearch from "analytics/search"; 8import gtmViewItemList from "analytics/viewItemList"; 9import gtmSelectItem from "analytics/selectItem"; 10import presentSlug from "helpers/presentSlug"; 11import formatStockCode from "helpers/formatStockCode"; 12import { 13 ajaxURL, 14 getNonce, 15 getMessage, 16 getCurrentLanguage, 17 getMasterPricingLink, 18 getTNKSLink, 19} from "helpers/serverData"; 20 21const $ = jQuery; 22/** 23 * DOM procedures for Melett Custom Search bars. 24 * 25 * @template template-parts/forms/search.php 26 * @author Contedia Ltd 27 */ 28class MelettSearch { 29 /** 30 * The Container for the Melett Search bar. 31 * 32 * Set DOM elements and event listeners. 33 * 34 * @param {string} selector The selector for the search container 35 */ 36 constructor(data) { 37 this.container = $(data.selector); 38 39 if(!this.container.length) { 40 return; 41 } 42 43 this.name = data.name; 44 this.title = data.title ?? presentSlug(data.name); 45 this.form = this.container.find('.melett-search-form'); 46 this.searchBy = data.searchBy; 47 this.field = this.form.find('.search-field'); 48 this.type = this.form.data('search-type'); 49 this.resultsContainer = this.container.find('.ajax-results-container'); 50 this.ajaxURL = ajaxURL; 51 this.currentLang = getCurrentLanguage(); 52 53 /** 54 * Clear and hide Ajax Search results containers of items when search is unfocused. 55 */ 56 $(document).on("click", function(event){ 57 if(!$(event.target).closest(data.selector).length){ 58 $('.melett-search-form-container .ajax-results-container').hide(); 59 $('.melett-search-form-container .ajax-results-container').empty(); 60 $('.melett-search-form-container .melett-search-form').removeClass('form-in-focus'); 61 } 62 }); 63 64 this.debouncedInput = this.debounce(this.input.bind(this), 500); 65 66 this.form.submit(this.submit.bind(this)); 67 this.field.on('input', (e) => this.debouncedInput(e)); 68 this.field.focusin(this.focusSearch); 69 70 } 71 72 debounce(func, wait) { 73 let timeout; 74 return function executedFunction(...args) { 75 const later = () => { 76 clearTimeout(timeout); 77 func(...args); 78 }; 79 clearTimeout(timeout); 80 timeout = setTimeout(later, wait); 81 }; 82 } 83 84 85 getValue() { 86 return this.field.val().trim(); 87 } 88 89 submit(e) { 90 let searchVal = this.getValue(); 91 92 gtmSearch({ 93 searchBy: this.searchBy, 94 searchTerm: searchVal, 95 searchBar: this.name, 96 }); 97 this.init(e, (subResponse) => { 98 let d = JSON.parse(subResponse); 99 let success = d.success; 100 let excluded = d.excluded ?? false; 101 let partsList = d.partsList; 102 let redirectURL = null; 103 104 this.resultsContainer.hide(); 105 this.resultsContainer.css('max-height', '0'); 106 this.resultsContainer.empty(); 107 108 if (success) { 109 if(partsList.length === 1) { 110 let part = partsList[0]; 111 gtmSelectItem({ 112 listId: this.name, 113 listName: this.title, 114 item: { id: part.code } 115 }); 116 redirectURL = `${window.location.origin}/master-pricing/?partcode=${encodeURIComponent(`${part.code}`)}` 117 } else { 118 if (excluded) { 119 redirectURL = `${window.location.origin}/product-disabled/?partno=${encodeURIComponent(searchVal)}` 120 } else { 121 redirectURL = `${window.location.origin}/turbo-no-keyword-search/?partno=${encodeURIComponent(searchVal)}` 122 } 123 } 124 125 window.location = redirectURL; 126 } 127 }); 128 } 129 130 async input(e) { 131 await this.init(e, (subResponse) => { 132 let d = JSON.parse(subResponse); 133 let success = d.success; 134 let partsList = d.partsList; 135 this.#clearResponseItems(); 136 137 if (success) { 138 let gtmArgs = { 139 listId: this.name, 140 listName: this.title, 141 items: [] 142 }; 143 this.form.removeClass('loading'); 144 145 if(partsList.length) { 146 this.resultsContainer.show(); 147 this.resultsContainer.css('max-height', '550px'); 148 partsList.map((part, partIndex) => {
149 let defaultDesc = String(part.name_en).trim(); 150 let translatedDesc = part.hasOwnProperty(`name_${this.currentLang}`) ? String(part[`name_${this.currentLang}`]).trim() : false; 151 let desc = translatedDesc && translatedDesc.length ? translatedDesc : defaultDesc; 152 let isObsolete = desc.toLowerCase().includes('obsolete'); 153 let isBlank = part.code.toLowerCase().includes('blank'); 154 155 if (!isObsolete && !isBlank) { 156 this.#ajaxResponseItem( 157 `${formatStockCode(part.code)} - ${desc}`, 158 getMasterPricingLink(part.code), 159 { 160 stockCode: part.code, 161 description: desc, 162 index: partIndex 163 } 164 ); 165 166 gtmArgs.items.push({ 167 id: part.code, 168 description: desc, 169 index: partIndex, 170 }); 171 } 172 173 }) 174 175 gtmViewItemList(gtmArgs); 176 } else { 177 this.resultsContainer.show(); 178 this.resultsContainer.css('max-height', 'unset'); 179 this.#ajaxResponseItem( 180 getMessage('search_click_here_for_further_results'), 181 getTNKSLink(this.getValue()), 182 ); 183 } 184 } 185 }) 186 } 187 188 /** 189 * Initiates the search submission. 190 * 191 * Declare common variables that are essential for any search submission, 192 * feed them to the relevant AJAX submission 193 * and call the delegated "success" function. 194 * 195 * @param e The event object (submission, input change, etc) 196 * @param successF The function to be called when the AJAX request is successful (no errors). 197 * @returns {Promise<void>} 198 */ 199 async init(e, successF) { 200 e.preventDefault(); 201 let searchVal = this.getValue(); 202 let data = {}; 203 204 if(searchVal.length < 3) { 205 return; 206 } 207 208 this.form.addClass('loading'); 209 210 if(this.type === 'turbo-parts') { 211 data.action = 'melett_parts_list_search'; 212 data.stock_code = searchVal; 213 data.nonce = getNonce('mtt_search_nonce'); 214 this.searchAjaxSub( 215 data, 216 successF 217 ) 218 } 219 } 220 221 /** 222 * Send the AJAX request with supplied data. 223 * 224 * This is the template for all Melett search requests. 225 * 226 * @param {object} subData Post data for the AJAX request. 227 * @param {dom element} ajaxCont The HTML container for the AJAX search result items. 228 * @param {function} successF The function to call when the request is successful (no errors). 229 * @returns {Promise<void>} 230 */ 231 searchAjaxSub(subData, successF) { 232 $.ajax({ 233 url: this.ajaxURL, 234 type: 'POST', 235 dataType: "text", 236 data: subData, 237 success: function(data, textStatus, jqXHR) { 238 successF(data); 239 }, 240 error: function(jqXHR, textStatus, errorThrown){ 241 console.log(textStatus + errorThrown); 242 } 243 }); 244 } 245 246 /** 247 * Run procedure for when the user clicks on a search field. 248 * 249 * @param e Event object. 250 * @returns {Promise<void>} 251 */ 252 focusSearch(e) 253 { 254 $(this.form).addClass('form-in-focus'); 255 } 256 257 /** 258 * Generate an AJAX search response items in the results container. 259 * 260 * @param url 261 * @param content 262 * @param data 263 * @returns {Promise<void>} 264 */ 265 #ajaxResponseItem(content, url = false, data = {}) { 266 let item; 267 if (!url) { 268 item = $(`<p class="ajax-result-item">${content}</p>`); 269 } else { 270 item = $(`<a class="ajax-result-item ajax-result-item--link" href="${url}">${content}</a>`); 271 if (data.stockCode && data.description && data.index) { 272 item.click(e => { 273 gtmSearch({ 274 searchBy: this.searchBy, 275 searchTerm: data.stockCode ?? '', 276 searchBar: this.name, 277 }); 278 279 gtmSelectItem({ 280 listId: this.name, 281 listName: this.title, 282 item: { 283 id: data.stockCode, 284 description: data.description, 285 index: data.index, 286 }, 287 }); 288 }); 289 } 290 } 291 this.resultsContainer.append(item); 292 } 293 294 #clearResponseItems() { 295 this.resultsContainer.hide(); 296 this.resultsContainer.css('max-height', '0'); 297 this.resultsContainer.empty(); 298 } 299} 300 301export default MelettSearch;
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.