1// Copyright (C) 2024 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 15import m from 'mithril'; 16import {raf} from '../../../../core/raf_scheduler'; 17import {Spinner} from '../../../../widgets/spinner'; 18import { 19 TableColumn, 20 tableColumnId, 21 TableColumnSet, 22 TableManager, 23} from './column'; 24import {TextInput} from '../../../../widgets/text_input'; 25import {scheduleFullRedraw} from '../../../../widgets/raf'; 26import {hasModKey, modKey} from '../../../../base/hotkeys'; 27import {MenuItem} from '../../../../widgets/menu'; 28import {uuidv4} from '../../../../base/uuid'; 29 30const MAX_ARGS_TO_DISPLAY = 15; 31 32interface ArgumentSelectorAttrs { 33 tableManager: TableManager; 34 columnSet: TableColumnSet; 35 alreadySelectedColumnIds: Set<string>; 36 onArgumentSelected: (column: TableColumn) => void; 37} 38 39// This class is responsible for rendering a menu which allows user to select which column out of ColumnSet to add. 40export class ArgumentSelector 41 implements m.ClassComponent<ArgumentSelectorAttrs> 42{ 43 searchText = ''; 44 columns?: {key: string; column: TableColumn | TableColumnSet}[]; 45 46 constructor({attrs}: m.Vnode<ArgumentSelectorAttrs>) { 47 this.load(attrs); 48 } 49 50 private async load(attrs: ArgumentSelectorAttrs) { 51 this.columns = await attrs.columnSet.discover(attrs.tableManager); 52 raf.scheduleFullRedraw(); 53 } 54 55 view({attrs}: m.Vnode<ArgumentSelectorAttrs>) { 56 const columns = this.columns; 57 if (columns === undefined) return m(Spinner); 58 59 // Candidates are the columns which have not been selected yet. 60 const candidates = columns.filter( 61 ({column}) => 62 column instanceof TableColumnSet || 63 !attrs.alreadySelectedColumnIds.has(tableColumnId(column)), 64 ); 65 66 // Filter the candidates based on the search text. 67 const filtered = candidates.filter(({key}) => { 68 return key.toLowerCase().includes(this.searchText.toLowerCase()); 69 }); 70 71 const displayed = filtered.slice(0, MAX_ARGS_TO_DISPLAY); 72 73 const extraItems = Math.max(0, filtered.length - MAX_ARGS_TO_DISPLAY); 74 75 const firstButtonUuid = uuidv4(); 76 77 return [ 78 m( 79 '.pf-search-bar', 80 m(TextInput, { 81 autofocus: true, 82 oninput: (event: Event) => { 83 const eventTarget = event.target as HTMLTextAreaElement; 84 this.searchText = eventTarget.value; 85 scheduleFullRedraw(); 86 }, 87 onkeydown: (event: KeyboardEvent) => { 88 if (filtered.length === 0) return; 89 if (event.key === 'Enter') { 90 // If there is only one item or Mod-Enter was pressed, select the first element. 91 if (filtered.length === 1 || hasModKey(event)) { 92 const params = {bubbles: true}; 93 if (hasModKey(event)) { 94 Object.assign(params, modKey()); 95 } 96 const pointerEvent = new PointerEvent('click', params); 97 ( 98 document.getElementById(firstButtonUuid) as HTMLElement | null 99 )?.dispatchEvent(pointerEvent); 100 } 101 } 102 }, 103 value: this.searchText, 104 placeholder: 'Filter...', 105 className: 'pf-search-box', 106 }), 107 ), 108 ...displayed.map(({key, column}, index) => 109 m( 110 MenuItem, 111 { 112 id: index === 0 ? firstButtonUuid : undefined, 113 label: key, 114 onclick: (event) => { 115 if (column instanceof TableColumnSet) return; 116 attrs.onArgumentSelected(column); 117 // For Control-Click, we don't want to close the menu to allow the user 118 // to select multiple items in one go. 119 if (hasModKey(event)) { 120 event.stopPropagation(); 121 } 122 // Otherwise this popup will be closed. 123 }, 124 }, 125 column instanceof TableColumnSet && 126 m(ArgumentSelector, { 127 columnSet: column, 128 alreadySelectedColumnIds: attrs.alreadySelectedColumnIds, 129 onArgumentSelected: attrs.onArgumentSelected, 130 tableManager: attrs.tableManager, 131 }), 132 ), 133 ), 134 Boolean(extraItems) && m('i', `+${extraItems} more`), 135 ]; 136 } 137} 138