Afform Gui - Support editing blocks
[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 getEntity: function(entityName) {
119 return CRM.afGuiEditor.entities[entityName];
120 },
121
122 getField: function(entityName, fieldName) {
123 return CRM.afGuiEditor.entities[entityName].fields[fieldName];
124 },
125
126 // Recursively searches a collection and its children using _.filter
127 // Returns an array of all matches, or an object if the indexBy param is used
128 findRecursive: function findRecursive(collection, predicate, indexBy) {
129 var items = _.filter(collection, predicate);
130 _.each(collection, function(item) {
131 if (_.isPlainObject(item) && item['#children']) {
132 var childMatches = findRecursive(item['#children'], predicate);
133 if (childMatches.length) {
134 Array.prototype.push.apply(items, childMatches);
135 }
136 }
137 });
138 return indexBy ? _.indexBy(items, indexBy) : items;
139 },
140
141 // Applies _.remove() to an item and its children
142 removeRecursive: function removeRecursive(collection, removeParams) {
143 _.remove(collection, removeParams);
144 _.each(collection, function(item) {
145 if (_.isPlainObject(item) && item['#children']) {
146 removeRecursive(item['#children'], removeParams);
147 }
148 });
149 },
150
151 splitClass: splitClass,
152 modifyClasses: modifyClasses,
153 getStyles: getStyles,
154 setStyle: setStyle,
155
156 pickIcon: function() {
157 var deferred = $q.defer();
158 $('#af-gui-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
159 $('#af-gui-icon-picker').on('change', function() {
160 deferred.resolve($(this).val());
161 });
162 return deferred.promise;
163 }
164 };
165 });
166
167 // Shoehorn in a non-angular widget for picking icons
168 $(function() {
169 $('#crm-container').append('<div style="display:none"><input id="af-gui-icon-picker"></div>');
170 CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() {
171 $('#af-gui-icon-picker').crmIconPicker();
172 });
173 });
174
175 // Connect bootstrap dropdown.js with angular
176 // Allows menu content to be conditionally rendered only if open
177 // This gives a large performance boost for a page with lots of menus
178 angular.module('afGuiEditor').directive('afGuiMenu', function() {
179 return {
180 restrict: 'A',
181 link: function($scope, element, attrs) {
182 $scope.menu = {};
183 element
184 .on('show.bs.dropdown', function() {
185 $scope.$apply(function() {
186 $scope.menu.open = true;
187 });
188 })
189 .on('hidden.bs.dropdown', function() {
190 $scope.$apply(function() {
191 $scope.menu.open = false;
192 });
193 });
194 }
195 };
196 });
197
198 })(angular, CRM.$, CRM._);