1(function(angular, $, _) { 2 angular.module('ng') 3 .config(['$locationProvider', function($locationProvider) { 4 $locationProvider.hashPrefix(''); 5 }]); 6 7 8angular.module('af', CRM.angRequires('af')); 9 10 angular.module('af').directive('afButton', function() { 11 return { 12 restrict: 'C', 13 bindToController: { 14 type: '@' 15 }, 16 require: { 17 afForm: '?^^afForm', 18 }, 19 controller: function($scope, $element) { 20 const ctrl = this; 21 22 this.$onInit = () => { 23 const type = $element.attr('type'); 24 if (type === 'reset') { 25 $element.on('click', (e) => { 26 e.preventDefault(); 27 $scope.$apply(() => { 28 if (this.afForm) { 29 this.afForm.resetForm(); 30 } 31 else { 32 $scope.$parent.$broadcast('afFormReset'); 33 } 34 }); 35 }); 36 } 37 }; 38 } 39 }; 40 }); 41 42 angular.module('af').component('afCharacterCount', { 43 bindings: { 44 maxlength: '<', 45 getter: '<', 46 }, 47 templateUrl: '~/af/afCharacterCount.html', 48 controller: function($scope, $element) { 49 this.$onInit = () => { 50 }; 51 52 this.getLength = function() { 53 return (this.getter() || '').length; 54 }; 55
56 this.getStatusClass = function() { 57 let fraction = this.getLength() / (this.maxlength || 1); 58 if (fraction > 0.9) { 59 return 'text-danger'; 60 } 61 if (fraction > 0.7) { 62 return 'text-warning'; 63 } 64 return 'text-success'; 65 }; 66 67 } 68 }); 69 70const modelProps = { 71 type: '@', 72 data: '=', 73 actions: '=', 74 modelName: '@name', 75 label: '@' 76 }; 77 angular.module('af').component('afEntity', { 78 require: {afForm: '^afForm'}, 79 bindings: modelProps, 80 controller: function() { 81 82 this.$onInit = () => { 83 const entity = Object.keys(modelProps).reduce((obj, key) => { 84 if (this[key] !== undefined) { 85 obj[key] = this[key]; 86 } 87 return obj; 88 }, {}); 89 entity.actions = entity.actions || {update: true, create: true}; 90 entity.id = null; 91 this.afForm.registerEntity(entity); 92 }; 93 } 94 95 }); 96 97let afFieldId = 0; 98 angular.module('af').component('afField', { 99 require: { 100 afForm: '?^^afForm', 101 afFieldset: '?^^afFieldset', 102 afJoin: '?^^afJoin', 103 afRepeatItem: '?^^afRepeatItem' 104 }, 105 templateUrl: '~/af/afField.html', 106 bindings: { 107 fieldName: '@name', 108 defn: '=' 109 }, 110 controller: function($scope, $element, crmApi4, $timeout) { 111 const ts = $scope.ts = CRM.ts('org.civicrm.afform'); 112 const ctrl = this; 113 let namePrefix = ''; 114 let fieldOptions = null; 115 let isExtra = false; 116 this.inputAttrs = []; 117 118 this.$onInit = () => { 119 if (!this.fieldName) { 120 isExtra = true; 121 $scope.dataProvider = this.afForm; 122 this.fieldName = this.defn.name; 123 } else { 124 const closestController = $($element).closest('[af-fieldset],[af-join],[af-repeat-item]'); 125 $scope.dataProvider = closestController.is('[af-repeat-item]') ? this.afRepeatItem : this.afJoin || this.afFieldset; 126 } 127 $scope.fieldId = _.kebabCase(this.fieldName) + '-' + afFieldId++; 128 129 $element.addClass('af-field-type-' + _.kebabCase(this.defn.input_type)); 130 131 if (this.defn.input_attrs && this.defn.input_attrs.multiple) { 132 $element.addClass('af-field-type-multiple'); 133 } 134 135 this.fkEntity = this.defn.fk_entity || null; 136 137 if (this.defn.name !== this.fieldName) { 138 if (!this.defn.name) { 139 console.error('Missing field definition for: ' + this.fieldName); 140 return; 141 } 142 namePrefix = this.fieldName.substr(0, this.fieldName.length - this.defn.name.length); 143 } 144 145 if (this.defn.search_operator) { 146 this.search_operator = this.defn.search_operator; 147 } 148 149 fieldOptions = this.defn.options || null; 150 if (this.defn.data_type === 'Boolean') { 151 if (Array.isArray(fieldOptions)) { 152 fieldOptions.forEach((option) => option.id = !!option.id); 153 } else { 154 fieldOptions = [{id: true, label: ts('Yes')}, {id: false, label: ts('No')}]; 155 } 156 } 157 if (this.fieldName === 'is_primary' && 'repeatIndex' in $scope.dataProvider) { 158 fieldOptions = [{id: true, label: ''}]; 159 $scope.$watch('dataProvider.afRepeat.getEntityController().getData()', function (items, prev) { 160 const index = $scope.dataProvider.repeatIndex; 161 if (items && !index && !items.some((item) => item.is_primary)) { 162 $scope.dataProvider.getFieldData().is_primary = true; 163 } 164 if (items && prev && 165 items.length === prev.length && 166 items[index].is_primary && 167 prev[index].is_primary && 168 items.filter((item) => item.is_primary).length > 1 169 ) { 170 $scope.dataProvider.getFieldData().is_primary = false; 171 } 172 }, true); 173 } 174 const tokens = this.afForm?.identifyTokens(this.defn.afform_default); 175 if (tokens && tokens.length) { 176 const calculateValueWatcher = $scope.$watchCollection(() => Object.values(this.afForm.getTokenValues(tokens)), () => { 177 if ($element[0].querySelector('.ng-touched')) { 178 calculateValueWatcher(); 179 return; 180 } 181 const calculatedValue = this.afForm.replaceTokens(this.defn.afform_default); 182 setValue(calculatedValue); 183 }); 184 } 185 if (this.defn.input_type === 'ChainSelect' && this.defn.input_attrs.control_field) { 186 const controlField = namePrefix + this.defn.input_attrs.control_field; 187 $scope.$watch('dataProvider.getFieldData()["' + controlField + '"]', function(val) { 188 if (val && (typeof val === 'number' || val.length)) { 189 $('input[crm-ui-select]', $element).addClass('loading').prop('disabled', true); 190 const params = { 191 name: ctrl.afFieldset.getFormName(), 192 modelName: ctrl.afFieldset.getName(), 193 fieldName: ctrl.fieldName, 194 joinEntity: ctrl.afJoin ? ctrl.afJoin.entity : null, 195 values: $scope.dataProvider.getFieldData() 196 }; 197 crmApi4('Afform', 'getOptions', params) 198 .then((data) => { 199 $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', !data.length); 200 fieldOptions = data; 201 }); 202 } else { 203 fieldOptions = null; 204 } 205 }, true); 206 } 207 if (this.defn.input_type === 'ChainSelect' || (Array.isArray(fieldOptions) && fieldOptions.some((o) => o && o.if && o.if.length))) { 208 $scope.$watchCollection(() => $scope.getOptions().map((o) => o.id), () => this.validateValue()); 209 } 210 if (this.defn.input_type === 'EntityRef' && this.defn.dfk_entities && this.defn.input_attrs.control_field) { 211 const controlField = namePrefix + this.defn.input_attrs.control_field; 212 $scope.$watch('dataProvider.getFieldData()["' + controlField + '"]', (val) => { 213 if (val && val.length) { 214 if (Array.isArray(val)) { 215 this.fkEntity = this.defn.dfk_entities[val[0]]; 216 } else { 217 this.fkEntity = this.defn.dfk_entities[val]; 218 } 219 } else { 220 this.fkEntity = null; 221 } 222 }); 223 } 224 $timeout(() => { 225 initializeValue(true);
226 $scope.$watch('$parent.routeParams', setValueFromRouteParams); 227 }); 228 function setValueFromRouteParams(routeParams) { 229 if (!routeParams) { 230 return; 231 } 232 const entityName = ctrl.afFieldset?.getName(); 233 const joinEntity = ctrl.afJoin?.entity; 234 let uniquePrefix = ''; 235 if (!isExtra && entityName) { 236 const index = ctrl.getEntityIndex(); 237 uniquePrefix = entityName + (index ? index + 1 : '') + (joinEntity ? '.' + joinEntity : '') + '.'; 238 } 239 if ((uniquePrefix + ctrl.fieldName) in routeParams) { 240 setValue(routeParams[uniquePrefix + ctrl.fieldName]); 241 } 242 else if (ctrl.fieldName in routeParams) { 243 setValue(routeParams[ctrl.fieldName]); 244 } 245 else if (!isExtra && routeParams._s) { 246 setValue(ctrl.afFieldset.getSearchParamSetFieldValue(ctrl.fieldName)); 247 } 248 } 249 250 function initializeValue(firstLoad) { 251 if (firstLoad && ctrl.afFieldset?.getStoredValue(ctrl.fieldName) !== undefined) { 252 setValue(ctrl.afFieldset.getStoredValue(ctrl.fieldName)); 253 } 254 else if ('afform_default' in ctrl.defn) { 255 if (ctrl.afForm?.identifyTokens(ctrl.defn.afform_default)) { 256 setValue(ctrl.afForm.replaceTokens(ctrl.defn.afform_default)); 257 } 258 else { 259 setValue(ctrl.defn.afform_default); 260 } 261 } 262 263 if (ctrl.defn.search_range) { 264 const initialVal = $scope.dataProvider.getFieldData()[ctrl.fieldName]; 265 if (!Array.isArray($scope.dataProvider.getFieldData()[ctrl.fieldName]) && 266 (ctrl.defn.input_type !== 'Select' || !ctrl.defn.is_date || initialVal === '{}') 267 ) { 268 $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; 269 } 270 if (ctrl.defn.is_date) { 271 ctrl.inputAttrs.push(ctrl.defn.input_attrs || {}); 272 for (let i = 1; i <= 2; ++i) { 273 const attrs = _.cloneDeep(ctrl.defn.input_attrs || {}); 274 attrs.placeholder = attrs['placeholder' + i]; 275 attrs.timePlaceholder = attrs['timePlaceholder' + i]; 276 ctrl.inputAttrs.push(attrs); 277 } 278 } 279 } 280 } 281 $scope.$on('afFormReset', function() { 282 delete $scope.dataProvider.getFieldData()[ctrl.fieldName]; 283 initializeValue(false); 284 }); 285 }; 286 $scope.$on('afIfDestroy', function() { 287 if (ctrl.defn.input_type !== 'DisplayOnly') { 288 delete $scope.dataProvider.getFieldData()[ctrl.fieldName]; 289 } 290 }); 291 function correctValueType(value, dataType) { 292 if (value === null) { 293 return value; 294 } 295 if (Array.isArray(value)) { 296 return value.map((val) => correctValueType(val, dataType)); 297 } else if (dataType === 'Integer' || dataType === 'Float') { 298 return Number(value); 299 } else if (dataType === 'Boolean') { 300 return (value == 1); 301 } 302 return value; 303 } 304 305 this.isMultiple = function() { 306 return ( 307 (['Select', 'EntityRef', 'ChainSelect'].includes(ctrl.defn.input_type) && ctrl.defn.input_attrs.multiple) || 308 ((ctrl.defn.input_type === 'CheckBox' || ctrl.defn.input_type === 'Toggle') && ctrl.defn.data_type !== 'Boolean') || 309 ((ctrl.defn.input_type === 'Hidden' || ctrl.defn.input_type === 'DisplayOnly') && (ctrl.defn.serialize || ctrl.defn.data_type === 'Array')) 310 ); 311 }; 312 function setValue(value) { 313 if (typeof value === 'string' && ctrl.isMultiple()) { 314 value = value.split(','); 315 } 316 if (typeof value === 'object' && value !== null && ctrl.search_operator) { 317 if (ctrl.defn.expose_operator) { 318 ctrl.search_operator = Object.keys(value)[0]; 319 } 320 value = value[ctrl.search_operator] ? value[ctrl.search_operator] : null; 321 } 322 if (ctrl.defn.input_type === 'EntityRef' && ['Contact', 'Individual'].includes(ctrl.fkEntity) && value === 'user_contact_id') { 323 value = CRM.config.cid; 324 } 325 if (ctrl.defn.input_type !== 'DisplayOnly') { 326 value = correctValueType(value, ctrl.defn.data_type); 327 } 328 329 if (ctrl.defn.input_type === 'Date' && typeof value === 'string' && value.startsWith('now')) { 330 value = getRelativeDate(value, ctrl.defn.input_attrs.time); 331 } 332 if (ctrl.defn.input_type === 'Number' && ctrl.defn.search_range) { 333 if (!_.isPlainObject(value)) { 334 value = { 335 '>=': +(('' + value).split('-')[0] || 0), 336 '<=': +(('' + value).split('-')[1] || 0), 337 }; 338 } 339 } else if (ctrl.defn.input_type === 'Number') { 340 value = Number(value); 341 } 342 else if (ctrl.defn.search_range && !_.isPlainObject(value) && 343 !(ctrl.defn.options && ctrl.defn.options.some((option) => option.id === value)) 344 ) { 345 value = { 346 '>=': ('' + value).split('-')[0], 347 '<=': ('' + value).split('-')[1] || '', 348 }; 349 } 350 $scope.getSetValue(value); 351 } 352 ctrl.getEntityIndex = function() { 353 if ('repeatIndex' in $scope.dataProvider && $scope.dataProvider.afRepeat.getRepeatType() === 'join') { 354 return $scope.dataProvider.outerRepeatItem ? $scope.dataProvider.outerRepeatItem.repeatIndex : 0; 355 } else { 356 return ctrl.afRepeatItem ? ctrl.afRepeatItem.repeatIndex : 0; 357 } 358 }; 359 360 ctrl.isReadonly = function() { 361 if (!isExtra && ctrl.defn.input_attrs && ctrl.defn.input_attrs.autofill && !ctrl.afJoin) { 362 return ctrl.afFieldset.getEntity()?.actions?.[ctrl.defn.input_attrs.autofill] === false; 363 } 364 return ctrl.defn.input_type === 'DisplayOnly'; 365 }; 366 367 ctrl.isDisabled = function() { 368 if (ctrl.isReadonly()) { 369 return true; 370 } 371 return ctrl.defn.input_type === 'EntityRef' && !ctrl.fkEntity; 372 }; 373 374 ctrl.getDisplayValue = function(value) { 375 if (value === undefined || value === null || value === '' || (Array.isArray(value) && !value.length)) { 376 return ''; 377 } 378 if (Array.isArray(fieldOptions)) { 379 let keys = Array.isArray(value) ? value : [value]; 380 let options = fieldOptions.filter((option) => keys.includes(option.id)); 381 return options.map((option) => option.label).join(', '); 382 } 383 if (ctrl.defn.data_type === 'Date' || ctrl.defn.data_type === 'Timestamp') { 384 try { 385 return CRM.utils.formatDate(value, null, ctrl.defn.data_type === 'Timestamp'); 386 } catch (e) { 387 return ''; 388 } 389 } 390 if (ctrl.fkEntity) { 391 const ids = Array.isArray(value) ? value : [value]; 392 if (!ctrl._entityLabels) { 393 ctrl._entityLabels = {}; 394 } 395 if (!(ids.join() in ctrl._entityLabels)) { 396 ctrl._entityLabels[ids.join()] = null; 397 const params = ctrl.getAutocompleteParams(); 398 params.ids = ids; 399 crmApi4(ctrl.fkEntity, 'autocomplete', params) 400 .then((result) => { 401 ctrl._entityLabels[ids.join()] = result.map((item) => item.label).join(', '); 402 }); 403 } 404 return ctrl._entityLabels[ids.join()] || ts('Loading...'); 405 } 406 return value; 407 }; 408 ctrl.onSelectEntity = function() { 409 if (ctrl.afForm && ctrl.defn.input_attrs && ctrl.defn.input_attrs.autofill) { 410 const val = $scope.getSetSelect(); 411 const entity = ctrl.afFieldset.modelName; 412 const entityIndex = ctrl.getEntityIndex(); 413 const joinEntity = ctrl.afJoin ? ctrl.afJoin.entity : null; 414 const joinIndex = ctrl.afJoin && $scope.dataProvider.repeatIndex || 0; 415 ctrl.afFieldset.afFormCtrl.loadData(entity, entityIndex, val, ctrl.defn.name, joinEntity, joinIndex); 416 } 417 }; 418 ctrl.getFileUploadParams = function() { 419 return { 420 modelName: ctrl.afFieldset.getName(), 421 fieldName: ctrl.fieldName, 422 joinEntity: ctrl.afJoin ? ctrl.afJoin.entity : null, 423 entityIndex: ctrl.getEntityIndex(), 424 joinIndex: ctrl.afJoin && $scope.dataProvider.repeatIndex || null 425 }; 426 }; 427 428 ctrl.getAutocompleteParams = function() { 429 let fieldName = isExtra ? 'extra' : ctrl.afFieldset.getName(); 430 if (!isExtra && ctrl.afJoin) { 431 fieldName += '+' + ctrl.afJoin.entity; 432 } 433 fieldName += ':' + ctrl.fieldName; 434 return { 435 formName: 'afform:' + ctrl.afFieldset.getFormName(), 436 fieldName: fieldName, 437 values: $scope.dataProvider.getFieldData() 438 }; 439 }; 440 441 $scope.getOptions = () => { 442 if (!Array.isArray(fieldOptions)) { 443 return []; 444 } 445 return fieldOptions.filter((opt) => { 446 if (!opt.if || !opt.if.length) return true; 447 try { 448 return this.afForm.checkConditions(opt.if); 449 } catch (e) { 450 console.warn('[afField] `if` evaluation failed for option', opt.id, e); 451 return true; 452 } 453 }); 454 }; 455 456 this.validateValue = () => {
457 const current = $scope.dataProvider.getFieldData()[this.fieldName]; 458 const options = $scope.getOptions().map((o) => o.id); 459 460 if (Array.isArray(current)) { 461 const valid = current.filter((v) => options.includes(v)); 462 if (valid.length < current.length) { 463 $scope.dataProvider.getFieldData()[this.fieldName] = valid; 464 $('input[crm-ui-select]', $element).val(valid).change(); 465 } 466 } else { 467 if (!options.includes(current)) { 468 $('input[crm-ui-select]', $element).val('').change(); 469 delete $scope.dataProvider.getFieldData()[this.fieldName]; 470 } 471 } 472 }; 473 474 $scope.select2Options = function() { 475 return { 476 results: _.transform($scope.getOptions(), function(result, opt) { 477 result.push({id: opt.id, text: opt.label}); 478 }, []) 479 }; 480 }; 481 482 this.onChangeOperator = function() { 483 $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; 484 }; 485 $scope.getSetValue = function(val) { 486 const currentVal = $scope.dataProvider.getFieldData()[ctrl.fieldName]; 487 if (arguments.length) { 488 if (ctrl.search_operator) { 489 if (typeof currentVal !== 'object') { 490 $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; 491 } 492 return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.search_operator] = val); 493 } 494 return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val); 495 } 496 if (ctrl.search_operator) { 497 return (currentVal || {})[ctrl.search_operator]; 498 } 499 return currentVal; 500 }; 501 $scope.getSetSelect = function(val) { 502 const currentVal = $scope.dataProvider.getFieldData()[ctrl.fieldName]; 503 if (arguments.length) { 504 if (ctrl.defn.is_date) { 505 if (val === '{}') { 506 val = !_.isPlainObject(currentVal) ? {} : currentVal; 507 } 508 } 509 else if (ctrl.defn.search_range) { 510 return ($scope.dataProvider.getFieldData()[ctrl.fieldName]['>='] = val); 511 } 512 else if (ctrl.search_operator) { 513 if (typeof currentVal !== 'object') { 514 $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; 515 } 516 return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.search_operator] = val); 517 } 518 if (ctrl.defn.data_type === 'Boolean') { 519 return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = (val === 'true')); 520 } 521 if (ctrl.defn.data_type === 'Integer' || ctrl.defn.data_type === 'Float') { 522 if (typeof val === 'string') { 523 return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val.length ? Number(val) : null); 524 } else if (Array.isArray(val)) { 525 return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val.map(Number)); 526 } 527 } 528 return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val); 529 } 530 if (ctrl.defn.is_date) { 531 return _.isPlainObject(currentVal) ? '{}' : currentVal; 532 } 533 else if (ctrl.defn.search_range) { 534 return currentVal['>=']; 535 } 536 else if (ctrl.search_operator) { 537 return (currentVal || {})[ctrl.search_operator]; 538 } 539 else if (!ctrl.isMultiple() && (typeof currentVal === 'boolean' || typeof currentVal === 'number')) { 540 return JSON.stringify(currentVal); 541 } 542 return currentVal; 543 }; 544 545 function getRelativeDate(dateString, includeTime) { 546 const parts = dateString.split(' '); 547 const baseDate = new Date(); 548 let unit = parts[2] || 'day'; 549 let offset = parseInt(parts[1] || '0', 10); 550 551 switch (unit) { 552 case 'week': 553 offset *= 7; 554 break; 555 556 case 'year': 557 offset *= 365; 558 } 559 let newDate = new Date(baseDate.getTime() + offset * 24 * 60 * 60 * 1000); 560 let localYear = newDate.getFullYear(); 561 let localMonth = String(newDate.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed 562 let localDay = String(newDate.getDate()).padStart(2, '0'); 563 let defaultDate = `${localYear}-${localMonth}-${localDay}
563`; // Format YYYY-MM-DD 564 565 if (includeTime) { 566 defaultDate += ' ' + newDate.toTimeString().slice(0,8); 567 } 568 return defaultDate; 569 } 570 571 } 572 }); 573 574angular.module('af').directive('afFieldset', function() { 575 return { 576 restrict: 'A', 577 require: ['afFieldset', '?^^afForm'], 578 bindToController: { 579 modelName: '@afFieldset', 580 storeValues: '<' 581 }, 582 link: function($scope, $el, $attr, ctrls) { 583 const self = ctrls[0]; 584 self.afFormCtrl = ctrls[1]; 585 }, 586 controller: function($scope, $element, crmApi4) { 587 const ctrl = this; 588 const localData = []; 589 const joinOffsets = {}; 590 const ts = $scope.ts = CRM.ts('org.civicrm.afform'); 591 592 this.getData = function() { 593 return ctrl.afFormCtrl ? ctrl.afFormCtrl.getData(ctrl.modelName) : localData; 594 }; 595 this.getName = function() { 596 return this.modelName || 597 $element.find('[search-name][display-name]').attr('display-name'); 598 }; 599 this.getEntity = function() { 600 return this.afFormCtrl ? this.afFormCtrl.getEntity(this.modelName) : null; 601 }; 602 this.getEntityType = function() { 603 return this.afFormCtrl ? this.afFormCtrl.getEntity(this.modelName)?.type : null; 604 }; 605 this.getFieldData = function() { 606 const data = ctrl.getData(); 607 if (!data.length) { 608 data.push({fields: {}}); 609 } 610 return data[0].fields; 611 }; 612 this.getFormName = function() { 613 return ctrl.afFormCtrl ? ctrl.afFormCtrl.getFormMeta().name : $scope.meta.name; 614 }; 615 616 this.getFieldMeta = () => { 617 const meta = {}; 618 const fieldElements = $element[0].querySelectorAll('af-field'); 619 fieldElements.forEach((field) => { 620 const name = field.getAttribute('name'); 621 const defn = $scope.$eval(field.getAttribute('defn')); 622 meta[name] = defn; 623 }); 624 return meta; 625 }; 626 627 this.getJoinOffset = function(joinEntity) { 628 joinOffsets[joinEntity] = joinEntity in joinOffsets ? joinOffsets[joinEntity] + 1 : 0; 629 return joinOffsets[joinEntity]; 630 }; 631 function getCacheKey() { 632 return 'afform:' + ctrl.getFormName() + ctrl.getName(); 633 }
634 this.getStoredValue = function(fieldName) { 635 if (!this.storeValues) { 636 return; 637 } 638 if (!this.reloadedStoredValues) { 639 this.reloadedStoredValues = CRM.cache.get(getCacheKey(), {}); 640 } 641 return this.reloadedStoredValues[fieldName]; 642 }; 643 644 this.$onInit = function() { 645 $scope.$watch(ctrl.getFieldData, (newVal, oldVal) => { 646 $element[0].dispatchEvent(new Event('crmFormChangeFilters')); 647 if (this.storeValues) { 648 if (typeof newVal === 'object' && typeof oldVal === 'object' && Object.keys(newVal).length) { 649 CRM.cache.set(getCacheKey(), newVal); 650 } 651 } 652 }, true); 653 654 $scope.$watch(this.getSearchParamSetId, () => this.fetchSearchParamSetValues()); 655 }; 656 657 /** 658 * Get fieldset values to use for afform filters 659 * 660 * Filter out empty values (null, '', [], {}) at top level and one level 661 * below for object values (fields with ranges/operators) 662 * 663 * @returns Object 664 */ 665 this.getFilterValues = () => { 666 const isEmpty = (v) => { 667 if (v === null || v === '') { 668 return true; 669 } 670 if (Array.isArray(v) && v.length === 0) { 671 return true; 672 } 673 if (typeof v === 'object' && Object.keys(v).length === 0) { 674 return true; 675 } 676 }; 677 const data = _.cloneDeep(this.getFieldData()); 678 679 Object.keys(data).forEach((key) => { 680 if (typeof data[key] === 'object' && data[key] !== null) { 681 Object.keys(data[key]).forEach((subKey) => { 682 if (isEmpty(data[key][subKey])) { 683 delete data[key][subKey]; 684 } 685 }); 686 } 687 if (isEmpty(data[key])) { 688 delete data[key]; 689 } 690 }); 691 692 return data; 693 }; 694 695 /** 696 * Get value for a given field based on currently applied SearchParamSet 697 * Used by afField controller on load 698 */
699 this.getSearchParamSetFieldValue = (fieldName) => { 700 if (this.selectedSearchParamSet && this.selectedSearchParamSet.filters && this.selectedSearchParamSet.filters.hasOwnProperty(fieldName)) { 701 return this.selectedSearchParamSet.filters[fieldName]; 702 } 703 return null; 704 }; 705 706 /** 707 * Fetch values for a SearchParamSet from the server 708 */ 709 this.fetchSearchParamSetValues = () => { 710 const searchParamSetId = this.getSearchParamSetId(); 711 if (!searchParamSetId) { 712 return; 713 } 714 crmApi4('SearchParamSet', 'get', { 715 where: [ 716 ['id', '=', searchParamSetId], 717 ['afform_name', '=', this.getFormName()] 718 ], 719 select: ['filters', 'columns'], 720 }) 721 .then((result) => { 722 if (result && result[0]) { 723 this.selectedSearchParamSet = result[0]; 724 } 725 else { 726 this.selectedSearchParamSet = {}; 727 } 728 $scope.$broadcast('afFormReset'); 729 }); 730 }; 731 732 /** 733 * Get search param set ID from URL hash param 734 */
735 this.getSearchParamSetId = () => ($scope && $scope.routeParams) ? $scope.routeParams._s : null; 736 } 737 }; 738 }); 739 740 angular.module('af').component('afForm', { 741 bindings: { 742 ctrl: '@' 743 }, 744 require: { 745 ngForm: 'form' 746 }, 747 controller: function($scope, $element, $timeout, crmApi4, crmStatus, $window, $location, $parse, FileUploader) { 748 const 749 ctrl = this, 750 ts = CRM.ts('org.civicrm.afform'); 751 752 const saveDraftButtons = []; 753 const schema = {}; 754 const data = { 755 extra: {fields: {}}, 756 }; 757 758 let 759 args, 760 submissionResponse, 761 autoSave = () => {}, 762 draftStatus = 'pristine', 763 cancelDraftWatcher, 764 uploadingDraftFiles = false; 765 766 this.$onInit = () => { 767 $scope.$parent[this.ctrl] = this; 768 769 $timeout(() => { 770 $element[0].querySelectorAll('.af-markup:not(af-markup)').forEach((el) => { 771 const renderer = document.createElement('af-markup'); 772 renderer.markup = el.innerHTML; 773 el.replaceChildren(renderer); 774 }); 775 776 ctrl.loadData() 777 .then(setupDraftWatcher); 778 779 ctrl.showSubmitButton = displaySubmitButton(args); 780 }); 781 }; 782 783 this.registerEntity = function registerEntity(entity) { 784 schema[entity.modelName] = entity; 785 data[entity.modelName] = []; 786 }; 787 this.getEntity = function getEntity(name) { 788 return schema[name]; 789 }; 790 this.getData = function getData(name) { 791 return data[name]; 792 };
793 this.getSchema = function getSchema(name) { 794 return schema[name]; 795 }; 796 this.getFormMeta = function getFormMeta() { 797 return $scope.$parent.meta; 798 }; 799 this.resetForm = function() { 800 this.ngForm.$setPristine(); 801 $scope.$parent.$broadcast('afFormReset'); 802 this.loadData(); 803 }; 804 this.loadData = function(selectedEntity, selectedIndex, selectedId, selectedField, joinEntity, joinIndex) { 805 let toLoad = true; 806 const params = {name: ctrl.getFormMeta().name, args: {}}; 807 if (selectedEntity) { 808 toLoad = !!selectedId; 809 params.args[selectedEntity] = {}; 810 params.args[selectedEntity][selectedIndex] = {}; 811 if (joinEntity) { 812 params.fillMode = 'join'; 813 params.args[selectedEntity][selectedIndex].joins = {}; 814 params.args[selectedEntity][selectedIndex].joins[joinEntity] = {}; 815 params.args[selectedEntity][selectedIndex].joins[joinEntity][joinIndex] = {}; 816 params.args[selectedEntity][selectedIndex].joins[joinEntity][joinIndex][selectedField] = selectedId; 817 } else { 818 params.fillMode = 'entity'; 819 params.args[selectedEntity][selectedIndex][selectedField] = selectedId; 820 } 821 } 822 else { 823 params.fillMode = 'form'; 824 args = Object.assign({}, $scope.$parent.routeParams || {}, $scope.$parent.options || {}); 825 Object.keys(schema).forEach((entityName) => { 826 if (args[entityName] && typeof args[entityName] === 'string') { 827 args[entityName] = args[entityName].split(','); 828 } 829 }); 830 params.args = args; 831 ctrl.showSubmitButton = displaySubmitButton(args); 832 } 833 if (toLoad) { 834 if (params.fillMode === 'form') { 835 $element.block(); 836 } 837 return crmApi4('Afform', 'prefill', params) 838 .then((result) => { 839 result.forEach((item) => { 840 _.each(item.values, (values, index) => { 841 data[item.name][index] = data[item.name][index] || {}; 842 data[item.name][index].joins = data[item.name][index].joins || {}; 843 angular.merge(data[item.name][index], values, {fields: _.cloneDeep(schema[item.name]?.data || {})}); 844 }); 845 }); 846 $element.unblock(); 847 }, (error) => { 848 disableForm(error.error_message, ts('Sorry'), 'error'); 849 $element.unblock(); 850 }); 851 } 852 else if (joinEntity) { 853 data[selectedEntity][selectedIndex].joins[joinEntity][joinIndex] = {}; 854 } 855 else if (selectedEntity) { 856 Object.keys(data[selectedEntity][selectedIndex].fields).forEach((key) => delete data[selectedEntity][selectedIndex].fields[key]); 857 angular.merge(data[selectedEntity][selectedIndex].fields, _.cloneDeep(schema[selectedEntity].data || {})); 858 data[selectedEntity][selectedIndex].joins = {}; 859 } 860 }; 861 862 function displaySubmitButton(args) { 863 if (args.sid && args.sid.length > 0) { 864 return false; 865 } 866 return true; 867 } 868 const token = new URLSearchParams(window.location.search).get('_aff'); 869 const headers = {'X-Requested-With': 'XMLHttpRequest'}; 870 if (token) { 871 headers['X-Civi-Auth-Afform'] = token; 872 } 873 this.fileUploader = new FileUploader({ 874 url: CRM.url('civicrm/ajax/api4/Afform/submitFile'), 875 headers: headers, 876 onAfterAddingFile: (item) => { 877 setDraftStatus('unsaved'); 878 }, 879 onSuccessItem: onFileUploadSuccess, 880 onCompleteAll: onFileUploadsComplete, 881 onBeforeUploadItem: (item) => { 882 item._status = CRM.status({ 883 start: ts('Uploading %1', {1: item.file.name}), 884 error: ts('Upload failed'), 885 success: ts('Upload complete'), 886 }); 887 }, 888 onCompleteItem: (item, response, status, headers) => { 889 if (status === 200) { 890 item._status.resolve(); 891 } else { 892 item._status.reject(); 893 } 894 } 895 }); 896 897 function onFileUploadSuccess(item, response, status, headers) { 898 if (response.values && response.values[0] && response.values[0].id) { 899 const dataProvider = item.crmDataProvider; 900 dataProvider.getFieldData()[item.crmFieldName] = response.values[0]; 901 } 902 } 903 904 function onFileUploadsComplete() { 905 if (uploadingDraftFiles) { 906 uploadingDraftFiles = false;
907 setDraftStatus('saved'); 908 if (draftStatus === 'unsaved') { 909 autoSave(); 910 } 911 } else { 912 postProcess(); 913 } 914 } 915 function setupDraftWatcher() { 916 const buttons = getDraftButtons(); 917 const autoSaveEnabled = ctrl.getFormMeta().autosave_draft; 918 919 if ((!autoSaveEnabled && !buttons.length) || !ctrl.showSubmitButton || (!CRM.config.cid && !token)) { 920 return; 921 } 922 $.each(buttons, function(index, button) { 923 saveDraftButtons[index] = { 924 text: $(button).text(), 925 icon: $(button).attr('crm-icon'), 926 }; 927 }); 928 if (autoSaveEnabled) { 929 autoSave = _.debounce(ctrl.submitDraft, 10000); 930 } 931 932 cancelDraftWatcher = $scope.$watch(() => data, function (newVal, oldVal) { 933 if (oldVal) { 934 if (draftStatus === 'pristine') { 935 setDraftStatus('saved'); 936 } else { 937 setDraftStatus('unsaved'); 938 autoSave(newVal); 939 } 940 } 941 }, 942 true 943 ); 944 } 945 this.checkConditions = function(conditions, op) { 946 op = op || 'AND'; 947 let ret = op === 'AND', 948 flip = !ret; 949 _.each(conditions, function(clause) { 950 if (Array.isArray(clause[1])) { 951 if (ctrl.checkConditions(clause[1], clause[0]) === flip) { 952 ret = flip; 953 } 954 } else { 955 if (typeof clause[0] === 'string' && clause[0].charAt(0) !== '"') { 956 clause[0] = clause[0].replace(/\[([^'"])/g, "['$1").replace(/([^'"])]/g, "$1']"); 957 } 958 let parser1 = $parse(clause[0]); 959 let parser2 = $parse(clause[2]); 960 let result = compareConditions(parser1(data), clause[1], parser2(data)); 961 if (result === flip) { 962 ret = flip; 963 } 964 } 965 }); 966 return op === 'NOT' ? !ret : ret; 967 }; 968 969 function compareConditions(val1, op, val2) { 970 const yes = (op !== '!=' && !op.includes('NOT ')); 971 972 switch (op) { 973 case '=': 974 case '!=': 975 case '==': 976 if (typeof val1 === 'string') { 977 val1 = val1.toLowerCase(); 978 } 979 if (typeof val2 === 'string') { 980 val2 = val2.toLowerCase(); 981 } 982 return angular.equals(val1, val2) === yes; 983 984 case '>': 985 return val1 > val2; 986 987 case '<': 988 return val1 < val2; 989 990 case '>=': 991 return val1 >= val2; 992 993 case '<=': 994 return val1 <= val2; 995 996 case 'IS EMPTY': 997 return !val1; 998 999 case 'IS NOT EMPTY': 1000 return !!val1; 1001 1002 case 'CONTAINS': 1003 case 'NOT CONTAINS': 1004 if (Array.isArray(val1)) { 1005 return val1.includes(val2) === yes; 1006 } else if (typeof val1 === 'string' && typeof val2 === 'string') { 1007 return val1.toLowerCase().includes(val2.toLowerCase()) === yes; 1008 } 1009 return angular.equals(val1, val2) === yes; 1010 1011 case 'IN': 1012 case 'NOT IN': 1013 if (Array.isArray(val2)) { 1014 return val2.includes(val1) === yes; 1015 } 1016 return angular.equals(val1, val2) === yes; 1017 1018 case 'LIKE': 1019 case 'NOT LIKE': 1020 if (typeof val1 === 'string' && typeof val2 === 'string') { 1021 return likeCompare(val1, val2) === yes; 1022 } 1023 return angular.equals(val1, val2) === yes; 1024 } 1025 } 1026 1027 function likeCompare(str, pattern) { 1028 const regexPattern = pattern 1029 .replace(/([.+?^=!:${}()|\[\]\/\\])/g, "\\$1") 1030 .replace(/%/g, '.*') // Convert % to .* 1031 .replace(/_/g, '.'); // Convert _ to . 1032 const regex = new RegExp(`^${regexPattern}$`, 'i'); 1033 return regex.test(str); 1034 } 1035 function postProcess() { 1036 const metaData = ctrl.getFormMeta(), 1037 dialog = $element.closest('.ui-dialog-content'); 1038 ctrl.ngForm.$setPristine(); 1039 1040 $element.trigger('crmFormSuccess', { 1041 afform: metaData, 1042 data: data, 1043 submissionResponse: submissionResponse, 1044 }); 1045 1046 if (submissionResponse[0].redirect) { 1047 let url = submissionResponse[0].redirect; 1048 if (url.indexOf('civicrm/') === 0) { 1049 url = CRM.url(url); 1050 } else if (url.indexOf('/') === 0) { 1051 let port = $location.port(); 1052 port = port ? `:${port}` : ''; 1053 url = `${$location.protocol()}://${$location.host()}${port}${url}`; 1054 } 1055 $window.location.href = url; 1056 return; 1057 } 1058 1059 if (submissionResponse[0].message) { 1060 $element.hide(); 1061 const $confirmation = $('<div class="afform-confirmation" />'); 1062 $confirmation.html(submissionResponse[0].message); 1063 $confirmation.insertAfter($element); 1064 } 1065 else if (dialog.length) { 1066 dialog.dialog('close'); 1067 } 1068 else { 1069 $element.unblock(); 1070 } 1071 } 1072 1073 function validateFileFields() { 1074 let valid = true; 1075 $("af-form[ng-form=" + ctrl.getFormMeta().name + "] input[type='file']").each((index, fld) => { 1076 if ($(fld).attr('required') && $(fld).get(0).files.length == 0) { 1077 valid = false;
1078 } 1079 }); 1080 return valid; 1081 } 1082 1083 function disableForm(errorMsg) { 1084 $('af-form[ng-form="' + ctrl.getFormMeta().name + '"]') 1085 .addClass('disabled') 1086 .find('button[ng-click="afform.submit()"]').prop('disabled', true); 1087 CRM.alert(errorMsg, ts('Sorry'), 'error'); 1088 } 1089 1090 const handleError = (error) => { 1091 if (error && error.error_code !== '1') { 1092 CRM.alert(error.error_message, ts('Please resolve these issues'), 'warning'); 1093 } 1094 else { 1095 const message = error?.error_message ? error.error_message : ts('Unknown error'); 1096 CRM.alert(message, ts('There is a problem'), 'error'); 1097 } 1098 }; 1099 1100 this.submit = function () { 1101 if (!ctrl.ngForm.$valid || !validateFileFields()) { 1102 const firstInvalidInput = $element[0].closest('af-form').querySelector('.ng-invalid'); 1103 const isRequired = firstInvalidInput.classList.contains('ng-invalid-required'); 1104 const message = isRequired ? ts('Please fill all required fields.') : ts('Please check all answers are valid.'); 1105 1106 if (document.getElementById('crm-notification-container')) { 1107 CRM.alert(message, ts('Form Error')); 1108 } 1109 else { 1110 if (firstInvalidInput.classList.contains('select2-container')) { 1111 firstInvalidInput.scrollIntoView(); 1112 firstInvalidInput.focus(); 1113 CRM.alert(message, ts('Form Error')); 1114 } 1115 } 1116 return; 1117 } 1118 $element.block(); 1119 if (cancelDraftWatcher) { 1120 cancelDraftWatcher(); 1121 } 1122 1123 const submitApi = crmApi4('Afform', 'submit', { 1124 name: ctrl.getFormMeta().name, 1125 args: args, 1126 values: data, 1127 }).then((response) => { 1128 submissionResponse = response; 1129 if (ctrl.fileUploader.getNotUploadedItems().length) { 1130 _.each(ctrl.fileUploader.getNotUploadedItems(), function(file) { 1131 file.formData.push({ 1132 params: JSON.stringify(_.extend({ 1133 token: response[0].token, 1134 name: ctrl.getFormMeta().name 1135 }, file.crmApiParams())) 1136 }); 1137 }); 1138 ctrl.fileUploader.uploadAll(); 1139 } 1140 else { 1141 postProcess(); 1142 } 1143 }) 1144 .catch((error) => { 1145 $element.unblock(); 1146 1147 handleError(error); 1148 1149 $element.trigger('crmFormError', { 1150 afform: ctrl.getFormMeta(), 1151 data: data, 1152 submissionResponse: submissionResponse, 1153 error: error 1154 }); 1155 }); 1156 crmStatus({ 1157 error: ts('Not Saved'), 1158 }, submitApi); 1159 }; 1160 1161 this.submitDraft = function() { 1162 if (uploadingDraftFiles) { 1163 return; 1164 } 1165 setDraftStatus('saving'); 1166 const submitDraft = crmApi4('Afform', 'submitDraft', { 1167 name: ctrl.getFormMeta().name, 1168 args: args, 1169 values: data, 1170 }).then((response) => { 1171 if (ctrl.fileUploader.getNotUploadedItems().length) { 1172 uploadingDraftFiles = true;
1173 ctrl.fileUploader.getNotUploadedItems().forEach((file) => { 1174 file.formData.push({ 1175 params: JSON.stringify(_.extend({ 1176 name: ctrl.getFormMeta().name 1177 }, file.crmApiParams())) 1178 }); 1179 }); 1180 ctrl.fileUploader.uploadAll(); 1181 } else { 1182 setDraftStatus('saved'); 1183 } 1184 }) 1185 .catch((error) => { 1186 setDraftStatus('unsaved'); 1187 handleError(error); 1188 }); 1189 crmStatus({ 1190 start: ts('Saving Draft'), 1191 success: ts('Draft Saved'), 1192 error: ts('Not Saved'), 1193 }, submitDraft); 1194 }; 1195 1196 function getDraftButtons() { 1197 return $element.find('button[ng-click="afform.submitDraft()"]'); 1198 } 1199 1200 function setDraftStatus(newStatus) { 1201 if (draftStatus === newStatus) { 1202 return; 1203 } 1204 if (draftStatus === 'unsaved' && newStatus === 'saved') { 1205 return; 1206 } 1207 if (newStatus === 'unsaved' && !uploadingDraftFiles) { 1208 restoreDraftButtons(); 1209 } 1210 else if (!uploadingDraftFiles) { 1211 const newText = newStatus === 'saving' ? ts('Saving Draft') : ts('Draft Saved'); 1212 const newIcon = newStatus === 'saving' ? 'fa-spinner fa-spin' : 'fa-check'; 1213 disableDraftButtons(newText, newIcon); 1214 } 1215 draftStatus = newStatus; 1216 } 1217 1218 function disableDraftButtons(text, icon) { 1219 const buttons = getDraftButtons(); 1220 $.each(buttons, function(index, button) { 1221 $(button).text(text).attr('disabled', true); 1222 $(button).prepend('<i class="crm-i ' + icon + '" role="img" aria-hidden="true"></i> '); 1223 }); 1224 } 1225 1226 function restoreDraftButtons() { 1227 const buttons = getDraftButtons(); 1228 $.each(buttons, function(index, button) { 1229 const initialState = saveDraftButtons[index] || saveDraftButtons[0]; 1230 $(button).text(initialState.text).attr('disabled', false); 1231 if (initialState.icon) { 1232 $(button).prepend('<i class="crm-i ' + saveDraftButtons[index].icon + '" role="img" aria-hidden="true"></i> '); 1233 } 1234 }); 1235 } 1236 this.getFieldData = () => { 1237 return data.extra.fields; 1238 }; 1239 this.identifyTokens = (message) => { 1240 if (typeof message !== 'string') { 1241 return null; 1242 } 1243 const tokens = new Set(message.match(/\[[a-zA-Z0-9_]+\.[0-9]+\.[^\]]+\]/g)); 1244 1245 return tokens.size ? tokens : null; 1246 }; 1247 1248 this.getTokenValues = (tokens) => { 1249 const values = {}; 1250 1251 tokens.forEach((token) => { 1252 const parts = token.slice(1, -1).split('.'); 1253 const entity = parts[0]; 1254 const index = parts[1]; 1255 const fieldName = parts.slice(2).join('.'); 1256 if (!data || !data[entity] || !data[entity][index] || !data[entity][index].fields || (data[entity][index].fields[fieldName] === undefined) ) { 1257 values[token] = ''; 1258 } 1259 else { 1260 values[token] = data[entity][index].fields[fieldName]; 1261 } 1262 }); 1263 1264 return values; 1265 }; 1266 1267 this.replaceTokens = (message) => { 1268 const tokens = this.identifyTokens(message); 1269 const tokenValues = this.getTokenValues(tokens); 1270 tokens.forEach((token) => message = message.replaceAll(token, tokenValues[token])); 1271 return message; 1272 }; 1273 } 1274 }); 1275 1276angular.module('af').directive('afIf', function($compile, $animate, $parse) { 1277 return { 1278 multiElement: true, 1279 transclude: 'element', 1280 priority: 601, 1281 terminal: true, 1282 restrict: 'A', 1283 require: ['^^afForm'], 1284 $$tlb: true, 1285 link: function($scope, $element, $attr, ctrl, $transclude) { 1286 let block, childScope, previousElements; 1287 1288 function watcher() { 1289 const conditions = $parse($attr.afIf)(); 1290 return ctrl[0].checkConditions(conditions); 1291 } 1292 1293 $scope.$watch(watcher, function(value) { 1294 if (value) { 1295 if (!childScope) { 1296 $transclude(function(clone, newScope) { 1297 childScope = newScope; 1298 clone[clone.length++] = $compile.$$createComment('end afIf', $attr.afIf); 1299 block = { 1300 clone: clone 1301 }; 1302 $animate.enter(clone, $element.parent(), $element); 1303 }); 1304 } 1305 } else { 1306 if (previousElements) { 1307 previousElements.remove(); 1308 previousElements = null; 1309 } 1310 if (childScope) { 1311 childScope.$broadcast('afIfDestroy'); 1312 childScope.$destroy(); 1313 childScope = null; 1314 } 1315 if (block) { 1316 previousElements = getBlockNodes(block.clone); 1317 $animate.leave(previousElements).done(function(response) { 1318 if (response !== false) previousElements = null; 1319 }); 1320 block = null; 1321 } 1322 } 1323 }); 1324 } 1325 }; 1326 }); 1327 1328 /** 1329 * Return the DOM siblings between the first and last node in the given array. 1330 * @param {Array} array like object 1331 * @returns {Array} the inputted object or a jqLite collection containing the nodes 1332 */
1333 function getBlockNodes(nodes) { 1334 var node = nodes[0]; 1335 var endNode = nodes[nodes.length - 1]; 1336 var blockNodes; 1337 1338 for (var i = 1; node !== endNode && (node = node.nextSibling); i++) { 1339 if (blockNodes || nodes[i] !== node) { 1340 if (!blockNodes) { 1341 blockNodes = $(slice.call(nodes, 0, i)); 1342 } 1343 blockNodes.push(node); 1344 } 1345 } 1346 1347 return blockNodes || nodes; 1348 } 1349 1350 1351angular.module('af') 1352 .directive('afJoin', function() { 1353 return { 1354 restrict: 'A', 1355 require: ['afJoin', '^^afFieldset', '?^^afRepeatItem'], 1356 bindToController: { 1357 entity: '@afJoin', 1358 }, 1359 link: function($scope, $el, $attr, ctrls) { 1360 const self = ctrls[0]; 1361 self.afFieldset = ctrls[1]; 1362 self.repeatItem = ctrls[2]; 1363 self.offset = self.afFieldset.getJoinOffset($attr.afJoin); 1364 }, 1365 controller: function($scope) { 1366 const self = this; 1367 this.getEntityType = function() { 1368 return this.entity; 1369 }; 1370 this.getData = function() { 1371 let data, fieldsetData; 1372 if (self.repeatItem) { 1373 data = self.repeatItem.item; 1374 } else { 1375 fieldsetData = self.afFieldset.getData(); 1376 if (!fieldsetData.length) { 1377 fieldsetData.push({fields: {}, joins: {}}); 1378 } 1379 data = fieldsetData[0]; 1380 } 1381 if (!data.joins) { 1382 data.joins = {}; 1383 } 1384 if (!data.joins[self.entity]) { 1385 data.joins[self.entity] = []; 1386 } 1387 return data.joins[self.entity]; 1388 }; 1389 this.getFieldData = function() { 1390 const data = this.getData(); 1391 if (!data[this.offset] || (Array.isArray(data[this.offset]) && !data[this.offset].length)) { 1392 data[this.offset] = {}; 1393 } 1394 return data[this.offset]; 1395 }; 1396 } 1397 }; 1398 }); 1399})(angular, CRM.$, CRM._); 1400 1401(function (CRM, angular) { 1402 1403 1404 class AfMarkup extends HTMLElement { 1405 1406 /* jshint ignore:start */ 1407 static observedAttributes = ['markup']; 1408 /* jshint ignore:end */ 1409 1410 connectedCallback() { 1411 this.afForm = this.closest('af-form'); 1412 setTimeout(() => this.render()); 1413 } 1414 1415 attributeChangedCallback() { 1416 this.render(); 1417 } 1418 1419 render() { 1420 let markup = this.markup; 1421 if (this.afForm) { 1422 markup = this.replaceTokensWithElements(markup); 1423 } 1424 this.innerHTML = markup; 1425 } 1426 1427 get markup() { 1428 return this.getAttribute('markup'); 1429 } 1430 1431 set markup(content) { 1432 this.setAttribute('markup', content); 1433 } 1434 1435 replaceTokensWithElements(markup) { 1436 const tokens = new Set(markup.match(/\[[a-zA-Z0-9_]+\.[0-9]+\.[^\]]+\]/g)); 1437 tokens?.forEach((token) => markup = markup.replaceAll(token, `<af-token expression="${token}"></af-token>`)); 1438 return markup; 1439 } 1440 } 1441 1442 customElements.define('af-markup', AfMarkup); 1443 1444})(CRM, angular); 1445 1446(function(angular, $, _) { 1447 angular.module('af') 1448 .directive('afRepeat', function() { 1449 return { 1450 restrict: 'A', 1451 require: ['?afFieldset', '?afJoin'], 1452 transclude: true, 1453 scope: { 1454 min: '=', 1455 max: '=', 1456 addLabel: '@afRepeat', 1457 addIcon: '@', 1458 copyLabel: '@afCopy', 1459 copyIcon: '@' 1460 }, 1461 templateUrl: '~/af/afRepeat.html', 1462 link: function($scope, $el, $attr, ctrls) { 1463 $scope.afFieldset = ctrls[0]; 1464 $scope.afJoin = ctrls[1]; 1465 $scope.element = $el; 1466 }, 1467 controller: function($scope) { 1468 this.getItems = $scope.getItems = function() { 1469 const data = getEntityController().getData(); 1470 while ($scope.min && data.length < $scope.min) { 1471 data.push(getRepeatType() === 'join' ? {} : {fields: {}, joins: {}}); 1472 } 1473 return data; 1474 }; 1475 1476 function getRepeatType() { 1477 return $scope.afJoin ? 'join' : 'fieldset'; 1478 } 1479 this.getRepeatType = getRepeatType; 1480 1481 function getEntityController() { 1482 return $scope.afJoin || $scope.afFieldset; 1483 } 1484 this.getEntityController = getEntityController; 1485 1486 $scope.addItem = function() { 1487 $scope.getItems().push(getRepeatType() === 'join' ? {} : {fields: {}}); 1488 }; 1489 1490 $scope.copyItem = function() { 1491 const data = $scope.getItems(); 1492 const last = data[data.length - 1]; 1493 data.push(getRepeatType() === 'join' ? angular.copy(last) : {fields: angular.copy(last.fields)}); 1494 }; 1495 1496 $scope.removeItem = function(index) { 1497 $scope.getItems().splice(index, 1); 1498 }; 1499 1500 $scope.canAdd = function() { 1501 return !$scope.max || $scope.getItems().length < $scope.max; 1502 }; 1503 1504 $scope.canRemove = function() { 1505 return !$scope.min || $scope.getItems().length > $scope.min; 1506 }; 1507 } 1508 }; 1509 }) 1510 .directive('afRepeatItem', function() { 1511 return { 1512 restrict: 'A', 1513 require: { 1514 afRepeat: '^^', 1515 outerRepeatItem: '?^^afRepeatItem' 1516 }, 1517 bindToController: { 1518 item: '=afRepeatItem', 1519 repeatIndex: '=' 1520 }, 1521 controller: function() { 1522 this.getFieldData = function() { 1523 return this.afRepeat.getRepeatType() === 'join' ? this.item : this.item.fields; 1524 }; 1525 1526 this.getEntityType = function() { 1527 return this.afRepeat.getEntityController().getEntityType(); 1528 }; 1529 } 1530 }; 1531 }); 1532 1533 angular.module('af').component('afSearchParamSets', { 1534 require: { 1535 afFieldset: '^^' 1536 }, 1537 templateUrl: '~/af/afSearchParamSets.html', 1538 controller: function($scope, $element, crmApi4, $window, $location) { 1539 const ts = $scope.ts = CRM.ts('org.civicrm.afform'); 1540 this.savedSets = []; 1541 1542 this.$onInit = () => { 1543 this.formName = this.afFieldset.getFormName(); 1544 this.saveDialog.reset(); 1545 1546 this.fetchSearchParamSets(); 1547 }; 1548 1549 this.fetchSearchParamSets = () => crmApi4('SearchParamSet', 'get', { 1550 select: ['label', 'filters', 'columns', 'created_by.display_name', 'created_date'], 1551 where: [['afform_name', '=', this.formName]], 1552 orderBy: {'label': 'ASC'} 1553 }) 1554 .then((results) => this.savedSets = results.map((paramSet) => { 1555 const created = new Date(paramSet.created_date); 1556 const createdDate = created.toLocaleDateString(); 1557 const isToday = createdDate === (new Date()).toLocaleDateString(); 1558 paramSet.created_date_or_time = isToday ? ts('%1 today', {1: created.toLocaleTimeString()}) : createdDate; 1559 return paramSet; 1560 })) 1561 .catch(() => this.savedSets = [{id: '', label: ts('Error fetching Saved Searches')}]); 1562 1563 this.applySearchParamSet = (id) => $window.location.hash = `#?_s=${id ? id : 0}`; 1564
1565 this.getSetSearchParamSet = (value) => { 1566 if (value !== undefined) { 1567 this.applySearchParamSet(value); 1568 return ''; 1569 } 1570 const selected = this.getSelectedParamSet(); 1571 return (selected && selected.id) ? `${selected.id}` : ''; 1572 }; 1573 1574 this.getSelectedParamSet = () => { 1575 const hashParams = new URLSearchParams($window.location.hash.slice(1)); 1576 const urlValue = parseInt(hashParams.get('_s')); 1577 return this.savedSets.find((searchParamSet) => searchParamSet.id === urlValue); 1578 }; 1579 1580 this.getCurrentParams = () => { 1581 if (!this.afFieldset) { 1582 return {}; 1583 } 1584 const params = {}; 1585 1586 params.filters = this.afFieldset.getFilterValues(); 1587 1588 params.columns = {}; 1589 $element.closest('[af-fieldset]').find('crm-search-display-table').each((i, $table) => { 1590 const tableCtrl = angular.element($table).controller('crmSearchDisplayTable'); 1591 if (!tableCtrl) { 1592 return; 1593 } 1594 const columns = tableCtrl.getToggledColumns(); 1595 if (!Object.keys(columns).length) { 1596 return; 1597 } 1598 params.columns[tableCtrl.getSearchDisplayKey()] = columns; 1599 }); 1600 return params; 1601 }; 1602 1603 this.renderSearchParamSetDescription = (paramSet) => ts('Created %1 by %2', { 1604 1: paramSet.created_date_or_time, 1605 2: (paramSet['created_by.display_name'] ? paramSet['created_by.display_name'] : 'UNKNOWN') 1606 }); 1607 1608 this.renderSearchParamSetDetails = (paramSet) => { 1609 const rendered = {}; 1610 1611 if (paramSet.filters) { 1612 const fieldMeta = this.afFieldset.getFieldMeta(); 1613 1614 Object.keys(paramSet.filters).forEach((key) => { 1615 const defn = fieldMeta[key]; 1616 const rawValue = paramSet.filters[key]; 1617 1618 const formatValue = (v) => { 1619 if (v && v.map) { 1620 return v.map(formatValue).join(' OR '); 1621 } 1622 if (defn.options && defn.options.length) { 1623 const selected = defn.options.find((o) => o.id == v); 1624 if (selected) { 1625 return selected.label; 1626 } 1627 } 1628 switch (typeof v) { 1629 case 'number': 1630 case 'string': 1631 return v; 1632 } 1633 return Object.entries(v) 1634 .filter((e) => e[1]) 1635 .map((e) => `${e[0]} ${e[1]}`) 1636 .join(' AND '); 1637 }; 1638 rendered[defn.label] = formatValue(rawValue); 1639 }); 1640 } 1641 1642 if (paramSet.columns) { 1643 const displayKeys = Object.keys(paramSet.columns); 1644 if (displayKeys.length > 1) 1645 displayKeys.forEach((displayKey) => { 1646 const label = ts('%1 columns', {1: displayKey}); 1647 const columns = Object.values(paramSet.columns[displayKey]).join(', '); 1648 rendered[label] = columns; 1649 }); 1650 else if (displayKeys.length === 1) { 1651 const displayKey = displayKeys[0]; 1652 rendered[ts('Columns')] = Object.values(paramSet.columns[displayKey]).join(', '); 1653 } 1654 } 1655 1656 return rendered; 1657 }; 1658 1659 this.saveDialog = { 1660 open: () => $element[0].querySelector('dialog.af-search-param-set-new').showModal(), 1661 close: () => $element[0].querySelector('dialog.af-search-param-set-new').close(), 1662 reset: () => { 1663 this.saveDialog.label = ''; 1664 this.saveDialog.inProgress = false; 1665 }, 1666 canOpen: () => { 1667 const params = this.getCurrentParams(); 1668 if (Object.keys(params.filters).length) { 1669 return true; 1670 } 1671 if (Object.keys(params.columns).length) { 1672 return true; 1673 } 1674 return false; 1675 }, 1676 canSave: () => !this.saveDialog.inProgress && this.saveDialog.label && Object.keys(this.getCurrentParams()).length, 1677 save: () => { 1678 if (!this.saveDialog.canSave()) { 1679 return; 1680 } 1681 1682 this.saveDialog.inProgress = true; 1683
1684 const current = this.getCurrentParams(); 1685 1686 const values = { 1687 afform_name: this.formName, 1688 filters: current.filters, 1689 columns: current.columns, 1690 label: this.saveDialog.label, 1691 created_by: CRM.config.cid, 1692 }; 1693 1694 let newId = null; 1695 1696 return crmApi4('SearchParamSet', 'create', { 1697 values: values 1698 }) 1699 .then((result) => newId = (result && result[0]) ? result[0].id : null) 1700 .then(() => this.fetchSearchParamSets()) 1701 .then(() => this.applySearchParamSet(newId)) 1702 .then(() => this.saveDialog.close()) 1703 .catch((error) => { 1704 const errorMessage = (error && error.error_message) ? error.error_message : ts('Unknown error'); 1705 CRM.alert(ts('Error saving filters: %1', {1: errorMessage})); 1706 }) 1707 .finally(() => this.saveDialog.reset()); 1708 } 1709 }; 1710 1711 this.manageDialog = { 1712 open: () => $element[0].querySelector('dialog.af-search-param-sets-manage').showModal(), 1713 close: () => $element[0].querySelector('dialog.af-search-param-sets-manage').close(), 1714 deleteItem: (searchParamSet) => { 1715 crmApi4('SearchParamSet', 'delete', { 1716 where: [['id', '=', searchParamSet.id]] 1717 }) 1718 .then(() => this.fetchSearchParamSets()); 1719 }, 1720 }; 1721 1722 this.updateDialog = { 1723 id: null, 1724 label: '', 1725 open: () => { 1726 const selected = this.getSelectedParamSet(); 1727 this.updateDialog.id = selected.id; 1728 this.updateDialog.oldLabel = selected.label; 1729 this.updateDialog.newLabel = selected.label; 1730 this.updateDialog.valueComparison = this.updateDialog.getValueComparison(); 1731 $element[0].querySelector('dialog.af-search-param-set-update').showModal(); 1732 }, 1733 close: () => $element[0].querySelector('dialog.af-search-param-set-update').close(), 1734 inProgress: false, 1735 canOpen: () => { 1736 const selected = this.getSelectedParamSet(); 1737 if (!selected) { 1738 return false; 1739 } 1740 const newParams = this.getCurrentParams(); 1741 if (JSON.stringify(selected.filters) !== JSON.stringify(newParams.filters)) { 1742 return true; 1743 } 1744 if (JSON.stringify(selected.columns) !== JSON.stringify(newParams.columns)) { 1745 return true; 1746 } 1747 return false; 1748 }, 1749 canUpdate: () => this.updateDialog.id && this.updateDialog.newLabel, 1750 update: () => { 1751 if (!this.updateDialog.canUpdate()) { 1752 return; 1753 } 1754 1755 this.updateDialog.inProgress = true; 1756 1757 const newParams = this.getCurrentParams(); 1758 1759 crmApi4('SearchParamSet', 'update', { 1760 where: [['id', '=', this.updateDialog.id]], 1761 values: { 1762 label: this.updateDialog.newLabel, 1763 filters: newParams.filters, 1764 columns: newParams.columns 1765 } 1766 }) 1767 .then(() => this.fetchSearchParamSets()) 1768 .then(() => this.updateDialog.inProgress = false) 1769 .then(() => this.updateDialog.close()); 1770 }, 1771 getValueComparison: () => { 1772 const current = this.getSelectedParamSet(); 1773 const oldValues = this.renderSearchParamSetDetails(current); 1774 const newValues = this.renderSearchParamSetDetails(this.getCurrentParams()); 1775 1776 const oldKeys = Object.keys(oldValues); 1777 const newKeys = Object.keys(newValues); 1778 1779 const comparison = {}; 1780 oldKeys.filter((key) => !newKeys.includes(key)).forEach((key) => comparison[key] = [oldValues[key], ts('[none]')]); 1781 oldKeys.filter((key) => newKeys.includes(key)).forEach((key) => { 1782 if (newValues[key] === oldValues[key]) { 1783 comparison[key] = [oldValues[key]]; 1784 return; 1785 } 1786 comparison[key] = [oldValues[key], newValues[key]]; 1787 }); 1788 newKeys.filter((key) => !oldKeys.includes(key)).forEach((key) => comparison[key] = [ts('[none]'), newValues[key]]); 1789 1790 return comparison; 1791 } 1792 }; 1793 } 1794 }); 1795 1796 1797 angular.module('af').component('afTabset', { 1798 templateUrl: '~/af/afTabset.html', 1799 transclude: true, 1800 controller: function($scope, $element) { 1801 this.tabs = []; 1802 1803 this.$onInit = function() { 1804 $element.addClass('crm-tabset'); 1805 }; 1806 1807 this.addTab = function(tab) { 1808 tab.tabSelected = !this.tabs.length; 1809 this.tabs.push(tab); 1810 }; 1811 1812 this.selectTab = function(tabIndex) {
1813 this.tabs.forEach(function(tab, index) { 1814 tab.tabSelected = index === tabIndex; 1815 }); 1816 }; 1817 } 1818 1819 }); 1820 1821 angular.module('af').directive('afTab', function() { 1822 return { 1823 restrict: 'E', 1824 require: '^afTabset', 1825 scope: { 1826 title: '@', 1827 icon: '@', 1828 count: '@', 1829 }, 1830 transclude: true, 1831 template: '<div ng-transclude role="tabpanel" ng-show="tabSelected" class="ng-hide"></div>', 1832 link: function (scope, element, attrs, afTabsetCtrl) { 1833 afTabsetCtrl.addTab(scope); 1834 } 1835 }; 1836 }); 1837 1838 angular.module('af').directive('afTitle', function() { 1839 return { 1840 restrict: 'A', 1841 bindToController: { 1842 title: '@afTitle' 1843 }, 1844 controller: function($scope, $element) { 1845 1846 $scope.$watch(() => this.title, (text) => { 1847 let tag = 'h4'; 1848 if ($element.is('fieldset')) { 1849 tag = 'legend'; 1850 } 1851 if ($element.is('details')) { 1852 tag = 'summary'; 1853 } 1854 let $title = $element.children(tag + '.af-title'); 1855 if (!$title.length) { 1856 $title = $(`<${tag} class="af-title" />`).prependTo($element); 1857 } 1858 $title.text(text); 1859 }); 1860 } 1861 }; 1862 }); 1863})(angular, CRM.$, CRM._); 1864 1865(function (CRM, angular) { 1866 1867 1868 class AfToken extends HTMLElement { 1869 1870 connectedCallback() { 1871 this.afForm = this.closest('af-form'); 1872 if (!this.afForm) { 1873 throw new Error('af-token should be placed within an af-form'); 1874 } 1875 1876 this.render(); 1877 this.registerListener(); 1878 } 1879 1880 disconnectedCallback() { 1881 this.removeListener(); 1882 } 1883 1884 render() { 1885 this.innerText = this.evaluate(this.expression); 1886 } 1887 1888 registerListener() { 1889 this.afForm?.addEventListener('change', () => this.render()); 1890 } 1891 1892 removeListener() { 1893 this.afForm?.removeEventListener('change', () => this.render()); 1894 } 1895 1896 get expression() { 1897 return this.getAttribute('expression'); 1898 } 1899 1900 get afFormCtrl() { 1901 return angular.element(this.afForm).controller('afForm'); 1902 } 1903 1904 evaluate(expression) { 1905 return this.afFormCtrl ? this.afFormCtrl.replaceTokens(expression) : ''; 1906 } 1907 1908 } 1909 1910 customElements.define('af-token', AfToken); 1911 1912})(CRM, angular); 1913(function(angular, $, _) { 1914 "use strict"; 1915 angular.module('af').component('afLocationInput', { 1916 require: { 1917 ngModel: 'ngModel', 1918 }, 1919 templateUrl: '~/af/fields/afLocationInput.html', 1920 controller: function($scope, $element) { 1921 const ts = $scope.ts = CRM.ts('org.civicrm.afform'); 1922 1923 this.values = { 1924 distance: null, 1925 distance_unit: null, 1926 address: '', 1927 }; 1928 1929 this.$onInit = () => { 1930 $scope.$watchCollection('$ctrl.values', (values) => { 1931 if (values.address && values.distance_unit && values.distance !== null) { 1932 this.ngModel.$setViewValue(values); 1933 } else { 1934 this.ngModel.$setViewValue(null); 1935 } 1936 }); 1937 this.ngModel.$render = () => { 1938 this.values = this.ngModel.$viewValue || this.values; 1939 }; 1940 1941 }; 1942 1943 } 1944 }); 1945 1946angular.module('afCore', CRM.angRequires('afCore')); 1947 angular.module('afCore').service('afCoreDirective', function($location, crmApi4, crmStatus, crmUiAlert) { 1948 return function(camelName, meta, d) { 1949 d.restrict = 'E'; 1950 d.scope = {}; 1951 d.scope.options = '='; 1952 d.link = { 1953 pre: function($scope, $el, $attr) { 1954 $scope.ts = CRM.ts(camelName); 1955 $scope.meta = meta; 1956 $scope.crmApi4 = crmApi4; 1957 $scope.crmStatus = crmStatus; 1958 $scope.crmUiAlert = crmUiAlert; 1959 $scope.crmUrl = CRM.url; 1960 1961 $el.addClass('afform-directive'); 1962 const dialog = $el.closest('.ui-dialog-content'); 1963 if (!dialog.length) { 1964 $scope.routeParams = $location.search(); 1965 $scope.$watch(() => $location.search(), params => $scope.routeParams = params); 1966 } else { 1967 $scope.routeParams = {}; 1968 if (typeof dialog.data('urlHash') === 'string' && dialog.data('urlHash').includes('?')) { 1969 const searchParams = new URLSearchParams(dialog.data('urlHash').split('?')[1]);
1970 searchParams.forEach((value, key) => $scope.routeParams[key] = value); 1971 } 1972 } 1973 1974 $scope.$parent.afformTitle = meta.title; 1975 $scope.addTitle = function(addition) { 1976 $scope.$parent.afformTitle = addition + ' ' + meta.title; 1977 }; 1978 1979 $scope.checkLinkPerm = function(permissionName, createdId) { 1980 if (permissionName.startsWith('manage own') && CRM.config.cid !== createdId) { 1981 permissionName = permissionName.replace('manage own', 'administer'); 1982 } 1983 return CRM.checkPerm(permissionName); 1984 }; 1985 1986 } 1987 }; 1988 return d; 1989 }; 1990 }); 1991 1992angular.module('afCore').directive('afApi3Ctrl', function() { 1993 return { 1994 restrict: 'EA', 1995 scope: { 1996 afApi3Ctrl: '=', 1997 afApi3: '@', 1998 afApi3Refresh: '@', 1999 onRefresh: '@' 2000 }, 2001 controllerAs: 'afApi3Ctrl', 2002 controller: function($scope, $parse, crmThrottle, crmApi) { 2003 var ctrl = this; 2004 var parts = $parse($scope.afApi3)($scope.$parent); 2005 ctrl.entity = parts[0]; 2006 ctrl.action = parts[1]; 2007 ctrl.params = parts[2]; 2008 ctrl.result = {}; 2009 ctrl.loading = ctrl.firstLoad = true; 2010 2011 ctrl.refresh = function refresh() { 2012 ctrl.loading = true; 2013 crmThrottle(function () { 2014 return crmApi(ctrl.entity, ctrl.action, ctrl.params) 2015 .then(function (response) { 2016 ctrl.result = response; 2017 ctrl.loading = ctrl.firstLoad = false; 2018 if ($scope.onRefresh) { 2019 $scope.$parent.$eval($scope.onRefresh, ctrl); 2020 } 2021 }); 2022 }); 2023 }; 2024 2025 $scope.afApi3Ctrl = this; 2026 2027 var mode = $scope.afApi3Refresh ? $scope.afApi3Refresh : 'auto'; 2028 switch (mode) { 2029 case 'auto': $scope.$watchCollection('afApi3Ctrl.params', ctrl.refresh); break; 2030 case 'init': ctrl.refresh(); break; 2031 case 'manual': break; 2032 default: throw 'Unrecognized refresh mode: '+ mode; 2033 } 2034 } 2035 }; 2036 }); 2037 2038 2039angular.module('afCore').directive('afApi4Action', function($parse, crmStatus, crmApi4) { 2040 return { 2041 restrict: 'A', 2042 scope: { 2043 afApi4Action: '@', 2044 afApi4StartMsg: '=', 2045 afApi4ErrorMsg: '=', 2046 afApi4SuccessMsg: '=', 2047 afApi4Success: '@', 2048 onError: '@' 2049 }, 2050 link: function($scope, $el, $attr) { 2051 const ts = CRM.ts(null); 2052 2053 function running(x) {$el.toggleClass('af-api4-action-running', x).toggleClass('af-api4-action-idle', !x);} 2054 2055 running(false); 2056 2057 $el.click(function(){ 2058 const parts = $parse($scope.afApi4Action)($scope.$parent); 2059 const msgs = { 2060 start: $scope.afApi4StartMsg || ts('Submitting...'), 2061 success: $scope.afApi4SuccessMsg, 2062 error: $scope.afApi4ErrorMsg 2063 }; 2064 running(true); 2065 crmStatus(msgs, crmApi4(parts[0], parts[1], parts[2])) 2066 .finally(function(){running(false);}) 2067 .then(function(response){$scope.$parent.$eval($scope.afApi4Success, {response: response});}) 2068 .catch(function(error){$scope.$parent.$eval($scope.onError, {error: error});}); 2069 }); 2070 } 2071 }; 2072 }); 2073 2074 2075angular.module('afCore').directive('afApi4Ctrl', function() { 2076 return { 2077 restrict: 'EA', 2078 scope: { 2079 afApi4Ctrl: '=', 2080 afApi4: '@', 2081 afApi4Refresh: '@', 2082 onRefresh: '@' 2083 }, 2084 controllerAs: 'afApi4Ctrl', 2085 controller: function($scope, $parse, crmThrottle, crmApi4) { 2086 var ctrl = this; 2087 var parts = $parse($scope.afApi4)($scope.$parent); 2088 ctrl.entity = parts[0]; 2089 ctrl.action = parts[1]; 2090 ctrl.params = parts[2]; 2091 ctrl.index = parts[3]; 2092 ctrl.result = {}; 2093 ctrl.loading = ctrl.firstLoad = true; 2094 2095 ctrl.refresh = function refresh() { 2096 ctrl.loading = true; 2097 crmThrottle(function () { 2098 return crmApi4(ctrl.entity, ctrl.action, ctrl.params, ctrl.index) 2099 .then(function (response) { 2100 ctrl.result = response; 2101 ctrl.loading = ctrl.firstLoad = false;
2102 if ($scope.onRefresh) { 2103 $scope.$parent.$eval($scope.onRefresh, ctrl); 2104 } 2105 }); 2106 }); 2107 }; 2108 2109 $scope.afApi4Ctrl = this; 2110 2111 var mode = $scope.afApi4Refresh ? $scope.afApi4Refresh : 'auto'; 2112 switch (mode) { 2113 case 'auto': 2114 $scope.$watchCollection('afApi4Ctrl.params', ctrl.refresh, true); 2115 $scope.$watch('afApi4Ctrl.index', ctrl.refresh, true); 2116 $scope.$watch('afApi4Ctrl.entity', ctrl.refresh, true); 2117 $scope.$watch('afApi4Ctrl.action', ctrl.refresh, true); 2118 break; 2119 case 'init': ctrl.refresh(); break; 2120 case 'manual': break; 2121 default: throw 'Unrecognized refresh mode: '+ mode; 2122 } 2123 } 2124 }; 2125 }); 2126 2127 2128angular.module('afformHomepageSubscriptionForm', CRM.angRequires('afformHomepageSubscriptionForm')); 2129 angular.module('afformHomepageSubscriptionForm').directive('afformHomepageSubscriptionForm', function(afCoreDirective) { 2130 return afCoreDirective("afformHomepageSubscriptionForm", {"title":"Homepage Subscription Form","redirect":null,"autosave_draft":false,"confirmation_type":"redirect_to_url","confirmation_message":null,"name":"afformHomepageSubscriptionForm"}, { 2131 templateUrl: "~/afformHomepageSubscriptionForm/afformHomepageSubscriptionForm.aff.html" 2132 }); 2133 }); 2134 2135angular.module('afformHomepageSubscriptionHeaderAndFooter', CRM.angRequires('afformHomepageSubscriptionHeaderAndFooter')); 2136 angular.module('afformHomepageSubscriptionHeaderAndFooter').directive('afformHomepageSubscriptionHeaderAndFooter', function(afCoreDirective) { 2137 return afCoreDirective("afformHomepageSubscriptionHeaderAndFooter", {"title":"Homepage Subscription Header and Footer","redirect":null,"name":"afformHomepageSubscriptionHeaderAndFooter","autosave_draft":null,"confirmation_type":"redirect_to_url","confirmation_message":null}, { 2138 templateUrl: "~/afformHomepageSubscriptionHeaderAndFooter/afformHomepageSubscriptionHeaderAndFooter.aff.html" 2139 }); 2140 }); 2141})(angular, CRM.$, CRM._); 2142 2143
vendor: 23,361 bytes, lines 2144-2149
2144/* 2145 angular-file-upload v2.6.1 2146 https://github.com/nervgh/angular-file-upload 2147*/ 2148 2149!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["angular-file-upload"]=t():e["angular-file-upload"]=t()}(this,function(){return function(e){function t(n){if(o[n])return o[n].exports;var r=o[n]={exports:{},id:n,loaded:!1};return e[n].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var o={};return t.m=e,t.c=o,t.p="",t(0)}([function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}var r=o(1),i=n(r),s=o(2),a=n(s),u=o(3),l=n(u),p=o(4),c=n(p),f=o(5),d=n(f),h=o(6),y=n(h),m=o(7),v=n(m),_=o(8),g=n(_),b=o(9),F=n(b),O=o(10),C=n(O),T=o(11),I=n(T),w=o(12),A=n(w),U=o(13),x=n(U);angular.module(i.default.name,[]).value("fileUploaderOptions",a.default).factory("FileUploader",l.default).factory("FileLikeObject",c.default).factory("FileItem",d.default).factory("FileDirective",y.default).factory("FileSelect",v.default).factory("FileDrop",F.default).factory("FileOver",C.default).factory("Pipeline",g.default).directive("nvFileSelect",I.default).directive("nvFileDrop",A.default).directive("nvFileOver",x.default).run(["FileUploader","FileLikeObject","FileItem","FileDirective","FileSelect","FileDrop","FileOver","Pipeline",function(e,t,o,n,r,i,s,a){e.FileLikeObject=t,e.FileItem=o,e.FileDirective=n,e.FileSelect=r,e.FileDrop=i,e.FileOver=s,e.Pipeline=a}])},function(e,t){e.exports={name:"angularFileUpload"}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={url:"/",alias:"file",headers:{},queue:[],progress:0,autoUpload:!1,removeAfterUpload:!1,method:"POST",filters:[],formData:[],queueLimit:Number.MAX_VALUE,withCredentials:!1,disableMultipart:!1}},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t,o,n,i,a,u,g){var b=n.File,F=n.FormData,O=function(){function n(t){r(this,n);var o=p(e);c(this,o,t,{isUploading:!1,_nextIndex:0,_directives:{select:[],drop:[],over:[]}}),this.filters.unshift({name:"queueLimit",fn:this._queueLimitFilter}),this.filters.unshift({name:"folder",fn:this._folderFilter})}return n.prototype.addToQueue=function(e,t,o){var n=this,r=this.isArrayLikeObject(e)?Array.prototype.slice.call(e):[e],i=this._getFilters(o),l=this.queue.length,p=[],c=function e(){var o=r.shift();if(v(o))return f();var l=n.isFile(o)?o:new a(o),c=n._convertFiltersToPipes(i),d=new g(c),h=function(t){var o=t.pipe.originalFilter,r=s(t.args,2),i=r[0],a=r[1];n._onWhenAddingFileFailed(i,o,a),e()},y=function(t,o){var r=new u(n,t,o);p.push(r),n.queue.push(r),n._onAfterAddingFile(r),e()};d.onThrown=h,d.onSuccessful=y,d.exec(l,t)},f=function(){n.queue.length!==l&&(n._onAfterAddingAll(p),n.progress=n._getTotalProgress()),n._render(),n.autoUpload&&n.uploadAll()};c()},n.prototype.removeFromQueue=function(e){var t=this.getIndexOfItem(e),o=this.queue[t];o.isUploading&&o.cancel(),this.queue.splice(t,1),o._destroy(),this.progress=this._getTotalProgress()},n.prototype.clearQueue=function(){for(;this.queue.length;)this.queue[0].remove();this.progress=0},n.prototype.uploadItem=function(e){var t=this.getIndexOfItem(e),o=this.queue[t],n=this.isHTML5?"_xhrTransport":"_iframeTransport";o._prepareToUploading(),this.isUploading||(this._onBeforeUploadItem(o),o.isCancel||(o.isUploading=!0,this.isUploading=!0,this[n](o),this._render()))},n.prototype.cancelItem=function(e){var t=this,o=this.getIndexOfItem(e),n=this.queue[o],r=this.isHTML5?"_xhr":"_form";if(n)if(n.isCancel=!0,n.isUploading)n[r].abort();else{var s=[void 0,0,{}],a=function(){t._onCancelItem.apply(t,[n].concat(s)),t._onCompleteItem.apply(t,[n].concat(s))};i(a)}},n.prototype.uploadAll=function(){var e=this.getNotUploadedItems().filter(function(e){return!e.isUploading});e.length&&(f(e,function(e){return e._prepareToUploading()}),e[0].upload())},n.prototype.cancelAll=function(){var e=this.getNotUploadedItems();f(e,function(e){return e.cancel()})},n.prototype.isFile=function(e){return this.constructor.isFile(e)},n.prototype.isFileLikeObject=function(e){return this.constructor.isFileLikeObject(e)},n.prototype.isArrayLikeObject=function(e){return this.constructor.isArrayLikeObject(e)},n.prototype.getIndexOfItem=function(e){return h(e)?e:this.queue.indexOf(e)},n.prototype.getNotUploadedItems=function(){return this.queue.filter(function(e){return!e.isUploaded})},n.prototype.getReadyItems=function(){return this.queue.filter(function(e){return e.isReady&&!e.isUploading}).sort(function(e,t){return e.index-t.index})},n.prototype.destroy=function(){var e=this;f(this._directives,function(t){f(e._directives[t],function(e){e.destroy()})})},n.prototype.onAfterAddingAll=function(e){},n.prototype.onAfterAddingFile=function(e){},n.prototype.onWhenAddingFileFailed=function(e,t,o){},n.prototype.onBeforeUploadItem=function(e){},n.prototype.onProgressItem=function(e,t){},n.prototype.onProgressAll=function(e){},n.prototype.onSuccessItem=function(e,t,o,n){},n.prototype.onErrorItem=function(e,t,o,n){},n.prototype.onCancelItem=function(e,t,o,n){},n.prototype.onCompleteItem=function(e,t,o,n){},n.prototype.onTimeoutItem=function(e){},n.prototype.onCompleteAll=function(){},n.prototype._getTotalProgress=function(e){if(this.removeAfterUpload)return e||0;var t=this.getNotUploadedItems().length,o=t?this.queue.length-t:this.queue.length,n=100/this.queue.length,r=(e||0)*n/100;return Math.round(o*n+r)},n.prototype._getFilters=function(e){if(!e)return this.filters;if(m(e))return e;var t=e.match(/[^\s,]+/g);return this.filters.filter(function(e){return t.indexOf(e.name)!==-1})},n.prototype._convertFiltersToPipes=function(e){var t=this;return e.map(function(e){var o=l(t,e.fn);return o.isAsync=3===e.fn.length,o.originalFilter=e,o})},n.prototype._render=function(){t.$$phase||t.$apply()},n.prototype._folderFilter=function(e){return!(!e.size&&!e.type)},n.prototype._queueLimitFilter=function(){return this.queue.length<this.queueLimit},n.prototype._isSuccessCode=function(e){return e>=200&&e<300||304===e},n.prototype._transformResponse=function(e,t){var n=this._headersGetter(t);return f(o.defaults.transformResponse,function(t){e=t(e,n)}),e},n.prototype._parseHeaders=function(e){var t,o,n,r={};return e?(f(e.split("\n"),function(e){n=e.indexOf(":"),t=e.slice(0,n).trim().toLowerCase(),o=e.slice(n+1).trim(),t&&(r[t]=r[t]?r[t]+", "+o:o)}),r):r},n.prototype._headersGetter=function(e){return function(t){return t?e[t.toLowerCase()]||null:e}},n.prototype._xhrTransport=function(e){var t,o=this,n=e._xhr=new XMLHttpRequest;if(e.disableMultipart?t=e._file:(t=new F,f(e.formData,function(e){f(e,function(e,o){t.append(o,e)})}),t.append(e.alias,e._file,e.file.name)),"number"!=typeof e._file.size)throw new TypeError("The file specified is no longer valid");n.upload.onprogress=function(t){var n=Math.round(t.lengthComputable?100*t.loaded/t.total:0);o._onProgressItem(e,n)},n.onload=function(){var t=o._parseHeaders(n.getAllResponseHeaders()),r=o._transformResponse(n.response,t),i=o._isSuccessCode(n.status)?"Success":"Error",s="_on"+i+"Item";o[s](e,r,n.status,t),o._onCompleteItem(e,r,n.status,t)},n.onerror=function(){var t=o._parseHeaders(n.getAllResponseHeaders()),r=o._transformResponse(n.response,t);o._onErrorItem(e,r,n.status,t),o._onCompleteItem(e,r,n.status,t)},n.onabort=function(){var t=o._parseHeaders(n.getAllResponseHeaders()),r=o._transformResponse(n.response,t);o._onCancelItem(e,r,n.status,t),o._onCompleteItem(e,r,n.status,t)},n.ontimeout=function(t){var r=o._parseHeaders(n.getAllResponseHeaders()),i="Request Timeout.";o._onTimeoutItem(e),o._onCompleteItem(e,i,408,r)},n.open(e.method,e.url,!0),n.timeout=e.timeout||0,n.withCredentials=e.withCredentials,f(e.headers,function(e,t){n.setRequestHeader(t,e)}),n.send(t)},n.prototype._iframeTransport=function(e){var t=this,o=_('<form style="display: none;" />'),n=_('<iframe name="iframeTransport'+Date.now()+'">'),r=e._input,i=0,s=null,a=!1;e._form&&e._form.replaceWith(r),e._form=o,r.prop("name",e.alias),f(e.formData,function(e){f(e,function(e,t){var n=_('<input type="hidden" name="'+t+'" />');n.val(e),o.append(n)})}),o.prop({action:e.url,method:"POST",target:n.prop("name"),enctype:"multipart/form-data",encoding:"multipart/form-data"}),n.bind("load",function(){var o="",r=200;try{o=n[0].contentDocument.body.innerHTML}catch(e){r=500}if(s&&clearTimeout(s),s=null,a)return!1;var i={response:o,status:r,dummy:!0},u={},l=t._transformResponse(i.response,u);t._onSuccessItem(e,l,i.status,u),t._onCompleteItem(e,l,i.status,u)}),o.abort=function(){var i,s={status:0,dummy:!0},a={};n.unbind("load").prop("src","javascript:false;"),o.replaceWith(r),t._onCancelItem(e,i,s.status,a),t._onCompleteItem(e,i,s.status,a)},r.after(o),o.append(r).append(n),i=e.timeout||0,s=null,i&&(s=setTimeout(function(){a=!0,e.isCancel=!0,e.isUploading&&(n.unbind("load").prop("src","javascript:false;"),o.replaceWith(r));var i={},s="Request Timeout.";t._onTimeoutItem(e),t._onCompleteItem(e,s,408,i)},i)),o[0].submit()},n.prototype._onWhenAddingFileFailed=function(e,t,o){this.onWhenAddingFileFailed(e,t,o)},n.prototype._onAfterAddingFile=function(e){this.onAfterAddingFile(e)},n.prototype._onAfterAddingAll=function(e){this.onAfterAddingAll(e)},n.prototype._onBeforeUploadItem=function(e){e._onBeforeUpload(),this.onBeforeUploadItem(e)},n.prototype._onProgressItem=function(e,t){var o=this._getTotalProgress(t);this.progress=o,e._onProgress(t),this.onProgressItem(e,t),this.onProgressAll(o),this._render()},n.prototype._onSuccessItem=function(e,t,o,n){e._onSuccess(t,o,n),this.onSuccessItem(e,t,o,n)},n.prototype._onErrorItem=function(e,t,o,n){e._onError(t,o,n),this.onErrorItem(e,t,o,n)},n.prototype._onCancelItem=function(e,t,o,n){e._onCancel(t,o,n),this.onCancelItem(e,t,o,n)},n.prototype._onCompleteItem=function(e,t,o,n){e._onComplete(t,o,n),this.onCompleteItem(e,t,o,n);var r=this.getReadyItems()[0];return this.isUploading=!1,y(r)?void r.upload():(this.onCompleteAll(),this.progress=this._getTotalProgress(),void this._render())},n.prototype._onTimeoutItem=function(e){e._onTimeout(),this.onTimeoutItem(e)},n.isFile=function(e){return b&&e instanceof b},n.isFileLikeObject=function(e){return e instanceof a},n.isArrayLikeObject=function(e){return d(e)&&"length"in e},n.inherit=function(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.super_=t},n}();return O.prototype.isHTML5=!(!b||!F),O.isHTML5=O.prototype.isHTML5,O}Object.defineProperty(t,"__esModule",{value:!0});var s=function(){function e(e,t){var o=[],n=!0,r=!1,i=void 0;try{for(var s,a=e[Symbol.iterator]();!(n=(s=a.next()).done)&&(o.push(s.value),!t||o.length!==t);n=!0);}catch(e){r=!0,i=e}finally{try{!n&&a.return&&a.return()}finally{if(r)throw i}}return o}return function(t,o){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,o);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}();t.default=i;var a=o(1),u=(n(a),angular),l=u.bind,p=u.copy,c=u.extend,f=u.forEach,d=u.isObject,h=u.isNumber,y=u.isDefined,m=u.isArray,v=u.isUndefined,_=u.element;i.$inject=["fileUploaderOptions","$rootScope","$http","$window","$timeout","FileLikeObject","FileItem","Pipeline"]},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(){return function(){function e(t){r(this,e);var o=l(t),n=o?t.value:t,i=p(n)?"FakePath":"Object",s="_createFrom"+i;this[s](n,t)}return e.prototype._createFromFakePath=function(e,t){this.lastModifiedDate=null,this.size=null,this.type="like/"+e.slice(e.lastIndexOf(".")+1).toLowerCase(),this.name=e.slice(e.lastIndexOf("/")+e.lastIndexOf("\\")+2),this.input=t},e.prototype._createFromObject=function(e){this.lastModifiedDate=u(e.lastModifiedDate),this.size=e.size,this.type=e.type,this.name=e.name,this.input=e.input},e}()}Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var s=o(1),a=(n(s),angular),u=a.copy,l=a.isElement,p=a.isString},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){return function(){function o(e,n,i){r(this,o);var s=!!n.input,a=s?p(n.input):null,c=s?null:n;l(this,{url:e.url,alias:e.alias,headers:u(e.headers),formData:u(e.formData),removeAfterUpload:e.removeAfterUpload,withCredentials:e.withCredentials,disableMultipart:e.disableMultipart,method:e.method,timeout:e.timeout},i,{uploader:e,file:new t(n),isReady:!1,isUploading:!1,isUploaded:!1,isSuccess:!1,isCancel:!1,isError:!1,progress:0,index:null,_file:c,_input:a}),a&&this._replaceNode(a)}return o.prototype.upload=function(){try{this.uploader.uploadItem(this)}catch(t){var e=t.name+":"+t.message;this.uploader._onCompleteItem(this,e,t.code,[]),this.uploader._onErrorItem(this,e,t.code,[])}},o.prototype.cancel=function(){this.uploader.cancelItem(this)},o.prototype.remove=function(){this.uploader.removeFromQueue(this)},o.prototype.onBeforeUpload=function(){},o.prototype.onProgress=function(e){},o.prototype.onSuccess=function(e,t,o){},o.prototype.onError=function(e,t,o){},o.prototype.onCancel=function(e,t,o){},o.prototype.onComplete=function(e,t,o){},o.prototype.onTimeout=function(){},o.prototype._onBeforeUpload=function(){this.isReady=!0,this.isUploading=!1,this.isUploaded=!1,this.isSuccess=!1,this.isCancel=!1,this.isError=!1,this.progress=0,this.onBeforeUpload()},o.prototype._onProgress=function(e){this.progress=e,this.onProgress(e)},o.prototype._onSuccess=function(e,t,o){this.isReady=!1,this.isUploading=!1,this.isUploaded=!0,this.isSuccess=!0,this.isCancel=!1,this.isError=!1,this.progress=100,this.index=null,this.onSuccess(e,t,o)},o.prototype._onError=function(e,t,o){this.isReady=!1,this.isUploading=!1,this.isUploaded=!0,this.isSuccess=!1,this.isCancel=!1,this.isError=!0,this.progress=0,this.index=null,this.onError(e,t,o)},o.prototype._onCancel=function(e,t,o){this.isReady=!1,this.isUploading=!1,this.isUploaded=!1,this.isSuccess=!1,this.isCancel=!0,this.isError=!1,this.progress=0,this.index=null,this.onCancel(e,t,o)},o.prototype._onComplete=function(e,t,o){this.onComplete(e,t,o),this.removeAfterUpload&&this.remove()},o.prototype._onTimeout=function(){this.isReady=!1,this.isUploading=!1,this.isUploaded=!1,this.isSuccess=!1,this.isCancel=!1,this.isError=!0,this.progress=0,this.index=null,this.onTimeout()},o.prototype._destroy=function(){this._input&&this._input.remove(),this._form&&this._form.remove(),delete this._form,delete this._input},o.prototype._prepareToUploading=function(){this.index=this.index||++this.uploader._nextIndex,this.isReady=!0},o.prototype._replaceNode=function(t){var o=e(t.clone())(t.scope());o.prop("value",null),t.css("display","none"),t.after(o)},o}()}Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var s=o(1),a=(n(s),angular),u=a.copy,l=a.extend,p=a.element;a.isElement;i.$inject=["$compile","FileLikeObject"]},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(){var e=function(){function e(t){r(this,e),u(this,t),this.uploader._directives[this.prop].push(this),this._saveLinks(),this.bind()}return e.prototype.bind=function(){for(var e in this.events){var t=this.events[e];this.element.bind(e,this[t])}},e.prototype.unbind=function(){for(var e in this.events)this.element.unbind(e,this.events[e])},e.prototype.destroy=function(){var e=this.uploader._directives[this.prop].indexOf(this);this.uploader._directives[this.prop].splice(e,1),this.unbind()},e.prototype._saveLinks=function(){for(var e in this.events){var t=this.events[e];this[t]=this[t].bind(this)}},e}();return e.prototype.events={},e}Object.defineProperty(t,"__esModule",{value:!0}),t.default=i;var s=o(1),a=(n(s),angular),u=a.extend},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function a(e,t){return function(t){function o(e){r(this,o);var n=p(e,{events:{$destroy:"destroy",change:"onChange"},prop:"select"}),s=i(this,t.call(this,n));return s.uploader.isHTML5||s.element.removeAttr("multiple"),s.element.prop("value",null),s}return s(o,t),o.prototype.getOptions=function(){},o.prototype.getFilters=function(){},o.prototype.isEmptyAfterSelection=function(){return!!this.element.attr("multiple")},o.prototype.onChange=function(){var t=this.uploader.isHTML5?this.element[0].files:this.element[0],o=this.getOptions(),n=this.getFilters();this.uploader.isHTML5||this.destroy(),this.uploader.addToQueue(t,o,n),this.isEmptyAfterSelection()&&(this.element.prop("value",null),this.element.replaceWith(e(this.element.clone())(this.scope)))},o}(t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var u=o(1),l=(n(u),angular),p=l.extend;a.$inject=["$compile","FileDirective"]},function(e,t){"use strict";function o(e){if(Array.isArray(e)){for(var t=0,o=Array(e.length);t<e.length;t++)o[t]=e[t];return o}return Array.from(e)}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e){return function(){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];n(this,t),this.pipes=e}return t.prototype.next=function(t){var n=this.pipes.shift();if(a(n))return void this.onSuccessful.apply(this,o(t));var r=new Error("The filter has not passed");if(r.pipe=n,r.args=t,n.isAsync){var i=e.defer(),u=s(this,this.next,t),l=s(this,this.onThrown,r);i.promise.then(u,l),n.apply(void 0,o(t).concat([i]))}else{var p=Boolean(n.apply(void 0,o(t)));p?this.next(t):this.onThrown(r)}},t.prototype.exec=function(){for(var e=arguments.length,t=Array(e),o=0;o<e;o++)t[o]=arguments[o];this.next(t)},t.prototype.onThrown=function(e){},t.prototype.onSuccessful=function(){},t}()}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=angular,s=i.bind,a=i.isUndefined;r.$inject=["$q"]},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function a(e){return function(e){function t(o){r(this,t);var n=p(o,{events:{$destroy:"destroy",drop:"onDrop",dragover:"onDragOver",dragleave:"onDragLeave"},prop:"drop"});return i(this,e.call(this,n))}return s(t,e),t.prototype.getOptions=function(){},t.prototype.getFilters=function(){},t.prototype.onDrop=function(e){var t=this._getTransfer(e);if(t){var o=this.getOptions(),n=this.getFilters();this._preventAndStop(e),c(this.uploader._directives.over,this._removeOverClass,this),this.uploader.addToQueue(t.files,o,n)}},t.prototype.onDragOver=function(e){var t=this._getTransfer(e);this._haveFiles(t.types)&&(t.dropEffect="copy",this._preventAndStop(e),c(this.uploader._directives.over,this._addOverClass,this))},t.prototype.onDragLeave=function(e){e.currentTarget!==this.element[0]&&(this._preventAndStop(e),c(this.uploader._directives.over,this._removeOverClass,this))},t.prototype._getTransfer=function(e){return e.dataTransfer?e.dataTransfer:e.originalEvent.dataTransfer},t.prototype._preventAndStop=function(e){e.preventDefault(),e.stopPropagation()},t.prototype._haveFiles=function(e){return!!e&&(e.indexOf?e.indexOf("Files")!==-1:!!e.contains&&e.contains("Files"))},t.prototype._addOverClass=function(e){e.addOverClass()},t.prototype._removeOverClass=function(e){e.removeOverClass()},t}(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var u=o(1),l=(n(u),angular),p=l.extend,c=l.forEach;a.$inject=["FileDirective"]},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function a(e){return function(e){function t(o){r(this,t);var n=p(o,{events:{$destroy:"destroy"},prop:"over",overClass:"nv-file-over"});return i(this,e.call(this,n))}return s(t,e),t.prototype.addOverClass=function(){this.element.addClass(this.getOverClass())},t.prototype.removeOverClass=function(){this.element.removeClass(this.getOverClass())},t.prototype.getOverClass=function(){return this.overClass},t}(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var u=o(1),l=(n(u),angular),p=l.extend;a.$inject=["FileDirective"]},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t,o){return{link:function(n,r,i){var s=n.$eval(i.uploader);if(!(s instanceof t))throw new TypeError('"Uploader" must be an instance of FileUploader');var a=new o({uploader:s,element:r,scope:n});a.getOptions=e(i.options).bind(a,n),a.getFilters=function(){return i.filters}}}}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=o(1);n(i);r.$inject=["$parse","FileUploader","FileSelect"]},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t,o){return{link:function(n,r,i){var s=n.$eval(i.uploader);if(!(s instanceof t))throw new TypeError('"Uploader" must be an instance of FileUploader');if(s.isHTML5){var a=new o({uploader:s,element:r});a.getOptions=e(i.options).bind(a,n),a.getFilters=function(){return i.filters}}}}}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=o(1);n(i);r.$inject=["$parse","FileUploader","FileDrop"]},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){return{link:function(o,n,r){var i=o.$eval(r.uploader);if(!(i instanceof e))throw new TypeError('"Uploader" must be an instance of FileUploader');var s=new t({uploader:i,element:n});s.getOverClass=function(){return r.overClass||s.overClass}}}}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=o(1);n(i);r.$inject=["FileUploader","FileOver"]}])});
2150 2151(function(angular, $, _) { 2152 angular.module('api4', CRM.angRequires('api4')); 2153 2154angular.module('api4').factory('crmApi4', function($q) { 2155 var crmApi4 = function(entity, action, params, index) { 2156 var deferred = $q.defer(); 2157 var p; 2158 var backend = crmApi4.backend || CRM.api4; 2159 if (_.isObject(entity)) { 2160 /*jshint -W061 */ 2161 p = backend(eval('('+angular.toJson(entity)+')'), action); 2162 } else { 2163 /*jshint -W061 */ 2164 p = backend(entity, action, eval('('+angular.toJson(params)+')'), index); 2165 } 2166 p.then( 2167 function(result) { 2168 deferred.resolve(result); 2169 }, 2170 function(error) { 2171 deferred.reject(error); 2172 } 2173 ); 2174 return deferred.promise; 2175 }; 2176 crmApi4.backend = null; 2177 crmApi4.val = function(value) { 2178 var d = $.Deferred(); 2179 d.resolve(value); 2180 return d.promise(); 2181 }; 2182 return crmApi4; 2183 }); 2184 2185})(angular, CRM.$, CRM._); 2186 2187/** 2188 * Checklist-model 2189 * AngularJS directive for list of checkboxes 2190 * https://github.com/vitalets/checklist-model 2191 * License: MIT http://opensource.org/licenses/MIT 2192 */ 2193 2194 /* commonjs package manager support (eg componentjs) */ 2195 if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ 2196 module.exports = 'checklist-model'; 2197 } 2198 2199angular.module('checklist-model', []) 2200.directive('checklistModel', ['$parse', '$compile', function($parse, $compile) { 2201 function contains(arr, item, comparator) { 2202 if (angular.isArray(arr)) { 2203 for (var i = arr.length; i--;) { 2204 if (comparator(arr[i], item)) { 2205 return true; 2206 } 2207 } 2208 } 2209 return false; 2210 } 2211 function add(arr, item, comparator) { 2212 arr = angular.isArray(arr) ? arr : []; 2213 if(!contains(arr, item, comparator)) { 2214 arr.push(item); 2215 } 2216 return arr; 2217 } 2218 function remove(arr, item, comparator) { 2219 if (angular.isArray(arr)) { 2220 for (var i = arr.length; i--;) { 2221 if (comparator(arr[i], item)) { 2222 arr.splice(i, 1); 2223 break; 2224 } 2225 } 2226 } 2227 return arr; 2228 } 2229 function postLinkFn(scope, elem, attrs) { 2230 var checklistModel = attrs.checklistModel; 2231 attrs.$set("checklistModel", null); 2232 $compile(elem)(scope); 2233 attrs.$set("checklistModel", checklistModel); 2234 var checklistModelGetter = $parse(checklistModel); 2235 var checklistChange = $parse(attrs.checklistChange); 2236 var checklistBeforeChange = $parse(attrs.checklistBeforeChange); 2237 var ngModelGetter = $parse(attrs.ngModel); 2238 2239 2240 2241 var comparator = function (a, b) { 2242 if(!isNaN(a) && !isNaN(b)) { 2243 return String(a) === String(b); 2244 } else { 2245 return angular.equals(a,b); 2246 } 2247 }; 2248 2249 if (attrs.hasOwnProperty('checklistComparator')){ 2250 if (attrs.checklistComparator[0] == '.') { 2251 var comparatorExpression = attrs.checklistComparator.substring(1); 2252 comparator = function (a, b) { 2253 return a[comparatorExpression] === b[comparatorExpression]; 2254 }; 2255 2256 } else { 2257 comparator = $parse(attrs.checklistComparator)(scope.$parent); 2258 } 2259 } 2260 var unbindModel = scope.$watch(attrs.ngModel, function(newValue, oldValue) { 2261 if (newValue === oldValue) { 2262 return; 2263 } 2264 2265 if (checklistBeforeChange && (checklistBeforeChange(scope) === false)) { 2266 ngModelGetter.assign(scope, contains(checklistModelGetter(scope.$parent), getChecklistValue(), comparator)); 2267 return; 2268 } 2269 2270 setValueInChecklistModel(getChecklistValue(), newValue); 2271 2272 if (checklistChange) { 2273 checklistChange(scope); 2274 } 2275 }); 2276 var unbindCheckListValue = scope.$watch(getChecklistValue, function(newValue, oldValue) { 2277 if( newValue != oldValue && angular.isDefined(oldValue) && scope[attrs.ngModel] === true ) { 2278 var current = checklistModelGetter(scope.$parent); 2279 checklistModelGetter.assign(scope.$parent, remove(current, oldValue, comparator)); 2280 checklistModelGetter.assign(scope.$parent, add(current, newValue, comparator)); 2281 } 2282 }, true); 2283 2284 var unbindDestroy = scope.$on('$destroy', destroy); 2285 2286 function destroy() { 2287 unbindModel(); 2288 unbindCheckListValue(); 2289 unbindDestroy(); 2290 } 2291 2292 function getChecklistValue() { 2293 return attrs.checklistValue ? $parse(attrs.checklistValue)(scope.$parent) : attrs.value; 2294 } 2295 2296 function setValueInChecklistModel(value, checked) { 2297 var current = checklistModelGetter(scope.$parent); 2298 if (angular.isFunction(checklistModelGetter.assign)) { 2299 if (checked === true) { 2300 checklistModelGetter.assign(scope.$parent, add(current, value, comparator)); 2301 } else { 2302 checklistModelGetter.assign(scope.$parent, remove(current, value, comparator)); 2303 } 2304 } 2305 2306 } 2307 function setChecked(newArr, oldArr) {
2308 if (checklistBeforeChange && (checklistBeforeChange(scope) === false)) { 2309 setValueInChecklistModel(getChecklistValue(), ngModelGetter(scope)); 2310 return; 2311 } 2312 ngModelGetter.assign(scope, contains(newArr, getChecklistValue(), comparator)); 2313 } 2314 if (angular.isFunction(scope.$parent.$watchCollection)) { 2315 scope.$parent.$watchCollection(checklistModel, setChecked); 2316 } else { 2317 scope.$parent.$watch(checklistModel, setChecked, true); 2318 } 2319 } 2320 2321 return { 2322 restrict: 'A', 2323 priority: 1000, 2324 terminal: true, 2325 scope: true, 2326 compile: function(tElement, tAttrs) { 2327 2328 if (!tAttrs.checklistValue && !tAttrs.value) { 2329 throw 'You should provide `value` or `checklist-value`.'; 2330 } 2331 if (!tAttrs.ngModel) { 2332 tAttrs.$set("ngModel", "checked"); 2333 } 2334 2335 return postLinkFn; 2336 } 2337 }; 2338}]); 2339 2340(function(angular, $, _) { 2341 angular.module('crmResource', []); 2342 2343 angular.module('crmResource').factory('crmResource', function($q, $http) { 2344 var deferreds = {}; // null|object; deferreds[url][idx] = Deferred; 2345 2346 var notify = function notify() { 2347 var oldDfrds = deferreds; 2348 deferreds = null; 2349 2350 angular.forEach(oldDfrds, function(dfrs, url) { 2351 if (CRM.angular.templates[url]) { 2352 angular.forEach(dfrs, function(dfr) { 2353 dfr.resolve({ 2354 status: 200, 2355 headers: function(name) { 2356 var headers = {'Content-type': 'text/html'}; 2357 return name ? headers[name] : headers; 2358 }, 2359 data: CRM.angular.templates[url] 2360 }); 2361 }); 2362 } 2363 else { 2364 angular.forEach(dfrs, function(dfr) { 2365 dfr.reject({status: 500}); // FIXME 2366 }); 2367 } 2368 }); 2369 }; 2370 2371 var moduleUrl = CRM.angular.bundleUrl; 2372 $http.get(moduleUrl) 2373 .then(function httpSuccess(response) { 2374 CRM.angular.templates = CRM.angular.templates || {}; 2375 angular.forEach(response.data, function (module) { 2376 if (module.partials) { 2377 angular.extend(CRM.angular.templates, module.partials); 2378 } 2379 if (module.strings) { 2380 CRM.addStrings(module.domain, module.strings); 2381 } 2382 }); 2383 notify(); 2384 }, function httpError() { 2385 notify(); 2386 }); 2387 2388 return { 2389 getUrl: function getUrl(url) { 2390 if (CRM.angular.templates && CRM.angular.templates[url]) { 2391 return CRM.angular.templates[url]; 2392 } 2393 else { 2394 var deferred = $q.defer(); 2395 if (!deferreds[url]) { 2396 deferreds[url] = []; 2397 } 2398 deferreds[url].push(deferred); 2399 return deferred.promise; 2400 } 2401 } 2402 }; 2403 }); 2404 2405 angular.module('crmResource').config(function($provide) { 2406 $provide.decorator('$templateCache', function($delegate, $http, $q, crmResource) { 2407 var origGet = $delegate.get; 2408 var urlPat = /^~\//; 2409 $delegate.get = function(url) { 2410 if (urlPat.test(url)) { 2411 return crmResource.getUrl(url); 2412 } 2413 else { 2414 return origGet.call(this, url); 2415 } 2416 }; 2417 return $delegate; 2418 }); 2419 }); 2420 2421 2422let uidCount = 0, 2423 pageTitleHTML = 'CiviCRM', 2424 documentTitle = 'CiviCRM'; 2425 2426 angular.module('crmUi', CRM.angRequires('crmUi')) 2427 .directive('crmUiAccordion', function() { 2428 return { 2429 scope: { 2430 crmUiAccordion: '=' 2431 }, 2432 template: '<details class="crm-accordion-bold"><summary>{{crmUiAccordion.title}} <a crm-ui-help="help" ng-if="help"></a></summary><div class="crm-accordion-body" ng-transclude></div></details>', 2433 transclude: true, 2434 link: function (scope, element, attrs) { 2435 scope.help = null; 2436 let openSet = false; 2437 scope.$watch('crmUiAccordion', function(crmUiAccordion, oldVal) { 2438 if (crmUiAccordion) { 2439 if (!openSet) { 2440 $(element).children('details').prop('open', !crmUiAccordion.collapsed); 2441 openSet = true; 2442 } 2443 if (crmUiAccordion.help) {
2444 scope.help = crmUiAccordion.help.clone({}, { 2445 title: crmUiAccordion.title 2446 }); 2447 } 2448 } 2449 }); 2450 } 2451 }; 2452 }) 2453 .service('crmUiAlert', function($compile, $rootScope, $templateRequest, $q) { 2454 let count = 0; 2455 return function crmUiAlert(params) { 2456 const id = 'crmUiAlert_' + (++count); 2457 let tpl = null; 2458 if (params.templateUrl) { 2459 tpl = $templateRequest(params.templateUrl); 2460 } 2461 else if (params.template) { 2462 tpl = params.template; 2463 } 2464 if (tpl) { 2465 params.text = '<div id="' + id + '"></div>'; // temporary stub 2466 } 2467 const result = CRM.alert(params.text, params.title, params.type, params.options); 2468 if (tpl) { 2469 $q.when(tpl, function(html) { 2470 const scope = params.scope || $rootScope.$new(); 2471 const linker = $compile(html); 2472 $('#' + id).append($(linker(scope))); 2473 }); 2474 } 2475 return result; 2476 }; 2477 }) 2478 .directive('crmUiDatepicker', function ($timeout) { 2479 return { 2480 restrict: 'AE', 2481 require: 'ngModel', 2482 scope: { 2483 crmUiDatepicker: '=' 2484 }, 2485 link: function (scope, element, attrs, ngModel) { 2486 ngModel.$render = function () { 2487 const viewVal = ngModel.$viewValue || ''; 2488 if (element.val() != viewVal) { 2489 element.val(viewVal).change(); 2490 } 2491 }; 2492 let settings = angular.copy(scope.crmUiDatepicker || {}); 2493 settings.start_date_years = settings.start_date_years || 100; 2494 settings.end_date_years = settings.end_date_years || 100; 2495 $timeout(function() { 2496 element 2497 .crmDatepicker(settings) 2498 .on('change', function () { 2499 $timeout(function() { 2500 let requiredLength = 19; 2501 if (settings.time === false) { 2502 requiredLength = 10; 2503 } 2504 if (settings.date === false) { 2505 requiredLength = 8; 2506 } 2507 else if (typeof settings.date === 'string') { 2508 const lowerFormat = settings.date.toLowerCase(); 2509 if (lowerFormat.indexOf('y') < 0 || lowerFormat.indexOf('m') < 0 || lowerFormat.indexOf('d') < 0) { 2510 requiredLength = element.val().length; 2511 } 2512 } 2513 ngModel.$setValidity('incompleteDateTime', !(element.val().length && element.val().length !== requiredLength)); 2514 }); 2515 }); 2516 }); 2517 } 2518 }; 2519 }) 2520 .directive('crmUiDebug', function ($location) { 2521 return { 2522 restrict: 'AE', 2523 scope: { 2524 crmUiDebug: '@' 2525 }, 2526 template: function() { 2527 const args = $location.search(); 2528 if (args && args.angularDebug) { 2529 const jsonTpl = (CRM.angular.modules.indexOf('jsonFormatter') < 0) ? '<pre>{{data|json}}</pre>' : '<json-formatter json="data" open="1"></json-formatter>'; 2530 return '<div crm-ui-accordion=\'{title: ts("Debug (%1)", {1: crmUiDebug}), collapsed: true}\'>' + jsonTpl + '</div>'; 2531 } 2532 return ''; 2533 }, 2534 link: function(scope, element, attrs) { 2535 const args = $location.search(); 2536 if (args && args.angularDebug) { 2537 scope.ts = CRM.ts(null); 2538 scope.$parent.$watch(attrs.crmUiDebug, function(data) { 2539 scope.data = data; 2540 }); 2541 } 2542 } 2543 }; 2544 }) 2545 .directive('crmUiField', function() { 2546 const templateUrls = { 2547 default: '~/crmUi/field.html', 2548 checkbox: '~/crmUi/field-cb.html' 2549 }; 2550 2551 return { 2552 require: '^crmUiIdScope', 2553 restrict: 'EA', 2554 scope: { 2555 crmUiField: '=' 2556 },
2557 templateUrl: function(tElement, tAttrs){ 2558 const layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default'; 2559 return templateUrls[layout]; 2560 }, 2561 transclude: true, 2562 link: function (scope, element, attrs, crmUiIdCtrl) { 2563 $(element).addClass('crm-section'); 2564 scope.help = null; 2565 scope.$watch('crmUiField', function(crmUiField) { 2566 if (crmUiField && crmUiField.help) { 2567 scope.help = crmUiField.help.clone({}, { 2568 title: crmUiField.title 2569 }); 2570 } 2571 }); 2572 } 2573 }; 2574 }) 2575 .directive('crmUiId', function () { 2576 return { 2577 require: '^crmUiIdScope', 2578 restrict: 'EA', 2579 link: { 2580 pre: function (scope, element, attrs, crmUiIdCtrl) { 2581 const id = crmUiIdCtrl.get(attrs.crmUiId); 2582 element.attr('id', id); 2583 } 2584 } 2585 }; 2586 }) 2587 .service('crmUiHelp', function(){ 2588 function FieldHelp(options) { 2589 this.options = options; 2590 } 2591 angular.extend(FieldHelp.prototype, { 2592 get: function(n) { 2593 return this.options[n]; 2594 }, 2595 open: function open() { 2596 CRM.help(this.options.title, {id: this.options.id, file: this.options.file}); 2597 }, 2598 clone: function clone(options, defaults) { 2599 return new FieldHelp(angular.extend({}, defaults, this.options, options)); 2600 } 2601 }); 2602 return function(defaults){ 2603 return function(options) { 2604 if (typeof options === 'string') { 2605 options = {id: options}; 2606 } 2607 return new FieldHelp(angular.extend({}, defaults, options)); 2608 }; 2609 }; 2610 }) 2611 .directive('crmUiHelp', function() { 2612 return { 2613 restrict: 'EA', 2614 link: function(scope, element, attrs) { 2615 setTimeout(function() { 2616 const crmUiHelp = scope.$eval(attrs.crmUiHelp); 2617 let title = crmUiHelp && crmUiHelp.get('title') ? ts('%1 Help', {1: crmUiHelp.get('title')}) : ts('Help'); 2618 element.attr('title', title); 2619 }, 50); 2620 2621 element 2622 .addClass('helpicon') 2623 .attr('href', '#') 2624 .on('click', function(e) { 2625 e.preventDefault(); 2626 scope.$eval(attrs.crmUiHelp).open(); 2627 }); 2628 } 2629 }; 2630 }) 2631 .directive('crmUiFor', function ($parse, $timeout) { 2632 return { 2633 require: '^crmUiIdScope', 2634 restrict: 'EA', 2635 template: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>', 2636 transclude: true, 2637 link: function (scope, element, attrs, crmUiIdCtrl) { 2638 scope.crmIsRequired = false; 2639 scope.cssClasses = {}; 2640 2641 if (!attrs.crmUiFor) return; 2642 2643 const id = crmUiIdCtrl.get(attrs.crmUiFor); 2644 element.attr('for', id); 2645 let ngModel = null; 2646 2647 const updateCss = function () { 2648 scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine; 2649 }; 2650 const init = function (retries, retryDelay) { 2651 const input = $('#' + id); 2652 if (input.length === 0 && !attrs.crmUiForceRequired) { 2653 if (retries) { 2654 $timeout(function(){ 2655 init(retries-1, retryDelay); 2656 }, retryDelay); 2657 } 2658 return; 2659 } 2660 2661 if (attrs.crmUiForceRequired) { 2662 scope.crmIsRequired = true; 2663 return; 2664 } 2665 2666 let tgtScope = scope;//.$parent; 2667 if (attrs.crmDepth) { 2668 for (let i = attrs.crmDepth; i > 0; i--) { 2669 tgtScope = tgtScope.$parent; 2670 } 2671 } 2672 2673 if (input.attr('ng-required')) { 2674 scope.crmIsRequired = scope.$parent.$eval(input.attr('ng-required')); 2675 scope.$parent.$watch(input.attr('ng-required'), function (isRequired) { 2676 scope.crmIsRequired = isRequired; 2677 }); 2678 } 2679 else { 2680 scope.crmIsRequired = input.prop('required'); 2681 } 2682 2683 ngModel = $parse(attrs.crmUiFor)(tgtScope); 2684 if (ngModel) { 2685 ngModel.$viewChangeListeners.push(updateCss); 2686 } 2687 }; 2688 2689 $timeout(function(){ 2690 init(3, 100); 2691 }); 2692 } 2693 }; 2694 }) 2695 .directive('crmUiIdScope', function () { 2696 return { 2697 restrict: 'EA', 2698 scope: {}, 2699 controllerAs: 'crmUiIdCtrl', 2700 controller: function($scope) { 2701 const ids = {}; 2702 this.get = function(name) { 2703 if (!ids[name]) { 2704 ids[name] = "crmUiId_" + (++uidCount); 2705 } 2706 return ids[name]; 2707 }; 2708 }, 2709 link: function (scope, element, attrs) {} 2710 }; 2711 }) 2712 .directive('crmUiIframe', function ($parse) { 2713 return { 2714 scope: { 2715 crmUiIframeSrc: '@', // expression which evaluates to a URL 2716 crmUiIframe: '@' // expression which evaluates to HTML content 2717 }, 2718 link: function (scope, elm, attrs) { 2719 const iframe = $(elm)[0]; 2720 iframe.setAttribute('width', '100%'); 2721 iframe.setAttribute('height', '250px'); 2722 iframe.setAttribute('frameborder', '0'); 2723 2724 const refresh = function () { 2725 if (attrs.crmUiIframeSrc) { 2726 iframe.setAttribute('src', scope.$parent.$eval(attrs.crmUiIframeSrc)); 2727 } 2728 else { 2729 let iframeHtml = scope.$parent.$eval(attrs.crmUiIframe); 2730 2731 let doc = iframe.document; 2732 if (iframe.contentDocument) { 2733 doc = iframe.contentDocument; 2734 } 2735 else if (iframe.contentWindow) { 2736 doc = iframe.contentWindow.document; 2737 } 2738 2739 doc.open(); 2740 doc.writeln(iframeHtml); 2741 doc.close(); 2742 } 2743 }; 2744 $(elm).parent().on('dialogresize dialogopen', function(e, ui) { 2745 $(this).css({padding: '0', margin: '0', overflow: 'hidden'}); 2746 iframe.setAttribute('height', '' + $(this).innerHeight() + 'px'); 2747 }); 2748 2749 $(elm).parent().on('dialogresize', function(e, ui) { 2750 iframe.setAttribute('class', 'resized'); 2751 }); 2752 2753 scope.$parent.$watch(attrs.crmUiIframe, refresh); 2754 } 2755 }; 2756 }) 2757 .directive('crmUiInsertRx', function() { 2758 return { 2759 link: function(scope, element, attrs) { 2760 scope.$on(attrs.crmUiInsertRx, function(e, tokenName) { 2761 CRM.wysiwyg.insert(element, tokenName); 2762 $(element).select2('close').select2('val', ''); 2763 CRM.wysiwyg.focus(element); 2764 }); 2765 } 2766 }; 2767 }) 2768 .directive('crmUiRichtext', function ($timeout) { 2769 return { 2770 require: '?ngModel', 2771 link: function (scope, elm, attr, ngModel) { 2772 $timeout(function() { 2773 const editor = CRM.wysiwyg.create(elm); 2774 2775 if (!ngModel) { 2776 return; 2777 } 2778 2779 if (attr.ngBlur) { 2780 $(elm).on('blur', function() { 2781 $timeout(function() { 2782 scope.$eval(attr.ngBlur); 2783 }); 2784 }); 2785 } 2786 2787 ngModel.$render = function(value) { 2788 editor.done(function() { 2789 CRM.wysiwyg.setVal(elm, ngModel.$viewValue || ''); 2790 }); 2791 }; 2792 }); 2793 } 2794 }; 2795 }) 2796 .directive('crmUiLock', function ($parse, $rootScope) { 2797 const defaultVal = function (defaultValue) { 2798 const f = function (scope) { 2799 return defaultValue; 2800 }; 2801 f.assign = function (scope, value) { 2802 }; 2803 return f; 2804 }; 2805 const parse = function (expr, defaultValue) { 2806 return expr ? $parse(expr) : defaultVal(defaultValue); 2807 }; 2808 2809 return { 2810 template: '', 2811 link: function (scope, element, attrs) { 2812 const binding = parse(attrs.binding, true); 2813 const titleLocked = parse(attrs.titleLocked, ts('Locked')); 2814 const titleUnlocked = parse(attrs.titleUnlocked, ts('Unlocked')); 2815 2816 $(element).addClass('crm-i lock-button'); 2817 const refresh = function () { 2818 const locked = binding(scope); 2819 if (locked) { 2820 $(element) 2821 .removeClass('fa-unlock') 2822 .addClass('fa-lock') 2823 .prop('title', titleLocked(scope)) 2824 ; 2825 } 2826 else { 2827 $(element) 2828 .removeClass('fa-lock') 2829 .addClass('fa-unlock') 2830 .prop('title', titleUnlocked(scope)) 2831 ; 2832 } 2833 }; 2834 2835 $(element).click(function () { 2836 binding.assign(scope, !binding(scope)); 2837 $rootScope.$digest(); 2838 }); 2839 2840 scope.$watch(attrs.binding, refresh); 2841 scope.$watch(attrs.titleLocked, refresh); 2842 scope.$watch(attrs.titleUnlocked, refresh); 2843 2844 refresh(); 2845 } 2846 }; 2847 }) 2848 .service('CrmUiOrderCtrl', function(){ 2849 function CrmUiOrderCtrl(defaults){ 2850 this.values = defaults; 2851 } 2852 angular.extend(CrmUiOrderCtrl.prototype, { 2853 get: function get() { 2854 return this.values; 2855 }, 2856 getDir: function getDir(name) { 2857 if (this.values.indexOf(name) >= 0 || this.values.indexOf('+' + name) >= 0) { 2858 return '+'; 2859 } 2860 if (this.values.indexOf('-' + name) >= 0) { 2861 return '-'; 2862 } 2863 return ''; 2864 }, 2865 remove: function remove(name) { 2866 const idx = this.values.indexOf(name); 2867 if (idx >= 0) { 2868 this.values.splice(idx, 1); 2869 return true; 2870 } 2871 else { 2872 return false;
2873 } 2874 }, 2875 setDir: function setDir(name, dir) { 2876 return this.toggle(name, dir); 2877 }, 2878 toggle: function toggle(name, next) { 2879 if (!next && next !== '') { 2880 next = '+'; 2881 if (this.remove(name) || this.remove('+' + name)) { 2882 next = '-'; 2883 } 2884 if (this.remove('-' + name)) { 2885 next = ''; 2886 } 2887 } 2888 2889 if (next == '+') { 2890 this.values.unshift('+' + name); 2891 } 2892 else if (next == '-') { 2893 this.values.unshift('-' + name); 2894 } 2895 } 2896 }); 2897 return CrmUiOrderCtrl; 2898 }) 2899 .directive('crmUiOrder', function(CrmUiOrderCtrl) { 2900 return { 2901 link: function(scope, element, attrs){ 2902 const options = angular.extend({var: 'crmUiOrderBy'}, scope.$eval(attrs.crmUiOrder)); 2903 scope[options.var] = new CrmUiOrderCtrl(options.defaults); 2904 } 2905 }; 2906 }) 2907 .directive('crmUiOrderBy', function() { 2908 return { 2909 link: function(scope, element, attrs) { 2910 function updateClass(crmUiOrderCtrl, name) { 2911 const dir = crmUiOrderCtrl.getDir(name); 2912 element 2913 .toggleClass('sorting_asc', dir === '+') 2914 .toggleClass('sorting_desc', dir === '-') 2915 .toggleClass('sorting', dir === ''); 2916 } 2917 2918 element.on('click', function(e){ 2919 const tgt = scope.$eval(attrs.crmUiOrderBy); 2920 tgt[0].toggle(tgt[1]); 2921 updateClass(tgt[0], tgt[1]); 2922 e.preventDefault(); 2923 scope.$digest(); 2924 }); 2925 2926 const tgt = scope.$eval(attrs.crmUiOrderBy); 2927 updateClass(tgt[0], tgt[1]); 2928 } 2929 }; 2930 }) 2931 .directive('crmUiSelect', function ($parse, $timeout) { 2932 return { 2933 require: '?ngModel', 2934 priority: 1, 2935 scope: { 2936 crmUiSelect: '=' 2937 }, 2938 link: function (scope, element, attrs, ngModel) { 2939 2940 if (ngModel && !attrs.ngOptions) { 2941 ngModel.$render = function () { 2942 $timeout(function () { 2943 let newVal = _.cloneDeep(ngModel.$modelValue); 2944 if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) { 2945 newVal = newVal.length ? newVal.split(scope.crmUiSelect.separator || ',') : []; 2946 } 2947 element.select2('val', newVal); 2948 }); 2949 }; 2950 } 2951 function refreshModel() { 2952 const oldValue = ngModel.$viewValue; 2953 let newValue = element.select2('val'); 2954 if (Array.isArray(newValue) && attrs.ngList) { 2955 newValue = newValue.join(attrs.ngList); 2956 } 2957 if (oldValue != newValue) { 2958 scope.$parent.$apply(function () { 2959 ngModel.$setViewValue(newValue); 2960 }); 2961 } 2962 } 2963 2964 function init() { 2965 element.crmSelect2(scope.crmUiSelect || {}); 2966 if (ngModel) { 2967 element.on('change', refreshModel); 2968 } 2969 } 2970 if (attrs.ngOptions) { 2971 $timeout(function() { 2972 element.crmSelect2(scope.crmUiSelect || {}); 2973 ngModel.$render = function () { 2974 element.val(ngModel.$viewValue || '').change(); 2975 }; 2976 }); 2977 } else { 2978 $timeout(init); 2979 } 2980 } 2981 }; 2982 }) 2983 .directive('onCrmUiSelect', function () { 2984 return { 2985 priority: 10, 2986 link: function (scope, element, attrs) { 2987 element.on('select2-selecting', function(e) { 2988 e.preventDefault(); 2989 element.select2('close').select2('val', ''); 2990 scope.$apply(function() { 2991 scope.$eval(attrs.onCrmUiSelect, {selection: e.val}); 2992 }); 2993 }); 2994 } 2995 }; 2996 }) 2997 .directive('crmEntityref', function ($parse, $timeout) { 2998 return { 2999 require: '?ngModel', 3000 scope: { 3001 crmEntityref: '=' 3002 }, 3003 link: function (scope, element, attrs, ngModel) { 3004 3005 ngModel.$render = function () { 3006 $timeout(function () { 3007 let newVal = _.cloneDeep(ngModel.$modelValue); 3008 if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) { 3009 newVal = newVal.length ? newVal.split(',') : []; 3010 } 3011 element.select2('val', newVal); 3012 }); 3013 }; 3014 function refreshModel() { 3015 const oldValue = ngModel.$viewValue; 3016 let newValue = element.select2('val'); 3017 if (oldValue != newValue) { 3018 scope.$parent.$apply(function () { 3019 ngModel.$setViewValue(newValue); 3020 }); 3021 } 3022 } 3023
3024 function init() { 3025 element.crmEntityRef(scope.crmEntityref || {}); 3026 element.on('change', refreshModel); 3027 $timeout(ngModel.$render); 3028 } 3029 3030 init(); 3031 } 3032 }; 3033 }) 3034 .directive('crmAutocomplete', function () { 3035 return { 3036 require: { 3037 crmAutocomplete: 'crmAutocomplete', 3038 ngModel: '?ngModel' 3039 }, 3040 priority: 100, 3041 bindToController: { 3042 entity: '<crmAutocomplete', 3043 crmAutocompleteParams: '<', 3044 multi: '<', 3045 autoOpen: '<', 3046 quickAdd: '<', 3047 quickEdit: '<', 3048 staticOptions: '<' 3049 }, 3050 link: function(scope, element, attr, ctrl) { 3051 const parseList = function(viewValue) { 3052 if (typeof viewValue === 'undefined') { 3053 return; 3054 } 3055 3056 if (!ctrl.crmAutocomplete.multi) { 3057 return viewValue; 3058 } 3059 3060 const list = []; 3061 3062 if (viewValue) { 3063 _.each(viewValue.split(','), function(value) { 3064 if (value) { 3065 list.push(_.trim(value)); 3066 } 3067 }); 3068 } 3069 3070 return list; 3071 }; 3072 3073 if (ctrl.ngModel) { 3074 ctrl.ngModel.$render = function() { 3075 const newValue = (ctrl.ngModel.$viewValue === null || ctrl.ngModel.$viewValue === undefined || ctrl.ngModel.$viewValue === false) ? '' : ctrl.ngModel.$viewValue.toString(); 3076 if (newValue !== element.val().toString()) { 3077 element.val(newValue).change(); 3078 } 3079 }; 3080 ctrl.ngModel.$parsers.push(parseList); 3081 ctrl.ngModel.$formatters.push(function(value) { 3082 return Array.isArray(value) ? value.join(',') : value; 3083 }); 3084 ctrl.ngModel.$isEmpty = function(value) { 3085 return !value || !value.length; 3086 }; 3087 } 3088 }, 3089 controller: function($element, $timeout) { 3090 const ctrl = this; 3091 this.$onChanges = function() { 3092 $timeout(function() { 3093 const autoOpen = ctrl.autoOpen && 3094 !(ctrl.staticOptions && ctrl.staticOptions.length) && 3095 !(ctrl.quickAdd === true || (ctrl.quickAdd && ctrl.quickAdd.length)); 3096 $element.crmAutocomplete(ctrl.entity, ctrl.crmAutocompleteParams || {}, { 3097 multiple: ctrl.multi, 3098 minimumInputLength: autoOpen ? 0 : 1, 3099 static: ctrl.staticOptions || [], 3100 quickAdd: ctrl.quickAdd, 3101 quickEdit: ctrl.quickEdit, 3102 }); 3103 }); 3104 }; 3105 } 3106 }; 3107 }) 3108 .directive('crmMultipleEmail', function ($parse, $timeout) { 3109 return { 3110 require: 'ngModel', 3111 link: function(scope, element, attrs, ctrl) { 3112 ctrl.$parsers.unshift(function(viewValue) { 3113 if (_.isEmpty(viewValue)) { 3114 ctrl.$setValidity('crmMultipleEmail', true); 3115 return viewValue; 3116 } 3117 const emails = viewValue.split(','); 3118 const emailRegex = /\S+@\S+\.\S+/; 3119 3120 const validityArr = emails.map(function(str){ 3121 return emailRegex.test(str.trim()); 3122 }); 3123 3124 if ($.inArray(false, validityArr) > -1) { 3125 ctrl.$setValidity('crmMultipleEmail', false); 3126 } else { 3127 ctrl.$setValidity('crmMultipleEmail', true); 3128 } 3129 return viewValue; 3130 }); 3131 } 3132 }; 3133 }) 3134 .directive('crmUiTab', function($parse) { 3135 return { 3136 require: '^crmUiTabSet', 3137 restrict: 'EA', 3138 scope: { 3139 crmTitle: '@', 3140 crmIcon: '@', 3141 count: '@', 3142 id: '@' 3143 }, 3144 template: '<div ng-transclude></div>', 3145 transclude: true, 3146 link: function (scope, element, attrs, crmUiTabSetCtrl) { 3147 crmUiTabSetCtrl.add(scope); 3148 } 3149 }; 3150 }) 3151 .directive('crmUiTabSet', function() { 3152 return { 3153 restrict: 'EA', 3154 scope: { 3155 crmUiTabSet: '@', 3156 tabSetOptions: '<' 3157 }, 3158 templateUrl: '~/crmUi/tabset.html', 3159 transclude: true, 3160 controllerAs: 'crmUiTabSetCtrl', 3161 controller: function($scope, $element, $timeout) { 3162 let init; 3163 $scope.tabs = []; 3164 this.add = function(tab) { 3165 if (!tab.id) throw "Tab is missing 'id'"; 3166 $scope.tabs.push(tab); 3167 if (init) { 3168 $timeout.cancel(init); 3169 } 3170 init = $timeout(function() { 3171 $element.find('.crm-tabset').tabs($scope.tabSetOptions); 3172 }); 3173 }; 3174 } 3175 }; 3176 }) 3177 .directive('crmUiValidate', function() { 3178 return { 3179 restrict: 'EA', 3180 require: 'ngModel', 3181 link: function(scope, element, attrs, ngModel) { 3182 const validationKey = attrs.crmUiValidateName ? attrs.crmUiValidateName : 'crmUiValidate'; 3183 scope.$watch(attrs.crmUiValidate, function(newValue){ 3184 ngModel.$setValidity(validationKey, !!newValue); 3185 }); 3186 } 3187 }; 3188 }) 3189 .directive('crmUiVisible', function($parse) { 3190 return { 3191 restrict: 'EA', 3192 scope: { 3193 crmUiVisible: '@' 3194 }, 3195 link: function (scope, element, attrs) { 3196 const model = $parse(attrs.crmUiVisible); 3197 function updatecChildren() { 3198 element.css('visibility', model(scope.$parent) ? 'inherit' : 'hidden'); 3199 } 3200 updatecChildren(); 3201 scope.$parent.$watch(attrs.crmUiVisible, updatecChildren); 3202 } 3203 }; 3204 }) 3205 .directive('crmUiWizard', function() { 3206 return { 3207 restrict: 'EA', 3208 scope: { 3209 crmUiWizard: '@', 3210 crmUiWizardNavClass: '@' // string, A list of classes that will be added to the nav items 3211 }, 3212 templateUrl: '~/crmUi/wizard.html', 3213 transclude: true, 3214 controllerAs: 'crmUiWizardCtrl', 3215 controller: function($scope, $parse) { 3216 const steps = $scope.steps = []; // array<$scope> 3217 let maxVisited = 0; 3218 let selectedIndex = null; 3219 3220 const findIndex = function() { 3221 let found = null; 3222 angular.forEach(steps, function(step, stepKey) { 3223 if (step.selected) found = stepKey; 3224 }); 3225 return found; 3226 }; 3227 this.$index = function() { return selectedIndex; }; 3228 this.$first = function() { return this.$index() === 0; }; 3229 this.$last = function() { return this.$index() === steps.length -1; }; 3230 this.$maxVisit = function() { return maxVisited; }; 3231 this.$validStep = function() { 3232 return steps[selectedIndex] && steps[selectedIndex].isStepValid(); 3233 }; 3234 this.iconFor = function(index) { 3235 if (index < this.$index()) return 'crm-i fa-check'; 3236 if (index === this.$index()) return 'crm-i fa-angle-double-right'; 3237 return ''; 3238 }; 3239 this.isSelectable = function(step) { 3240 if (step.selected) return false;
3241 return this.$validStep(); 3242 }; 3243 3244 /*** @param Object step the $scope of the step */ 3245 this.select = function(step) { 3246 angular.forEach(steps, function(otherStep, otherKey) { 3247 otherStep.selected = (otherStep === step); 3248 if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey; 3249 }); 3250 selectedIndex = findIndex(); 3251 }; 3252 /*** @param Object step the $scope of the step */ 3253 this.add = function(step) { 3254 if (steps.length === 0) { 3255 step.selected = true; 3256 selectedIndex = 0; 3257 } 3258 steps.push(step); 3259 steps.sort(function(a,b){ 3260 return a.crmUiWizardStep - b.crmUiWizardStep; 3261 }); 3262 selectedIndex = findIndex(); 3263 }; 3264 this.remove = function(step) { 3265 let key = null; 3266 angular.forEach(steps, function(otherStep, otherKey) { 3267 if (otherStep === step) key = otherKey; 3268 }); 3269 if (key !== null) { 3270 steps.splice(key, 1); 3271 } 3272 }; 3273 this.goto = function(index) { 3274 if (index < 0) index = 0; 3275 if (index >= steps.length) index = steps.length-1; 3276 this.select(steps[index]); 3277 }; 3278 this.previous = function() { this.goto(this.$index()-1); }; 3279 this.next = function() { this.goto(this.$index()+1); }; 3280 if ($scope.crmUiWizard) { 3281 $parse($scope.crmUiWizard).assign($scope.$parent, this); 3282 } 3283 }, 3284 link: function (scope, element, attrs) { 3285 scope.ts = CRM.ts(null); 3286 3287 element.find('.crm-wizard-buttons button[ng-click^=crmUiWizardCtrl]').click(function () { 3288 const topOfWizard = element.offset().top; 3289 const heightOfMenu = $('#civicrm-menu').height() || 0; 3290 3291 $('html') 3292 .stop() 3293 .animate({scrollTop: topOfWizard - heightOfMenu}, 1000); 3294 }); 3295 } 3296 }; 3297 }) 3298 .directive('crmUiWizardButtons', function() { 3299 return { 3300 require: '^crmUiWizard', 3301 restrict: 'EA', 3302 scope: {}, 3303 template: '<span ng-transclude></span>', 3304 transclude: true, 3305 link: function (scope, element, attrs, crmUiWizardCtrl) { 3306 const realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons'); 3307 $(element).appendTo(realButtonsEl); 3308 } 3309 }; 3310 }) 3311 .directive('crmIcon', function() { 3312 return { 3313 restrict: 'EA', 3314 link: function (scope, element, attrs) { 3315 if (element.is('[crm-ui-tab]')) { 3316 return; 3317 } 3318 if (attrs.crmIcon) { 3319 if (attrs.crmIcon.substring(0,3) == 'fa-') { 3320 $(element).prepend('<i class="crm-i ' + attrs.crmIcon + '" role="img" aria-hidden="true"></i> '); 3321 } 3322 else { 3323 $(element).prepend('<span class="icon ui-icon-' + attrs.crmIcon + '"></span> '); 3324 } 3325 } 3326 if ($(element).is('button:not(.btn)')) { 3327 $(element).addClass('crm-button'); 3328 } 3329 } 3330 }; 3331 }) 3332 .directive('crmUiWizardStep', function() { 3333 let nextWeight = 1; 3334 return { 3335 require: ['^crmUiWizard', 'form'], 3336 restrict: 'EA', 3337 scope: { 3338 crmTitle: '@', // expression, evaluates to a printable string 3339 crmUiWizardStep: '@', // int, a weight which determines the ordering of the steps 3340 crmUiWizardStepClass: '@' // string, A list of classes that will be added to the template 3341 }, 3342 template: '<div class="crm-wizard-step {{crmUiWizardStepClass}}" ng-show="selected" ng-transclude/></div>', 3343 transclude: true, 3344 link: function (scope, element, attrs, ctrls) { 3345 const crmUiWizardCtrl = ctrls[0]; 3346 const form = ctrls[1]; 3347 if (scope.crmUiWizardStep) { 3348 scope.crmUiWizardStep = parseInt(scope.crmUiWizardStep); 3349 } else { 3350 scope.crmUiWizardStep = nextWeight++; 3351 } 3352 scope.isStepValid = function() { 3353 return form.$valid; 3354 }; 3355 crmUiWizardCtrl.add(scope); 3356 scope.$on('$destroy', function(){ 3357 crmUiWizardCtrl.remove(scope); 3358 }); 3359 } 3360 }; 3361 }) 3362 .directive('crmConfirm', function ($compile, $rootScope, $templateRequest, $q) { 3363 const defaultFuncs = { 3364 'disable': function (options) { 3365 return {
3366 message: ts('Are you sure you want to disable this?'), 3367 options: {no: ts('Cancel'), yes: ts('Disable')}, 3368 width: 300, 3369 title: ts('Disable %1?', { 3370 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') 3371 }) 3372 }; 3373 }, 3374 'revert': function (options) { 3375 return { 3376 message: ts('Are you sure you want to revert this?'), 3377 options: {no: ts('Cancel'), yes: ts('Revert')}, 3378 width: 300, 3379 title: ts('Revert %1?', { 3380 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') 3381 }) 3382 }; 3383 }, 3384 'delete': function (options) { 3385 return { 3386 message: ts('Are you sure you want to delete this?'), 3387 options: {no: ts('Cancel'), yes: ts('Delete')}, 3388 width: 300, 3389 title: ts('Delete %1?', { 3390 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') 3391 }) 3392 }; 3393 } 3394 }; 3395 let confirmCount = 0; 3396 return { 3397 link: function (scope, element, attrs) { 3398 $(element).click(function () { 3399 const options = scope.$eval(attrs.crmConfirm); 3400 if (attrs.title && !options.title) { 3401 options.title = attrs.title; 3402 } 3403 const defaults = (options.type) ? defaultFuncs[options.type](options) : {}; 3404 3405 let tpl = null; 3406 let stubId = null; 3407 if (!options.message) { 3408 if (options.templateUrl) { 3409 tpl = $templateRequest(options.templateUrl); 3410 } 3411 else if (options.template) { 3412 tpl = options.template; 3413 } 3414 if (tpl) { 3415 stubId = 'crmUiConfirm_' + (++confirmCount); 3416 options.message = '<div id="' + stubId + '"></div>'; 3417 } 3418 } 3419 3420 if (options.confirmed) { 3421 scope.$apply(attrs.onYes); 3422 return; 3423 } 3424 3425 CRM.confirm(_.extend(defaults, options)) 3426 .on('crmConfirm:yes', function() { scope.$apply(attrs.onYes); }) 3427 .on('crmConfirm:no', function() { scope.$apply(attrs.onNo); }); 3428 3429 if (tpl && stubId) { 3430 $q.when(tpl, function(html) { 3431 const scope = options.scope || $rootScope.$new(); 3432 if (options.export) { 3433 angular.extend(scope, options.export); 3434 } 3435 const linker = $compile(html); 3436 $('#' + stubId).append($(linker(scope))); 3437 }); 3438 } 3439 }); 3440 } 3441 }; 3442 }) 3443 .directive('crmPageTitle', function($timeout) { 3444 return { 3445 scope: { 3446 crmDocumentTitle: '=' 3447 }, 3448 link: function(scope, $el, attrs) { 3449 pageTitleHTML = attrs.initialPageTitle || 'CiviCRM'; 3450 documentTitle = attrs.initialDocumentTitle || 'CiviCRM'; 3451 3452 function update() { 3453 $timeout(function() { 3454 const newPageTitleHTML = $el.html().trim(), 3455 newDocumentTitle = scope.crmDocumentTitle || $el.text(), 3456 dialog = $el.closest('.ui-dialog-content'); 3457 if (dialog.length) { 3458 dialog.dialog('option', 'title', newDocumentTitle); 3459 $el.hide(); 3460 } else { 3461 document.title = $('title').text().replace(documentTitle, newDocumentTitle); 3462 [].forEach.call(document.querySelectorAll('h1:not(.crm-container h1), .crm-page-title-wrapper>h1'), h1 => { 3463 if (h1.classList.contains('crm-page-title') || h1.innerHTML.trim() === pageTitleHTML) { 3464 h1.classList.add('crm-page-title'); 3465 h1.innerHTML = newPageTitleHTML; 3466 $el.hide(); 3467 } 3468 }); 3469 pageTitleHTML = newPageTitleHTML; 3470 documentTitle = newDocumentTitle; 3471 } 3472 }); 3473 } 3474 3475 scope.$watch(function() {return scope.crmDocumentTitle + $el.html();}, update); 3476 } 3477 }; 3478 }) 3479 .directive("crmUiEditable", function() { 3480 return { 3481 restrict: "A", 3482 require: "ngModel", 3483 scope: { 3484 defaultValue: '=' 3485 }, 3486 link: function(scope, element, attrs, ngModel) { 3487 function read() { 3488 let htmlVal = element.text(); 3489 if (!htmlVal) { 3490 htmlVal = scope.defaultValue || ''; 3491 element.text(htmlVal); 3492 } 3493 ngModel.$setViewValue(htmlVal); 3494 } 3495 3496 ngModel.$render = function() { 3497 element.text(ngModel.$viewValue || scope.defaultValue || ''); 3498 }; 3499 element.on('keydown', function(e) { 3500 if (e.which === 13) { 3501 e.preventDefault(); 3502 element.blur(); 3503 } 3504 if (e.which === 27) { 3505 element.text(ngModel.$viewValue || scope.defaultValue || ''); 3506 element.blur(); 3507 } 3508 }); 3509 3510 element.on("blur change", function() { 3511 scope.$apply(read); 3512 }); 3513 3514 element.attr('contenteditable', 'true'); 3515 } 3516 }; 3517 }) 3518 .directive('crmUiIconPicker', function($timeout) { 3519 return { 3520 restrict: 'A', 3521 require: '?ngModel', // Soft require ngModel 3522 controller: function($element, $scope, $attrs) { 3523 CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').then(function() { 3524 $timeout(function() { 3525 $element.crmIconPicker(); 3526 if ($attrs.ngModel) { 3527 $scope.$watch($attrs.ngModel, function(newValue) { 3528 if (newValue !== undefined) { 3529 $element.val(newValue).trigger('change'); 3530 } 3531 }); 3532 } 3533 }); 3534 }); 3535 } 3536 }; 3537 }) 3538 .factory('formatForSelect2', function() { 3539 return function(input, key, label, extra) { 3540 return input.reduce((result, item) => { 3541 const formatted = {id: item[key], text: item[label]}; 3542 3543 if (extra) { 3544 extra.forEach(prop => formatted[prop] = item[prop]); 3545 } 3546 3547 result.push(formatted); 3548 return result; 3549 }, []); 3550 }; 3551 }) 3552 3553 .run(function($rootScope, $location) { 3554 $rootScope.goto = function(path) { 3555 $location.path(path); 3556 }; 3557 }); 3558 3559
3560angular.module('crmUtil', CRM.angRequires('crmUtil')); 3561 angular.module('crmUtil').factory('crmApi', function($q) { 3562 var crmApi = function(entity, action, params, message) { 3563 var deferred = $q.defer(); 3564 var p; 3565 var backend = crmApi.backend || CRM.api3; 3566 if (params && params.body_html) { 3567 params.body_html = params.body_html.replace(/([\u2028]|[\u2029])/g, '\n'); 3568 } 3569 if (_.isObject(entity)) { 3570 /*jshint -W061 */ 3571 p = backend(eval('('+angular.toJson(entity)+')'), action); 3572 } else { 3573 /*jshint -W061 */ 3574 p = backend(entity, action, eval('('+angular.toJson(params)+')'), message); 3575 } 3576 p.then( 3577 function(result) { 3578 if (result.is_error) { 3579 deferred.reject(result); 3580 } else { 3581 deferred.resolve(result); 3582 } 3583 }, 3584 function(error) { 3585 deferred.reject(error); 3586 } 3587 ); 3588 return deferred.promise; 3589 }; 3590 crmApi.backend = null; 3591 crmApi.val = function(value) { 3592 var d = $.Deferred(); 3593 d.resolve(value); 3594 return d.promise(); 3595 }; 3596 return crmApi; 3597 }); 3598 angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) { 3599 function convertOptionsToMap(options) { 3600 var result = {}; 3601 angular.forEach(options, function(o) { 3602 result[o.key] = o.value; 3603 }); 3604 return result; 3605 } 3606 3607 var cache = {}; // cache[entityName+'::'+action][fieldName].title 3608 var deferreds = {}; // deferreds[cacheKey].push($q.defer()) 3609 var crmMetadata = { 3610 getField: function getField(entity, field) { 3611 return $q.when(crmMetadata.getFields(entity)).then(function(fields){ 3612 return fields[field]; 3613 }); 3614 }, 3615 getFields: function getFields(entity) { 3616 var action = '', cacheKey; 3617 if (_.isArray(entity)) { 3618 action = entity[1]; 3619 entity = entity[0]; 3620 cacheKey = entity + '::' + action; 3621 } else { 3622 cacheKey = entity; 3623 } 3624 3625 if (_.isObject(cache[cacheKey])) { 3626 return cache[cacheKey]; 3627 } 3628 3629 var needFetch = _.isEmpty(deferreds[cacheKey]); 3630 deferreds[cacheKey] = deferreds[cacheKey] || []; 3631 var deferred = $q.defer(); 3632 deferreds[cacheKey].push(deferred); 3633 3634 if (needFetch) { 3635 crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}}) 3636 .then( 3637 function(fields) { 3638 cache[cacheKey] = _.indexBy(fields.values, 'name'); 3639 angular.forEach(cache[cacheKey],function (field){ 3640 if (field.options) { 3641 field.optionsMap = convertOptionsToMap(field.options); 3642 } 3643 }); 3644 angular.forEach(deferreds[cacheKey], function(dfr) { 3645 dfr.resolve(cache[cacheKey]); 3646 }); 3647 delete deferreds[cacheKey]; 3648 }, 3649 function() { 3650 cache[cacheKey] = {}; // cache nack 3651 angular.forEach(deferreds[cacheKey], function(dfr) { 3652 dfr.reject(); 3653 }); 3654 delete deferreds[cacheKey]; 3655 } 3656 ); 3657 } 3658 3659 return deferred.promise; 3660 } 3661 }; 3662 3663 return crmMetadata; 3664 }); 3665 angular.module('crmUtil').factory('crmBlocker', function() { 3666 return function() { 3667 var blocks = 0; 3668 var result = function(promise) { 3669 blocks++; 3670 return promise.finally(function() { 3671 blocks--; 3672 }); 3673 }; 3674 result.check = function() { 3675 return blocks > 0; 3676 }; 3677 return result; 3678 }; 3679 }); 3680 3681 angular.module('crmUtil').factory('crmLegacy', function() { 3682 return CRM; 3683 }); 3684 angular.module('crmUtil').factory('crmLog', function(){ 3685 var level = 0; 3686 var write = console.log; 3687 function indent() { 3688 var s = '>'; 3689 for (var i = 0; i < level; i++) s = s + ' '; 3690 return s; 3691 } 3692 var crmLog = { 3693 log: function(msg, vars) { 3694 write(indent() + msg, vars); 3695 }, 3696 wrap: function(label, f) { 3697 return function(){ 3698 level++; 3699 crmLog.log(label + ": start", arguments); 3700 var r; 3701 try { 3702 r = f.apply(this, arguments); 3703 } finally { 3704 crmLog.log(label + ": end"); 3705 level--; 3706 } 3707 return r; 3708 }; 3709 } 3710 }; 3711 return crmLog; 3712 }); 3713
3714 angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) { 3715 return { 3716 redirect: function(path) { 3717 $window.location.href = path; 3718 } 3719 }; 3720 }]); 3721 angular.module('crmUtil').factory('crmQueue', function($q) { 3722 return function crmQueue(worker) { 3723 var queue = []; 3724 function next() { 3725 var task = queue[0]; 3726 worker.apply(null, task.a).then( 3727 function onOk(data) { 3728 queue.shift(); 3729 task.dfr.resolve(data); 3730 if (queue.length > 0) next(); 3731 }, 3732 function onErr(err) { 3733 queue.shift(); 3734 task.dfr.reject(err); 3735 if (queue.length > 0) next(); 3736 } 3737 ); 3738 } 3739 function enqueue() { 3740 var dfr = $q.defer(); 3741 queue.push({a: arguments, dfr: dfr}); 3742 if (queue.length === 1) { 3743 next(); 3744 } 3745 return dfr.promise; 3746 } 3747 return enqueue; 3748 }; 3749 }); 3750 angular.module('crmUtil').factory('crmStatus', function($q){ 3751 return function(options, aPromise){ 3752 if (aPromise) { 3753 return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise))); 3754 } else { 3755 return CRM.toAPromise($q, CRM.status(options)); 3756 } 3757 }; 3758 }); 3759 angular.module('crmUtil').factory('crmWatcher', function(){ 3760 return function() { 3761 var unwatches = {}, watchFactories = {}, suspends = {}; 3762 this.setup = function(name, newWatchFactory) { 3763 watchFactories[name] = newWatchFactory; 3764 unwatches[name] = watchFactories[name](); 3765 suspends[name] = 0; 3766 return this; 3767 }; 3768 this.suspend = function(name, f) { 3769 suspends[name]++; 3770 this.teardown(name); 3771 var r; 3772 try { 3773 r = f.apply(this, []); 3774 } finally { 3775 if (suspends[name] === 1) { 3776 unwatches[name] = watchFactories[name](); 3777 if (!angular.isArray(unwatches[name])) { 3778 unwatches[name] = [unwatches[name]]; 3779 } 3780 } 3781 suspends[name]--; 3782 } 3783 return r; 3784 }; 3785 3786 this.teardown = function(name) { 3787 if (!unwatches[name]) return; 3788 _.each(unwatches[name], function(unwatch){ 3789 unwatch(); 3790 }); 3791 delete unwatches[name]; 3792 }; 3793 3794 return this; 3795 }; 3796 }); 3797 angular.module('crmUtil').factory('crmThrottle', function($q) { 3798 var pending = [], 3799 executing = []; 3800 return function(func) { 3801 var deferred = $q.defer(); 3802 3803 function checkResult(result, success) { 3804 _.pull(executing, func); 3805 if (_.includes(pending, func)) { 3806 runNext(); 3807 } else if (success) { 3808 deferred.resolve(result); 3809 } else { 3810 deferred.reject(result); 3811 } 3812 } 3813 3814 function runNext() { 3815 executing.push(func); 3816 _.pull(pending, func); 3817 func().then(function(result) { 3818 checkResult(result, true); 3819 }, function(result) { 3820 checkResult(result, false); 3821 }); 3822 } 3823 3824 if (!_.includes(executing, func)) { 3825 runNext(); 3826 } else if (!_.includes(pending, func)) { 3827 pending.push(func); 3828 } 3829 return deferred.promise; 3830 }; 3831 }); 3832 3833 angular.module('crmUtil').factory('crmLoadScript', function($q) { 3834 return function(url) { 3835 var deferred = $q.defer(); 3836 3837 CRM.loadScript(url).done(function() { 3838 deferred.resolve(true); 3839 }); 3840 3841 return deferred.promise; 3842 }; 3843 }); 3844 angular.module('crmUtil').filter('encodeURIComponent', function() { 3845 return function(input) { 3846 return input ? encodeURIComponent(input) : ''; 3847 }; 3848 }); 3849 3850 3851 3852 /** 3853 * Allows using ng-model with a custom element like civi-rich-text-input 3854 */ 3855 angular.module("crmUtil").directive("crmCustomElementModel", () => ({ 3856 restrict: "A", 3857 require: 'ngModel', 3858 link: function ($scope, $element, attributes, ngModelController) { 3859 const element = $element[0]; 3860 ngModelController.$render = () => element.value = ngModelController.$viewValue; 3861 element.addEventListener('change', () => ngModelController.$setViewValue(element.value)); 3862 } 3863 })); 3864 3865})(angular, CRM.$, CRM._); 3866 3867/* 3868 AngularJS v1.8.0 3869 (c) 2010-2020 Google, Inc. http://angularjs.org 3870 License: MIT 3871*/ 3872(function(s,c){'use strict';function P(c){var h=[];C(h,E).chars(c);return h.join("")}var D=c.$$minErr("$sanitize"),F,h,G,H,I,q,E,J,K,C;c.module("ngSanitize",[]).provider("$sanitize",function(){function f(a,e){return B(a.split(","),e)}function B(a,e){var d={},b;for(b=0;b<a.length;b++)d[e?q(a[b]):a[b]]=!0;return d}function t(a,e){e&&e.length&&h(a,B(e))}function Q(a){for(var e={},d=0,b=a.length;d<b;d++){var k=a[d];e[k.name]=k.value}return e}function L(a){return a.replace(/&/g,"&").replace(z,function(a){var d=
vendor: 4,311 bytes, lines 3873-3879
3873a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(d-55296)+(a-56320)+65536)+";"}).replace(u,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}function A(a){for(;a;){if(a.nodeType===s.Node.ELEMENT_NODE)for(var e=a.attributes,d=0,b=e.length;d<b;d++){var k=e[d],g=k.name.toLowerCase();if("xmlns:ns1"===g||0===g.lastIndexOf("ns1:",0))a.removeAttributeNode(k),d--,b--}(e=a.firstChild)&&A(e);a=v("nextSibling",a)}}function v(a,e){var d=e[a];if(d&&J.call(e,d))throw D("elclob", 3874e.outerHTML||e.outerText);return d}var y=!1,g=!1;this.$get=["$$sanitizeUri",function(a){y=!0;g&&h(m,l);return function(e){var d=[];K(e,C(d,function(b,d){return!/^unsafe:/.test(a(b,d))}));return d.join("")}}];this.enableSvg=function(a){return I(a)?(g=a,this):g};this.addValidElements=function(a){y||(H(a)&&(a={htmlElements:a}),t(l,a.svgElements),t(r,a.htmlVoidElements),t(m,a.htmlVoidElements),t(m,a.htmlElements));return this};this.addValidAttrs=function(a){y||h(M,B(a,!0));return this};F=c.bind;h=c.extend; 3875G=c.forEach;H=c.isArray;I=c.isDefined;q=c.$$lowercase;E=c.noop;K=function(a,e){null===a||void 0===a?a="":"string"!==typeof a&&(a=""+a);var d=N(a);if(!d)return"";var b=5;do{if(0===b)throw D("uinput");b--;a=d.innerHTML;d=N(a)}while(a!==d.innerHTML);for(b=d.firstChild;b;){switch(b.nodeType){case 1:e.start(b.nodeName.toLowerCase(),Q(b.attributes));break;case 3:e.chars(b.textContent)}var k;if(!(k=b.firstChild)&&(1===b.nodeType&&e.end(b.nodeName.toLowerCase()),k=v("nextSibling",b),!k))for(;null==k;){b= 3876v("parentNode",b);if(b===d)break;k=v("nextSibling",b);1===b.nodeType&&e.end(b.nodeName.toLowerCase())}b=k}for(;b=d.firstChild;)d.removeChild(b)};C=function(a,e){var d=!1,b=F(a,a.push);return{start:function(a,g){a=q(a);!d&&w[a]&&(d=a);d||!0!==m[a]||(b("<"),b(a),G(g,function(d,g){var c=q(g),f="img"===a&&"src"===c||"background"===c;!0!==M[c]||!0===O[c]&&!e(d,f)||(b(" "),b(g),b('="'),b(L(d)),b('"'))}),b(">"))},end:function(a){a=q(a);d||!0!==m[a]||!0===r[a]||(b("</"),b(a),b(">"));a==d&&(d=!1)},chars:function(a){d|| 3877b(L(a))}}};J=s.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)};var z=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,u=/([^#-~ |!])/g,r=f("area,br,col,hr,img,wbr"),x=f("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),p=f("rp,rt"),n=h({},p,x),x=h({},x,f("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")),p=h({},p,f("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 3878l=f("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan"),w=f("script,style"),m=h({},r,x,p,n),O=f("background,cite,href,longdesc,src,xlink:href,xml:base"),n=f("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,valign,value,vspace,width"), 3879p=f("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode
3879-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan", 3880!0),M=h({},O,p,n),N=function(a,e){function d(b){b="<remove></remove>"+b;try{var d=(new a.DOMParser).parseFromString(b,"text/html").body;d.firstChild.remove();return d}catch(e){}}function b(a){c.innerHTML=a;e.documentMode&&A(c);return c}var g;if(e&&e.implementation)g=e.implementation.createHTMLDocument("inert");else throw D("noinert");var c=(g.documentElement||g.getDocumentElement()).querySelector("body");c.innerHTML='<svg><g onload="this.parentNode.remove()"></g></svg>';return c.querySelector("svg")? 3881(c.innerHTML='<svg><p><style><img src="</style><img src=x onerror=alert(1)//">',c.querySelector("svg img")?d:b):function(b){b="<remove></remove>"+b;try{b=encodeURI(b)}catch(d){return}var e=new a.XMLHttpRequest;e.responseType="document";e.open("GET","data:text/html;charset=utf-8,"+b,!1);e.send(null);b=e.response.body;b.firstChild.remove();return b}}(s,s.document)}).info({angularVersion:"1.8.0"});c.module("ngSanitize").filter("linky",["$sanitize",function(f){var h=/((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, 3882t=/^mailto:/i,q=c.$$minErr("linky"),s=c.isDefined,A=c.isFunction,v=c.isObject,y=c.isString;return function(c,z,u){function r(c){c&&l.push(P(c))}function x(c,g){var f,a=p(c);l.push("<a ");for(f in a)l.push(f+'="'+a[f]+'" ');!s(z)||"target"in a||l.push('target="',z,'" ');l.push('href="',c.replace(/"/g,"""),'">');r(g);l.push("</a>")}if(null==c||""===c)return c;if(!y(c))throw q("notstring",c);for(var p=A(u)?u:v(u)?function(){return u}:function(){return{}},n=c,l=[],w,m;c=n.match(h);)w=c[0],c[2]|| 3883c[4]||(w=(c[3]?"http://":"mailto:")+w),m=c.index,r(n.substr(0,m)),x(w,c[0].replace(t,"")),n=n.substring(m+c[0].length);r(n);return f(l.join(""))}}])})(window,window.angular);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.