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]:not(:hover)').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" style="text-align: center; margin-top: -8px;">' + _
.escape(ts('Restore CiviCRM Menu')) + '</a>', '', 'none', {expires
: 10000});
119 $('#crm-restore-menu')
120 .button({icons
: {primary
: 'fa-undo'}})
124 CRM
.menubar
.show(speed
);
126 .parent().css('text-align', 'center').find('.ui-button-text').css({'padding-top': '4px', 'padding-bottom': '4px'});
129 open: function(itemName
) {
130 var $item
= $('li[data-name="' + itemName
+ '"] > a', '#civicrm-menu');
132 $('#civicrm-menu').smartmenus('itemActivate', $item
);
136 close
: $.SmartMenus
.hideAll
,
137 isOpen: function(itemName
) {
139 return !!$('li[data-name="' + itemName
+ '"] > ul[aria-expanded="true"]', '#civicrm-menu').length
;
141 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length
;
143 spin: function(spin
) {
144 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin
);
146 getItem: function(itemName
) {
147 return traverse(CRM
.menubar
.data
.menu
, itemName
, 'get');
149 addItems: function(position
, targetName
, items
) {
150 var list
, container
, $ul
;
151 if (position
=== 'before' || position
=== 'after') {
153 throw 'Cannot add sibling of main menu';
155 list
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'parent');
157 throw targetName
+ ' not found';
159 var offset
= position
=== 'before' ? 0 : 1;
160 position
= offset
+ _
.findIndex(list
, {name
: targetName
});
161 $ul
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu').closest('ul');
162 } else if (targetName
) {
163 container
= traverse(CRM
.menubar
.data
.menu
, targetName
, 'get');
165 throw targetName
+ ' not found';
167 container
.child
= container
.child
|| [];
168 list
= container
.child
;
169 var $target
= $('li[data-name="' + targetName
+ '"]', '#civicrm-menu');
170 if (!$target
.children('ul').length
) {
171 $target
.append('<ul>');
173 $ul
= $target
.children('ul').first();
175 list
= CRM
.menubar
.data
.menu
;
178 position
= list
.length
+ 1 + position
;
180 if (position
>= list
.length
) {
181 list
.push
.apply(list
, items
);
182 position
= list
.length
- 1;
184 list
.splice
.apply(list
, [position
, 0].concat(items
));
186 if (targetName
&& !$ul
.is('#civicrm-menu')) {
187 $ul
.html(getTpl('branch')({items
: list
, branchTpl
: getTpl('branch')}));
189 $('#civicrm-menu > li').eq(position
).after(getTpl('branch')({items
: items
, branchTpl
: getTpl('branch')}));
191 CRM
.menubar
.refresh();
193 removeItem: function(itemName
) {
194 var item
= traverse(CRM
.menubar
.data
.menu
, itemName
, 'delete');
196 $('li[data-name="' + itemName
+ '"]', '#civicrm-menu').remove();
197 CRM
.menubar
.refresh();
201 updateItem: function(item
) {
203 throw 'No name passed to CRM.menubar.updateItem';
205 var menuItem
= CRM
.menubar
.getItem(item
.name
);
207 throw item
.name
+ ' not found';
209 _
.extend(menuItem
, item
);
210 $('li[data-name="' + item
.name
+ '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items
: [menuItem
], branchTpl
: getTpl('branch')}));
211 CRM
.menubar
.refresh();
213 refresh: function() {
215 $('#civicrm-menu').smartmenus('refresh');
219 togglePosition: function(persist
) {
220 $('body').toggleClass('crm-menubar-over-cms-menu crm-menubar-below-cms-menu');
221 CRM
.menubar
.position
= CRM
.menubar
.position
=== 'over-cms-menu' ? 'below-cms-menu' : 'over-cms-menu';
223 if (persist
!== false) {
224 CRM
.cache
.set('menubarPosition', CRM
.menubar
.position
);
227 initializePosition: function() {
228 if (CRM
.menubar
.position
=== 'over-cms-menu' || CRM
.menubar
.position
=== 'below-cms-menu') {
230 .on('click', 'a[href="#toggle-position"]', function(e
) {
232 CRM
.menubar
.togglePosition();
234 .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>');
235 CRM
.menubar
.position
= CRM
.cache
.get('menubarPosition', CRM
.menubar
.position
);
237 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM
.menubar
.position
);
239 initializeResponsive: function() {
240 var $mainMenuState
= $('#crm-menubar-state');
241 // hide mobile menu beforeunload
242 $(window
).on('beforeunload unload', function() {
243 CRM
.menubar
.spin(true);
244 if ($mainMenuState
[0].checked
) {
245 $mainMenuState
[0].click();
248 .on('resize', function() {
249 if ($(window
).width() >= 768 && $mainMenuState
[0].checked
) {
250 $mainMenuState
[0].click();
254 $mainMenuState
.click(function() {
255 // Use absolute position instead of fixed when open to allow scrolling menu
256 var open
= $(this).is(':checked');
258 window
.scroll({top
: 0});
260 $('#civicrm-menu-nav')
261 .css('position', open
? 'absolute' : '')
262 .parentsUntil('body')
263 .css('position', open
? 'static' : '');
266 initializeSearch: function() {
267 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM
.menubar
.qfKey
);
268 $('#crm-qsearch-input')
270 source: function(request
, response
) {
271 //start spinning the civi logo
272 CRM
.menubar
.spin(true);
274 option
= $('input[name=quickSearchField]:checked'),
277 field_name
: option
.val()
279 CRM
.api3('contact', 'getquick', params
).done(function(result
) {
281 if (result
.values
.length
> 0) {
282 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', false);
283 $.each(result
.values
, function(k
, v
) {
284 ret
.push({value
: v
.id
, label
: v
.data
});
287 $('#crm-qsearch-input').autocomplete('widget').menu('option', 'disabled', true);
288 var label
= option
.closest('label').text();
289 var msg
= ts('%1 not found.', {1: label
});
290 // Remind user they are not searching by contact name (unless they enter a number)
291 if (params
.field_name
!== 'sort_name' && !(/[\d].*/.test(params
.name
))) {
292 msg
+= ' ' + ts('Did you mean to search by Name/Email instead?');
294 ret
.push({value
: '0', label
: msg
});
297 //stop spinning the civi logo
298 CRM
.menubar
.spin(false);
302 focus: function (event
, ui
) {
305 select: function (event
, ui
) {
306 if (ui
.item
.value
> 0) {
307 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: ui
.item
.value
});
312 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
315 .on('keyup change', function() {
316 $(this).toggleClass('has-user-input', !!$(this).val());
320 if (e
.which
=== ENTER_KEY
) {
321 if (!$(this).val()) {
322 CRM
.menubar
.open('QuickSearch');
326 $('#crm-qsearch > a').keyup(function(e
) {
327 if ($(e
.target
).is(this)) {
328 $('#crm-qsearch-input').focus();
332 $('#crm-qsearch form[name=search_block]').on('submit', function() {
333 if (!$('#crm-qsearch-input').val()) {
336 var $menu
= $('#crm-qsearch-input').autocomplete('widget');
337 if ($('li.ui-menu-item', $menu
).length
=== 1) {
338 var cid
= $('li.ui-menu-item', $menu
).data('ui-autocomplete-item').value
;
340 document
.location
= CRM
.url('civicrm/contact/view', {reset
: 1, cid
: cid
});
345 $('#civicrm-menu').on('show.smapi', function(e
, menu
) {
346 if ($(menu
).parent().attr('data-name') === 'QuickSearch') {
347 $('#crm-qsearch-input').focus();
350 function setQuickSearchValue() {
351 var $selection
= $('.crm-quickSearchField input:checked'),
352 label
= $selection
.parent().text(),
353 value
= $selection
.val();
354 // These fields are not supported by advanced search
355 if (!value
|| value
=== 'first_name' || value
=== 'last_name') {
358 $('#crm-qsearch-input').attr({name
: value
, placeholder
: '\uf002 ' + label
});
360 $('.crm-quickSearchField').click(function() {
361 var input
= $('input', this);
362 // Wait for event - its default was prevented by our link handler which interferes with checking the radio input
363 window
.setTimeout(function() {
364 input
.prop('checked', true);
365 CRM
.cache
.set('quickSearchField', input
.val());
366 setQuickSearchValue();
367 $('#crm-qsearch-input').focus().autocomplete("search");
370 $('.crm-quickSearchField input[value="' + CRM
.cache
.get('quickSearchField', 'sort_name') + '"]').prop('checked', true);
371 setQuickSearchValue();
372 $('#civicrm-menu').on('activate.smapi', function(e
, item
) {
373 return !$('ul.crm-quickSearch-results').is(':visible:not(.ui-state-disabled)');
377 '<nav id="civicrm-menu-nav">' +
378 '<input id="crm-menubar-state" type="checkbox" />' +
379 '<label class="crm-menubar-toggle-btn" for="crm-menubar-state">' +
380 '<span class="crm-menu-logo"></span>' +
381 '<span class="crm-menubar-toggle-btn-icon"></span>' +
382 '<%- ts("Toggle main menu") %>' +
384 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
385 '<%= searchTpl({items: search}) %>' +
386 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
390 '<li id="crm-qsearch" data-name="QuickSearch">' +
392 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
394 '<input type="text" id="crm-qsearch-input" name="sort_name" placeholder="\uf002" accesskey="q" />' +
395 '<input type="hidden" name="hidden_location" value="1" />' +
396 '<input type="hidden" name="hidden_custom" value="1" />' +
397 '<input type="hidden" name="qfKey" />' +
398 '<input type="hidden" name="_qf_Advanced_refresh" value="Search" />' +
403 '<% _.forEach(items, function(item) { %>' +
404 '<li><a href="#" class="crm-quickSearchField"><label><input type="radio" value="<%= item.key %>" name="quickSearchField"> <%- item.value %></label></a></li>' +
409 '<% _.forEach(items, function(item) { %>' +
410 '<li <%= attr("li", item) %>>' +
411 '<a <%= attr("a", item) %>>' +
412 '<% if (item.icon) { %>' +
413 '<i class="<%- item.icon %>"></i>' +
415 '<% if (item.label) { %>' +
416 '<span><%- item.label %></span>' +
419 '<% if (item.child) { %>' +
420 '<ul><%= branchTpl({items: item.child, branchTpl: branchTpl}) %></ul>' +
424 }, CRM
.menubar
|| {});
426 function getTpl(name
) {
429 branch
: _
.template(CRM
.menubar
.branchTpl
, {imports
: {_
: _
, attr
: attr
}}),
430 search
: _
.template(CRM
.menubar
.searchTpl
, {imports
: {_
: _
, ts
: ts
, CRM
: CRM
}})
432 templates
.tree
= _
.template(CRM
.menubar
.treeTpl
, {imports
: {branchTpl
: templates
.branch
, searchTpl
: templates
.search
, ts
: ts
}});
434 return templates
[name
];
437 function handleResize() {
438 if ($(window
).width() >= 768 && $('#civicrm-menu').height() > 50) {
439 $('body').addClass('crm-menubar-wrapped');
441 $('body').removeClass('crm-menubar-wrapped');
445 function traverse(items
, itemName
, op
) {
447 _
.each(items
, function(item
, index
) {
448 if (item
.name
=== itemName
) {
449 found
= (op
=== 'parent' ? items
: item
);
450 if (op
=== 'delete') {
451 items
.splice(index
, 1);
456 found
= traverse(item
.child
, itemName
, op
);
465 function attr(el
, item
) {
466 var ret
= [], attr
= _
.cloneDeep(item
.attr
|| {}), a
= ['rel', 'accesskey'];
468 attr
= _
.pick(attr
, a
);
469 attr
.href
= item
.url
|| "#";
471 attr
= _
.omit(attr
, a
);
472 attr
['data-name'] = item
.name
;
473 if (item
.separator
) {
474 attr
.class = (attr
.class ? attr
.class + ' ' : '') + 'crm-menu-border-' + item
.separator
;
477 _
.each(attr
, function(val
, name
) {
478 ret
.push(name
+ '="' + val
+ '"');
480 return ret
.join(' ');
483 CRM
.menubar
.initialize();