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