|  | // Copyright 2018 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | import {PageClassification} from './omnibox.mojom-webui.js'; | 
|  | import {OmniboxElement} from './omnibox_element.js'; | 
|  | import sheet from './omnibox_input.css' with {type : 'css'}; | 
|  |  | 
|  | export interface QueryInputs { | 
|  | inputText: string; | 
|  | resetAutocompleteController: boolean; | 
|  | cursorLock: boolean; | 
|  | cursorPosition: number; | 
|  | zeroSuggest: boolean; | 
|  | preventInlineAutocomplete: boolean; | 
|  | preferKeyword: boolean; | 
|  | currentUrl: string; | 
|  | pageClassification: number; | 
|  | } | 
|  |  | 
|  | export interface DisplayInputs { | 
|  | showIncompleteResults: boolean; | 
|  | showDetails: boolean; | 
|  | showAllProviders: boolean; | 
|  | elideCells: boolean; | 
|  | thinRows: boolean; | 
|  | } | 
|  |  | 
|  | export class OmniboxInput extends OmniboxElement { | 
|  | private elements: { | 
|  | top: HTMLElement, | 
|  | arrowPadding: HTMLElement, | 
|  | connectWindowOmnibox: HTMLInputElement, | 
|  | currentUrl: HTMLInputElement, | 
|  | elideCells: HTMLInputElement, | 
|  | exportClipboard: HTMLElement, | 
|  | exportFile: HTMLElement, | 
|  | filterText: HTMLInputElement, | 
|  | historyWarning: HTMLElement, | 
|  | importClipboard: HTMLElement, | 
|  | importedWarning: HTMLElement, | 
|  | importFile: HTMLElement, | 
|  | importFileInput: HTMLInputElement, | 
|  | inputText: HTMLInputElement, | 
|  | lockCursorPosition: HTMLInputElement, | 
|  | pageClassification: HTMLSelectElement, | 
|  | preferKeyword: HTMLInputElement, | 
|  | preventInlineAutocomplete: HTMLInputElement, | 
|  | processBatch: HTMLElement, | 
|  | processBatchInput: HTMLInputElement, | 
|  | resetAutocompleteController: HTMLInputElement, | 
|  | responsesCount: HTMLElement, | 
|  | responseSelection: HTMLInputElement, | 
|  | showAllProviders: HTMLInputElement, | 
|  | showDetails: HTMLInputElement, | 
|  | showIncompleteResults: HTMLInputElement, | 
|  | thinRows: HTMLInputElement, | 
|  | zeroSuggest: HTMLInputElement, | 
|  | }; | 
|  |  | 
|  | constructor() { | 
|  | super('omnibox-input-template'); | 
|  | this.shadowRoot!.adoptedStyleSheets = [sheet]; | 
|  | } | 
|  |  | 
|  | connectedCallback() { | 
|  | this.elements = { | 
|  | top: this.$<HTMLElement>('#top')!, | 
|  | arrowPadding: this.$<HTMLElement>('#arrow-padding')!, | 
|  | connectWindowOmnibox: this.$<HTMLInputElement>('#connect-window-omnibox')! | 
|  | , | 
|  | currentUrl: this.$<HTMLInputElement>('#current-url')!, | 
|  | elideCells: this.$<HTMLInputElement>('#elide-cells')!, | 
|  | exportClipboard: this.$<HTMLElement>('#export-clipboard')!, | 
|  | exportFile: this.$<HTMLElement>('#export-file')!, | 
|  | filterText: this.$<HTMLInputElement>('#filter-text')!, | 
|  | historyWarning: this.$<HTMLElement>('#history-warning')!, | 
|  | importClipboard: this.$<HTMLElement>('#import-clipboard')!, | 
|  | importedWarning: this.$<HTMLElement>('#imported-warning')!, | 
|  | importFile: this.$<HTMLElement>('#import-file')!, | 
|  | importFileInput: this.$<HTMLInputElement>('#import-file-input')!, | 
|  | inputText: this.$<HTMLInputElement>('#input-text')!, | 
|  | lockCursorPosition: this.$<HTMLInputElement>('#lock-cursor-position')!, | 
|  | pageClassification: this.$<HTMLSelectElement>('#page-classification')!, | 
|  | preferKeyword: this.$<HTMLInputElement>('#prefer-keyword')!, | 
|  | preventInlineAutocomplete: | 
|  | this.$<HTMLInputElement>('#prevent-inline-autocomplete')!, | 
|  | processBatch: this.$<HTMLElement>('#process-batch')!, | 
|  | processBatchInput: this.$<HTMLInputElement>('#process-batch-input')!, | 
|  | resetAutocompleteController: | 
|  | this.$<HTMLInputElement>('#reset-autocomplete-controller')!, | 
|  | responsesCount: this.$<HTMLElement>('#responses-count')!, | 
|  | responseSelection: this.$<HTMLInputElement>('#response-selection')!, | 
|  | showAllProviders: this.$<HTMLInputElement>('#show-all-providers')!, | 
|  | showDetails: this.$<HTMLInputElement>('#show-details')!, | 
|  | showIncompleteResults: | 
|  | this.$<HTMLInputElement>('#show-incomplete-results')!, | 
|  | thinRows: this.$<HTMLInputElement>('#thin-rows')!, | 
|  | zeroSuggest: this.$<HTMLInputElement>('#zero-suggest')!, | 
|  | }; | 
|  | this.restoreInputs(); | 
|  | this.setupElementListeners(); | 
|  | this.addPageClassification(); | 
|  | } | 
|  |  | 
|  | // Add Page Classification labels as options to dropdown. | 
|  | private addPageClassification() { | 
|  | const dropdown = this.$<HTMLSelectElement>('#page-classification')!; | 
|  | for (const page in Object.keys(PageClassification)) { | 
|  | const label = PageClassification[page]; | 
|  | // Filter out built-in reverse mappings for this numeric enum. | 
|  | if (label === undefined) { | 
|  | continue; | 
|  | } | 
|  | const option = document.createElement('option'); | 
|  | option.value = page; | 
|  | option.text = label; | 
|  | // Pre-select the OTHER option. | 
|  | page === '4' ? option.selected = true : null; | 
|  | dropdown.appendChild(option); | 
|  | } | 
|  | } | 
|  |  | 
|  | private storeInputs() { | 
|  | const inputs = { | 
|  | connectWindowOmnibox: this.connectWindowOmnibox, | 
|  | displayInputs: this.displayInputs, | 
|  | }; | 
|  | window.localStorage.setItem('preserved-inputs', JSON.stringify(inputs)); | 
|  | } | 
|  |  | 
|  | private restoreInputs() { | 
|  | const inputsString = window.localStorage.getItem('preserved-inputs'); | 
|  | const inputs = inputsString && JSON.parse(inputsString) || {}; | 
|  | this.elements.connectWindowOmnibox.checked = inputs.connectWindowOmnibox; | 
|  | this.displayInputs = | 
|  | inputs.displayInputs || OmniboxInput.defaultDisplayInputs; | 
|  | } | 
|  |  | 
|  | private setupElementListeners() { | 
|  | [this.elements.inputText, | 
|  | this.elements.resetAutocompleteController, | 
|  | this.elements.lockCursorPosition, | 
|  | this.elements.zeroSuggest, | 
|  | this.elements.preventInlineAutocomplete, | 
|  | this.elements.preferKeyword, | 
|  | this.elements.currentUrl, | 
|  | this.elements.pageClassification, | 
|  | ].forEach(element => { | 
|  | element.addEventListener('input', this.onQueryInputsChanged.bind(this)); | 
|  | }); | 
|  |  | 
|  | // Set text of #arrow-padding to substring of #input-text text, from | 
|  | // beginning until cursor position, in order to correctly align .arrow-up. | 
|  | this.elements.inputText.addEventListener( | 
|  | 'input', this.positionCursorPositionIndicators.bind(this)); | 
|  |  | 
|  | this.elements.connectWindowOmnibox.addEventListener( | 
|  | 'input', this.storeInputs.bind(this)); | 
|  |  | 
|  | this.elements.responseSelection.addEventListener( | 
|  | 'input', this.onResponseSelectionChanged.bind(this)); | 
|  | this.elements.responseSelection.addEventListener( | 
|  | 'blur', this.onResponseSelectionBlur.bind(this)); | 
|  |  | 
|  | [this.elements.showIncompleteResults, | 
|  | this.elements.showDetails, | 
|  | this.elements.showAllProviders, | 
|  | this.elements.elideCells, | 
|  | this.elements.thinRows, | 
|  | ].forEach(element => { | 
|  | element.addEventListener('input', this.onDisplayInputsChanged.bind(this)); | 
|  | }); | 
|  |  | 
|  | this.elements.filterText.addEventListener( | 
|  | 'input', this.onFilterInputsChanged.bind(this)); | 
|  |  | 
|  | this.elements.exportClipboard.addEventListener( | 
|  | 'click', this.onExportClipboard.bind(this)); | 
|  | this.elements.exportFile.addEventListener( | 
|  | 'click', this.onExportFile.bind(this)); | 
|  | this.elements.importClipboard.addEventListener( | 
|  | 'click', this.onImportClipboard.bind(this)); | 
|  | this.elements.importFileInput.addEventListener( | 
|  | 'input', this.onImportFile.bind(this)); | 
|  | this.elements.processBatchInput.addEventListener( | 
|  | 'input', this.onProcessBatchFile.bind(this)); | 
|  |  | 
|  | this.setupDragListeners(this.elements.top); | 
|  | this.elements.top.addEventListener('drop', this.onImportDropped.bind(this)); | 
|  |  | 
|  | this.setupDragListeners(this.elements.processBatch); | 
|  | this.elements.processBatch.addEventListener( | 
|  | 'drop', this.onProcessBatchDropped.bind(this)); | 
|  |  | 
|  | this.$all<HTMLElement>('.button').forEach( | 
|  | el => el.addEventListener('keypress', (e: KeyboardEvent) => { | 
|  | if (e.key === ' ' || e.key === 'Enter') { | 
|  | el.click(); | 
|  | } | 
|  | })); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Sets up boilerplate event listeners for an element that is able to receive | 
|  | * drag events. | 
|  | */ | 
|  | private setupDragListeners(element: Element) { | 
|  | // There are 2 classes toggled during drags: | 
|  | // - `drag-background` alters the `element`'s background when the mouse is | 
|  | //   over it to indicate it's a drag target. | 
|  | // - `drag-silence` silences mouse events from the children of `element` | 
|  | //   while the drag is active. This is necessary to avoid receiving | 
|  | //   `dragenter` & `dragleave` events when children of `element` are entered | 
|  | //   or left. | 
|  | // Ideally, there'd be just 1 class controlling both behaviors and active | 
|  | // when dragging over `element`. However, `dragenter` and `dragleave` events | 
|  | // are racy (see https://stackoverflow.com/questions/7110353). So instead, | 
|  | // toggle `drag-background` when the dragging-mouse enters/leaves `element`. | 
|  | // And toggle `drag-silence` when the mouse begins/stops dragging. This | 
|  | // workaround isn't 100% accurate, but all the other workarounds (e.g. those | 
|  | // listed in the stackoverflow answers), were significantly worse. | 
|  | element.addEventListener('dragenter', () => { | 
|  | element.classList.add('drag-background'); | 
|  | element.classList.add('drag-silence'); | 
|  | }); | 
|  | element.addEventListener( | 
|  | 'dragleave', () => element.classList.remove('drag-background')); | 
|  | element.addEventListener('dragover', e => e.preventDefault()); | 
|  | element.addEventListener('drop', e => { | 
|  | e.preventDefault(); | 
|  | element.classList.remove('drag-background'); | 
|  | element.classList.remove('drag-silence'); | 
|  | }); | 
|  | element.addEventListener( | 
|  | 'mouseenter', () => element.classList.remove('drag-silence')); | 
|  | } | 
|  |  | 
|  | private onQueryInputsChanged() { | 
|  | this.elements.importedWarning.hidden = true; | 
|  | this.dispatchEvent( | 
|  | new CustomEvent('query-inputs-changed', {detail: this.queryInputs})); | 
|  | } | 
|  |  | 
|  | get queryInputs(): QueryInputs { | 
|  | return { | 
|  | inputText: this.elements.inputText.value, | 
|  | resetAutocompleteController: | 
|  | this.elements.resetAutocompleteController.checked, | 
|  | cursorLock: this.elements.lockCursorPosition.checked, | 
|  | cursorPosition: this.cursorPosition, | 
|  | zeroSuggest: this.elements.zeroSuggest.checked, | 
|  | preventInlineAutocomplete: | 
|  | this.elements.preventInlineAutocomplete.checked, | 
|  | preferKeyword: this.elements.preferKeyword.checked, | 
|  | currentUrl: this.elements.currentUrl.value, | 
|  | pageClassification: Number(this.elements.pageClassification.value), | 
|  | }; | 
|  | } | 
|  |  | 
|  | set queryInputs(queryInputs: QueryInputs) { | 
|  | this.elements.inputText.value = queryInputs.inputText; | 
|  | this.elements.resetAutocompleteController.checked = | 
|  | queryInputs.resetAutocompleteController; | 
|  | this.elements.lockCursorPosition.checked = queryInputs.cursorLock; | 
|  | this.cursorPosition = queryInputs.cursorPosition; | 
|  | this.elements.zeroSuggest.checked = queryInputs.zeroSuggest; | 
|  | this.elements.preventInlineAutocomplete.checked = | 
|  | queryInputs.preventInlineAutocomplete; | 
|  | this.elements.preferKeyword.checked = queryInputs.preferKeyword; | 
|  | this.elements.currentUrl.value = queryInputs.currentUrl; | 
|  | this.elements.pageClassification.value = | 
|  | String(queryInputs.pageClassification); | 
|  | } | 
|  |  | 
|  | private get cursorPosition(): number { | 
|  | return this.elements.lockCursorPosition.checked ? | 
|  | this.elements.inputText.value.length : | 
|  | Number(this.elements.inputText.selectionEnd); | 
|  | } | 
|  |  | 
|  | private set cursorPosition(value: number) { | 
|  | this.elements.inputText.setSelectionRange(value, value); | 
|  | this.positionCursorPositionIndicators(); | 
|  | } | 
|  |  | 
|  | private positionCursorPositionIndicators() { | 
|  | this.elements.arrowPadding.textContent = | 
|  | this.elements.inputText.value.substring(0, this.cursorPosition); | 
|  | } | 
|  |  | 
|  | get connectWindowOmnibox(): boolean { | 
|  | return this.elements.connectWindowOmnibox.checked; | 
|  | } | 
|  |  | 
|  | private onResponseSelectionChanged() { | 
|  | const {value, max} = this.elements.responseSelection; | 
|  | this.elements.historyWarning.hidden = value === '0' || value === max; | 
|  | this.dispatchEvent( | 
|  | new CustomEvent('response-select', {detail: Number(value) - 1})); | 
|  | } | 
|  |  | 
|  | private onResponseSelectionBlur() { | 
|  | const {value, min, max} = this.elements.responseSelection; | 
|  | this.elements.responseSelection.value = | 
|  | String(Math.max(Math.min(Number(value), Number(max)), Number(min))); | 
|  | this.onResponseSelectionChanged(); | 
|  | } | 
|  |  | 
|  | set responsesCount(value: number) { | 
|  | if (this.elements.responseSelection.value === | 
|  | this.elements.responseSelection.max) { | 
|  | this.elements.responseSelection.value = String(value); | 
|  | } | 
|  | this.elements.responseSelection.max = String(value); | 
|  | this.elements.responseSelection.min = String(value ? 1 : 0); | 
|  | this.elements.responsesCount.textContent = String(value); | 
|  | this.onResponseSelectionBlur(); | 
|  | } | 
|  |  | 
|  | private onDisplayInputsChanged() { | 
|  | this.storeInputs(); | 
|  | this.dispatchEvent(new CustomEvent( | 
|  | 'display-inputs-changed', {detail: this.displayInputs})); | 
|  | } | 
|  |  | 
|  | get displayInputs(): DisplayInputs { | 
|  | return { | 
|  | showIncompleteResults: this.elements.showIncompleteResults.checked, | 
|  | showDetails: this.elements.showDetails.checked, | 
|  | showAllProviders: this.elements.showAllProviders.checked, | 
|  | elideCells: this.elements.elideCells.checked, | 
|  | thinRows: this.elements.thinRows.checked, | 
|  | }; | 
|  | } | 
|  |  | 
|  | set displayInputs(displayInputs: DisplayInputs) { | 
|  | this.elements.showIncompleteResults.checked = | 
|  | displayInputs.showIncompleteResults; | 
|  | this.elements.showDetails.checked = displayInputs.showDetails; | 
|  | this.elements.showAllProviders.checked = displayInputs.showAllProviders; | 
|  | this.elements.elideCells.checked = displayInputs.elideCells; | 
|  | this.elements.thinRows.checked = displayInputs.thinRows; | 
|  | } | 
|  |  | 
|  | private onFilterInputsChanged() { | 
|  | this.dispatchEvent(new CustomEvent( | 
|  | 'filter-input-changed', {detail: this.elements.filterText.value})); | 
|  | } | 
|  |  | 
|  | private onExportClipboard() { | 
|  | this.dispatchEvent(new CustomEvent('export-clipboard')); | 
|  | } | 
|  |  | 
|  | private onExportFile() { | 
|  | this.dispatchEvent(new CustomEvent('export-file')); | 
|  | } | 
|  |  | 
|  | private async onImportClipboard() { | 
|  | this.import(await navigator.clipboard.readText()); | 
|  | } | 
|  |  | 
|  | private onImportFile(event: Event) { | 
|  | const file = (event.target as HTMLInputElement).files?.[0]; | 
|  | if (file) { | 
|  | this.importFile(file); | 
|  | } | 
|  | } | 
|  |  | 
|  | private onProcessBatchFile(event: Event) { | 
|  | const file = (event.target as HTMLInputElement).files?.[0]; | 
|  | if (file) { | 
|  | this.processBatchFile(file); | 
|  | } | 
|  | } | 
|  |  | 
|  | private onImportDropped(event: DragEvent) { | 
|  | const data = event.dataTransfer!; | 
|  | const dragText = data.getData('Text'); | 
|  | if (dragText) { | 
|  | this.import(dragText); | 
|  | } else if (data.files[0]) { | 
|  | this.importFile(data.files[0]); | 
|  | } | 
|  | } | 
|  |  | 
|  | private onProcessBatchDropped(event: DragEvent) { | 
|  | const data = event.dataTransfer!; | 
|  | const dragText = data.getData('Text'); | 
|  | if (dragText) { | 
|  | this.processBatch(dragText); | 
|  | } else if (data.files[0]) { | 
|  | this.processBatchFile(data.files[0]); | 
|  | } | 
|  | } | 
|  |  | 
|  | private importFile(file: File) { | 
|  | OmniboxInput.readFile(file).then(this.import.bind(this)); | 
|  | } | 
|  |  | 
|  | private processBatchFile(file: File) { | 
|  | OmniboxInput.readFile(file).then(this.processBatch.bind(this)); | 
|  | } | 
|  |  | 
|  | private import(importString: string) { | 
|  | try { | 
|  | const importData = JSON.parse(importString); | 
|  | // TODO(manukh): If import fails, this UI state change shouldn't happen. | 
|  | this.elements.importedWarning.hidden = false; | 
|  | this.dispatchEvent(new CustomEvent('import', {detail: importData})); | 
|  | } catch (error) { | 
|  | console.error('error during import, invalid json:', error); | 
|  | } | 
|  | } | 
|  |  | 
|  | private processBatch(processBatchString: string) { | 
|  | try { | 
|  | const processBatchData = JSON.parse(processBatchString); | 
|  | this.dispatchEvent( | 
|  | new CustomEvent('process-batch', {detail: processBatchData})); | 
|  | } catch (error) { | 
|  | console.error('error during process batch, invalid json:', error); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static readFile(file: File): Promise<string> { | 
|  | return new Promise(resolve => { | 
|  | const reader = new FileReader(); | 
|  | reader.onloadend = () => { | 
|  | if (reader.readyState === FileReader.DONE) { | 
|  | resolve(reader.result as string); | 
|  | } else { | 
|  | console.error('error importing, unable to read file:', reader.error); | 
|  | } | 
|  | }; | 
|  | reader.readAsText(file); | 
|  | }); | 
|  | } | 
|  |  | 
|  | static get defaultDisplayInputs(): DisplayInputs { | 
|  | return { | 
|  | showIncompleteResults: false, | 
|  | showDetails: false, | 
|  | showAllProviders: true, | 
|  | elideCells: true, | 
|  | thinRows: false, | 
|  | }; | 
|  | } | 
|  | } | 
|  |  | 
|  | customElements.define('omnibox-input', OmniboxInput); |