Merge pull request #18536 from eileenmcnaughton/main
[civicrm-core.git] / ext / afform / gui / ang / afGuiEditor.js
CommitLineData
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._);