AfformGui - Improve dragging & dropping with more space & clearer borders
[civicrm-core.git] / ext / afform / admin / ang / afGuiEditor / elements / afGuiContainer.component.js
1 // https://civicrm.org/licensing
2 (function(angular, $, _) {
3 "use strict";
4
5 angular.module('afGuiEditor').component('afGuiContainer', {
6 templateUrl: '~/afGuiEditor/elements/afGuiContainer.html',
7 bindings: {
8 node: '<',
9 join: '<',
10 entityName: '<',
11 deleteThis: '&'
12 },
13 require: {editor: '^^afGuiEditor'},
14 controller: function($scope, crmApi4, dialogService, afGui) {
15 var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'),
16 ctrl = this;
17
18 this.$onInit = function() {
19 if (ctrl.node['#tag'] && ((ctrl.node['#tag'] in afGui.meta.blocks) || ctrl.join)) {
20 var blockNode = getBlockNode(),
21 blockTag = blockNode ? blockNode['#tag'] : null;
22 if (blockTag && (blockTag in afGui.meta.blocks) && !afGui.meta.blocks[blockTag].layout) {
23 ctrl.loading = true;
24 crmApi4('Afform', 'loadAdminData', {
25 definition: {name: afGui.meta.blocks[blockTag].name},
26 skipEntities: _.transform(afGui.meta.entities, function(result, entity, entityName) {
27 if (entity.fields) {
28 result.push(entityName);
29 }
30 }, [])
31 }, 0).then(function(data) {
32 afGui.addMeta(data);
33 initializeBlockContainer();
34 ctrl.loading = false;
35 });
36 }
37 initializeBlockContainer();
38 }
39 };
40
41 this.sortableOptions = {
42 handle: '.af-gui-bar',
43 connectWith: '[ui-sortable]',
44 cancel: 'input,textarea,button,select,option,a,.dropdown-menu',
45 placeholder: 'af-gui-dropzone',
46 containment: '#afGuiEditor-canvas-body'
47 };
48
49 $scope.isSelectedFieldset = function(entityName) {
50 return entityName === ctrl.editor.getSelectedEntityName();
51 };
52
53 $scope.selectEntity = function() {
54 if (ctrl.node['af-fieldset']) {
55 ctrl.editor.selectEntity(ctrl.node['af-fieldset']);
56 }
57 };
58
59 $scope.tags = {
60 div: ts('Container'),
61 fieldset: ts('Fieldset')
62 };
63
64 // Block settings
65 var block = {};
66 $scope.block = null;
67
68 $scope.getSetChildren = function(val) {
69 var collection = block.layout || (ctrl.node && ctrl.node['#children']);
70 return arguments.length ? (collection = val) : collection;
71 };
72
73 $scope.isRepeatable = function() {
74 return ctrl.node['af-fieldset'] || (block.directive && afGui.meta.blocks[block.directive].repeat) || ctrl.join;
75 };
76
77 $scope.toggleRepeat = function() {
78 if ('af-repeat' in ctrl.node) {
79 delete ctrl.node.max;
80 delete ctrl.node.min;
81 delete ctrl.node['af-repeat'];
82 delete ctrl.node['add-icon'];
83 } else {
84 ctrl.node.min = '1';
85 ctrl.node['af-repeat'] = ts('Add');
86 }
87 };
88
89 $scope.getSetMin = function(val) {
90 if (arguments.length) {
91 if (ctrl.node.max && val > parseInt(ctrl.node.max, 10)) {
92 ctrl.node.max = '' + val;
93 }
94 if (!val) {
95 delete ctrl.node.min;
96 }
97 else {
98 ctrl.node.min = '' + val;
99 }
100 }
101 return ctrl.node.min ? parseInt(ctrl.node.min, 10) : null;
102 };
103
104 $scope.getSetMax = function(val) {
105 if (arguments.length) {
106 if (ctrl.node.min && val && val < parseInt(ctrl.node.min, 10)) {
107 ctrl.node.min = '' + val;
108 }
109 if (typeof val !== 'number') {
110 delete ctrl.node.max;
111 }
112 else {
113 ctrl.node.max = '' + val;
114 }
115 }
116 return ctrl.node.max ? parseInt(ctrl.node.max, 10) : null;
117 };
118
119 $scope.pickAddIcon = function() {
120 afGui.pickIcon().then(function(val) {
121 ctrl.node['add-icon'] = val;
122 });
123 };
124
125 function getBlockNode() {
126 return !ctrl.join ? ctrl.node : (ctrl.node['#children'] && ctrl.node['#children'].length === 1 ? ctrl.node['#children'][0] : null);
127 }
128
129 function setBlockDirective(directive) {
130 if (ctrl.join) {
131 ctrl.node['#children'] = [{'#tag': directive}];
132 } else {
133 delete ctrl.node['#children'];
134 delete ctrl.node['class'];
135 ctrl.node['#tag'] = directive;
136 }
137 }
138
139 function overrideBlockContents(layout) {
140 ctrl.node['#children'] = layout || [];
141 if (!ctrl.join) {
142 ctrl.node['#tag'] = 'div';
143 ctrl.node['class'] = 'af-container';
144 }
145 block.layout = block.directive = null;
146 }
147
148 $scope.layouts = {
149 'af-layout-rows': ts('Contents display as rows'),
150 'af-layout-cols': ts('Contents are evenly-spaced columns'),
151 'af-layout-inline': ts('Contents are arranged inline')
152 };
153
154 $scope.getLayout = function() {
155 if (!ctrl.node) {
156 return '';
157 }
158 return _.intersection(afGui.splitClass(ctrl.node['class']), _.keys($scope.layouts))[0] || 'af-layout-rows';
159 };
160
161 $scope.setLayout = function(val) {
162 var classes = ['af-container'];
163 if (val !== 'af-layout-rows') {
164 classes.push(val);
165 }
166 afGui.modifyClasses(ctrl.node, _.keys($scope.layouts), classes);
167 };
168
169 $scope.selectBlockDirective = function() {
170 if (block.directive) {
171 block.layout = _.cloneDeep(afGui.meta.blocks[block.directive].layout);
172 block.original = block.directive;
173 setBlockDirective(block.directive);
174 }
175 else {
176 overrideBlockContents(block.layout);
177 }
178 };
179
180 function initializeBlockContainer() {
181
182 // Cancel the below $watch expressions if already set
183 _.each(block.listeners, function(deregister) {
184 deregister();
185 });
186
187 block = $scope.block = {
188 directive: null,
189 layout: null,
190 original: null,
191 options: [],
192 listeners: []
193 };
194
195 _.each(afGui.meta.blocks, function(blockInfo, directive) {
196 if (directive === ctrl.node['#tag'] || (blockInfo.join && blockInfo.join === ctrl.getFieldEntityType())) {
197 block.options.push({
198 id: directive,
199 text: blockInfo.title
200 });
201 }
202 });
203
204 if (getBlockNode() && getBlockNode()['#tag'] in afGui.meta.blocks) {
205 block.directive = block.original = getBlockNode()['#tag'];
206 block.layout = _.cloneDeep(afGui.meta.blocks[block.directive].layout);
207 }
208
209 block.listeners.push($scope.$watch('block.layout', function (layout, oldVal) {
210 if (block.directive && layout && layout !== oldVal && !angular.equals(layout, afGui.meta.blocks[block.directive].layout)) {
211 overrideBlockContents(block.layout);
212 }
213 }, true));
214 }
215
216 $scope.saveBlock = function() {
217 var options = CRM.utils.adjustDialogDefaults({
218 width: '500px',
219 height: '300px',
220 autoOpen: false,
221 title: ts('Save block')
222 });
223 var model = {
224 title: '',
225 name: null,
226 type: 'block',
227 layout: ctrl.node['#children']
228 };
229 if (ctrl.join) {
230 model.join = ctrl.join;
231 }
232 if ($scope.block && $scope.block.original) {
233 model.title = afGui.meta.blocks[$scope.block.original].title;
234 model.name = afGui.meta.blocks[$scope.block.original].name;
235 model.block = afGui.meta.blocks[$scope.block.original].block;
236 }
237 else {
238 model.block = ctrl.getFieldEntityType();
239 }
240 dialogService.open('saveBlockDialog', '~/afGuiEditor/saveBlock.html', model, options)
241 .then(function(block) {
242 afGui.meta.blocks[block.directive_name] = block;
243 setBlockDirective(block.directive_name);
244 initializeBlockContainer();
245 });
246 };
247
248 this.node = ctrl.node;
249
250 this.getNodeType = function(node) {
251 if (!node || !node['#tag']) {
252 return null;
253 }
254 if (node['#tag'] === 'af-field') {
255 return 'field';
256 }
257 if ('af-fieldset' in node) {
258 return 'fieldset';
259 }
260 if (node['af-join']) {
261 return 'join';
262 }
263 if (node['#tag'] && node['#tag'] in afGui.meta.blocks) {
264 return 'container';
265 }
266 if (node['#tag'] && (node['#tag'].slice(0, 19) === 'crm-search-display-')) {
267 return 'searchDisplay';
268 }
269 var classes = afGui.splitClass(node['class']),
270 types = ['af-container', 'af-text', 'af-button', 'af-markup'],
271 type = _.intersection(types, classes);
272 return type.length ? type[0].replace('af-', '') : null;
273 };
274
275 this.removeElement = function(element) {
276 afGui.removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey});
277 };
278
279 this.getEntityName = function() {
280 return ctrl.entityName ? ctrl.entityName.split('-join-')[0] : null;
281 };
282
283 // Returns the primary entity type for this container e.g. "Contact"
284 this.getMainEntityType = function() {
285 return ctrl.editor && ctrl.editor.getEntity(ctrl.getEntityName()).type;
286 };
287
288 // Returns the entity type for fields within this conainer (join entity type if this is a join, else the primary entity type)
289 this.getFieldEntityType = function(fieldName) {
290 // If entityName is declared for this fieldset, return entity-type or join-type
291 if (ctrl.entityName) {
292 var joinType = ctrl.entityName.split('-join-');
293 return joinType[1] || (ctrl.editor && ctrl.editor.getEntity(joinType[0]).type);
294 }
295 // If entityName is not declared, this field belongs to a search
296 var entityType,
297 prefix = _.includes(fieldName, '.') ? fieldName.split('.')[0] : null;
298 _.each(afGui.meta.searchDisplays, function(searchDisplay) {
299 if (prefix) {
300 _.each(searchDisplay['saved_search.api_params'].join, function(join) {
301 var joinInfo = join[0].split(' AS ');
302 if (prefix === joinInfo[1]) {
303 entityType = joinInfo[0];
304 return false;
305 }
306 });
307 }
308 if (!entityType && fieldName && afGui.getField(searchDisplay['saved_search.api_entity'], fieldName)) {
309 entityType = searchDisplay['saved_search.api_entity'];
310 }
311 if (entityType) {
312 return false;
313 }
314 });
315 return entityType || _.map(afGui.meta.searchDisplays, 'saved_search.api_entity')[0];
316 };
317
318 }
319 });
320
321 })(angular, CRM.$, CRM._);