| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert} from 'chrome://resources/js/assert_ts.js'; |
| |
| import {getAllNodes, SyncNode, SyncNodeMap, Timer} from './chrome_sync.js'; |
| |
| const ERROR_ATTR: string = 'error'; |
| const SELECTED_ATTR: string = 'selected'; |
| |
| export class SyncSearchManager { |
| private currSearchId_: number = 0; |
| private resultsData_: object[] = []; |
| private selected_: HTMLElement|null = null; |
| private selectedIndex_: number = -1; |
| private resultsControl_: HTMLElement; |
| private detailsControl_: HTMLElement; |
| private queryControl_: HTMLInputElement; |
| private statusControl_: HTMLElement; |
| |
| /** |
| * @param queryControl The <input> object of |
| * type=search where the user's query is typed. |
| * @param submitControl The <button> object |
| * where the user can click to submit the query. |
| * @param statusControl The <span> object display the |
| * search status. |
| * @param resultsControl The <list> object which holds |
| * the list of returned results. |
| * @param detailsControl The <pre> object which |
| * holds the details of the selected result. |
| */ |
| constructor( |
| queryControl: HTMLInputElement, submitControl: HTMLButtonElement, |
| statusControl: HTMLElement, resultsControl: HTMLElement, |
| detailsControl: HTMLElement) { |
| this.resultsControl_ = resultsControl; |
| this.detailsControl_ = detailsControl; |
| this.queryControl_ = queryControl; |
| this.statusControl_ = statusControl; |
| |
| submitControl.addEventListener('click', () => this.startSearch_()); |
| // Decorate search box. |
| this.queryControl_.addEventListener('search', () => this.startSearch_()); |
| this.queryControl_.value = ''; |
| this.resultsControl_.setAttribute('role', 'list'); |
| this.resultsControl_.tabIndex = 0; |
| this.resultsControl_.addEventListener( |
| 'keydown', e => this.handleKeydown_(e)); |
| } |
| |
| private startSearch_() { |
| const query = this.queryControl_.value; |
| this.statusControl_.textContent = ''; |
| this.resultsData_ = []; |
| this.drawResultsList_(); |
| if (!query) { |
| return; |
| } |
| |
| this.statusControl_.textContent = 'Searching for ' + query + '...'; |
| this.queryControl_.toggleAttribute(ERROR_ATTR, false); |
| this.doSearch_(query); |
| } |
| |
| private setSelected_(newSelected: HTMLElement, newIndex: number) { |
| if (this.selected_) { |
| this.selected_.toggleAttribute(SELECTED_ATTR, false); |
| } |
| newSelected.toggleAttribute(SELECTED_ATTR, true); |
| this.selected_ = newSelected; |
| this.selectedIndex_ = newIndex; |
| this.detailsControl_.textContent = |
| JSON.stringify(this.resultsData_[this.selectedIndex_], null, 2); |
| } |
| |
| private handleKeydown_(e: KeyboardEvent) { |
| let newIndex: number = -1; |
| if (e.key === 'ArrowUp') { |
| newIndex = this.selectedIndex_ === -1 ? |
| this.resultsData_.length - 1 : |
| Math.min(0, this.selectedIndex_ - 1); |
| } else if ( |
| e.key === 'ArrowDown' && |
| this.selectedIndex_ < this.resultsData_.length - 1) { |
| newIndex = this.selectedIndex_ === -1 ? 0 : this.selectedIndex_ + 1; |
| } else if (e.key === 'Home') { |
| newIndex = 0; |
| } else if (e.key === 'End') { |
| newIndex = this.resultsData_.length - 1; |
| } |
| |
| if (newIndex === -1) { |
| return; |
| } |
| |
| const items = this.resultsControl_.querySelectorAll('li'); |
| this.setSelected_(items[newIndex]!, newIndex); |
| assert(this.selected_); |
| this.selected_.scrollIntoViewIfNeeded(); |
| e.preventDefault(); |
| } |
| |
| private drawResultsList_() { |
| this.selected_ = null; |
| this.selectedIndex_ = -1; |
| this.resultsControl_.innerHTML = |
| window.trustedTypes ? window.trustedTypes.emptyHTML : ''; |
| this.resultsData_.forEach((item: object, index: number) => { |
| const li = document.createElement('li'); |
| li.setAttribute('role', 'listitem'); |
| li.textContent = item.toString(); |
| this.resultsControl_.appendChild(li); |
| li.addEventListener('click', () => { |
| this.setSelected_(li, index); |
| }); |
| }); |
| } |
| |
| /** |
| * Runs a search with the given query. |
| * @param query The regex to do the search with. |
| */ |
| private doSearch_(query: string) { |
| const timer = new Timer(); |
| this.currSearchId_++; |
| const searchId = this.currSearchId_; |
| try { |
| const regex = new RegExp(query); |
| getAllNodes((nodeMap: SyncNodeMap) => { |
| // Put all nodes into one big list that ignores the type. |
| const nodes: SyncNode[] = |
| nodeMap.map(x => x.nodes).reduce((a, b) => a.concat(b)); |
| if (this.currSearchId_ !== searchId) { |
| return; |
| } |
| this.displayResults_( |
| timer, nodes.filter(function(elem) { |
| return regex.test(JSON.stringify(elem, null, 2)); |
| }), |
| null); |
| }); |
| } catch (err) { |
| // Sometimes the provided regex is invalid. This and other errors will |
| // be caught and handled here. |
| this.displayResults_(timer, [], err as Error); |
| } |
| } |
| |
| private displayResults_( |
| timer: Timer, nodes: Array<{NON_UNIQUE_NAME: string}>, |
| error: Error|null) { |
| if (error) { |
| this.statusControl_.textContent = 'Error: ' + error; |
| this.queryControl_.toggleAttribute(ERROR_ATTR, true); |
| } else { |
| this.statusControl_.textContent = 'Found ' + nodes.length + ' nodes in ' + |
| timer.getElapsedSeconds() + 's'; |
| this.queryControl_.toggleAttribute(ERROR_ATTR, false); |
| |
| // TODO(akalin): Write a nicer list display. |
| for (let i = 0; i < nodes.length; ++i) { |
| nodes[i]!.toString = function() { |
| return this.NON_UNIQUE_NAME; |
| }; |
| } |
| this.resultsData_.push(...nodes); |
| this.drawResultsList_(); |
| } |
| } |
| |
| setDataForTest(data: object[]) { |
| this.resultsData_ = data; |
| this.drawResultsList_(); |
| } |
| } |
| |
| function createDoQueryFunction( |
| queryControl: HTMLInputElement, submitControl: HTMLButtonElement, |
| query: string): () => void { |
| return function() { |
| queryControl.value = query; |
| submitControl.click(); |
| }; |
| } |
| |
| /** |
| * Decorates the quick search controls |
| * |
| * @param quickLinkArray The <a> object which |
| * will be given a link to a quick filter option. |
| * @param submitControl |
| * @param queryControl The <input> object of type=search where user's query is |
| * typed. |
| */ |
| export function decorateQuickQueryControls( |
| quickLinkArray: NodeListOf<HTMLElement>, submitControl: HTMLButtonElement, |
| queryControl: HTMLInputElement) { |
| for (let index = 0; index < quickLinkArray.length; ++index) { |
| const quickQuery = quickLinkArray[index]!.getAttribute('data-query'); |
| assert(quickQuery); |
| const quickQueryFunction = |
| createDoQueryFunction(queryControl, submitControl, quickQuery); |
| quickLinkArray[index]!.addEventListener('click', quickQueryFunction); |
| } |
| } |