@@ -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'>▸</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