5b1253fc920f8e61bc2155171f65ac1257cf67ec
[civicrm-core.git] / ext / afform / admin / ang / afGuiEditor.js
1 (function(angular, $, _) {
2 "use strict";
3
4 angular.module('afGuiEditor', CRM.angRequires('afGuiEditor'))
5
6 .service('afGui', function(crmApi4, $parse, $q) {
7
8 // Parse strings of javascript that php couldn't interpret
9 function evaluate(collection) {
10 _.each(collection, function(item) {
11 if (_.isPlainObject(item)) {
12 evaluate(item['#children']);
13 _.each(item, function(node, idx) {
14 if (_.isString(node)) {
15 var str = _.trim(node);
16 if (str[0] === '{' || str[0] === '[' || str.slice(0, 3) === 'ts(') {
17 item[idx] = $parse(str)({ts: CRM.ts('afform')});
18 }
19 }
20 });
21 }
22 });
23 }
24
25 function getStyles(node) {
26 return !node || !node.style ? {} : _.transform(node.style.split(';'), function(styles, style) {
27 var keyVal = _.map(style.split(':'), _.trim);
28 if (keyVal.length > 1 && keyVal[1].length) {
29 styles[keyVal[0]] = keyVal[1];
30 }
31 }, {});
32 }
33
34 function setStyle(node, name, val) {
35 var styles = getStyles(node);
36 styles[name] = val;
37 if (!val) {
38 delete styles[name];
39 }
40 if (_.isEmpty(styles)) {
41 delete node.style;
42 } else {
43 node.style = _.transform(styles, function(combined, val, name) {
44 combined.push(name + ': ' + val);
45 }, []).join('; ');
46 }
47 }
48
49 // Turns a space-separated list (e.g. css classes) into an array
50 function splitClass(str) {
51 if (_.isArray(str)) {
52 return str;
53 }
54 return str ? _.unique(_.trim(str).split(/\s+/g)) : [];
55 }
56
57 function modifyClasses(node, toRemove, toAdd) {
58 var classes = splitClass(node['class']);
59 if (toRemove) {
60 classes = _.difference(classes, splitClass(toRemove));
61 }
62 if (toAdd) {
63 classes = _.unique(classes.concat(splitClass(toAdd)));
64 }
65 node['class'] = classes.join(' ');
66 }
67
68 return {
69 // Called when loading a new afform for editing - clears out stale metadata
70 resetMeta: function() {
71 _.each(CRM.afGuiEditor.entities, function(entity) {
72 delete entity.fields;
73 });
74 CRM.afGuiEditor.blocks = {};
75 },
76
77 // Takes the results from api.Afform.loadAdminData and processes the metadata
78 // Note this runs once when loading a new afform for editing (just after this.resetMeta is called)
79 // and it also runs when adding new entities or joins to the form.
80 addMeta: function(data) {
81 evaluate(data.definition.layout);
82 if (data.definition.type === 'block') {
83 CRM.afGuiEditor.blocks[data.definition.directive_name] = data.definition;
84 }
85 // Add new or updated blocks
86 _.each(data.blocks, function(block) {
87 // Avoid overwriting complete block record with an incomplete one
88 if (!CRM.afGuiEditor.blocks[block.directive_name] || block.layout) {
89 if (block.layout) {
90 evaluate(block.layout);
91 }
92 CRM.afGuiEditor.blocks[block.directive_name] = block;
93 }
94 });
95 _.each(data.entities, function(entity, entityName) {
96 if (!CRM.afGuiEditor.entities[entityName]) {
97 CRM.afGuiEditor.entities[entityName] = entity;
98 }
99 });
100 _.each(data.fields, function(fields, entityName) {
101 if (CRM.afGuiEditor.entities[entityName]) {
102 CRM.afGuiEditor.entities[entityName].fields = fields;
103 }
104 });
105 // Optimization - since contact fields are a combination of these three,
106 // the server doesn't send contact fields if sending contact-type fields
107 if ('Individual' in data.fields || 'Household' in data.fields || 'Organization' in data.fields) {
108 CRM.afGuiEditor.entities.Contact.fields = _.assign({},
109 (CRM.afGuiEditor.entities.Individual || {}).fields,
110 (CRM.afGuiEditor.entities.Household || {}).fields,
111 (CRM.afGuiEditor.entities.Organization || {}).fields
112 );
113 }
114 },
115
116 meta: CRM.afGuiEditor,
117
118 getField: function(entityType, fieldName) {
119 return CRM.afGuiEditor.entities[entityType].fields[fieldName];
120 },
121
122 // Recursively searches a collection and its children using _.filter
123 // Returns an array of all matches, or an object if the indexBy param is used
124 findRecursive: function findRecursive(collection, predicate, indexBy) {
125 var items = _.filter(collection, predicate);
126 _.each(collection, function(item) {
127 if (_.isPlainObject(item) && item['#children']) {
128 var childMatches = findRecursive(item['#children'], predicate);
129 if (childMatches.length) {
130 Array.prototype.push.apply(items, childMatches);
131 }
132 }
133 });
134 return indexBy ? _.indexBy(items, indexBy) : items;
135 },
136
137 // Applies _.remove() to an item and its children
138 removeRecursive: function removeRecursive(collection, removeParams) {
139 _.remove(collection, removeParams);
140 _.each(collection, function(item) {
141 if (_.isPlainObject(item) && item['#children']) {
142 removeRecursive(item['#children'], removeParams);
143 }
144 });
145 },
146
147 splitClass: splitClass,
148 modifyClasses: modifyClasses,
149 getStyles: getStyles,
150 setStyle: setStyle,
151
152 pickIcon: function() {
153 var deferred = $q.defer();
154 $('#af-gui-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
155 $('#af-gui-icon-picker').on('change', function() {
156 deferred.resolve($(this).val());
157 });
158 return deferred.promise;
159 }
160 };
161 });
162
163 // Shoehorn in a non-angular widget for picking icons
164 $(function() {
165 $('#crm-container').append('<div style="display:none"><input id="af-gui-icon-picker"></div>');
166 CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() {
167 $('#af-gui-icon-picker').crmIconPicker();
168 });
169 });
170
171 // Connect bootstrap dropdown.js with angular
172 // Allows menu content to be conditionally rendered only if open
173 // This gives a large performance boost for a page with lots of menus
174 angular.module('afGuiEditor').directive('afGuiMenu', function() {
175 return {
176 restrict: 'A',
177 link: function($scope, element, attrs) {
178 $scope.menu = {};
179 element
180 .on('show.bs.dropdown', function() {
181 $scope.$apply(function() {
182 $scope.menu.open = true;
183 });
184 })
185 .on('hidden.bs.dropdown', function() {
186 $scope.$apply(function() {
187 $scope.menu.open = false;
188 });
189 });
190 }
191 };
192 });
193
194 })(angular, CRM.$, CRM._);