xref: /aosp_15_r20/external/perfetto/ui/src/frontend/omnibox.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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