Merge pull request #17211 from eileenmcnaughton/id
[civicrm-core.git] / js / crm.menubar.js
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',
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);
17 } else {
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);
24 insert(markup);
25 });
26 }
27
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) {
31 render(markup);
32 } else {
33 new MutationObserver(function(mutations, observer) {
34 _.each(mutations, function(mutant) {
35 _.each(mutant.addedNodes, function(node) {
36 if ($(node).is('#crm-container')) {
37 render(markup);
38 observer.disconnect();
39 }
40 });
41 });
42 }).observe(document, {childList: true, subtree: true});
43 }
44 }
45
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() {
52 handleResize();
53 $('#civicrm-menu')
54 .on('click', 'a[href="#"]', function() {
55 // For empty links - keep the menu open and don't jump the page anchor
56 return false;
57 })
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.
61 e.stopPropagation();
62 }
63 })
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.
67 return false;
68 })
69 .on('click', 'a[href="#hidemenu"]', function(e) {
70 e.preventDefault();
71 CRM.menubar.hide(250, true);
72 })
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();
77 }
78 })
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();
83 } else {
84 $(menu).siblings('a[accesskey]').focus();
85 }
86 })
87 .smartmenus(CRM.menubar.settings);
88 initialized = true;
89 CRM.menubar.initializeResponsive();
90 CRM.menubar.initializeSearch();
91 CRM.menubar.initializeDrill();
92 });
93 }
94 },
95 destroy: function() {
96 $.SmartMenus.destroy();
97 $('#civicrm-menu-nav').remove();
98 initialized = false;
99 $('body[class]').attr('class', function(i, c) {
100 return c.replace(/(^|\s)crm-menubar-\S+/g, '');
101 });
102 },
103 show: function(speed) {
104 if (typeof speed === 'number') {
105 $('#civicrm-menu').slideDown(speed, function() {
106 $(this).css('display', '');
107 });
108 }
109 $('body')
110 .removeClass('crm-menubar-hidden')
111 .addClass('crm-menubar-visible');
112 },
113 hide: function(speed, showMessage) {
114 if (typeof speed === 'number') {
115 $('#civicrm-menu').slideUp(speed, function() {
116 $(this).css('display', '');
117 });
118 }
119 $('body')
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')
125 .click(function(e) {
126 e.preventDefault();
127 alert.close();
128 CRM.menubar.show(speed);
129 });
130 }
131 },
132 open: function(itemName) {
133 var $item = $('li[data-name="' + itemName + '"] > a', '#civicrm-menu');
134 if ($item.length) {
135 $('#civicrm-menu').smartmenus('itemActivate', $item);
136 $item[0].focus();
137 }
138 },
139 close: $.SmartMenus.hideAll,
140 isOpen: function(itemName) {
141 if (itemName) {
142 return !!$('li[data-name="' + itemName + '"] > ul[aria-expanded="true"]', '#civicrm-menu').length;
143 }
144 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length;
145 },
146 spin: function(spin) {
147 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin);
148 },
149 getItem: function(itemName) {
150 return traverse(CRM.menubar.data.menu, itemName, 'get');
151 },
152 findItems: function(searchTerm) {
153 return findRecursive(CRM.menubar.data.menu, searchTerm.toLowerCase().replace(/ /g, ''));
154 },
155 addItems: function(position, targetName, items) {
156 var list, container, $ul;
157 if (position === 'before' || position === 'after') {
158 if (!targetName) {
159 throw 'Cannot add sibling of main menu';
160 }
161 list = traverse(CRM.menubar.data.menu, targetName, 'parent');
162 if (!list) {
163 throw targetName + ' not found';
164 }
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');
170 if (!container) {
171 throw targetName + ' not found';
172 }
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>');
178 }
179 $ul = $target.children('ul').first();
180 } else {
181 list = CRM.menubar.data.menu;
182 }
183 if (position < 0) {
184 position = list.length + 1 + position;
185 }
186 if (position >= list.length) {
187 list.push.apply(list, items);
188 position = list.length - 1;
189 } else {
190 list.splice.apply(list, [position, 0].concat(items));
191 }
192 if (targetName && !$ul.is('#civicrm-menu')) {
193 $ul.html(getTpl('branch')({items: list, branchTpl: getTpl('branch')}));
194 } else {
195 $('#civicrm-menu > li').eq(position).after(getTpl('branch')({items: items, branchTpl: getTpl('branch')}));
196 }
197 CRM.menubar.refresh();
198 },
199 removeItem: function(itemName) {
200 var item = traverse(CRM.menubar.data.menu, itemName, 'delete');
201 if (item) {
202 $('li[data-name="' + itemName + '"]', '#civicrm-menu').remove();
203 CRM.menubar.refresh();
204 }
205 return item;
206 },
207 updateItem: function(item) {
208 if (!item.name) {
209 throw 'No name passed to CRM.menubar.updateItem';
210 }
211 var menuItem = CRM.menubar.getItem(item.name);
212 if (!menuItem) {
213 throw item.name + ' not found';
214 }
215 _.extend(menuItem, item);
216 $('li[data-name="' + item.name + '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items: [menuItem], branchTpl: getTpl('branch')}));
217 CRM.menubar.refresh();
218 },
219 refresh: function() {
220 if (initialized) {
221 $('#civicrm-menu').smartmenus('refresh');
222 handleResize();
223 }
224 },
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';
228 handleResize();
229 if (persist !== false) {
230 CRM.cache.set('menubarPosition', CRM.menubar.position);
231 }
232 },
233 initializePosition: function() {
234 if (CRM.menubar.position === 'over-cms-menu' || CRM.menubar.position === 'below-cms-menu') {
235 $('#civicrm-menu')
236 .on('click', 'a[href="#toggle-position"]', function(e) {
237 e.preventDefault();
238 CRM.menubar.togglePosition();
239 })
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);
242 }
243 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM.menubar.position);
244 },
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();
252 }
253 })
254 .on('resize', function() {
255 if (!isMobile() && $mainMenuState[0].checked) {
256 $mainMenuState[0].click();
257 }
258 handleResize();
259 });
260 $mainMenuState.click(function() {
261 // Use absolute position instead of fixed when open to allow scrolling menu
262 var open = $(this).is(':checked');
263 if (open) {
264 window.scroll({top: 0});
265 }
266 $('#civicrm-menu-nav')
267 .css('position', open ? 'absolute' : '')
268 .parentsUntil('body')
269 .css('position', open ? 'static' : '');
270 });
271 },
272 initializeSearch: function() {
273 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM.menubar.qfKey);
274 $('#crm-qsearch-input')
275 .autocomplete({
276 source: function(request, response) {
277 //start spinning the civi logo
278 CRM.menubar.spin(true);
279 var
280 option = $('input[name=quickSearchField]:checked'),
281 params = {
282 name: request.term,
283 field_name: option.val()
284 };
285 CRM.api3('contact', 'getquick', params).done(function(result) {
286 var ret = [];
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});
291 });
292 } else {
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?');
299 }
300 ret.push({value: '0', label: msg});
301 }
302 response(ret);
303 //stop spinning the civi logo
304 CRM.menubar.spin(false);
305 CRM.menubar.close();
306 });
307 },
308 focus: function (event, ui) {
309 return false;
310 },
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});
314 }
315 return false;
316 },
317 create: function() {
318 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
319 }
320 })
321 .on('keyup change', function() {
322 $(this).toggleClass('has-user-input', !!$(this).val());
323 })
324 .keyup(function(e) {
325 CRM.menubar.close();
326 if (e.which === ENTER_KEY) {
327 if (!$(this).val()) {
328 CRM.menubar.open('QuickSearch');
329 }
330 }
331 });
332 $('#crm-qsearch > a').keyup(function(e) {
333 if ($(e.target).is(this)) {
334 $('#crm-qsearch-input').focus();
335 CRM.menubar.close();
336 }
337 });
338 $('#crm-qsearch form[name=search_block]').on('submit', function() {
339 if (!$('#crm-qsearch-input').val()) {
340 return false;
341 }
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;
345 if (cid > 0) {
346 document.location = CRM.url('civicrm/contact/view', {reset: 1, cid: cid});
347 return false;
348 }
349 }
350 });
351 $('#civicrm-menu').on('show.smapi', function(e, menu) {
352 if ($(menu).parent().attr('data-name') === 'QuickSearch') {
353 $('#crm-qsearch-input').focus();
354 }
355 });
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') {
362 value = 'sort_name';
363 }
364 $('#crm-qsearch-input').attr({name: value, placeholder: '\uf002 ' + label});
365 }
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");
374 }, 1);
375 });
376 var savedDefault = CRM.cache.get('quickSearchField');
377 if (savedDefault) {
378 $('.crm-quickSearchField input[value="' + savedDefault + '"]').prop('checked', true);
379 } else {
380 $('.crm-quickSearchField:first input').prop('checked', true);
381 }
382 setQuickSearchValue();
383 $('#civicrm-menu').on('activate.smapi', function(e, item) {
384 return !$('ul.crm-quickSearch-results').is(':visible:not(.ui-state-disabled)');
385 });
386 },
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'));
393 });
394 },
395 treeTpl:
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") %>' +
402 '</label>' +
403 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
404 '<%= searchTpl({items: search}) %>' +
405 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
406 '</ul>' +
407 '</nav>',
408 searchTpl:
409 '<li id="crm-qsearch" data-name="QuickSearch">' +
410 '<a href="#"> ' +
411 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
412 '<div>' +
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" />' +
418 '</div>' +
419 '</form>' +
420 '</a>' +
421 '<ul>' +
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>' +
424 '<% }) %>' +
425 '</ul>' +
426 '</li>',
427 drillTpl:
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>' +
430 '<ul></ul>' +
431 '</li>',
432 branchTpl:
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>' +
438 '<% } %>' +
439 '<% if (item.label) { %>' +
440 '<span><%- item.label %></span>' +
441 '<% } %>' +
442 '</a>' +
443 '<% if (item.child) { %>' +
444 '<ul>' +
445 '<% if (item.name === "Home") { %><%= drillTpl() %><% } %>' +
446 '<%= branchTpl({items: item.child, branchTpl: branchTpl}) %>' +
447 '</ul>' +
448 '<% } %>' +
449 '</li>' +
450 '<% }) %>'
451 }, CRM.menubar || {});
452
453 function getTpl(name) {
454 if (!templates) {
455 templates = {
456 drill: _.template(CRM.menubar.drillTpl, {}),
457 search: _.template(CRM.menubar.searchTpl, {imports: {_: _, ts: ts, CRM: CRM}})
458 };
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}});
461 }
462 return templates[name];
463 }
464
465 function handleResize() {
466 if (!isMobile() && ($('#civicrm-menu').height() >= (2 * $('#civicrm-menu > li').height()))) {
467 $('body').addClass('crm-menubar-wrapped');
468 } else {
469 $('body').removeClass('crm-menubar-wrapped');
470 }
471 }
472
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';
476 }
477
478 function traverse(items, itemName, op) {
479 var found;
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);
485 }
486 return false;
487 }
488 if (item.child) {
489 found = traverse(item.child, itemName, op);
490 if (found) {
491 return false;
492 }
493 }
494 });
495 return found;
496 }
497
498 function findRecursive(collection, searchTerm) {
499 var items = _.filter(collection, function(item) {
500 return item.label && _.includes(item.label.toLowerCase().replace(/ /g, ''), searchTerm);
501 });
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);
507 }
508 }
509 });
510 return items;
511 }
512
513 function attr(el, item) {
514 var ret = [], attr = _.cloneDeep(item.attr || {}), a = ['rel', 'accesskey', 'target'];
515 if (el === 'a') {
516 attr = _.pick(attr, a);
517 attr.href = item.url || "#";
518 } else {
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;
523 }
524 }
525 _.each(attr, function(val, name) {
526 ret.push(name + '="' + val + '"');
527 });
528 return ret.join(' ');
529 }
530
531 CRM.menubar.initialize();
532
533 })(CRM.$, CRM._);