• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2022 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 * as m from 'mithril';
16import {globals} from './globals';
17
18interface PopupMenuItem {
19  // Display text
20  text: string;
21  // Action on menu item click
22  callback: () => void;
23}
24
25interface PopupMenuButtonAttrs {
26  // Icon for button opening a menu
27  icon: string;
28  // List of popup menu items
29  items: PopupMenuItem[];
30}
31
32// To ensure having at most one popup menu on the screen at a time, we need to
33// listen to click events on the whole page and close currently opened popup, if
34// there's any. This class, used as a singleton, does exactly that.
35class PopupHolder {
36  // Invariant: global listener should be register if and only if this.popup is
37  // not undefined.
38  popup: PopupMenuButton|undefined = undefined;
39  initialized = false;
40  listener: (e: MouseEvent) => void;
41
42  constructor() {
43    this.listener = (e: MouseEvent) => {
44      // Only handle those events that are not part of dropdown menu themselves.
45      const hasDropdown =
46          e.composedPath().find(PopupHolder.isDropdownElement) !== undefined;
47      if (!hasDropdown) {
48        this.ensureHidden();
49      }
50    };
51  }
52
53  static isDropdownElement(target: EventTarget) {
54    if (target instanceof HTMLElement) {
55      return target.tagName === 'DIV' && target.classList.contains('dropdown');
56    }
57    return false;
58  }
59
60  ensureHidden() {
61    if (this.popup !== undefined) {
62      this.popup.setVisible(false);
63    }
64  }
65
66  clear() {
67    if (this.popup !== undefined) {
68      this.popup = undefined;
69      window.removeEventListener('click', this.listener);
70    }
71  }
72
73  showPopup(popup: PopupMenuButton) {
74    this.ensureHidden();
75    this.popup = popup;
76    window.addEventListener('click', this.listener);
77  }
78}
79
80// Singleton instance of PopupHolder
81const popupHolder = new PopupHolder();
82
83// Component that displays a button that shows a popup menu on click.
84export class PopupMenuButton implements m.ClassComponent<PopupMenuButtonAttrs> {
85  popupShown = false;
86
87  setVisible(visible: boolean) {
88    this.popupShown = visible;
89    if (this.popupShown) {
90      popupHolder.showPopup(this);
91    } else {
92      popupHolder.clear();
93    }
94    globals.rafScheduler.scheduleFullRedraw();
95  }
96
97  view(vnode: m.Vnode<PopupMenuButtonAttrs, this>) {
98    return m(
99        '.dropdown',
100        m('i.material-icons',
101          {
102            onclick: () => {
103              this.setVisible(!this.popupShown);
104            }
105          },
106          vnode.attrs.icon),
107        m(this.popupShown ? '.popup-menu.opened' : '.popup-menu.closed',
108          vnode.attrs.items.map(
109              item =>
110                  m('button',
111                    {
112                      onclick: () => {
113                        item.callback();
114                        // Hide the menu item after the action has been invoked
115                        this.setVisible(false);
116                      }
117                    },
118                    item.text))));
119  }
120}
121