Skip to content

Commit d7d6f51

Browse files
marktriggsottenhoff
authored andcommitted
SAK-30530 Sakai Favorite Site Tool Dropdown Menu Enhancements (sakaiproject#2395)
* Show the tool dropdown for the active site as well. * Left align menus and adjust styles to match * Improve keyboard handling for tool menus * Close the active tool menu when clicking outside of the menu
1 parent d9403bd commit d7d6f51

File tree

2 files changed

+231
-144
lines changed

2 files changed

+231
-144
lines changed

reference/library/src/morpheus-master/js/src/sakai.morpheus.subnav.js

+163-138
Original file line numberDiff line numberDiff line change
@@ -2,181 +2,206 @@
22
* Sets up subnav on the sitenav
33
*/
44

5-
var setupSiteNav = function(){
5+
var closeAllDropdownMenus = function() {
6+
$PBJQ('.Mrphs-sitesNav__menuitem').removeClass('dropdown-is-visible');
7+
$PBJQ('.Mrphs-sitesNav__menuitem').find('.is-visible').removeClass('is-visible');
8+
$PBJQ('.Mrphs-sitesNav__menuitem').find('.is-clicked').removeClass('is-clicked');
9+
10+
$PBJQ('.sitenav-dropdown-overlay').remove();
11+
};
12+
13+
var buildDropdownMenu = function(container, siteId, callback) {
14+
var navsubmenu = "<ul class=\"Mrphs-sitesNav__submenu\" role=\"menu\">";
15+
var maxToolsInt = parseInt($PBJQ('#linkNav').attr('data-max-tools-int'));
16+
var maxToolsText = $PBJQ('#linkNav').attr('data-max-tools-anchor');
17+
var goToSite = '<li class=\"Mrphs-sitesNav__submenuitem\"><a role=\"menuitem\" href=\"' + portal.portalPath + '/site/' + siteId + '\" title=\"' + maxToolsText + '\"><span class=\"toolMenuIcon icon-sakai-see-all-tools\"></span>' + maxToolsText + '</a></li>';
18+
var siteURL = '/direct/site/' + siteId + '/pages.json';
19+
var currentSite = window.location.pathname.split('/').pop();
20+
21+
$PBJQ.ajax({
22+
url: siteURL,
23+
dataType: "json",
24+
success: function(data){
25+
26+
$PBJQ.each(data, function(i, item) {
27+
28+
// Check to see if this is the current tool in the site
29+
var isCurrent = "";
30+
if (currentSite == item.tools[0].id) {
31+
isCurrent = " is-current";
32+
}
633

7-
$PBJQ("ul.Mrphs-sitesNav__menu").each(function(){
34+
if (i <= maxToolsInt) {
35+
var li_template;
36+
37+
if (item.toolpopup) {
38+
var link_attrs = ' role="menuitem" href="{{tool_url}}?sakai.popup=yes" title="{{item_title}}" onclick="window.open(\'{{item_toolpopupurl}}\');"';
39+
li_template = '<li class="Mrphs-sitesNav__submenuitem" >' +
40+
'<a class="Mrphs-sitesNav__submenuitem-icon"' + link_attrs + '><span class="toolMenuIcon icon-{{icon}}"></span></a>' +
41+
'<a class="Mrphs-sitesNav__submenuitem-title"' + link_attrs + '>{{item_title}}</a>' +
42+
'</li>';
43+
} else {
44+
var link_attrs = ' role="menuitem" href="{{tool_url}}" title="{{item_title}}"';
45+
46+
li_template = '<li class="Mrphs-sitesNav__submenuitem{{is_current}}">' +
47+
'<a class="Mrphs-sitesNav__submenuitem-icon"' + link_attrs + '><span class="toolMenuIcon icon-{{icon}}"></span></a>' +
48+
'<a class="Mrphs-sitesNav__submenuitem-title"' + link_attrs + '>{{item_title}}</a>' +
49+
'</li>';
50+
}
851

9-
// Add an escape key handler to slide the page menu up
10-
$PBJQ(this).keydown(function(e) {
11-
if (e.keyCode == 27) {
12-
$PBJQ(this).parent().children('a').focus();
13-
$PBJQ(this).toggleClass('is-visible');
14-
}
15-
});
52+
navsubmenu += (li_template
53+
.replace(/{{tool_url}}/g, item.tools[0].url)
54+
.replace(/{{item_title}}/g, item.title)
55+
.replace(/{{item_toolpopupurl}}/g, item.toolpopupurl)
56+
.replace(/{{icon}}/g, item.tools[0].toolId.replace(/\./gi, '-'))
57+
.replace(/{{is_current}}/g, isCurrent));
58+
}
59+
});
1660

17-
$PBJQ(this).children('li:last').addClass('is-last-item')
18-
});
61+
if((data.length - 1) > maxToolsInt) {
62+
navsubmenu += goToSite
63+
}
1964

20-
$PBJQ("ul.Mrphs-sitesNav__menu > li").mouseleave(function(){
21-
$PBJQ(this).find('ul').toggleClass('is-visible');
22-
});
65+
navsubmenu += "</ul>"
2366

24-
fixTopNav = function(e) {
67+
navsubmenu = $(navsubmenu);
2568

26-
if (e.keyCode == 40) { // downarrow
27-
28-
e.preventDefault();
29-
$PBJQ('#selectSite').hide();
30-
//$PBJQ('.nav-submenu').hide();
31-
// Trigger click on the drop <span>, passing true to set focus on
32-
// the first tool in the dropdown.
33-
$PBJQ(this).parent().find(".Mrphs-sitesNav__dropdown").trigger('click',[true]);
69+
container.append(navsubmenu);
3470

35-
} else if (e.keyCode == 27) { // uparrow ? or ESC
71+
addArrowNavAndDisableTabNav(navsubmenu);
3672

37-
$PBJQ(this).parent().children('a').focus();
38-
$PBJQ(this).toggleClass('is-visible');
73+
callback(navsubmenu);
74+
},
3975

76+
error: function(XMLHttpRequest, status, error){
77+
// Something happened getting the tool list.
4078
}
41-
}
42-
43-
// SAK-25505 - Switch from live() to on()
44-
// $PBJQ( "a.offsite" ).live( "click", function() {
45-
// $PBJQ('.topnav > li.nav-menu > a').live('keydown', function(e){
46-
if($PBJQ(document).on) {
79+
});
80+
};
4781

48-
$PBJQ(document).on('keydown', '.Mrphs-sitesNav__menu > li.Mrphs-sitesNav__menuitem > a', fixTopNav);
4982

50-
} else {
83+
var setupSiteNav = function(){
5184

52-
$PBJQ('.Mrphs-sitesNav__menu > .Mrphs-sitesNav__menuitem > a').live('keydown', fixTopNav);
85+
$PBJQ("ul.Mrphs-sitesNav__menu").each(function(){
5386

54-
}
55-
56-
$PBJQ("ul.Mrphs-sitesNav__menu > li").mouseleave(function(){
57-
$PBJQ(this).find('ul').toggleClass('is-visible');
87+
// Add an escape key handler to slide the page menu up
88+
$PBJQ(this).keydown(function(e) {
89+
if (e.keyCode == 27) {
90+
closeAllDropdownMenus();
91+
}
92+
});
5893
});
94+
95+
$PBJQ(document).on('keydown', '.Mrphs-sitesNav__menu > li.Mrphs-sitesNav__menuitem > a',
96+
function (e) {
97+
if (e.keyCode == 40) {
98+
// downarrow
99+
e.preventDefault();
100+
// Trigger click on the drop <span>, passing true to set focus on
101+
// the first tool in the dropdown.
102+
var dropdown = $PBJQ(this).parent().find(".Mrphs-sitesNav__dropdown");
103+
104+
if (dropdown.data('clicked')) {
105+
// If the user has already triggered a click, give the
106+
// AJAX a chance to finish.
107+
} else {
108+
dropdown.data('clicked', true);
109+
dropdown.trigger('click', [true]);
110+
}
111+
} else if (e.keyCode == 27) {
112+
// escape
113+
e.preventDefault();
114+
closeAllDropdownMenus();
115+
}
116+
117+
});
118+
59119
// focusFirstLink is only ever passed from the keydown handler. We
60120
// don't want to focus on click; it looks odd.
61-
62121
$PBJQ("ul.Mrphs-sitesNav__menu li span.Mrphs-sitesNav__dropdown").click(function(e, focusFirstLink) {
122+
e.preventDefault()
63123

64-
/*
65-
* see if there is a menu sibling
66-
* if there is a child, display it
67-
* if no menu sibling
68-
* retrieve data, construct the menu, append
69-
*/
124+
var jqObjDrop = $PBJQ(e.target);
125+
var container = jqObjDrop.closest('.Mrphs-sitesNav__menuitem');
70126

71-
$PBJQ(this).toggleClass("is-clicked"); //On click toggle class "is-clicked"
72-
e.preventDefault()
127+
var dropdownWasShown = container.hasClass('dropdown-is-visible');
73128

74-
var jqObjDrop = $PBJQ(e.target);
129+
// Hide any currently shown menus so we don't end up with multiple dropdowns shown
130+
closeAllDropdownMenus();
75131

76-
if(jqObjDrop.next('ul').length) {
77-
jqObjDrop.next('ul').toggleClass('is-visible');
78-
79-
if(focusFirstLink) {
80-
jqObjDrop.next('ul').find("a:first").focus();
132+
if (dropdownWasShown) {
133+
// We've hidden the dropdown now, so all done.
134+
return;
81135
}
82136

83-
}
137+
var dropdownArrow = $PBJQ(this);
84138

85-
else {
139+
var displayDropdown = function (navsubmenu) {
140+
// Mark the dropdown arrow and the menu itself as clicked
141+
dropdownArrow.addClass("is-clicked");
142+
container.addClass('dropdown-is-visible');
86143

87-
var navsubmenu = "<ul class=\"Mrphs-sitesNav__submenu is-visible\" role=\"menu\">";
88-
var siteId = jqObjDrop.attr('data-site-id');
89-
var maxToolsInt = parseInt($PBJQ('#linkNav').attr('data-max-tools-int'));
90-
var maxToolsText = $PBJQ('#linkNav').attr('data-max-tools-anchor');
91-
var goToSite = '<li class=\"Mrphs-sitesNav__submenuitem\"><a role=\"menuitem\" href=\"' + portal.portalPath + '/site/' + siteId + '\" title=\"' + maxToolsText + '\"><span class=\"toolMenuIcon icon-sakai-see-all-tools\"></span>' + maxToolsText + '</a></li>';
92-
var siteURL = '/direct/site/' + jqObjDrop.attr('data-site-id') + '/pages.json';
93-
var currentSite = window.location.pathname.split('/').pop();
144+
// now display the menu
145+
navsubmenu.addClass('is-visible');
94146

95-
$PBJQ.ajax({
96-
url: siteURL,
97-
dataType: "json",
98-
success: function(data){
147+
if(focusFirstLink) {
148+
container.find('a.Mrphs-sitesNav__submenuitem-title').first().focus();
149+
}
99150

100-
$PBJQ.each(data, function(i, item) {
151+
// Add an invisible overlay to allow clicks to close the dropdown
152+
var overlay = $('<div class="sitenav-dropdown-overlay" />');
101153

102-
if (i <= maxToolsInt) {
103-
104-
if (item.toolpopup) {
105-
navsubmenu = navsubmenu + '<li class=\"Mrphs-sitesNav__submenuitem\" ><a role=\"menuitem\" href=\"' + item.tools[0].url + "?sakai.popup=yes\" title=\"" + item.title + "\" onclick=\"window.open('" + item.toolpopupurl + "');\"><span class=\"toolMenuIcon icon-" + item.tools[0].toolId.replace(/\./gi, '-') + "\"></span>" + item.title + "</a></li>";
154+
overlay.on('click', function (e) {
155+
closeAllDropdownMenus();
156+
});
106157

107-
} else if (item.tools.length >= 1) { // changed from item.tools.length === 1
158+
$('body').prepend(overlay);
108159

109-
// Check to see if this is the current tool in the site
110-
var isCurrent = "";
111-
if (currentSite == item.tools[0].id) {
112-
var isCurrent = " is-current";
113-
}
114-
navsubmenu = navsubmenu + '<li class=\"Mrphs-sitesNav__submenuitem' + isCurrent + '\"><a role=\"menuitem\" href=\"' + item.tools[0].url + "\" title=\"" + item.title + "\"><span class=\"toolMenuIcon icon-" + item.tools[0].toolId.replace(/\./gi, '-') + "\"></span>" + item.title + "</a></li>";
115-
}
116-
}
117-
});
160+
dropdownArrow.removeData('clicked');
161+
};
118162

119-
if((data.length - 1) > maxToolsInt) {
120-
navsubmenu = navsubmenu + goToSite
121-
}
122-
123-
navsubmenu = navsubmenu + "</ul>"
124-
125-
jqObjDrop.after(navsubmenu);
126-
127-
if(focusFirstLink) {
128-
jqObjDrop.next('ul').find("a:first").focus();
129-
}
163+
if (!container.find('ul').length) {
164+
// We haven't yet built the dropdown menu for this item. Do that now.
165+
buildDropdownMenu(container, jqObjDrop.attr('data-site-id'), displayDropdown);
166+
} else {
167+
displayDropdown(container.find('ul'));
168+
}
130169

131-
addArrowNavAndDisableTabNav($PBJQ(".Mrphs-sitesNav__submenu"));
170+
}).hover(function(){
171+
$PBJQ(this).toggleClass("Mrphs-sitesNav__dropdown--hover"); //On hover over, add
172+
});
173+
}
132174

133-
},
175+
/* Callback is a function and is called after sliding up ul */
176+
function addArrowNavAndDisableTabNav(ul) {
177+
ul.find('li a').attr('tabindex','-1').keydown(function (e) {
178+
var obj = $PBJQ(e.target);
179+
if (e.keyCode == 40) {
180+
// Down arrow. Move to the next item, or loop around if we're at the bottom.
181+
e.preventDefault();
182+
var next = obj.closest('li').next();
134183

135-
error: function(XMLHttpRequest, status, error){
136-
// Something happened getting the tool list.
184+
if (next.length === 0 || next.find('a.Mrphs-sitesNav__submenuitem-title').length == 0) {
185+
// loop around
186+
next = ul.find('li').first();
137187
}
138-
});
139-
}
140188

141-
}).hover(function(){
142-
$PBJQ(this).toggleClass("Mrphs-sitesNav__dropdown--hover"); //On hover over, add
143-
});
189+
next.find('a.Mrphs-sitesNav__submenuitem-title').focus();
144190

145-
}
191+
} else if (e.keyCode == 38) {
192+
// Up arrow. Move to the previous item, or loop around if we're at the top.
193+
e.preventDefault();
194+
var prev = obj.closest('li').prev();
146195

147-
/* Callback is a function and is called after sliding up ul */
148-
function addArrowNavAndDisableTabNav(ul,callback) {
149-
ul.find('li a').attr('tabindex','-1').keydown(function (e) {
150-
var obj = $PBJQ(e.target);
151-
if(e.keyCode == 40) {
152-
e.preventDefault();
153-
var next = obj.parent().parent().next();
154-
if(next[0] === undefined) {
155-
ul.slideUp('fast');
156-
if(callback !== undefined) {
157-
callback();
158-
} else {
159-
obj.parent().parent().parent().parent().children('a').focus();
160-
}
161-
} else {
162-
next.find('a').focus();
163-
}
164-
} else if(e.keyCode == 9) { // Suck up the menu if tab is pressed
165-
ul.slideUp('fast');
166-
} else if(e.keyCode == 38) {
167-
// Up arrow
168-
e.preventDefault();
169-
var prev = obj.parent().parent().prev();
170-
if(prev[0] === undefined) {
171-
ul.slideUp('fast');
172-
if(callback !== undefined) {
173-
callback();
174-
} else {
175-
obj.parent().parent().parent().parent().children('a').focus();
176-
}
177-
} else {
178-
prev.find('a').focus();
179-
}
180-
}
181-
});
196+
if (prev.length === 0) {
197+
// jump to the bottom
198+
prev = ul.find('a.Mrphs-sitesNav__submenuitem-title').closest('ul')
199+
}
200+
201+
prev.find('a.Mrphs-sitesNav__submenuitem-title').focus();
202+
203+
} else if (e.keyCode == 9) { // Suck up the menu if tab is pressed
204+
closeAllDropdownMenus();
205+
}
206+
});
182207
}

0 commit comments

Comments
 (0)