Afform - Refactor elements as components & move to their own files
[civicrm-core.git] / ext / afform / admin / ang / afGuiEditor.js
CommitLineData
f6c0358e 1(function(angular, $, _) {
881d52bb 2 "use strict";
cb46dc65
CW
3 angular.module('afGuiEditor', CRM.angRequires('afGuiEditor'))
4
d132f0a6 5 .service('afAdmin', function(crmApi4, $parse, $q) {
cb46dc65
CW
6
7 // Parse strings of javascript that php couldn't interpret
8 function evaluate(collection) {
9 _.each(collection, function(item) {
10 if (_.isPlainObject(item)) {
11 evaluate(item['#children']);
12 _.each(item, function(node, idx) {
13 if (_.isString(node)) {
14 var str = _.trim(node);
15 if (str[0] === '{' || str[0] === '[' || str.slice(0, 3) === 'ts(') {
16 item[idx] = $parse(str)({ts: CRM.ts('afform')});
17 }
18 }
19 });
20 }
21 });
22 }
23
24 function getStyles(node) {
25 return !node || !node.style ? {} : _.transform(node.style.split(';'), function(styles, style) {
26 var keyVal = _.map(style.split(':'), _.trim);
27 if (keyVal.length > 1 && keyVal[1].length) {
28 styles[keyVal[0]] = keyVal[1];
29 }
30 }, {});
31 }
32
33 function setStyle(node, name, val) {
34 var styles = getStyles(node);
35 styles[name] = val;
36 if (!val) {
37 delete styles[name];
38 }
39 if (_.isEmpty(styles)) {
40 delete node.style;
41 } else {
42 node.style = _.transform(styles, function(combined, val, name) {
43 combined.push(name + ': ' + val);
44 }, []).join('; ');
45 }
46 }
47
48 // Turns a space-separated list (e.g. css classes) into an array
49 function splitClass(str) {
50 if (_.isArray(str)) {
51 return str;
52 }
53 return str ? _.unique(_.trim(str).split(/\s+/g)) : [];
54 }
55
56 function modifyClasses(node, toRemove, toAdd) {
57 var classes = splitClass(node['class']);
58 if (toRemove) {
59 classes = _.difference(classes, splitClass(toRemove));
60 }
61 if (toAdd) {
62 classes = _.unique(classes.concat(splitClass(toAdd)));
63 }
64 node['class'] = classes.join(' ');
65 }
66
67 return {
68 // Initialize/refresh data about the current afform + available blocks
69 initialize: function(afName) {
70 var promise = crmApi4('Afform', 'get', {
71 layoutFormat: 'shallow',
72 formatWhitespace: true,
73 where: [afName ? ["OR", [["name", "=", afName], ["block", "IS NOT NULL"]]] : ["block", "IS NOT NULL"]]
74 });
75 promise.then(function(afforms) {
76 CRM.afGuiEditor.blocks = {};
77 _.each(afforms, function(form) {
78 evaluate(form.layout);
79 if (form.block) {
80 CRM.afGuiEditor.blocks[form.directive_name] = form;
81 }
82 });
83 });
84 return promise;
85 },
86
87 meta: CRM.afGuiEditor,
88
89 getField: function(entityType, fieldName) {
90 return CRM.afGuiEditor.entities[entityType].fields[fieldName];
91 },
92
93 // Recursively searches a collection and its children using _.filter
94 // Returns an array of all matches, or an object if the indexBy param is used
95 findRecursive: function findRecursive(collection, predicate, indexBy) {
96 var items = _.filter(collection, predicate);
97 _.each(collection, function(item) {
98 if (_.isPlainObject(item) && item['#children']) {
99 var childMatches = findRecursive(item['#children'], predicate);
100 if (childMatches.length) {
101 Array.prototype.push.apply(items, childMatches);
102 }
103 }
104 });
105 return indexBy ? _.indexBy(items, indexBy) : items;
106 },
107
108 // Applies _.remove() to an item and its children
109 removeRecursive: function removeRecursive(collection, removeParams) {
110 _.remove(collection, removeParams);
111 _.each(collection, function(item) {
112 if (_.isPlainObject(item) && item['#children']) {
113 removeRecursive(item['#children'], removeParams);
114 }
115 });
116 },
117
118 splitClass: splitClass,
119 modifyClasses: modifyClasses,
120 getStyles: getStyles,
d132f0a6
CW
121 setStyle: setStyle,
122
123 pickIcon: function() {
124 var deferred = $q.defer();
125 $('#af-gui-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
126 $('#af-gui-icon-picker').on('change', function() {
127 deferred.resolve($(this).val());
128 });
129 return deferred.promise;
130 }
cb46dc65
CW
131 };
132 });
f6c0358e 133
d132f0a6
CW
134 // Shoehorn in a non-angular widget for picking icons
135 $(function() {
136 $('#crm-container').append('<div style="display:none"><input id="af-gui-icon-picker"></div>');
137 CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() {
138 $('#af-gui-icon-picker').crmIconPicker();
139 });
140 });
141
9633741a
CW
142 angular.module('afGuiEditor').controller('afGuiSaveBlock', function($scope, crmApi4, dialogService) {
143 var ts = $scope.ts = CRM.ts(),
144 model = $scope.model,
145 original = $scope.original = {
146 title: model.title,
147 name: model.name
148 };
149 if (model.name) {
150 $scope.$watch('model.name', function(val, oldVal) {
151 if (!val && model.title === original.title) {
152 model.title += ' ' + ts('(copy)');
153 }
154 else if (val === original.name && val !== oldVal) {
155 model.title = original.title;
156 }
157 });
158 }
159 $scope.cancel = function() {
160 dialogService.cancel('saveBlockDialog');
161 };
162 $scope.save = function() {
163 $('.ui-dialog:visible').block();
164 crmApi4('Afform', 'save', {formatWhitespace: true, records: [JSON.parse(angular.toJson(model))]})
165 .then(function(result) {
166 dialogService.close('saveBlockDialog', result[0]);
167 });
168 };
169 });
170
b4def6e9
CW
171 angular.module('afGuiEditor').directive('afGuiEditOptions', function() {
172 return {
173 restrict: 'A',
174 templateUrl: '~/afGuiEditor/editOptions.html',
175 scope: true,
176 require: '^^afGuiField',
177 link: function ($scope, element, attrs, afGuiField) {
178 $scope.field = afGuiField;
179 $scope.options = JSON.parse(angular.toJson(afGuiField.getOptions()));
180 var optionKeys = _.map($scope.options, 'key');
181 $scope.deletedOptions = _.filter(JSON.parse(angular.toJson(afGuiField.getDefn().options || [])), function(item) {
182 return !_.contains(optionKeys, item.key);
183 });
184 $scope.originalLabels = _.transform(afGuiField.getDefn().options || [], function(originalLabels, item) {
185 originalLabels[item.key] = item.label;
186 }, {});
187 },
188 controller: function ($scope) {
189 var ts = $scope.ts = CRM.ts();
190
191 $scope.deleteOption = function(option, $index) {
192 $scope.options.splice($index, 1);
193 $scope.deletedOptions.push(option);
194 };
195
196 $scope.restoreOption = function(option, $index) {
197 $scope.deletedOptions.splice($index, 1);
198 $scope.options.push(option);
199 };
6fb9e8d2 200
b4def6e9
CW
201 $scope.save = function() {
202 $scope.field.getSet('options', JSON.parse(angular.toJson($scope.options)));
203 $scope.close();
204 };
205
206 $scope.close = function() {
207 $scope.field.setEditingOptions(false);
192695ae 208 $('#afGuiEditor').removeClass('af-gui-editing-content');
b4def6e9 209 };
66af6937
CW
210 }
211 };
212 });
213
b4def6e9
CW
214 // Connect bootstrap dropdown.js with angular
215 // Allows menu content to be conditionally rendered only if open
216 // This gives a large performance boost for a page with lots of menus
217 angular.module('afGuiEditor').directive('afGuiMenu', function() {
218 return {
219 restrict: 'A',
220 link: function($scope, element, attrs) {
221 $scope.menu = {};
222 element
223 .on('show.bs.dropdown', function() {
224 $scope.$apply(function() {
225 $scope.menu.open = true;
226 });
227 })
228 .on('hidden.bs.dropdown', function() {
229 $scope.$apply(function() {
230 $scope.menu.open = false;
231 });
232 });
233 }
234 };
235 });
236
192695ae 237 // Menu item to control the border property of a node
cb46dc65 238 angular.module('afGuiEditor').directive('afGuiMenuItemBorder', function(afAdmin) {
192695ae
CW
239 return {
240 restrict: 'A',
241 templateUrl: '~/afGuiEditor/menu-item-border.html',
242 scope: {
243 node: '=afGuiMenuItemBorder'
244 },
245 link: function($scope, element, attrs) {
246 var ts = $scope.ts = CRM.ts();
247
248 $scope.getSetBorderWidth = function(width) {
249 return getSetBorderProp($scope.node, 0, arguments.length ? width : null);
250 };
251
252 $scope.getSetBorderStyle = function(style) {
253 return getSetBorderProp($scope.node, 1, arguments.length ? style : null);
254 };
255
256 $scope.getSetBorderColor = function(color) {
257 return getSetBorderProp($scope.node, 2, arguments.length ? color : null);
258 };
259
260 function getSetBorderProp(node, idx, val) {
261 var border = getBorder(node) || ['1px', '', '#000000'];
262 if (val === null) {
263 return border[idx];
264 }
265 border[idx] = val;
cb46dc65 266 afAdmin.setStyle(node, 'border', val ? border.join(' ') : null);
192695ae
CW
267 }
268
269 function getBorder(node) {
cb46dc65 270 var border = _.map((afAdmin.getStyles(node).border || '').split(' '), _.trim);
192695ae
CW
271 return border.length > 2 ? border : null;
272 }
273 }
274 };
275 });
276
277 // Menu item to control the background property of a node
cb46dc65 278 angular.module('afGuiEditor').directive('afGuiMenuItemBackground', function(afAdmin) {
192695ae
CW
279 return {
280 restrict: 'A',
281 templateUrl: '~/afGuiEditor/menu-item-background.html',
282 scope: {
283 node: '=afGuiMenuItemBackground'
284 },
285 link: function($scope, element, attrs) {
286 var ts = $scope.ts = CRM.ts();
287
288 $scope.getSetBackgroundColor = function(color) {
289 if (!arguments.length) {
cb46dc65 290 return afAdmin.getStyles($scope.node)['background-color'] || '#ffffff';
192695ae 291 }
cb46dc65 292 afAdmin.setStyle($scope.node, 'background-color', color);
192695ae
CW
293 };
294 }
295 };
296 });
297
f6c0358e 298})(angular, CRM.$, CRM._);