1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {classNames} from '../base/classnames'; 17*6dbdd20aSAndroid Build Coastguard Workerimport {FuzzySegment} from '../base/fuzzy'; 18*6dbdd20aSAndroid Build Coastguard Workerimport {isString} from '../base/object_utils'; 19*6dbdd20aSAndroid Build Coastguard Workerimport {exists} from '../base/utils'; 20*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler'; 21*6dbdd20aSAndroid Build Coastguard Workerimport {EmptyState} from '../widgets/empty_state'; 22*6dbdd20aSAndroid Build Coastguard Workerimport {KeycapGlyph} from '../widgets/hotkey_glyphs'; 23*6dbdd20aSAndroid Build Coastguard Workerimport {Popup} from '../widgets/popup'; 24*6dbdd20aSAndroid Build Coastguard Worker 25*6dbdd20aSAndroid Build Coastguard Workerinterface OmniboxOptionRowAttrs { 26*6dbdd20aSAndroid Build Coastguard Worker // Human readable display name for the option. 27*6dbdd20aSAndroid Build Coastguard Worker // This can either be a simple string, or a list of fuzzy segments in which 28*6dbdd20aSAndroid Build Coastguard Worker // case highlighting will be applied to the matching segments. 29*6dbdd20aSAndroid Build Coastguard Worker displayName: FuzzySegment[] | string; 30*6dbdd20aSAndroid Build Coastguard Worker 31*6dbdd20aSAndroid Build Coastguard Worker // Highlight this option. 32*6dbdd20aSAndroid Build Coastguard Worker highlighted: boolean; 33*6dbdd20aSAndroid Build Coastguard Worker 34*6dbdd20aSAndroid Build Coastguard Worker // Arbitrary components to put on the right hand side of the option. 35*6dbdd20aSAndroid Build Coastguard Worker rightContent?: m.Children; 36*6dbdd20aSAndroid Build Coastguard Worker 37*6dbdd20aSAndroid Build Coastguard Worker // Some tag to place on the right (to the left of the right content). 38*6dbdd20aSAndroid Build Coastguard Worker label?: string; 39*6dbdd20aSAndroid Build Coastguard Worker 40*6dbdd20aSAndroid Build Coastguard Worker // Additional attrs forwarded to the underlying element. 41*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 42*6dbdd20aSAndroid Build Coastguard Worker [htmlAttrs: string]: any; 43*6dbdd20aSAndroid Build Coastguard Worker} 44*6dbdd20aSAndroid Build Coastguard Worker 45*6dbdd20aSAndroid Build Coastguard Workerclass OmniboxOptionRow implements m.ClassComponent<OmniboxOptionRowAttrs> { 46*6dbdd20aSAndroid Build Coastguard Worker private highlightedBefore = false; 47*6dbdd20aSAndroid Build Coastguard Worker 48*6dbdd20aSAndroid Build Coastguard Worker view({attrs}: m.Vnode<OmniboxOptionRowAttrs>): void | m.Children { 49*6dbdd20aSAndroid Build Coastguard Worker const {displayName, highlighted, rightContent, label, ...htmlAttrs} = attrs; 50*6dbdd20aSAndroid Build Coastguard Worker return m( 51*6dbdd20aSAndroid Build Coastguard Worker 'li', 52*6dbdd20aSAndroid Build Coastguard Worker { 53*6dbdd20aSAndroid Build Coastguard Worker class: classNames(highlighted && 'pf-highlighted'), 54*6dbdd20aSAndroid Build Coastguard Worker ...htmlAttrs, 55*6dbdd20aSAndroid Build Coastguard Worker }, 56*6dbdd20aSAndroid Build Coastguard Worker m('span.pf-title', this.renderTitle(displayName)), 57*6dbdd20aSAndroid Build Coastguard Worker label && m('span.pf-tag', label), 58*6dbdd20aSAndroid Build Coastguard Worker rightContent, 59*6dbdd20aSAndroid Build Coastguard Worker ); 60*6dbdd20aSAndroid Build Coastguard Worker } 61*6dbdd20aSAndroid Build Coastguard Worker 62*6dbdd20aSAndroid Build Coastguard Worker private renderTitle(title: FuzzySegment[] | string): m.Children { 63*6dbdd20aSAndroid Build Coastguard Worker if (isString(title)) { 64*6dbdd20aSAndroid Build Coastguard Worker return title; 65*6dbdd20aSAndroid Build Coastguard Worker } else { 66*6dbdd20aSAndroid Build Coastguard Worker return title.map(({matching, value}) => { 67*6dbdd20aSAndroid Build Coastguard Worker return matching ? m('b', value) : value; 68*6dbdd20aSAndroid Build Coastguard Worker }); 69*6dbdd20aSAndroid Build Coastguard Worker } 70*6dbdd20aSAndroid Build Coastguard Worker } 71*6dbdd20aSAndroid Build Coastguard Worker 72*6dbdd20aSAndroid Build Coastguard Worker onupdate({attrs, dom}: m.VnodeDOM<OmniboxOptionRowAttrs, this>) { 73*6dbdd20aSAndroid Build Coastguard Worker if (this.highlightedBefore !== attrs.highlighted) { 74*6dbdd20aSAndroid Build Coastguard Worker if (attrs.highlighted) { 75*6dbdd20aSAndroid Build Coastguard Worker dom.scrollIntoView({block: 'nearest'}); 76*6dbdd20aSAndroid Build Coastguard Worker } 77*6dbdd20aSAndroid Build Coastguard Worker this.highlightedBefore = attrs.highlighted; 78*6dbdd20aSAndroid Build Coastguard Worker } 79*6dbdd20aSAndroid Build Coastguard Worker } 80*6dbdd20aSAndroid Build Coastguard Worker} 81*6dbdd20aSAndroid Build Coastguard Worker 82*6dbdd20aSAndroid Build Coastguard Worker// Omnibox option. 83*6dbdd20aSAndroid Build Coastguard Workerexport interface OmniboxOption { 84*6dbdd20aSAndroid Build Coastguard Worker // The value to place into the omnibox. This is what's returned in onSubmit. 85*6dbdd20aSAndroid Build Coastguard Worker key: string; 86*6dbdd20aSAndroid Build Coastguard Worker 87*6dbdd20aSAndroid Build Coastguard Worker // Display name provided as a string or a list of fuzzy segments to enable 88*6dbdd20aSAndroid Build Coastguard Worker // fuzzy match highlighting. 89*6dbdd20aSAndroid Build Coastguard Worker displayName: FuzzySegment[] | string; 90*6dbdd20aSAndroid Build Coastguard Worker 91*6dbdd20aSAndroid Build Coastguard Worker // Some tag to place on the right (to the left of the right content). 92*6dbdd20aSAndroid Build Coastguard Worker tag?: string; 93*6dbdd20aSAndroid Build Coastguard Worker 94*6dbdd20aSAndroid Build Coastguard Worker // Arbitrary components to put on the right hand side of the option. 95*6dbdd20aSAndroid Build Coastguard Worker rightContent?: m.Children; 96*6dbdd20aSAndroid Build Coastguard Worker} 97*6dbdd20aSAndroid Build Coastguard Worker 98*6dbdd20aSAndroid Build Coastguard Workerexport interface OmniboxAttrs { 99*6dbdd20aSAndroid Build Coastguard Worker // Current value of the omnibox input. 100*6dbdd20aSAndroid Build Coastguard Worker value: string; 101*6dbdd20aSAndroid Build Coastguard Worker 102*6dbdd20aSAndroid Build Coastguard Worker // What to show when value is blank. 103*6dbdd20aSAndroid Build Coastguard Worker placeholder?: string; 104*6dbdd20aSAndroid Build Coastguard Worker 105*6dbdd20aSAndroid Build Coastguard Worker // Called when the text changes. 106*6dbdd20aSAndroid Build Coastguard Worker onInput?: (value: string, previousValue: string) => void; 107*6dbdd20aSAndroid Build Coastguard Worker 108*6dbdd20aSAndroid Build Coastguard Worker // Class or list of classes to append to the Omnibox element. 109*6dbdd20aSAndroid Build Coastguard Worker extraClasses?: string; 110*6dbdd20aSAndroid Build Coastguard Worker 111*6dbdd20aSAndroid Build Coastguard Worker // Called on close. 112*6dbdd20aSAndroid Build Coastguard Worker onClose?: () => void; 113*6dbdd20aSAndroid Build Coastguard Worker 114*6dbdd20aSAndroid Build Coastguard Worker // Dropdown items to show. If none are supplied, the omnibox runs in free text 115*6dbdd20aSAndroid Build Coastguard Worker // mode, where anyt text can be input. Otherwise, onSubmit will always be 116*6dbdd20aSAndroid Build Coastguard Worker // called with one of the options. 117*6dbdd20aSAndroid Build Coastguard Worker // Options are provided in groups called categories. If the category has a 118*6dbdd20aSAndroid Build Coastguard Worker // name the name will be listed at the top of the group rendered with a little 119*6dbdd20aSAndroid Build Coastguard Worker // divider as well. 120*6dbdd20aSAndroid Build Coastguard Worker options?: OmniboxOption[]; 121*6dbdd20aSAndroid Build Coastguard Worker 122*6dbdd20aSAndroid Build Coastguard Worker // Called when the user expresses the intent to "execute" the thing. 123*6dbdd20aSAndroid Build Coastguard Worker onSubmit?: (value: string, mod: boolean, shift: boolean) => void; 124*6dbdd20aSAndroid Build Coastguard Worker 125*6dbdd20aSAndroid Build Coastguard Worker // Called when the user hits backspace when the field is empty. 126*6dbdd20aSAndroid Build Coastguard Worker onGoBack?: () => void; 127*6dbdd20aSAndroid Build Coastguard Worker 128*6dbdd20aSAndroid Build Coastguard Worker // When true, disable and grey-out the omnibox's input. 129*6dbdd20aSAndroid Build Coastguard Worker readonly?: boolean; 130*6dbdd20aSAndroid Build Coastguard Worker 131*6dbdd20aSAndroid Build Coastguard Worker // Ref to use on the input - useful for extracing this element from the DOM. 132*6dbdd20aSAndroid Build Coastguard Worker inputRef?: string; 133*6dbdd20aSAndroid Build Coastguard Worker 134*6dbdd20aSAndroid Build Coastguard Worker // Whether to close when the user presses Enter. Default = false. 135*6dbdd20aSAndroid Build Coastguard Worker closeOnSubmit?: boolean; 136*6dbdd20aSAndroid Build Coastguard Worker 137*6dbdd20aSAndroid Build Coastguard Worker // Whether to close the omnibox (i.e. call the |onClose| handler) when we 138*6dbdd20aSAndroid Build Coastguard Worker // click outside the omnibox or its dropdown. Default = false. 139*6dbdd20aSAndroid Build Coastguard Worker closeOnOutsideClick?: boolean; 140*6dbdd20aSAndroid Build Coastguard Worker 141*6dbdd20aSAndroid Build Coastguard Worker // Some content to place into the right hand side of the after the input. 142*6dbdd20aSAndroid Build Coastguard Worker rightContent?: m.Children; 143*6dbdd20aSAndroid Build Coastguard Worker 144*6dbdd20aSAndroid Build Coastguard Worker // If we have options, this value indicates the index of the option which 145*6dbdd20aSAndroid Build Coastguard Worker // is currently highlighted. 146*6dbdd20aSAndroid Build Coastguard Worker selectedOptionIndex?: number; 147*6dbdd20aSAndroid Build Coastguard Worker 148*6dbdd20aSAndroid Build Coastguard Worker // Callback for when the user pressed up/down, expressing a desire to change 149*6dbdd20aSAndroid Build Coastguard Worker // the |selectedOptionIndex|. 150*6dbdd20aSAndroid Build Coastguard Worker onSelectedOptionChanged?: (index: number) => void; 151*6dbdd20aSAndroid Build Coastguard Worker} 152*6dbdd20aSAndroid Build Coastguard Worker 153*6dbdd20aSAndroid Build Coastguard Workerexport class Omnibox implements m.ClassComponent<OmniboxAttrs> { 154*6dbdd20aSAndroid Build Coastguard Worker private popupElement?: HTMLElement; 155*6dbdd20aSAndroid Build Coastguard Worker private dom?: Element; 156*6dbdd20aSAndroid Build Coastguard Worker private attrs?: OmniboxAttrs; 157*6dbdd20aSAndroid Build Coastguard Worker 158*6dbdd20aSAndroid Build Coastguard Worker view({attrs}: m.Vnode<OmniboxAttrs>): m.Children { 159*6dbdd20aSAndroid Build Coastguard Worker const { 160*6dbdd20aSAndroid Build Coastguard Worker value, 161*6dbdd20aSAndroid Build Coastguard Worker placeholder, 162*6dbdd20aSAndroid Build Coastguard Worker extraClasses, 163*6dbdd20aSAndroid Build Coastguard Worker onInput = () => {}, 164*6dbdd20aSAndroid Build Coastguard Worker onSubmit = () => {}, 165*6dbdd20aSAndroid Build Coastguard Worker onGoBack = () => {}, 166*6dbdd20aSAndroid Build Coastguard Worker inputRef = 'omnibox', 167*6dbdd20aSAndroid Build Coastguard Worker options, 168*6dbdd20aSAndroid Build Coastguard Worker closeOnSubmit = false, 169*6dbdd20aSAndroid Build Coastguard Worker rightContent, 170*6dbdd20aSAndroid Build Coastguard Worker selectedOptionIndex = 0, 171*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 172*6dbdd20aSAndroid Build Coastguard Worker 173*6dbdd20aSAndroid Build Coastguard Worker return m( 174*6dbdd20aSAndroid Build Coastguard Worker Popup, 175*6dbdd20aSAndroid Build Coastguard Worker { 176*6dbdd20aSAndroid Build Coastguard Worker onPopupMount: (dom: HTMLElement) => (this.popupElement = dom), 177*6dbdd20aSAndroid Build Coastguard Worker onPopupUnMount: (_dom: HTMLElement) => (this.popupElement = undefined), 178*6dbdd20aSAndroid Build Coastguard Worker isOpen: exists(options), 179*6dbdd20aSAndroid Build Coastguard Worker showArrow: false, 180*6dbdd20aSAndroid Build Coastguard Worker matchWidth: true, 181*6dbdd20aSAndroid Build Coastguard Worker offset: 2, 182*6dbdd20aSAndroid Build Coastguard Worker trigger: m( 183*6dbdd20aSAndroid Build Coastguard Worker '.omnibox', 184*6dbdd20aSAndroid Build Coastguard Worker { 185*6dbdd20aSAndroid Build Coastguard Worker class: extraClasses, 186*6dbdd20aSAndroid Build Coastguard Worker }, 187*6dbdd20aSAndroid Build Coastguard Worker m('input', { 188*6dbdd20aSAndroid Build Coastguard Worker ref: inputRef, 189*6dbdd20aSAndroid Build Coastguard Worker value, 190*6dbdd20aSAndroid Build Coastguard Worker placeholder, 191*6dbdd20aSAndroid Build Coastguard Worker oninput: (e: Event) => { 192*6dbdd20aSAndroid Build Coastguard Worker onInput((e.target as HTMLInputElement).value, value); 193*6dbdd20aSAndroid Build Coastguard Worker }, 194*6dbdd20aSAndroid Build Coastguard Worker onkeydown: (e: KeyboardEvent) => { 195*6dbdd20aSAndroid Build Coastguard Worker if (e.key === 'Backspace' && value === '') { 196*6dbdd20aSAndroid Build Coastguard Worker onGoBack(); 197*6dbdd20aSAndroid Build Coastguard Worker } else if (e.key === 'Escape') { 198*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 199*6dbdd20aSAndroid Build Coastguard Worker this.close(attrs); 200*6dbdd20aSAndroid Build Coastguard Worker } 201*6dbdd20aSAndroid Build Coastguard Worker 202*6dbdd20aSAndroid Build Coastguard Worker if (options) { 203*6dbdd20aSAndroid Build Coastguard Worker if (e.key === 'ArrowUp') { 204*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 205*6dbdd20aSAndroid Build Coastguard Worker this.highlightPreviousOption(attrs); 206*6dbdd20aSAndroid Build Coastguard Worker } else if (e.key === 'ArrowDown') { 207*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 208*6dbdd20aSAndroid Build Coastguard Worker this.highlightNextOption(attrs); 209*6dbdd20aSAndroid Build Coastguard Worker } else if (e.key === 'Enter') { 210*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 211*6dbdd20aSAndroid Build Coastguard Worker 212*6dbdd20aSAndroid Build Coastguard Worker const option = options[selectedOptionIndex]; 213*6dbdd20aSAndroid Build Coastguard Worker // Return values from indexing arrays can be undefined. 214*6dbdd20aSAndroid Build Coastguard Worker // We should enable noUncheckedIndexedAccess in 215*6dbdd20aSAndroid Build Coastguard Worker // tsconfig.json. 216*6dbdd20aSAndroid Build Coastguard Worker /* eslint-disable 217*6dbdd20aSAndroid Build Coastguard Worker @typescript-eslint/strict-boolean-expressions */ 218*6dbdd20aSAndroid Build Coastguard Worker if (option) { 219*6dbdd20aSAndroid Build Coastguard Worker /* eslint-enable */ 220*6dbdd20aSAndroid Build Coastguard Worker closeOnSubmit && this.close(attrs); 221*6dbdd20aSAndroid Build Coastguard Worker 222*6dbdd20aSAndroid Build Coastguard Worker const mod = e.metaKey || e.ctrlKey; 223*6dbdd20aSAndroid Build Coastguard Worker const shift = e.shiftKey; 224*6dbdd20aSAndroid Build Coastguard Worker onSubmit(option.key, mod, shift); 225*6dbdd20aSAndroid Build Coastguard Worker } 226*6dbdd20aSAndroid Build Coastguard Worker } 227*6dbdd20aSAndroid Build Coastguard Worker } else { 228*6dbdd20aSAndroid Build Coastguard Worker if (e.key === 'Enter') { 229*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 230*6dbdd20aSAndroid Build Coastguard Worker 231*6dbdd20aSAndroid Build Coastguard Worker closeOnSubmit && this.close(attrs); 232*6dbdd20aSAndroid Build Coastguard Worker 233*6dbdd20aSAndroid Build Coastguard Worker const mod = e.metaKey || e.ctrlKey; 234*6dbdd20aSAndroid Build Coastguard Worker const shift = e.shiftKey; 235*6dbdd20aSAndroid Build Coastguard Worker onSubmit(value, mod, shift); 236*6dbdd20aSAndroid Build Coastguard Worker } 237*6dbdd20aSAndroid Build Coastguard Worker } 238*6dbdd20aSAndroid Build Coastguard Worker }, 239*6dbdd20aSAndroid Build Coastguard Worker }), 240*6dbdd20aSAndroid Build Coastguard Worker rightContent, 241*6dbdd20aSAndroid Build Coastguard Worker ), 242*6dbdd20aSAndroid Build Coastguard Worker }, 243*6dbdd20aSAndroid Build Coastguard Worker options && this.renderDropdown(attrs), 244*6dbdd20aSAndroid Build Coastguard Worker ); 245*6dbdd20aSAndroid Build Coastguard Worker } 246*6dbdd20aSAndroid Build Coastguard Worker 247*6dbdd20aSAndroid Build Coastguard Worker private renderDropdown(attrs: OmniboxAttrs): m.Children { 248*6dbdd20aSAndroid Build Coastguard Worker const {options} = attrs; 249*6dbdd20aSAndroid Build Coastguard Worker 250*6dbdd20aSAndroid Build Coastguard Worker if (!options) return null; 251*6dbdd20aSAndroid Build Coastguard Worker 252*6dbdd20aSAndroid Build Coastguard Worker if (options.length === 0) { 253*6dbdd20aSAndroid Build Coastguard Worker return m(EmptyState, {title: 'No matching options...'}); 254*6dbdd20aSAndroid Build Coastguard Worker } else { 255*6dbdd20aSAndroid Build Coastguard Worker return m( 256*6dbdd20aSAndroid Build Coastguard Worker '.pf-omnibox-dropdown', 257*6dbdd20aSAndroid Build Coastguard Worker this.renderOptionsContainer(attrs, options), 258*6dbdd20aSAndroid Build Coastguard Worker this.renderFooter(), 259*6dbdd20aSAndroid Build Coastguard Worker ); 260*6dbdd20aSAndroid Build Coastguard Worker } 261*6dbdd20aSAndroid Build Coastguard Worker } 262*6dbdd20aSAndroid Build Coastguard Worker 263*6dbdd20aSAndroid Build Coastguard Worker private renderFooter() { 264*6dbdd20aSAndroid Build Coastguard Worker return m( 265*6dbdd20aSAndroid Build Coastguard Worker '.pf-omnibox-dropdown-footer', 266*6dbdd20aSAndroid Build Coastguard Worker m( 267*6dbdd20aSAndroid Build Coastguard Worker 'section', 268*6dbdd20aSAndroid Build Coastguard Worker m(KeycapGlyph, {keyValue: 'ArrowUp'}), 269*6dbdd20aSAndroid Build Coastguard Worker m(KeycapGlyph, {keyValue: 'ArrowDown'}), 270*6dbdd20aSAndroid Build Coastguard Worker 'to navigate', 271*6dbdd20aSAndroid Build Coastguard Worker ), 272*6dbdd20aSAndroid Build Coastguard Worker m('section', m(KeycapGlyph, {keyValue: 'Enter'}), 'to use'), 273*6dbdd20aSAndroid Build Coastguard Worker m('section', m(KeycapGlyph, {keyValue: 'Escape'}), 'to dismiss'), 274*6dbdd20aSAndroid Build Coastguard Worker ); 275*6dbdd20aSAndroid Build Coastguard Worker } 276*6dbdd20aSAndroid Build Coastguard Worker 277*6dbdd20aSAndroid Build Coastguard Worker private renderOptionsContainer( 278*6dbdd20aSAndroid Build Coastguard Worker attrs: OmniboxAttrs, 279*6dbdd20aSAndroid Build Coastguard Worker options: OmniboxOption[], 280*6dbdd20aSAndroid Build Coastguard Worker ): m.Children { 281*6dbdd20aSAndroid Build Coastguard Worker const { 282*6dbdd20aSAndroid Build Coastguard Worker onClose = () => {}, 283*6dbdd20aSAndroid Build Coastguard Worker onSubmit = () => {}, 284*6dbdd20aSAndroid Build Coastguard Worker closeOnSubmit = false, 285*6dbdd20aSAndroid Build Coastguard Worker selectedOptionIndex, 286*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 287*6dbdd20aSAndroid Build Coastguard Worker 288*6dbdd20aSAndroid Build Coastguard Worker const opts = options.map(({displayName, key, rightContent, tag}, index) => { 289*6dbdd20aSAndroid Build Coastguard Worker return m(OmniboxOptionRow, { 290*6dbdd20aSAndroid Build Coastguard Worker key, 291*6dbdd20aSAndroid Build Coastguard Worker label: tag, 292*6dbdd20aSAndroid Build Coastguard Worker displayName: displayName, 293*6dbdd20aSAndroid Build Coastguard Worker highlighted: index === selectedOptionIndex, 294*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 295*6dbdd20aSAndroid Build Coastguard Worker closeOnSubmit && onClose(); 296*6dbdd20aSAndroid Build Coastguard Worker onSubmit(key, false, false); 297*6dbdd20aSAndroid Build Coastguard Worker }, 298*6dbdd20aSAndroid Build Coastguard Worker rightContent, 299*6dbdd20aSAndroid Build Coastguard Worker }); 300*6dbdd20aSAndroid Build Coastguard Worker }); 301*6dbdd20aSAndroid Build Coastguard Worker 302*6dbdd20aSAndroid Build Coastguard Worker return m('ul.pf-omnibox-options-container', opts); 303*6dbdd20aSAndroid Build Coastguard Worker } 304*6dbdd20aSAndroid Build Coastguard Worker 305*6dbdd20aSAndroid Build Coastguard Worker oncreate({attrs, dom}: m.VnodeDOM<OmniboxAttrs, this>) { 306*6dbdd20aSAndroid Build Coastguard Worker this.attrs = attrs; 307*6dbdd20aSAndroid Build Coastguard Worker this.dom = dom; 308*6dbdd20aSAndroid Build Coastguard Worker const {closeOnOutsideClick} = attrs; 309*6dbdd20aSAndroid Build Coastguard Worker if (closeOnOutsideClick) { 310*6dbdd20aSAndroid Build Coastguard Worker document.addEventListener('mousedown', this.onMouseDown); 311*6dbdd20aSAndroid Build Coastguard Worker } 312*6dbdd20aSAndroid Build Coastguard Worker } 313*6dbdd20aSAndroid Build Coastguard Worker 314*6dbdd20aSAndroid Build Coastguard Worker onupdate({attrs, dom}: m.VnodeDOM<OmniboxAttrs, this>) { 315*6dbdd20aSAndroid Build Coastguard Worker this.attrs = attrs; 316*6dbdd20aSAndroid Build Coastguard Worker this.dom = dom; 317*6dbdd20aSAndroid Build Coastguard Worker const {closeOnOutsideClick} = attrs; 318*6dbdd20aSAndroid Build Coastguard Worker if (closeOnOutsideClick) { 319*6dbdd20aSAndroid Build Coastguard Worker document.addEventListener('mousedown', this.onMouseDown); 320*6dbdd20aSAndroid Build Coastguard Worker } else { 321*6dbdd20aSAndroid Build Coastguard Worker document.removeEventListener('mousedown', this.onMouseDown); 322*6dbdd20aSAndroid Build Coastguard Worker } 323*6dbdd20aSAndroid Build Coastguard Worker } 324*6dbdd20aSAndroid Build Coastguard Worker 325*6dbdd20aSAndroid Build Coastguard Worker onremove(_: m.VnodeDOM<OmniboxAttrs, this>) { 326*6dbdd20aSAndroid Build Coastguard Worker this.attrs = undefined; 327*6dbdd20aSAndroid Build Coastguard Worker this.dom = undefined; 328*6dbdd20aSAndroid Build Coastguard Worker document.removeEventListener('mousedown', this.onMouseDown); 329*6dbdd20aSAndroid Build Coastguard Worker } 330*6dbdd20aSAndroid Build Coastguard Worker 331*6dbdd20aSAndroid Build Coastguard Worker // This is defined as an arrow function to have a single handler that can be 332*6dbdd20aSAndroid Build Coastguard Worker // added/remove while keeping `this` bound. 333*6dbdd20aSAndroid Build Coastguard Worker private onMouseDown = (e: Event) => { 334*6dbdd20aSAndroid Build Coastguard Worker // We need to schedule a redraw manually as this event handler was added 335*6dbdd20aSAndroid Build Coastguard Worker // manually to the DOM and doesn't use Mithril's auto-redraw system. 336*6dbdd20aSAndroid Build Coastguard Worker raf.scheduleFullRedraw('force'); 337*6dbdd20aSAndroid Build Coastguard Worker 338*6dbdd20aSAndroid Build Coastguard Worker // Don't close if the click was within ourselves or our popup. 339*6dbdd20aSAndroid Build Coastguard Worker if (e.target instanceof Node) { 340*6dbdd20aSAndroid Build Coastguard Worker if (this.popupElement && this.popupElement.contains(e.target)) { 341*6dbdd20aSAndroid Build Coastguard Worker return; 342*6dbdd20aSAndroid Build Coastguard Worker } 343*6dbdd20aSAndroid Build Coastguard Worker if (this.dom && this.dom.contains(e.target)) return; 344*6dbdd20aSAndroid Build Coastguard Worker } 345*6dbdd20aSAndroid Build Coastguard Worker if (this.attrs) { 346*6dbdd20aSAndroid Build Coastguard Worker this.close(this.attrs); 347*6dbdd20aSAndroid Build Coastguard Worker } 348*6dbdd20aSAndroid Build Coastguard Worker }; 349*6dbdd20aSAndroid Build Coastguard Worker 350*6dbdd20aSAndroid Build Coastguard Worker private close(attrs: OmniboxAttrs): void { 351*6dbdd20aSAndroid Build Coastguard Worker const {onClose = () => {}} = attrs; 352*6dbdd20aSAndroid Build Coastguard Worker raf.scheduleFullRedraw(); 353*6dbdd20aSAndroid Build Coastguard Worker onClose(); 354*6dbdd20aSAndroid Build Coastguard Worker } 355*6dbdd20aSAndroid Build Coastguard Worker 356*6dbdd20aSAndroid Build Coastguard Worker private highlightPreviousOption(attrs: OmniboxAttrs) { 357*6dbdd20aSAndroid Build Coastguard Worker const {selectedOptionIndex = 0, onSelectedOptionChanged = () => {}} = attrs; 358*6dbdd20aSAndroid Build Coastguard Worker 359*6dbdd20aSAndroid Build Coastguard Worker onSelectedOptionChanged(Math.max(0, selectedOptionIndex - 1)); 360*6dbdd20aSAndroid Build Coastguard Worker } 361*6dbdd20aSAndroid Build Coastguard Worker 362*6dbdd20aSAndroid Build Coastguard Worker private highlightNextOption(attrs: OmniboxAttrs) { 363*6dbdd20aSAndroid Build Coastguard Worker const { 364*6dbdd20aSAndroid Build Coastguard Worker selectedOptionIndex = 0, 365*6dbdd20aSAndroid Build Coastguard Worker onSelectedOptionChanged = () => {}, 366*6dbdd20aSAndroid Build Coastguard Worker options = [], 367*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 368*6dbdd20aSAndroid Build Coastguard Worker 369*6dbdd20aSAndroid Build Coastguard Worker const max = options.length - 1; 370*6dbdd20aSAndroid Build Coastguard Worker onSelectedOptionChanged(Math.min(max, selectedOptionIndex + 1)); 371*6dbdd20aSAndroid Build Coastguard Worker } 372*6dbdd20aSAndroid Build Coastguard Worker} 373