CRM-15578 - crmUiIframe - Add directive for previewing content inside an iframe
[civicrm-core.git] / js / angular-crm-ui.js
1 /// crmUi: Sundry UI helpers
2 (function (angular, $, _) {
3 var idCount = 0;
4
5 var partialUrl = function (relPath) {
6 return CRM.resourceUrls['civicrm'] + '/partials/crmUi/' + relPath;
7 };
8
9
10 angular.module('crmUi', [])
11
12 // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
13 // WISHLIST: crmCollapsed should support two-way/continous binding
14 .directive('crmUiAccordion', function() {
15 return {
16 scope: {
17 crmTitle: '@',
18 crmCollapsed: '@'
19 },
20 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>',
21 transclude: true,
22 link: function (scope, element, attrs) {
23 scope.cssClasses = {
24 collapsed: scope.$parent.$eval(attrs.crmCollapsed)
25 };
26 }
27 };
28 })
29
30 // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
31 .directive('crmUiIframe', function ($parse) {
32 return {
33 scope: {
34 crmUiIframe: '@' // expression which evalutes to HTML content
35 },
36 link: function (scope, elm, attrs) {
37 var iframe = $(elm)[0];
38 iframe.setAttribute('width', '100%');
39 iframe.setAttribute('frameborder', '0');
40
41 var refresh = function () {
42 // var iframeHtml = '<html><head><base target="_blank"></head><body onload="parent.document.getElementById(\'' + iframe.id + '\').style.height=document.body.scrollHeight + \'px\'"><scr' + 'ipt type="text/javascript" src="https://gist.github.com/' + iframeId + '.js"></sc' + 'ript></body></html>';
43 var iframeHtml = scope.$parent.$eval(attrs.crmUiIframe);
44
45 var doc = iframe.document;
46 if (iframe.contentDocument) {
47 doc = iframe.contentDocument;
48 }
49 else if (iframe.contentWindow) {
50 doc = iframe.contentWindow.document;
51 }
52
53 doc.open();
54 doc.writeln(iframeHtml);
55 doc.close();
56 }
57
58 scope.$parent.$watch(attrs.crmUiIframe, refresh);
59 //setTimeout(function () { refresh(); }, 50);
60 }
61 };
62 })
63
64 // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
65 //
66 // Label adapts based on <input required>, <input ng-required>, or any other validation.
67 //
68 // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
69 // but if the scopes are materially different then problems could arise.
70 .directive('crmUiLabel', function($parse) {
71 return {
72 scope: {
73 name: '@'
74 },
75 transclude: true,
76 template: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
77 link: function(scope, element, attrs) {
78 if (attrs.crmFor == 'name') {
79 throw new Error('Validation monitoring does not work for field name "name"');
80 }
81
82 // 1. Figure out form and input elements
83
84 var form = $(element).closest('form');
85 var formCtrl = scope.$parent.$eval(form.attr('name'));
86 var input = $('input[name="' + attrs.crmFor + '"],select[name="' + attrs.crmFor + '"],textarea[name="' + attrs.crmFor + '"]', form);
87 if (form.length != 1 || input.length != 1) {
88 if (console.log) console.log('Label cannot be matched to input element. Expected to find one form and one input[name='+attrs.crmFor+'].', form.length, input.length);
89 return;
90 }
91
92 // 2. Make sure that inputs are well-defined (with name+id).
93
94 if (!input.attr('id')) {
95 input.attr('id', 'crmUi_' + (++idCount));
96 }
97 $(element).attr('for', input.attr('id'));
98
99 // 3. Monitor is the "required" and "$valid" properties
100
101 if (input.attr('ng-required')) {
102 scope.crmRequired = scope.$parent.$eval(input.attr('ng-required'));
103 scope.$parent.$watch(input.attr('ng-required'), function(isRequired) {
104 scope.crmRequired = isRequired;
105 });
106 } else {
107 scope.crmRequired = input.prop('required');
108 }
109
110 var inputCtrl = form.attr('name') + '.' + input.attr('name');
111 scope.cssClasses = {};
112 scope.$parent.$watch(inputCtrl + '.$valid', function(newValue) {
113 //scope.cssClasses['ng-valid'] = newValue;
114 //scope.cssClasses['ng-invalid'] = !newValue;
115 scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
116 });
117 scope.$parent.$watch(inputCtrl + '.$pristine', function(newValue) {
118 //scope.cssClasses['ng-pristine'] = newValue;
119 //scope.cssClasses['ng-dirty'] = !newValue;
120 scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
121 });
122
123 }
124 };
125 })
126
127 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
128 // example: <a crm-ui-lock
129 // binding="mymodel.boolfield"
130 // title-locked="ts('Boolfield is locked')"
131 // title-unlocked="ts('Boolfield is unlocked')"></a>
132 .directive('crmUiLock', function ($parse, $rootScope) {
133 var defaultVal = function (defaultValue) {
134 var f = function (scope) {
135 return defaultValue;
136 }
137 f.assign = function (scope, value) {
138 // ignore changes
139 }
140 return f;
141 };
142
143 // like $parse, but accepts a defaultValue in case expr is undefined
144 var parse = function (expr, defaultValue) {
145 return expr ? $parse(expr) : defaultVal(defaultValue);
146 };
147
148 return {
149 template: '',
150 link: function (scope, element, attrs) {
151 var binding = parse(attrs['binding'], true);
152 var titleLocked = parse(attrs['titleLocked'], ts('Locked'));
153 var titleUnlocked = parse(attrs['titleUnlocked'], ts('Unlocked'));
154
155 $(element).addClass('ui-icon lock-button');
156 var refresh = function () {
157 var locked = binding(scope);
158 if (locked) {
159 $(element)
160 .removeClass('ui-icon-unlocked')
161 .addClass('ui-icon-locked')
162 .prop('title', titleLocked(scope))
163 ;
164 }
165 else {
166 $(element)
167 .removeClass('ui-icon-locked')
168 .addClass('ui-icon-unlocked')
169 .prop('title', titleUnlocked(scope))
170 ;
171 }
172 };
173
174 $(element).click(function () {
175 binding.assign(scope, !binding(scope));
176 //scope.$digest();
177 $rootScope.$digest();
178 });
179
180 scope.$watch(attrs.binding, refresh);
181 scope.$watch(attrs.titleLocked, refresh);
182 scope.$watch(attrs.titleUnlocked, refresh);
183
184 refresh();
185 }
186 };
187 })
188
189 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
190 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
191 .directive('crmUiTab', function($parse) {
192 return {
193 require: '^crmUiTabSet',
194 restrict: 'EA',
195 scope: {
196 crmTitle: '@',
197 id: '@'
198 },
199 template: '<div ng-transclude></div>',
200 transclude: true,
201 link: function (scope, element, attrs, crmUiTabSetCtrl) {
202 crmUiTabSetCtrl.add(scope);
203 }
204 };
205 })
206
207 // 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>
208 .directive('crmUiTabSet', function() {
209 return {
210 restrict: 'EA',
211 scope: {
212 crmUiTabSet: '@'
213 },
214 templateUrl: partialUrl('tabset.html'),
215 transclude: true,
216 controllerAs: 'crmUiTabSetCtrl',
217 controller: function($scope, $parse) {
218 var tabs = $scope.tabs = []; // array<$scope>
219 this.add = function(tab) {
220 if (!tab.id) throw "Tab is missing 'id'";
221 tabs.push(tab);
222 };
223 },
224 link: function (scope, element, attrs) {}
225 };
226 })
227
228 // example: <div crm-ui-wizard="myWizardCtrl"><div crm-ui-wizard-step crm-title="ts('Step 1')">...</div><div crm-ui-wizard-step crm-title="ts('Step 2')">...</div></div>
229 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
230 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
231 // WISHLIST: Allow each step to enable/disable (show/hide) itself
232 .directive('crmUiWizard', function() {
233 return {
234 restrict: 'EA',
235 scope: {
236 crmUiWizard: '@'
237 },
238 templateUrl: partialUrl('wizard.html'),
239 transclude: true,
240 controllerAs: 'crmUiWizardCtrl',
241 controller: function($scope, $parse) {
242 var steps = $scope.steps = []; // array<$scope>
243 var crmUiWizardCtrl = this;
244 var maxVisited = 0;
245 var selectedIndex = null;
246
247 var findIndex = function() {
248 var found = null;
249 angular.forEach(steps, function(step, stepKey) {
250 if (step.selected) found = stepKey;
251 });
252 return found;
253 };
254
255 /// @return int the index of the current step
256 this.$index = function() { return selectedIndex; };
257 /// @return bool whether the currentstep is first
258 this.$first = function() { return this.$index() === 0; };
259 /// @return bool whether the current step is last
260 this.$last = function() { return this.$index() === steps.length -1; };
261 this.$maxVisit = function() { return maxVisited; }
262 this.iconFor = function(index) {
263 if (index < this.$index()) return '√';
264 if (index === this.$index()) return '»';
265 return ' ';
266 }
267 this.isSelectable = function(step) {
268 if (step.selected) return false;
269 var result = false;
270 angular.forEach(steps, function(otherStep, otherKey) {
271 if (step === otherStep && otherKey <= maxVisited) result = true;
272 });
273 return result;
274 };
275
276 /*** @param Object step the $scope of the step */
277 this.select = function(step) {
278 angular.forEach(steps, function(otherStep, otherKey) {
279 otherStep.selected = (otherStep === step);
280 if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey;
281 });
282 selectedIndex = findIndex();
283 };
284 /*** @param Object step the $scope of the step */
285 this.add = function(step) {
286 if (steps.length === 0) {
287 step.selected = true;
288 selectedIndex = 0;
289 }
290 steps.push(step);
291 };
292 this.goto = function(index) {
293 if (index < 0) index = 0;
294 if (index >= steps.length) index = steps.length-1;
295 this.select(steps[index]);
296 };
297 this.previous = function() { this.goto(this.$index()-1); };
298 this.next = function() { this.goto(this.$index()+1); };
299 if ($scope.crmUiWizard) {
300 $parse($scope.crmUiWizard).assign($scope.$parent, this)
301 }
302 },
303 link: function (scope, element, attrs) {}
304 };
305 })
306
307 // Use this to add extra markup to wizard
308 .directive('crmUiWizardButtons', function() {
309 return {
310 require: '^crmUiWizard',
311 restrict: 'EA',
312 scope: {},
313 template: '<span ng-transclude></span>',
314 transclude: true,
315 link: function (scope, element, attrs, crmUiWizardCtrl) {
316 var realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons');
317 $(element).appendTo(realButtonsEl);
318 }
319 };
320 })
321
322 // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
323 .directive('crmUiWizardStep', function() {
324 return {
325 require: '^crmUiWizard',
326 restrict: 'EA',
327 scope: {
328 crmTitle: '@'
329 },
330 template: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
331 transclude: true,
332 link: function (scope, element, attrs, crmUiWizardCtrl) {
333 crmUiWizardCtrl.add(scope);
334 }
335 };
336 })
337
338 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
339 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
340 .directive('crmConfirm', function () {
341 // Helpers to calculate default options for CRM.confirm()
342 var defaultFuncs = {
343 'disable': function (options) {
344 return {
345 message: ts('Are you sure you want to disable this?'),
346 options: {no: ts('Cancel'), yes: ts('Disable')},
347 width: 300,
348 title: ts('Disable %1?', {
349 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
350 })
351 };
352 },
353 'revert': function (options) {
354 return {
355 message: ts('Are you sure you want to revert this?'),
356 options: {no: ts('Cancel'), yes: ts('Revert')},
357 width: 300,
358 title: ts('Revert %1?', {
359 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
360 })
361 };
362 },
363 'delete': function (options) {
364 return {
365 message: ts('Are you sure you want to delete this?'),
366 options: {no: ts('Cancel'), yes: ts('Delete')},
367 width: 300,
368 title: ts('Delete %1?', {
369 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
370 })
371 };
372 }
373 };
374 return {
375 template: '',
376 link: function (scope, element, attrs) {
377 $(element).click(function () {
378 var options = scope.$eval(attrs['crmConfirm']);
379 var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
380 CRM.confirm(_.extend(defaults, options))
381 .on('crmConfirm:yes', function () { scope.$apply(attrs['onYes']); })
382 .on('crmConfirm:no', function () { scope.$apply(attrs['onNo']); });
383 });
384 }
385 };
386 })
387 .run(function($rootScope, $location) {
388 /// Example: <button ng-click="goto('home')">Go home!</button>
389 $rootScope.goto = function(path) {
390 $location.path(path);
391 };
392 // useful for debugging: $rootScope.log = console.log || function() {};
393 })
394 ;
395
396 })(angular, CRM.$, CRM._);