Merge pull request #15037 from demeritcowboy/fin-account-limbo
[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',
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) {
e5abda02 30 if ($('#crm-container').length) {
b30809e4
CW
31 render(markup);
32 } else {
33 new MutationObserver(function(mutations, observer) {
34 _.each(mutations, function(mutant) {
35 _.each(mutant.addedNodes, function(node) {
e5abda02 36 if ($(node).is('#crm-container')) {
b30809e4
CW
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
a356d68f 81 $(menu).siblings('a[accesskey]').focus();
b30809e4
CW
82 })
83 .smartmenus(CRM.menubar.settings);
84 initialized = true;
85 CRM.menubar.initializeResponsive();
86 CRM.menubar.initializeSearch();
87 });
88 }
89 },
90 destroy: function() {
91 $.SmartMenus.destroy();
92 $('#civicrm-menu-nav').remove();
93 initialized = false;
94 $('body[class]').attr('class', function(i, c) {
95 return c.replace(/(^|\s)crm-menubar-\S+/g, '');
96 });
97 },
98 show: function(speed) {
99 if (typeof speed === 'number') {
100 $('#civicrm-menu').slideDown(speed, function() {
101 $(this).css('display', '');
102 });
103 }
104 $('body')
105 .removeClass('crm-menubar-hidden')
106 .addClass('crm-menubar-visible');
107 },
108 hide: function(speed, showMessage) {
109 if (typeof speed === 'number') {
110 $('#civicrm-menu').slideUp(speed, function() {
111 $(this).css('display', '');
112 });
113 }
114 $('body')
115 .addClass('crm-menubar-hidden')
116 .removeClass('crm-menubar-visible');
117 if (showMessage === true && $('#crm-notification-container').length && initialized) {
eb27db61 118 var alert = CRM.alert('<a href="#" id="crm-restore-menu" >' + _.escape(ts('Restore CiviCRM Menu')) + '</a>', ts('Menu hidden'), 'none', {expires: 10000});
b30809e4 119 $('#crm-restore-menu')
b30809e4
CW
120 .click(function(e) {
121 e.preventDefault();
122 alert.close();
123 CRM.menubar.show(speed);
eb27db61 124 });
b30809e4
CW
125 }
126 },
127 open: function(itemName) {
128 var $item = $('li[data-name="' + itemName + '"] > a', '#civicrm-menu');
129 if ($item.length) {
130 $('#civicrm-menu').smartmenus('itemActivate', $item);
131 $item[0].focus();
132 }
133 },
134 close: $.SmartMenus.hideAll,
135 isOpen: function(itemName) {
136 if (itemName) {
137 return !!$('li[data-name="' + itemName + '"] > ul[aria-expanded="true"]', '#civicrm-menu').length;
138 }
139 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length;
140 },
141 spin: function(spin) {
142 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin);
143 },
144 getItem: function(itemName) {
145 return traverse(CRM.menubar.data.menu, itemName, 'get');
146 },
147 addItems: function(position, targetName, items) {
148 var list, container, $ul;
149 if (position === 'before' || position === 'after') {
150 if (!targetName) {
151 throw 'Cannot add sibling of main menu';
152 }
153 list = traverse(CRM.menubar.data.menu, targetName, 'parent');
154 if (!list) {
155 throw targetName + ' not found';
156 }
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');
162 if (!container) {
163 throw targetName + ' not found';
164 }
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>');
170 }
171 $ul = $target.children('ul').first();
172 } else {
173 list = CRM.menubar.data.menu;
174 }
175 if (position < 0) {
176 position = list.length + 1 + position;
177 }
178 if (position >= list.length) {
179 list.push.apply(list, items);
180 position = list.length - 1;
181 } else {
182 list.splice.apply(list, [position, 0].concat(items));
183 }
184 if (targetName && !$ul.is('#civicrm-menu')) {
185 $ul.html(getTpl('branch')({items: list, branchTpl: getTpl('branch')}));
186 } else {
187 $('#civicrm-menu > li').eq(position).after(getTpl('branch')({items: items, branchTpl: getTpl('branch')}));
188 }
189 CRM.menubar.refresh();
190 },
191 removeItem: function(itemName) {
192 var item = traverse(CRM.menubar.data.menu, itemName, 'delete');
193 if (item) {
194 $('li[data-name="' + itemName + '"]', '#civicrm-menu').remove();
195 CRM.menubar.refresh();
196 }
197 return item;
198 },
199 updateItem: function(item) {
200 if (!item.name) {
201 throw 'No name passed to CRM.menubar.updateItem';
202 }
203 var menuItem = CRM.menubar.getItem(item.name);
204 if (!menuItem) {
205 throw item.name + ' not found';
206 }
207 _.extend(menuItem, item);
208 $('li[data-name="' + item.name + '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items: [menuItem], branchTpl: getTpl('branch')}));
209 CRM.menubar.refresh();
210 },
211 refresh: function() {
212 if (initialized) {
213 $('#civicrm-menu').smartmenus('refresh');
214 handleResize();
215 }
216 },
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';
220 handleResize();
221 if (persist !== false) {
222 CRM.cache.set('menubarPosition', CRM.menubar.position);
223 }
224 },
225 initializePosition: function() {
226 if (CRM.menubar.position === 'over-cms-menu' || CRM.menubar.position === 'below-cms-menu') {
227 $('#civicrm-menu')
228 .on('click', 'a[href="#toggle-position"]', function(e) {
229 e.preventDefault();
230 CRM.menubar.togglePosition();
231 })
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);
234 }
235 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM.menubar.position);
236 },
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();
244 }
245 })
246 .on('resize', function() {
fba7aa1c 247 if (!isMobile() && $mainMenuState[0].checked) {
b30809e4
CW
248 $mainMenuState[0].click();
249 }
250 handleResize();
251 });
252 $mainMenuState.click(function() {
253 // Use absolute position instead of fixed when open to allow scrolling menu
254 var open = $(this).is(':checked');
255 if (open) {
256 window.scroll({top: 0});
257 }
258 $('#civicrm-menu-nav')
259 .css('position', open ? 'absolute' : '')
260 .parentsUntil('body')
261 .css('position', open ? 'static' : '');
262 });
263 },
264 initializeSearch: function() {
265 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM.menubar.qfKey);
266 $('#crm-qsearch-input')
267 .autocomplete({
268 source: function(request, response) {
269 //start spinning the civi logo
270 CRM.menubar.spin(true);
271 var
272 option = $('input[name=quickSearchField]:checked'),
273 params = {
274 name: request.term,
275 field_name: option.val()
276 };
277 CRM.api3('contact', 'getquick', params).done(function(result) {
278 var ret = [];
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});
283 });
284 } else {
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?');
291 }
292 ret.push({value: '0', label: msg});
293 }
294 response(ret);
295 //stop spinning the civi logo
296 CRM.menubar.spin(false);
297 CRM.menubar.close();
298 });
299 },
300 focus: function (event, ui) {
301 return false;
302 },
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});
306 }
307 return false;
308 },
309 create: function() {
310 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
311 }
312 })
313 .on('keyup change', function() {
314 $(this).toggleClass('has-user-input', !!$(this).val());
315 })
316 .keyup(function(e) {
317 CRM.menubar.close();
318 if (e.which === ENTER_KEY) {
319 if (!$(this).val()) {
320 CRM.menubar.open('QuickSearch');
321 }
322 }
323 });
324 $('#crm-qsearch > a').keyup(function(e) {
325 if ($(e.target).is(this)) {
326 $('#crm-qsearch-input').focus();
327 CRM.menubar.close();
328 }
329 });
330 $('#crm-qsearch form[name=search_block]').on('submit', function() {
331 if (!$('#crm-qsearch-input').val()) {
332 return false;
333 }
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;
337 if (cid > 0) {
338 document.location = CRM.url('civicrm/contact/view', {reset: 1, cid: cid});
339 return false;
340 }
341 }
342 });
343 $('#civicrm-menu').on('show.smapi', function(e, menu) {
344 if ($(menu).parent().attr('data-name') === 'QuickSearch') {
345 $('#crm-qsearch-input').focus();
346 }
347 });
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') {
354 value = 'sort_name';
355 }
356 $('#crm-qsearch-input').attr({name: value, placeholder: '\uf002 ' + label});
357 }
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");
366 }, 1);
367 });
4e086328
CW
368 var savedDefault = CRM.cache.get('quickSearchField');
369 if (savedDefault) {
370 $('.crm-quickSearchField input[value="' + savedDefault + '"]').prop('checked', true);
371 } else {
372 $('.crm-quickSearchField:first input').prop('checked', true);
373 }
b30809e4
CW
374 setQuickSearchValue();
375 $('#civicrm-menu').on('activate.smapi', function(e, item) {
376 return !$('ul.crm-quickSearch-results').is(':visible:not(.ui-state-disabled)');
377 });
378 },
379 treeTpl:
380 '<nav id="civicrm-menu-nav">' +
381 '<input id="crm-menubar-state" type="checkbox" />' +
382 '<label class="crm-menubar-toggle-btn" for="crm-menubar-state">' +
383 '<span class="crm-menu-logo"></span>' +
384 '<span class="crm-menubar-toggle-btn-icon"></span>' +
385 '<%- ts("Toggle main menu") %>' +
386 '</label>' +
387 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
388 '<%= searchTpl({items: search}) %>' +
389 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
390 '</ul>' +
391 '</nav>',
392 searchTpl:
393 '<li id="crm-qsearch" data-name="QuickSearch">' +
394 '<a href="#"> ' +
395 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
396 '<div>' +
397 '<input type="text" id="crm-qsearch-input" name="sort_name" placeholder="\uf002" accesskey="q" />' +
398 '<input type="hidden" name="hidden_location" value="1" />' +
399 '<input type="hidden" name="hidden_custom" value="1" />' +
400 '<input type="hidden" name="qfKey" />' +
401 '<input type="hidden" name="_qf_Advanced_refresh" value="Search" />' +
402 '</div>' +
403 '</form>' +
404 '</a>' +
405 '<ul>' +
406 '<% _.forEach(items, function(item) { %>' +
407 '<li><a href="#" class="crm-quickSearchField"><label><input type="radio" value="<%= item.key %>" name="quickSearchField"> <%- item.value %></label></a></li>' +
408 '<% }) %>' +
409 '</ul>' +
410 '</li>',
411 branchTpl:
412 '<% _.forEach(items, function(item) { %>' +
413 '<li <%= attr("li", item) %>>' +
414 '<a <%= attr("a", item) %>>' +
415 '<% if (item.icon) { %>' +
416 '<i class="<%- item.icon %>"></i>' +
417 '<% } %>' +
418 '<% if (item.label) { %>' +
419 '<span><%- item.label %></span>' +
420 '<% } %>' +
421 '</a>' +
422 '<% if (item.child) { %>' +
423 '<ul><%= branchTpl({items: item.child, branchTpl: branchTpl}) %></ul>' +
424 '<% } %>' +
425 '</li>' +
426 '<% }) %>'
427 }, CRM.menubar || {});
428
429 function getTpl(name) {
430 if (!templates) {
431 templates = {
432 branch: _.template(CRM.menubar.branchTpl, {imports: {_: _, attr: attr}}),
433 search: _.template(CRM.menubar.searchTpl, {imports: {_: _, ts: ts, CRM: CRM}})
434 };
435 templates.tree = _.template(CRM.menubar.treeTpl, {imports: {branchTpl: templates.branch, searchTpl: templates.search, ts: ts}});
436 }
437 return templates[name];
438 }
439
440 function handleResize() {
fba7aa1c 441 if (!isMobile() && ($('#civicrm-menu').height() >= (2 * $('#civicrm-menu > li').height()))) {
b30809e4
CW
442 $('body').addClass('crm-menubar-wrapped');
443 } else {
444 $('body').removeClass('crm-menubar-wrapped');
445 }
446 }
447
fba7aa1c
CW
448 // Figure out if we've hit the mobile breakpoint, based on the rule in crm-menubar.css
449 function isMobile() {
450 return $('.crm-menubar-toggle-btn', '#civicrm-menu-nav').css('top') !== '-99999px';
451 }
452
b30809e4
CW
453 function traverse(items, itemName, op) {
454 var found;
455 _.each(items, function(item, index) {
456 if (item.name === itemName) {
457 found = (op === 'parent' ? items : item);
458 if (op === 'delete') {
459 items.splice(index, 1);
460 }
461 return false;
462 }
463 if (item.child) {
464 found = traverse(item.child, itemName, op);
465 if (found) {
466 return false;
467 }
468 }
469 });
470 return found;
471 }
472
473 function attr(el, item) {
474 var ret = [], attr = _.cloneDeep(item.attr || {}), a = ['rel', 'accesskey'];
475 if (el === 'a') {
476 attr = _.pick(attr, a);
477 attr.href = item.url || "#";
478 } else {
479 attr = _.omit(attr, a);
480 attr['data-name'] = item.name;
481 if (item.separator) {
482 attr.class = (attr.class ? attr.class + ' ' : '') + 'crm-menu-border-' + item.separator;
483 }
484 }
485 _.each(attr, function(val, name) {
486 ret.push(name + '="' + val + '"');
487 });
488 return ret.join(' ');
489 }
490
491 CRM.menubar.initialize();
492
493})(CRM.$, CRM._);