Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit ea94c8b

Browse files
authored
Merge pull request #13824 from adobe/boopeshmahendran/ContextSubMenu
Add api for adding context submenus
2 parents 05b3247 + 5616075 commit ea94c8b

File tree

2 files changed

+418
-21
lines changed

2 files changed

+418
-21
lines changed

src/command/Menus.js

Lines changed: 226 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ define(function (require, exports, module) {
124124
* Other constants
125125
*/
126126
var DIVIDER = "---";
127+
var SUBMENU = "SUBMENU";
127128

128129
/**
129130
* Error Codes from Brackets Shell
@@ -310,7 +311,7 @@ define(function (require, exports, module) {
310311
this.isDivider = (command === DIVIDER);
311312
this.isNative = false;
312313

313-
if (!this.isDivider) {
314+
if (!this.isDivider && command !== SUBMENU) {
314315
// Bind event handlers
315316
this._enabledChanged = this._enabledChanged.bind(this);
316317
this._checkedChanged = this._checkedChanged.bind(this);
@@ -612,6 +613,11 @@ define(function (require, exports, module) {
612613
$menuItem.on("click", function () {
613614
menuItem._command.execute();
614615
});
616+
617+
var self = this;
618+
$menuItem.on("mouseenter", function () {
619+
self.closeSubMenu();
620+
});
615621
}
616622

617623
// Insert menu item
@@ -740,6 +746,160 @@ define(function (require, exports, module) {
740746
// NOT IMPLEMENTED
741747
// };
742748

749+
/**
750+
*
751+
* Creates a new submenu and a menuItem and adds the menuItem of the submenu
752+
* to the menu and returns the submenu.
753+
*
754+
* A submenu will have the same structure of a menu with a additional field
755+
* parentMenuItem which has the reference of the submenu's parent menuItem.
756+
757+
* A submenu will raise the following events:
758+
* - beforeSubMenuOpen
759+
* - beforeSubMenuClose
760+
*
761+
* Note, This function will create only a context submenu.
762+
*
763+
* TODO: Make this function work for Menus
764+
*
765+
*
766+
* @param {!string} name displayed in menu item of the submenu
767+
* @param {!string} id
768+
* @param {?string} position - constant defining the position of new MenuItem of the submenu relative to
769+
* other MenuItems. Values:
770+
* - With no relativeID, use Menus.FIRST or LAST (default is LAST)
771+
* - Relative to a command id, use BEFORE or AFTER (required)
772+
* - Relative to a MenuSection, use FIRST_IN_SECTION or LAST_IN_SECTION (required)
773+
* @param {?string} relativeID - command id OR one of the MenuSection.* constants. Required
774+
* for all position constants except FIRST and LAST.
775+
*
776+
* @return {Menu} the newly created submenu
777+
*/
778+
Menu.prototype.addSubMenu = function (name, id, position, relativeID) {
779+
780+
if (!name || !id) {
781+
console.error("addSubMenu(): missing required parameters: name and id");
782+
return null;
783+
}
784+
785+
// Guard against duplicate context menu ids
786+
if (contextMenuMap[id]) {
787+
console.log("Context menu added with id of existing Context Menu: " + id);
788+
return null;
789+
}
790+
791+
var menu = new ContextMenu(id);
792+
contextMenuMap[id] = menu;
793+
794+
var menuItemID = this.id + "-" + id;
795+
796+
if (menuItemMap[menuItemID]) {
797+
console.log("MenuItem added with same id of existing MenuItem: " + id);
798+
return null;
799+
}
800+
801+
// create MenuItem
802+
var menuItem = new MenuItem(menuItemID, SUBMENU);
803+
menuItemMap[menuItemID] = menuItem;
804+
805+
menu.parentMenuItem = menuItem;
806+
807+
// create MenuItem DOM
808+
if (_isHTMLMenu(this.id)) {
809+
// Create the HTML MenuItem
810+
var $menuItem = $("<li><a href='#' id='" + menuItemID + "'>" +
811+
"<span class='menu-name'>" + name + "</span>" +
812+
"<span style='float: right'>&rtrif;</span>" +
813+
"</a></li>");
814+
815+
var self = this;
816+
$menuItem.on("mouseenter", function(e) {
817+
if (self.openSubMenu && self.openSubMenu.id === menu.id) {
818+
return;
819+
}
820+
self.closeSubMenu();
821+
self.openSubMenu = menu;
822+
menu.open();
823+
});
824+
825+
// Insert menu item
826+
var $relativeElement = this._getRelativeMenuItem(relativeID, position);
827+
_insertInList($("li#" + StringUtils.jQueryIdEscape(this.id) + " > ul.dropdown-menu"),
828+
$menuItem, position, $relativeElement);
829+
} else {
830+
// TODO: add submenus for native menus
831+
}
832+
return menu;
833+
};
834+
835+
836+
/**
837+
* Removes the specified submenu from this Menu.
838+
*
839+
* Note, this function will only remove context submenus
840+
*
841+
* TODO: Make this function work for Menus
842+
*
843+
* @param {!string} subMenuID - the menu id of the submenu to remove.
844+
*/
845+
Menu.prototype.removeSubMenu = function (subMenuID) {
846+
var subMenu,
847+
parentMenuItem,
848+
commandID = "";
849+
850+
if (!subMenuID) {
851+
console.error("removeSubMenu(): missing required parameters: subMenuID");
852+
return;
853+
}
854+
855+
subMenu = getContextMenu(subMenuID);
856+
857+
if (!subMenu || !subMenu.parentMenuItem) {
858+
console.error("removeSubMenu(): parameter subMenuID: %s is not a valid submenu id", subMenuID);
859+
return;
860+
}
861+
862+
parentMenuItem = subMenu.parentMenuItem;
863+
864+
865+
if (!menuItemMap[parentMenuItem.id]) {
866+
console.error("removeSubMenu(): parent menuItem not found in menuItemMap: %s", parentMenuItem.id);
867+
return;
868+
}
869+
870+
// Remove all of the menu items in the submenu
871+
_.forEach(menuItemMap, function (value, key) {
872+
if (_.startsWith(key, subMenuID)) {
873+
if (value.isDivider) {
874+
subMenu.removeMenuDivider(key);
875+
} else {
876+
commandID = value.getCommand();
877+
subMenu.removeMenuItem(commandID);
878+
}
879+
}
880+
});
881+
882+
if (_isHTMLMenu(this.id)) {
883+
$(_getHTMLMenuItem(parentMenuItem.id)).parent().remove(); // remove the menu item
884+
$(_getHTMLMenu(subMenuID)).remove(); // remove the menu
885+
} else {
886+
// TODO: remove submenus for native menus
887+
}
888+
889+
890+
delete menuItemMap[parentMenuItem.id];
891+
delete contextMenuMap[subMenuID];
892+
};
893+
894+
/**
895+
* Closes the submenu if the menu has a submenu open.
896+
*/
897+
Menu.prototype.closeSubMenu = function() {
898+
if (this.openSubMenu) {
899+
this.openSubMenu.close();
900+
this.openSubMenu = null;
901+
}
902+
};
743903
/**
744904
* Gets the Command associated with a MenuItem
745905
* @return {Command}
@@ -1038,17 +1198,24 @@ define(function (require, exports, module) {
10381198

10391199
/**
10401200
* Displays the ContextMenu at the specified location and dispatches the
1041-
* "beforeContextMenuOpen" event.The menu location may be adjusted to prevent
1042-
* clipping by the browser window. All other menus and ContextMenus will be closed
1043-
* bofore a new menu is shown.
1201+
* "beforeContextMenuOpen" event or "beforeSubMenuOpen" event (for submenus).
1202+
* The menu location may be adjusted to prevent clipping by the browser window.
1203+
* All other menus and ContextMenus will be closed before a new menu
1204+
* will be closed before a new menu is shown (if the new menu is not
1205+
* a submenu).
1206+
*
1207+
* In case of submenus, the parentMenu of the submenu will not be closed when the
1208+
* sub menu is open.
10441209
*
10451210
* @param {MouseEvent | {pageX:number, pageY:number}} mouseOrLocation - pass a MouseEvent
10461211
* to display the menu near the mouse or pass in an object with page x/y coordinates
1047-
* for a specific location.
1212+
* for a specific location.This paramter is not used for submenus. Submenus are always
1213+
* displayed at a position relative to the parent menu.
10481214
*/
10491215
ContextMenu.prototype.open = function (mouseOrLocation) {
10501216

1051-
if (!mouseOrLocation || !mouseOrLocation.hasOwnProperty("pageX") || !mouseOrLocation.hasOwnProperty("pageY")) {
1217+
if (!this.parentMenuItem &&
1218+
(!mouseOrLocation || !mouseOrLocation.hasOwnProperty("pageX") || !mouseOrLocation.hasOwnProperty("pageY"))) {
10521219
console.error("ContextMenu open(): missing required parameter");
10531220
return;
10541221
}
@@ -1057,37 +1224,70 @@ define(function (require, exports, module) {
10571224
escapedId = StringUtils.jQueryIdEscape(this.id),
10581225
$menuAnchor = $("#" + escapedId),
10591226
$menuWindow = $("#" + escapedId + " > ul"),
1060-
posTop = mouseOrLocation.pageY,
1061-
posLeft = mouseOrLocation.pageX;
1227+
posTop,
1228+
posLeft;
10621229

10631230
// only show context menu if it has menu items
10641231
if ($menuWindow.children().length <= 0) {
10651232
return;
10661233
}
10671234

1068-
this.trigger("beforeContextMenuOpen");
1069-
1070-
// close all other dropdowns
1071-
closeAll();
10721235

10731236
// adjust positioning so menu is not clipped off bottom or right
1074-
var elementRect = {
1237+
if (this.parentMenuItem) { // If context menu is a submenu
1238+
1239+
this.trigger("beforeSubMenuOpen");
1240+
1241+
var $parentMenuItem = $(_getHTMLMenuItem(this.parentMenuItem.id));
1242+
1243+
posTop = $parentMenuItem.offset().top;
1244+
posLeft = $parentMenuItem.offset().left + $parentMenuItem.outerWidth();
1245+
1246+
var elementRect = {
10751247
top: posTop,
10761248
left: posLeft,
10771249
height: $menuWindow.height() + 25,
10781250
width: $menuWindow.width()
10791251
},
10801252
clip = ViewUtils.getElementClipSize($window, elementRect);
10811253

1082-
if (clip.bottom > 0) {
1083-
posTop = Math.max(0, posTop - clip.bottom);
1084-
}
1085-
posTop -= 30; // shift top for hidden parent element
1086-
posLeft += 5;
1254+
if (clip.bottom > 0) {
1255+
posTop = Math.max(0, posTop + $parentMenuItem.height() - $menuWindow.height());
1256+
}
10871257

1258+
posTop -= 30; // shift top for hidden parent element
1259+
posLeft += 3;
10881260

1089-
if (clip.right > 0) {
1090-
posLeft = Math.max(0, posLeft - clip.right);
1261+
if (clip.right > 0) {
1262+
posLeft = Math.max(0, posLeft - 2 * $parentMenuItem.outerWidth());
1263+
}
1264+
} else {
1265+
this.trigger("beforeContextMenuOpen");
1266+
1267+
// close all other dropdowns
1268+
closeAll();
1269+
1270+
posTop = mouseOrLocation.pageY;
1271+
posLeft = mouseOrLocation.pageX;
1272+
1273+
var elementRect = {
1274+
top: posTop,
1275+
left: posLeft,
1276+
height: $menuWindow.height() + 25,
1277+
width: $menuWindow.width()
1278+
},
1279+
clip = ViewUtils.getElementClipSize($window, elementRect);
1280+
1281+
if (clip.bottom > 0) {
1282+
posTop = Math.max(0, posTop - clip.bottom);
1283+
}
1284+
posTop -= 30; // shift top for hidden parent element
1285+
posLeft += 5;
1286+
1287+
1288+
if (clip.right > 0) {
1289+
posLeft = Math.max(0, posLeft - clip.right);
1290+
}
10911291
}
10921292

10931293
// open the context menu at final location
@@ -1100,7 +1300,12 @@ define(function (require, exports, module) {
11001300
* Closes the context menu.
11011301
*/
11021302
ContextMenu.prototype.close = function () {
1103-
this.trigger("beforeContextMenuClose");
1303+
if (this.parentMenuItem) {
1304+
this.trigger("beforeSubMenuClose");
1305+
} else {
1306+
this.trigger("beforeContextMenuClose");
1307+
}
1308+
this.closeSubMenu();
11041309
$("#" + StringUtils.jQueryIdEscape(this.id)).removeClass("open");
11051310
};
11061311

0 commit comments

Comments
 (0)