/// crmUi: Sundry UI helpers (function (angular, $, _) { var uidCount = 0, pageTitle = 'CiviCRM', documentTitle = 'CiviCRM'; angular.module('crmUi', CRM.angRequires('crmUi')) // example
...content...
// WISHLIST: crmCollapsed should support two-way/continuous binding .directive('crmUiAccordion', function() { return { scope: { crmUiAccordion: '=' }, template: '
{{crmUiAccordion.title}}
', transclude: true, link: function (scope, element, attrs) { scope.cssClasses = { 'crm-accordion-wrapper': true, collapsed: scope.crmUiAccordion.collapsed }; scope.help = null; scope.$watch('crmUiAccordion', function(crmUiAccordion) { if (crmUiAccordion && crmUiAccordion.help) { scope.help = crmUiAccordion.help.clone({}, { title: crmUiAccordion.title }); } }); } }; }) // Examples: // crmUiAlert({text: 'My text', title: 'My title', type: 'error'}); // crmUiAlert({template: 'Hello', scope: $scope.$new()}); // var h = crmUiAlert({templateUrl: '~/crmFoo/alert.html', scope: $scope.$new()}); // ... h.close(); ... .service('crmUiAlert', function($compile, $rootScope, $templateRequest, $q) { var count = 0; return function crmUiAlert(params) { var id = 'crmUiAlert_' + (++count); var tpl = null; if (params.templateUrl) { tpl = $templateRequest(params.templateUrl); } else if (params.template) { tpl = params.template; } if (tpl) { params.text = '
'; // temporary stub } var result = CRM.alert(params.text, params.title, params.type, params.options); if (tpl) { $q.when(tpl, function(html) { var scope = params.scope || $rootScope.$new(); var linker = $compile(html); $('#' + id).append($(linker(scope))); }); } return result; }; }) // Simple wrapper around $.crmDatepicker. // example with no time input: // example with custom date format: .directive('crmUiDatepicker', function () { return { restrict: 'AE', require: 'ngModel', scope: { crmUiDatepicker: '=' }, link: function (scope, element, attrs, ngModel) { ngModel.$render = function () { element.val(ngModel.$viewValue).change(); }; element .crmDatepicker(scope.crmUiDatepicker) .on('change', function() { var requiredLength = 19; if (scope.crmUiDatepicker && scope.crmUiDatepicker.time === false) { requiredLength = 10; } if (scope.crmUiDatepicker && scope.crmUiDatepicker.date === false) { requiredLength = 8; } ngModel.$setValidity('incompleteDateTime', !($(this).val().length && $(this).val().length !== requiredLength)); }); } }; }) // Display debug information (if available) // For richer DX, checkout Batarang/ng-inspector (Chrome/Safari), or AngScope/ng-inspect (Firefox). // example:
.directive('crmUiDebug', function ($location) { return { restrict: 'AE', scope: { crmUiDebug: '@' }, template: function() { var args = $location.search(); return (args && args.angularDebug) ? '
{{data|json}}
' : ''; }, link: function(scope, element, attrs) { var args = $location.search(); if (args && args.angularDebug) { scope.ts = CRM.ts(null); scope.$parent.$watch(attrs.crmUiDebug, function(data) { scope.data = data; }); } } }; }) // Display a field/row in a field list // example:
{{mydata}}
// example:
// example:
// example:
{{mydata}}
.directive('crmUiField', function() { // Note: When writing new templates, the "label" position is particular. See/patch "var label" below. var templateUrls = { default: '~/crmUi/field.html', checkbox: '~/crmUi/field-cb.html' }; return { require: '^crmUiIdScope', restrict: 'EA', scope: { // {title, name, help, helpFile} crmUiField: '=' }, templateUrl: function(tElement, tAttrs){ var layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default'; return templateUrls[layout]; }, transclude: true, link: function (scope, element, attrs, crmUiIdCtrl) { $(element).addClass('crm-section'); scope.help = null; scope.$watch('crmUiField', function(crmUiField) { if (crmUiField && crmUiField.help) { scope.help = crmUiField.help.clone({}, { title: crmUiField.title }); } }); } }; }) // example:
.directive('crmUiId', function () { return { require: '^crmUiIdScope', restrict: 'EA', link: { pre: function (scope, element, attrs, crmUiIdCtrl) { var id = crmUiIdCtrl.get(attrs.crmUiId); element.attr('id', id); } } }; }) // for example, see crmUiHelp .service('crmUiHelp', function(){ // example: var h = new FieldHelp({id: 'foo'}); h.open(); function FieldHelp(options) { this.options = options; } angular.extend(FieldHelp.prototype, { get: function(n) { return this.options[n]; }, open: function open() { CRM.help(this.options.title, {id: this.options.id, file: this.options.file}); }, clone: function clone(options, defaults) { return new FieldHelp(angular.extend({}, defaults, this.options, options)); } }); // example: var hs = crmUiHelp({file: 'CRM/Foo/Bar'}); return function(defaults){ // example: hs('myfield') // example: hs({id: 'myfield', title: 'Foo Bar', file: 'Whiz/Bang'}) return function(options) { if (_.isString(options)) { options = {id: options}; } return new FieldHelp(angular.extend({}, defaults, options)); }; }; }) // Display a help icon // Example: Use a default *.hlp file // scope.hs = crmUiHelp({file: 'Path/To/Help/File'}); // HTML: // Example: Use an explicit *.hlp file // HTML: .directive('crmUiHelp', function() { return { restrict: 'EA', link: function(scope, element, attrs) { setTimeout(function() { var crmUiHelp = scope.$eval(attrs.crmUiHelp); var title = crmUiHelp && crmUiHelp.get('title') ? ts('%1 Help', {1: crmUiHelp.get('title')}) : ts('Help'); element.attr('title', title); }, 50); element .addClass('helpicon') .attr('href', '#') .on('click', function(e) { e.preventDefault(); scope.$eval(attrs.crmUiHelp).open(); }); } }; }) // example:
.directive('crmUiFor', function ($parse, $timeout) { return { require: '^crmUiIdScope', restrict: 'EA', template: '*', transclude: true, link: function (scope, element, attrs, crmUiIdCtrl) { scope.crmIsRequired = false; scope.cssClasses = {}; if (!attrs.crmUiFor) return; var id = crmUiIdCtrl.get(attrs.crmUiFor); element.attr('for', id); var ngModel = null; var updateCss = function () { scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine; }; // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available // immediately for initialization. Use retries/retryDelay to initialize such elements. var init = function (retries, retryDelay) { var input = $('#' + id); if (input.length === 0 && !attrs.crmUiForceRequired) { if (retries) { $timeout(function(){ init(retries-1, retryDelay); }, retryDelay); } return; } if (attrs.crmUiForceRequired) { scope.crmIsRequired = true; return; } var tgtScope = scope;//.$parent; if (attrs.crmDepth) { for (var i = attrs.crmDepth; i > 0; i--) { tgtScope = tgtScope.$parent; } } if (input.attr('ng-required')) { scope.crmIsRequired = scope.$parent.$eval(input.attr('ng-required')); scope.$parent.$watch(input.attr('ng-required'), function (isRequired) { scope.crmIsRequired = isRequired; }); } else { scope.crmIsRequired = input.prop('required'); } ngModel = $parse(attrs.crmUiFor)(tgtScope); if (ngModel) { ngModel.$viewChangeListeners.push(updateCss); } }; $timeout(function(){ init(3, 100); }); } }; }) // Define a scope in which a name like "subform.foo" maps to a unique ID. // example:
.directive('crmUiIdScope', function () { return { restrict: 'EA', scope: {}, controllerAs: 'crmUiIdCtrl', controller: function($scope) { var ids = {}; this.get = function(name) { if (!ids[name]) { ids[name] = "crmUiId_" + (++uidCount); } return ids[name]; }; }, link: function (scope, element, attrs) {} }; }) // Display an HTML blurb inside an IFRAME. // example: // example: .directive('crmUiIframe', function ($parse) { return { scope: { crmUiIframeSrc: '@', // expression which evaluates to a URL crmUiIframe: '@' // expression which evaluates to HTML content }, link: function (scope, elm, attrs) { var iframe = $(elm)[0]; iframe.setAttribute('width', '100%'); iframe.setAttribute('height', '250px'); iframe.setAttribute('frameborder', '0'); var refresh = function () { if (attrs.crmUiIframeSrc) { iframe.setAttribute('src', scope.$parent.$eval(attrs.crmUiIframeSrc)); } else { var iframeHtml = scope.$parent.$eval(attrs.crmUiIframe); var doc = iframe.document; if (iframe.contentDocument) { doc = iframe.contentDocument; } else if (iframe.contentWindow) { doc = iframe.contentWindow.document; } doc.open(); doc.writeln(iframeHtml); doc.close(); } }; // If the iframe is in a dialog, respond to resize events $(elm).parent().on('dialogresize dialogopen', function(e, ui) { $(this).css({padding: '0', margin: '0', overflow: 'hidden'}); iframe.setAttribute('height', '' + $(this).innerHeight() + 'px'); }); $(elm).parent().on('dialogresize', function(e, ui) { iframe.setAttribute('class', 'resized'); }); scope.$parent.$watch(attrs.crmUiIframe, refresh); } }; }) // Example: //
.directive('crmUiRichtext', function ($timeout) { return { require: '?ngModel', link: function (scope, elm, attr, ngModel) { var editor = CRM.wysiwyg.create(elm); if (!ngModel) { return; } if (attr.ngBlur) { $(elm).on('blur', function() { $timeout(function() { scope.$eval(attr.ngBlur); }); }); } ngModel.$render = function(value) { editor.done(function() { CRM.wysiwyg.setVal(elm, ngModel.$viewValue || ''); }); }; } }; }) // Display a lock icon (based on a boolean). // example: // example: .directive('crmUiLock', function ($parse, $rootScope) { var defaultVal = function (defaultValue) { var f = function (scope) { return defaultValue; }; f.assign = function (scope, value) { // ignore changes }; return f; }; // like $parse, but accepts a defaultValue in case expr is undefined var parse = function (expr, defaultValue) { return expr ? $parse(expr) : defaultVal(defaultValue); }; return { template: '', link: function (scope, element, attrs) { var binding = parse(attrs.binding, true); var titleLocked = parse(attrs.titleLocked, ts('Locked')); var titleUnlocked = parse(attrs.titleUnlocked, ts('Unlocked')); $(element).addClass('crm-i lock-button'); var refresh = function () { var locked = binding(scope); if (locked) { $(element) .removeClass('fa-unlock') .addClass('fa-lock') .prop('title', titleLocked(scope)) ; } else { $(element) .removeClass('fa-lock') .addClass('fa-unlock') .prop('title', titleUnlocked(scope)) ; } }; $(element).click(function () { binding.assign(scope, !binding(scope)); //scope.$digest(); $rootScope.$digest(); }); scope.$watch(attrs.binding, refresh); scope.$watch(attrs.titleLocked, refresh); scope.$watch(attrs.titleUnlocked, refresh); refresh(); } }; }) // CrmUiOrderCtrl is a controller class which manages sort orderings. // Ex: // JS: $scope.myOrder = new CrmUiOrderCtrl(['+field1', '-field2]); // $scope.myOrder.toggle('field1'); // $scope.myOrder.setDir('field2', ''); // HTML: ... .service('CrmUiOrderCtrl', function(){ // function CrmUiOrderCtrl(defaults){ this.values = defaults; } angular.extend(CrmUiOrderCtrl.prototype, { get: function get() { return this.values; }, getDir: function getDir(name) { if (this.values.indexOf(name) >= 0 || this.values.indexOf('+' + name) >= 0) { return '+'; } if (this.values.indexOf('-' + name) >= 0) { return '-'; } return ''; }, // @return bool TRUE if something is removed remove: function remove(name) { var idx = this.values.indexOf(name); if (idx >= 0) { this.values.splice(idx, 1); return true; } else { return false; } }, setDir: function setDir(name, dir) { return this.toggle(name, dir); }, // Toggle sort order on a field. // To set a specific order, pass optional parameter 'next' ('+', '-', or ''). toggle: function toggle(name, next) { if (!next && next !== '') { next = '+'; if (this.remove(name) || this.remove('+' + name)) { next = '-'; } if (this.remove('-' + name)) { next = ''; } } if (next == '+') { this.values.unshift('+' + name); } else if (next == '-') { this.values.unshift('-' + name); } } }); return CrmUiOrderCtrl; }) // Define a controller which manages sort order. You may interact with the controller // directly ("myOrder.toggle('fieldname')") order using the helper, crm-ui-order-by. // example: // // My Field // ... // // Example for jQuery UI (deprecated): .directive('crmIcon', function() { return { restrict: 'EA', link: function (scope, element, attrs) { if (element.is('[crm-ui-tab]')) { // handled in crmUiTab ctrl return; } if (attrs.crmIcon.substring(0,3) == 'fa-') { $(element).prepend(' '); } else { $(element).prepend(' '); } if ($(element).is('button')) { $(element).addClass('crm-button'); } } }; }) // example:
...content...
// If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering. // example:
...content...
// example with custom classes:
...content...
.directive('crmUiWizardStep', function() { var nextWeight = 1; return { require: ['^crmUiWizard', 'form'], restrict: 'EA', scope: { crmTitle: '@', // expression, evaluates to a printable string crmUiWizardStep: '@', // int, a weight which determines the ordering of the steps crmUiWizardStepClass: '@' // string, A list of classes that will be added to the template }, template: '
', transclude: true, link: function (scope, element, attrs, ctrls) { var crmUiWizardCtrl = ctrls[0], form = ctrls[1]; if (scope.crmUiWizardStep) { scope.crmUiWizardStep = parseInt(scope.crmUiWizardStep); } else { scope.crmUiWizardStep = nextWeight++; } scope.isStepValid = function() { return form.$valid; }; crmUiWizardCtrl.add(scope); scope.$on('$destroy', function(){ crmUiWizardCtrl.remove(scope); }); } }; }) // Example: // Example: // Example: .directive('crmConfirm', function ($compile, $rootScope, $templateRequest, $q) { // Helpers to calculate default options for CRM.confirm() var defaultFuncs = { 'disable': function (options) { return { message: ts('Are you sure you want to disable this?'), options: {no: ts('Cancel'), yes: ts('Disable')}, width: 300, title: ts('Disable %1?', { 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') }) }; }, 'revert': function (options) { return { message: ts('Are you sure you want to revert this?'), options: {no: ts('Cancel'), yes: ts('Revert')}, width: 300, title: ts('Revert %1?', { 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') }) }; }, 'delete': function (options) { return { message: ts('Are you sure you want to delete this?'), options: {no: ts('Cancel'), yes: ts('Delete')}, width: 300, title: ts('Delete %1?', { 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') }) }; } }; var confirmCount = 0; return { link: function (scope, element, attrs) { $(element).click(function () { var options = scope.$eval(attrs.crmConfirm); if (attrs.title && !options.title) { options.title = attrs.title; } var defaults = (options.type) ? defaultFuncs[options.type](options) : {}; var tpl = null, stubId = null; if (!options.message) { if (options.templateUrl) { tpl = $templateRequest(options.templateUrl); } else if (options.template) { tpl = options.template; } if (tpl) { stubId = 'crmUiConfirm_' + (++confirmCount); options.message = '
'; } } CRM.confirm(_.extend(defaults, options)) .on('crmConfirm:yes', function() { scope.$apply(attrs.onYes); }) .on('crmConfirm:no', function() { scope.$apply(attrs.onNo); }); if (tpl && stubId) { $q.when(tpl, function(html) { var scope = options.scope || $rootScope.$new(); if (options.export) { angular.extend(scope, options.export); } var linker = $compile(html); $('#' + stubId).append($(linker(scope))); }); } }); } }; }) // Sets document title & page title; attempts to override CMS title markup for the latter // WARNING: Use only once per route! // Example (same title for both):

{{ts('Hello')}}

// Example (separate document title):

{{ts('Hello')}}

.directive('crmPageTitle', function($timeout) { return { scope: { crmDocumentTitle: '=' }, link: function(scope, $el, attrs) { function update() { $timeout(function() { var newPageTitle = _.trim($el.html()), newDocumentTitle = scope.crmDocumentTitle || $el.text(); document.title = $('title').text().replace(documentTitle, newDocumentTitle); // If the CMS has already added title markup to the page, use it $('h1').not('.crm-container h1').each(function() { if (_.trim($(this).html()) === pageTitle) { $(this).addClass('crm-page-title').html(newPageTitle); $el.hide(); } }); pageTitle = newPageTitle; documentTitle = newDocumentTitle; }); } scope.$watch(function() {return scope.crmDocumentTitle + $el.html();}, update); } }; }) .run(function($rootScope, $location) { /// Example: $rootScope.goto = function(path) { $location.path(path); }; // useful for debugging: $rootScope.log = console.log || function() {}; }) ; })(angular, CRM.$, CRM._);