/* * searchBar.aus - Search bar UI component for Lucene-backed search. * * Builds the search input and results dropdown. The template's inline * script must wire up the keyup listener and HTTP callbacks because * :: callback references only resolve in inline script blocks. * * Usage from the template: * searchBar = new SearchBar(docIndex, 'Search docs ...', null); * page.add(searchBar); * searchBar.getInputField().addListener('keyup', ::onKeyUp, null); */ /** * SearchBar provides a search input with a results dropdown. */ class SearchBar : Div { public docIndex = null; public scopeProduct = null; public resultsDiv = null; public inputField = null; /** * Constructs a SearchBar. * @p DocIndex is a list of doc entry maps (kept for sidebar compatibility). * @p Placeholder is a string with the input placeholder text. * @p ScopeProduct is an optional string to pre-filter to a product. */ public SearchBar(list DocIndex, string Placeholder, string ScopeProduct = null) { this.Div(); this.setAttr('class', 'search-bar'); this.docIndex = DocIndex; this.scopeProduct = ScopeProduct; /* Input wrapper */ inputWrapper = new Div(); inputWrapper.setAttr('class', 'search-input-wrapper'); /* Scope tag (shown when filtered to a product) */ if (ScopeProduct != null) { scopeTag = new Span(); scopeTag.setAttr('class', 'search-scope-tag'); scopeTag.add(new Text(ScopeProduct)); inputWrapper.add(scopeTag); } this.inputField = new Input('text', 'aussom-search-query'); this.inputField.setAttr('class', 'search-input'); this.inputField.setPlaceholder(Placeholder); this.inputField.setAttr('autocomplete', 'off'); inputWrapper.add(this.inputField); this.add(inputWrapper); /* Results dropdown */ this.resultsDiv = new Div(); this.resultsDiv.setAttr('class', 'search-results-dropdown'); this.resultsDiv.setStyle('display', 'none'); this.add(this.resultsDiv); } /** * Returns the input field HNode for event wiring. * @r The Input HNode. */ public getInputField() { return this.inputField; } /** * Builds the search URL from the current input value. Returns * null if the query is too short. Also hides the dropdown when * the query is cleared. * @r A string URL or null. */ public getSearchUrl() { query = this.inputField.getValue(); if (query == null || query.trim() == '' || query.trim().length() < 2) { this.resultsDiv.setStyle('display', 'none'); this.resultsDiv.clear(); return null; } url = 'search?q=' + query.trim(); if (this.scopeProduct != null) { url = url + '&product=' + this.scopeProduct; } return url; } /** * Processes the search HTTP response and updates the results * dropdown. * @p resp is an HttpResponse object from Http.get(). */ public handleResponse(resp) { if (!resp.isSuccessful()) { return; } data = json.parse(resp.body); results = data.results; this.resultsDiv.clear(); if (results.size() > 0) { for (result : results) { item = new A(); /* Common docs are shared across products. Open them in * the user's current scope so the breadcrumb and sidebar * stay in their product. Fall back to the aussom CLI * product when there is no scope (e.g. on /docs). */ linkProduct = result.product; if (linkProduct == 'common') { if (this.scopeProduct != null) { linkProduct = this.scopeProduct; } else { linkProduct = 'aussom'; } } href = 'docPage?product=' + linkProduct + '&page=' + result.path + '&title=' + result.name; item.setAttr('href', href); item.setAttr('class', 'search-result-item'); nameSpan = new Span(); nameSpan.add(new Text(result.name)); item.add(nameSpan); /* Snippet preview */ if (result.snippet != null && result.snippet != '') { snippetDiv = new Div(); snippetDiv.setAttr('class', 'search-result-snippet'); snippetDiv.setHtml(result.snippet); item.add(snippetDiv); } /* Product tag */ tagSpan = new Span(); tagSpan.setAttr('class', 'search-result-tag'); if (this.scopeProduct == null) { tagSpan.add(new Text(result.product)); } else { tagSpan.add(new Text(result.type)); } item.add(tagSpan); this.resultsDiv.add(item); } /* Cross-product hint */ if (data.otherCount > 0) { otherCount = '' + data.otherCount; hint = new Div(); hint.setAttr('class', 'search-result-item text-muted'); hint.add(new Text('Also found ' + otherCount + ' results in other products.')); this.resultsDiv.add(hint); } this.resultsDiv.setStyle('display', 'block'); } else { noResult = new Div(); noResult.setAttr('class', 'search-result-item text-muted'); noResult.add(new Text('No results found.')); this.resultsDiv.add(noResult); this.resultsDiv.setStyle('display', 'block'); } } }