Commit | Line | Data |
---|---|---|
f6c0358e CW |
1 | (function(angular, $, _) { |
2 | angular.module('afGuiEditor', CRM.angRequires('afGuiEditor')); | |
3 | ||
65c9e7ae | 4 | angular.module('afGuiEditor').directive('afGuiEditor', function(crmApi4, $parse, $timeout) { |
f6c0358e CW |
5 | return { |
6 | restrict: 'A', | |
f6c0358e CW |
7 | templateUrl: '~/afGuiEditor/main.html', |
8 | scope: { | |
9 | afGuiEditor: '=' | |
10 | }, | |
f3cd3852 | 11 | controller: function($scope) { |
f6c0358e CW |
12 | $scope.ts = CRM.ts(); |
13 | $scope.afform = null; | |
14 | $scope.selectedEntity = null; | |
c820e08c | 15 | $scope.meta = CRM.afformAdminData; |
a064e90d | 16 | $scope.controls = {}; |
65c9e7ae | 17 | $scope.fieldList = {}; |
f3cd3852 | 18 | $scope.editor = this; |
f6c0358e CW |
19 | var newForm = { |
20 | title: ts('Untitled Form'), | |
66af6937 | 21 | layout: [{ |
f6c0358e CW |
22 | '#tag': 'af-form', |
23 | ctrl: 'modelListCtrl', | |
24 | '#children': [ | |
25 | { | |
26 | '#tag': 'af-entity', | |
27 | type: 'Contact', | |
28 | data: { | |
29 | contact_type: 'Individual' | |
30 | }, | |
31 | name: 'Contact1', | |
32 | label: 'Contact 1', | |
33 | 'url-autofill': '1', | |
34 | autofill: 'user' | |
35 | } | |
36 | ] | |
66af6937 | 37 | }] |
f6c0358e CW |
38 | }; |
39 | if ($scope.afGuiEditor.name) { | |
66af6937 | 40 | crmApi4('Afform', 'get', {where: [['name', '=', $scope.afGuiEditor.name]], layoutFormat: 'shallow'}, 0) |
f6c0358e CW |
41 | .then(initialize); |
42 | } | |
43 | else { | |
44 | initialize(newForm); | |
45 | } | |
46 | ||
47 | function initialize(afform) { | |
48 | // Todo - show error msg if form is not found | |
49 | $scope.afform = afform; | |
66af6937 CW |
50 | $scope.layout = getTags($scope.afform.layout, 'af-form')[0]; |
51 | evaluate($scope.layout['#children']); | |
66af6937 | 52 | $scope.entities = getTags($scope.layout['#children'], 'af-entity', 'name'); |
65c9e7ae CW |
53 | expandFields($scope.layout['#children']); |
54 | _.each(_.keys($scope.entities), buildFieldList); | |
f6c0358e CW |
55 | } |
56 | ||
f3cd3852 | 57 | this.addEntity = function(entityType) { |
f6c0358e CW |
58 | var existingEntitiesofThisType = _.map(_.filter($scope.entities, {type: entityType}), 'name'), |
59 | num = existingEntitiesofThisType.length + 1; | |
60 | // Give this new entity a unique name | |
61 | while (_.contains(existingEntitiesofThisType, entityType + num)) { | |
62 | num++; | |
63 | } | |
64 | $scope.entities[entityType + num] = { | |
65 | '#tag': 'af-entity', | |
66 | type: entityType, | |
67 | name: entityType + num, | |
68 | label: entityType + ' ' + num | |
69 | }; | |
66af6937 | 70 | $scope.layout['#children'].unshift($scope.entities[entityType + num]); |
f3cd3852 | 71 | this.selectEntity(entityType + num); |
f6c0358e CW |
72 | }; |
73 | ||
f3cd3852 | 74 | this.removeEntity = function(entityName) { |
f6c0358e | 75 | delete $scope.entities[entityName]; |
66af6937 | 76 | _.remove($scope.layout['#children'], {'#tag': 'af-entity', name: entityName}); |
f3cd3852 | 77 | this.selectEntity(null); |
f6c0358e CW |
78 | }; |
79 | ||
f3cd3852 | 80 | this.selectEntity = function(entityName) { |
f6c0358e CW |
81 | $scope.selectedEntity = entityName; |
82 | }; | |
83 | ||
f3cd3852 CW |
84 | this.getField = function(entityType, fieldName) { |
85 | return _.filter($scope.meta.fields[entityType], {name: fieldName})[0]; | |
86 | }; | |
87 | ||
88 | this.getEntity = function(entityName) { | |
89 | return $scope.entities[entityName]; | |
90 | }; | |
91 | ||
92 | this.getSelectedEntity = function() { | |
93 | return $scope.selectedEntity; | |
a064e90d CW |
94 | }; |
95 | ||
65c9e7ae CW |
96 | $scope.rebuildFieldList = function() { |
97 | $timeout(function() { | |
98 | $scope.$apply(function() { | |
99 | buildFieldList($scope.selectedEntity); | |
100 | }); | |
101 | }); | |
102 | }; | |
103 | ||
104 | function buildFieldList(entityName) { | |
105 | $scope.fieldList[entityName] = $scope.fieldList[entityName] || []; | |
106 | $scope.fieldList[entityName].length = 0; | |
107 | _.each($scope.meta.fields[$scope.entities[entityName].type], function(field) { | |
108 | $scope.fieldList[entityName].push({ | |
109 | "#tag": "af-field", | |
110 | name: field.name, | |
111 | defn: _.cloneDeep(_.pick(field, ['title', 'input_type', 'input_attrs'])) | |
112 | }); | |
113 | }); | |
114 | } | |
115 | ||
a064e90d CW |
116 | $scope.valuesFields = function() { |
117 | var fields = _.transform($scope.meta.fields[$scope.entities[$scope.selectedEntity].type], function(fields, field) { | |
65c9e7ae | 118 | fields.push({id: field.name, text: field.title, disabled: $scope.fieldInUse($scope.selectedEntity, field.name)}); |
a064e90d CW |
119 | }, []); |
120 | return {results: fields}; | |
121 | }; | |
122 | ||
123 | $scope.removeValue = function(entity, fieldName) { | |
124 | delete entity.data[fieldName]; | |
125 | }; | |
126 | ||
127 | $scope.$watch('controls.addValue', function(fieldName) { | |
128 | if (fieldName) { | |
66af6937 CW |
129 | if (!$scope.entities[$scope.selectedEntity].data) { |
130 | $scope.entities[$scope.selectedEntity].data = {}; | |
131 | } | |
a064e90d CW |
132 | $scope.entities[$scope.selectedEntity].data[fieldName] = ''; |
133 | $scope.controls.addValue = ''; | |
134 | } | |
135 | }); | |
136 | ||
65c9e7ae CW |
137 | // Checks if a field is on the form or set as a value |
138 | $scope.fieldInUse = function(entityName, fieldName) { | |
139 | var data = $scope.entities[entityName].data || {}, | |
140 | found = false; | |
141 | if (fieldName in data) { | |
142 | return true; | |
143 | } | |
144 | return check($scope.layout['#children']); | |
145 | function check(group) { | |
146 | _.each(group, function(item) { | |
147 | if (found) { | |
148 | return false; | |
149 | } | |
150 | if (_.isPlainObject(item)) { | |
151 | if ((!item['af-fieldset'] || (item['af-fieldset'] === entityName)) && item['#children']) { | |
152 | check(item['#children']); | |
153 | } | |
154 | if (item['#tag'] === 'af-field' && item.name === fieldName) { | |
155 | found = true; | |
156 | } | |
157 | } | |
158 | }); | |
159 | return found; | |
160 | } | |
161 | }; | |
162 | ||
163 | // Parse strings of javascript that php couldn't interpret | |
66af6937 CW |
164 | function evaluate(collection) { |
165 | _.each(collection, function(item) { | |
166 | if (_.isPlainObject(item)) { | |
167 | evaluate(item['#children']); | |
168 | _.each(item, function(node, idx) { | |
169 | if (_.isString(node)) { | |
170 | var str = _.trim(node); | |
171 | if (str[0] === '{' || str[0] === '[' || str.slice(0, 3) === 'ts(') { | |
172 | item[idx] = $parse(str)({ts: $scope.ts}); | |
173 | } | |
174 | } | |
175 | }); | |
176 | } | |
177 | }); | |
178 | } | |
179 | ||
65c9e7ae CW |
180 | function expandFields(collection, entityType) { |
181 | _.each(collection, function (item) { | |
182 | if (_.isPlainObject(item)) { | |
183 | if (item['af-fieldset']) { | |
184 | expandFields(item['#children'], $scope.editor.getEntity(item['af-fieldset']).type); | |
185 | } | |
186 | else if (item['#tag'] === 'af-field') { | |
187 | item.defn = item.defn || {}; | |
188 | _.defaults(item.defn, _.cloneDeep(_.pick($scope.editor.getField(entityType, item.name), ['title', 'input_type', 'input_attrs']))); | |
189 | } else { | |
190 | expandFields(item['#children'], entityType); | |
191 | } | |
192 | } | |
193 | }); | |
194 | } | |
195 | ||
f6c0358e CW |
196 | } |
197 | }; | |
198 | }); | |
199 | ||
200 | function getTags(collection, tagName, indexBy) { | |
201 | var items = []; | |
202 | _.each(collection, function(item) { | |
203 | if (item && typeof item === 'object') { | |
204 | if (item['#tag'] === tagName) { | |
205 | items.push(item); | |
206 | } | |
207 | var childTags = item['#children'] ? getTags(item['#children'], tagName) : []; | |
208 | if (childTags.length) { | |
209 | Array.prototype.push.apply(items, childTags); | |
210 | } | |
211 | } | |
212 | }); | |
213 | return indexBy ? _.indexBy(items, indexBy) : items; | |
214 | } | |
215 | ||
f3cd3852 CW |
216 | // Turns a space-separated list (e.g. css classes) into an array |
217 | function splitClass(str) { | |
218 | if (_.isArray(str)) { | |
219 | return str; | |
220 | } | |
221 | return str ? _.unique(_.trim(str).split(/\s+/g)) : []; | |
222 | } | |
223 | ||
66af6937 CW |
224 | angular.module('afGuiEditor').directive('afGuiBlock', function() { |
225 | return { | |
226 | restrict: 'A', | |
227 | templateUrl: '~/afGuiEditor/block.html', | |
228 | scope: { | |
f3cd3852 CW |
229 | node: '=afGuiBlock', |
230 | entityName: '=' | |
66af6937 | 231 | }, |
f3cd3852 CW |
232 | require: '^^afGuiEditor', |
233 | link: function($scope, element, attrs, editor) { | |
234 | $scope.editor = editor; | |
235 | }, | |
236 | controller: function($scope) { | |
237 | $scope.block = this; | |
238 | this.node = $scope.node; | |
239 | ||
240 | this.modifyClasses = function(item, toRemove, toAdd) { | |
241 | var classes = splitClass(item['class']); | |
242 | if (toRemove) { | |
243 | classes = _.difference(classes, splitClass(toRemove)); | |
244 | } | |
245 | if (toAdd) { | |
246 | classes = _.unique(classes.concat(splitClass(toAdd))); | |
247 | } | |
248 | item['class'] = classes.join(' '); | |
249 | }; | |
250 | ||
251 | this.getNodeType = function(node) { | |
252 | if (!node) { | |
253 | return null; | |
254 | } | |
255 | if (node['#tag'] === 'af-field') { | |
256 | return 'field'; | |
257 | } | |
258 | if (node['af-fieldset']) { | |
259 | return 'fieldset'; | |
260 | } | |
261 | var classes = splitClass(node['class']); | |
262 | if (_.contains(classes, 'af-block')) { | |
263 | return 'block'; | |
264 | } | |
265 | if (_.contains(classes, 'af-text')) { | |
266 | return 'text'; | |
267 | } | |
268 | return null; | |
269 | }; | |
270 | ||
271 | $scope.isSelectedFieldset = function(entityName) { | |
272 | return entityName === $scope.editor.getSelectedEntity(); | |
273 | }; | |
274 | ||
275 | $scope.selectEntity = function() { | |
276 | if ($scope.node['af-fieldset']) { | |
277 | $scope.editor.selectEntity($scope.node['af-fieldset']); | |
278 | } | |
66af6937 | 279 | }; |
f3cd3852 CW |
280 | |
281 | $scope.tags = { | |
282 | div: ts('Block'), | |
283 | fieldset: ts('Fieldset') | |
284 | }; | |
285 | ||
66af6937 CW |
286 | } |
287 | }; | |
288 | }); | |
289 | ||
f3cd3852 | 290 | angular.module('afGuiEditor').directive('afGuiField', function() { |
66af6937 CW |
291 | return { |
292 | restrict: 'A', | |
f3cd3852 | 293 | templateUrl: '~/afGuiEditor/field.html', |
66af6937 | 294 | scope: { |
f3cd3852 CW |
295 | node: '=afGuiField', |
296 | entityName: '=' | |
297 | }, | |
298 | require: '^^afGuiEditor', | |
299 | link: function($scope, element, attrs, editor) { | |
300 | $scope.editor = editor; | |
66af6937 | 301 | }, |
f3cd3852 CW |
302 | controller: function($scope) { |
303 | ||
304 | $scope.getEntity = function() { | |
305 | return $scope.editor.getEntity($scope.entityName); | |
306 | }; | |
307 | ||
308 | $scope.getDefn = function() { | |
309 | return $scope.editor.getField($scope.getEntity().type, $scope.node.name); | |
66af6937 CW |
310 | }; |
311 | } | |
312 | }; | |
313 | }); | |
314 | ||
f3cd3852 | 315 | angular.module('afGuiEditor').directive('afGuiText', function() { |
66af6937 CW |
316 | return { |
317 | restrict: 'A', | |
f3cd3852 | 318 | templateUrl: '~/afGuiEditor/text.html', |
66af6937 | 319 | scope: { |
f3cd3852 | 320 | node: '=afGuiText' |
66af6937 | 321 | }, |
f3cd3852 CW |
322 | require: '^^afGuiBlock', |
323 | link: { | |
324 | pre: function($scope, element, attrs, block) { | |
325 | $scope.block = block; | |
326 | }, | |
327 | post: function($scope, element, attrs) { | |
328 | if ($scope.block.node && $scope.block.node['#tag'] === 'fieldset') { | |
329 | $scope.tags.legend = ts('Fieldset Legend'); | |
330 | } | |
331 | } | |
332 | }, | |
333 | controller: function($scope) { | |
334 | $scope.tags = { | |
335 | p: ts('Normal Text'), | |
336 | h1: ts('Heading 1'), | |
337 | h2: ts('Heading 2'), | |
338 | h3: ts('Heading 3'), | |
339 | h4: ts('Heading 4'), | |
340 | h5: ts('Heading 5'), | |
341 | h6: ts('Heading 6') | |
342 | }; | |
343 | ||
344 | $scope.alignments = { | |
345 | 'text-left': ts('Align left'), | |
346 | 'text-center': ts('Align center'), | |
347 | 'text-right': ts('Align right'), | |
348 | 'text-justify': ts('Justify') | |
349 | }; | |
350 | ||
351 | $scope.getAlign = function() { | |
352 | return _.intersection(splitClass($scope.node['class']), _.keys($scope.alignments))[0]; | |
353 | }; | |
354 | ||
355 | $scope.setAlign = function(val) { | |
356 | $scope.block.modifyClasses($scope.node, _.keys($scope.alignments), val); | |
357 | }; | |
66af6937 CW |
358 | } |
359 | }; | |
360 | }); | |
361 | ||
a2854e2e | 362 | // Editable titles using ngModel & html5 contenteditable |
a064e90d | 363 | // Cribbed from ContactLayoutEditor |
a2854e2e CW |
364 | angular.module('afGuiEditor').directive("afGuiEditable", function() { |
365 | return { | |
366 | restrict: "A", | |
367 | require: "ngModel", | |
368 | link: function(scope, element, attrs, ngModel) { | |
369 | var ts = CRM.ts(); | |
370 | ||
371 | function read() { | |
372 | var htmlVal = element.html(); | |
373 | if (!htmlVal) { | |
374 | htmlVal = ts('Unnamed'); | |
375 | element.html(htmlVal); | |
376 | } | |
377 | ngModel.$setViewValue(htmlVal); | |
378 | } | |
379 | ||
380 | ngModel.$render = function() { | |
381 | element.html(ngModel.$viewValue || ' '); | |
382 | }; | |
383 | ||
384 | // Special handling for enter and escape keys | |
385 | element.on('keydown', function(e) { | |
386 | // Enter: prevent line break and save | |
387 | if (e.which === 13) { | |
388 | e.preventDefault(); | |
389 | element.blur(); | |
390 | } | |
391 | // Escape: undo | |
392 | if (e.which === 27) { | |
393 | element.html(ngModel.$viewValue || ' '); | |
394 | element.blur(); | |
395 | } | |
396 | }); | |
397 | ||
398 | element.on("blur change", function() { | |
399 | scope.$apply(read); | |
400 | }); | |
401 | ||
402 | element.attr('contenteditable', 'true').addClass('crm-editable-enabled'); | |
403 | } | |
404 | }; | |
405 | }); | |
406 | ||
a064e90d CW |
407 | // Cribbed from the Api4 Explorer |
408 | angular.module('afGuiEditor').directive('afGuiFieldValue', function() { | |
409 | return { | |
410 | scope: { | |
411 | field: '=afGuiFieldValue' | |
412 | }, | |
413 | require: 'ngModel', | |
414 | link: function (scope, element, attrs, ctrl) { | |
415 | var ts = scope.ts = CRM.ts(), | |
416 | multi; | |
417 | ||
418 | function destroyWidget() { | |
419 | var $el = $(element); | |
420 | if ($el.is('.crm-form-date-wrapper .crm-hidden-date')) { | |
421 | $el.crmDatepicker('destroy'); | |
422 | } | |
423 | if ($el.is('.select2-container + input')) { | |
424 | $el.crmEntityRef('destroy'); | |
425 | } | |
426 | $(element).removeData().removeAttr('type').removeAttr('placeholder').show(); | |
427 | } | |
428 | ||
429 | function makeWidget(field) { | |
430 | var $el = $(element), | |
431 | inputType = field.input_type, | |
432 | dataType = field.data_type; | |
433 | multi = field.serialize || dataType === 'Array'; | |
434 | if (inputType === 'Date') { | |
435 | $el.crmDatepicker({time: (field.input_attrs && field.input_attrs.time) || false}); | |
436 | } | |
437 | else if (field.fk_entity || field.options || dataType === 'Boolean') { | |
438 | if (field.fk_entity) { | |
439 | $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}}); | |
440 | } else if (field.options) { | |
441 | var options = _.transform(field.options, function(options, val, key) { | |
442 | options.push({id: key, text: val}); | |
443 | }, []); | |
444 | $el.select2({data: options, multiple: multi}); | |
445 | } else if (dataType === 'Boolean') { | |
446 | $el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [ | |
447 | {id: '1', text: ts('Yes')}, | |
448 | {id: '0', text: ts('No')} | |
449 | ]}); | |
450 | } | |
451 | } else if (dataType === 'Integer' && !multi) { | |
452 | $el.attr('type', 'number'); | |
453 | } | |
454 | } | |
455 | ||
456 | // Copied from ng-list but applied conditionally if field is multi-valued | |
457 | var parseList = function(viewValue) { | |
458 | // If the viewValue is invalid (say required but empty) it will be `undefined` | |
459 | if (_.isUndefined(viewValue)) return; | |
460 | ||
461 | if (!multi) { | |
462 | return viewValue; | |
463 | } | |
464 | ||
465 | var list = []; | |
466 | ||
467 | if (viewValue) { | |
468 | _.each(viewValue.split(','), function(value) { | |
469 | if (value) list.push(_.trim(value)); | |
470 | }); | |
471 | } | |
472 | ||
473 | return list; | |
474 | }; | |
475 | ||
476 | // Copied from ng-list | |
477 | ctrl.$parsers.push(parseList); | |
478 | ctrl.$formatters.push(function(value) { | |
479 | return _.isArray(value) ? value.join(', ') : value; | |
480 | }); | |
481 | ||
482 | // Copied from ng-list | |
483 | ctrl.$isEmpty = function(value) { | |
484 | return !value || !value.length; | |
485 | }; | |
486 | ||
487 | scope.$watchCollection('field', function(field) { | |
488 | destroyWidget(); | |
489 | if (field) { | |
490 | makeWidget(field); | |
491 | } | |
492 | }); | |
493 | } | |
494 | }; | |
495 | }); | |
496 | ||
f6c0358e | 497 | })(angular, CRM.$, CRM._); |