1 (function(angular
, $, _
) {
4 angular
.module('afGuiEditor', CRM
.angRequires('afGuiEditor'))
6 .service('afGui', function(crmApi4
, $parse
, $q
) {
8 // Parse strings of javascript that php couldn't interpret
9 // TODO: Figure out which attributes actually need to be evaluated, as a whitelist would be less error-prone than a blacklist
10 var doNotEval
= ['filters'];
11 function evaluate(collection
) {
12 _
.each(collection
, function(item
) {
13 if (_
.isPlainObject(item
)) {
14 evaluate(item
['#children']);
15 _
.each(item
, function(prop
, key
) {
16 if (_
.isString(prop
) && !_
.includes(doNotEval
, key
)) {
17 var str
= _
.trim(prop
);
18 if (str
[0] === '{' || str
[0] === '[' || str
.slice(0, 3) === 'ts(') {
19 item
[key
] = $parse(str
)({ts
: CRM
.ts('afform')});
27 function getStyles(node
) {
28 return !node
|| !node
.style
? {} : _
.transform(node
.style
.split(';'), function(styles
, style
) {
29 var keyVal
= _
.map(style
.split(':'), _
.trim
);
30 if (keyVal
.length
> 1 && keyVal
[1].length
) {
31 styles
[keyVal
[0]] = keyVal
[1];
36 function setStyle(node
, name
, val
) {
37 var styles
= getStyles(node
);
42 if (_
.isEmpty(styles
)) {
45 node
.style
= _
.transform(styles
, function(combined
, val
, name
) {
46 combined
.push(name
+ ': ' + val
);
51 // Turns a space-separated list (e.g. css classes) into an array
52 function splitClass(str
) {
56 return str
? _
.unique(_
.trim(str
).split(/\s+/g)) : [];
59 function modifyClasses(node
, toRemove
, toAdd
) {
60 var classes
= splitClass(node
['class']);
62 classes
= _
.difference(classes
, splitClass(toRemove
));
65 classes
= _
.unique(classes
.concat(splitClass(toAdd
)));
67 node
['class'] = classes
.join(' ');
71 // Called when loading a new afform for editing - clears out stale metadata
72 resetMeta: function() {
73 _
.each(CRM
.afGuiEditor
.entities
, function(entity
, type
) {
74 // Skip the "*" pseudo-entity which should always have an empty list of fields
75 if (entity
.fields
&& type
!== '*') {
79 CRM
.afGuiEditor
.blocks
= {};
80 CRM
.afGuiEditor
.searchDisplays
= {};
83 // Takes the results from api.Afform.loadAdminData and processes the metadata
84 // Note this runs once when loading a new afform for editing (just after this.resetMeta is called)
85 // and it also runs when adding new entities or blocks to the form.
86 addMeta: function(data
) {
87 evaluate(data
.definition
.layout
);
88 if (data
.definition
.type
=== 'block' && data
.definition
.name
) {
89 CRM
.afGuiEditor
.blocks
[data
.definition
.directive_name
] = data
.definition
;
91 // Add new or updated blocks
92 _
.each(data
.blocks
, function(block
) {
93 // Avoid overwriting complete block record with an incomplete one
94 if (!CRM
.afGuiEditor
.blocks
[block
.directive_name
] || block
.layout
) {
96 evaluate(block
.layout
);
98 CRM
.afGuiEditor
.blocks
[block
.directive_name
] = block
;
101 _
.each(data
.entities
, function(entity
, entityName
) {
102 if (!CRM
.afGuiEditor
.entities
[entityName
]) {
103 CRM
.afGuiEditor
.entities
[entityName
] = entity
;
106 _
.each(data
.fields
, function(fields
, entityName
) {
107 if (CRM
.afGuiEditor
.entities
[entityName
]) {
108 CRM
.afGuiEditor
.entities
[entityName
].fields
= fields
;
111 // Optimization - since contact fields are a combination of these three,
112 // the server doesn't send contact fields if sending contact-type fields
113 if ('Individual' in data
.fields
|| 'Household' in data
.fields
|| 'Organization' in data
.fields
) {
114 CRM
.afGuiEditor
.entities
.Contact
.fields
= _
.assign({},
115 (CRM
.afGuiEditor
.entities
.Individual
|| {}).fields
,
116 (CRM
.afGuiEditor
.entities
.Household
|| {}).fields
,
117 (CRM
.afGuiEditor
.entities
.Organization
|| {}).fields
120 _
.each(data
.search_displays
, function(display
) {
121 CRM
.afGuiEditor
.searchDisplays
[display
['saved_search.name'] + '.' + display
.name
] = display
;
125 meta
: CRM
.afGuiEditor
,
127 getEntity: function(entityName
) {
128 return CRM
.afGuiEditor
.entities
[entityName
];
131 getField: function(entityName
, fieldName
) {
132 var fields
= CRM
.afGuiEditor
.entities
[entityName
].fields
;
133 return fields
[fieldName
] || fields
[fieldName
.substr(fieldName
.indexOf('.') + 1)];
136 // Recursively searches a collection and its children using _.filter
137 // Returns an array of all matches, or an object if the indexBy param is used
138 findRecursive
: function findRecursive(collection
, predicate
, indexBy
) {
139 var items
= _
.filter(collection
, predicate
);
140 _
.each(collection
, function(item
) {
141 if (_
.isPlainObject(item
) && item
['#children']) {
142 var childMatches
= findRecursive(item
['#children'], predicate
);
143 if (childMatches
.length
) {
144 Array
.prototype.push
.apply(items
, childMatches
);
148 return indexBy
? _
.indexBy(items
, indexBy
) : items
;
151 // Applies _.remove() to an item and its children
152 removeRecursive
: function removeRecursive(collection
, removeParams
) {
153 _
.remove(collection
, removeParams
);
154 _
.each(collection
, function(item
) {
155 if (_
.isPlainObject(item
) && item
['#children']) {
156 removeRecursive(item
['#children'], removeParams
);
161 splitClass
: splitClass
,
162 modifyClasses
: modifyClasses
,
163 getStyles
: getStyles
,
166 pickIcon: function() {
167 var deferred
= $q
.defer();
168 $('#af-gui-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
169 $('#af-gui-icon-picker').on('change', function() {
170 deferred
.resolve($(this).val());
172 return deferred
.promise
;
178 // Shoehorn in a non-angular widget for picking icons
179 $('#crm-container').append('<div style="display:none"><input id="af-gui-icon-picker"></div>');
180 CRM
.loadScript(CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmIconPicker.js').done(function() {
181 $('#af-gui-icon-picker').crmIconPicker();
183 // Add css class while dragging
185 .on('sortover', function(e
) {
186 $('.af-gui-container').removeClass('af-gui-dragtarget');
187 $(e
.target
).closest('.af-gui-container').addClass('af-gui-dragtarget');
189 .on('sortout', '.af-gui-container', function() {
190 $(this).removeClass('af-gui-dragtarget');
192 .on('sortstart', '#afGuiEditor', function() {
193 $('body').addClass('af-gui-dragging');
195 .on('sortstop', function() {
196 $('body').removeClass('af-gui-dragging');
197 $('.af-gui-dragtarget').removeClass('af-gui-dragtarget');
201 // Connect bootstrap dropdown.js with angular
202 // Allows menu content to be conditionally rendered only if open
203 // This gives a large performance boost for a page with lots of menus
204 angular
.module('afGuiEditor').directive('afGuiMenu', function() {
207 link: function($scope
, element
, attrs
) {
210 .on('show.bs.dropdown', function() {
211 $scope
.$apply(function() {
212 $scope
.menu
.open
= true;
215 .on('hidden.bs.dropdown', function() {
216 $scope
.$apply(function() {
217 $scope
.menu
.open
= false;
224 })(angular
, CRM
.$, CRM
._
);