Merge pull request #13853 from agh1/5.12.0-releasenotes
[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) {
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 $(menu).siblings('a[accesskey]:not(:hover)').focus();
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() {
247 if ($(window).width() >= 768 && $mainMenuState[0].checked) {
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 });
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)');
372 });
373 },
374 treeTpl:
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") %>' +
381 '</label>' +
382 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
383 '<%= searchTpl({items: search}) %>' +
384 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
385 '</ul>' +
386 '</nav>',
387 searchTpl:
388 '<li id="crm-qsearch" data-name="QuickSearch">' +
389 '<a href="#"> ' +
390 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
391 '<div>' +
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" />' +
397 '</div>' +
398 '</form>' +
399 '</a>' +
400 '<ul>' +
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>' +
403 '<% }) %>' +
404 '</ul>' +
405 '</li>',
406 branchTpl:
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>' +
412 '<% } %>' +
413 '<% if (item.label) { %>' +
414 '<span><%- item.label %></span>' +
415 '<% } %>' +
416 '</a>' +
417 '<% if (item.child) { %>' +
418 '<ul><%= branchTpl({items: item.child, branchTpl: branchTpl}) %></ul>' +
419 '<% } %>' +
420 '</li>' +
421 '<% }) %>'
422 }, CRM.menubar || {});
423
424 function getTpl(name) {
425 if (!templates) {
426 templates = {
427 branch: _.template(CRM.menubar.branchTpl, {imports: {_: _, attr: attr}}),
428 search: _.template(CRM.menubar.searchTpl, {imports: {_: _, ts: ts, CRM: CRM}})
429 };
430 templates.tree = _.template(CRM.menubar.treeTpl, {imports: {branchTpl: templates.branch, searchTpl: templates.search, ts: ts}});
431 }
432 return templates[name];
433 }
434
435 function handleResize() {
436 if ($(window).width() >= 768 && $('#civicrm-menu').height() > 50) {
437 $('body').addClass('crm-menubar-wrapped');
438 } else {
439 $('body').removeClass('crm-menubar-wrapped');
440 }
441 }
442
443 function traverse(items, itemName, op) {
444 var found;
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);
450 }
451 return false;
452 }
453 if (item.child) {
454 found = traverse(item.child, itemName, op);
455 if (found) {
456 return false;
457 }
458 }
459 });
460 return found;
461 }
462
463 function attr(el, item) {
464 var ret = [], attr = _.cloneDeep(item.attr || {}), a = ['rel', 'accesskey'];
465 if (el === 'a') {
466 attr = _.pick(attr, a);
467 attr.href = item.url || "#";
468 } else {
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;
473 }
474 }
475 _.each(attr, function(val, name) {
476 ret.push(name + '="' + val + '"');
477 });
478 return ret.join(' ');
479 }
480
481 CRM.menubar.initialize();
482
483})(CRM.$, CRM._);