1/** 2 * sin/audience-wizard â Interactivity API store. 3 * 4 * State machine: picker â result, with reset. 5 * Search: debounced (250ms) fetch against context.searchUrl 6 * (rest_url('sin/v1/wizard-search')), with AbortController 7 * to drop stale responses while typing fast. 8 * 9 * Note: getContext() only works inside the call stack of a directive 10 * invocation. Async timers must capture ctx via closure outside the 11 * timer callback. 12 */ 13import { store, getContext } from '@wordpress/interactivity'; 14 15const DEBOUNCE_MS = 250; 16const MIN_QUERY = 2; 17 18let debounceTimer = null; 19let activeAbort = null; 20 21const { state } = store( 'sin/audienceWizard', { 22 state: { 23 // Derived flags read by data-wp-class--is-* directives. 24 get isPicker() { 25 return getContext().step === 'picker'; 26 }, 27 get isResult() { 28 return getContext().step === 'result'; 29 }, 30 get isStartup() { 31 const ctx = getContext(); 32 return ctx.step === 'result' && ctx.selectedAudience === 'startup'; 33 }, 34 get isBedrift() { 35 const ctx = getContext(); 36 return ctx.step === 'result' && ctx.selectedAudience === 'bedrift'; 37 }, 38 get isOffentlig() { 39 const ctx = getContext(); 40 return ctx.step === 'result' && ctx.selectedAudience === 'offentlig'; 41 }, 42 get isForskning() { 43 const ctx = getContext(); 44 return ctx.step === 'result' && ctx.selectedAudience === 'forskning'; 45 }, 46 get showEmpty() { 47 const ctx = getContext(); 48 return ( 49 ctx.showResults && 50 ! ctx.isLoading && 51 ctx.query.length >= MIN_QUERY && 52 Array.isArray( ctx.results ) && 53 ctx.results.length === 0 54 ); 55 }, 56 }, 57 58 actions: { 59 selectAudience: ( event ) => { 60 const audience = event.currentTarget.getAttribute( 'data-audience' ); 61 if ( ! audience ) { 62 return; 63 } 64 const ctx = getContext(); 65 ctx.selectedAudience = audience; 66 ctx.step = 'result'; 67 }, 68 69 reset: () => { 70 const ctx = getContext(); 71 ctx.selectedAudience = ''; 72 ctx.step = 'picker'; 73 }, 74 75 handleSearchInput: ( event ) => { 76 const ctx = getContext(); 77 const value = event.target.value || ''; 78 ctx.query = value; 79 80 if ( debounceTimer ) { 81 clearTimeout( debounceTimer ); 82 } 83 84 const trimmed = value.trim(); 85 if ( trimmed.length < MIN_QUERY ) { 86 ctx.results = []; 87 ctx.showResults = false; 88 ctx.isLoading = false; 89 if ( activeAbort ) { 90 activeAbort.abort(); 91 activeAbort = null; 92 } 93 return; 94 } 95 96 // Capture references via closure â getContext() does NOT work 97 // inside the setTimeout/fetch callbacks because the directive 98 // frame has unwound by the time they fire. 99 const capturedCtx = ctx; 100 const searchUrl = ctx.searchUrl; 101 102 debounceTimer = setTimeout( async () => { 103 capturedCtx.isLoading = true; 104 capturedCtx.showResults = true; 105 106 if ( activeAbort ) { 107 activeAbort.abort(); 108 } 109 activeAbort = new AbortController(); 110 const thisAbort = activeAbort; 111 112 try { 113 const url = `${ searchUrl }?q=${ encodeURIComponent( trimmed ) }`; 114 const response = await fetch( url, { signal: thisAbort.signal } ); 115 if ( ! response.ok ) { 116 throw new Error( `HTTP ${ response.status }` ); 117 } 118 const data = await response.json(); 119 // Only commit if this query is still what's in the box. 120 if ( capturedCtx.query.trim() === trimmed ) { 121 capturedCtx.results = Array.isArray( data.results ) ? data.results : []; 122 capturedCtx.isLoading = false; 123 } 124 } catch ( err ) { 125 if ( err && err.name === 'AbortError' ) { 126 return; // Superseded by a newer request. 127 } 128 capturedCtx.results = []; 129 capturedCtx.isLoading = false; 130 } 131 }, DEBOUNCE_MS ); 132 }, 133 }, 134} );
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.