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