diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css index 6f5ce7ab33..94d636298f 100644 --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -133,7 +133,8 @@ div.jx-typeahead-results { z-index: 20; } -.dropdown-menu-frame { +.dropdown-menu-frame, +.phuix-dropdown-menu { z-index: 32; } diff --git a/webroot/rsrc/css/phui/phui-button.css b/webroot/rsrc/css/phui/phui-button.css index 9969b33d16..a504457b3a 100644 --- a/webroot/rsrc/css/phui/phui-button.css +++ b/webroot/rsrc/css/phui/phui-button.css @@ -166,7 +166,8 @@ button.link:hover { text-decoration: underline; } -.dropdown-menu-frame { +.dropdown-menu-frame, +.phuix-dropdown-menu { position: absolute; width: 240px; background: #fff; diff --git a/webroot/rsrc/externals/javelin/lib/DOM.js b/webroot/rsrc/externals/javelin/lib/DOM.js index ca017a8f5d..dea3d880f6 100644 --- a/webroot/rsrc/externals/javelin/lib/DOM.js +++ b/webroot/rsrc/externals/javelin/lib/DOM.js @@ -944,6 +944,7 @@ JX.install('DOM', { try { node.focus(); } catch (lol_ie) {} }, + /** * Scroll to the position of an element in the document. * @task view diff --git a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js index 8fbf856e79..d8fe6ee0bb 100644 --- a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js +++ b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js @@ -4,13 +4,13 @@ * javelin-dom * javelin-util * javelin-stratcom - * phabricator-dropdown-menu - * phabricator-menu-item + * phuix-dropdown-menu + * phuix-action-list-view + * phuix-action-view * phabricator-phtize */ JX.behavior('differential-dropdown-menus', function(config) { - var pht = JX.phtize(config.pht); function show_more(container) { @@ -30,109 +30,6 @@ JX.behavior('differential-dropdown-menus', function(config) { } } - function build_menu(button, data) { - - function link_to(name, uri) { - var item = new JX.PhabricatorMenuItem( - name, - JX.bind(null, window.open, uri), - uri); - item.setDisabled(!uri); - return item; - } - - var reveal_item = new JX.PhabricatorMenuItem('', function () { - show_more(JX.$(data.containerID)); - }); - - var diffusion_item; - if (data.diffusionURI) { - // Show this only if we have a link, since when this appears in Diffusion - // it is otherwise potentially confusing. - diffusion_item = link_to(pht('Browse in Diffusion'), data.diffusionURI); - } - - var menu = new JX.PhabricatorDropdownMenu(buttons[ii]) - .addItem(reveal_item); - - var visible_item = new JX.PhabricatorMenuItem('', function () { - JX.Stratcom.invoke('differential-toggle-file', null, { - diff: JX.DOM.scry(JX.$(data.containerID), 'table', 'differential-diff') - }); - }); - menu.addItem(visible_item); - - if (diffusion_item) { - menu.addItem(diffusion_item); - } - - menu.addItem(link_to(pht('View Standalone'), data.standaloneURI)); - - if (data.leftURI) { - menu.addItem(link_to(pht('Show Raw File (Left)'), data.leftURI)); - } - - if (data.rightURI) { - menu.addItem(link_to(pht('Show Raw File (Right)'), data.rightURI)); - } - - if (data.editor) { - menu.addItem(new JX.PhabricatorMenuItem( - pht('Open in Editor'), - // Open in the same window. - JX.bind(location, location.assign, data.editor), - data.editor)); - } - - if (data.editorConfigure) { - menu.addItem(link_to(pht('Configure Editor'), data.editorConfigure)); - } - - menu.listen( - 'open', - function() { - - // When the user opens the menu, check if there are any "Show More" - // links in the changeset body. If there aren't, disable the "Show - // Entire File" menu item since it won't change anything. - - var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more'); - if (nodes.length) { - reveal_item.setDisabled(false); - reveal_item.setName(pht('Show Entire File')); - } else { - reveal_item.setDisabled(true); - reveal_item.setName(pht('Entire File Shown')); - } - - visible_item.setDisabled(true); - visible_item.setName(pht("Can't Toggle Unloaded File")); - var diffs = JX.DOM.scry(JX.$(data.containerID), - 'table', 'differential-diff'); - if (diffs.length > 1) { - JX.$E( - 'More than one node with sigil "differential-diff" was found in "'+ - data.containerID+'."'); - } else if (diffs.length == 1) { - diff = diffs[0]; - visible_item.setDisabled(false); - if (JX.Stratcom.getData(diff).hidden) { - visible_item.setName(pht('Expand File')); - } else { - visible_item.setName(pht('Collapse File')); - } - } else { - // Do nothing when there is no diff shown in the table. For example, - // the file is binary. - } - }); - } - - var buttons = JX.DOM.scry(window.document, 'a', 'differential-view-options'); - for (var ii = 0; ii < buttons.length; ii++) { - build_menu(buttons[ii], JX.Stratcom.getData(buttons[ii])); - } - JX.Stratcom.listen( 'click', 'differential-reveal-all', @@ -147,4 +44,124 @@ JX.behavior('differential-dropdown-menus', function(config) { e.kill(); }); + var buildmenu = function(e) { + var button = e.getNode('differential-view-options'); + var data = JX.Stratcom.getData(button); + + if (data.menu) { + return; + } + + e.prevent(); + + var menu = new JX.PHUIXDropdownMenu(button); + var list = new JX.PHUIXActionListView(); + + var add_link = function(icon, name, href, local) { + if (!href) { + return; + } + + var link = new JX.PHUIXActionView() + .setIcon(icon) + .setName(name) + .setHref(href) + .setHandler(function(e) { + if (local) { + window.location.assign(href); + } else { + window.open(href); + } + menu.close(); + e.prevent(); + }); + + list.addItem(link); + return link; + }; + + var reveal_item = new JX.PHUIXActionView() + .setIcon('preview'); + list.addItem(reveal_item); + + var visible_item = new JX.PHUIXActionView() + .setHandler(function(e) { + var diff = JX.DOM.scry( + JX.$(data.containerID), + 'table', + 'differential-diff'); + + JX.Stratcom.invoke('differential-toggle-file', null, {diff: diff}); + e.prevent(); + menu.close(); + }); + list.addItem(visible_item); + + add_link('file', pht('Browse in Diffusion'), data.diffusionURI); + add_link('transcript', pht('View Standalone'), data.standaloneURI); + add_link('arrow_left', pht('Show Raw File (Left)'), data.leftURI); + add_link('arrow_right', pht('Show Raw File (Right)'), data.rightURI); + add_link('edit', pht('Open in Editor'), data.editor, true); + add_link('wrench', pht('Configure Editor'), data.editorConfigure); + + + menu.setContent(list.getNode()); + + menu.listen('open', function() { + + // When the user opens the menu, check if there are any "Show More" + // links in the changeset body. If there aren't, disable the "Show + // Entire File" menu item since it won't change anything. + + var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more'); + if (nodes.length) { + reveal_item + .setDisabled(false) + .setName(pht('Show Entire File')) + .setHandler(function(e) { + show_more(JX.$(data.containerID)); + e.prevent(); + menu.close(); + }); + } else { + reveal_item + .setDisabled(true) + .setName(pht('Entire File Shown')) + .setHandler(function(e) { e.prevent(); }); + } + + visible_item.setDisabled(true); + visible_item.setName(pht("Can't Toggle Unloaded File")); + var diffs = JX.DOM.scry( + JX.$(data.containerID), + 'table', + 'differential-diff'); + + if (diffs.length > 1) { + JX.$E( + 'More than one node with sigil "differential-diff" was found in "'+ + data.containerID+'."'); + } else if (diffs.length == 1) { + diff = diffs[0]; + visible_item.setDisabled(false); + if (JX.Stratcom.getData(diff).hidden) { + visible_item + .setName(pht('Expand File')) + .setIcon('unmerge'); + } else { + visible_item + .setName(pht('Collapse File')) + .setIcon('merge'); + } + } else { + // Do nothing when there is no diff shown in the table. For example, + // the file is binary. + } + + }); + data.menu = menu; + menu.open(); + }; + + JX.Stratcom.listen('click', 'differential-view-options', buildmenu); }); diff --git a/webroot/rsrc/js/phuix/PHUIXActionListView.js b/webroot/rsrc/js/phuix/PHUIXActionListView.js new file mode 100644 index 0000000000..1eef60176b --- /dev/null +++ b/webroot/rsrc/js/phuix/PHUIXActionListView.js @@ -0,0 +1,36 @@ +/** + * @provides phuix-action-list-view + * @requires javelin-install + * javelin-dom + */ + +JX.install('PHUIXActionListView', { + + construct: function() { + this._items = []; + }, + + members: { + _items: null, + _node: null, + + addItem: function(item) { + this._items.push(item); + this.getNode().appendChild(item.getNode()); + return this; + }, + + getNode: function() { + if (!this._node) { + var attrs = { + className: 'phabricator-action-list-view' + }; + + this._node = JX.$N('ul', attrs); + } + + return this._node; + } + } + +}); diff --git a/webroot/rsrc/js/phuix/PHUIXActionView.js b/webroot/rsrc/js/phuix/PHUIXActionView.js new file mode 100644 index 0000000000..09b0edc467 --- /dev/null +++ b/webroot/rsrc/js/phuix/PHUIXActionView.js @@ -0,0 +1,138 @@ +/** + * @provides phuix-action-view + * @requires javelin-install + * javelin-dom + * javelin-util + * @javelin + */ + +JX.install('PHUIXActionView', { + + members: { + _node: null, + _name: null, + _icon: 'none', + _disabled: false, + _handler: null, + + _iconNode: null, + _nameNode: null, + + setDisabled: function(disabled) { + this._disabled = disabled; + JX.DOM.alterClass( + this.getNode(), + 'phabricator-action-view-disabled', + disabled); + + this._buildIconNode(true); + + return this; + }, + + getDisabled: function() { + return this._disabled; + }, + + setName: function(name) { + this._name = name; + this._buildNameNode(true); + return this; + }, + + setHandler: function(handler) { + this._handler = handler; + this._buildNameNode(true); + return this; + }, + + setIcon: function(icon) { + this._icon = icon; + this._buildIconNode(true); + return this; + }, + + setHref: function(href) { + this._href = href; + this._buildNameNode(true); + return this; + }, + + getNode: function() { + if (!this._node) { + var attr = { + className: 'phabricator-action-view' + }; + + var content = [ + this._buildIconNode(), + this._buildNameNode() + ]; + + this._node = JX.$N('li', attr, content); + } + + return this._node; + }, + + _buildIconNode: function(dirty) { + if (!this._iconNode || dirty) { + var attr = { + className: 'phui-icon-view sprite-icons phabricator-action-view-icon' + }; + var node = JX.$N('span', attr); + + var icon_class = 'icons-' + this._icon; + if (this._disabled) { + icon_class = icon_class + '-grey'; + } + + JX.DOM.alterClass(node, icon_class, true); + + if (this._iconNode && this._iconNode.parentNode) { + JX.DOM.replace(this._iconNode, node); + } + this._iconNode = node; + } + + return this._iconNode; + }, + + _buildNameNode: function(dirty) { + if (!this._nameNode || dirty) { + var attr = { + className: 'phabricator-action-view-item' + }; + + var href = this._href; + if (!href && this._handler) { + href = '#'; + } + if (href) { + attr.href = href; + + } + + var tag = href ? 'a' : 'span'; + + var node = JX.$N(tag, attr, this._name); + JX.DOM.listen(node, 'click', null, JX.bind(this, this._onclick)); + + if (this._nameNode && this._nameNode.parentNode) { + JX.DOM.replace(this._nameNode, node); + } + this._nameNode = node; + } + + return this._nameNode; + }, + + _onclick: function(e) { + if (this._handler) { + this._handler(e); + } + } + + } + +}); diff --git a/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js new file mode 100644 index 0000000000..ed47a8a792 --- /dev/null +++ b/webroot/rsrc/js/phuix/PHUIXDropdownMenu.js @@ -0,0 +1,177 @@ +/** + * @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)); + }, + + events: ['open'], + + properties: { + width: null, + align: 'right' + }, + + 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(); + + return this; + }, + + _getMenuNode: function() { + if (!this._menu) { + var attrs = { + className: 'phuix-dropdown-menu', + role: 'button' + }; + + var menu = JX.$N('div', attrs); + + this._node.setAttribute('aria-haspopup', 'true'); + this._node.setAttribute('aria-expanded', 'false'); + + this._menu = menu; + } + + return this._menu; + }, + + _onclick : function(e) { + if (this._open) { + this.close(); + } else { + this.open(); + } + e.prevent(); + }, + + _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'); + }, + + _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.setPos(this._menu); + } + } +});