CRM-15578 - crmUiAccordion - Basic implementation
[civicrm-core.git] / js / angular-crm-ui.js
1 /// crmUi: Sundry UI helpers
2 (function (angular, $, _) {
3 var idCount = 0;
4
5 angular.module('crmUi', [])
6
7 // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
8 // WISHLIST: crmCollapsed should support two-way/continous binding
9 .directive('crmUiAccordion', function() {
10 return {
11 scope: {
12 crmTitle: '@',
13 crmCollapsed: '@'
14 },
15 template: '<div class="crm-accordion-wrapper" ng-class="cssClasses"><div class="crm-accordion-header">{{$parent.$eval(crmTitle)}}</div><div class="crm-accordion-body" ng-transclude></div></div>',
16 transclude: true,
17 link: function (scope, element, attrs) {
18 scope.cssClasses = {
19 collapsed: scope.$parent.$eval(attrs.crmCollapsed)
20 };
21 }
22 };
23 })
24
25 // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
26 //
27 // Label adapts based on <input required>, <input ng-required>, or any other validation.
28 //
29 // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
30 // but if the scopes are materially different then problems could arise.
31 .directive('crmUiLabel', function($parse) {
32 return {
33 scope: {
34 name: '@'
35 },
36 transclude: true,
37 template: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
38 link: function(scope, element, attrs) {
39 if (attrs.crmFor == 'name') {
40 throw new Error('Validation monitoring does not work for field name "name"');
41 }
42
43 // 1. Figure out form and input elements
44
45 var form = $(element).closest('form');
46 var formCtrl = scope.$parent.$eval(form.attr('name'));
47 var input = $('input[name="' + attrs.crmFor + '"],select[name="' + attrs.crmFor + '"],textarea[name="' + attrs.crmFor + '"]', form);
48 if (form.length != 1 || input.length != 1) {
49 if (console.log) console.log('Label cannot be matched to input element. Expected to find one form and one input.', form.length, input.length);
50 return;
51 }
52
53 // 2. Make sure that inputs are well-defined (with name+id).
54
55 if (!input.attr('id')) {
56 input.attr('id', 'crmUi_' + (++idCount));
57 }
58 $(element).attr('for', input.attr('id'));
59
60 // 3. Monitor is the "required" and "$valid" properties
61
62 if (input.attr('ng-required')) {
63 scope.crmRequired = scope.$parent.$eval(input.attr('ng-required'));
64 scope.$parent.$watch(input.attr('ng-required'), function(isRequired) {
65 scope.crmRequired = isRequired;
66 });
67 } else {
68 scope.crmRequired = input.prop('required');
69 }
70
71 var inputCtrl = form.attr('name') + '.' + input.attr('name');
72 scope.cssClasses = {};
73 scope.$parent.$watch(inputCtrl + '.$valid', function(newValue) {
74 //scope.cssClasses['ng-valid'] = newValue;
75 //scope.cssClasses['ng-invalid'] = !newValue;
76 scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
77 });
78 scope.$parent.$watch(inputCtrl + '.$pristine', function(newValue) {
79 //scope.cssClasses['ng-pristine'] = newValue;
80 //scope.cssClasses['ng-dirty'] = !newValue;
81 scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
82 });
83
84 }
85 };
86 })
87
88 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
89 // example: <a crm-ui-lock
90 // binding="mymodel.boolfield"
91 // title-locked="ts('Boolfield is locked')"
92 // title-unlocked="ts('Boolfield is unlocked')"></a>
93 .directive('crmUiLock', function ($parse, $rootScope) {
94 var defaultVal = function (defaultValue) {
95 var f = function (scope) {
96 return defaultValue;
97 }
98 f.assign = function (scope, value) {
99 // ignore changes
100 }
101 return f;
102 };
103
104 // like $parse, but accepts a defaultValue in case expr is undefined
105 var parse = function (expr, defaultValue) {
106 return expr ? $parse(expr) : defaultVal(defaultValue);
107 };
108
109 return {
110 template: '',
111 link: function (scope, element, attrs) {
112 var binding = parse(attrs['binding'], true);
113 var titleLocked = parse(attrs['titleLocked'], ts('Locked'));
114 var titleUnlocked = parse(attrs['titleUnlocked'], ts('Unlocked'));
115
116 $(element).addClass('ui-icon lock-button');
117 var refresh = function () {
118 var locked = binding(scope);
119 if (locked) {
120 $(element)
121 .removeClass('ui-icon-unlocked')
122 .addClass('ui-icon-locked')
123 .prop('title', titleLocked(scope))
124 ;
125 }
126 else {
127 $(element)
128 .removeClass('ui-icon-locked')
129 .addClass('ui-icon-unlocked')
130 .prop('title', titleUnlocked(scope))
131 ;
132 }
133 };
134
135 $(element).click(function () {
136 binding.assign(scope, !binding(scope));
137 //scope.$digest();
138 $rootScope.$digest();
139 });
140
141 scope.$watch(attrs.binding, refresh);
142 scope.$watch(attrs.titleLocked, refresh);
143 scope.$watch(attrs.titleUnlocked, refresh);
144
145 refresh();
146 }
147 };
148 })
149
150 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
151 .directive('crmUiTab', function($parse) {
152 return {
153 scope: {
154 crmTitle: '@'
155 },
156 template: '<div><b>(Tab: {{$parent.$eval(crmTitle)}})</b><span ng-transclude/></div>',
157 transclude: true,
158 link: function (scope, element, attrs) {}
159 };
160 })
161
162 // example: <div crm-ui-tab-set><div crm-ui-tab crm-title="Tab 1">...</div><div crm-ui-tab crm-title="Tab 2">...</div></div>
163 .directive('crmUiTabSet', function() {
164 return {
165 template: '<div><span ng-transclude/></div>',
166 transclude: true,
167 link: function (scope, element, attrs) {}
168 };
169 })
170
171 // example: <div crm-ui-wizard><div crm-ui-wizard-step crm-title="Step 1">...</div><div crm-ui-wizard-step crm-title="Step 2">...</div></div>
172 .directive('crmUiWizard', function() {
173 return {
174 template: '<div><span ng-transclude/></div>',
175 transclude: true,
176 link: function (scope, element, attrs) {}
177 };
178 })
179
180 .directive('crmUiWizardFooter', function() {
181 return {
182 template: '<div><span ng-transclude/></div>',
183 transclude: true,
184 link: function (scope, element, attrs) {}
185 };
186 })
187
188 // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
189 .directive('crmUiWizardStep', function() {
190 return {
191 scope: {
192 crmTitle: '@'
193 },
194 template: '<div><b>(Step: {{$parent.$eval(crmTitle)}})</b><span ng-transclude/></div>',
195 transclude: true,
196 link: function (scope, element, attrs) {}
197 };
198 })
199
200 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
201 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
202 .directive('crmConfirm', function () {
203 // Helpers to calculate default options for CRM.confirm()
204 var defaultFuncs = {
205 'disable': function (options) {
206 return {
207 message: ts('Are you sure you want to disable this?'),
208 options: {no: ts('Cancel'), yes: ts('Disable')},
209 width: 300,
210 title: ts('Disable %1?', {
211 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
212 })
213 };
214 },
215 'revert': function (options) {
216 return {
217 message: ts('Are you sure you want to revert this?'),
218 options: {no: ts('Cancel'), yes: ts('Revert')},
219 width: 300,
220 title: ts('Revert %1?', {
221 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
222 })
223 };
224 },
225 'delete': function (options) {
226 return {
227 message: ts('Are you sure you want to delete this?'),
228 options: {no: ts('Cancel'), yes: ts('Delete')},
229 width: 300,
230 title: ts('Delete %1?', {
231 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
232 })
233 };
234 }
235 };
236 return {
237 template: '',
238 link: function (scope, element, attrs) {
239 $(element).click(function () {
240 var options = scope.$eval(attrs['crmConfirm']);
241 var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
242 CRM.confirm(_.extend(defaults, options))
243 .on('crmConfirm:yes', function () { scope.$apply(attrs['onYes']); })
244 .on('crmConfirm:no', function () { scope.$apply(attrs['onNo']); });
245 });
246 }
247 };
248 })
249 .run(function($rootScope, $location) {
250 /// Example: <button ng-click="goto('home')">Go home!</button>
251 $rootScope.goto = function(path) {
252 $location.path(path);
253 };
254 // useful for debugging: $rootScope.log = console.log || function() {};
255 })
256 ;
257
258 })(angular, CRM.$, CRM._);