1 // https://civicrm.org/licensing
4 var templates
, initialized
,
7 CRM
.menubar
= _
.extend({
9 settings
: {collapsibleBehavior
: 'accordion'},
10 position
: 'over-cms-menu',
11 attachTo
: (CRM
.menubar
&& CRM
.menubar
.position
=== 'above-crm-container') ? '#crm-container' : 'body',
12 initialize: function() {
13 var cache
= CRM
.cache
.get('menubar');
14 if (cache
&& cache
.code
=== CRM
.menubar
.cacheCode
&& cache
.locale
=== CRM
.config
.locale
&& cache
.cid
=== CRM
.config
.cid
&& localStorage
.civiMenubar
) {
15 CRM
.menubar
.data
= cache
.data
;
16 insert(localStorage
.civiMenubar
);
18 $.getJSON(CRM
.url('civicrm/ajax/navmenu', {code
: CRM
.menubar
.cacheCode
, locale
: CRM
.config
.locale
, cid
: CRM
.config
.cid
}))
19 .done(function(data
) {
20 var markup
= getTpl('tree')(data
);
21 CRM
.cache
.set('menubar', {code
: CRM
.menubar
.cacheCode
, locale
: CRM
.config
.locale
, cid
: CRM
.config
.cid
, data
: data
});
22 CRM
.menubar
.data
= data
;
23 localStorage
.setItem('civiMenubar', markup
);
28 // Wait for crm-container present on the page as it's faster than document.ready
29 function insert(markup
) {
30 if ($('#crm-container').length
) {
33 new MutationObserver(function(mutations
, observer
) {
34 _
.each(mutations
, function(mutant
) {
35 _
.each(mutant
.addedNodes
, function(node
) {
36 if ($(node
).is('#crm-container')) {
38 observer
.disconnect();
42 }).observe(document
, {childList
: true, subtree
: true});
46 function render(markup
) {
47 var position
= CRM
.menubar
.attachTo
=== 'body' ? 'beforeend' : 'afterbegin';
48 $(CRM
.menubar
.attachTo
)[0].insertAdjacentHTML(position
, markup
);
49 CRM
.menubar
.initializePosition();
50 $('#civicrm-menu').trigger('crmLoad');
51 $(document
).ready(function() {
54 .on('click', 'a[href="#"]', function() {
55 // For empty links - keep the menu open and don't jump the page anchor
58 .on('click', 'a:not([href^="#"])', function(e
) {
59 if (e
.ctrlKey
|| e
.altKey
|| e
.shiftKey
|| e
.metaKey
) {
60 // Prevent menu closing when link is clicked with a keyboard modifier.
64 .on('dragstart', function() {
65 // Stop user from accidentally dragging menu links
66 // This was added because a user noticed they could drag the civi icon into the quicksearch box.
69 .on('click', 'a[href="#hidemenu"]', function(e
) {
71 CRM
.menubar
.hide(250, true);
73 .on('keyup', 'a', function(e
) {
74 // Simulate a click when spacebar key is pressed
75 if (e
.which
== SPACE_KEY
) {
76 $(e
.currentTarget
)[0].click();
79 .on('show.smapi', function(e
, menu
) {
80 // Focus menu when opened with an accesskey
81 if ($(menu
).parent().data('name') === 'Home') {
82 $('#crm-menubar-drilldown').focus();
84 $(menu
).siblings('a[accesskey]').focus();
87 .smartmenus(CRM
.menubar
.settings
);
89 CRM
.menubar
.initializeResponsive();
90 CRM
.menubar
.initializeSearch();
91 CRM
.menubar
.initializeDrill();
96 $.SmartMenus
.destroy();
97 $('#civicrm-menu-nav').remove();
99 $('body[class]').attr('class', function(i
, c
) {
100 return c
.replace(/(^|\s)crm-menubar-\S+/g, '');
103 show: function(speed
) {
104 if (typeof speed
=== 'number') {
105 $('#civicrm-menu').slideDown(speed
, function() {
106 $(this).css('display', '');
110 .removeClass('crm-menubar-hidden')
111 .addClass('crm-menubar-visible');
113 hide: function(speed
, showMessage
) {
114 if (typeof speed
=== 'number') {
115 $('#civicrm-menu').slideUp(speed
, function() {
116 $(this).css('display', '');
120 .addClass('crm-menubar-hidden')
121 .removeClass('crm-menubar-visible');
122 if (showMessage
=== true && $('#crm-notification-container').length
&& initialized
) {
123 var alert
= CRM
.alert('<a href="#" id="crm-restore-menu" >' + _
.escape(ts('Restore CiviCRM Menu')) + '</a>', ts('Menu hidden'), 'none', {expires
: 10000});
124 $('#crm-restore-menu')
128 CRM
.menubar
.show(speed
);
132 open: function(itemName
) {
133 var $item
= $('li[data-name="' + itemName
+ '"] > a', '#civicrm-menu');
135 $('#civicrm-menu').smartmenus('itemActivate', $item
);
139 close
: $.SmartMenus
.hideAll
,
140 isOpen: function(itemName
) {
142 return !!$('li[data-name="' + itemName
+ '"] > ul[aria-expanded="true"]', '#civicrm-menu').length
;
144 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length
;
146 spin: function(spin
) {
147 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin
);
149 getItem: function(itemName
) {
150 return traverse(CRM
.menubar
.data
.menu
, itemName
, 'get');
152 findItems: function(searchTerm
) {
153 return findRecursive(CRM
.menubar
.data
.menu
, searchTerm
.toLowerCase().replace(/ /g
, ''));
155 addItems: function(position
, targetName
, items
) {
156 var list
, container
, $ul
;
157 if (position
=== 'before' || position
=== 'after') {
159 throw 'Cannot add sibling of main menu';
161 list
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'parent');
163 throw targetName
+ ' not found';
165 var offset
= position
=== 'before' ? 0 : 1;
166 position
= offset
+ _
.findIndex(list
, {name
: targetName
});
167 $ul
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu').closest('ul');
168 } else if (targetName
) {
169 container
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'get');
171 throw targetName
+ ' not found';
173 container
.child
= container
.child
|| [];
174 list
= container
.child
;
175 var $target
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu');
176 if (!$target
.children('ul').length
) {
177 $target
.append('<ul>');
179 $ul
= $target
.children('ul').first();
181 list
= CRM
.menubar
.data
.menu
;
184 position
= list
.length
+ 1 + position
;
186 if (position
>= list
.length
) {
187 list
.push
.apply(list
, items
);
188 position
= list
.length
- 1;
190 list
.splice
.apply(list
, [position
, 0].concat(items
));
192 if (targetName
&& !$ul
.is('#civicrm-menu')) {
193 $ul
.html(getTpl('branch')({items
: list
, branchTpl
: getTpl('branch')}));
195 $('#civicrm-menu > li').eq(position
).after(getTpl('branch')({items
: items
, branchTpl
: getTpl('branch')}));
197 CRM
.menubar
.refresh();
199 removeItem: function(itemName
) {
200 var item
= traverse(CRM
.menubar
.data
.menu
, itemName
, 'delete');
202 $('li[data-name="' + itemName
+ '"]', '#civicrm-menu').remove();
203 CRM
.menubar
.refresh();
207 updateItem: function(item
) {
209 throw 'No name passed to CRM.menubar.updateItem';
211 var menuItem
= CRM
.menubar
.getItem(item
.name
);
213 throw item
.name
+ ' not found';
215 _
.extend(menuItem
, item
);
216 $('li[data-name="' + item
.name
+ '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items
: [menuItem
], branchTpl
: getTpl('branch')}));
217 CRM
.menubar
.refresh();
219 refresh: function() {
221 $('#civicrm-menu').smartmenus('refresh');
225 togglePosition: function(persist
) {
226 $('body').toggleClass('crm-menubar-over-cms-menu crm-menubar-below-cms-menu');
227 CRM
.menubar
.position
= CRM
.menubar
.position
=== 'over-cms-menu' ? 'below-cms-menu' : 'over-cms-menu';
229 if (persist
!== false) {
230 CRM
.cache
.set('menubarPosition', CRM
.menubar
.position
);
233 initializePosition: function() {
234 if (CRM
.menubar
.position
=== 'over-cms-menu' || CRM
.menubar
.position
=== 'below-cms-menu') {
236 .on('click', 'a[href="#toggle-position"]', function(e
) {
238 CRM
.menubar
.togglePosition();
240 .append('<li id="crm-menubar-toggle-position"><a href="#toggle-position" title="' + ts('Adjust menu position') + '"><i class="crm-i fa-arrow-up"></i></a>');
241 CRM
.menubar
.position
= CRM
.cache
.get('menubarPosition', CRM
.menubar
.position
);
243 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM
.menubar
.position
);
245 initializeResponsive: function() {
246 var $mainMenuState
= $('#crm-menubar-state');
247 // hide mobile menu beforeunload
248 $(window
).on('beforeunload unload', function() {
249 CRM
.menubar
.spin(true);
250 if ($mainMenuState
[0].checked
) {
251 $mainMenuState
[0].click();
254 .on('resize', function() {
255 if (!isMobile() && $mainMenuState
[0].checked
) {
256 $mainMenuState
[0].click();
260 $mainMenuState
.click(function() {
261 // Use absolute position instead of fixed when open to allow scrolling menu
262 var open
= $(this).is(':checked');
264 window
.scroll({top
: 0});
266 $('#civicrm-menu-nav')
267 .css('position', open
? 'absolute' : '')
268 .parentsUntil('body')
269 .css('position', open
? 'static' : '');
272 initializeSearch: function() {
273 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM
.menubar
.qfKey
);
274 $('#crm-qsearch-input')
276 source: function(request
, response
) {
277 //start spinning the civi logo
278 CRM
.menubar
.spin(true);
280 option
= $('input[name=quickSearchField]:checked'),
283 field_name
: option
.val()
285 CRM
.api3('contact', 'getquick', params
).done(function(result
) {
287 if (result
.values
.length
> 0) {
288 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', false);
289 $.each(result
.values
, function(k
, v
) {
290 ret
.push({value
: v
.id
, label
: v
.data
});
293 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', true);
294 var label
= option
.closest('label').text();
295 var msg
= ts('%1 not found.', {1: label
});
296 // Remind user they are not searching by contact name (unless they enter a number)
297 if (params
.field_name
!== 'sort_name' && !(/[\d].*/.test(params
.name
))) {
298 msg
+= ' ' + ts('Did you mean to search by Name/Email instead?');
300 ret
.push({value
: '0', label
: msg
});
303 //stop spinning the civi logo
304 CRM
.menubar
.spin(false);
308 focus: function (event
, ui
) {
311 select: function (event
, ui
) {
312 if (ui
.item
.value
> 0) {
313 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: ui
.item
.value
});
318 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
321 .on('keyup change', function() {
322 $(this).toggleClass('has-user-input', !!$(this).val());
326 if (e
.which
=== ENTER_KEY
) {
327 if (!$(this).val()) {
328 CRM
.menubar
.open('QuickSearch');
332 $('#crm-qsearch > a').keyup(function(e
) {
333 if ($(e
.target
).is(this)) {
334 $('#crm-qsearch-input').focus();
338 $('#crm-qsearch form[name=search_block]').on('submit', function() {
339 if (!$('#crm-qsearch-input').val()) {
342 var $menu
= $('#crm-qsearch-input').autocomplete('widget');
343 if ($('li.ui-menu-item', $menu
).length
=== 1) {
344 var cid
= $('li.ui-menu-item', $menu
).data('ui-autocomplete-item').value
;
346 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: cid
});
351 $('#civicrm-menu').on('show.smapi', function(e
, menu
) {
352 if ($(menu
).parent().attr('data-name') === 'QuickSearch') {
353 $('#crm-qsearch-input').focus();
356 function setQuickSearchValue() {
357 var $selection
= $('.crm-quickSearchField input:checked'),
358 label
= $selection
.parent().text(),
359 value
= $selection
.val();
360 // These fields are not supported by advanced search
361 if (!value
|| value
=== 'first_name' || value
=== 'last_name') {
364 $('#crm-qsearch-input').attr({name
: value
, placeholder
: '\uf002 ' + label
});
366 $('.crm-quickSearchField').click(function() {
367 var input
= $('input', this);
368 // Wait for event - its default was prevented by our link handler which interferes with checking the radio input
369 window
.setTimeout(function() {
370 input
.prop('checked', true);
371 CRM
.cache
.set('quickSearchField', input
.val());
372 setQuickSearchValue();
373 $('#crm-qsearch-input').focus().autocomplete("search");
376 var savedDefault
= CRM
.cache
.get('quickSearchField');
378 $('.crm-quickSearchField input[value="' + savedDefault
+ '"]').prop('checked', true);
380 $('.crm-quickSearchField:first input').prop('checked', true);
382 setQuickSearchValue();
383 $('#civicrm-menu').on('activate.smapi', function(e
, item
) {
384 return !$('ul.crm-quickSearch-results').is(':visible:not(.ui-state-disabled)');
387 initializeDrill: function() {
388 $('#civicrm-menu').on('keyup', '#crm-menubar-drilldown', function() {
389 var term
= $(this).val(),
390 results
= term
? CRM
.menubar
.findItems(term
).slice(0, 20) : [];
391 $(this).parent().next('ul').html(getTpl('branch')({items
: results
, branchTpl
: getTpl('branch'), drillTpl
: _
.noop
}));
392 $('#civicrm-menu').smartmenus('refresh').smartmenus('itemActivate', $(this).closest('a'));
396 '<nav id="civicrm-menu-nav">' +
397 '<input id="crm-menubar-state" type="checkbox" />' +
398 '<label class="crm-menubar-toggle-btn" for="crm-menubar-state">' +
399 '<span class="crm-menu-logo"></span>' +
400 '<span class="crm-menubar-toggle-btn-icon"></span>' +
401 '<%- ts("Toggle main menu") %>' +
403 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
404 '<%= searchTpl({items: search}) %>' +
405 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
409 '<li id="crm-qsearch" data-name="QuickSearch">' +
411 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
413 '<input type="text" id="crm-qsearch-input" name="sort_name" placeholder="\uf002" accesskey="q" />' +
414 '<input type="hidden" name="hidden_location" value="1" />' +
415 '<input type="hidden" name="hidden_custom" value="1" />' +
416 '<input type="hidden" name="qfKey" />' +
417 '<input type="hidden" name="_qf_Advanced_refresh" value="Search" />' +
422 '<% _.forEach(items, function(item) { %>' +
423 '<li><a href="#" class="crm-quickSearchField"><label><input type="radio" value="<%= item.key %>" name="quickSearchField"> <%- item.value %></label></a></li>' +
428 '<li class="crm-menu-border-bottom" data-name="MenubarDrillDown">' +
429 '<a href="#"><input type="text" id="crm-menubar-drilldown" placeholder="' + _
.escape(ts('Find menu item...')) + '"></a>' +
433 '<% _.forEach(items, function(item) { %>' +
434 '<li <%= attr("li", item) %>>' +
435 '<a <%= attr("a", item) %>>' +
436 '<% if (item.icon) { %>' +
437 '<i class="<%- item.icon %>"></i>' +
439 '<% if (item.label) { %>' +
440 '<span><%- item.label %></span>' +
443 '<% if (item.child) { %>' +
445 '<% if (item.name === "Home") { %><%= drillTpl() %><% } %>' +
446 '<%= branchTpl({items: item.child, branchTpl: branchTpl}) %>' +
451 }, CRM
.menubar
|| {});
453 function getTpl(name
) {
456 drill
: _
.template(CRM
.menubar
.drillTpl
, {}),
457 search
: _
.template(CRM
.menubar
.searchTpl
, {imports
: {_
: _
, ts
: ts
, CRM
: CRM
}})
459 templates
.branch
= _
.template(CRM
.menubar
.branchTpl
, {imports
: {_
: _
, attr
: attr
, drillTpl
: templates
.drill
}});
460 templates
.tree
= _
.template(CRM
.menubar
.treeTpl
, {imports
: {branchTpl
: templates
.branch
, searchTpl
: templates
.search
, ts
: ts
}});
462 return templates
[name
];
465 function handleResize() {
466 if (!isMobile() && ($('#civicrm-menu').height() >= (2 * $('#civicrm-menu > li').height()))) {
467 $('body').addClass('crm-menubar-wrapped');
469 $('body').removeClass('crm-menubar-wrapped');
473 // Figure out if we've hit the mobile breakpoint, based on the rule in crm-menubar.css
474 function isMobile() {
475 return $('.crm-menubar-toggle-btn', '#civicrm-menu-nav').css('top') !== '-99999px';
478 function traverse(items
, itemName
, op
) {
480 _
.each(items
, function(item
, index
) {
481 if (item
.name
=== itemName
) {
482 found
= (op
=== 'parent' ? items
: item
);
483 if (op
=== 'delete') {
484 items
.splice(index
, 1);
489 found
= traverse(item
.child
, itemName
, op
);
498 function findRecursive(collection
, searchTerm
) {
499 var items
= _
.filter(collection
, function(item
) {
500 return item
.label
&& _
.includes(item
.label
.toLowerCase().replace(/ /g
, ''), searchTerm
);
502 _
.each(collection
, function(item
) {
503 if (_
.isPlainObject(item
) && item
.child
) {
504 var childMatches
= findRecursive(item
.child
, searchTerm
);
505 if (childMatches
.length
) {
506 Array
.prototype.push
.apply(items
, childMatches
);
513 function attr(el
, item
) {
514 var ret
= [], attr
= _
.cloneDeep(item
.attr
|| {}), a
= ['rel', 'accesskey', 'target'];
516 attr
= _
.pick(attr
, a
);
517 attr
.href
= item
.url
|| "#";
519 attr
= _
.omit(attr
, a
);
520 attr
['data-name'] = item
.name
;
521 if (item
.separator
) {
522 attr
.class = (attr
.class ? attr
.class + ' ' : '') + 'crm-menu-border-' + item
.separator
;
525 _
.each(attr
, function(val
, name
) {
526 ret
.push(name
+ '="' + val
+ '"');
528 return ret
.join(' ');
531 CRM
.menubar
.initialize();