1/** 2 * RFJ Search Dropdown Component 3 * 4 * æç´¢ä¸æç»ä»¶ - é¶ jQuery ä¾èµï¼å¯é jQuery éçº§ï¼ 5 * æ¯æå¤å®ä¾ãå®å¶åé ç½® 6 * 7 * ä½¿ç¨æ¹å¼: 8 * ```html 9 * <link rel="stylesheet" href="/css/rfj-search-dropdown.css"> 10 * <script src="/js/rfj-search-dropdown.js"><\/script> 11 * <input type="text" id="my-search" placeholder="æç´¢..."> 12 * <div id="my-search-dropdown"></div> 13 * <script> 14 * RFJSearchDropdown.init('#my-search', '#my-search-dropdown', { 15 * ajaxUrl: '<?php echo admin_url("admin-ajax.php"); ?>', 16 * nonce: '<?php echo wp_create_nonce("rfj_search_nonce"); ?>' 17 * }); 18 *<\/script> 19 * ``` 20 */ 21 22(function (root, factory) { 23 if (typeof module === 'object' && module.exports) { 24 module.exports = factory(); 25 } else if (typeof define === 'function' && define.amd) { 26 define(factory); 27 } else { 28 root.RFJSearchDropdown = factory(); 29 } 30}(typeof self !== 'undefined' ? self : this, function () { 31 32 'use strict'; 33 34 // ==================== 徿 SVG ==================== 35 var ICONS = { 36 loading: '<svg class="rfj-sd-icon" viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="30 70" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="0.8s" repeatCount="indefinite"/></circle></svg>', 37 category: '<svg class="rfj-sd-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M3 5h7v2H3zm0 5h7v2H3zm0 5h7v2H3zm11-5h7v2h-7zm0 5h7v2h-7zm0 5h7v2h-7z"/></svg>', 38 software: '<svg class="rfj-sd-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V6h16v12zM6 10h2v2H6zm0 4h8v2H6zm10 0h2v2h-2zm-6-4h8v2h-8z"/></svg>', 39 company: '<svg class="rfj-sd-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>', 40 arrow: '<svg class="rfj-sd-icon" viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/></svg>', 41 close: '<svg class="rfj-sd-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>' 42 }; 43 44 // ==================== å·¥å ·å½æ° ==================== 45 function debounce(fn, delay) { 46 var timer = null; 47 return function () { 48 var args = arguments; 49 var ctx = this; 50 clearTimeout(timer); 51 timer = setTimeout(function () { 52 fn.apply(ctx, args); 53 }, delay); 54 }; 55 } 56 57 function isMobile() { 58 return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 59 } 60 61 // ==================== ç»ä»¶ç±» ==================== 62 function RFJSearchDropdown() { 63 this.instances = new Map(); 64 this.globalConfig = { 65 ajaxUrl: '/wp-admin/admin-ajax.php', 66 nonce: '', 67 debounceDelay: 300, 68 maxResults: { 69 categories: 5, 70 software: 5, 71 companies: 3 72 }, 73 portalToBody: false, 74 onSelect: null, 75 onMore: null 76 }; 77 } 78 79 /** 80 * åå§åæç´¢ä¸æ
81 * @param {string} inputSelector - è¾å ¥æ¡éæ©å¨ 82 * @param {string|Element} dropdownContainer - 䏿容å¨éæ©å¨æå ç´ 83 * @param {Object} options - é 置项 84 */ 85 RFJSearchDropdown.prototype.init = function (inputSelector, dropdownContainer, options) { 86 var self = this; 87 // æ¯æåç¬¦ä¸²éæ©å¨æç´æ¥ä¼ å ¥ DOM å ç´ 88 var input = typeof inputSelector === 'string' 89 ? document.querySelector(inputSelector) 90 : inputSelector; 91 if (!input) { 92 console.warn('[RFJSearchDropdown] Input not found:', inputSelector); 93 return null; 94 } 95 96 var container = typeof dropdownContainer === 'string' 97 ? document.querySelector(dropdownContainer) 98 : dropdownContainer; 99 100 if (!container) { 101 console.warn('[RFJSearchDropdown] Dropdown container not found:', dropdownContainer); 102 return null; 103 } 104 105 // åå¹¶é ç½® 106 var config = Object.assign({}, this.globalConfig, options); 107 108 // çæå¯ä¸å®ä¾ ID 109 var instanceId = Date.now() + '_' + Math.random().toString(36).substr(2, 6); 110 111 // å®ä¾ç¶æ 112 var state = { 113 id: instanceId, 114 input: input, 115 container: container, 116 config: config, 117 xhr: null, 118 isLoading: false, 119 currentKeyword: '', 120 resizeHandler: null, 121 scrollHandler: null 122 }; 123 124 // å建 DOM ç»æ 125 this._createDOM(state); 126 127 // ç»å®äºä»¶ 128 this._bindEvents(state); 129 130 // ä¿åå®ä¾ 131 this.instances.set(instanceId, state); 132 133 return state; 134 }; 135 136 /** 137 * å建 DOM ç»æ 138 */ 139 RFJSearchDropdown.prototype._createDOM = function (state) { 140 var container = state.container; 141 container.className = 'rfj-search-dropdown'; 142 container.innerHTML = ''; 143 container.style.display = 'none'; 144 145 if (state.config.portalToBody) { 146 container.className += ' rfj-search-dropdown--body-portal'; 147 if (container.parentNode !== document.body) { 148 document.body.appendChild(container); 149 } 150 } 151 152 // åè¡¨å®¹å¨ 153 var listEl = document.createElement('div'); 154 listEl.className = 'rfj-sd-list'; 155 container.appendChild(listEl); 156 state.listEl = listEl; 157 158 // åºé¨"æ¥çæ´å¤" 159 var footerEl = document.createElement('div'); 160 footerEl.className = 'rfj-sd-footer'; 161 footerEl.style.display = 'none'; 162 container.appendChild(footerEl); 163 state.footerEl = footerEl; 164 }; 165 166 /** 167 * ç»å®äºä»¶ 168 */ 169 RFJSearchDropdown.prototype._bindEvents = function (state) { 170 var self = this; 171 var input = state.input; 172 var container = state.container; 173 174 if (state.config.portalToBody) { 175 state.resizeHandler = function () { 176 self._positionPortal(state); 177 }; 178 state.scrollHandler = function () { 179 if (container.style.display !== 'none') { 180 self._positionPortal(state); 181 } 182 }; 183 window.addEventListener('resize', state.resizeHandler); 184 window.addEventListener('scroll', state.scrollHandler, true); 185 } 186 187 // è¾å ¥äºä»¶ï¼é²æï¼ 188 var debouncedSearch = debounce(function () { 189 var keyword = input.value.trim(); 190 // è³å°è¾å ¥2个åç¬¦ææ§è¡æç´¢ 191 if (keyword.length < 2) { 192 if (container.getAttribute('data-rfj-history-active') === '1') { 193 return; 194 } 195 self.hide(state); 196 return; 197 } 198 self._fetchData(state, keyword); 199 }, state.config.debounceDelay); 200 201 input.addEventListener('input', debouncedSearch); 202 203 // Focus äºä»¶ 204 input.addEventListener('focus', function () { 205 var keyword = input.value.trim(); 206 if (keyword.length >= 2 && container.innerHTML.trim() !== '') { 207 self._positionPortal(state); 208 container.style.display = 'block'; 209 } 210 }); 211 212 // ç¹å»å¤é¨å ³é 213 document.addEventListener('click', function (e) { 214 if (!container.contains(e.target) && e.target !== input) { 215 container.style.display = 'none'; 216 } 217 }); 218 219 // é®çäºä»¶ 220 input.addEventListener('keydown', function (e) { 221 if (e.key === 'Escape') { 222 container.style.display = 'none'; 223 } else if (e.key === 'Enter') { 224 var keyword = input.value.trim(); 225 if (keyword.length > 0) { 226 var searchUrl = 'https://www.ruanfujia.com/search?kw=' + encodeURIComponent(keyword); 227 window.open(searchUrl, '_blank'); 228 } 229 } 230 }); 231 }; 232 233 /** 234 * è·åæ°æ® 235 */ 236 RFJSearchDropdown.prototype._fetchData = function (state, keyword) { 237 var self = this; 238 239 // åæ¶ä¹åçè¯·æ± 240 if (state.xhr && state.xhr.abort) { 241 state.xhr.abort(); 242 } 243 244 state.isLoading = true; 245 state.currentKeyword = keyword; 246 this._showLoading(state); 247 248 var formData = new FormData(); 249 formData.append('action', 'rfj_search_dropdown'); 250 formData.append('nonce', state.config.nonce); 251 formData.append('keyword', keyword); 252 253 var xhr = new XMLHttpRequest(); 254 xhr.open('POST', state.config.ajaxUrl, true); 255 xhr.onload = function () { 256 state.isLoading = false;
257 if (xhr.status === 200) { 258 try { 259 var res = JSON.parse(xhr.responseText); 260 if (res.success) { 261 self._render(state, res.data); 262 } else { 263 self._renderError(state, res.data ? res.data.message : '请æ±å¤±è´¥'); 264 } 265 } catch (e) { 266 self._renderError(state, 'æ°æ®è§£æå¤±è´¥'); 267 } 268 } else { 269 self._renderError(state, 'ç½ç»é误'); 270 } 271 }; 272 xhr.onerror = function () { 273 state.isLoading = false; 274 self._renderError(state, 'ç½ç»é误'); 275 }; 276 xhr.send(formData); 277 278 state.xhr = xhr; 279 }; 280 281 RFJSearchDropdown.prototype._positionPortal = function (state) { 282 if (!state.config.portalToBody) { 283 return; 284 } 285 286 var rect = state.input.getBoundingClientRect(); 287 var minWidth = parseInt(state.input.getAttribute('data-rfj-search-dropdown-min-width'), 10) || 280; 288 var gutter = 12; 289 var width = Math.max(rect.width, minWidth); 290 width = Math.min(width, window.innerWidth - gutter * 2); 291 var left = Math.min(Math.max(gutter, rect.right - width), window.innerWidth - gutter - width); 292 293 state.container.style.position = 'fixed'; 294 state.container.style.top = Math.round(rect.bottom + 8) + 'px'; 295 state.container.style.left = Math.round(left) + 'px'; 296 state.container.style.right = 'auto'; 297 state.container.style.width = Math.round(width) + 'px'; 298 state.container.style.zIndex = getComputedStyle(document.documentElement) 299 .getPropertyValue('--rfj-z-dropdown-portal').trim() || '10020'; 300 }; 301 302 /** 303 * 渲æç»æ 304 */ 305 RFJSearchDropdown.prototype._render = function (state, data) { 306 var listEl = state.listEl; 307 var footerEl = state.footerEl; 308 var keyword = state.currentKeyword; 309 310 listEl.innerHTML = ''; 311 var hasContent = false; 312 313 // åç±» 314 if (data.categories && data.categories.length > 0) { 315 hasContent = true; 316 listEl.appendChild(this._renderSection(state, 'åç±»', data.categories, 'category', data.search_url + '&type=cat')); 317 } 318 319 // 软件 320 if (data.software && data.software.length > 0) { 321 hasContent = true; 322 listEl.appendChild(this._renderSection(state, '软件', data.software, 'software', data.search_url + '&type=software')); 323 } 324 325 // ä¾åºå 326 if (data.companies && data.companies.length > 0) { 327 hasContent = true; 328 listEl.appendChild(this._renderSection(state, 'ä¾åºå', data.companies, 'company', data.search_url + '&type=company')); 329 } 330 331 // åºé¨æ´å¤ 332 if (data.has_more || (data.categories && data.categories.length > 0) || (data.software && data.software.length > 0) || (data.companies && data.companies.length > 0)) { 333 footerEl.style.display = 'block'; 334 footerEl.innerHTML = '<a href="' + data.search_url + '" class="rfj-sd-more-btn" target="_blank">' + ICONS.arrow + ' æ¥çæ´å¤ç»æ</a>'; 335 } else { 336 footerEl.style.display = 'none'; 337 } 338 339 if (hasContent) { 340 this._positionPortal(state); 341 state.container.style.display = 'block'; 342 } else { 343 state.container.style.display = 'none'; 344 } 345 }; 346 347 /** 348 * 渲æåç±»åºå 349 */ 350 RFJSearchDropdown.prototype._renderSection = function (state, title, items, type, moreUrl) { 351 var self = this; 352 var section = document.createElement('div'); 353 section.className = 'rfj-sd-section'; 354 355 // æ é¢è¡ 356 var header = document.createElement('div'); 357 header.className = 'rfj-sd-section-header'; 358 header.innerHTML = '<span class="rfj-sd-section-title">' + ICONS[type] + title + '</span>'; 359 section.appendChild(header); 360 361 // å表 362 var list = document.createElement('ul'); 363 list.className = 'rfj-sd-items'; 364
365 items.forEach(function (item) { 366 var li = document.createElement('li'); 367 li.className = 'rfj-sd-item'; 368 369 var a = document.createElement('a'); 370 a.href = item.link || 'javascript:void(0);'; 371 a.target = '_blank'; 372 a.className = 'rfj-sd-item-link'; 373 a.textContent = item.title || item.name || ''; 374 375 a.addEventListener('click', function (e) { 376 if (typeof state.config.onSelect === 'function') { 377 state.config.onSelect(type, item); 378 } 379 self.hide(state); 380 }); 381 382 li.appendChild(a); 383 list.appendChild(li); 384 }); 385 386 section.appendChild(list); 387 return section; 388 }; 389 390 /** 391 * æ¾ç¤ºå è½½ç¶æ 392 */ 393 RFJSearchDropdown.prototype._showLoading = function (state) { 394 state.listEl.innerHTML = '<div class="rfj-sd-loading">' + ICONS.loading + ' æç´¢ä¸...</div>'; 395 this._positionPortal(state); 396 state.container.style.display = 'block'; 397 }; 398 399 /** 400 * æ¾ç¤ºé误 401 */ 402 RFJSearchDropdown.prototype._renderError = function (state, message) { 403 state.listEl.innerHTML = '<div class="rfj-sd-error">' + message + '</div>'; 404 this._positionPortal(state); 405 state.container.style.display = 'block'; 406 }; 407 408 /** 409 * éè䏿 410 */ 411 RFJSearchDropdown.prototype.hide = function (state) { 412 if (state) { 413 state.container.style.display = 'none'; 414 } 415 }; 416 417 /** 418 * 鿝å®ä¾ 419 */ 420 RFJSearchDropdown.prototype.destroy = function (state) { 421 if (state && this.instances.has(state.id)) { 422 if (state.resizeHandler) { 423 window.removeEventListener('resize', state.resizeHandler); 424 } 425 if (state.scrollHandler) { 426 window.removeEventListener('scroll', state.scrollHandler, true); 427 } 428 this.instances.delete(state.id); 429 state.container.innerHTML = ''; 430 state.container.style.display = ''; 431 } 432 }; 433 434 // ==================== éææ¹æ³ ==================== 435 RFJSearchDropdown.init = function (inputSelector, dropdownContainer, options) { 436 var instance = new RFJSearchDropdown(); 437 return instance.init(inputSelector, dropdownContainer, options); 438 }; 439 440 RFJSearchDropdown.hideAll = function () { 441 document.querySelectorAll('.rfj-search-dropdown').forEach(function (el) { 442 el.style.display = 'none'; 443 }); 444 }; 445 446 // ==================== åå§åå ¥å£ ==================== 447 // 妿页颿é¢å®ä¹ç data 屿§ï¼èªå¨åå§å 448 document.addEventListener('DOMContentLoaded', function () { 449 document.querySelectorAll('[data-rfj-search]').forEach(function (input) { 450 var containerId = input.getAttribute('data-rfj-search-dropdown'); 451 var container = containerId ? document.querySelector(containerId) : null; 452 if (!container) { 453 container = document.createElement('div'); 454 container.id = 'rfj-sd-' + Date.now(); 455 input.parentNode.insertBefore(container, input.nextSibling); 456 } 457 var ajaxUrl = input.getAttribute('data-ajax-url') || '/wp-admin/admin-ajax.php'; 458 var nonce = input.getAttribute('data-nonce') || ''; 459 RFJSearchDropdown.init(input, container, { 460 ajaxUrl: ajaxUrl, 461 nonce: nonce, 462 portalToBody: input.getAttribute('data-rfj-search-portal') === 'body' 463 }); 464 }); 465 }); 466 467 return RFJSearchDropdown; 468}));
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.