PageSourceSearch

https://community.europeanschoolradio.eu/wp-content/themes/olympus/src/interactivity/search.js?ver=4.8.1

js europeanschoolradio.eu collected 2026-09-24 08:45:19 UTC 3,393 bytes, 117 lines download raw bytes

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.