Fix mglaman mapping
[civicrm-core.git] / js / crm.menubar.js
CommitLineData
b30809e4
CW
1// https://civicrm.org/licensing
2(function($, _) {
3 "use strict";
4 var templates, initialized,
5 ENTER_KEY = 13,
6 SPACE_KEY = 32;
7 CRM.menubar = _.extend({
8 data: null,
9 settings: {collapsibleBehavior: 'accordion'},
10 position: 'over-cms-menu',
ab77d9df 11 toggleButton: true,
b30809e4
CW
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);
18 } else {
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);
25 insert(markup);
26 });
27 }
28
29 // Wait for crm-container present on the page as it's faster than document.ready
30 function insert(markup) {
1284e3c8 31 if ($('#crm-container').length) {
b30809e4
CW
32 render(markup);
33 } else {
34 new MutationObserver(function(mutations, observer) {
35 _.each(mutations, function(mutant) {
36 _.each(mutant.addedNodes, function(node) {
1284e3c8 37 if ($(node).is('#crm-container')) {
b30809e4
CW
38 render(markup);
39 observer.disconnect();
40 }
41 });
42 });
43 }).observe(document, {childList: true, subtree: true});
44 }
45 }
46
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() {
53 handleResize();
54 $('#civicrm-menu')
55 .on('click', 'a[href="#"]', function() {
56 // For empty links - keep the menu open and don't jump the page anchor
57 return false;
58 })
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.
62 e.stopPropagation();
63 }
64 })
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.
68 return false;
69 })
70 .on('click', 'a[href="#hidemenu"]', function(e) {
71 e.preventDefault();
72 CRM.menubar.hide(250, true);
73 })
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();
78 }
79 })
80 .on('show.smapi', function(e, menu) {
81 // Focus menu when opened with an accesskey
ed10290a
CW
82 if ($(menu).parent().data('name') === 'Home') {
83 $('#crm-menubar-drilldown').focus();
84 } else {
85 $(menu).siblings('a[accesskey]').focus();
86 }
b30809e4
CW
87 })
88 .smartmenus(CRM.menubar.settings);
89 initialized = true;
90 CRM.menubar.initializeResponsive();
91 CRM.menubar.initializeSearch();
ed10290a 92 CRM.menubar.initializeDrill();
b30809e4
CW
93 });
94 }
95 },
96 destroy: function() {
97 $.SmartMenus.destroy();
98 $('#civicrm-menu-nav').remove();
99 initialized = false;
100 $('body[class]').attr('class', function(i, c) {
101 return c.replace(/(^|\s)crm-menubar-\S+/g, '');
102 });
103 },
104 show: function(speed) {
105 if (typeof speed === 'number') {
106 $('#civicrm-menu').slideDown(speed, function() {
107 $(this).css('display', '');
108 });
109 }
110 $('body')
111 .removeClass('crm-menubar-hidden')
112 .addClass('crm-menubar-visible');
113 },
114 hide: function(speed, showMessage) {
115 if (typeof speed === 'number') {
116 $('#civicrm-menu').slideUp(speed, function() {
117 $(this).css('display', '');
118 });
119 }
120 $('body')
121 .addClass('crm-menubar-hidden')
122 .removeClass('crm-menubar-visible');
123 if (showMessage === true && $('#crm-notification-container').length && initialized) {
eb27db61 124 var alert = CRM.alert('<a href="#" id="crm-restore-menu" >' + _.escape(ts('Restore CiviCRM Menu')) + '</a>', ts('Menu hidden'), 'none', {expires: 10000});
b30809e4 125 $('#crm-restore-menu')
b30809e4
CW
126 .click(function(e) {
127 e.preventDefault();
128 alert.close();
129 CRM.menubar.show(speed);
eb27db61 130 });
b30809e4
CW
131 }
132 },
133 open: function(itemName) {
134 var $item = $('li[data-name="' + itemName + '"] > a', '#civicrm-menu');
135 if ($item.length) {
136 $('#civicrm-menu').smartmenus('itemActivate', $item);
137 $item[0].focus();
138 }
139 },
140 close: $.SmartMenus.hideAll,
141 isOpen: function(itemName) {
142 if (itemName) {
143 return !!$('li[data-name="' + itemName + '"] > ul[aria-expanded="true"]', '#civicrm-menu').length;
144 }
145 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length;
146 },
147 spin: function(spin) {
148 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin);
149 },
150 getItem: function(itemName) {
151 return traverse(CRM.menubar.data.menu, itemName, 'get');
152 },
ed10290a
CW
153 findItems: function(searchTerm) {
154 return findRecursive(CRM.menubar.data.menu, searchTerm.toLowerCase().replace(/ /g, ''));
155 },
b30809e4
CW
156 addItems: function(position, targetName, items) {
157 var list, container, $ul;
158 if (position === 'before' || position === 'after') {
159 if (!targetName) {
160 throw 'Cannot add sibling of main menu';
161 }
162 list = traverse(CRM.menubar.data.menu, targetName, 'parent');
163 if (!list) {
164 throw targetName + ' not found';
165 }
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');
171 if (!container) {
172 throw targetName + ' not found';
173 }
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>');
179 }
180 $ul = $target.children('ul').first();
181 } else {
182 list = CRM.menubar.data.menu;
183 }
184 if (position < 0) {
185 position = list.length + 1 + position;
186 }
187 if (position >= list.length) {
188 list.push.apply(list, items);
189 position = list.length - 1;
190 } else {
191 list.splice.apply(list, [position, 0].concat(items));
192 }
193 if (targetName && !$ul.is('#civicrm-menu')) {
194 $ul.html(getTpl('branch')({items: list, branchTpl: getTpl('branch')}));
195 } else {
196 $('#civicrm-menu > li').eq(position).after(getTpl('branch')({items: items, branchTpl: getTpl('branch')}));
197 }
198 CRM.menubar.refresh();
199 },
200 removeItem: function(itemName) {
201 var item = traverse(CRM.menubar.data.menu, itemName, 'delete');
202 if (item) {
203 $('li[data-name="' + itemName + '"]', '#civicrm-menu').remove();
204 CRM.menubar.refresh();
205 }
206 return item;
207 },
208 updateItem: function(item) {
209 if (!item.name) {
210 throw 'No name passed to CRM.menubar.updateItem';
211 }
212 var menuItem = CRM.menubar.getItem(item.name);
213 if (!menuItem) {
214 throw item.name + ' not found';
215 }
216 _.extend(menuItem, item);
217 $('li[data-name="' + item.name + '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items: [menuItem], branchTpl: getTpl('branch')}));
218 CRM.menubar.refresh();
219 },
220 refresh: function() {
221 if (initialized) {
222 $('#civicrm-menu').smartmenus('refresh');
223 handleResize();
224 }
225 },
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';
229 handleResize();
230 if (persist !== false) {
231 CRM.cache.set('menubarPosition', CRM.menubar.position);
232 }
233 },
234 initializePosition: function() {
ab77d9df 235 if (CRM.menubar.toggleButton && (CRM.menubar.position === 'over-cms-menu' || CRM.menubar.position === 'below-cms-menu')) {
b30809e4
CW
236 $('#civicrm-menu')
237 .on('click', 'a[href="#toggle-position"]', function(e) {
238 e.preventDefault();
239 CRM.menubar.togglePosition();
240 })
13a3d214 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>');
b30809e4
CW
242 CRM.menubar.position = CRM.cache.get('menubarPosition', CRM.menubar.position);
243 }
244 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM.menubar.position);
245 },
ab77d9df
CW
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();
251 }
252 },
b30809e4
CW
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();
260 }
261 })
262 .on('resize', function() {
fba7aa1c 263 if (!isMobile() && $mainMenuState[0].checked) {
b30809e4
CW
264 $mainMenuState[0].click();
265 }
266 handleResize();
267 });
268 $mainMenuState.click(function() {
269 // Use absolute position instead of fixed when open to allow scrolling menu
270 var open = $(this).is(':checked');
271 if (open) {
272 window.scroll({top: 0});
273 }
274 $('#civicrm-menu-nav')
275 .css('position', open ? 'absolute' : '')
276 .parentsUntil('body')
277 .css('position', open ? 'static' : '');
278 });
279 },
280 initializeSearch: function() {
281 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM.menubar.qfKey);
282 $('#crm-qsearch-input')
283 .autocomplete({
284 source: function(request, response) {
285 //start spinning the civi logo
286 CRM.menubar.spin(true);
287 var
288 option = $('input[name=quickSearchField]:checked'),
289 params = {
290 name: request.term,
291 field_name: option.val()
292 };
293 CRM.api3('contact', 'getquick', params).done(function(result) {
294 var ret = [];
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});
299 });
300 } else {
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?');
307 }
308 ret.push({value: '0', label: msg});
309 }
310 response(ret);
311 //stop spinning the civi logo
312 CRM.menubar.spin(false);
313 CRM.menubar.close();
314 });
315 },
316 focus: function (event, ui) {
317 return false;
318 },
319 select: function (event, ui) {
320 if (ui.item.value > 0) {
321 document.location = CRM.url('civicrm/contact/view', {reset: 1, cid: ui.item.value});
322 }
323 return false;
324 },
325 create: function() {
326 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
327 }
328 })
329 .on('keyup change', function() {
330 $(this).toggleClass('has-user-input', !!$(this).val());
331 })
332 .keyup(function(e) {
333 CRM.menubar.close();
334 if (e.which === ENTER_KEY) {
335 if (!$(this).val()) {
336 CRM.menubar.open('QuickSearch');
337 }
338 }
339 });
340 $('#crm-qsearch > a').keyup(function(e) {
341 if ($(e.target).is(this)) {
342 $('#crm-qsearch-input').focus();
343 CRM.menubar.close();
344 }
345 });
346 $('#crm-qsearch form[name=search_block]').on('submit', function() {
347 if (!$('#crm-qsearch-input').val()) {
348 return false;
349 }
350 var $menu = $('#crm-qsearch-input').autocomplete('widget');
351 if ($('li.ui-menu-item', $menu).length === 1) {
352 var cid = $('li.ui-menu-item', $menu).data('ui-autocomplete-item').value;
353 if (cid > 0) {
354 document.location = CRM.url('civicrm/contact/view', {reset: 1, cid: cid});
355 return false;
356 }
357 }
358 });
359 $('#civicrm-menu').on('show.smapi', function(e, menu) {
360 if ($(menu).parent().attr('data-name') === 'QuickSearch') {
361 $('#crm-qsearch-input').focus();
362 }
363 });
364 function setQuickSearchValue() {
365 var $selection = $('.crm-quickSearchField input:checked'),
366 label = $selection.parent().text(),
367 value = $selection.val();
b30809e4
CW
368 $('#crm-qsearch-input').attr({name: value, placeholder: '\uf002 ' + label});
369 }
370 $('.crm-quickSearchField').click(function() {
371 var input = $('input', this);
372 // Wait for event - its default was prevented by our link handler which interferes with checking the radio input
373 window.setTimeout(function() {
374 input.prop('checked', true);
375 CRM.cache.set('quickSearchField', input.val());
376 setQuickSearchValue();
377 $('#crm-qsearch-input').focus().autocomplete("search");
378 }, 1);
379 });
4e086328
CW
380 var savedDefault = CRM.cache.get('quickSearchField');
381 if (savedDefault) {
382 $('.crm-quickSearchField input[value="' + savedDefault + '"]').prop('checked', true);
383 } else {
384 $('.crm-quickSearchField:first input').prop('checked', true);
385 }
b30809e4
CW
386 setQuickSearchValue();
387 $('#civicrm-menu').on('activate.smapi', function(e, item) {
388 return !$('ul.crm-quickSearch-results').is(':visible:not(.ui-state-disabled)');
389 });
390 },
ed10290a
CW
391 initializeDrill: function() {
392 $('#civicrm-menu').on('keyup', '#crm-menubar-drilldown', function() {
393 var term = $(this).val(),
394 results = term ? CRM.menubar.findItems(term).slice(0, 20) : [];
395 $(this).parent().next('ul').html(getTpl('branch')({items: results, branchTpl: getTpl('branch'), drillTpl: _.noop}));
396 $('#civicrm-menu').smartmenus('refresh').smartmenus('itemActivate', $(this).closest('a'));
397 });
398 },
b30809e4
CW
399 treeTpl:
400 '<nav id="civicrm-menu-nav">' +
401 '<input id="crm-menubar-state" type="checkbox" />' +
402 '<label class="crm-menubar-toggle-btn" for="crm-menubar-state">' +
403 '<span class="crm-menu-logo"></span>' +
404 '<span class="crm-menubar-toggle-btn-icon"></span>' +
405 '<%- ts("Toggle main menu") %>' +
406 '</label>' +
407 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
408 '<%= searchTpl({items: search}) %>' +
409 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
410 '</ul>' +
411 '</nav>',
412 searchTpl:
413 '<li id="crm-qsearch" data-name="QuickSearch">' +
414 '<a href="#"> ' +
415 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
416 '<div>' +
417 '<input type="text" id="crm-qsearch-input" name="sort_name" placeholder="\uf002" accesskey="q" />' +
418 '<input type="hidden" name="hidden_location" value="1" />' +
419 '<input type="hidden" name="hidden_custom" value="1" />' +
420 '<input type="hidden" name="qfKey" />' +
421 '<input type="hidden" name="_qf_Advanced_refresh" value="Search" />' +
422 '</div>' +
423 '</form>' +
424 '</a>' +
425 '<ul>' +
426 '<% _.forEach(items, function(item) { %>' +
427 '<li><a href="#" class="crm-quickSearchField"><label><input type="radio" value="<%= item.key %>" name="quickSearchField"> <%- item.value %></label></a></li>' +
428 '<% }) %>' +
429 '</ul>' +
430 '</li>',
ed10290a
CW
431 drillTpl:
432 '<li class="crm-menu-border-bottom" data-name="MenubarDrillDown">' +
433 '<a href="#"><input type="text" id="crm-menubar-drilldown" placeholder="' + _.escape(ts('Find menu item...')) + '"></a>' +
434 '<ul></ul>' +
435 '</li>',
b30809e4
CW
436 branchTpl:
437 '<% _.forEach(items, function(item) { %>' +
438 '<li <%= attr("li", item) %>>' +
439 '<a <%= attr("a", item) %>>' +
440 '<% if (item.icon) { %>' +
441 '<i class="<%- item.icon %>"></i>' +
442 '<% } %>' +
443 '<% if (item.label) { %>' +
444 '<span><%- item.label %></span>' +
445 '<% } %>' +
446 '</a>' +
447 '<% if (item.child) { %>' +
ed10290a
CW
448 '<ul>' +
449 '<% if (item.name === "Home") { %><%= drillTpl() %><% } %>' +
450 '<%= branchTpl({items: item.child, branchTpl: branchTpl}) %>' +
451 '</ul>' +
b30809e4
CW
452 '<% } %>' +
453 '</li>' +
454 '<% }) %>'
455 }, CRM.menubar || {});
456
457 function getTpl(name) {
458 if (!templates) {
459 templates = {
ed10290a 460 drill: _.template(CRM.menubar.drillTpl, {}),
b30809e4
CW
461 search: _.template(CRM.menubar.searchTpl, {imports: {_: _, ts: ts, CRM: CRM}})
462 };
ed10290a 463 templates.branch = _.template(CRM.menubar.branchTpl, {imports: {_: _, attr: attr, drillTpl: templates.drill}});
b30809e4
CW
464 templates.tree = _.template(CRM.menubar.treeTpl, {imports: {branchTpl: templates.branch, searchTpl: templates.search, ts: ts}});
465 }
466 return templates[name];
467 }
468
469 function handleResize() {
fba7aa1c 470 if (!isMobile() && ($('#civicrm-menu').height() >= (2 * $('#civicrm-menu > li').height()))) {
b30809e4
CW
471 $('body').addClass('crm-menubar-wrapped');
472 } else {
473 $('body').removeClass('crm-menubar-wrapped');
474 }
475 }
476
fba7aa1c
CW
477 // Figure out if we've hit the mobile breakpoint, based on the rule in crm-menubar.css
478 function isMobile() {
479 return $('.crm-menubar-toggle-btn', '#civicrm-menu-nav').css('top') !== '-99999px';
480 }
481
b30809e4
CW
482 function traverse(items, itemName, op) {
483 var found;
484 _.each(items, function(item, index) {
485 if (item.name === itemName) {
486 found = (op === 'parent' ? items : item);
487 if (op === 'delete') {
488 items.splice(index, 1);
489 }
490 return false;
491 }
492 if (item.child) {
493 found = traverse(item.child, itemName, op);
494 if (found) {
495 return false;
496 }
497 }
498 });
499 return found;
500 }
501
ed10290a
CW
502 function findRecursive(collection, searchTerm) {
503 var items = _.filter(collection, function(item) {
504 return item.label && _.includes(item.label.toLowerCase().replace(/ /g, ''), searchTerm);
505 });
506 _.each(collection, function(item) {
507 if (_.isPlainObject(item) && item.child) {
508 var childMatches = findRecursive(item.child, searchTerm);
509 if (childMatches.length) {
510 Array.prototype.push.apply(items, childMatches);
511 }
512 }
513 });
514 return items;
515 }
516
b30809e4 517 function attr(el, item) {
f519ab9f 518 var ret = [], attr = _.cloneDeep(item.attr || {}), a = ['rel', 'accesskey', 'target'];
b30809e4
CW
519 if (el === 'a') {
520 attr = _.pick(attr, a);
521 attr.href = item.url || "#";
522 } else {
523 attr = _.omit(attr, a);
524 attr['data-name'] = item.name;
525 if (item.separator) {
526 attr.class = (attr.class ? attr.class + ' ' : '') + 'crm-menu-border-' + item.separator;
527 }
528 }
529 _.each(attr, function(val, name) {
530 ret.push(name + '="' + val + '"');
531 });
532 return ret.join(' ');
533 }
534
535 CRM.menubar.initialize();
536
537})(CRM.$, CRM._);