PageSourceSearch

https://www.youthrights.org/wp-content/uploads/civicrm/persist/con…modules.07f7ce5dd8d380641ae0153e866aa779.js

js youthrights.org collected 2026-10-02 05:38:06 UTC 158,559 bytes, 3,883 lines download raw bytes

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,"&amp;").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,"&lt;").replace(/>/g,"&gt;")}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,"&quot;"),'">');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.