1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15 16import * as m from 'mithril'; 17 18import {Actions} from '../common/actions'; 19 20import {globals} from './globals'; 21import {createPage} from './pages'; 22import {QueryTable} from './query_table'; 23 24const INPUT_PLACEHOLDER = 'Enter query and press Cmd/Ctrl + Enter'; 25const INPUT_MIN_LINES = 2; 26const INPUT_MAX_LINES = 10; 27const INPUT_LINE_HEIGHT_EM = 1.2; 28const TAB_SPACES = 2; 29const QUERY_ID = 'analyze-page-query'; 30 31class QueryInput implements m.ClassComponent { 32 // How many lines to display if the user hasn't resized the input box. 33 displayLines = INPUT_MIN_LINES; 34 35 static onKeyDown(e: Event) { 36 const event = e as KeyboardEvent; 37 const target = e.target as HTMLTextAreaElement; 38 39 if (event.code === 'Enter' && (event.metaKey || event.ctrlKey)) { 40 event.preventDefault(); 41 const query = target.value; 42 if (!query) return; 43 globals.dispatch( 44 Actions.executeQuery({engineId: '0', queryId: QUERY_ID, query})); 45 } 46 47 if (event.code === 'Tab') { 48 // Handle tabs to insert spaces. 49 event.preventDefault(); 50 const whitespace = ' '.repeat(TAB_SPACES); 51 const {selectionStart, selectionEnd} = target; 52 target.value = target.value.substring(0, selectionStart) + whitespace + 53 target.value.substring(selectionEnd); 54 target.selectionEnd = selectionStart + TAB_SPACES; 55 } 56 } 57 58 onInput(textareaValue: string) { 59 const textareaLines = textareaValue.split('\n').length; 60 const clampedNumLines = 61 Math.min(Math.max(textareaLines, INPUT_MIN_LINES), INPUT_MAX_LINES); 62 this.displayLines = clampedNumLines; 63 globals.dispatch(Actions.setAnalyzePageQuery({query: textareaValue})); 64 globals.rafScheduler.scheduleFullRedraw(); 65 } 66 67 // This method exists because unfortunatley setting custom properties on an 68 // element's inline style attribue doesn't seem to work in mithril, even 69 // though the docs claim so. 70 setHeightBeforeResize(node: HTMLElement) { 71 // +2em for some extra breathing space to account for padding. 72 const heightEm = this.displayLines * INPUT_LINE_HEIGHT_EM + 2; 73 // We set a height based on the number of lines that we want to display by 74 // default. If the user resizes the textbox using the resize handle in the 75 // bottom-right corner, this height is overridden. 76 node.style.setProperty('--height-before-resize', `${heightEm}em`); 77 // TODO(dproy): The resized height is lost if user navigates away from the 78 // page and comes back. 79 } 80 81 oncreate(vnode: m.VnodeDOM) { 82 // This makes sure query persists if user navigates to other pages and comes 83 // back to analyze page. 84 const existingQuery = globals.state.analyzePageQuery; 85 const textarea = vnode.dom as HTMLTextAreaElement; 86 if (existingQuery) { 87 textarea.value = existingQuery; 88 this.onInput(existingQuery); 89 } 90 91 this.setHeightBeforeResize(textarea); 92 } 93 94 onupdate(vnode: m.VnodeDOM) { 95 this.setHeightBeforeResize(vnode.dom as HTMLElement); 96 } 97 98 view() { 99 return m('textarea.query-input', { 100 placeholder: INPUT_PLACEHOLDER, 101 onkeydown: (e: Event) => QueryInput.onKeyDown(e), 102 oninput: (e: Event) => 103 this.onInput((e.target as HTMLTextAreaElement).value), 104 }); 105 } 106} 107 108 109export const AnalyzePage = createPage({ 110 view() { 111 return m( 112 '.analyze-page', 113 m(QueryInput), 114 m(QueryTable, {queryId: QUERY_ID}), 115 ); 116 } 117}); 118