1 // https://civicrm.org/licensing
4 var templates
, initialized
,
7 CRM
.menubar
= _
.extend({
9 settings
: {collapsibleBehavior
: 'accordion'},
10 position
: 'over-cms-menu',
12 attachTo
: (CRM
.menubar
&& CRM
.menubar
.position
=== 'above-crm-container') ? '#crm-container' : 'body',
13 initialize: function() {
14 var cache
= CRM
.cache
.get('menubar');
15 if (cache
&& cache
.code
=== CRM
.menubar
.cacheCode
&& cache
.locale
=== CRM
.config
.locale
&& cache
.cid
=== CRM
.config
.cid
&& localStorage
.civiMenubar
) {
16 CRM
.menubar
.data
= cache
.data
;
17 insert(localStorage
.civiMenubar
);
19 $.getJSON(CRM
.url('civicrm/ajax/navmenu', {code
: CRM
.menubar
.cacheCode
, locale
: CRM
.config
.locale
, cid
: CRM
.config
.cid
}))
20 .done(function(data
) {
21 var markup
= getTpl('tree')(data
);
22 CRM
.cache
.set('menubar', {code
: CRM
.menubar
.cacheCode
, locale
: CRM
.config
.locale
, cid
: CRM
.config
.cid
, data
: data
});
23 CRM
.menubar
.data
= data
;
24 localStorage
.setItem('civiMenubar', markup
);
29 // Wait for crm-container present on the page as it's faster than document.ready
30 function insert(markup
) {
31 if ($('#crm-container').length
) {
34 new MutationObserver(function(mutations
, observer
) {
35 _
.each(mutations
, function(mutant
) {
36 _
.each(mutant
.addedNodes
, function(node
) {
37 if ($(node
).is('#crm-container')) {
39 observer
.disconnect();
43 }).observe(document
, {childList
: true, subtree
: true});
47 function render(markup
) {
48 var position
= CRM
.menubar
.attachTo
=== 'body' ? 'beforeend' : 'afterbegin';
49 $(CRM
.menubar
.attachTo
)[0].insertAdjacentHTML(position
, markup
);
50 CRM
.menubar
.initializePosition();
51 $('#civicrm-menu').trigger('crmLoad');
52 $(document
).ready(function() {
55 .on('click', 'a[href="#"]', function() {
56 // For empty links - keep the menu open and don't jump the page anchor
59 .on('click', 'a:not([href^="#"])', function(e
) {
60 if (e
.ctrlKey
|| e
.altKey
|| e
.shiftKey
|| e
.metaKey
) {
61 // Prevent menu closing when link is clicked with a keyboard modifier.
65 .on('dragstart', function() {
66 // Stop user from accidentally dragging menu links
67 // This was added because a user noticed they could drag the civi icon into the quicksearch box.
70 .on('click', 'a[href="#hidemenu"]', function(e
) {
72 CRM
.menubar
.hide(250, true);
74 .on('keyup', 'a', function(e
) {
75 // Simulate a click when spacebar key is pressed
76 if (e
.which
== SPACE_KEY
) {
77 $(e
.currentTarget
)[0].click();
80 .on('show.smapi', function(e
, menu
) {
81 // Focus menu when opened with an accesskey
82 if ($(menu
).parent().data('name') === 'Home') {
83 $('#crm-menubar-drilldown').focus();
85 $(menu
).siblings('a[accesskey]').focus();
88 .smartmenus(CRM
.menubar
.settings
);
90 CRM
.menubar
.initializeResponsive();
91 CRM
.menubar
.initializeSearch();
92 CRM
.menubar
.initializeDrill();
97 $.SmartMenus
.destroy();
98 $('#civicrm-menu-nav').remove();
100 $('body[class]').attr('class', function(i
, c
) {
101 return c
.replace(/(^|\s)crm-menubar-\S+/g, '');
104 show: function(speed
) {
105 if (typeof speed
=== 'number') {
106 $('#civicrm-menu').slideDown(speed
, function() {
107 $(this).css('display', '');
111 .removeClass('crm-menubar-hidden')
112 .addClass('crm-menubar-visible');
114 hide: function(speed
, showMessage
) {
115 if (typeof speed
=== 'number') {
116 $('#civicrm-menu').slideUp(speed
, function() {
117 $(this).css('display', '');
121 .addClass('crm-menubar-hidden')
122 .removeClass('crm-menubar-visible');
123 if (showMessage
=== true && $('#crm-notification-container').length
&& initialized
) {
124 var alert
= CRM
.alert('<a href="#" id="crm-restore-menu" >' + _
.escape(ts('Restore CiviCRM Menu')) + '</a>', ts('Menu hidden'), 'none', {expires
: 10000});
125 $('#crm-restore-menu')
129 CRM
.menubar
.show(speed
);
133 open: function(itemName
) {
134 var $item
= $('li[data-name="' + itemName
+ '"] > a', '#civicrm-menu');
136 $('#civicrm-menu').smartmenus('itemActivate', $item
);
140 close
: $.SmartMenus
.hideAll
,
141 isOpen: function(itemName
) {
143 return !!$('li[data-name="' + itemName
+ '"] > ul[aria-expanded="true"]', '#civicrm-menu').length
;
145 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length
;
147 spin: function(spin
) {
148 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin
);
150 getItem: function(itemName
) {
151 return traverse(CRM
.menubar
.data
.menu
, itemName
, 'get');
153 findItems: function(searchTerm
) {
154 return findRecursive(CRM
.menubar
.data
.menu
, searchTerm
.toLowerCase().replace(/ /g
, ''));
156 addItems: function(position
, targetName
, items
) {
157 var list
, container
, $ul
;
158 if (position
=== 'before' || position
=== 'after') {
160 throw 'Cannot add sibling of main menu';
162 list
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'parent');
164 throw targetName
+ ' not found';
166 var offset
= position
=== 'before' ? 0 : 1;
167 position
= offset
+ _
.findIndex(list
, {name
: targetName
});
168 $ul
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu').closest('ul');
169 } else if (targetName
) {
170 container
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'get');
172 throw targetName
+ ' not found';
174 container
.child
= container
.child
|| [];
175 list
= container
.child
;
176 var $target
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu');
177 if (!$target
.children('ul').length
) {
178 $target
.append('<ul>');
180 $ul
= $target
.children('ul').first();
182 list
= CRM
.menubar
.data
.menu
;
185 position
= list
.length
+ 1 + position
;
187 if (position
>= list
.length
) {
188 list
.push
.apply(list
, items
);
189 position
= list
.length
- 1;
191 list
.splice
.apply(list
, [position
, 0].concat(items
));
193 if (targetName
&& !$ul
.is('#civicrm-menu')) {
194 $ul
.html(getTpl('branch')({items
: list
, branchTpl
: getTpl('branch')}));
196 $('#civicrm-menu > li').eq(position
).after(getTpl('branch')({items
: items
, branchTpl
: getTpl('branch')}));
198 CRM
.menubar
.refresh();
200 removeItem: function(itemName
) {
201 var item
= traverse(CRM
.menubar
.data
.menu
, itemName
, 'delete');
203 $('li[data-name="' + itemName
+ '"]', '#civicrm-menu').remove();
204 CRM
.menubar
.refresh();
208 updateItem: function(item
) {
210 throw 'No name passed to CRM.menubar.updateItem';
212 var menuItem
= CRM
.menubar
.getItem(item
.name
);
214 throw item
.name
+ ' not found';
216 _
.extend(menuItem
, item
);
217 $('li[data-name="' + item
.name
+ '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items
: [menuItem
], branchTpl
: getTpl('branch')}));
218 CRM
.menubar
.refresh();
220 refresh: function() {
222 $('#civicrm-menu').smartmenus('refresh');
226 togglePosition: function(persist
) {
227 $('body').toggleClass('crm-menubar-over-cms-menu crm-menubar-below-cms-menu');
228 CRM
.menubar
.position
= CRM
.menubar
.position
=== 'over-cms-menu' ? 'below-cms-menu' : 'over-cms-menu';
230 if (persist
!== false) {
231 CRM
.cache
.set('menubarPosition', CRM
.menubar
.position
);
234 initializePosition: function() {
235 if (CRM
.menubar
.toggleButton
&& (CRM
.menubar
.position
=== 'over-cms-menu' || CRM
.menubar
.position
=== 'below-cms-menu')) {
237 .on('click', 'a[href="#toggle-position"]', function(e
) {
239 CRM
.menubar
.togglePosition();
241 .append('<li id="crm-menubar-toggle-position"><a href="#toggle-position" title="' + ts('Adjust menu position') + '"><i class="crm-i fa-arrow-up" aria-hidden="true"></i></a>');
242 CRM
.menubar
.position
= CRM
.cache
.get('menubarPosition', CRM
.menubar
.position
);
244 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM
.menubar
.position
);
246 removeToggleButton: function() {
247 $('#crm-menubar-toggle-position').remove();
248 CRM
.menubar
.toggleButton
= false;
249 if (CRM
.menubar
.position
=== 'below-cms-menu') {
250 CRM
.menubar
.togglePosition();
253 initializeResponsive: function() {
254 var $mainMenuState
= $('#crm-menubar-state');
255 // hide mobile menu beforeunload
256 $(window
).on('beforeunload unload', function() {
257 CRM
.menubar
.spin(true);
258 if ($mainMenuState
[0].checked
) {
259 $mainMenuState
[0].click();
262 .on('resize', function() {
263 if (!isMobile() && $mainMenuState
[0].checked
) {
264 $mainMenuState
[0].click();
268 $mainMenuState
.click(function() {
269 // Use absolute position instead of fixed when open to allow scrolling menu
270 var open
= $(this).is(':checked');
272 window
.scroll({top
: 0});
274 $('#civicrm-menu-nav')
275 .css('position', open
? 'absolute' : '')
276 .parentsUntil('body')
277 .css('position', open
? 'static' : '');
280 initializeSearch: function() {
281 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM
.menubar
.qfKey
);
282 $('#crm-qsearch-input')
284 source: function(request
, response
) {
285 //start spinning the civi logo
286 CRM
.menubar
.spin(true);
288 option
= $('input[name=quickSearchField]:checked'),
291 field_name
: option
.val()
293 CRM
.api3('contact', 'getquick', params
).done(function(result
) {
295 if (result
.values
.length
> 0) {
296 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', false);
297 $.each(result
.values
, function(k
, v
) {
298 ret
.push({value
: v
.id
, label
: v
.data
});
301 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', true);
302 var label
= option
.closest('label').text();
303 var msg
= ts('%1 not found.', {1: label
});
304 // Remind user they are not searching by contact name (unless they enter a number)
305 if (params
.field_name
!== 'sort_name' && !(/[\d].*/.test(params
.name
))) {
306 msg
+= ' ' + ts('Did you mean to search by Name/Email instead?');
308 ret
.push({value
: '0', label
: msg
});
311 //stop spinning the civi logo
312 CRM
.menubar
.spin(false);
316 focus: function (event
, ui
) {
317 // This is when an item is 'focussed' by keyboard up/down or mouse hover.
318 // It is not the same as actually having focus, i.e. it is not :focus
319 var lis
= $(event
.currentTarget
).find('li[data-cid="' + ui
.item
.value
+ '"]');
320 lis
.children('div').addClass('ui-state-active');
321 lis
.siblings().children('div').removeClass('ui-state-active');
322 // Returning false leaves the user-entered text as it was.
325 select: function (event
, ui
) {
326 if (ui
.item
.value
> 0) {
327 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: ui
.item
.value
});
332 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
335 .on('keyup change', function() {
336 $(this).toggleClass('has-user-input', !!$(this).val());
340 if (e
.which
=== ENTER_KEY
) {
341 if (!$(this).val()) {
342 CRM
.menubar
.open('QuickSearch');
346 .autocomplete( "instance" )._renderItem = function( ul
, item
) {
347 var uiMenuItemWrapper
= $("<div class='ui-menu-item-uiMenuItemWrapper'>");
348 if (item
.value
== 0) {
350 uiMenuItemWrapper
.text(item
.label
);
353 uiMenuItemWrapper
.append($('<a>')
354 .attr('href', CRM
.url('civicrm/contact/view', {reset
: 1, cid
: item
.value
}))
355 .css({ display
: 'block' })
358 if (e
.ctrlKey
|| e
.shiftKey
|| e
.altKey
) {
359 // Special-clicking lets you open several tabs.
363 // Fall back to original behaviour.
369 return $( "<li class='ui-menu-item' data-cid=" + item
.value
+ ">" )
370 .append(uiMenuItemWrapper
)
373 $('#crm-qsearch > a').keyup(function(e
) {
374 if ($(e
.target
).is(this)) {
375 $('#crm-qsearch-input').focus();
379 $('#crm-qsearch form[name=search_block]').on('submit', function() {
380 if (!$('#crm-qsearch-input').val()) {
383 var $menu
= $('#crm-qsearch-input').autocomplete('widget');
384 if ($('li.ui-menu-item', $menu
).length
=== 1) {
385 var cid
= $('li.ui-menu-item', $menu
).data('ui-autocomplete-item').value
;
387 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: cid
});
392 $('#civicrm-menu').on('show.smapi', function(e
, menu
) {
393 if ($(menu
).parent().attr('data-name') === 'QuickSearch') {
394 $('#crm-qsearch-input').focus();
397 function setQuickSearchValue() {
398 var $selection
= $('.crm-quickSearchField input:checked'),
399 label
= $selection
.parent().text(),
400 value
= $selection
.val();
401 $('#crm-qsearch-input').attr({name
: value
, placeholder
: '\uf002 ' + label
});
403 $('.crm-quickSearchField').click(function() {
404 var input
= $('input', this);
405 // Wait for event - its default was prevented by our link handler which interferes with checking the radio input
406 window
.setTimeout(function() {
407 input
.prop('checked', true);
408 CRM
.cache
.set('quickSearchField', input
.val());
409 setQuickSearchValue();
410 $('#crm-qsearch-input').focus().autocomplete("search");
413 var savedDefault
= CRM
.cache
.get('quickSearchField');
415 $('.crm-quickSearchField input[value="' + savedDefault
+ '"]').prop('checked', true);
417 $('.crm-quickSearchField:first input').prop('checked', true);
419 setQuickSearchValue();
420 $('#civicrm-menu').on('activate.smapi', function(e
, item
) {
421 return !$('ul.crm-quickSearch-results').is(':visible:not(.ui-state-disabled)');
424 initializeDrill: function() {
425 $('#civicrm-menu').on('keyup', '#crm-menubar-drilldown', function() {
426 var term
= $(this).val(),
427 results
= term
? CRM
.menubar
.findItems(term
).slice(0, 20) : [];
428 $(this).parent().next('ul').html(getTpl('branch')({items
: results
, branchTpl
: getTpl('branch'), drillTpl
: _
.noop
}));
429 $('#civicrm-menu').smartmenus('refresh').smartmenus('itemActivate', $(this).closest('a'));
433 '<nav id="civicrm-menu-nav">' +
434 '<input id="crm-menubar-state" type="checkbox" />' +
435 '<label class="crm-menubar-toggle-btn" for="crm-menubar-state">' +
436 '<span class="crm-menu-logo"></span>' +
437 '<span class="crm-menubar-toggle-btn-icon"></span>' +
438 '<%- ts("Toggle main menu") %>' +
440 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
441 '<%= searchTpl({items: search}) %>' +
442 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
446 '<li id="crm-qsearch" data-name="QuickSearch">' +
448 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
450 '<input type="text" id="crm-qsearch-input" name="sort_name" placeholder="\uf002" accesskey="q" />' +
451 '<input type="hidden" name="hidden_location" value="1" />' +
452 '<input type="hidden" name="hidden_custom" value="1" />' +
453 '<input type="hidden" name="qfKey" />' +
454 '<input type="hidden" name="_qf_Advanced_refresh" value="Search" />' +
459 '<% _.forEach(items, function(item) { %>' +
460 '<li><a href="#" class="crm-quickSearchField"><label><input type="radio" value="<%= item.key %>" name="quickSearchField"> <%- item.value %></label></a></li>' +
465 '<li class="crm-menu-border-bottom" data-name="MenubarDrillDown">' +
466 '<a href="#"><input type="text" id="crm-menubar-drilldown" placeholder="' + _
.escape(ts('Find menu item...')) + '"></a>' +
470 '<% _.forEach(items, function(item) { %>' +
471 '<li <%= attr("li", item) %>>' +
472 '<a <%= attr("a", item) %>>' +
473 '<% if (item.icon) { %>' +
474 '<i class="<%- item.icon %>"></i>' +
476 '<% if (item.label) { %>' +
477 '<span><%- item.label %></span>' +
480 '<% if (item.child) { %>' +
482 '<% if (item.name === "Home") { %><%= drillTpl() %><% } %>' +
483 '<%= branchTpl({items: item.child, branchTpl: branchTpl}) %>' +
488 }, CRM
.menubar
|| {});
490 function getTpl(name
) {
493 drill
: _
.template(CRM
.menubar
.drillTpl
, {}),
494 search
: _
.template(CRM
.menubar
.searchTpl
, {imports
: {_
: _
, ts
: ts
, CRM
: CRM
}})
496 templates
.branch
= _
.template(CRM
.menubar
.branchTpl
, {imports
: {_
: _
, attr
: attr
, drillTpl
: templates
.drill
}});
497 templates
.tree
= _
.template(CRM
.menubar
.treeTpl
, {imports
: {branchTpl
: templates
.branch
, searchTpl
: templates
.search
, ts
: ts
}});
499 return templates
[name
];
502 function handleResize() {
503 if (!isMobile() && ($('#civicrm-menu').height() >= (2 * $('#civicrm-menu > li').height()))) {
504 $('body').addClass('crm-menubar-wrapped');
506 $('body').removeClass('crm-menubar-wrapped');
510 // Figure out if we've hit the mobile breakpoint, based on the rule in crm-menubar.css
511 function isMobile() {
512 return $('.crm-menubar-toggle-btn', '#civicrm-menu-nav').css('top') !== '-99999px';
515 function traverse(items
, itemName
, op
) {
517 _
.each(items
, function(item
, index
) {
518 if (item
.name
=== itemName
) {
519 found
= (op
=== 'parent' ? items
: item
);
520 if (op
=== 'delete') {
521 items
.splice(index
, 1);
526 found
= traverse(item
.child
, itemName
, op
);
535 function findRecursive(collection
, searchTerm
) {
536 var items
= _
.filter(collection
, function(item
) {
537 return item
.label
&& _
.includes(item
.label
.toLowerCase().replace(/ /g
, ''), searchTerm
);
539 _
.each(collection
, function(item
) {
540 if (_
.isPlainObject(item
) && item
.child
) {
541 var childMatches
= findRecursive(item
.child
, searchTerm
);
542 if (childMatches
.length
) {
543 Array
.prototype.push
.apply(items
, childMatches
);
550 function attr(el
, item
) {
551 var ret
= [], attr
= _
.cloneDeep(item
.attr
|| {}), a
= ['rel', 'accesskey', 'target'];
553 attr
= _
.pick(attr
, a
);
554 attr
.href
= item
.url
|| "#";
556 attr
= _
.omit(attr
, a
);
557 attr
['data-name'] = item
.name
;
558 if (item
.separator
) {
559 attr
.class = (attr
.class ? attr
.class + ' ' : '') + 'crm-menu-border-' + item
.separator
;
562 _
.each(attr
, function(val
, name
) {
563 ret
.push(name
+ '="' + val
+ '"');
565 return ret
.join(' ');
568 CRM
.menubar
.initialize();