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 $(menu
).siblings('a[accesskey]').focus();
83 .smartmenus(CRM
.menubar
.settings
);
85 CRM
.menubar
.initializeResponsive();
86 CRM
.menubar
.initializeSearch();
91 $.SmartMenus
.destroy();
92 $('#civicrm-menu-nav').remove();
94 $('body[class]').attr('class', function(i
, c
) {
95 return c
.replace(/(^|\s)crm-menubar-\S+/g, '');
98 show: function(speed
) {
99 if (typeof speed
=== 'number') {
100 $('#civicrm-menu').slideDown(speed
, function() {
101 $(this).css('display', '');
105 .removeClass('crm-menubar-hidden')
106 .addClass('crm-menubar-visible');
108 hide: function(speed
, showMessage
) {
109 if (typeof speed
=== 'number') {
110 $('#civicrm-menu').slideUp(speed
, function() {
111 $(this).css('display', '');
115 .addClass('crm-menubar-hidden')
116 .removeClass('crm-menubar-visible');
117 if (showMessage
=== true && $('#crm-notification-container').length
&& initialized
) {
118 var alert
= CRM
.alert('<a href="#" id="crm-restore-menu" >' + _
.escape(ts('Restore CiviCRM Menu')) + '</a>', ts('Menu hidden'), 'none', {expires
: 10000});
119 $('#crm-restore-menu')
123 CRM
.menubar
.show(speed
);
127 open: function(itemName
) {
128 var $item
= $('li[data-name="' + itemName
+ '"] > a', '#civicrm-menu');
130 $('#civicrm-menu').smartmenus('itemActivate', $item
);
134 close
: $.SmartMenus
.hideAll
,
135 isOpen: function(itemName
) {
137 return !!$('li[data-name="' + itemName
+ '"] > ul[aria-expanded="true"]', '#civicrm-menu').length
;
139 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length
;
141 spin: function(spin
) {
142 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin
);
144 getItem: function(itemName
) {
145 return traverse(CRM
.menubar
.data
.menu
, itemName
, 'get');
147 addItems: function(position
, targetName
, items
) {
148 var list
, container
, $ul
;
149 if (position
=== 'before' || position
=== 'after') {
151 throw 'Cannot add sibling of main menu';
153 list
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'parent');
155 throw targetName
+ ' not found';
157 var offset
= position
=== 'before' ? 0 : 1;
158 position
= offset
+ _
.findIndex(list
, {name
: targetName
});
159 $ul
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu').closest('ul');
160 } else if (targetName
) {
161 container
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'get');
163 throw targetName
+ ' not found';
165 container
.child
= container
.child
|| [];
166 list
= container
.child
;
167 var $target
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu');
168 if (!$target
.children('ul').length
) {
169 $target
.append('<ul>');
171 $ul
= $target
.children('ul').first();
173 list
= CRM
.menubar
.data
.menu
;
176 position
= list
.length
+ 1 + position
;
178 if (position
>= list
.length
) {
179 list
.push
.apply(list
, items
);
180 position
= list
.length
- 1;
182 list
.splice
.apply(list
, [position
, 0].concat(items
));
184 if (targetName
&& !$ul
.is('#civicrm-menu')) {
185 $ul
.html(getTpl('branch')({items
: list
, branchTpl
: getTpl('branch')}));
187 $('#civicrm-menu > li').eq(position
).after(getTpl('branch')({items
: items
, branchTpl
: getTpl('branch')}));
189 CRM
.menubar
.refresh();
191 removeItem: function(itemName
) {
192 var item
= traverse(CRM
.menubar
.data
.menu
, itemName
, 'delete');
194 $('li[data-name="' + itemName
+ '"]', '#civicrm-menu').remove();
195 CRM
.menubar
.refresh();
199 updateItem: function(item
) {
201 throw 'No name passed to CRM.menubar.updateItem';
203 var menuItem
= CRM
.menubar
.getItem(item
.name
);
205 throw item
.name
+ ' not found';
207 _
.extend(menuItem
, item
);
208 $('li[data-name="' + item
.name
+ '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items
: [menuItem
], branchTpl
: getTpl('branch')}));
209 CRM
.menubar
.refresh();
211 refresh: function() {
213 $('#civicrm-menu').smartmenus('refresh');
217 togglePosition: function(persist
) {
218 $('body').toggleClass('crm-menubar-over-cms-menu crm-menubar-below-cms-menu');
219 CRM
.menubar
.position
= CRM
.menubar
.position
=== 'over-cms-menu' ? 'below-cms-menu' : 'over-cms-menu';
221 if (persist
!== false) {
222 CRM
.cache
.set('menubarPosition', CRM
.menubar
.position
);
225 initializePosition: function() {
226 if (CRM
.menubar
.position
=== 'over-cms-menu' || CRM
.menubar
.position
=== 'below-cms-menu') {
228 .on('click', 'a[href="#toggle-position"]', function(e
) {
230 CRM
.menubar
.togglePosition();
232 .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>');
233 CRM
.menubar
.position
= CRM
.cache
.get('menubarPosition', CRM
.menubar
.position
);
235 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM
.menubar
.position
);
237 initializeResponsive: function() {
238 var $mainMenuState
= $('#crm-menubar-state');
239 // hide mobile menu beforeunload
240 $(window
).on('beforeunload unload', function() {
241 CRM
.menubar
.spin(true);
242 if ($mainMenuState
[0].checked
) {
243 $mainMenuState
[0].click();
246 .on('resize', function() {
247 if ($(window
).width() >= 768 && $mainMenuState
[0].checked
) {
248 $mainMenuState
[0].click();
252 $mainMenuState
.click(function() {
253 // Use absolute position instead of fixed when open to allow scrolling menu
254 var open
= $(this).is(':checked');
256 window
.scroll({top
: 0});
258 $('#civicrm-menu-nav')
259 .css('position', open
? 'absolute' : '')
260 .parentsUntil('body')
261 .css('position', open
? 'static' : '');
264 initializeSearch: function() {
265 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM
.menubar
.qfKey
);
266 $('#crm-qsearch-input')
268 source: function(request
, response
) {
269 //start spinning the civi logo
270 CRM
.menubar
.spin(true);
272 option
= $('input[name=quickSearchField]:checked'),
275 field_name
: option
.val()
277 CRM
.api3('contact', 'getquick', params
).done(function(result
) {
279 if (result
.values
.length
> 0) {
280 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', false);
281 $.each(result
.values
, function(k
, v
) {
282 ret
.push({value
: v
.id
, label
: v
.data
});
285 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', true);
286 var label
= option
.closest('label').text();
287 var msg
= ts('%1 not found.', {1: label
});
288 // Remind user they are not searching by contact name (unless they enter a number)
289 if (params
.field_name
!== 'sort_name' && !(/[\d].*/.test(params
.name
))) {
290 msg
+= ' ' + ts('Did you mean to search by Name/Email instead?');
292 ret
.push({value
: '0', label
: msg
});
295 //stop spinning the civi logo
296 CRM
.menubar
.spin(false);
300 focus: function (event
, ui
) {
303 select: function (event
, ui
) {
304 if (ui
.item
.value
> 0) {
305 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: ui
.item
.value
});
310 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
313 .on('keyup change', function() {
314 $(this).toggleClass('has-user-input', !!$(this).val());
318 if (e
.which
=== ENTER_KEY
) {
319 if (!$(this).val()) {
320 CRM
.menubar
.open('QuickSearch');
324 $('#crm-qsearch > a').keyup(function(e
) {
325 if ($(e
.target
).is(this)) {
326 $('#crm-qsearch-input').focus();
330 $('#crm-qsearch form[name=search_block]').on('submit', function() {
331 if (!$('#crm-qsearch-input').val()) {
334 var $menu
= $('#crm-qsearch-input').autocomplete('widget');
335 if ($('li.ui-menu-item', $menu
).length
=== 1) {
336 var cid
= $('li.ui-menu-item', $menu
).data('ui-autocomplete-item').value
;
338 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: cid
});
343 $('#civicrm-menu').on('show.smapi', function(e
, menu
) {
344 if ($(menu
).parent().attr('data-name') === 'QuickSearch') {
345 $('#crm-qsearch-input').focus();
348 function setQuickSearchValue() {
349 var $selection
= $('.crm-quickSearchField input:checked'),
350 label
= $selection
.parent().text(),
351 value
= $selection
.val();
352 // These fields are not supported by advanced search
353 if (!value
|| value
=== 'first_name' || value
=== 'last_name') {
356 $('#crm-qsearch-input').attr({name
: value
, placeholder
: '\uf002 ' + label
});
358 $('.crm-quickSearchField').click(function() {
359 var input
= $('input', this);
360 // Wait for event - its default was prevented by our link handler which interferes with checking the radio input
361 window
.setTimeout(function() {
362 input
.prop('checked', true);
363 CRM
.cache
.set('quickSearchField', input
.val());
364 setQuickSearchValue();
365 $('#crm-qsearch-input').focus().autocomplete("search");
368 $('.crm-quickSearchField input[value="' + CRM
.cache
.get('quickSearchField', 'sort_name') + '"]').prop('checked', true);
369 setQuickSearchValue();
370 $('#civicrm-menu').on('activate.smapi', function(e
, item
) {
371 return !$('ul.crm-quickSearch-results').is(':visible:not(.ui-state-disabled)');
375 '<nav id="civicrm-menu-nav">' +
376 '<input id="crm-menubar-state" type="checkbox" />' +
377 '<label class="crm-menubar-toggle-btn" for="crm-menubar-state">' +
378 '<span class="crm-menu-logo"></span>' +
379 '<span class="crm-menubar-toggle-btn-icon"></span>' +
380 '<%- ts("Toggle main menu") %>' +
382 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
383 '<%= searchTpl({items: search}) %>' +
384 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
388 '<li id="crm-qsearch" data-name="QuickSearch">' +
390 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
392 '<input type="text" id="crm-qsearch-input" name="sort_name" placeholder="\uf002" accesskey="q" />' +
393 '<input type="hidden" name="hidden_location" value="1" />' +
394 '<input type="hidden" name="hidden_custom" value="1" />' +
395 '<input type="hidden" name="qfKey" />' +
396 '<input type="hidden" name="_qf_Advanced_refresh" value="Search" />' +
401 '<% _.forEach(items, function(item) { %>' +
402 '<li><a href="#" class="crm-quickSearchField"><label><input type="radio" value="<%= item.key %>" name="quickSearchField"> <%- item.value %></label></a></li>' +
407 '<% _.forEach(items, function(item) { %>' +
408 '<li <%= attr("li", item) %>>' +
409 '<a <%= attr("a", item) %>>' +
410 '<% if (item.icon) { %>' +
411 '<i class="<%- item.icon %>"></i>' +
413 '<% if (item.label) { %>' +
414 '<span><%- item.label %></span>' +
417 '<% if (item.child) { %>' +
418 '<ul><%= branchTpl({items: item.child, branchTpl: branchTpl}) %></ul>' +
422 }, CRM
.menubar
|| {});
424 function getTpl(name
) {
427 branch
: _
.template(CRM
.menubar
.branchTpl
, {imports
: {_
: _
, attr
: attr
}}),
428 search
: _
.template(CRM
.menubar
.searchTpl
, {imports
: {_
: _
, ts
: ts
, CRM
: CRM
}})
430 templates
.tree
= _
.template(CRM
.menubar
.treeTpl
, {imports
: {branchTpl
: templates
.branch
, searchTpl
: templates
.search
, ts
: ts
}});
432 return templates
[name
];
435 function handleResize() {
436 if ($(window
).width() >= 768 && $('#civicrm-menu').height() > 50) {
437 $('body').addClass('crm-menubar-wrapped');
439 $('body').removeClass('crm-menubar-wrapped');
443 function traverse(items
, itemName
, op
) {
445 _
.each(items
, function(item
, index
) {
446 if (item
.name
=== itemName
) {
447 found
= (op
=== 'parent' ? items
: item
);
448 if (op
=== 'delete') {
449 items
.splice(index
, 1);
454 found
= traverse(item
.child
, itemName
, op
);
463 function attr(el
, item
) {
464 var ret
= [], attr
= _
.cloneDeep(item
.attr
|| {}), a
= ['rel', 'accesskey'];
466 attr
= _
.pick(attr
, a
);
467 attr
.href
= item
.url
|| "#";
469 attr
= _
.omit(attr
, a
);
470 attr
['data-name'] = item
.name
;
471 if (item
.separator
) {
472 attr
.class = (attr
.class ? attr
.class + ' ' : '') + 'crm-menu-border-' + item
.separator
;
475 _
.each(attr
, function(val
, name
) {
476 ret
.push(name
+ '="' + val
+ '"');
478 return ret
.join(' ');
481 CRM
.menubar
.initialize();