1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
odoo.define('web.DropdownMenu', function (require) {
"use strict";
const DropdownMenuItem = require('web.DropdownMenuItem');
const { Component, hooks } = owl;
const { useExternalListener, useRef, useState } = hooks;
/**
* Dropdown menu
*
* Generic component used to generate a list of interactive items. It uses some
* bootstrap classes but most interactions are handled in here or in the dropdown
* menu item class definition, including some keyboard navigation and escaping
* system (click outside to close the dropdown).
*
* The layout of a dropdown menu is as following:
* > a Button (always rendered) with a `title` and an optional `icon`;
* > a Dropdown (rendered when open) containing a collection of given items.
* These items must be objects and can have two shapes:
* 1. item.Component & item.props > will instantiate the given Component with
* the given props. Any additional key will be useless.
* 2. any other shape > will instantiate a DropdownMenuItem with the item
* object being its props. There is no props validation as this object
* will be passed as-is when `selected` and can contain additional meta-keys
* that will not affect the displayed item. For more information regarding
* the behaviour of these items, @see DropdownMenuItem.
* @extends Component
*/
class DropdownMenu extends Component {
constructor() {
super(...arguments);
this.dropdownMenu = useRef('dropdown');
this.state = useState({ open: false });
useExternalListener(window, 'click', this._onWindowClick, true);
useExternalListener(window, 'keydown', this._onWindowKeydown);
}
//---------------------------------------------------------------------
// Getters
//---------------------------------------------------------------------
/**
* In desktop, by default, we do not display a caret icon next to the
* dropdown.
* @returns {boolean}
*/
get displayCaret() {
return false;
}
/**
* In mobile, by default, we display a chevron icon next to the dropdown
* button. Note that when 'displayCaret' is true, we display a caret
* instead of a chevron, no matter the value of 'displayChevron'.
* @returns {boolean}
*/
get displayChevron() {
return this.env.device.isMobile;
}
/**
* Can be overriden to force an icon on an inheriting class.
* @returns {string} Font Awesome icon class
*/
get icon() {
return this.props.icon;
}
/**
* Meant to be overriden to provide the list of items to display.
* @returns {Object[]}
*/
get items() {
return this.props.items;
}
/**
* @returns {string}
*/
get title() {
return this.props.title;
}
//---------------------------------------------------------------------
// Handlers
//---------------------------------------------------------------------
/**
* @private
* @param {KeyboardEvent} ev
*/
_onButtonKeydown(ev) {
switch (ev.key) {
case 'ArrowLeft':
case 'ArrowRight':
case 'ArrowUp':
case 'ArrowDown':
const firstItem = this.el.querySelector('.dropdown-item');
if (firstItem) {
ev.preventDefault();
firstItem.focus();
}
}
}
/**
* @private
* @param {OwlEvent} ev
*/
_onItemSelected(/* ev */) {
if (this.props.closeOnSelected) {
this.state.open = false;
}
}
/**
* @private
* @param {MouseEvent} ev
*/
_onWindowClick(ev) {
if (
this.state.open &&
!this.el.contains(ev.target) &&
!this.el.contains(document.activeElement)
) {
if (document.body.classList.contains("modal-open")) {
// retrieve the active modal and check if the dropdown is a child of this modal
const modal = document.querySelector('body > .modal:not(.o_inactive_modal)');
if (modal && !modal.contains(this.el)) {
return;
}
const owlModal = document.querySelector('body > .o_dialog > .modal:not(.o_inactive_modal)');
if (owlModal && !owlModal.contains(this.el)) {
return;
}
}
// check for an active open bootstrap calendar like the filter dropdown inside the search panel)
if (document.querySelector('body > .bootstrap-datetimepicker-widget')) {
return;
}
this.state.open = false;
}
}
/**
* @private
* @param {KeyboardEvent} ev
*/
_onWindowKeydown(ev) {
if (this.state.open && ev.key === 'Escape') {
this.state.open = false;
}
}
}
DropdownMenu.components = { DropdownMenuItem };
DropdownMenu.defaultProps = { items: [] };
DropdownMenu.props = {
icon: { type: String, optional: 1 },
items: {
type: Array,
element: Object,
optional: 1,
},
title: { type: String, optional: 1 },
closeOnSelected: { type: Boolean, optional: 1 },
};
DropdownMenu.template = 'web.DropdownMenu';
return DropdownMenu;
});
|