Merge pull request #13768 from eileenmcnaughton/activity_yay
[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) {
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'}})
121 .click(function(e) {
122 e.preventDefault();
123 alert.close();
124 CRM.menubar.show(speed);
125 })
126 .parent().css('text-align', 'center').find('.ui-button-text').css({'padding-top': '4px', 'padding-bottom': '4px'});
127 }
128 },
129 open: function(itemName) {
130 var $item = $('li[data-name="' + itemName + '"] > a', '#civicrm-menu');
131 if ($item.length) {
132 $('#civicrm-menu').smartmenus('itemActivate', $item);
133 $item[0].focus();
134 }
135 },
136 close: $.SmartMenus.hideAll,
137 isOpen: function(itemName) {
138 if (itemName) {
139 return !!$('li[data-name="' + itemName + '"] > ul[aria-expanded="true"]', '#civicrm-menu').length;
140 }
141 return !!$('ul[aria-expanded="true"]', '#civicrm-menu').length;
142 },
143 spin: function(spin) {
144 $('.crm-logo-sm', '#civicrm-menu').toggleClass('fa-spin', spin);
145 },
146 getItem: function(itemName) {
147 return traverse(CRM.menubar.data.menu, itemName, 'get');
148 },
149 addItems: function(position, targetName, items) {
150 var list, container, $ul;
151 if (position === 'before' || position === 'after') {
152 if (!targetName) {
153 throw 'Cannot add sibling of main menu';
154 }
155 list = traverse(CRM.menubar.data.menu, targetName, 'parent');
156 if (!list) {
157 throw targetName + ' not found';
158 }
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');
164 if (!container) {
165 throw targetName + ' not found';
166 }
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>');
172 }
173 $ul = $target.children('ul').first();
174 } else {
175 list = CRM.menubar.data.menu;
176 }
177 if (position < 0) {
178 position = list.length + 1 + position;
179 }
180 if (position >= list.length) {
181 list.push.apply(list, items);
182 position = list.length - 1;
183 } else {
184 list.splice.apply(list, [position, 0].concat(items));
185 }
186 if (targetName && !$ul.is('#civicrm-menu')) {
187 $ul.html(getTpl('branch')({items: list, branchTpl: getTpl('branch')}));
188 } else {
189 $('#civicrm-menu > li').eq(position).after(getTpl('branch')({items: items, branchTpl: getTpl('branch')}));
190 }
191 CRM.menubar.refresh();
192 },
193 removeItem: function(itemName) {
194 var item = traverse(CRM.menubar.data.menu, itemName, 'delete');
195 if (item) {
196 $('li[data-name="' + itemName + '"]', '#civicrm-menu').remove();
197 CRM.menubar.refresh();
198 }
199 return item;
200 },
201 updateItem: function(item) {
202 if (!item.name) {
203 throw 'No name passed to CRM.menubar.updateItem';
204 }
205 var menuItem = CRM.menubar.getItem(item.name);
206 if (!menuItem) {
207 throw item.name + ' not found';
208 }
209 _.extend(menuItem, item);
210 $('li[data-name="' + item.name + '"]', '#civicrm-menu').replaceWith(getTpl('branch')({items: [menuItem], branchTpl: getTpl('branch')}));
211 CRM.menubar.refresh();
212 },
213 refresh: function() {
214 if (initialized) {
215 $('#civicrm-menu').smartmenus('refresh');
216 handleResize();
217 }
218 },
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';
222 handleResize();
223 if (persist !== false) {
224 CRM.cache.set('menubarPosition', CRM.menubar.position);
225 }
226 },
227 initializePosition: function() {
228 if (CRM.menubar.position === 'over-cms-menu' || CRM.menubar.position === 'below-cms-menu') {
229 $('#civicrm-menu')
230 .on('click', 'a[href="#toggle-position"]', function(e) {
231 e.preventDefault();
232 CRM.menubar.togglePosition();
233 })
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);
236 }
237 $('body').addClass('crm-menubar-visible crm-menubar-' + CRM.menubar.position);
238 },
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();
246 }
247 })
248 .on('resize', function() {
249 if ($(window).width() >= 768 && $mainMenuState[0].checked) {
250 $mainMenuState[0].click();
251 }
252 handleResize();
253 });
254 $mainMenuState.click(function() {
255 // Use absolute position instead of fixed when open to allow scrolling menu
256 var open = $(this).is(':checked');
257 if (open) {
258 window.scroll({top: 0});
259 }
260 $('#civicrm-menu-nav')
261 .css('position', open ? 'absolute' : '')
262 .parentsUntil('body')
263 .css('position', open ? 'static' : '');
264 });
265 },
266 initializeSearch: function() {
267 $('input[name=qfKey]', '#crm-qsearch').attr('value', CRM.menubar.qfKey);
268 $('#crm-qsearch-input')
269 .autocomplete({
270 source: function(request, response) {
271 //start spinning the civi logo
272 CRM.menubar.spin(true);
273 var
274 option = $('input[name=quickSearchField]:checked'),
275 params = {
276 name: request.term,
277 field_name: option.val()
278 };
279 CRM.api3('contact', 'getquick', params).done(function(result) {
280 var ret = [];
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});
285 });
286 } else {
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?');
293 }
294 ret.push({value: '0', label: msg});
295 }
296 response(ret);
297 //stop spinning the civi logo
298 CRM.menubar.spin(false);
299 CRM.menubar.close();
300 });
301 },
302 focus: function (event, ui) {
303 return false;
304 },
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});
308 }
309 return false;
310 },
311 create: function() {
312 $(this).autocomplete('widget').addClass('crm-quickSearch-results');
313 }
314 })
315 .on('keyup change', function() {
316 $(this).toggleClass('has-user-input', !!$(this).val());
317 })
318 .keyup(function(e) {
319 CRM.menubar.close();
320 if (e.which === ENTER_KEY) {
321 if (!$(this).val()) {
322 CRM.menubar.open('QuickSearch');
323 }
324 }
325 });
326 $('#crm-qsearch > a').keyup(function(e) {
327 if ($(e.target).is(this)) {
328 $('#crm-qsearch-input').focus();
329 CRM.menubar.close();
330 }
331 });
332 $('#crm-qsearch form[name=search_block]').on('submit', function() {
333 if (!$('#crm-qsearch-input').val()) {
334 return false;
335 }
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;
339 if (cid > 0) {
340 document.location = CRM.url('civicrm/contact/view', {reset: 1, cid: cid});
341 return false;
342 }
343 }
344 });
345 $('#civicrm-menu').on('show.smapi', function(e, menu) {
346 if ($(menu).parent().attr('data-name') === 'QuickSearch') {
347 $('#crm-qsearch-input').focus();
348 }
349 });
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') {
356 value = 'sort_name';
357 }
358 $('#crm-qsearch-input').attr({name: value, placeholder: '\uf002 ' + label});
359 }
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");
368 }, 1);
369 });
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)');
374 });
375 },
376 treeTpl:
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") %>' +
383 '</label>' +
384 '<ul id="civicrm-menu" class="sm sm-civicrm">' +
385 '<%= searchTpl({items: search}) %>' +
386 '<%= branchTpl({items: menu, branchTpl: branchTpl}) %>' +
387 '</ul>' +
388 '</nav>',
389 searchTpl:
390 '<li id="crm-qsearch" data-name="QuickSearch">' +
391 '<a href="#"> ' +
392 '<form action="<%= CRM.url(\'civicrm/contact/search/advanced\') %>" name="search_block" method="post">' +
393 '<div>' +
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" />' +
399 '</div>' +
400 '</form>' +
401 '</a>' +
402 '<ul>' +
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>' +
405 '<% }) %>' +
406 '</ul>' +
407 '</li>',
408 branchTpl:
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>' +
414 '<% } %>' +
415 '<% if (item.label) { %>' +
416 '<span><%- item.label %></span>' +
417 '<% } %>' +
418 '</a>' +
419 '<% if (item.child) { %>' +
420 '<ul><%= branchTpl({items: item.child, branchTpl: branchTpl}) %></ul>' +
421 '<% } %>' +
422 '</li>' +
423 '<% }) %>'
424 }, CRM.menubar || {});
425
426 function getTpl(name) {
427 if (!templates) {
428 templates = {
429 branch: _.template(CRM.menubar.branchTpl, {imports: {_: _, attr: attr}}),
430 search: _.template(CRM.menubar.searchTpl, {imports: {_: _, ts: ts, CRM: CRM}})
431 };
432 templates.tree = _.template(CRM.menubar.treeTpl, {imports: {branchTpl: templates.branch, searchTpl: templates.search, ts: ts}});
433 }
434 return templates[name];
435 }
436
437 function handleResize() {
438 if ($(window).width() >= 768 && $('#civicrm-menu').height() > 50) {
439 $('body').addClass('crm-menubar-wrapped');
440 } else {
441 $('body').removeClass('crm-menubar-wrapped');
442 }
443 }
444
445 function traverse(items, itemName, op) {
446 var found;
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);
452 }
453 return false;
454 }
455 if (item.child) {
456 found = traverse(item.child, itemName, op);
457 if (found) {
458 return false;
459 }
460 }
461 });
462 return found;
463 }
464
465 function attr(el, item) {
466 var ret = [], attr = _.cloneDeep(item.attr || {}), a = ['rel', 'accesskey'];
467 if (el === 'a') {
468 attr = _.pick(attr, a);
469 attr.href = item.url || "#";
470 } else {
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;
475 }
476 }
477 _.each(attr, function(val, name) {
478 ret.push(name + '="' + val + '"');
479 });
480 return ret.join(' ');
481 }
482
483 CRM.menubar.initialize();
484
485})(CRM.$, CRM._);