Path: blob/master/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js
12241 views
/**1* @provides phuix-dropdown-menu2* @requires javelin-install3* javelin-util4* javelin-dom5* javelin-vector6* javelin-stratcom7* @javelin8*/91011/**12* Basic interaction for a dropdown menu.13*14* The menu is unaware of the content inside it, so it can not close itself15* when an item is selected. Callers must make a call to @{method:close} after16* an item is chosen in order to close the menu.17*/18JX.install('PHUIXDropdownMenu', {1920construct : function(node) {21this._node = node;2223if (node) {24JX.DOM.listen(25this._node,26'click',27null,28JX.bind(this, this._onclick));29}3031JX.Stratcom.listen(32'mousedown',33null,34JX.bind(this, this._onanyclick));3536JX.Stratcom.listen(37'resize',38null,39JX.bind(this, this._adjustposition));4041JX.Stratcom.listen('phuix.dropdown.open', null, JX.bind(this, this.close));4243JX.Stratcom.listen('keydown', null, JX.bind(this, this._onkey));4445JX.DOM.listen(46this._getMenuNode(),47'click',48'tag:a',49JX.bind(this, this._onlink));50},5152events: ['open', 'close'],5354properties: {55width: null,56align: 'right',57offsetX: 0,58offsetY: 0,59disableAutofocus: false60},6162members: {63_node: null,64_menu: null,65_open: false,66_content: null,67_position: null,68_visible: false,6970setContent: function(content) {71JX.DOM.setContent(this._getMenuNode(), content);72return this;73},7475open: function() {76if (this._open) {77return;78}7980this.invoke('open');81JX.Stratcom.invoke('phuix.dropdown.open');8283this._open = true;84this._show();8586return this;87},8889close: function() {90if (!this._open) {91return;92}93this._open = false;94this._hide();9596this.invoke('close');9798return this;99},100101setPosition: function(pos) {102this._position = pos;103this._setMenuNodePosition(pos);104return this;105},106107_getMenuNode: function() {108if (!this._menu) {109var attrs = {110className: 'phuix-dropdown-menu',111role: 'button'112};113114var menu = JX.$N('div', attrs);115116this._menu = menu;117}118119return this._menu;120},121122_onclick : function(e) {123if (this._open) {124this.close();125} else {126this.open();127}128e.prevent();129},130131_onlink: function(e) {132if (!e.isNormalClick()) {133return;134}135136// If this action was built dynamically with PHUIXActionView, don't137// do anything by default. The caller is responsible for installing a138// handler if they want to react to clicks.139if (e.getNode('phuix-action-view')) {140return;141}142143// If this item opens a submenu, we don't want to close the current144// menu. One submenu is "Edit Related Objects..." on mobile.145var link = e.getNode('tag:a');146if (JX.Stratcom.hasSigil(link, 'keep-open')) {147return;148}149150this.close();151},152153_onanyclick : function(e) {154if (!this._open) {155return;156}157158if (JX.Stratcom.pass(e)) {159return;160}161162var t = e.getTarget();163while (t) {164if (t == this._menu || t == this._node) {165return;166}167t = t.parentNode;168}169170this.close();171},172173_show : function() {174if (!this._visible) {175this._visible = true;176document.body.appendChild(this._menu);177}178179if (this.getWidth()) {180new JX.Vector(this.getWidth(), null).setDim(this._menu);181}182183this._adjustposition();184185if (this._node) {186JX.DOM.alterClass(this._node, 'phuix-dropdown-open', true);187this._node.setAttribute('aria-expanded', 'true');188}189190// Try to highlight the first link in the menu for assistive technologies.191if (!this.getDisableAutofocus()) {192var links = JX.DOM.scry(this._menu, 'a');193if (links[0]) {194JX.DOM.focus(links[0]);195}196}197},198199_hide : function() {200this._visible = false;201JX.DOM.remove(this._menu);202203if (this._node) {204JX.DOM.alterClass(this._node, 'phuix-dropdown-open', false);205this._node.setAttribute('aria-expanded', 'false');206}207},208209_adjustposition : function() {210if (!this._open) {211return;212}213214if (this._position) {215this._setMenuNodePosition(this._position);216return;217}218219if (!this._node) {220return;221}222223var m = JX.Vector.getDim(this._menu);224225var v = JX.$V(this._node);226var d = JX.Vector.getDim(this._node);227228var alignments = ['right', 'left'];229var disallow = {};230var margin = 8;231232// If "right" alignment would leave us with the dropdown near or off the233// left side of the screen, disallow it.234var x_min = ((v.x + d.x) - m.x);235if (x_min < margin) {236disallow.right = true;237}238239var align = this.getAlign();240241// If the position disallows the configured alignment, try the next242// best alignment instead.243244// If no alignment is allowed, we'll stick with the original alignment245// and accept that it isn't going to render very nicely. This can happen246// if the browser window is very, very small.247if (align in disallow) {248for (var ii = 0; ii < alignments.length; ii++) {249if (!(alignments[ii] in disallow)) {250align = alignments[ii];251break;252}253}254}255256switch (align) {257case 'right':258v = v.add(d)259.add(JX.$V(-m.x, 0));260break;261default:262v = v.add(0, d.y);263break;264}265266this._setMenuNodePosition(v);267},268269_setMenuNodePosition: function(v) {270v = v.add(this.getOffsetX(), this.getOffsetY());271v.setPos(this._menu);272},273274getMenuNodeDimensions: function() {275if (!this._visible) {276document.body.appendChild(this._menu);277}278279var dim = JX.Vector.getDim(this._menu);280281if (!this._visible) {282JX.DOM.remove(this._menu);283}284285return dim;286},287288_onkey: function(e) {289// When the user presses escape with a menu open, close the menu and290// refocus the button which activates the menu. In particular, this makes291// popups more usable with assistive technologies.292293if (!this._open) {294return;295}296297if (e.getSpecialKey() != 'esc') {298return;299}300301this.close();302303if (this._node) {304JX.DOM.focus(this._node);305}306307e.prevent();308}309310}311});312313314