Brackets Application Menu Constants
var AppMenuBar = {
FILE_MENU : "file-menu",
EDIT_MENU : "edit-menu",
FIND_MENU : "find-menu",
VIEW_MENU : "view-menu",
NAVIGATE_MENU : "navigate-menu",
HELP_MENU : "help-menu"
};
Insertion position constants Used by addMenu(), addMenuItem(), and addSubMenu() to specify the relative position of a newly created menu object
var BEFORE = "before",
AFTER = "after",
FIRST = "first",
LAST = "last",
FIRST_IN_SECTION = "firstInSection",
LAST_IN_SECTION = "lastInSection";
Brackets Context Menu Constants
var ContextMenuIds = {
EDITOR_MENU: "editor-context-menu",
INLINE_EDITOR_MENU: "inline-editor-context-menu",
PROJECT_MENU: "project-context-menu",
WORKING_SET_CONTEXT_MENU: "workingset-context-menu",
WORKING_SET_CONFIG_MENU: "workingset-configuration-menu",
SPLITVIEW_MENU: "splitview-menu"
};
Brackets Application Menu Section Constants It is preferred that plug-ins specify the location of new MenuItems in terms of a menu section rather than a specific MenuItem. This provides looser coupling to Bracket's internal MenuItems and makes menu organization more semantic. Use these constants as the "relativeID" parameter when calling addMenuItem() and specify a position of FIRST_IN_SECTION or LAST_IN_SECTION.
Menu sections are denoted by dividers or the beginning/end of a menu
var MenuSection = {
// Menu Section Command ID to mark the section
FILE_OPEN_CLOSE_COMMANDS: {sectionMarker: Commands.FILE_NEW},
FILE_SAVE_COMMANDS: {sectionMarker: Commands.FILE_SAVE},
FILE_LIVE: {sectionMarker: Commands.FILE_LIVE_FILE_PREVIEW},
FILE_EXTENSION_MANAGER: {sectionMarker: Commands.FILE_EXTENSION_MANAGER},
EDIT_UNDO_REDO_COMMANDS: {sectionMarker: Commands.EDIT_UNDO},
EDIT_TEXT_COMMANDS: {sectionMarker: Commands.EDIT_CUT},
EDIT_SELECTION_COMMANDS: {sectionMarker: Commands.EDIT_SELECT_ALL},
EDIT_MODIFY_SELECTION: {sectionMarker: Commands.EDIT_INDENT},
EDIT_COMMENT_SELECTION: {sectionMarker: Commands.EDIT_LINE_COMMENT},
EDIT_CODE_HINTS_COMMANDS: {sectionMarker: Commands.SHOW_CODE_HINTS},
EDIT_TOGGLE_OPTIONS: {sectionMarker: Commands.TOGGLE_CLOSE_BRACKETS},
FIND_FIND_COMMANDS: {sectionMarker: Commands.CMD_FIND},
FIND_FIND_IN_COMMANDS: {sectionMarker: Commands.CMD_FIND_IN_FILES},
FIND_REPLACE_COMMANDS: {sectionMarker: Commands.CMD_REPLACE},
VIEW_HIDESHOW_COMMANDS: {sectionMarker: Commands.VIEW_HIDE_SIDEBAR},
VIEW_FONTSIZE_COMMANDS: {sectionMarker: Commands.VIEW_INCREASE_FONT_SIZE},
VIEW_TOGGLE_OPTIONS: {sectionMarker: Commands.TOGGLE_ACTIVE_LINE},
NAVIGATE_GOTO_COMMANDS: {sectionMarker: Commands.NAVIGATE_QUICK_OPEN},
NAVIGATE_DOCUMENTS_COMMANDS: {sectionMarker: Commands.NAVIGATE_NEXT_DOC},
NAVIGATE_OS_COMMANDS: {sectionMarker: Commands.NAVIGATE_SHOW_IN_FILE_TREE},
NAVIGATE_QUICK_EDIT_COMMANDS: {sectionMarker: Commands.TOGGLE_QUICK_EDIT},
NAVIGATE_QUICK_DOCS_COMMANDS: {sectionMarker: Commands.TOGGLE_QUICK_DOCS}
};
Error Codes from Brackets Shell
var NO_ERROR = 0,
ERR_UNKNOWN = 1,
ERR_INVALID_PARAMS = 2,
ERR_NOT_FOUND = 3;
Maps contextMenuID's to ContextMenu objects
var contextMenuMap = {};
Check whether a ContextMenu exists for the given id.
function _isContextMenu(id) {
return !!getContextMenu(id);
}
function _isHTMLMenu(id) {
return (!brackets.nativeMenus || _isContextMenu(id));
}
Adds a top-level menu to the application menu bar which may be native or HTML-based.
function addMenu(name, id, position, relativeID) {
name = _.escape(name);
var $menubar = $("#titlebar .nav"),
menu;
if (!name || !id) {
console.error("call to addMenu() is missing required parameters");
return null;
}
// Guard against duplicate menu ids
if (menuMap[id]) {
console.log("Menu added with same name and id of existing Menu: " + id);
return null;
}
menu = new Menu(id);
menuMap[id] = menu;
if (!_isHTMLMenu(id)) {
brackets.app.addMenu(name, id, position, relativeID, function (err) {
switch (err) {
case NO_ERROR:
// Make sure name is up to date
brackets.app.setMenuTitle(id, name, function (err) {
if (err) {
console.error("setMenuTitle() -- error: " + err);
}
});
break;
case ERR_UNKNOWN:
console.error("addMenu(): Unknown Error when adding the menu " + id);
break;
case ERR_INVALID_PARAMS:
console.error("addMenu(): Invalid Parameters when adding the menu " + id);
break;
case ERR_NOT_FOUND:
console.error("addMenu(): Menu with command " + relativeID + " could not be found when adding the menu " + id);
break;
default:
console.error("addMenu(): Unknown Error (" + err + ") when adding the menu " + id);
}
});
return menu;
}
var $toggle = $("<a href='#' class='dropdown-toggle' data-toggle='dropdown'>" + name + "</a>"),
$popUp = $("<ul class='dropdown-menu'></ul>"),
$newMenu = $("<li class='dropdown' id='" + id + "'></li>").append($toggle).append($popUp);
// Insert menu
var $relativeElement = relativeID && $(_getHTMLMenu(relativeID));
_insertInList($menubar, $newMenu, position, $relativeElement);
// Install ESC key handling
PopUpManager.addPopUp($popUp, closeAll, false);
// todo error handling
return menu;
}
Closes all menus that are open
function closeAll() {
$(".dropdown").removeClass("open");
}
Retrieves the map of all Menu objects.
function getAllMenus() {
return menuMap;
}
Retrieves the ContextMenu object for the corresponding id.
function getContextMenu(id) {
return contextMenuMap[id];
}
Retrieves the Menu object for the corresponding id.
function getMenu(id) {
return menuMap[id];
}
Retrieves the MenuItem object for the corresponding id.
function getMenuItem(id) {
return menuItemMap[id];
}
function _getHTMLMenu(id) {
return $("#" + StringUtils.jQueryIdEscape(id)).get(0);
}
function _getHTMLMenuItem(id) {
return $("#" + StringUtils.jQueryIdEscape(id)).get(0);
}
function _addKeyBindingToMenuItem($menuItem, key, displayKey) {
var $shortcut = $menuItem.find(".menu-shortcut");
if ($shortcut.length === 0) {
$shortcut = $("<span class='menu-shortcut' />");
$menuItem.append($shortcut);
}
$shortcut.data("key", key);
$shortcut.text(KeyBindingManager.formatKeyDescriptor(displayKey));
}
function _addExistingKeyBinding(menuItem) {
var bindings = KeyBindingManager.getKeyBindings(menuItem.getCommand().getID()),
binding = null;
if (bindings.length > 0) {
// add the latest key binding
binding = bindings[bindings.length - 1];
_addKeyBindingToMenuItem($(_getHTMLMenuItem(menuItem.id)), binding.key, binding.displayKey);
}
return binding;
}
var _menuDividerIDCount = 1;
function _getNextMenuItemDividerID() {
return "brackets-menuDivider-" + _menuDividerIDCount++;
}
// Help function for inserting elements into a list
function _insertInList($list, $element, position, $relativeElement) {
// Determine where to insert. Default is LAST.
var inserted = false;
if (position) {
// Adjust relative position for menu section positions since $relativeElement
// has already been resolved by _getRelativeMenuItem() to a menuItem
if (position === FIRST_IN_SECTION) {
position = BEFORE;
} else if (position === LAST_IN_SECTION) {
position = AFTER;
}
if (position === FIRST) {
$list.prepend($element);
inserted = true;
} else if ($relativeElement && $relativeElement.length > 0) {
if (position === AFTER) {
$relativeElement.after($element);
inserted = true;
} else if (position === BEFORE) {
$relativeElement.before($element);
inserted = true;
}
}
}
// Default to LAST
if (!inserted) {
$list.append($element);
}
}
Registers new context menu with Brackets.
Extensions should generally use the predefined context menus built into Brackets. Use this API to add a new context menu to UI that is specific to an extension.
After registering a new context menu clients should:
- use addMenuItem() to add items to the context menu
- call open() to show the context menu.
For example:
$("#my_ID").contextmenu(function (e) {
if (e.which === 3) {
my_cmenu.open(e);
}
});
To make menu items be contextual to things like selection, listen for the "beforeContextMenuOpen" to make changes to Command objects before the context menu is shown. MenuItems are views of Commands, which control a MenuItem's name, enabled state, and checked state.
function registerContextMenu(id) {
if (!id) {
console.error("call to registerContextMenu() is missing required parameters");
return null;
}
// Guard against duplicate menu ids
if (contextMenuMap[id]) {
console.log("Context Menu added with same name and id of existing Context Menu: " + id);
return null;
}
var cmenu = new ContextMenu(id);
contextMenuMap[id] = cmenu;
return cmenu;
}
// Deprecated menu ids
DeprecationWarning.deprecateConstant(ContextMenuIds, "WORKING_SET_MENU", "WORKING_SET_CONTEXT_MENU");
DeprecationWarning.deprecateConstant(ContextMenuIds, "WORKING_SET_SETTINGS_MENU", "WORKING_SET_CONFIG_MENU");
// Define public API
exports.AppMenuBar = AppMenuBar;
exports.ContextMenuIds = ContextMenuIds;
exports.MenuSection = MenuSection;
exports.BEFORE = BEFORE;
exports.AFTER = AFTER;
exports.LAST = LAST;
exports.FIRST = FIRST;
exports.FIRST_IN_SECTION = FIRST_IN_SECTION;
exports.LAST_IN_SECTION = LAST_IN_SECTION;
exports.DIVIDER = DIVIDER;
exports.getMenu = getMenu;
exports.getAllMenus = getAllMenus;
exports.getMenuItem = getMenuItem;
exports.getContextMenu = getContextMenu;
exports.addMenu = addMenu;
exports.removeMenu = removeMenu;
exports.registerContextMenu = registerContextMenu;
exports.closeAll = closeAll;
exports.Menu = Menu;
exports.MenuItem = MenuItem;
exports.ContextMenu = ContextMenu;
});
Removes a top-level menu from the application menu bar which may be native or HTML-based.
function removeMenu(id) {
var menu,
commandID = "";
if (!id) {
console.error("removeMenu(): missing required parameter: id");
return;
}
if (!menuMap[id]) {
console.error("removeMenu(): menu id not found: %s", id);
return;
}
// Remove all of the menu items in the menu
menu = getMenu(id);
_.forEach(menuItemMap, function (value, key) {
if (_.startsWith(key, id)) {
if (value.isDivider) {
menu.removeMenuDivider(key);
} else {
commandID = value.getCommand();
menu.removeMenuItem(commandID);
}
}
});
if (_isHTMLMenu(id)) {
$(_getHTMLMenu(id)).remove();
} else {
brackets.app.removeMenu(id, function (err) {
if (err) {
console.error("removeMenu() -- id not found: " + id + " (error: " + err + ")");
}
});
}
delete menuMap[id];
}
Removes the attached event listeners from the corresponding object.
function removeMenuItemEventListeners(menuItem) {
menuItem._command
.off("enabledStateChange", menuItem._enabledChanged)
.off("checkedStateChange", menuItem._checkedChanged)
.off("nameChange", menuItem._nameChanged)
.off("keyBindingAdded", menuItem._keyBindingAdded)
.off("keyBindingRemoved", menuItem._keyBindingRemoved);
}
MenuItem represents a single menu item that executes a Command or a menu divider. MenuItems may have a sub-menu. A MenuItem may correspond to an HTML-based menu item or a native menu item if Brackets is running in a native application shell
Since MenuItems may have a native implementation clients should create MenuItems through addMenuItem() and should NOT construct a MenuItem object directly. Clients should also not access HTML content of a menu directly and instead use the MenuItem API to query and modify menus items.
MenuItems are views on to Command objects so modify the underlying Command to modify the name, enabled, and checked state of a MenuItem. The MenuItem will update automatically
function MenuItem(id, command) {
this.id = id;
this.isDivider = (command === DIVIDER);
this.isNative = false;
if (!this.isDivider && command !== SUBMENU) {
// Bind event handlers
this._enabledChanged = this._enabledChanged.bind(this);
this._checkedChanged = this._checkedChanged.bind(this);
this._nameChanged = this._nameChanged.bind(this);
this._keyBindingAdded = this._keyBindingAdded.bind(this);
this._keyBindingRemoved = this._keyBindingRemoved.bind(this);
this._command = command;
this._command
.on("enabledStateChange", this._enabledChanged)
.on("checkedStateChange", this._checkedChanged)
.on("nameChange", this._nameChanged)
.on("keyBindingAdded", this._keyBindingAdded)
.on("keyBindingRemoved", this._keyBindingRemoved);
}
}
Synchronizes MenuItem checked state with underlying Command checked state
MenuItem.prototype._checkedChanged = function () {
var checked = !!this._command.getChecked();
if (this.isNative) {
var enabled = !!this._command.getEnabled();
brackets.app.setMenuItemState(this._command.getID(), enabled, checked, function (err) {
if (err) {
console.log("Error setting menu item state: " + err);
}
});
} else {
ViewUtils.toggleClass($(_getHTMLMenuItem(this.id)), "checked", checked);
}
};
Synchronizes MenuItem enabled state with underlying Command enabled state
MenuItem.prototype._enabledChanged = function () {
if (this.isNative) {
var enabled = !!this._command.getEnabled();
var checked = !!this._command.getChecked();
brackets.app.setMenuItemState(this._command.getID(), enabled, checked, function (err) {
if (err) {
console.log("Error setting menu item state: " + err);
}
});
} else {
ViewUtils.toggleClass($(_getHTMLMenuItem(this.id)), "disabled", !this._command.getEnabled());
}
};
MenuItem.prototype._keyBindingAdded = function (event, keyBinding) {
if (this.isNative) {
var shortcutKey = keyBinding.displayKey || keyBinding.key;
brackets.app.setMenuItemShortcut(this._command.getID(), shortcutKey, KeyBindingManager.formatKeyDescriptor(shortcutKey), function (err) {
if (err) {
console.error("Error setting menu item shortcut: " + err);
}
});
} else {
_addKeyBindingToMenuItem($(_getHTMLMenuItem(this.id)), keyBinding.key, keyBinding.displayKey);
}
};
MenuItem.prototype._keyBindingRemoved = function (event, keyBinding) {
if (this.isNative) {
brackets.app.setMenuItemShortcut(this._command.getID(), "", "", function (err) {
if (err) {
console.error("Error setting menu item shortcut: " + err);
}
});
} else {
var $shortcut = $(_getHTMLMenuItem(this.id)).find(".menu-shortcut");
if ($shortcut.length > 0 && $shortcut.data("key") === keyBinding.key) {
// check for any other bindings
if (_addExistingKeyBinding(this) === null) {
$shortcut.empty();
}
}
}
};
Synchronizes MenuItem name with underlying Command name
MenuItem.prototype._nameChanged = function () {
if (this.isNative) {
brackets.app.setMenuTitle(this._command.getID(), this._command.getName(), function (err) {
if (err) {
console.log("Error setting menu title: " + err);
}
});
} else {
$(_getHTMLMenuItem(this.id)).find(".menu-name").text(this._command.getName());
}
};
Menu represents a top-level menu in the menu bar. A Menu may correspond to an HTML-based menu or a native menu if Brackets is running in a native application shell.
Since menus may have a native implementation clients should create Menus through addMenu() and should NOT construct a Menu object directly. Clients should also not access HTML content of a menu directly and instead use the Menu API to query and modify menus.
function Menu(id) {
this.id = id;
}
Menu.prototype._getMenuItemId = function (commandId) {
return (this.id + "-" + commandId);
};
Determine MenuItem in this Menu, that has the specified command
Menu.prototype._getMenuItemForCommand = function (command) {
if (!command) {
return null;
}
var foundMenuItem = menuItemMap[this._getMenuItemId(command.getID())];
if (!foundMenuItem) {
return null;
}
return $(_getHTMLMenuItem(foundMenuItem.id)).closest("li");
};
Determine relative MenuItem
Menu.prototype._getRelativeMenuItem = function (relativeID, position) {
var $relativeElement;
if (relativeID) {
if (position === FIRST_IN_SECTION || position === LAST_IN_SECTION) {
if (!relativeID.hasOwnProperty("sectionMarker")) {
console.error("Bad Parameter in _getRelativeMenuItem(): relativeID must be a MenuSection when position refers to a menu section");
return null;
}
// Determine the $relativeElement by traversing the sibling list and
// stop at the first divider found
// TODO: simplify using nextUntil()/prevUntil()
var $sectionMarker = this._getMenuItemForCommand(CommandManager.get(relativeID.sectionMarker));
if (!$sectionMarker) {
console.error("_getRelativeMenuItem(): MenuSection " + relativeID.sectionMarker +
" not found in Menu " + this.id);
return null;
}
var $listElem = $sectionMarker;
$relativeElement = $listElem;
while (true) {
$listElem = (position === FIRST_IN_SECTION ? $listElem.prev() : $listElem.next());
if ($listElem.length === 0) {
break;
} else if ($listElem.find(".divider").length > 0) {
break;
} else {
$relativeElement = $listElem;
}
}
} else {
if (relativeID.hasOwnProperty("sectionMarker")) {
console.error("Bad Parameter in _getRelativeMenuItem(): if relativeID is a MenuSection, position must be FIRST_IN_SECTION or LAST_IN_SECTION");
return null;
}
// handle FIRST, LAST, BEFORE, & AFTER
var command = CommandManager.get(relativeID);
if (command) {
// Lookup Command for this Command id
// Find MenuItem that has this command
$relativeElement = this._getMenuItemForCommand(command);
}
if (!$relativeElement) {
console.error("_getRelativeMenuItem(): MenuItem with Command id " + relativeID +
" not found in Menu " + this.id);
return null;
}
}
return $relativeElement;
} else if (position && position !== FIRST && position !== LAST) {
console.error("Bad Parameter in _getRelativeMenuItem(): relative position specified with no relativeID");
return null;
}
return $relativeElement;
};
Inserts divider item in menu.
Menu.prototype.addMenuDivider = function (position, relativeID) {
return this.addMenuItem(DIVIDER, "", position, relativeID);
};
Adds a new menu item with the specified id and display text. The insertion position is specified via the relativeID and position arguments which describe a position relative to another MenuItem or MenuGroup. It is preferred that plug-ins insert new MenuItems relative to a menu section rather than a specific MenuItem (see Menu Section Constants).
TODO: Sub-menus are not yet supported, but when they are implemented this API will allow adding new MenuItems to sub-menus as well.
Note, keyBindings are bound to Command objects not MenuItems. The provided keyBindings will be bound to the supplied Command object rather than the MenuItem.
Menu.prototype.addMenuItem = function (command, keyBindings, position, relativeID) {
var menuID = this.id,
id,
$menuItem,
menuItem,
name,
commandID;
if (!command) {
console.error("addMenuItem(): missing required parameters: command");
return null;
}
if (typeof (command) === "string") {
if (command === DIVIDER) {
name = DIVIDER;
commandID = _getNextMenuItemDividerID();
} else {
commandID = command;
command = CommandManager.get(commandID);
if (!command) {
console.error("addMenuItem(): commandID not found: " + commandID);
return null;
}
name = command.getName();
}
} else {
commandID = command.getID();
name = command.getName();
}
// Internal id is the a composite of the parent menu id and the command id.
id = this._getMenuItemId(commandID);
if (menuItemMap[id]) {
console.log("MenuItem added with same id of existing MenuItem: " + id);
return null;
}
// create MenuItem
menuItem = new MenuItem(id, command);
menuItemMap[id] = menuItem;
// create MenuItem DOM
if (_isHTMLMenu(this.id)) {
if (name === DIVIDER) {
$menuItem = $("<li><hr class='divider' id='" + id + "' /></li>");
} else {
// Create the HTML Menu
$menuItem = $("<li><a href='#' id='" + id + "'> <span class='menu-name'></span></a></li>");
$menuItem.on("click", function () {
menuItem._command.execute();
});
var self = this;
$menuItem.on("mouseenter", function () {
self.closeSubMenu();
});
}
// Insert menu item
var $relativeElement = this._getRelativeMenuItem(relativeID, position);
_insertInList($("li#" + StringUtils.jQueryIdEscape(this.id) + " > ul.dropdown-menu"),
$menuItem, position, $relativeElement);
} else {
var bindings = KeyBindingManager.getKeyBindings(commandID),
binding,
bindingStr = "",
displayStr = "";
if (bindings && bindings.length > 0) {
binding = bindings[bindings.length - 1];
bindingStr = binding.displayKey || binding.key;
}
if (bindingStr.length > 0) {
displayStr = KeyBindingManager.formatKeyDescriptor(bindingStr);
}
if (position === FIRST_IN_SECTION || position === LAST_IN_SECTION) {
if (!relativeID.hasOwnProperty("sectionMarker")) {
console.error("Bad Parameter in _getRelativeMenuItem(): relativeID must be a MenuSection when position refers to a menu section");
return null;
}
// For sections, pass in the marker for that section.
relativeID = relativeID.sectionMarker;
}
brackets.app.addMenuItem(this.id, name, commandID, bindingStr, displayStr, position, relativeID, function (err) {
switch (err) {
case NO_ERROR:
break;
case ERR_INVALID_PARAMS:
console.error("addMenuItem(): Invalid Parameters when adding the command " + commandID);
break;
case ERR_NOT_FOUND:
console.error("_getRelativeMenuItem(): MenuItem with Command id " + relativeID + " not found in the Menu " + menuID);
break;
default:
console.error("addMenuItem(); Unknown Error (" + err + ") when adding the command " + commandID);
}
});
menuItem.isNative = true;
}
// Initialize MenuItem state
if (menuItem.isDivider) {
menuItem.dividerId = commandID;
} else {
if (keyBindings) {
// Add key bindings. The MenuItem listens to the Command object to update MenuItem DOM with shortcuts.
if (!Array.isArray(keyBindings)) {
keyBindings = [keyBindings];
}
}
// Note that keyBindings passed during MenuItem creation take precedent over any existing key bindings
KeyBindingManager.addBinding(commandID, keyBindings);
// Look for existing key bindings
_addExistingKeyBinding(menuItem);
menuItem._checkedChanged();
menuItem._enabledChanged();
menuItem._nameChanged();
}
return menuItem;
};
Creates a new submenu and a menuItem and adds the menuItem of the submenu to the menu and returns the submenu.
A submenu will have the same structure of a menu with a additional field parentMenuItem which has the reference of the submenu's parent menuItem.
A submenu will raise the following events:
Note, This function will create only a context submenu.
TODO: Make this function work for Menus
Menu.prototype.addSubMenu = function (name, id, position, relativeID) {
if (!name || !id) {
console.error("addSubMenu(): missing required parameters: name and id");
return null;
}
// Guard against duplicate context menu ids
if (contextMenuMap[id]) {
console.log("Context menu added with id of existing Context Menu: " + id);
return null;
}
var menu = new ContextMenu(id);
contextMenuMap[id] = menu;
var menuItemID = this.id + "-" + id;
if (menuItemMap[menuItemID]) {
console.log("MenuItem added with same id of existing MenuItem: " + id);
return null;
}
// create MenuItem
var menuItem = new MenuItem(menuItemID, SUBMENU);
menuItemMap[menuItemID] = menuItem;
menu.parentMenuItem = menuItem;
// create MenuItem DOM
if (_isHTMLMenu(this.id)) {
// Create the HTML MenuItem
var $menuItem = $("<li><a href='#' id='" + menuItemID + "'> " +
"<span class='menu-name'>" + name + "</span>" +
"<span style='float: right'>▸</span>" +
"</a></li>");
var self = this;
$menuItem.on("mouseenter", function(e) {
if (self.openSubMenu && self.openSubMenu.id === menu.id) {
return;
}
self.closeSubMenu();
self.openSubMenu = menu;
menu.open();
});
// Insert menu item
var $relativeElement = this._getRelativeMenuItem(relativeID, position);
_insertInList($("li#" + StringUtils.jQueryIdEscape(this.id) + " > ul.dropdown-menu"),
$menuItem, position, $relativeElement);
} else {
// TODO: add submenus for native menus
}
return menu;
};
Closes the submenu if the menu has a submenu open.
Menu.prototype.closeSubMenu = function() {
if (this.openSubMenu) {
this.openSubMenu.close();
this.openSubMenu = null;
}
};
Removes the specified menu divider from this Menu.
Menu.prototype.removeMenuDivider = function (menuItemID) {
var menuItem,
$HTMLMenuItem;
if (!menuItemID) {
console.error("removeMenuDivider(): missing required parameters: menuItemID");
return;
}
menuItem = getMenuItem(menuItemID);
if (!menuItem) {
console.error("removeMenuDivider(): parameter menuItemID: %s is not a valid menu item id", menuItemID);
return;
}
if (!menuItem.isDivider) {
console.error("removeMenuDivider(): parameter menuItemID: %s is not a menu divider", menuItemID);
return;
}
if (_isHTMLMenu(this.id)) {
// Targeting parent to get the menu divider <hr> and the <li> that contains it
$HTMLMenuItem = $(_getHTMLMenuItem(menuItemID)).parent();
if ($HTMLMenuItem) {
$HTMLMenuItem.remove();
} else {
console.error("removeMenuDivider(): HTML menu divider not found: %s", menuItemID);
return;
}
} else {
brackets.app.removeMenuItem(menuItem.dividerId, function (err) {
if (err) {
console.error("removeMenuDivider() -- divider not found: %s (error: %s)", menuItemID, err);
}
});
}
if (!menuItemMap[menuItemID]) {
console.error("removeMenuDivider(): menu divider not found in menuItemMap: %s", menuItemID);
return;
}
delete menuItemMap[menuItemID];
};
Removes the specified menu item from this Menu. Key bindings are unaffected; use KeyBindingManager directly to remove key bindings if desired.
Menu.prototype.removeMenuItem = function (command) {
var menuItemID,
commandID;
if (!command) {
console.error("removeMenuItem(): missing required parameters: command");
return;
}
if (typeof (command) === "string") {
var commandObj = CommandManager.get(command);
if (!commandObj) {
console.error("removeMenuItem(): command not found: " + command);
return;
}
commandID = command;
} else {
commandID = command.getID();
}
menuItemID = this._getMenuItemId(commandID);
var menuItem = getMenuItem(menuItemID);
removeMenuItemEventListeners(menuItem);
if (_isHTMLMenu(this.id)) {
// Targeting parent to get the menu item <a> and the <li> that contains it
$(_getHTMLMenuItem(menuItemID)).parent().remove();
} else {
brackets.app.removeMenuItem(commandID, function (err) {
if (err) {
console.error("removeMenuItem() -- command not found: " + commandID + " (error: " + err + ")");
}
});
}
delete menuItemMap[menuItemID];
};
Removes the specified submenu from this Menu.
Note, this function will only remove context submenus
TODO: Make this function work for Menus
Menu.prototype.removeSubMenu = function (subMenuID) {
var subMenu,
parentMenuItem,
commandID = "";
if (!subMenuID) {
console.error("removeSubMenu(): missing required parameters: subMenuID");
return;
}
subMenu = getContextMenu(subMenuID);
if (!subMenu || !subMenu.parentMenuItem) {
console.error("removeSubMenu(): parameter subMenuID: %s is not a valid submenu id", subMenuID);
return;
}
parentMenuItem = subMenu.parentMenuItem;
if (!menuItemMap[parentMenuItem.id]) {
console.error("removeSubMenu(): parent menuItem not found in menuItemMap: %s", parentMenuItem.id);
return;
}
// Remove all of the menu items in the submenu
_.forEach(menuItemMap, function (value, key) {
if (_.startsWith(key, subMenuID)) {
if (value.isDivider) {
subMenu.removeMenuDivider(key);
} else {
commandID = value.getCommand();
subMenu.removeMenuItem(commandID);
}
}
});
if (_isHTMLMenu(this.id)) {
$(_getHTMLMenuItem(parentMenuItem.id)).parent().remove(); // remove the menu item
$(_getHTMLMenu(subMenuID)).remove(); // remove the menu
} else {
// TODO: remove submenus for native menus
}
delete menuItemMap[parentMenuItem.id];
delete contextMenuMap[subMenuID];
};
Represents a context menu that can open at a specific location in the UI.
Clients should not create this object directly and should instead use registerContextMenu() to create new ContextMenu objects.
Context menus in brackets may be HTML-based or native so clients should not reach into the HTML and should instead manipulate ContextMenus through the API.
Events:
function ContextMenu(id) {
Menu.apply(this, arguments);
var $newMenu = $("<li class='dropdown context-menu' id='" + StringUtils.jQueryIdEscape(id) + "'></li>"),
$popUp = $("<ul class='dropdown-menu'></ul>"),
$toggle = $("<a href='#' class='dropdown-toggle' data-toggle='dropdown'></a>").hide();
// assemble the menu fragments
$newMenu.append($toggle).append($popUp);
// insert into DOM
$("#context-menu-bar > ul").append($newMenu);
var self = this;
PopUpManager.addPopUp($popUp,
function () {
self.close();
},
false);
// Listen to ContextMenu's beforeContextMenuOpen event to first close other popups
PopUpManager.listenToContextMenu(this);
}
ContextMenu.prototype = Object.create(Menu.prototype);
ContextMenu.prototype.constructor = ContextMenu;
ContextMenu.prototype.parentClass = Menu.prototype;
EventDispatcher.makeEventDispatcher(ContextMenu.prototype);
Associate a context menu to a DOM element. This static function take care of registering event handlers for the click event listener and passing the right "position" object to the Context#open method
ContextMenu.assignContextMenuToSelector = function (selector, cmenu) {
$(selector).on("click", function (e) {
var buttonOffset,
buttonHeight;
e.stopPropagation();
if (cmenu.isOpen()) {
cmenu.close();
} else {
buttonOffset = $(this).offset();
buttonHeight = $(this).outerHeight();
cmenu.open({
pageX: buttonOffset.left,
pageY: buttonOffset.top + buttonHeight
});
}
});
};
Closes the context menu.
ContextMenu.prototype.close = function () {
if (this.parentMenuItem) {
this.trigger("beforeSubMenuClose");
} else {
this.trigger("beforeContextMenuClose");
}
this.closeSubMenu();
$("#" + StringUtils.jQueryIdEscape(this.id)).removeClass("open");
};
Detect if current context menu is already open
ContextMenu.prototype.isOpen = function () {
return $("#" + StringUtils.jQueryIdEscape(this.id)).hasClass("open");
};
Displays the ContextMenu at the specified location and dispatches the "beforeContextMenuOpen" event or "beforeSubMenuOpen" event (for submenus). The menu location may be adjusted to prevent clipping by the browser window. All other menus and ContextMenus will be closed before a new menu will be closed before a new menu is shown (if the new menu is not a submenu).
In case of submenus, the parentMenu of the submenu will not be closed when the sub menu is open.
ContextMenu.prototype.open = function (mouseOrLocation) {
if (!this.parentMenuItem &&
(!mouseOrLocation || !mouseOrLocation.hasOwnProperty("pageX") || !mouseOrLocation.hasOwnProperty("pageY"))) {
console.error("ContextMenu open(): missing required parameter");
return;
}
var $window = $(window),
escapedId = StringUtils.jQueryIdEscape(this.id),
$menuAnchor = $("#" + escapedId),
$menuWindow = $("#" + escapedId + " > ul"),
posTop,
posLeft;
// only show context menu if it has menu items
if ($menuWindow.children().length <= 0) {
return;
}
// adjust positioning so menu is not clipped off bottom or right
if (this.parentMenuItem) { // If context menu is a submenu
this.trigger("beforeSubMenuOpen");
var $parentMenuItem = $(_getHTMLMenuItem(this.parentMenuItem.id));
posTop = $parentMenuItem.offset().top;
posLeft = $parentMenuItem.offset().left + $parentMenuItem.outerWidth();
var elementRect = {
top: posTop,
left: posLeft,
height: $menuWindow.height() + 25,
width: $menuWindow.width()
},
clip = ViewUtils.getElementClipSize($window, elementRect);
if (clip.bottom > 0) {
posTop = Math.max(0, posTop + $parentMenuItem.height() - $menuWindow.height());
}
posTop -= 30; // shift top for hidden parent element
posLeft += 3;
if (clip.right > 0) {
posLeft = Math.max(0, posLeft - $parentMenuItem.outerWidth() - $menuWindow.outerWidth());
}
} else {
this.trigger("beforeContextMenuOpen");
// close all other dropdowns
closeAll();
posTop = mouseOrLocation.pageY;
posLeft = mouseOrLocation.pageX;
var elementRect = {
top: posTop,
left: posLeft,
height: $menuWindow.height() + 25,
width: $menuWindow.width()
},
clip = ViewUtils.getElementClipSize($window, elementRect);
if (clip.bottom > 0) {
posTop = Math.max(0, posTop - clip.bottom);
}
posTop -= 30; // shift top for hidden parent element
posLeft += 5;
if (clip.right > 0) {
posLeft = Math.max(0, posLeft - clip.right);
}
}
// open the context menu at final location
$menuAnchor.addClass("open")
.css({"left": posLeft, "top": posTop});
};