1+ // ========================================
2+ // Generic Resource Browser
3+ // ========================================
4+
5+ // Configuration passed to init()
6+ let config = null ;
7+
8+ // Index data (loaded via script tag)
9+ let indexData = null ;
10+ let columns = [ ] ;
11+ let dictionaries = { } ;
12+ let resources = [ ] ;
13+
14+ // Column indices (set after loading)
15+ let colIdx = { } ;
16+
17+ // ========================================
18+ // Utility Functions
19+ // ========================================
20+
21+ // Parse URL parameters based on configured search params
22+ function getUrlParams ( ) {
23+ const params = new URLSearchParams ( window . location . search ) ;
24+ const result = { } ;
25+
26+ for ( const param of config . searchParams ) {
27+ result [ param ] = params . get ( param ) || '' ;
28+ }
29+
30+ return result ;
31+ }
32+
33+ // Check if any search params are present
34+ function hasSearchParams ( params ) {
35+ return config . searchParams . some ( param => params [ param ] ) ;
36+ }
37+
38+ // Escape HTML
39+ function escapeHtml ( text ) {
40+ if ( ! text ) return '' ;
41+ const div = document . createElement ( 'div' ) ;
42+ div . textContent = text ;
43+ return div . innerHTML ;
44+ }
45+
46+ // Get value from a resource row, resolving dictionary references
47+ function getValue ( row , colName ) {
48+ const idx = colIdx [ colName ] ;
49+ if ( idx === undefined ) return null ;
50+
51+ const val = row [ idx ] ;
52+ if ( val === null || val === undefined || val === - 1 ) return null ;
53+
54+ // Check if this column uses a dictionary
55+ const dict = dictionaries [ colName ] ;
56+ if ( dict ) {
57+ if ( Array . isArray ( val ) ) {
58+ return val . map ( i => i >= 0 ? dict [ i ] : null ) . filter ( v => v !== null ) ;
59+ }
60+ return val >= 0 ? dict [ val ] : null ;
61+ }
62+
63+ return val ;
64+ }
65+
66+ // Get single string value (first if array)
67+ function getStringValue ( row , colName ) {
68+ const val = getValue ( row , colName ) ;
69+ if ( Array . isArray ( val ) ) return val [ 0 ] || '' ;
70+ return val || '' ;
71+ }
72+
73+ // Check if a value matches the search term (case-insensitive, starts-with)
74+ function matchesSearch ( value , searchTerm ) {
75+ if ( ! searchTerm ) return true ;
76+ if ( ! value ) return false ;
77+
78+ const lowerSearch = searchTerm . toLowerCase ( ) ;
79+
80+ if ( Array . isArray ( value ) ) {
81+ return value . some ( v => v && v . toLowerCase ( ) . includes ( lowerSearch ) ) ;
82+ }
83+
84+ return value . toLowerCase ( ) . includes ( lowerSearch ) ;
85+ }
86+
87+ // ========================================
88+ // Search Functions
89+ // ========================================
90+
91+ // Perform the search using the in-memory index
92+ function performSearch ( params ) {
93+ const startTime = performance . now ( ) ;
94+ const resultsBody = document . getElementById ( 'resultsBody' ) ;
95+ const totalCount = document . getElementById ( 'totalCount' ) ;
96+ const searchTime = document . getElementById ( 'searchTime' ) ;
97+
98+ if ( ! indexData ) {
99+ resultsBody . innerHTML = '<div class="error">Index not loaded</div>' ;
100+ return ;
101+ }
102+
103+ // Filter resources based on all configured search params
104+ const matches = resources . filter ( row => {
105+ for ( const param of config . searchParams ) {
106+ if ( params [ param ] && ! matchesSearch ( getValue ( row , param ) , params [ param ] ) ) {
107+ return false ;
108+ }
109+ }
110+ return true ;
111+ } ) ;
112+
113+ const elapsed = ( performance . now ( ) - startTime ) . toFixed ( 1 ) ;
114+
115+ totalCount . textContent = `(${ matches . length } found)` ;
116+ searchTime . textContent = `${ elapsed } ms` ;
117+
118+ if ( matches . length === 0 ) {
119+ resultsBody . innerHTML = '<div class="empty-state">No content found matching your criteria</div>' ;
120+ return ;
121+ }
122+
123+ // Render results table
124+ const displayResults = matches . slice ( 0 , config . maxResults ) ;
125+
126+ // Build table header from configured columns
127+ let html = `
128+ <table class="grid">
129+ <thead>
130+ <tr>
131+ ${ config . displayColumns . map ( col => `<th>${ escapeHtml ( col . label ) } </th>` ) . join ( '' ) }
132+ </tr>
133+ </thead>
134+ <tbody>
135+ ` ;
136+
137+ // Build table rows
138+ for ( const row of displayResults ) {
139+ html += '<tr>' ;
140+
141+ for ( const col of config . displayColumns ) {
142+ const value = getStringValue ( row , col . key ) ;
143+
144+ if ( col . isLink && value ) {
145+ const id = getStringValue ( row , 'id' ) ;
146+ html += `<td><a href="${ config . detailPage } ?id=${ encodeURIComponent ( id ) } ">${ escapeHtml ( value ) } </a></td>` ;
147+ } else {
148+ html += `<td>${ escapeHtml ( value ) } </td>` ;
149+ }
150+ }
151+
152+ html += '</tr>' ;
153+ }
154+
155+ html += '</tbody></table>' ;
156+
157+ if ( matches . length > config . maxResults ) {
158+ html += `<div class="empty-state">Showing first ${ config . maxResults } of ${ matches . length } results</div>` ;
159+ }
160+
161+ resultsBody . innerHTML = html ;
162+ }
163+
164+ // ========================================
165+ // Index Loading
166+ // ========================================
167+
168+ // Initialize the index from the global variable
169+ function loadIndex ( ) {
170+ try {
171+ if ( typeof INDEX === 'undefined' ) {
172+ throw new Error ( 'Index not found. Make sure the index JS file is in the same folder.' ) ;
173+ }
174+
175+ indexData = INDEX ;
176+ columns = indexData . columns || [ ] ;
177+ dictionaries = indexData . dictionaries || { } ;
178+ resources = indexData . resources || [ ] ;
179+
180+ // Build column index map
181+ columns . forEach ( ( col , idx ) => {
182+ colIdx [ col ] = idx ;
183+ } ) ;
184+
185+ return true ;
186+
187+ } catch ( error ) {
188+ console . error ( 'Error loading index:' , error ) ;
189+ return false ;
190+ }
191+ }
192+
193+ // ========================================
194+ // Search Page Initialization
195+ // ========================================
196+
197+ function init ( cfg ) {
198+ config = cfg ;
199+ const params = getUrlParams ( ) ;
200+
201+ // Populate form fields from URL params
202+ for ( const param of config . searchParams ) {
203+ const element = document . getElementById ( param ) ;
204+ if ( element ) {
205+ element . value = params [ param ] ;
206+ }
207+ }
208+
209+ // Load index from global variable
210+ const loaded = loadIndex ( ) ;
211+
212+ // If we have search params and index loaded, perform the search
213+ if ( loaded && hasSearchParams ( params ) ) {
214+ performSearch ( params ) ;
215+ }
216+ }
217+
218+ // ========================================
219+ // Detail Page Functions
220+ // ========================================
221+
222+ // Get id from URL
223+ function getIdParam ( ) {
224+ const params = new URLSearchParams ( window . location . search ) ;
225+ return params . get ( 'id' ) || '' ;
226+ }
227+
228+ // Toggle JSON section visibility
229+ function toggleJson ( ) {
230+ const content = document . getElementById ( 'jsonContent' ) ;
231+ const toggle = document . getElementById ( 'jsonToggle' ) ;
232+
233+ if ( content . classList . contains ( 'open' ) ) {
234+ content . classList . remove ( 'open' ) ;
235+ toggle . textContent = 'Show' ;
236+ } else {
237+ content . classList . add ( 'open' ) ;
238+ toggle . textContent = 'Hide' ;
239+ }
240+ }
241+
242+ // Render resource details
243+ function renderResource ( res ) {
244+ const narrativeHtml = res . text ?. div || '<p>No narrative available</p>' ;
245+
246+ return `
247+ <div class="narrative">
248+ ${ narrativeHtml }
249+ </div>
250+
251+ <div class="json-section">
252+ <div class="json-header" onclick="toggleJson()">
253+ <span class="json-title">Raw JSON</span>
254+ <span class="json-toggle" id="jsonToggle">Show</span>
255+ </div>
256+ <div class="json-content" id="jsonContent">
257+ <pre class="raw-json">${ escapeHtml ( JSON . stringify ( res , null , 2 ) ) } </pre>
258+ </div>
259+ </div>
260+ ` ;
261+ }
262+
263+ // Load and display the resource
264+ function loadResource ( ) {
265+ const content = document . getElementById ( 'content' ) ;
266+ const id = getIdParam ( ) ;
267+
268+ if ( ! id ) {
269+ content . innerHTML = '<div class="empty-state">No resource ID specified</div>' ;
270+ return ;
271+ }
272+
273+ if ( typeof DATA_SET === 'undefined' ) {
274+ content . innerHTML = '<div class="error">Data not found. Make sure the data JS file is in the same folder.</div>' ;
275+ return ;
276+ }
277+
278+ const res = DATA_SET [ id ] ;
279+
280+ if ( ! res ) {
281+ content . innerHTML = `<div class="empty-state">Resource "${ escapeHtml ( id ) } " not found</div>` ;
282+ return ;
283+ }
284+
285+ content . innerHTML = renderResource ( res ) ;
286+
287+ // Update page title
288+ const name = res . code ?. coding ?. [ 0 ] ?. display || res . name || id ;
289+ document . title = `${ name } - ${ res . resourceType || 'Resource' } Details` ;
290+ }
0 commit comments