Commit | Line | Data |
---|---|---|
f6c0358e | 1 | (function(angular, $, _) { |
881d52bb | 2 | "use strict"; |
f6c0358e CW |
3 | angular.module('afGuiEditor', CRM.angRequires('afGuiEditor')); |
4 | ||
2f663f46 | 5 | angular.module('afGuiEditor').directive('afGuiEditor', function(crmApi4, $parse, $timeout, $location) { |
f6c0358e CW |
6 | return { |
7 | restrict: 'A', | |
f6c0358e CW |
8 | templateUrl: '~/afGuiEditor/main.html', |
9 | scope: { | |
10 | afGuiEditor: '=' | |
11 | }, | |
e72f4d81 CW |
12 | link: function($scope, element, attrs) { |
13 | // Shoehorn in a non-angular widget for picking icons | |
14 | CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() { | |
15 | $('#af-gui-icon-picker').crmIconPicker().change(function() { | |
16 | if (editingIcon) { | |
17 | $scope.$apply(function() { | |
e1aca853 | 18 | editingIcon[editingIconProp] = $('#af-gui-icon-picker').val(); |
e72f4d81 CW |
19 | editingIcon = null; |
20 | $('#af-gui-icon-picker').val('').change(); | |
21 | }); | |
22 | } | |
23 | }); | |
24 | }); | |
25 | }, | |
f3cd3852 | 26 | controller: function($scope) { |
edac0d6e | 27 | var ts = $scope.ts = CRM.ts(); |
f6c0358e | 28 | $scope.afform = null; |
28b4ace4 | 29 | $scope.saving = false; |
1f060357 | 30 | $scope.selectedEntityName = null; |
edac0d6e CW |
31 | $scope.meta = this.meta = CRM.afformAdminData; |
32 | this.scope = $scope; | |
fd6c0814 | 33 | var editor = $scope.editor = this; |
f6c0358e | 34 | var newForm = { |
ee1a7fb6 | 35 | title: '', |
2b286647 | 36 | permission: 'access CiviCRM', |
66af6937 | 37 | layout: [{ |
f6c0358e | 38 | '#tag': 'af-form', |
905b0fd5 | 39 | ctrl: 'afform', |
fd6c0814 | 40 | '#children': [] |
66af6937 | 41 | }] |
f6c0358e | 42 | }; |
9633741a CW |
43 | // Fetch the current form plus all blocks |
44 | crmApi4('Afform', 'get', {where: [["OR", [["name", "=", $scope.afGuiEditor.name], ["block", "IS NOT NULL"]]]], layoutFormat: 'shallow', formatWhitespace: true}) | |
45 | .then(initialize); | |
46 | ||
47 | // Initialize the current form + list of blocks | |
48 | function initialize(afforms) { | |
49 | $scope.meta.blocks = {}; | |
50 | _.each(afforms, function(form) { | |
51 | evaluate(form.layout); | |
52 | if (form.block) { | |
e38db494 | 53 | $scope.meta.blocks[form.directive_name] = form; |
9633741a CW |
54 | } |
55 | if (form.name === $scope.afGuiEditor.name) { | |
56 | $scope.afform = form; | |
57 | } | |
fd6c0814 | 58 | }); |
9633741a CW |
59 | if (!$scope.afform) { |
60 | $scope.afform = _.cloneDeep(newForm); | |
61 | if ($scope.afGuiEditor.name != '0') { | |
ee1a7fb6 | 62 | alert('Error: unknown form "' + $scope.afGuiEditor.name + '"'); |
9633741a CW |
63 | } |
64 | } | |
1f060357 | 65 | $scope.layout = findRecursive($scope.afform.layout, {'#tag': 'af-form'})[0]; |
1f060357 | 66 | $scope.entities = findRecursive($scope.layout['#children'], {'#tag': 'af-entity'}, 'name'); |
28b4ace4 | 67 | |
9633741a CW |
68 | if ($scope.afGuiEditor.name == '0') { |
69 | editor.addEntity('Individual'); | |
70 | $scope.layout['#children'].push($scope.meta.elements.submit.element); | |
71 | } | |
72 | ||
28b4ace4 | 73 | // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model |
ee1a7fb6 | 74 | $scope.changesSaved = $scope.afGuiEditor.name == '0' ? false : 1; |
28b4ace4 CW |
75 | $scope.$watch('afform', function () { |
76 | $scope.changesSaved = $scope.changesSaved === 1; | |
77 | }, true); | |
f6c0358e CW |
78 | } |
79 | ||
0406c8f9 CW |
80 | this.addEntity = function(type) { |
81 | var meta = editor.meta.entities[type], | |
82 | num = 1; | |
f6c0358e | 83 | // Give this new entity a unique name |
0406c8f9 | 84 | while (!!$scope.entities[type + num]) { |
f6c0358e CW |
85 | num++; |
86 | } | |
0406c8f9 | 87 | $scope.entities[type + num] = _.assign($parse(meta.defaults)($scope), { |
f6c0358e | 88 | '#tag': 'af-entity', |
0406c8f9 CW |
89 | type: meta.entity, |
90 | name: type + num, | |
91 | label: meta.label + ' ' + num | |
1b745abc | 92 | }); |
1f060357 CW |
93 | // Add this af-entity tag after the last existing one |
94 | var pos = 1 + _.findLastIndex($scope.layout['#children'], {'#tag': 'af-entity'}); | |
0406c8f9 | 95 | $scope.layout['#children'].splice(pos, 0, $scope.entities[type + num]); |
1f060357 | 96 | // Create a new af-fieldset container for the entity |
e9dc7ca5 CW |
97 | var fieldset = _.cloneDeep(editor.meta.elements.fieldset.element); |
98 | fieldset['af-fieldset'] = type + num; | |
99 | fieldset['#children'][0]['#children'][0]['#text'] = meta.label + ' ' + num; | |
0406c8f9 CW |
100 | // Add default contact name block |
101 | if (meta.entity === 'Contact') { | |
ee1a7fb6 | 102 | fieldset['#children'].push({'#tag': 'afblock-name-' + type.toLowerCase()}); |
0406c8f9 | 103 | } |
1f060357 CW |
104 | // Attempt to place the new af-fieldset after the last one on the form |
105 | pos = 1 + _.findLastIndex($scope.layout['#children'], 'af-fieldset'); | |
106 | if (pos) { | |
107 | $scope.layout['#children'].splice(pos, 0, fieldset); | |
108 | } else { | |
109 | $scope.layout['#children'].push(fieldset); | |
110 | } | |
0406c8f9 | 111 | return type + num; |
f6c0358e CW |
112 | }; |
113 | ||
f3cd3852 | 114 | this.removeEntity = function(entityName) { |
f6c0358e | 115 | delete $scope.entities[entityName]; |
1f060357 | 116 | removeRecursive($scope.layout['#children'], {'#tag': 'af-entity', name: entityName}); |
fd6c0814 | 117 | removeRecursive($scope.layout['#children'], {'af-fieldset': entityName}); |
f3cd3852 | 118 | this.selectEntity(null); |
f6c0358e CW |
119 | }; |
120 | ||
f3cd3852 | 121 | this.selectEntity = function(entityName) { |
1f060357 | 122 | $scope.selectedEntityName = entityName; |
f6c0358e CW |
123 | }; |
124 | ||
f3cd3852 | 125 | this.getField = function(entityType, fieldName) { |
0406c8f9 | 126 | return $scope.meta.entities[entityType].fields[fieldName]; |
f3cd3852 CW |
127 | }; |
128 | ||
129 | this.getEntity = function(entityName) { | |
130 | return $scope.entities[entityName]; | |
131 | }; | |
132 | ||
0406c8f9 | 133 | this.getSelectedEntityName = function() { |
1f060357 | 134 | return $scope.selectedEntityName; |
a064e90d CW |
135 | }; |
136 | ||
e9dc7ca5 CW |
137 | // Validates that a drag-n-drop action is allowed |
138 | this.onDrop = function(event, ui) { | |
139 | var sort = ui.item.sortable; | |
140 | // Check if this is a callback for an item dropped into a different container | |
141 | // @see https://github.com/angular-ui/ui-sortable notes on canceling | |
142 | if (!sort.received && sort.source[0] !== sort.droptarget[0]) { | |
143 | var $source = $(sort.source[0]), | |
144 | $target = $(sort.droptarget[0]), | |
145 | $item = $(ui.item[0]); | |
146 | // Fields cannot be dropped outside their own entity | |
147 | if ($item.is('[af-gui-field]') || $item.has('[af-gui-field]').length) { | |
148 | if ($source.closest('[data-entity]').attr('data-entity') !== $target.closest('[data-entity]').attr('data-entity')) { | |
149 | return sort.cancel(); | |
150 | } | |
151 | } | |
152 | // Entity-fieldsets cannot be dropped into other entity-fieldsets | |
153 | if ((sort.model['af-fieldset'] || $item.has('.af-gui-fieldset').length) && $target.closest('.af-gui-fieldset').length) { | |
154 | return sort.cancel(); | |
155 | } | |
156 | } | |
157 | }; | |
158 | ||
fd6c0814 CW |
159 | $scope.addEntity = function(entityType) { |
160 | var entityName = editor.addEntity(entityType); | |
161 | editor.selectEntity(entityName); | |
162 | }; | |
163 | ||
28b4ace4 | 164 | $scope.save = function() { |
2f663f46 | 165 | $scope.saving = $scope.changesSaved = true; |
20d4bccc | 166 | crmApi4('Afform', 'save', {formatWhitespace: true, records: [JSON.parse(angular.toJson($scope.afform))]}) |
2f663f46 CW |
167 | .then(function (data) { |
168 | $scope.saving = false; | |
169 | $scope.afform.name = data[0].name; | |
170 | // FIXME: This causes an unnecessary reload when saving a new form | |
171 | $location.search('name', data[0].name); | |
28b4ace4 CW |
172 | }); |
173 | }; | |
174 | ||
1b745abc CW |
175 | $scope.$watch('afform.title', function(newTitle, oldTitle) { |
176 | if (typeof oldTitle === 'string') { | |
177 | _.each($scope.entities, function(entity) { | |
178 | if (entity.data && entity.data.source === oldTitle) { | |
179 | entity.data.source = newTitle; | |
180 | } | |
181 | }); | |
182 | } | |
183 | }); | |
184 | ||
65c9e7ae | 185 | // Parse strings of javascript that php couldn't interpret |
66af6937 CW |
186 | function evaluate(collection) { |
187 | _.each(collection, function(item) { | |
188 | if (_.isPlainObject(item)) { | |
189 | evaluate(item['#children']); | |
190 | _.each(item, function(node, idx) { | |
191 | if (_.isString(node)) { | |
192 | var str = _.trim(node); | |
193 | if (str[0] === '{' || str[0] === '[' || str.slice(0, 3) === 'ts(') { | |
194 | item[idx] = $parse(str)({ts: $scope.ts}); | |
195 | } | |
196 | } | |
197 | }); | |
198 | } | |
199 | }); | |
200 | } | |
201 | ||
f6c0358e CW |
202 | } |
203 | }; | |
204 | }); | |
205 | ||
1f060357 CW |
206 | // Recursively searches a collection and its children using _.filter |
207 | // Returns an array of all matches, or an object if the indexBy param is used | |
208 | function findRecursive(collection, predicate, indexBy) { | |
209 | var items = _.filter(collection, predicate); | |
f6c0358e | 210 | _.each(collection, function(item) { |
1f060357 CW |
211 | if (_.isPlainObject(item) && item['#children']) { |
212 | var childMatches = findRecursive(item['#children'], predicate); | |
213 | if (childMatches.length) { | |
214 | Array.prototype.push.apply(items, childMatches); | |
f6c0358e CW |
215 | } |
216 | } | |
217 | }); | |
218 | return indexBy ? _.indexBy(items, indexBy) : items; | |
219 | } | |
220 | ||
1f060357 | 221 | // Applies _.remove() to an item and its children |
fd6c0814 CW |
222 | function removeRecursive(collection, removeParams) { |
223 | _.remove(collection, removeParams); | |
224 | _.each(collection, function(item) { | |
225 | if (_.isPlainObject(item) && item['#children']) { | |
226 | removeRecursive(item['#children'], removeParams); | |
227 | } | |
228 | }); | |
229 | } | |
230 | ||
1f060357 CW |
231 | // Turns a space-separated list (e.g. css classes) into an array |
232 | function splitClass(str) { | |
233 | if (_.isArray(str)) { | |
234 | return str; | |
235 | } | |
236 | return str ? _.unique(_.trim(str).split(/\s+/g)) : []; | |
237 | } | |
238 | ||
edac0d6e CW |
239 | angular.module('afGuiEditor').directive('afGuiEntity', function($timeout) { |
240 | return { | |
241 | restrict: 'A', | |
242 | templateUrl: '~/afGuiEditor/entity.html', | |
243 | scope: { | |
3fc5bbff | 244 | entity: '=afGuiEntity' |
edac0d6e CW |
245 | }, |
246 | require: '^^afGuiEditor', | |
247 | link: function ($scope, element, attrs, editor) { | |
248 | $scope.editor = editor; | |
249 | }, | |
250 | controller: function ($scope) { | |
251 | var ts = $scope.ts = CRM.ts(); | |
252 | $scope.controls = {}; | |
253 | $scope.fieldList = []; | |
e1aca853 | 254 | $scope.blockList = []; |
344e8290 | 255 | $scope.blockTitles = []; |
e1aca853 CW |
256 | $scope.elementList = []; |
257 | $scope.elementTitles = []; | |
edac0d6e | 258 | |
54dbfd05 CW |
259 | function getEntityType() { |
260 | return $scope.entity.type === 'Contact' ? $scope.entity.data.contact_type : $scope.entity.type; | |
261 | } | |
262 | ||
0406c8f9 | 263 | $scope.getMeta = function() { |
54dbfd05 | 264 | return $scope.editor ? $scope.editor.meta.entities[getEntityType()] : {}; |
0406c8f9 CW |
265 | }; |
266 | ||
edac0d6e | 267 | $scope.valuesFields = function() { |
0406c8f9 | 268 | var fields = _.transform($scope.getMeta().fields, function(fields, field) { |
edac0d6e | 269 | fields.push({id: field.name, text: field.title, disabled: $scope.fieldInUse(field.name)}); |
0406c8f9 | 270 | }, []); |
edac0d6e CW |
271 | return {results: fields}; |
272 | }; | |
273 | ||
274 | $scope.removeValue = function(entity, fieldName) { | |
275 | delete entity.data[fieldName]; | |
276 | }; | |
277 | ||
e1aca853 | 278 | function buildPaletteLists() { |
edac0d6e | 279 | var search = $scope.controls.fieldSearch ? $scope.controls.fieldSearch.toLowerCase() : null; |
e1aca853 CW |
280 | buildFieldList(search); |
281 | buildBlockList(search); | |
282 | buildElementList(search); | |
283 | } | |
284 | ||
285 | function buildFieldList(search) { | |
286 | $scope.fieldList.length = 0; | |
54dbfd05 CW |
287 | $scope.fieldList.push({ |
288 | entityName: $scope.entity.name, | |
289 | entityType: getEntityType(), | |
290 | label: ts('%1 Fields', {1: $scope.getMeta().label}), | |
291 | fields: filterFields($scope.getMeta().fields) | |
292 | }); | |
293 | ||
294 | _.each($scope.editor.meta.entities, function(entity, entityName) { | |
295 | if (check($scope.editor.scope.layout['#children'], {'af-join': entityName})) { | |
edac0d6e | 296 | $scope.fieldList.push({ |
54dbfd05 CW |
297 | entityName: $scope.entity.name + '-join-' + entityName, |
298 | entityType: entityName, | |
299 | label: ts('%1 Fields', {1: entity.label}), | |
300 | fields: filterFields(entity.fields) | |
edac0d6e CW |
301 | }); |
302 | } | |
303 | }); | |
54dbfd05 CW |
304 | |
305 | function filterFields(fields) { | |
306 | return _.transform(fields, function(fieldList, field) { | |
307 | if (!search || _.contains(field.name, search) || _.contains(field.title.toLowerCase(), search)) { | |
308 | fieldList.push({ | |
309 | "#tag": "af-field", | |
310 | name: field.name | |
311 | }); | |
312 | } | |
313 | }, []); | |
314 | } | |
e1aca853 CW |
315 | } |
316 | ||
317 | function buildBlockList(search) { | |
318 | $scope.blockList.length = 0; | |
344e8290 | 319 | $scope.blockTitles.length = 0; |
e1aca853 | 320 | _.each($scope.editor.meta.blocks, function(block, directive) { |
0406c8f9 CW |
321 | if ((!search || _.contains(directive, search) || _.contains(block.name.toLowerCase(), search) || _.contains(block.title.toLowerCase(), search)) && |
322 | (block.block === '*' || block.block === $scope.entity.type || ($scope.entity.type === 'Contact' && block.block === $scope.entity.data.contact_type)) | |
323 | ) { | |
344e8290 CW |
324 | var item = {"#tag": block.join ? "div" : directive}; |
325 | if (block.join) { | |
326 | item['af-join'] = block.join; | |
327 | item['#children'] = [{"#tag": directive}]; | |
328 | } | |
329 | if (block.repeat) { | |
330 | item['af-repeat'] = ts('Add'); | |
331 | item.min = '1'; | |
332 | if (typeof block.repeat === 'number') { | |
333 | item.max = '' + block.repeat; | |
334 | } | |
335 | } | |
336 | $scope.blockList.push(item); | |
337 | $scope.blockTitles.push(block.title); | |
e1aca853 CW |
338 | } |
339 | }); | |
340 | } | |
341 | ||
342 | function buildElementList(search) { | |
343 | $scope.elementList.length = 0; | |
344 | $scope.elementTitles.length = 0; | |
345 | _.each($scope.editor.meta.elements, function(element, name) { | |
346 | if (!search || _.contains(name, search) || _.contains(element.title.toLowerCase(), search)) { | |
e9dc7ca5 CW |
347 | var node = _.cloneDeep(element.element); |
348 | if (name === 'fieldset') { | |
349 | node['af-fieldset'] = $scope.entity.name; | |
350 | } | |
351 | $scope.elementList.push(node); | |
352 | $scope.elementTitles.push(name === 'fieldset' ? ts('Fieldset for %1', {1: $scope.entity.label}) : element.title); | |
e1aca853 CW |
353 | } |
354 | }); | |
355 | } | |
edac0d6e CW |
356 | |
357 | $scope.clearSearch = function() { | |
358 | $scope.controls.fieldSearch = ''; | |
359 | }; | |
360 | ||
e1aca853 CW |
361 | // This gets called from jquery-ui so we have to manually apply changes to scope |
362 | $scope.buildPaletteLists = function() { | |
edac0d6e CW |
363 | $timeout(function() { |
364 | $scope.$apply(function() { | |
e1aca853 | 365 | buildPaletteLists(); |
edac0d6e CW |
366 | }); |
367 | }); | |
368 | }; | |
369 | ||
370 | // Checks if a field is on the form or set as a value | |
371 | $scope.fieldInUse = function(fieldName) { | |
e1aca853 | 372 | var data = $scope.entity.data || {}; |
edac0d6e CW |
373 | if (fieldName in data) { |
374 | return true; | |
375 | } | |
e1aca853 CW |
376 | return check($scope.editor.scope.layout['#children'], {'#tag': 'af-field', name: fieldName}); |
377 | }; | |
378 | ||
344e8290 CW |
379 | $scope.blockInUse = function(block) { |
380 | if (block['af-join']) { | |
381 | return check($scope.editor.scope.layout['#children'], {'af-join': block['af-join']}); | |
382 | } | |
383 | var fieldsInBlock = _.pluck(findRecursive($scope.editor.meta.blocks[block['#tag']].layout, {'#tag': 'af-field'}), 'name'); | |
384 | return check($scope.editor.scope.layout['#children'], function(item) { | |
385 | return item['#tag'] === 'af-field' && _.includes(fieldsInBlock, item.name); | |
386 | }); | |
edac0d6e CW |
387 | }; |
388 | ||
344e8290 CW |
389 | // Check for a matching item for this entity |
390 | // Recursively checks the form layout, including block directives | |
e1aca853 CW |
391 | function check(group, criteria, found) { |
392 | if (!found) { | |
393 | found = {}; | |
394 | } | |
395 | if (_.find(group, criteria)) { | |
396 | found.match = true; | |
397 | return true; | |
398 | } | |
399 | _.each(group, function(item) { | |
400 | if (found.match) { | |
401 | return false; | |
402 | } | |
403 | if (_.isPlainObject(item)) { | |
344e8290 | 404 | // Recurse through everything but skip fieldsets for other entities |
e1aca853 CW |
405 | if ((!item['af-fieldset'] || (item['af-fieldset'] === $scope.entity.name)) && item['#children']) { |
406 | check(item['#children'], criteria, found); | |
407 | } | |
344e8290 CW |
408 | // Recurse into block directives |
409 | else if (item['#tag'] && item['#tag'] in $scope.editor.meta.blocks) { | |
410 | check($scope.editor.meta.blocks[item['#tag']].layout, criteria, found); | |
411 | } | |
e1aca853 CW |
412 | } |
413 | }); | |
414 | return found.match; | |
415 | } | |
416 | ||
edac0d6e CW |
417 | $scope.$watch('controls.addValue', function(fieldName) { |
418 | if (fieldName) { | |
419 | if (!$scope.entity.data) { | |
420 | $scope.entity.data = {}; | |
421 | } | |
422 | $scope.entity.data[fieldName] = ''; | |
423 | $scope.controls.addValue = ''; | |
424 | } | |
425 | }); | |
426 | ||
e1aca853 | 427 | $scope.$watch('controls.fieldSearch', buildPaletteLists); |
edac0d6e CW |
428 | } |
429 | }; | |
430 | }); | |
431 | ||
9633741a | 432 | angular.module('afGuiEditor').directive('afGuiContainer', function(crmApi4, dialogService) { |
66af6937 CW |
433 | return { |
434 | restrict: 'A', | |
6fb9e8d2 | 435 | templateUrl: '~/afGuiEditor/container.html', |
66af6937 | 436 | scope: { |
6fb9e8d2 | 437 | node: '=afGuiContainer', |
344e8290 | 438 | join: '=', |
f3cd3852 | 439 | entityName: '=' |
66af6937 | 440 | }, |
e1aca853 CW |
441 | require: ['^^afGuiEditor', '?^^afGuiContainer'], |
442 | link: function($scope, element, attrs, ctrls) { | |
344e8290 | 443 | var ts = $scope.ts = CRM.ts(); |
e1aca853 CW |
444 | $scope.editor = ctrls[0]; |
445 | $scope.parentContainer = ctrls[1]; | |
f3cd3852 CW |
446 | |
447 | $scope.isSelectedFieldset = function(entityName) { | |
0406c8f9 | 448 | return entityName === $scope.editor.getSelectedEntityName(); |
f3cd3852 CW |
449 | }; |
450 | ||
451 | $scope.selectEntity = function() { | |
452 | if ($scope.node['af-fieldset']) { | |
453 | $scope.editor.selectEntity($scope.node['af-fieldset']); | |
454 | } | |
66af6937 | 455 | }; |
f3cd3852 CW |
456 | |
457 | $scope.tags = { | |
6fb9e8d2 | 458 | div: ts('Container'), |
f3cd3852 CW |
459 | fieldset: ts('Fieldset') |
460 | }; | |
461 | ||
e1aca853 | 462 | // Block settings |
344e8290 CW |
463 | var block = {}; |
464 | $scope.block = null; | |
5eaf91d9 | 465 | |
e1aca853 | 466 | $scope.getSetChildren = function(val) { |
344e8290 | 467 | var collection = block.layout || ($scope.node && $scope.node['#children']); |
e1aca853 | 468 | return arguments.length ? (collection = val) : collection; |
5eaf91d9 CW |
469 | }; |
470 | ||
344e8290 CW |
471 | $scope.isRepeatable = function() { |
472 | return $scope.node['af-fieldset'] || (block.directive && $scope.editor.meta.blocks[block.directive].repeat) || $scope.join; | |
473 | }; | |
e1aca853 | 474 | |
344e8290 CW |
475 | $scope.toggleRepeat = function() { |
476 | if ('af-repeat' in $scope.node) { | |
477 | delete $scope.node.max; | |
478 | delete $scope.node.min; | |
479 | delete $scope.node['af-repeat']; | |
480 | delete $scope.node['add-icon']; | |
481 | } else { | |
482 | $scope.node.min = '1'; | |
483 | $scope.node['af-repeat'] = ts('Add'); | |
484 | } | |
485 | }; | |
e1aca853 | 486 | |
344e8290 CW |
487 | $scope.getSetMin = function(val) { |
488 | if (arguments.length) { | |
489 | if ($scope.node.max && val > parseInt($scope.node.max, 10)) { | |
490 | $scope.node.max = '' + val; | |
491 | } | |
492 | if (!val) { | |
e1aca853 | 493 | delete $scope.node.min; |
e1aca853 | 494 | } |
344e8290 CW |
495 | else { |
496 | $scope.node.min = '' + val; | |
e1aca853 | 497 | } |
344e8290 CW |
498 | } |
499 | return $scope.node.min ? parseInt($scope.node.min, 10) : null; | |
500 | }; | |
e1aca853 | 501 | |
344e8290 CW |
502 | $scope.getSetMax = function(val) { |
503 | if (arguments.length) { | |
504 | if ($scope.node.min && val && val < parseInt($scope.node.min, 10)) { | |
505 | $scope.node.min = '' + val; | |
e1aca853 | 506 | } |
344e8290 CW |
507 | if (typeof val !== 'number') { |
508 | delete $scope.node.max; | |
509 | } | |
510 | else { | |
511 | $scope.node.max = '' + val; | |
512 | } | |
513 | } | |
514 | return $scope.node.max ? parseInt($scope.node.max, 10) : null; | |
515 | }; | |
516 | ||
517 | $scope.pickAddIcon = function() { | |
518 | openIconPicker($scope.node, 'add-icon'); | |
519 | }; | |
520 | ||
521 | function getBlockNode() { | |
522 | return !$scope.join ? $scope.node : ($scope.node['#children'] && $scope.node['#children'].length === 1 ? $scope.node['#children'][0] : null); | |
523 | } | |
524 | ||
525 | function setBlockDirective(directive) { | |
526 | if ($scope.join) { | |
527 | $scope.node['#children'] = [{'#tag': directive}]; | |
528 | } else { | |
529 | delete $scope.node['#children']; | |
530 | delete $scope.node['class']; | |
531 | $scope.node['#tag'] = directive; | |
532 | } | |
533 | } | |
534 | ||
535 | function overrideBlockContents(layout) { | |
536 | $scope.node['#children'] = layout || []; | |
537 | if (!$scope.join) { | |
538 | $scope.node['#tag'] = 'div'; | |
539 | $scope.node['class'] = 'af-container'; | |
540 | } | |
9633741a | 541 | block.layout = block.directive = null; |
344e8290 CW |
542 | } |
543 | ||
544 | $scope.layouts = { | |
545 | 'af-layout-rows': ts('Contents display as rows'), | |
546 | 'af-layout-cols': ts('Contents are evenly-spaced columns'), | |
547 | 'af-layout-inline': ts('Contents are arranged inline') | |
548 | }; | |
549 | ||
550 | $scope.getLayout = function() { | |
551 | if (!$scope.node) { | |
552 | return ''; | |
553 | } | |
554 | return _.intersection(splitClass($scope.node['class']), _.keys($scope.layouts))[0] || 'af-layout-rows'; | |
555 | }; | |
556 | ||
557 | $scope.setLayout = function(val) { | |
558 | var classes = ['af-container']; | |
559 | if (val !== 'af-layout-rows') { | |
560 | classes.push(val); | |
561 | } | |
562 | modifyClasses($scope.node, _.keys($scope.layouts), classes); | |
563 | }; | |
e1aca853 | 564 | |
9633741a CW |
565 | $scope.selectBlockDirective = function() { |
566 | if (block.directive) { | |
567 | block.layout = _.cloneDeep($scope.editor.meta.blocks[block.directive].layout); | |
568 | block.original = block.directive; | |
569 | setBlockDirective(block.directive); | |
570 | } | |
571 | else { | |
572 | overrideBlockContents(block.layout); | |
573 | } | |
574 | }; | |
575 | ||
344e8290 | 576 | if (($scope.node['#tag'] in $scope.editor.meta.blocks) || $scope.join) { |
9633741a CW |
577 | initializeBlockContainer(); |
578 | } | |
579 | ||
580 | function initializeBlockContainer() { | |
581 | ||
582 | // Cancel the below $watch expressions if already set | |
583 | _.each(block.listeners, function(deregister) { | |
584 | deregister(); | |
585 | }); | |
344e8290 CW |
586 | |
587 | block = $scope.block = { | |
588 | directive: null, | |
589 | layout: null, | |
9633741a | 590 | original: null, |
344e8290 | 591 | options: [], |
9633741a | 592 | listeners: [] |
e1aca853 | 593 | }; |
51ec7d5a | 594 | |
344e8290 CW |
595 | _.each($scope.editor.meta.blocks, function(blockInfo, directive) { |
596 | if (directive === $scope.node['#tag'] || blockInfo.join === $scope.container.getFieldEntityType()) { | |
597 | block.options.push({ | |
598 | id: directive, | |
599 | text: blockInfo.title | |
600 | }); | |
e1aca853 | 601 | } |
344e8290 CW |
602 | }); |
603 | ||
9633741a CW |
604 | if (getBlockNode() && getBlockNode()['#tag'] in $scope.editor.meta.blocks) { |
605 | block.directive = block.original = getBlockNode()['#tag']; | |
606 | block.layout = _.cloneDeep($scope.editor.meta.blocks[block.directive].layout); | |
607 | } | |
e1aca853 | 608 | |
9633741a CW |
609 | block.listeners.push($scope.$watch('block.layout', function (layout, oldVal) { |
610 | if (block.directive && layout && layout !== oldVal && !angular.equals(layout, $scope.editor.meta.blocks[block.directive].layout)) { | |
344e8290 | 611 | overrideBlockContents(block.layout); |
e1aca853 | 612 | } |
9633741a | 613 | }, true)); |
e1aca853 | 614 | } |
9633741a CW |
615 | |
616 | $scope.saveBlock = function() { | |
617 | var options = CRM.utils.adjustDialogDefaults({ | |
618 | width: '500px', | |
619 | height: '300px', | |
620 | autoOpen: false, | |
621 | title: ts('Save block') | |
622 | }); | |
623 | var model = { | |
624 | title: '', | |
625 | name: null, | |
626 | layout: $scope.node['#children'] | |
627 | }; | |
628 | if ($scope.join) { | |
629 | model.join = $scope.join; | |
630 | } | |
631 | if ($scope.block && $scope.block.original) { | |
632 | model.title = $scope.editor.meta.blocks[$scope.block.original].title; | |
633 | model.name = $scope.editor.meta.blocks[$scope.block.original].name; | |
634 | model.block = $scope.editor.meta.blocks[$scope.block.original].block; | |
635 | } | |
636 | else { | |
637 | model.block = $scope.container.getFieldEntityType() || '*'; | |
638 | } | |
639 | dialogService.open('saveBlockDialog', '~/afGuiEditor/saveBlock.html', model, options) | |
640 | .then(function(block) { | |
e38db494 CW |
641 | $scope.editor.meta.blocks[block.directive_name] = block; |
642 | setBlockDirective(block.directive_name); | |
9633741a CW |
643 | initializeBlockContainer(); |
644 | }); | |
645 | }; | |
646 | ||
344e8290 CW |
647 | }, |
648 | controller: function($scope) { | |
649 | var container = $scope.container = this; | |
650 | this.node = $scope.node; | |
e1aca853 | 651 | |
344e8290 CW |
652 | this.getNodeType = function(node) { |
653 | if (!node) { | |
654 | return null; | |
655 | } | |
656 | if (node['#tag'] === 'af-field') { | |
657 | return 'field'; | |
658 | } | |
659 | if (node['af-fieldset']) { | |
660 | return 'fieldset'; | |
661 | } | |
662 | if (node['af-join']) { | |
663 | return 'join'; | |
664 | } | |
665 | if (node['#tag'] && node['#tag'] in $scope.editor.meta.blocks) { | |
666 | return 'container'; | |
667 | } | |
668 | var classes = splitClass(node['class']), | |
669 | types = ['af-container', 'af-text', 'af-button', 'af-markup'], | |
670 | type = _.intersection(types, classes); | |
671 | return type.length ? type[0].replace('af-', '') : null; | |
672 | }; | |
673 | ||
674 | this.removeElement = function(element) { | |
675 | removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey}); | |
676 | }; | |
677 | ||
678 | this.getEntityName = function() { | |
679 | return $scope.entityName.split('-join-')[0]; | |
680 | }; | |
681 | ||
682 | // Returns the primary entity type for this container e.g. "Contact" | |
683 | this.getMainEntityType = function() { | |
684 | return $scope.editor && $scope.editor.getEntity(container.getEntityName()).type; | |
685 | }; | |
686 | ||
687 | // Returns the entity type for fields within this conainer (join entity type if this is a join, else the primary entity type) | |
688 | this.getFieldEntityType = function() { | |
689 | var joinType = $scope.entityName.split('-join-'); | |
690 | return joinType[1] || ($scope.editor && $scope.editor.getEntity(joinType[0]).type); | |
691 | }; | |
e1aca853 | 692 | |
66af6937 CW |
693 | } |
694 | }; | |
695 | }); | |
696 | ||
9633741a CW |
697 | angular.module('afGuiEditor').controller('afGuiSaveBlock', function($scope, crmApi4, dialogService) { |
698 | var ts = $scope.ts = CRM.ts(), | |
699 | model = $scope.model, | |
700 | original = $scope.original = { | |
701 | title: model.title, | |
702 | name: model.name | |
703 | }; | |
704 | if (model.name) { | |
705 | $scope.$watch('model.name', function(val, oldVal) { | |
706 | if (!val && model.title === original.title) { | |
707 | model.title += ' ' + ts('(copy)'); | |
708 | } | |
709 | else if (val === original.name && val !== oldVal) { | |
710 | model.title = original.title; | |
711 | } | |
712 | }); | |
713 | } | |
714 | $scope.cancel = function() { | |
715 | dialogService.cancel('saveBlockDialog'); | |
716 | }; | |
717 | $scope.save = function() { | |
718 | $('.ui-dialog:visible').block(); | |
719 | crmApi4('Afform', 'save', {formatWhitespace: true, records: [JSON.parse(angular.toJson(model))]}) | |
720 | .then(function(result) { | |
721 | dialogService.close('saveBlockDialog', result[0]); | |
722 | }); | |
723 | }; | |
724 | }); | |
725 | ||
f3cd3852 | 726 | angular.module('afGuiEditor').directive('afGuiField', function() { |
66af6937 CW |
727 | return { |
728 | restrict: 'A', | |
f3cd3852 | 729 | templateUrl: '~/afGuiEditor/field.html', |
66af6937 | 730 | scope: { |
e1aca853 | 731 | node: '=afGuiField' |
f3cd3852 | 732 | }, |
6fb9e8d2 | 733 | require: ['^^afGuiEditor', '^^afGuiContainer'], |
4bd44053 CW |
734 | link: function($scope, element, attrs, ctrls) { |
735 | $scope.editor = ctrls[0]; | |
6fb9e8d2 | 736 | $scope.container = ctrls[1]; |
66af6937 | 737 | }, |
f3cd3852 | 738 | controller: function($scope) { |
4bd44053 | 739 | var ts = $scope.ts = CRM.ts(); |
b4def6e9 CW |
740 | $scope.editingOptions = false; |
741 | var yesNo = [ | |
742 | {key: '1', label: ts('Yes')}, | |
743 | {key: '0', label: ts('No')} | |
744 | ]; | |
f3cd3852 CW |
745 | |
746 | $scope.getEntity = function() { | |
e1aca853 | 747 | return $scope.editor ? $scope.editor.getEntity($scope.container.getEntityName()) : {}; |
f3cd3852 CW |
748 | }; |
749 | ||
b4def6e9 | 750 | $scope.getDefn = this.getDefn = function() { |
e1aca853 | 751 | return $scope.editor ? $scope.editor.getField($scope.container.getFieldEntityType(), $scope.node.name) : {}; |
66af6937 | 752 | }; |
4bd44053 | 753 | |
b4def6e9 CW |
754 | $scope.hasOptions = function() { |
755 | var inputType = $scope.getProp('input_type'); | |
756 | return _.contains(['CheckBox', 'Radio', 'Select'], inputType) && !(inputType === 'CheckBox' && !$scope.getDefn().options); | |
757 | }; | |
758 | ||
759 | $scope.getOptions = this.getOptions = function() { | |
760 | if ($scope.node.defn && $scope.node.defn.options) { | |
761 | return $scope.node.defn.options; | |
762 | } | |
763 | return $scope.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo); | |
764 | }; | |
765 | ||
b4def6e9 CW |
766 | $scope.resetOptions = function() { |
767 | delete $scope.node.defn.options; | |
768 | }; | |
769 | ||
770 | $scope.editOptions = function() { | |
771 | $scope.editingOptions = true; | |
192695ae | 772 | $('#afGuiEditor').addClass('af-gui-editing-content'); |
b4def6e9 CW |
773 | }; |
774 | ||
775 | $scope.inputTypeCanBe = function(type) { | |
776 | var defn = $scope.getDefn(); | |
777 | switch (type) { | |
778 | case 'CheckBox': | |
779 | case 'Radio': | |
780 | case 'Select': | |
781 | return !(!defn.options && defn.data_type !== 'Boolean'); | |
782 | ||
783 | case 'TextArea': | |
784 | case 'RichTextEditor': | |
785 | return (defn.data_type === 'Text' || defn.data_type === 'String'); | |
786 | } | |
787 | return true; | |
788 | }; | |
789 | ||
790 | // Returns a value from either the local field defn or the base defn | |
4bd44053 CW |
791 | $scope.getProp = function(propName) { |
792 | var path = propName.split('.'), | |
793 | item = path.pop(), | |
794 | localDefn = drillDown($scope.node.defn || {}, path); | |
795 | if (typeof localDefn[item] !== 'undefined') { | |
796 | return localDefn[item]; | |
797 | } | |
798 | return drillDown($scope.getDefn(), path)[item]; | |
799 | }; | |
800 | ||
b4def6e9 CW |
801 | // Checks for a value in either the local field defn or the base defn |
802 | $scope.propIsset = function(propName) { | |
803 | var val = $scope.getProp(propName); | |
804 | return !(typeof val === 'undefined' || val === null); | |
805 | }; | |
806 | ||
0d97b137 CW |
807 | $scope.toggleLabel = function() { |
808 | $scope.node.defn = $scope.node.defn || {}; | |
809 | if ($scope.node.defn.title === false) { | |
810 | delete $scope.node.defn.title; | |
811 | } else { | |
812 | $scope.node.defn.title = false; | |
813 | } | |
814 | }; | |
815 | ||
4bd44053 CW |
816 | $scope.toggleRequired = function() { |
817 | getSet('required', !getSet('required')); | |
818 | return false; | |
819 | }; | |
820 | ||
821 | $scope.toggleHelp = function(position) { | |
b4def6e9 | 822 | getSet('help_' + position, $scope.propIsset('help_' + position) ? null : ($scope.getDefn()['help_' + position] || ts('Enter text'))); |
4bd44053 CW |
823 | return false; |
824 | }; | |
825 | ||
826 | // Getter/setter for definition props | |
827 | $scope.getSet = function(propName) { | |
828 | return _.wrap(propName, getSet); | |
829 | }; | |
830 | ||
4bd44053 CW |
831 | // Getter/setter callback |
832 | function getSet(propName, val) { | |
833 | if (arguments.length > 1) { | |
834 | var path = propName.split('.'), | |
835 | item = path.pop(), | |
836 | localDefn = drillDown($scope.node, ['defn'].concat(path)), | |
837 | fieldDefn = drillDown($scope.getDefn(), path); | |
838 | // Set the value if different than the field defn, otherwise unset it | |
e1aca853 | 839 | if (typeof val !== 'undefined' && (val !== fieldDefn[item] && !(!val && !fieldDefn[item]))) { |
4bd44053 CW |
840 | localDefn[item] = val; |
841 | } else { | |
842 | delete localDefn[item]; | |
e1aca853 | 843 | clearOut($scope.node, ['defn'].concat(path)); |
4bd44053 CW |
844 | } |
845 | return val; | |
846 | } | |
847 | return $scope.getProp(propName); | |
848 | } | |
b4def6e9 CW |
849 | this.getSet = getSet; |
850 | ||
851 | this.setEditingOptions = function(val) { | |
852 | $scope.editingOptions = val; | |
853 | }; | |
854 | ||
855 | // Returns a reference to a path n-levels deep within an object | |
856 | function drillDown(parent, path) { | |
857 | var container = parent; | |
858 | _.each(path, function(level) { | |
859 | container[level] = container[level] || {}; | |
860 | container = container[level]; | |
861 | }); | |
862 | return container; | |
863 | } | |
e1aca853 CW |
864 | |
865 | // Recursively clears out empty arrays and objects | |
866 | function clearOut(parent, path) { | |
867 | var item; | |
868 | while (path.length && _.every(drillDown(parent, path), _.isEmpty)) { | |
869 | item = path.pop(); | |
870 | delete drillDown(parent, path)[item]; | |
871 | } | |
872 | } | |
b4def6e9 CW |
873 | } |
874 | }; | |
875 | }); | |
4bd44053 | 876 | |
b4def6e9 CW |
877 | angular.module('afGuiEditor').directive('afGuiEditOptions', function() { |
878 | return { | |
879 | restrict: 'A', | |
880 | templateUrl: '~/afGuiEditor/editOptions.html', | |
881 | scope: true, | |
882 | require: '^^afGuiField', | |
883 | link: function ($scope, element, attrs, afGuiField) { | |
884 | $scope.field = afGuiField; | |
885 | $scope.options = JSON.parse(angular.toJson(afGuiField.getOptions())); | |
886 | var optionKeys = _.map($scope.options, 'key'); | |
887 | $scope.deletedOptions = _.filter(JSON.parse(angular.toJson(afGuiField.getDefn().options || [])), function(item) { | |
888 | return !_.contains(optionKeys, item.key); | |
889 | }); | |
890 | $scope.originalLabels = _.transform(afGuiField.getDefn().options || [], function(originalLabels, item) { | |
891 | originalLabels[item.key] = item.label; | |
892 | }, {}); | |
893 | }, | |
894 | controller: function ($scope) { | |
895 | var ts = $scope.ts = CRM.ts(); | |
896 | ||
897 | $scope.deleteOption = function(option, $index) { | |
898 | $scope.options.splice($index, 1); | |
899 | $scope.deletedOptions.push(option); | |
900 | }; | |
901 | ||
902 | $scope.restoreOption = function(option, $index) { | |
903 | $scope.deletedOptions.splice($index, 1); | |
904 | $scope.options.push(option); | |
905 | }; | |
6fb9e8d2 | 906 | |
b4def6e9 CW |
907 | $scope.save = function() { |
908 | $scope.field.getSet('options', JSON.parse(angular.toJson($scope.options))); | |
909 | $scope.close(); | |
910 | }; | |
911 | ||
912 | $scope.close = function() { | |
913 | $scope.field.setEditingOptions(false); | |
192695ae | 914 | $('#afGuiEditor').removeClass('af-gui-editing-content'); |
b4def6e9 | 915 | }; |
66af6937 CW |
916 | } |
917 | }; | |
918 | }); | |
919 | ||
f3cd3852 | 920 | angular.module('afGuiEditor').directive('afGuiText', function() { |
66af6937 CW |
921 | return { |
922 | restrict: 'A', | |
f3cd3852 | 923 | templateUrl: '~/afGuiEditor/text.html', |
66af6937 | 924 | scope: { |
f3cd3852 | 925 | node: '=afGuiText' |
66af6937 | 926 | }, |
6fb9e8d2 CW |
927 | require: '^^afGuiContainer', |
928 | link: function($scope, element, attrs, container) { | |
929 | $scope.container = container; | |
f3cd3852 CW |
930 | }, |
931 | controller: function($scope) { | |
edac0d6e CW |
932 | var ts = $scope.ts = CRM.ts(); |
933 | ||
f3cd3852 CW |
934 | $scope.tags = { |
935 | p: ts('Normal Text'), | |
fd6c0814 | 936 | legend: ts('Fieldset Legend'), |
f3cd3852 CW |
937 | h1: ts('Heading 1'), |
938 | h2: ts('Heading 2'), | |
939 | h3: ts('Heading 3'), | |
940 | h4: ts('Heading 4'), | |
941 | h5: ts('Heading 5'), | |
942 | h6: ts('Heading 6') | |
943 | }; | |
944 | ||
945 | $scope.alignments = { | |
946 | 'text-left': ts('Align left'), | |
947 | 'text-center': ts('Align center'), | |
948 | 'text-right': ts('Align right'), | |
949 | 'text-justify': ts('Justify') | |
950 | }; | |
951 | ||
952 | $scope.getAlign = function() { | |
5eaf91d9 | 953 | return _.intersection(splitClass($scope.node['class']), _.keys($scope.alignments))[0] || 'text-left'; |
f3cd3852 CW |
954 | }; |
955 | ||
956 | $scope.setAlign = function(val) { | |
192695ae | 957 | modifyClasses($scope.node, _.keys($scope.alignments), val === 'text-left' ? null : val); |
f3cd3852 | 958 | }; |
51ec7d5a CW |
959 | |
960 | $scope.styles = _.transform(CRM.afformAdminData.styles, function(styles, val, key) { | |
961 | styles['text-' + key] = val; | |
962 | }); | |
963 | ||
964 | // Getter/setter for ng-model | |
965 | $scope.getSetStyle = function(val) { | |
966 | if (arguments.length) { | |
192695ae | 967 | return modifyClasses($scope.node, _.keys($scope.styles), val === 'text-default' ? null : val); |
51ec7d5a CW |
968 | } |
969 | return _.intersection(splitClass($scope.node['class']), _.keys($scope.styles))[0] || 'text-default'; | |
970 | }; | |
971 | ||
66af6937 CW |
972 | } |
973 | }; | |
974 | }); | |
192695ae CW |
975 | |
976 | var richtextId = 0; | |
977 | angular.module('afGuiEditor').directive('afGuiMarkup', function($sce, $timeout) { | |
978 | return { | |
979 | restrict: 'A', | |
980 | templateUrl: '~/afGuiEditor/markup.html', | |
981 | scope: { | |
982 | node: '=afGuiMarkup' | |
983 | }, | |
6fb9e8d2 CW |
984 | require: '^^afGuiContainer', |
985 | link: function($scope, element, attrs, container) { | |
986 | $scope.container = container; | |
192695ae CW |
987 | // CRM.wysiwyg doesn't work without a dom id |
988 | $scope.id = 'af-markup-editor-' + richtextId++; | |
989 | ||
6fb9e8d2 | 990 | // When creating a new markup container, go straight to edit mode |
192695ae CW |
991 | $timeout(function() { |
992 | if ($scope.node['#markup'] === false) { | |
993 | $scope.edit(); | |
994 | } | |
995 | }); | |
996 | }, | |
997 | controller: function($scope) { | |
998 | var ts = $scope.ts = CRM.ts(); | |
999 | ||
1000 | $scope.getMarkup = function() { | |
1001 | return $sce.trustAsHtml($scope.node['#markup'] || ''); | |
1002 | }; | |
1003 | ||
1004 | $scope.edit = function() { | |
1005 | $('#afGuiEditor').addClass('af-gui-editing-content'); | |
1006 | $scope.editingMarkup = true; | |
1007 | CRM.wysiwyg.create('#' + $scope.id); | |
1008 | CRM.wysiwyg.setVal('#' + $scope.id, $scope.node['#markup'] || '<p></p>'); | |
1009 | }; | |
1010 | ||
1011 | $scope.save = function() { | |
1012 | $scope.node['#markup'] = CRM.wysiwyg.getVal('#' + $scope.id); | |
1013 | $scope.close(); | |
1014 | }; | |
1015 | ||
1016 | $scope.close = function() { | |
1017 | CRM.wysiwyg.destroy('#' + $scope.id); | |
1018 | $('#afGuiEditor').removeClass('af-gui-editing-content'); | |
6fb9e8d2 | 1019 | // If a newly-added wysiwyg was canceled, just remove it |
192695ae | 1020 | if ($scope.node['#markup'] === false) { |
6fb9e8d2 | 1021 | $scope.container.removeElement($scope.node); |
192695ae CW |
1022 | } else { |
1023 | $scope.editingMarkup = false; | |
1024 | } | |
1025 | }; | |
1026 | } | |
1027 | }; | |
1028 | }); | |
1029 | ||
1030 | ||
e72f4d81 CW |
1031 | angular.module('afGuiEditor').directive('afGuiButton', function() { |
1032 | return { | |
1033 | restrict: 'A', | |
1034 | templateUrl: '~/afGuiEditor/button.html', | |
1035 | scope: { | |
1036 | node: '=afGuiButton' | |
1037 | }, | |
6fb9e8d2 CW |
1038 | require: '^^afGuiContainer', |
1039 | link: function($scope, element, attrs, container) { | |
1040 | $scope.container = container; | |
e72f4d81 CW |
1041 | }, |
1042 | controller: function($scope) { | |
edac0d6e | 1043 | var ts = $scope.ts = CRM.ts(); |
e72f4d81 CW |
1044 | |
1045 | // TODO: Add action selector to UI | |
1046 | // $scope.actions = { | |
905b0fd5 | 1047 | // "afform.submit()": ts('Submit Form') |
e72f4d81 CW |
1048 | // }; |
1049 | ||
51ec7d5a CW |
1050 | $scope.styles = _.transform(CRM.afformAdminData.styles, function(styles, val, key) { |
1051 | styles['btn-' + key] = val; | |
1052 | }); | |
e72f4d81 | 1053 | |
4bd44053 CW |
1054 | // Getter/setter for ng-model |
1055 | $scope.getSetStyle = function(val) { | |
1056 | if (arguments.length) { | |
192695ae | 1057 | return modifyClasses($scope.node, _.keys($scope.styles), ['btn', val]); |
4bd44053 | 1058 | } |
e72f4d81 CW |
1059 | return _.intersection(splitClass($scope.node['class']), _.keys($scope.styles))[0] || ''; |
1060 | }; | |
1061 | ||
e72f4d81 | 1062 | $scope.pickIcon = function() { |
e1aca853 | 1063 | openIconPicker($scope.node, 'crm-icon'); |
e72f4d81 CW |
1064 | }; |
1065 | ||
1066 | } | |
1067 | }; | |
1068 | }); | |
66af6937 | 1069 | |
b4def6e9 CW |
1070 | // Connect bootstrap dropdown.js with angular |
1071 | // Allows menu content to be conditionally rendered only if open | |
1072 | // This gives a large performance boost for a page with lots of menus | |
1073 | angular.module('afGuiEditor').directive('afGuiMenu', function() { | |
1074 | return { | |
1075 | restrict: 'A', | |
1076 | link: function($scope, element, attrs) { | |
1077 | $scope.menu = {}; | |
1078 | element | |
1079 | .on('show.bs.dropdown', function() { | |
1080 | $scope.$apply(function() { | |
1081 | $scope.menu.open = true; | |
1082 | }); | |
1083 | }) | |
1084 | .on('hidden.bs.dropdown', function() { | |
1085 | $scope.$apply(function() { | |
1086 | $scope.menu.open = false; | |
1087 | }); | |
1088 | }); | |
1089 | } | |
1090 | }; | |
1091 | }); | |
1092 | ||
192695ae CW |
1093 | // Menu item to control the border property of a node |
1094 | angular.module('afGuiEditor').directive('afGuiMenuItemBorder', function() { | |
1095 | return { | |
1096 | restrict: 'A', | |
1097 | templateUrl: '~/afGuiEditor/menu-item-border.html', | |
1098 | scope: { | |
1099 | node: '=afGuiMenuItemBorder' | |
1100 | }, | |
1101 | link: function($scope, element, attrs) { | |
1102 | var ts = $scope.ts = CRM.ts(); | |
1103 | ||
1104 | $scope.getSetBorderWidth = function(width) { | |
1105 | return getSetBorderProp($scope.node, 0, arguments.length ? width : null); | |
1106 | }; | |
1107 | ||
1108 | $scope.getSetBorderStyle = function(style) { | |
1109 | return getSetBorderProp($scope.node, 1, arguments.length ? style : null); | |
1110 | }; | |
1111 | ||
1112 | $scope.getSetBorderColor = function(color) { | |
1113 | return getSetBorderProp($scope.node, 2, arguments.length ? color : null); | |
1114 | }; | |
1115 | ||
1116 | function getSetBorderProp(node, idx, val) { | |
1117 | var border = getBorder(node) || ['1px', '', '#000000']; | |
1118 | if (val === null) { | |
1119 | return border[idx]; | |
1120 | } | |
1121 | border[idx] = val; | |
1122 | setStyle(node, 'border', val ? border.join(' ') : null); | |
1123 | } | |
1124 | ||
1125 | function getBorder(node) { | |
1126 | var border = _.map((getStyles(node).border || '').split(' '), _.trim); | |
1127 | return border.length > 2 ? border : null; | |
1128 | } | |
1129 | } | |
1130 | }; | |
1131 | }); | |
1132 | ||
1133 | // Menu item to control the background property of a node | |
1134 | angular.module('afGuiEditor').directive('afGuiMenuItemBackground', function() { | |
1135 | return { | |
1136 | restrict: 'A', | |
1137 | templateUrl: '~/afGuiEditor/menu-item-background.html', | |
1138 | scope: { | |
1139 | node: '=afGuiMenuItemBackground' | |
1140 | }, | |
1141 | link: function($scope, element, attrs) { | |
1142 | var ts = $scope.ts = CRM.ts(); | |
1143 | ||
1144 | $scope.getSetBackgroundColor = function(color) { | |
1145 | if (!arguments.length) { | |
1146 | return getStyles($scope.node)['background-color'] || '#ffffff'; | |
1147 | } | |
1148 | setStyle($scope.node, 'background-color', color); | |
1149 | }; | |
1150 | } | |
1151 | }; | |
1152 | }); | |
1153 | ||
a2854e2e | 1154 | // Editable titles using ngModel & html5 contenteditable |
a064e90d | 1155 | // Cribbed from ContactLayoutEditor |
a2854e2e CW |
1156 | angular.module('afGuiEditor').directive("afGuiEditable", function() { |
1157 | return { | |
1158 | restrict: "A", | |
1159 | require: "ngModel", | |
52cf76a7 CW |
1160 | scope: { |
1161 | defaultValue: '=' | |
1162 | }, | |
a2854e2e CW |
1163 | link: function(scope, element, attrs, ngModel) { |
1164 | var ts = CRM.ts(); | |
1165 | ||
1166 | function read() { | |
1167 | var htmlVal = element.html(); | |
1168 | if (!htmlVal) { | |
52cf76a7 | 1169 | htmlVal = scope.defaultValue; |
fb5cfee1 | 1170 | element.text(htmlVal); |
a2854e2e CW |
1171 | } |
1172 | ngModel.$setViewValue(htmlVal); | |
1173 | } | |
1174 | ||
1175 | ngModel.$render = function() { | |
fb5cfee1 | 1176 | element.text(ngModel.$viewValue || scope.defaultValue); |
a2854e2e CW |
1177 | }; |
1178 | ||
1179 | // Special handling for enter and escape keys | |
1180 | element.on('keydown', function(e) { | |
1181 | // Enter: prevent line break and save | |
1182 | if (e.which === 13) { | |
1183 | e.preventDefault(); | |
1184 | element.blur(); | |
1185 | } | |
1186 | // Escape: undo | |
1187 | if (e.which === 27) { | |
52cf76a7 | 1188 | element.html(ngModel.$viewValue || scope.defaultValue); |
a2854e2e CW |
1189 | element.blur(); |
1190 | } | |
1191 | }); | |
1192 | ||
1193 | element.on("blur change", function() { | |
1194 | scope.$apply(read); | |
1195 | }); | |
1196 | ||
1197 | element.attr('contenteditable', 'true').addClass('crm-editable-enabled'); | |
1198 | } | |
1199 | }; | |
1200 | }); | |
1201 | ||
a064e90d CW |
1202 | // Cribbed from the Api4 Explorer |
1203 | angular.module('afGuiEditor').directive('afGuiFieldValue', function() { | |
1204 | return { | |
1205 | scope: { | |
1206 | field: '=afGuiFieldValue' | |
1207 | }, | |
1208 | require: 'ngModel', | |
1209 | link: function (scope, element, attrs, ctrl) { | |
1210 | var ts = scope.ts = CRM.ts(), | |
1211 | multi; | |
1212 | ||
1213 | function destroyWidget() { | |
1214 | var $el = $(element); | |
1215 | if ($el.is('.crm-form-date-wrapper .crm-hidden-date')) { | |
1216 | $el.crmDatepicker('destroy'); | |
1217 | } | |
1218 | if ($el.is('.select2-container + input')) { | |
1219 | $el.crmEntityRef('destroy'); | |
1220 | } | |
1221 | $(element).removeData().removeAttr('type').removeAttr('placeholder').show(); | |
1222 | } | |
1223 | ||
1224 | function makeWidget(field) { | |
1225 | var $el = $(element), | |
1226 | inputType = field.input_type, | |
1227 | dataType = field.data_type; | |
1228 | multi = field.serialize || dataType === 'Array'; | |
1229 | if (inputType === 'Date') { | |
1230 | $el.crmDatepicker({time: (field.input_attrs && field.input_attrs.time) || false}); | |
1231 | } | |
1232 | else if (field.fk_entity || field.options || dataType === 'Boolean') { | |
1233 | if (field.fk_entity) { | |
1234 | $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}}); | |
1235 | } else if (field.options) { | |
0cbea2e8 CW |
1236 | var options = _.transform(field.options, function(options, val) { |
1237 | options.push({id: val.key, text: val.label}); | |
a064e90d CW |
1238 | }, []); |
1239 | $el.select2({data: options, multiple: multi}); | |
1240 | } else if (dataType === 'Boolean') { | |
1241 | $el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [ | |
1242 | {id: '1', text: ts('Yes')}, | |
1243 | {id: '0', text: ts('No')} | |
1244 | ]}); | |
1245 | } | |
1246 | } else if (dataType === 'Integer' && !multi) { | |
1247 | $el.attr('type', 'number'); | |
1248 | } | |
1249 | } | |
1250 | ||
1251 | // Copied from ng-list but applied conditionally if field is multi-valued | |
1252 | var parseList = function(viewValue) { | |
1253 | // If the viewValue is invalid (say required but empty) it will be `undefined` | |
1254 | if (_.isUndefined(viewValue)) return; | |
1255 | ||
1256 | if (!multi) { | |
1257 | return viewValue; | |
1258 | } | |
1259 | ||
1260 | var list = []; | |
1261 | ||
1262 | if (viewValue) { | |
1263 | _.each(viewValue.split(','), function(value) { | |
1264 | if (value) list.push(_.trim(value)); | |
1265 | }); | |
1266 | } | |
1267 | ||
1268 | return list; | |
1269 | }; | |
1270 | ||
1271 | // Copied from ng-list | |
1272 | ctrl.$parsers.push(parseList); | |
1273 | ctrl.$formatters.push(function(value) { | |
1274 | return _.isArray(value) ? value.join(', ') : value; | |
1275 | }); | |
1276 | ||
1277 | // Copied from ng-list | |
1278 | ctrl.$isEmpty = function(value) { | |
1279 | return !value || !value.length; | |
1280 | }; | |
1281 | ||
1282 | scope.$watchCollection('field', function(field) { | |
1283 | destroyWidget(); | |
1284 | if (field) { | |
1285 | makeWidget(field); | |
1286 | } | |
1287 | }); | |
1288 | } | |
1289 | }; | |
1290 | }); | |
1291 | ||
192695ae CW |
1292 | function getStyles(node) { |
1293 | return !node || !node.style ? {} : _.transform(node.style.split(';'), function(styles, style) { | |
1294 | var keyVal = _.map(style.split(':'), _.trim); | |
1295 | if (keyVal.length > 1 && keyVal[1].length) { | |
1296 | styles[keyVal[0]] = keyVal[1]; | |
1297 | } | |
1298 | }, {}); | |
1299 | } | |
1300 | ||
1301 | function setStyle(node, name, val) { | |
1302 | var styles = getStyles(node); | |
1303 | styles[name] = val; | |
1304 | if (!val) { | |
1305 | delete styles[name]; | |
1306 | } | |
1307 | if (_.isEmpty(styles)) { | |
1308 | delete node.style; | |
1309 | } else { | |
1310 | node.style = _.transform(styles, function(combined, val, name) { | |
1311 | combined.push(name + ': ' + val); | |
1312 | }, []).join('; '); | |
1313 | } | |
1314 | } | |
1315 | ||
1316 | function modifyClasses(node, toRemove, toAdd) { | |
1317 | var classes = splitClass(node['class']); | |
1318 | if (toRemove) { | |
1319 | classes = _.difference(classes, splitClass(toRemove)); | |
1320 | } | |
1321 | if (toAdd) { | |
1322 | classes = _.unique(classes.concat(splitClass(toAdd))); | |
1323 | } | |
1324 | node['class'] = classes.join(' '); | |
1325 | } | |
1326 | ||
e1aca853 CW |
1327 | var editingIcon, editingIconProp; |
1328 | function openIconPicker(node, propName) { | |
1329 | editingIcon = node; | |
1330 | editingIconProp = propName; | |
1331 | $('#af-gui-icon-picker ~ .crm-icon-picker-button').click(); | |
1332 | } | |
1333 | ||
f6c0358e | 1334 | })(angular, CRM.$, CRM._); |