/** * @provides phuix-dropdown-menu * @requires javelin-install * javelin-util * javelin-dom * javelin-vector * javelin-stratcom * @javelin */ /** * Basic interaction for a dropdown menu. * * The menu is unaware of the content inside it, so it can not close itself * when an item is selected. Callers must make a call to @{method:close} after * an item is chosen in order to close the menu. */ JX.install('PHUIXDropdownMenu', { construct : function(node) { this._node = node; JX.DOM.listen( this._node, 'click', null, JX.bind(this, this._onclick)); JX.Stratcom.listen( 'mousedown', null, JX.bind(this, this._onanyclick)); JX.Stratcom.listen( 'resize', null, JX.bind(this, this._adjustposition)); JX.Stratcom.listen('phuix.dropdown.open', null, JX.bind(this, this.close)); JX.Stratcom.listen('keydown', null, JX.bind(this, this._onkey)); JX.DOM.listen( this._getMenuNode(), 'click', 'tag:a', JX.bind(this, this._onlink)); }, events: ['open', 'close'], properties: { width: null, align: 'right', offsetX: 0, offsetY: 0 }, members: { _node: null, _menu: null, _open: false, _content: null, setContent: function(content) { JX.DOM.setContent(this._getMenuNode(), content); return this; }, open: function() { if (this._open) { return; } this.invoke('open'); JX.Stratcom.invoke('phuix.dropdown.open'); this._open = true; this._show(); return this; }, close: function() { if (!this._open) { return; } this._open = false; this._hide(); this.invoke('close'); return this; }, _getMenuNode: function() { if (!this._menu) { var attrs = { className: 'phuix-dropdown-menu', role: 'button' }; var menu = JX.$N('div', attrs); this._menu = menu; } return this._menu; }, _onclick : function(e) { if (this._open) { this.close(); } else { this.open(); } e.prevent(); }, _onlink: function(e) { if (!e.isNormalClick()) { return; } // If this action was built dynamically with PHUIXActionView, don't // do anything by default. The caller is repsonsible for installing a // handler if they want to react to clicks. if (e.getNode('phuix-action-view')) { return; } // If this item opens a submenu, we don't want to close the current // menu. One submenu is "Edit Related Objects..." on mobile. var link = e.getNode('tag:a'); if (JX.Stratcom.hasSigil(link, 'keep-open')) { return; } this.close(); }, _onanyclick : function(e) { if (!this._open) { return; } if (JX.Stratcom.pass(e)) { return; } var t = e.getTarget(); while (t) { if (t == this._menu || t == this._node) { return; } t = t.parentNode; } this.close(); }, _show : function() { document.body.appendChild(this._menu); if (this.getWidth()) { new JX.Vector(this.getWidth(), null).setDim(this._menu); } this._adjustposition(); JX.DOM.alterClass(this._node, 'phuix-dropdown-open', true); this._node.setAttribute('aria-expanded', 'true'); // Try to highlight the first link in the menu for assistive technologies. var links = JX.DOM.scry(this._menu, 'a'); if (links[0]) { JX.DOM.focus(links[0]); } }, _hide : function() { JX.DOM.remove(this._menu); JX.DOM.alterClass(this._node, 'phuix-dropdown-open', false); this._node.setAttribute('aria-expanded', 'false'); }, _adjustposition : function() { if (!this._open) { return; } var m = JX.Vector.getDim(this._menu); var v = JX.$V(this._node); var d = JX.Vector.getDim(this._node); switch (this.getAlign()) { case 'right': v = v.add(d) .add(JX.$V(-m.x, 0)); break; default: v = v.add(0, d.y); break; } v = v.add(this.getOffsetX(), this.getOffsetY()); v.setPos(this._menu); }, _onkey: function(e) { // When the user presses escape with a menu open, close the menu and // refocus the button which activates the menu. In particular, this makes // popups more usable with assistive technologies. if (!this._open) { return; } if (e.getSpecialKey() != 'esc') { return; } this.close(); JX.DOM.focus(this._node); e.prevent(); } } });