aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorelijahe2013-12-12 10:44:45 (EST)
committerSilenio Quarti2013-12-12 13:31:29 (EST)
commit0b2ad360d33a4c117fc1c5956b7b6395794ec12a (patch)
tree14133afdd6a6915e597acd608d79995699e48837
parent6926d313a39d3e72d1f3df94b7cedd2bf56addae (diff)
downloadorg.eclipse.orion.client-0b2ad360d33a4c117fc1c5956b7b6395794ec12a.zip
org.eclipse.orion.client-0b2ad360d33a4c117fc1c5956b7b6395794ec12a.tar.gz
org.eclipse.orion.client-0b2ad360d33a4c117fc1c5956b7b6395794ec12a.tar.bz2
Bug 421482 - UX: No context menu!
- Created initial implementation of context menus - Added context menu to common-nav, disabled for M1 - Added on hover showing and hiding of dropdown submenus - Fixed issue with dropdown closing when another subdropdown is opened - Fixed issue with vertical dropdown menu positioning - Fixed issue with tooltip tail positioning where the tail position was only set once and not reset if the position of the tooltip changed - Added function allowing the removal of an auto dismiss from littlelib via removeAutoDismiss(dismissFunction) --Signed-off-by: Elijah El-Haddad <elijahe@ca.ibm.com>
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/css/theme.css19
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/edit/edit.html3
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/commandRegistry.js2
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/nls/root/messages.js3
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/webui/browsercontextmenubutton.html3
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/webui/contextmenu.js198
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js242
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdownseparator.html1
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/webui/littlelib.js24
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/webui/tooltip.js44
-rw-r--r--bundles/org.eclipse.orion.client.ui/web/orion/widgets/nav/common-nav.js65
11 files changed, 507 insertions, 97 deletions
diff --git a/bundles/org.eclipse.orion.client.ui/web/css/theme.css b/bundles/org.eclipse.orion.client.ui/web/css/theme.css
index 178a2d3..2cf8feb 100644
--- a/bundles/org.eclipse.orion.client.ui/web/css/theme.css
+++ b/bundles/org.eclipse.orion.client.ui/web/css/theme.css
@@ -360,9 +360,8 @@ a.currentLocation:hover {
list-style-type: none;
border-radius: 1px;
border: 1px solid #DDD;
- padding: 0;
+ padding: 2px;
background-color: white;
- padding: 10px;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2);
display: none; /* don't take part in layout until open */
line-height: normal; /* don't want to inherit strange line-heights from ancestor elements */
@@ -371,6 +370,7 @@ a.currentLocation:hover {
.dropdownMenuOpen {
min-width: 120px;
display: block;
+ visibility: visible;
}
.dropdownSubMenu {
@@ -397,7 +397,6 @@ a.currentLocation:hover {
.dropdownMenu > li > *:focus {
outline: 1px dotted #ddd;
background: #f3f6fe;
- padding-right: 0;
}
.dropdownMenu > li:hover {
@@ -405,9 +404,6 @@ a.currentLocation:hover {
background: #ffeecc;
}
-.dropdownMenuOpen {
- visibility: visible;
-}
.dropdownSeparator {
height: 1px;
@@ -421,7 +417,7 @@ a.currentLocation:hover {
display: inline-block;
vertical-align: baseline;
color: #222 !important;
- padding: 5px;
+ padding: 5px 0; /* top and bottom 5px, left and right 0 */
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
@@ -437,6 +433,13 @@ a.currentLocation:hover {
border: 1px solid #ccc !important;
}
+span.browserContextButton { /* specifying span element to override .core-sprite-openarrow */
+ background-repeat: no-repeat;
+ background-position: center -527px;
+ text-align: center;
+ padding: 0;
+}
+
.checkedMenuItem {
margin: 4px 6px 2px 0;
vertical-align: bottom;
@@ -628,4 +631,4 @@ a.currentLocation:hover {
opacity: 0.5;
-webkit-transition: opacity 0.2s ease-in;
transition: opacity 0.2s ease-in;
-} \ No newline at end of file
+}
diff --git a/bundles/org.eclipse.orion.client.ui/web/edit/edit.html b/bundles/org.eclipse.orion.client.ui/web/edit/edit.html
index fd1bef3..46a8ab0 100644
--- a/bundles/org.eclipse.orion.client.ui/web/edit/edit.html
+++ b/bundles/org.eclipse.orion.client.ui/web/edit/edit.html
@@ -28,7 +28,8 @@
<div class="auxpane sidePanelLayout hasSplit">
<div class="fixedToolbarHolder">
<div id="sidebarToolbar" class="sidebarToolbar mainToolbar toolComposite toolbarLayout"></div>
- <div id="sidebar" class="toolbarTarget"></div>
+ <div id="sidebar" class="toolbarTarget dropdownTrigger"></div>
+ <ul id="sidebarContextMenu" class="dropdownMenu" role="menu"></ul>
</div>
</div>
<div class="split splitLayout" style="left: 25%;"></div> <!-- 1:3 -->
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/commandRegistry.js b/bundles/org.eclipse.orion.client.ui/web/orion/commandRegistry.js
index fad5380..9372e0c 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/commandRegistry.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/commandRegistry.js
@@ -941,7 +941,7 @@ define([
destroyButton = parent.lastChild;
newMenu = destroyButton.lastChild;
menuButton = newMenu.previousSibling;
- menuButton.dropdown = new mDropdown.Dropdown({dropdown: newMenu, populate: populateFunction});
+ menuButton.dropdown = new mDropdown.Dropdown({dropdown: newMenu, populate: populateFunction, parentDropdown: parent.dropdown});
newMenu.dropdown = menuButton.dropdown;
} else {
if (parent.nodeName.toLowerCase() === "ul") { //$NON-NLS-0$
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/nls/root/messages.js b/bundles/org.eclipse.orion.client.ui/web/orion/nls/root/messages.js
index acd82fc..bcbb6a1 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/nls/root/messages.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/nls/root/messages.js
@@ -89,5 +89,6 @@ define({
"OutlineProgress": "Getting outline for ${0} from ${1}",
"UnknownError": "An unknown error occurred.",
"UnknownWarning": "An unknown warning occurred.",
- "Filter": "Filter (* = any string, ? = any character)"
+ "Filter": "Filter (* = any string, ? = any character)",
+ "To view the browser's context menu, trigger the context menu again.": "To view the browser's context menu, trigger the context menu again."
});
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/browsercontextmenubutton.html b/bundles/org.eclipse.orion.client.ui/web/orion/webui/browsercontextmenubutton.html
new file mode 100644
index 0000000..616b48e
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/browsercontextmenubutton.html
@@ -0,0 +1,3 @@
+<li class="dropdownSubMenu">
+ <span class="dropdownMenuItem modelDecorationSprite core-sprite-openarrow browserContextButton"></span>
+</li> \ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/contextmenu.js b/bundles/org.eclipse.orion.client.ui/web/orion/webui/contextmenu.js
new file mode 100644
index 0000000..e047d3f
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/contextmenu.js
@@ -0,0 +1,198 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2012 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+/*global window define document */
+
+define([
+ 'orion/webui/littlelib',
+ 'orion/webui/dropdown',
+ 'orion/objects',
+ 'orion/webui/tooltip',
+ 'i18n!orion/nls/messages',
+ 'text!orion/webui/dropdownseparator.html',
+ 'text!orion/webui/browsercontextmenubutton.html'
+], function(lib, mDropdown, objects, Tooltip, messages, DropdownSeparatorFragment, BrowserContextMenuFragment) {
+
+ var Dropdown = mDropdown.Dropdown;
+
+ /**
+ * @class orion.webui.ContextMenu
+ * @extends orion.webui.Dropdown
+ *
+ * Attaches context menu behavior to a given node.
+ *
+ * @see orion.webui.Dropdown for more documentation
+ *
+ * @name orion.webui.contextmenu.ContextMenu
+ *
+ */
+ function ContextMenu(options) {
+ options.skipTriggerEventListeners = true; //we want different event listeners on the trigger node
+ Dropdown.call(this, options); //invoke super constructor
+ this._initialize(options);
+ }
+
+ ContextMenu.prototype = Object.create(Dropdown.prototype);
+
+ objects.mixin(ContextMenu.prototype, /** @lends orion.webui.contextmenu.ContextMenu.prototype */ {
+
+ _initialize: function(options) {
+ var self = this;
+
+ if (!this._dropdownNode.dropdown) {
+ //used by commandRegistry to set the parentNode of a child dropdown menu
+ this._dropdownNode.dropdown = this;
+ }
+
+ //add context menu event handlers
+ this._boundcontextmenuEventHandler = this._contextmenuEventHandler.bind(this);
+ this._boundContextMenuCloser = this._contextMenuCloser.bind(this);
+ this._triggerNode.addEventListener("contextmenu", this._boundcontextmenuEventHandler, true);
+ window.addEventListener("contextmenu", this._boundContextMenuCloser, false);
+
+ //clicking on the trigger node should close the context menu
+ this._triggerNode.addEventListener("click", function(event) { //$NON-NLS-0$
+ if (self.isVisible()) {
+ if (self.close()) {
+ lib.stop(event);
+ }
+ }
+ }, false);
+
+ this.addEventListener("postpopulate", this._addBrowserContextMenuArrow.bind(this));
+ },
+
+ _positionContextMenu: function(event) {
+ var mouseLeft = event.clientX;
+ var mouseTop = event.clientY;
+
+ this._dropdownNode.style.left = mouseLeft + "px"; //$NON-NLS-0$
+ this._dropdownNode.style.top = mouseTop + "px"; //$NON-NLS-0$
+ this._dropdownNode.style.position = "fixed"; //$NON-NLS-0$ //TODO convert to absolute position
+
+ var totalBounds = lib.bounds(this._boundingNode(this._triggerNode));
+ var bounds = lib.bounds(this._dropdownNode);
+ var bodyBounds = lib.bounds(document.body);
+ var triggerBounds = lib.bounds(this._triggerNode);
+
+ //ensure menu fits on page horizontally
+ if ((bounds.left + bounds.width) > (bodyBounds.left + bodyBounds.width)) {
+ if (this._triggerNode.classList.contains("dropdownMenuItem")) { //$NON-NLS-0$
+ this._dropdownNode.style.left = -bounds.width + "px"; //$NON-NLS-0$
+ } else {
+ this._dropdownNode.style.left = (triggerBounds.left - totalBounds.left - bounds.width + triggerBounds.width) + "px"; //$NON-NLS-0$
+ }
+ }
+
+ //ensure menu fits on page vertically
+ var overflowY = (bounds.top + bounds.height) - (bodyBounds.top + bodyBounds.height);
+ if (0 < overflowY) {
+ this._dropdownNode.style.top = (bounds.top - overflowY) + "px"; //TODO improve bottom padding estimate
+ }
+ },
+
+ _contextMenuCloser: function(event){
+ this.close(event);
+ },
+
+ _contextmenuEventHandler: function(event){
+ if (this.open(event)) {
+ lib.stop(event);
+ } else {
+ this.close();
+ }
+ },
+
+ _addBrowserContextMenuArrow: function(eventWrapper) {
+ var self = this;
+
+ if (this._separatorElement) {
+ this._dropdownNode.appendChild(this._separatorElement);
+ } else {
+ var separatorRange = document.createRange();
+ separatorRange.selectNode(this._dropdownNode);
+ var separatorFragment = separatorRange.createContextualFragment(DropdownSeparatorFragment);
+ this._dropdownNode.appendChild(separatorFragment);
+ this._separatorElement = this._dropdownNode.lastChild;
+ }
+
+ if (this._browserContextNode) {
+ this._dropdownNode.appendChild(this._browserContextNode);
+ } else {
+ var browserContextRange = document.createRange();
+ browserContextRange.selectNode(this._dropdownNode);
+ var browserContextMenuFragment = browserContextRange.createContextualFragment(BrowserContextMenuFragment);
+ this._dropdownNode.appendChild(browserContextMenuFragment);
+
+ this._browserContextNode = this._dropdownNode.lastChild;
+
+ var browserContextSpan = this._browserContextNode.firstElementChild;
+
+ browserContextSpan.commandTooltip = new Tooltip.Tooltip({
+ node: browserContextSpan,
+ text: messages["To view the browser's context menu, trigger the context menu again."], //$NON-NLS-0$
+ position: ["below", "right", "left", "above"], //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+ trigger: "none" //$NON-NLS-0$
+ });
+
+ // add handler to show tooltip
+ this._browserContextNode.addEventListener("click", function(e){
+ browserContextSpan.commandTooltip.show();
+ }, false);
+
+ // add handler to close submenu
+ this._browserContextNode.addEventListener("mouseover", function(e){
+ self._closeSelectedSubmenu();
+ }, false);
+
+ // add handlers to hide tooltip
+ var hideTooltip = function(e){
+ browserContextSpan.commandTooltip.hide(0);
+ };
+ this.addEventListener("dropdownclosed", hideTooltip, true);
+ this.addEventListener("submenuopen", hideTooltip, true);
+
+ this._browserContextTooltip = browserContextSpan.commandTooltip;
+ }
+ }
+ });
+
+ ContextMenu.prototype.constructor = ContextMenu;
+
+ // overrides Dropdown.protoype.open
+ ContextMenu.prototype.open = function(event /* optional */) {
+ var actionTaken = Dropdown.prototype.open.call(this, event); //call function in super class
+ if (actionTaken) {
+ if (event) {
+ this._positionContextMenu(event);
+ }
+ }
+ return actionTaken;
+ };
+
+ // overrides Dropdown.protoype.destroy
+ ContextMenu.prototype.destroy = function() {
+ this._triggerNode.removeEventListener("contextmenu", this._boundcontextmenuEventHandler, true);
+ window.removeEventListener("contextmenu", this._boundContextMenuCloser, false);
+ this._dropdownNode.dropdown = null;
+ if (this._browserContextNode) {
+ this._dropdownNode.removeChild(this._browserContextNode);
+ this._browserContextNode = null;
+ }
+ if (this._browserContextTooltip) {
+ this._browserContextTooltip.destroy();
+ this._browserContextTooltip = null;
+ }
+ Dropdown.prototype.destroy.call(this); //call function in super class
+ };
+
+ //return the module exports
+ return {ContextMenu: ContextMenu};
+}); \ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js b/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js
index 5e31f2f..8530d71 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js
@@ -10,7 +10,7 @@
******************************************************************************/
/*global window define document */
-define(['require', 'orion/webui/littlelib'], function(require, lib) {
+define(['require', 'orion/webui/littlelib', 'orion/EventTarget'], function(require, lib, EventTarget) {
/**
* Attaches dropdown behavior to a given node. Assumes the triggering node and dropdown node
@@ -35,10 +35,18 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
* @param options.dropdown The node for the dropdown presentation. Required.
* @param options.populate A function that should be called to populate the dropdown before it
* opens each time. Optional.
+ * @param options.triggerNode The node which will listen for events that trigger the
+ * opening of this drop down. If it is not specified the parent of the dropdown node will be searched
+ * for a node containing the dropdownTrigger class. Optional.
+ * @param options.parentDropdown The Dropdown that is the parent of this one if this is a sub-dropdown. Optional.
+ * @param options.skipTriggerEventListeners A boolean indicating whether or not to skip adding event
+ * listeners to the triggerNode. Optional.
+ *
* @name orion.webui.dropdown.Dropdown
*
*/
function Dropdown(options) {
+ EventTarget.attach(this);
this._init(options);
}
Dropdown.prototype = /** @lends orion.webui.dropdown.Dropdown.prototype */ {
@@ -46,32 +54,60 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
_init: function(options) {
this._dropdownNode = lib.node(options.dropdown);
if (!this._dropdownNode) { throw "no dom node for dropdown found"; } //$NON-NLS-0$
- this._triggerNode = lib.$(".dropdownTrigger", this._dropdownNode.parentNode); //$NON-NLS-0$
- if (!this._triggerNode) { throw "no dom node for dropdown trigger found"; } //$NON-NLS-0$
this._populate = options.populate;
this._selectionClass = options.selectionClass;
- var self = this;
+ this._parentDropdown = options.parentDropdown;
- // click on trigger opens.
- this._triggerNode.addEventListener("click", function(event) { //$NON-NLS-0$
- if (self.toggle()) {
- lib.stop(event);
+ if (!this._parentDropdown) {
+ //if a parentDropdown isn't specified move up in dom tree looking for one
+ var parentNode = this._dropdownNode.parentNode;
+ while(parentNode && (document !== parentNode)) {
+ if (parentNode.classList && parentNode.classList.contains("dropdownMenu")) {
+ this._parentDropdown = parentNode.dropdown;
+ break;
+ }
+ parentNode = parentNode.parentNode;
}
- }, false);
+ }
- // if trigger node is not key enabled...
- if (this._triggerNode.tagName.toLowerCase() === "span") { //$NON-NLS-0$
- this._triggerNode.addEventListener("keydown", function(event) { //$NON-NLS-0$
- if (event.keyCode === lib.KEY.ENTER || event.keyCode === lib.KEY.SPACE) {
- self.toggle();
+ if (this._parentDropdown) {
+ this._parentDropdownNode = this._parentDropdown._dropdownNode;
+ }
+
+ if (options.triggerNode) {
+ this._triggerNode = options.triggerNode;
+ } else {
+ this._triggerNode = lib.$(".dropdownTrigger", this._dropdownNode.parentNode); //$NON-NLS-0$
+ }
+ if (!this._triggerNode) { throw "no dom node for dropdown trigger found"; } //$NON-NLS-0$
+
+ if (!options.skipTriggerEventListeners) {
+ var self = this;
+ // click on trigger opens.
+ this._triggerNode.addEventListener("click", function(event) { //$NON-NLS-0$
+ if (self.toggle()) {
lib.stop(event);
}
}, false);
+
+ // if trigger node is not key enabled...
+ if (this._triggerNode.tagName.toLowerCase() === "span") { //$NON-NLS-0$
+ this._triggerNode.addEventListener("keydown", function(event) { //$NON-NLS-0$
+ if (event.keyCode === lib.KEY.ENTER || event.keyCode === lib.KEY.SPACE) {
+ self.toggle();
+ lib.stop(event);
+ }
+ }, false);
+ }
}
// keys
this._dropdownNode.addEventListener("keydown", this._dropdownKeyDown.bind(this), false); //$NON-NLS-0$
+ //submenu open handler
+ var boundSubmenuHandler = this._submenuOpenHandler.bind(this);
+ this.addEventListener("submenuopen", boundSubmenuHandler); //$NON-NLS-0$
+ this._boundSubmenuOpenHandler = boundSubmenuHandler;
},
/**
@@ -89,55 +125,73 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
* Answers whether the dropdown is visible.
*/
isVisible: function() {
- return this._triggerNode.classList.contains("dropdownTriggerOpen"); //$NON-NLS-0$
+ return this._isVisible;
},
/**
* Open the dropdown.
*/
- open: function() {
- if (this.isVisible()) {
- return;
- }
- lib.setFramesEnabled(false);
- if (this._populate) {
- this.empty();
- this._populate(this._dropdownNode);
- }
- var items = this.getItems();
- if (items.length > 0) {
- if (!this._hookedAutoDismiss) {
- // add auto dismiss. Clicking anywhere but trigger or a submenu item means close.
- var submenuNodes = lib.$$(".dropdownSubMenu", this._dropdownNode); //$NON-NLS-0$
- lib.addAutoDismiss([this._triggerNode].concat(Array.prototype.slice.call(submenuNodes)), function() {
- if (this.isVisible()) {
- this.close();
- // Dismiss parent menus
- var temp = this._dropdownNode.parentNode;
- while (temp) {
- if (temp.dropdown && typeof temp.dropdown.close === "function") { //$NON-NLS-0$
- temp.dropdown.close();
- }
- temp = temp.parentNode;
- }
+ open: function(event /* optional */) {
+ var actionTaken = false;
+ if (!this.isVisible()) {
+ lib.setFramesEnabled(false);
+ if (this._populate) {
+ this.empty();
+ this.dispatchEvent({type: "prepopulate", dropdown: this, event: event});
+ this._populate(this._dropdownNode);
+ this.dispatchEvent({type: "postpopulate", dropdown: this, event: event});
+ }
+ var items = this.getItems();
+ if (items.length > 0) {
+ if (!this._hookedAutoDismiss) {
+ if (this._boundAutoDismiss) {
+ lib.removeAutoDismiss(this._boundAutoDismiss);
+ } else {
+ this._boundAutoDismiss = this._autoDismiss.bind(this);
}
- }.bind(this));
- this._hookedAutoDismiss = true;
+ // add auto dismiss. Clicking anywhere but trigger or a submenu item means close.
+ var submenuNodes = lib.$$array(".dropdownSubMenu", this._dropdownNode); //$NON-NLS-0$
+ lib.addAutoDismiss([this._triggerNode].concat(submenuNodes), this._boundAutoDismiss);
+ this._hookedAutoDismiss = true;
+ }
+ this._triggerNode.classList.add("dropdownTriggerOpen"); //$NON-NLS-0$
+ if (this._selectionClass) {
+ this._triggerNode.classList.add(this._selectionClass);
+ }
+ this._dropdownNode.classList.add("dropdownMenuOpen"); //$NON-NLS-0$
+ this._isVisible = true;
+
+ this._positionDropdown();
+ items[0].focus();
+ actionTaken = true;
+
+ if (this._parentDropdown) {
+ this._parentDropdown.dispatchEvent({type: "submenuopen", dropdown: this, event: event});
+ }
}
- this._triggerNode.classList.add("dropdownTriggerOpen"); //$NON-NLS-0$
- if (this._selectionClass) {
- this._triggerNode.classList.add(this._selectionClass);
+ }
+ return actionTaken;
+ },
+
+ _autoDismiss: function(event) {
+ if (this.close(false)) {
+ // only trigger dismissal of parent menus if
+ // this dropdown's node contains the event.target
+ if (this._dropdownNode.contains(event.target)) {
+ // Dismiss parent menus
+ var temp = this._parentDropdown;
+ while (temp) {
+ temp.close(false);
+ temp = temp._parentDropdown;
+ }
}
- this._dropdownNode.classList.add("dropdownMenuOpen"); //$NON-NLS-0$
- this._positionDropdown();
- items[0].focus();
- return true;
}
- return false;
},
_positionDropdown: function() {
this._dropdownNode.style.left = "";
+ this._dropdownNode.style.top = "";
+
var bounds = lib.bounds(this._dropdownNode);
var bodyBounds = lib.bounds(document.body);
if (bounds.left + bounds.width > (bodyBounds.left + bodyBounds.width)) {
@@ -149,6 +203,13 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
this._dropdownNode.style.left = (triggerBounds.left - totalBounds.left - bounds.width + triggerBounds.width) + "px"; //$NON-NLS-0$
}
}
+
+ //ensure menu fits on page vertically
+ var overflowY = (bounds.top + bounds.height) - (bodyBounds.top + bodyBounds.height);
+ if (0 < overflowY) {
+ //TODO (minor) figure out proper bottom padding amount
+ this._dropdownNode.style.top = Math.ceil(this._dropdownNode.style.top - overflowY) + "px";
+ }
},
_boundingNode: function(node) {
@@ -168,19 +229,24 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
* Close the dropdown.
*/
close: function(restoreFocus) {
- if (!this.isVisible()) {
- return;
- }
- this._triggerNode.classList.remove("dropdownTriggerOpen"); //$NON-NLS-0$
- if (this._selectionClass) {
- this._triggerNode.classList.remove(this._selectionClass);
- }
- this._dropdownNode.classList.remove("dropdownMenuOpen"); //$NON-NLS-0$
- lib.setFramesEnabled(true);
- if (restoreFocus) {
- this._triggerNode.focus();
+ var actionTaken = false;
+ if (this.isVisible()) {
+ this._triggerNode.classList.remove("dropdownTriggerOpen"); //$NON-NLS-0$
+ if (this._selectionClass) {
+ this._triggerNode.classList.remove(this._selectionClass);
+ }
+ this._dropdownNode.classList.remove("dropdownMenuOpen"); //$NON-NLS-0$
+ lib.setFramesEnabled(true);
+ if (restoreFocus) {
+ this._triggerNode.focus();
+ }
+
+ this._isVisible = false;
+ actionTaken = true;
+
+ this.dispatchEvent({type: "dropdownclosed", dropdown: this});
}
- return true;
+ return actionTaken;
},
/**
@@ -197,6 +263,21 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
filtered.push(item);
}
});
+
+ //add handler to close open submenu when other items in the parent menu are hovered
+ filtered.forEach(function(item){
+ if (!item._hasDropdownMouseover) {
+ item.addEventListener("mouseover", function(e){
+ if (item.dropdown) {
+ item.dropdown.open(e);
+ } else {
+ self._closeSelectedSubmenu();
+ lib.stop(e);
+ }
+ });
+ item._hasDropdownMouseover = true;
+ }
+ });
return filtered;
},
@@ -212,6 +293,8 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
item.parentNode.removeChild(item);
}
});
+
+ this._hookedAutoDismiss = false; //the autoDismiss nodes need to be recalculated
},
@@ -249,9 +332,42 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
this.close(true);
lib.stop(event);
}
+ },
+
+ /**
+ * Closes this._selectedSubmenu, and its children, if it is open.
+ * Sets the this._selectedSubmenu to the one that's passed in.
+ * @param submenu The submenu that was opened and should be set as the next this._selectedSubmenu
+ */
+ _submenuOpenHandler: function(event) {
+ var submenu = event.dropdown;
+ if (submenu !== this._selectedSubmenu) {
+ //close the current menu and all its children
+ this._closeSelectedSubmenu();
+ this._selectedSubmenu = submenu;
+ }
+ },
+
+ _closeSelectedSubmenu: function() {
+ var currentSubmenu = this._selectedSubmenu;
+ while(currentSubmenu) {
+ currentSubmenu.close();
+ currentSubmenu = currentSubmenu._selectedSubmenu;
+ }
+ },
+
+ destroy: function() {
+ this.empty();
+ if (this._boundAutoDismiss) {
+ lib.removeAutoDismiss(this._boundAutoDismiss);
+ }
+ if (this._boundSubmenuOpenHandler) {
+ this.removeEventListener("submenuopen", this._boundSubmenuOpenHandler);
+ this._boundSubmenuOpenHandler = null;
+ }
}
};
Dropdown.prototype.constructor = Dropdown;
//return the module exports
return {Dropdown: Dropdown};
-}); \ No newline at end of file
+});
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdownseparator.html b/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdownseparator.html
new file mode 100644
index 0000000..00a83e7
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdownseparator.html
@@ -0,0 +1 @@
+<li class="dropdownSeparator"><span class="dropdownSeparator"></span></li> \ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/littlelib.js b/bundles/org.eclipse.orion.client.ui/web/orion/webui/littlelib.js
index 5999a04..4749164 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/webui/littlelib.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/littlelib.js
@@ -235,7 +235,7 @@ define(["orion/util"], function(util) {
});
if (excludeNodeInDocument && !excluded) {
try {
- autoDismissNode.dismiss();
+ autoDismissNode.dismiss(event);
} catch (e) {
if (typeof console !== "undefined" && console) { //$NON-NLS-0$
console.error(e && e.message);
@@ -267,10 +267,25 @@ define(["orion/util"], function(util) {
}, false);
}
}
+
autoDismissNodes.push({excludeNodes: excludeNodes, dismiss: dismissFunction});
}
/**
+ * Removes all auto-dismiss nodes which trigger the specified dismiss function.
+ *
+ * @name orion.webui.littlelib.removeAutoDismiss
+ * @function
+ * @static
+ * @param {Function} dismissFunction The dismiss function to look for.
+ */
+ function removeAutoDismiss(dismissFunction) {
+ autoDismissNodes = autoDismissNodes.filter(function(autoDismissNode) {
+ return dismissFunction !== autoDismissNode.dismiss;
+ });
+ }
+
+ /**
* Cancels the default behavior of an event and stops its propagation.
* @name orion.webui.littlelib.stop
* @function
@@ -331,8 +346,9 @@ define(["orion/util"], function(util) {
stop: stop,
processTextNodes: processTextNodes,
processDOMNodes: processDOMNodes,
- addAutoDismiss: addAutoDismiss,
- setFramesEnabled: setFramesEnabled,
+ addAutoDismiss: addAutoDismiss,
+ setFramesEnabled: setFramesEnabled,
+ removeAutoDismiss: removeAutoDismiss,
KEY: KEY
};
-}); \ No newline at end of file
+});
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/tooltip.js b/bundles/org.eclipse.orion.client.ui/web/orion/webui/tooltip.js
index f8a6876..a433cd3 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/webui/tooltip.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/tooltip.js
@@ -196,6 +196,12 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
}
}
+ if (this._tail && (this._tail.previousPosition !== position)) {
+ //position has changed, tail needs to be modified
+ this._tip.removeChild(this._tail);
+ this._tail = null;
+ }
+
if (!this._tail) {
this._tail = document.createElement("span"); //$NON-NLS-0$
this._tail.classList.add("tooltipTailFrom"+position); //$NON-NLS-0$
@@ -208,6 +214,7 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
if (position === 'left') { //$NON-NLS-0$
this._tail.style.left = tipRect.width + "px"; //$NON-NLS-0$
}
+ this._tail.previousPosition = position;
}
this._tip.style.top = top + "px"; //$NON-NLS-0$
this._tip.style.left = left + "px"; //$NON-NLS-0$
@@ -226,26 +233,31 @@ define(['require', 'orion/webui/littlelib'], function(require, lib) {
if (this._tip && this._tip.classList.contains("tooltipShowing")) { //$NON-NLS-0$
return;
}
- var self = this;
if (this._timeout) {
window.clearTimeout(this._timeout);
this._timeout = null;
}
- this._timeout = window.setTimeout(function() {
- var positioned = false;
- var index = 0;
- while (!positioned && index < self._position.length) {
- positioned = self._positionTip(self._position[index]);
- index++;
- }
- if (!positioned) {
- self._positionTip(self._position[0], true); // force it in, it doesn't fit anywhere
- }
- self._tip.classList.add("tooltipShowing"); //$NON-NLS-0$
- if (self._afterShowing) {
- self._afterShowing();
- }
- }, this._showDelay);
+ if (this._showDelay) {
+ this._timeout = window.setTimeout(this._showImmediately.bind(this), this._showDelay);
+ } else {
+ this._showImmediately();
+ }
+ },
+
+ _showImmediately: function() {
+ var positioned = false;
+ var index = 0;
+ while (!positioned && index < this._position.length) {
+ positioned = this._positionTip(this._position[index]);
+ index++;
+ }
+ if (!positioned) {
+ this._positionTip(this._position[0], true); // force it in, it doesn't fit anywhere
+ }
+ this._tip.classList.add("tooltipShowing"); //$NON-NLS-0$
+ if (this._afterShowing) {
+ this._afterShowing();
+ }
},
/**
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/widgets/nav/common-nav.js b/bundles/org.eclipse.orion.client.ui/web/orion/widgets/nav/common-nav.js
index bebd778..5856634 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/widgets/nav/common-nav.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/widgets/nav/common-nav.js
@@ -23,17 +23,19 @@ define([
'orion/selection',
'orion/URITemplate',
'orion/PageUtil',
- 'orion/Deferred'
+ 'orion/Deferred',
+ 'orion/webui/contextmenu'
], function(
messages, objects, lib, mExplorer, mNavigatorRenderer, mExplorerNavHandler, mKeyBinding,
- FileCommands, ExtensionCommands, Selection, URITemplate, PageUtil, Deferred
+ FileCommands, ExtensionCommands, Selection, URITemplate, PageUtil, Deferred, mContextMenu
) {
var FileExplorer = mExplorer.FileExplorer;
var KeyBinding = mKeyBinding.KeyBinding;
var NavigatorRenderer = mNavigatorRenderer.NavigatorRenderer;
var uriTemplate = new URITemplate("#{,resource,params*}"); //$NON-NLS-0$
-
+ var enableContextMenu = false;
+
/**
* @class orion.sidebar.CommonNavExplorer
* @extends orion.explorers.FileExplorer
@@ -79,6 +81,11 @@ define([
};
this.selection.addEventListener("selectionChanged", this._selectionListener); //$NON-NLS-0$
this.commandsRegistered = this.registerCommands();
+ this._parentNode = lib.node(this.parentId);
+
+ if (enableContextMenu) {
+ this._createContextMenu();
+ }
}
CommonNavExplorer.prototype = Object.create(FileExplorer.prototype);
objects.mixin(CommonNavExplorer.prototype, /** @lends orion.sidebar.CommonNavExplorer.prototype */ {
@@ -166,6 +173,11 @@ define([
});
this.editorInputManager.removeEventListener("InputChanged", this.editorInputListener); //$NON-NLS-0$
this.selection.removeEventListener("selectionChanged", this._selectionListener); //$NON-NLS-0$
+
+ if (this._contextMenu) {
+ this._contextMenu.destroy();
+ this._contextMenu = null;
+ }
},
display: function(root, force) {
return this.loadRoot(root, force).then(function(){
@@ -273,6 +285,53 @@ define([
commandRegistry.destroy(this.additionalNavActionsScope);
commandRegistry.renderCommands(this.folderNavActionsScope, this.folderNavActionsScope, this.treeRoot, this, "tool"); //$NON-NLS-0$
commandRegistry.renderCommands(this.additionalNavActionsScope, this.additionalNavActionsScope, this.treeRoot, this, "tool"); //$NON-NLS-0$
+ },
+ _createContextMenu: function() {
+ //create context menu
+ //function called before populating the context menu to set the nav selection properly
+ var prePopulateContextMenu = function(eventWrapper) {
+ var navHandler = this.getNavHandler();
+ var navDict = this.getNavDict();
+ var event = eventWrapper.event;
+ if (event.target) {
+ var node = event.target;
+ while (this._parentNode.contains(node)) {
+ if ("TR" === node.nodeName) { //TODO this is brittle, see if a better way exists
+ var rowId = node.id;
+ var item = navDict.getValue(rowId);
+ navHandler.setSelection(item.model, false, true);
+ break;
+ }
+ node = node.parentNode;
+ }
+ }
+ }.bind(this);
+
+ //function called to populate the context menu before every time it is shown
+ var populateContextMenu = function(contextMenu) {
+ var selectionService = this.selection;
+ var selections = selectionService.getSelections();
+
+ this.commandRegistry.renderCommands(this.newActionsScope, contextMenu, this.treeRoot, this, "menu"); //$NON-NLS-0$
+
+ if (!selections || (Array.isArray(selections) && !selections.length)) {
+ //no selections, use this.treeRoot to determine commands
+ this.commandRegistry.renderCommands(this.selectionActionsScope, contextMenu, this.treeRoot, this, "menu"); //$NON-NLS-0$
+ } else {
+ //there are selections, use default selection service to determine commands
+ this.commandRegistry.renderCommands(this.selectionActionsScope, contextMenu, null, this, "menu"); //$NON-NLS-0$
+ }
+ }.bind(this);
+
+ var contextMenu = new mContextMenu.ContextMenu({
+ dropdown: lib.node("sidebarContextMenu"),
+ triggerNode: this._parentNode,
+ populate: populateContextMenu
+ });
+
+ contextMenu.addEventListener("prepopulate", prePopulateContextMenu);
+
+ this._contextMenu = contextMenu;
}
});