1/** 2 * Olympus â header instant search (WordPress Interactivity API). 3 * 4 * Replaces the jQuery `CRUMINA.TopSearch` module. Debounced input queries the 5 * REST route (olympus/v1/search) and the server-rendered results HTML is bound 6 * into the dropdown. The REST URL comes from the form's `data-wp-context` 7 * (`restUrl`), set in templates/header/general-social.php. 8 * 9 * Async work uses *generator* actions (`yield`), which is required by the 10 * Interactivity API so it can preserve the element/context scope across awaits â 11 * plain async/await loses scope and reactive mutations stop propagating. 12 * 13 * Plain ESM script module â no build step; WordPress supplies 14 * `@wordpress/interactivity` via the import map (WP 6.5+). 15 */ 16import { store, getContext, getElement } from '@wordpress/interactivity'; 17 18const MIN_CHARS = 3; 19const DEBOUNCE_MS = 500; 20const NOTHING_FOUND = '<li class="nothing-found">Nothing found</li>'; 21 22store( 'olympus/search', { 23 state: { 24 // Per-region: the dropdown's inline display value. 25 get dropdownDisplay() { 26 return getContext().open ? 'block' : 'none'; 27 }, 28 }, 29 actions: { 30 *onInput( event ) { 31 const context = getContext(); 32 const value = event.target.value; 33 context.query = value; 34 35 // Debounce: wait, then bail if a newer keystroke superseded this one. 36 yield new Promise( ( resolve ) => 37 setTimeout( resolve, DEBOUNCE_MS ) 38 ); 39 if ( context.query !== value ) { 40 return; 41 } 42 43 yield* runSearch( context ); 44 }, 45 *onFocus( event ) { 46 const context = getContext(); 47 context.query = event.target.value; 48 49 if ( context.query.trim().length < MIN_CHARS ) { 50 return; 51 } 52 if ( context.results ) { 53 context.open = true; 54 } else { 55 yield* runSearch( context ); 56 } 57 }, 58 }, 59 callbacks: { 60 // Render the server-rendered results HTML into the dropdown. 61 // (There is no core `data-wp-html` directive; data-wp-watch re-runs 62 // whenever the read `context.results` changes.) 63 renderResults() { 64 const { ref } = getElement(); 65 ref.innerHTML = getContext().results || ''; 66 }, 67 // Close on Escape. 68 onEscape( event ) { 69 if ( 'Escape' === event.key ) { 70 getContext().open = false; 71 } 72 }, 73 // Close when clicking outside the search form. 74 onDocumentClick( event ) { 75 const { ref } = getElement(); 76 if ( ref && ! ref.contains( event.target ) ) { 77 getContext().open = false; 78 } 79 }, 80 }, 81} ); 82 83/** 84 * Query the REST endpoint and bind the rendered results. 85 * 86 * A generator (delegated with `yield*`) so the Interactivity API keeps the 87 * store scope across the network awaits and reactive mutations propagate. 88 * 89 * @param {Object} context Reactive store context for the search region. 90 */ 91function* runSearch( context ) { 92 const query = ( context.query || '' ).trim(); 93 94 if ( query.length < MIN_CHARS ) { 95 context.open = false; 96 context.results = ''; 97 return; 98 } 99 100 context.open = true; 101 context.loading = true; 102 103 try { 104 // Build the URL safely for both pretty and plain (?rest_route=) permalinks. 105 const url = new URL( context.restUrl, window.location.origin ); 106 url.searchParams.set( 's', query ); 107 const response = yield fetch( url, { 108 headers: { Accept: 'application/json' }, 109 } ); 110 const data = yield response.json(); 111 context.results = data && data.html ? data.html : NOTHING_FOUND; 112 } catch { 113 context.results = NOTHING_FOUND; 114 } finally { 115 context.loading = false; 116 } 117}
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.