Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js
12241 views
1
/**
2
* @provides phuix-dropdown-menu
3
* @requires javelin-install
4
* javelin-util
5
* javelin-dom
6
* javelin-vector
7
* javelin-stratcom
8
* @javelin
9
*/
10
11
12
/**
13
* Basic interaction for a dropdown menu.
14
*
15
* The menu is unaware of the content inside it, so it can not close itself
16
* when an item is selected. Callers must make a call to @{method:close} after
17
* an item is chosen in order to close the menu.
18
*/
19
JX.install('PHUIXDropdownMenu', {
20
21
construct : function(node) {
22
this._node = node;
23
24
if (node) {
25
JX.DOM.listen(
26
this._node,
27
'click',
28
null,
29
JX.bind(this, this._onclick));
30
}
31
32
JX.Stratcom.listen(
33
'mousedown',
34
null,
35
JX.bind(this, this._onanyclick));
36
37
JX.Stratcom.listen(
38
'resize',
39
null,
40
JX.bind(this, this._adjustposition));
41
42
JX.Stratcom.listen('phuix.dropdown.open', null, JX.bind(this, this.close));
43
44
JX.Stratcom.listen('keydown', null, JX.bind(this, this._onkey));
45
46
JX.DOM.listen(
47
this._getMenuNode(),
48
'click',
49
'tag:a',
50
JX.bind(this, this._onlink));
51
},
52
53
events: ['open', 'close'],
54
55
properties: {
56
width: null,
57
align: 'right',
58
offsetX: 0,
59
offsetY: 0,
60
disableAutofocus: false
61
},
62
63
members: {
64
_node: null,
65
_menu: null,
66
_open: false,
67
_content: null,
68
_position: null,
69
_visible: false,
70
71
setContent: function(content) {
72
JX.DOM.setContent(this._getMenuNode(), content);
73
return this;
74
},
75
76
open: function() {
77
if (this._open) {
78
return;
79
}
80
81
this.invoke('open');
82
JX.Stratcom.invoke('phuix.dropdown.open');
83
84
this._open = true;
85
this._show();
86
87
return this;
88
},
89
90
close: function() {
91
if (!this._open) {
92
return;
93
}
94
this._open = false;
95
this._hide();
96
97
this.invoke('close');
98
99
return this;
100
},
101
102
setPosition: function(pos) {
103
this._position = pos;
104
this._setMenuNodePosition(pos);
105
return this;
106
},
107
108
_getMenuNode: function() {
109
if (!this._menu) {
110
var attrs = {
111
className: 'phuix-dropdown-menu',
112
role: 'button'
113
};
114
115
var menu = JX.$N('div', attrs);
116
117
this._menu = menu;
118
}
119
120
return this._menu;
121
},
122
123
_onclick : function(e) {
124
if (this._open) {
125
this.close();
126
} else {
127
this.open();
128
}
129
e.prevent();
130
},
131
132
_onlink: function(e) {
133
if (!e.isNormalClick()) {
134
return;
135
}
136
137
// If this action was built dynamically with PHUIXActionView, don't
138
// do anything by default. The caller is responsible for installing a
139
// handler if they want to react to clicks.
140
if (e.getNode('phuix-action-view')) {
141
return;
142
}
143
144
// If this item opens a submenu, we don't want to close the current
145
// menu. One submenu is "Edit Related Objects..." on mobile.
146
var link = e.getNode('tag:a');
147
if (JX.Stratcom.hasSigil(link, 'keep-open')) {
148
return;
149
}
150
151
this.close();
152
},
153
154
_onanyclick : function(e) {
155
if (!this._open) {
156
return;
157
}
158
159
if (JX.Stratcom.pass(e)) {
160
return;
161
}
162
163
var t = e.getTarget();
164
while (t) {
165
if (t == this._menu || t == this._node) {
166
return;
167
}
168
t = t.parentNode;
169
}
170
171
this.close();
172
},
173
174
_show : function() {
175
if (!this._visible) {
176
this._visible = true;
177
document.body.appendChild(this._menu);
178
}
179
180
if (this.getWidth()) {
181
new JX.Vector(this.getWidth(), null).setDim(this._menu);
182
}
183
184
this._adjustposition();
185
186
if (this._node) {
187
JX.DOM.alterClass(this._node, 'phuix-dropdown-open', true);
188
this._node.setAttribute('aria-expanded', 'true');
189
}
190
191
// Try to highlight the first link in the menu for assistive technologies.
192
if (!this.getDisableAutofocus()) {
193
var links = JX.DOM.scry(this._menu, 'a');
194
if (links[0]) {
195
JX.DOM.focus(links[0]);
196
}
197
}
198
},
199
200
_hide : function() {
201
this._visible = false;
202
JX.DOM.remove(this._menu);
203
204
if (this._node) {
205
JX.DOM.alterClass(this._node, 'phuix-dropdown-open', false);
206
this._node.setAttribute('aria-expanded', 'false');
207
}
208
},
209
210
_adjustposition : function() {
211
if (!this._open) {
212
return;
213
}
214
215
if (this._position) {
216
this._setMenuNodePosition(this._position);
217
return;
218
}
219
220
if (!this._node) {
221
return;
222
}
223
224
var m = JX.Vector.getDim(this._menu);
225
226
var v = JX.$V(this._node);
227
var d = JX.Vector.getDim(this._node);
228
229
var alignments = ['right', 'left'];
230
var disallow = {};
231
var margin = 8;
232
233
// If "right" alignment would leave us with the dropdown near or off the
234
// left side of the screen, disallow it.
235
var x_min = ((v.x + d.x) - m.x);
236
if (x_min < margin) {
237
disallow.right = true;
238
}
239
240
var align = this.getAlign();
241
242
// If the position disallows the configured alignment, try the next
243
// best alignment instead.
244
245
// If no alignment is allowed, we'll stick with the original alignment
246
// and accept that it isn't going to render very nicely. This can happen
247
// if the browser window is very, very small.
248
if (align in disallow) {
249
for (var ii = 0; ii < alignments.length; ii++) {
250
if (!(alignments[ii] in disallow)) {
251
align = alignments[ii];
252
break;
253
}
254
}
255
}
256
257
switch (align) {
258
case 'right':
259
v = v.add(d)
260
.add(JX.$V(-m.x, 0));
261
break;
262
default:
263
v = v.add(0, d.y);
264
break;
265
}
266
267
this._setMenuNodePosition(v);
268
},
269
270
_setMenuNodePosition: function(v) {
271
v = v.add(this.getOffsetX(), this.getOffsetY());
272
v.setPos(this._menu);
273
},
274
275
getMenuNodeDimensions: function() {
276
if (!this._visible) {
277
document.body.appendChild(this._menu);
278
}
279
280
var dim = JX.Vector.getDim(this._menu);
281
282
if (!this._visible) {
283
JX.DOM.remove(this._menu);
284
}
285
286
return dim;
287
},
288
289
_onkey: function(e) {
290
// When the user presses escape with a menu open, close the menu and
291
// refocus the button which activates the menu. In particular, this makes
292
// popups more usable with assistive technologies.
293
294
if (!this._open) {
295
return;
296
}
297
298
if (e.getSpecialKey() != 'esc') {
299
return;
300
}
301
302
this.close();
303
304
if (this._node) {
305
JX.DOM.focus(this._node);
306
}
307
308
e.prevent();
309
}
310
311
}
312
});
313
314