Merge pull request #24117 from civicrm/5.52
[civicrm-core.git] / ext / afform / admin / ang / afGuiEditor / afGuiEditor.component.js
CommitLineData
b844d2ca
CW
1// https://civicrm.org/licensing
2(function(angular, $, _) {
3 "use strict";
4
3a2ca4f5
TO
5 function backfillEntityDefaults(entity) {
6 // These fields did not exist in prior versions. In absence of explicit, these are the values inferred by runtime/server-side.
7 // We cannot currently backfill schema in the upgrade, so this is the next best opportunity.
8 if (entity.actions === undefined) entity.actions = {create: true, update: true};
9 if (entity.security === undefined) entity.security = 'RBAC';
10 return entity;
11 }
12
b844d2ca
CW
13 angular.module('afGuiEditor').component('afGuiEditor', {
14 templateUrl: '~/afGuiEditor/afGuiEditor.html',
15 bindings: {
490565d0
CW
16 data: '<',
17 entity: '<',
18 mode: '@'
b844d2ca
CW
19 },
20 controllerAs: 'editor',
23fd8685 21 controller: function($scope, crmApi4, afGui, $parse, $timeout, $location) {
67d666c6 22 var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin');
2fb9e655 23
d617083a 24 this.afform = null;
b844d2ca
CW
25 $scope.saving = false;
26 $scope.selectedEntityName = null;
7cc347c2 27 $scope.searchDisplayListFilter = {};
23fd8685 28 this.meta = afGui.meta;
4da60774 29 var editor = this,
835aeacb
CW
30 undoHistory = [],
31 undoPosition = 0,
32 undoAction = null,
1d678002 33 lastSaved,
4da60774 34 sortableOptions = {};
b844d2ca 35
835aeacb
CW
36 // ngModelOptions to debounce input
37 // Used to prevent cluttering the undo history with every keystroke
38 this.debounceMode = {
39 updateOn: 'default blur',
40 debounce: {
41 default: 2000,
42 blur: 0
43 }
44 };
45
46 // Above mode for use with getterSetter
47 this.debounceWithGetterSetter = _.assign({getterSetter: true}, this.debounceMode);
48
b844d2ca 49 this.$onInit = function() {
490565d0
CW
50 // Load the current form plus blocks & fields
51 afGui.resetMeta();
52 afGui.addMeta(this.data);
53 initializeForm();
4da60774
CW
54
55 $timeout(fixEditorHeight);
56 $timeout(editor.adjustTabWidths);
57 $(window)
835aeacb 58 .off('.afGuiEditor')
4da60774 59 .on('resize.afGuiEditor', fixEditorHeight)
835aeacb
CW
60 .on('resize.afGuiEditor', editor.adjustTabWidths)
61 .on('keyup.afGuiEditor', editor.onKeyup);
62
63 // Warn of unsaved changes
64 window.onbeforeunload = function(e) {
65 if (!editor.isSaved()) {
66 e.returnValue = ts("Form has not been saved.");
67 return e.returnValue;
68 }
69 };
4da60774
CW
70 };
71
72 this.$onDestroy = function() {
73 $(window).off('.afGuiEditor');
835aeacb 74 window.onbeforeunload = null;
b844d2ca
CW
75 };
76
835aeacb
CW
77 function setEditorLayout() {
78 editor.layout = {};
79 if (editor.getFormType() === 'form') {
80 editor.layout['#children'] = afGui.findRecursive(editor.afform.layout, {'#tag': 'af-form'})[0]['#children'];
81 }
82 else {
83 editor.layout['#children'] = editor.afform.layout;
84 }
85 }
86
b844d2ca 87 // Initialize the current form
490565d0 88 function initializeForm() {
d617083a
CW
89 editor.afform = editor.data.definition;
90 if (!editor.afform) {
490565d0 91 alert('Error: unknown form');
b844d2ca 92 }
bc6e7a56 93 if (editor.mode === 'clone') {
d617083a
CW
94 delete editor.afform.name;
95 delete editor.afform.server_route;
96 editor.afform.is_dashlet = false;
97 editor.afform.title += ' ' + ts('(copy)');
bc6e7a56 98 }
34c92b2c 99 editor.afform.icon = editor.afform.icon || 'fa-list-alt';
b844d2ca
CW
100 $scope.canvasTab = 'layout';
101 $scope.layoutHtml = '';
fbcd8c17 102 $scope.entities = {};
835aeacb 103 setEditorLayout();
1d678002 104 setLastSaved();
b844d2ca 105
7b9f8bdc
CW
106 if (editor.afform.navigation) {
107 loadNavigationMenu();
108 }
109
d4e2c9ff 110 if (editor.getFormType() === 'form') {
fbcd8c17 111 editor.allowEntityConfig = true;
3a2ca4f5 112 $scope.entities = _.mapValues(afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name'), backfillEntityDefaults);
fbcd8c17
CW
113
114 if (editor.mode === 'create') {
115 editor.addEntity(editor.entity);
e6772fc6 116 editor.afform.create_submission = true;
fbcd8c17
CW
117 editor.layout['#children'].push(afGui.meta.elements.submit.element);
118 }
119 }
7cc347c2
CW
120
121 if (editor.getFormType() === 'block') {
736be208 122 editor.blockEntity = editor.afform.join_entity || editor.afform.entity_type || '*';
3a2ca4f5 123 $scope.entities[editor.blockEntity] = backfillEntityDefaults({
fbcd8c17
CW
124 type: editor.blockEntity,
125 name: editor.blockEntity,
126 label: afGui.getEntity(editor.blockEntity).label
3a2ca4f5 127 });
b844d2ca
CW
128 }
129
d4e2c9ff 130 else if (editor.getFormType() === 'search') {
7cc347c2 131 editor.searchDisplays = getSearchDisplaysOnForm();
2ef64700
CW
132 }
133
835aeacb
CW
134 // Initialize undo history
135 undoAction = 'initialLoad';
136 undoHistory = [{
137 afform: _.cloneDeep(editor.afform),
138 saved: editor.mode === 'edit',
139 selectedEntityName: null
140 }];
141 $scope.$watch('editor.afform', function(newValue, oldValue) {
142 if (!undoAction && newValue && oldValue) {
143 // Clear "redo" history
144 if (undoPosition) {
145 undoHistory.splice(0, undoPosition);
146 undoPosition = 0;
147 }
148 undoHistory.unshift({
149 afform: _.cloneDeep(editor.afform),
150 saved: false,
151 selectedEntityName: $scope.selectedEntityName
152 });
153 // Trim to a total length of 20
154 if (undoHistory.length > 20) {
155 undoHistory.splice(20, undoHistory.length - 20);
156 }
157 }
158 undoAction = null;
b844d2ca
CW
159 }, true);
160 }
161
835aeacb
CW
162 // Undo/redo keys (ctrl-z, ctrl-shift-z)
163 this.onKeyup = function(e) {
164 if (e.key === 'z' && e.ctrlKey && e.shiftKey) {
165 editor.redo();
166 }
167 else if (e.key === 'z' && e.ctrlKey) {
168 editor.undo();
169 }
170 };
171
172 this.canUndo = function() {
173 return !!undoHistory[undoPosition + 1];
174 };
175
176 this.canRedo = function() {
177 return !!undoHistory[undoPosition - 1];
178 };
179
180 // Revert to a previous/next revision in the undo history
181 function changeHistory(direction) {
182 if (!undoHistory[undoPosition + direction]) {
183 return;
184 }
185 undoPosition += direction;
186 undoAction = 'change';
187 editor.afform = _.cloneDeep(undoHistory[undoPosition].afform);
188 setEditorLayout();
189 $scope.canvasTab = 'layout';
190 $scope.selectedEntityName = undoHistory[undoPosition].selectedEntityName;
191 }
192
193 this.undo = _.wrap(1, changeHistory);
194
195 this.redo = _.wrap(-1, changeHistory);
196
197 this.isSaved = function() {
198 return undoHistory[undoPosition].saved;
199 };
200
d4e2c9ff 201 this.getFormType = function() {
d617083a 202 return editor.afform.type;
d4e2c9ff
CW
203 };
204
b844d2ca
CW
205 $scope.updateLayoutHtml = function() {
206 $scope.layoutHtml = '...Loading...';
d617083a 207 crmApi4('Afform', 'convert', {layout: editor.afform.layout, from: 'deep', to: 'html', formatWhitespace: true})
b844d2ca
CW
208 .then(function(r){
209 $scope.layoutHtml = r[0].layout || '(Error)';
210 })
211 .catch(function(r){
212 $scope.layoutHtml = '(Error)';
213 });
214 };
215
490565d0 216 this.addEntity = function(type, selectTab) {
23fd8685 217 var meta = afGui.meta.entities[type],
b844d2ca
CW
218 num = 1;
219 // Give this new entity a unique name
220 while (!!$scope.entities[type + num]) {
221 num++;
222 }
5543e896 223 $scope.entities[type + num] = backfillEntityDefaults(_.assign($parse(meta.defaults)(editor), {
b844d2ca
CW
224 '#tag': 'af-entity',
225 type: meta.entity,
226 name: type + num,
490565d0
CW
227 label: meta.label + ' ' + num,
228 loading: true,
3a2ca4f5 229 }));
490565d0
CW
230
231 function addToCanvas() {
232 // Add this af-entity tag after the last existing one
233 var pos = 1 + _.findLastIndex(editor.layout['#children'], {'#tag': 'af-entity'});
234 editor.layout['#children'].splice(pos, 0, $scope.entities[type + num]);
235 // Create a new af-fieldset container for the entity
e1f79950
CW
236 if (meta.boilerplate !== false) {
237 var fieldset = _.cloneDeep(afGui.meta.elements.fieldset.element);
238 fieldset['af-fieldset'] = type + num;
239 fieldset['af-title'] = meta.label + ' ' + num;
240 // Add boilerplate contents
241 _.each(meta.boilerplate, function (tag) {
242 fieldset['#children'].push(tag);
243 });
244 // Attempt to place the new af-fieldset after the last one on the form
245 pos = 1 + _.findLastIndex(editor.layout['#children'], 'af-fieldset');
246 if (pos) {
247 editor.layout['#children'].splice(pos, 0, fieldset);
248 } else {
249 editor.layout['#children'].push(fieldset);
250 }
490565d0
CW
251 }
252 delete $scope.entities[type + num].loading;
253 if (selectTab) {
254 editor.selectEntity(type + num);
933b8520
CW
255 $timeout(function() {
256 editor.scrollToEntity(type + num);
257 });
490565d0 258 }
4da60774 259 $timeout(editor.adjustTabWidths);
490565d0
CW
260 }
261
262 if (meta.fields) {
263 addToCanvas();
b844d2ca 264 } else {
d617083a 265 $timeout(editor.adjustTabWidths);
490565d0
CW
266 crmApi4('Afform', 'loadAdminData', {
267 definition: {type: 'form'},
268 entity: type
269 }, 0).then(function(data) {
270 afGui.addMeta(data);
271 addToCanvas();
272 });
b844d2ca 273 }
b844d2ca
CW
274 };
275
276 this.removeEntity = function(entityName) {
277 delete $scope.entities[entityName];
23fd8685
CW
278 afGui.removeRecursive(editor.layout['#children'], {'#tag': 'af-entity', name: entityName});
279 afGui.removeRecursive(editor.layout['#children'], {'af-fieldset': entityName});
b844d2ca
CW
280 this.selectEntity(null);
281 };
282
283 this.selectEntity = function(entityName) {
284 $scope.selectedEntityName = entityName;
4da60774 285 $timeout(editor.adjustTabWidths);
b844d2ca
CW
286 };
287
288 this.getEntity = function(entityName) {
289 return $scope.entities[entityName];
290 };
291
292 this.getSelectedEntityName = function() {
293 return $scope.selectedEntityName;
294 };
295
d617083a 296 this.getEntityDefn = function(entity) {
1c2bf8ee 297 if (entity.type === 'Contact' && entity.data && entity.data.contact_type) {
d617083a
CW
298 return editor.meta.entities[entity.data.contact_type];
299 }
300 return editor.meta.entities[entity.type];
301 };
302
933b8520
CW
303 // Scroll an entity's first fieldset into view of the canvas
304 this.scrollToEntity = function(entityName) {
305 var $canvas = $('#afGuiEditor-canvas-body'),
306 $entity = $('.af-gui-container-type-fieldset[data-entity="' + entityName + '"]').first(),
1c2bf8ee
CW
307 scrollValue, maxScroll;
308 if ($entity.length) {
933b8520 309 // Scrolltop value needed to place entity's fieldset at top of canvas
1c2bf8ee 310 scrollValue = $canvas.scrollTop() + ($entity.offset().top - $canvas.offset().top);
933b8520
CW
311 // Maximum possible scrollTop (height minus contents height, adjusting for padding)
312 maxScroll = $('#afGuiEditor-canvas-body > *').height() - $canvas.height() + 20;
1c2bf8ee
CW
313 // Exceeding the maximum scrollTop breaks the animation so keep it under the limit
314 $canvas.animate({scrollTop: scrollValue > maxScroll ? maxScroll : scrollValue}, 500);
315 }
933b8520
CW
316 };
317
2652d11b 318 this.getAfform = function() {
d617083a 319 return editor.afform;
2652d11b
CW
320 };
321
783a2874
CW
322 this.getEntities = function(filter) {
323 return filter ? _.filter($scope.entities, filter) : _.toArray($scope.entities);
324 };
325
c63f20d3 326 this.toggleContactSummary = function() {
d617083a
CW
327 if (editor.afform.contact_summary) {
328 editor.afform.contact_summary = false;
8d5faaa3
CW
329 _.each(editor.searchDisplays, function(searchDisplay) {
330 delete searchDisplay.element.filters;
331 });
c63f20d3 332 } else {
d617083a 333 editor.afform.contact_summary = 'block';
8d5faaa3
CW
334 _.each(editor.searchDisplays, function(searchDisplay) {
335 var filterOptions = getSearchFilterOptions(searchDisplay.settings);
336 if (filterOptions.length) {
337 searchDisplay.element.filters = filterOptions[0].key;
338 }
339 });
c63f20d3
CW
340 }
341 };
342
7b9f8bdc
CW
343 this.toggleNavigation = function() {
344 if (editor.afform.navigation) {
345 editor.afform.navigation = null;
346 } else {
347 loadNavigationMenu();
348 editor.afform.navigation = {
349 parent: null,
350 label: editor.afform.title,
351 weight: 0
352 };
353 }
354 };
355
356 function loadNavigationMenu() {
357 if ('navigationMenu' in editor) {
358 return;
359 }
360 editor.navigationMenu = null;
361 var conditions = [
362 ['domain_id', '=', 'current_domain'],
363 ['name', '!=', 'Home']
364 ];
365 if (editor.afform.name) {
366 conditions.push(['name', '!=', editor.afform.name]);
367 }
368 crmApi4('Navigation', 'get', {
369 select: ['name', 'label', 'parent_id', 'icon'],
370 where: conditions,
371 orderBy: {weight: 'ASC'}
372 }).then(function(items) {
373 editor.navigationMenu = buildTree(items, null);
374 });
375 }
376
377 function buildTree(items, parentId) {
378 return _.transform(items, function(navigationMenu, item) {
379 if (parentId === item.parent_id) {
380 var children = buildTree(items, item.id),
381 menuItem = {
382 id: item.name,
383 text: item.label,
384 icon: item.icon
385 };
386 if (children.length) {
387 menuItem.children = children;
388 }
389 navigationMenu.push(menuItem);
390 }
391 }, []);
392 }
393
7cc347c2
CW
394 // Collects all search displays currently on the form
395 function getSearchDisplaysOnForm() {
396 var searchFieldsets = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''});
397 return _.transform(searchFieldsets, function(searchDisplays, fieldset) {
398 var displayElement = afGui.findRecursive(fieldset['#children'], function(item) {
399 return item['search-name'] && item['#tag'] && item['#tag'].indexOf('crm-search-display-') === 0;
400 })[0];
401 if (displayElement) {
402 searchDisplays[displayElement['search-name'] + (displayElement['display-name'] ? '.' + displayElement['display-name'] : '')] = {
403 element: displayElement,
404 fieldset: fieldset,
405 settings: afGui.getSearchDisplay(displayElement['search-name'], displayElement['display-name'])
406 };
407 }
408 }, {});
409 }
410
411 // Load data for "Add search display" dropdown
412 this.getSearchDisplaySelector = function() {
413 // Reset search input in dropdown
414 $scope.searchDisplayListFilter.label = '';
415 // A value means it's alredy loaded. Null means it's loading.
416 if (!editor.searchOptions && editor.searchOptions !== null) {
417 editor.searchOptions = null;
418 afGui.getAllSearchDisplays().then(function(links) {
419 editor.searchOptions = links;
420 });
421 }
422 };
423
424 this.addSearchDisplay = function(display) {
425 var searchName = display.key.split('.')[0];
426 var displayName = display.key.split('.')[1] || '';
427 var fieldset = {
428 '#tag': 'div',
429 'af-fieldset': '',
be125358 430 'af-title': display.label,
7cc347c2
CW
431 '#children': [
432 {
433 '#tag': display.tag,
434 'search-name': searchName,
435 'display-name': displayName,
436 }
437 ]
438 };
439 var meta = {
440 fieldset: fieldset,
441 element: fieldset['#children'][0],
442 settings: afGui.getSearchDisplay(searchName, displayName),
443 };
444 editor.searchDisplays[display.key] = meta;
445
446 function addToCanvas() {
447 editor.layout['#children'].push(fieldset);
448 editor.selectEntity(display.key);
449 }
450 if (meta.settings) {
451 addToCanvas();
452 } else {
453 $timeout(editor.adjustTabWidths);
454 crmApi4('Afform', 'loadAdminData', {
455 definition: {type: 'search'},
456 entity: display.key
457 }, 0).then(function(data) {
458 afGui.addMeta(data);
459 meta.settings = afGui.getSearchDisplay(searchName, displayName);
460 addToCanvas();
461 });
462 }
463 };
464
465 // Triggered by afGuiContainer.removeElement
466 this.onRemoveElement = function() {
467 // Keep this.searchDisplays in-sync when deleteing stuff from the form
468 if (editor.getFormType() === 'search') {
469 var current = getSearchDisplaysOnForm();
470 _.each(_.keys(editor.searchDisplays), function(key) {
471 if (!(key in current)) {
472 delete editor.searchDisplays[key];
473 editor.selectEntity(null);
474 }
475 });
476 }
477 };
478
479 // This function used to be needed to build a menu of available contact_id fields
480 // but is no longer used for that and is overkill for what it does now.
8d5faaa3
CW
481 function getSearchFilterOptions(searchDisplay) {
482 var
c63f20d3
CW
483 entityCount = {},
484 options = [];
485
5c952e51 486 addFields(searchDisplay['saved_search_id.api_entity'], '');
c63f20d3 487
5c952e51 488 _.each(searchDisplay['saved_search_id.api_params'].join, function(join) {
c63f20d3
CW
489 var joinInfo = join[0].split(' AS ');
490 addFields(joinInfo[0], joinInfo[1] + '.');
491 });
492
493 function addFields(entityName, prefix) {
494 var entity = afGui.getEntity(entityName);
495 entityCount[entity.entity] = (entityCount[entity.entity] || 0) + 1;
496 var count = (entityCount[entity.entity] > 1 ? ' ' + entityCount[entity.entity] : '');
497 if (entityName === 'Contact') {
498 options.push({
499 key: "{'" + prefix + "id': options.contact_id}",
500 label: entity.label + count
501 });
502 } else {
503 _.each(entity.fields, function(field) {
504 if (field.fk_entity === 'Contact') {
505 options.push({
506 key: "{'" + prefix + field.name + "': options.contact_id}",
507 label: entity.label + count + ' ' + field.label
508 });
509 }
510 });
511 }
512 }
513 return options;
514 }
515
d617083a
CW
516 this.getLink = function() {
517 if (editor.afform.server_route) {
518 return CRM.url(editor.afform.server_route, null, editor.afform.is_public ? 'front' : 'back');
519 }
520 };
521
4da60774
CW
522 // Options for ui-sortable in field palette
523 this.getSortableOptions = function(entityName) {
524 if (!sortableOptions[entityName + '']) {
525 sortableOptions[entityName + ''] = {
4da60774
CW
526 appendTo: '#afGuiEditor-canvas-body > af-gui-container',
527 containment: '#afGuiEditor-canvas-body',
528 update: editor.onDrop,
529 items: '> div:not(.disabled)',
530 connectWith: '#afGuiEditor-canvas ' + (entityName ? '[data-entity="' + entityName + '"] > ' : '') + '[ui-sortable]',
531 placeholder: 'af-gui-dropzone',
ad00103f
CW
532 scrollSpeed: 8,
533 helper: function(e, $el) {
534 // Prevent draggable item from being too large for the drop zones.
535 return $el.clone().css({width: '50px', height: '20px'});
536 }
4da60774
CW
537 };
538 }
539 return sortableOptions[entityName + ''];
540 };
541
b844d2ca
CW
542 // Validates that a drag-n-drop action is allowed
543 this.onDrop = function(event, ui) {
544 var sort = ui.item.sortable;
545 // Check if this is a callback for an item dropped into a different container
546 // @see https://github.com/angular-ui/ui-sortable notes on canceling
547 if (!sort.received && sort.source[0] !== sort.droptarget[0]) {
548 var $source = $(sort.source[0]),
549 $target = $(sort.droptarget[0]),
550 $item = $(ui.item[0]);
551 // Fields cannot be dropped outside their own entity
261a6a5d 552 if ($item.find('af-gui-field').length) {
b844d2ca
CW
553 if ($source.closest('[data-entity]').attr('data-entity') !== $target.closest('[data-entity]').attr('data-entity')) {
554 return sort.cancel();
555 }
556 }
557 // Entity-fieldsets cannot be dropped into other entity-fieldsets
261a6a5d 558 if ((sort.model['af-fieldset'] || $item.find('.af-gui-fieldset').length) && $target.closest('.af-gui-fieldset').length) {
b844d2ca
CW
559 return sort.cancel();
560 }
561 }
562 };
563
b844d2ca 564 $scope.save = function() {
d617083a 565 var afform = JSON.parse(angular.toJson(editor.afform));
34f37d59
CW
566 // This might be set to undefined by validation
567 afform.server_route = afform.server_route || '';
835aeacb 568 $scope.saving = true;
34f37d59 569 crmApi4('Afform', 'save', {formatWhitespace: true, records: [afform]})
b844d2ca
CW
570 .then(function (data) {
571 $scope.saving = false;
835aeacb
CW
572 // When saving a new form for the first time
573 if (!editor.afform.name) {
574 undoAction = 'save';
575 editor.afform.name = data[0].name;
490565d0 576 }
835aeacb
CW
577 // Update undo history - mark current snapshot as "saved"
578 _.each(undoHistory, function(snapshot, index) {
579 snapshot.saved = index === undoPosition;
580 snapshot.afform.name = data[0].name;
581 });
1d678002
CW
582 if (!angular.equals(afform.navigation, lastSaved.navigation) ||
583 (afform.server_route !== lastSaved.server_route && afform.navigation)
584 (afform.icon !== lastSaved.icon && afform.navigation)
585 ) {
586 refreshMenubar();
587 }
588 setLastSaved();
b844d2ca
CW
589 });
590 };
591
d617083a 592 $scope.$watch('editor.afform.title', function(newTitle, oldTitle) {
b844d2ca
CW
593 if (typeof oldTitle === 'string') {
594 _.each($scope.entities, function(entity) {
5543e896 595 if (entity.data && 'source' in entity.data && (entity.data.source || '') === oldTitle) {
b844d2ca
CW
596 entity.data.source = newTitle;
597 }
598 });
599 }
600 });
4da60774 601
1d678002
CW
602 // Sets last-saved form metadata (used to determine if the menubar needs refresh)
603 function setLastSaved() {
604 lastSaved = JSON.parse(angular.toJson(editor.afform));
605 delete lastSaved.layout;
606 }
607
608 // Force-refresh the menubar to instantly display the afform menu item
609 function refreshMenubar() {
610 CRM.menubar.destroy();
611 CRM.menubar.cacheCode = Math.random();
612 CRM.menubar.initialize();
613 }
614
4da60774
CW
615 // Force editor panels to a fixed height, to avoid palette scrolling offscreen
616 function fixEditorHeight() {
617 var height = $(window).height() - $('#afGuiEditor').offset().top;
618 $('#afGuiEditor').height(Math.floor(height));
619 }
620
621 // Compress tabs on small screens
622 this.adjustTabWidths = function() {
623 $('#afGuiEditor .panel-heading ul.nav-tabs li.active').css('max-width', '');
624 $('#afGuiEditor .panel-heading ul.nav-tabs').each(function() {
625 var remainingSpace = Math.floor($(this).width()) - 1,
626 inactiveTabs = $(this).children('li.fluid-width-tab').not('.active');
627 $(this).children('.active,:not(.fluid-width-tab)').each(function() {
628 remainingSpace -= $(this).width();
629 });
630 if (inactiveTabs.length) {
631 inactiveTabs.css('max-width', Math.floor(remainingSpace / inactiveTabs.length) + 'px');
632 }
633 });
634 };
b844d2ca
CW
635 }
636 });
637
638})(angular, CRM.$, CRM._);