Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ang/crmUtil.ang.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// This file declares an Angular module which can be autoloaded
return [
'ext' => 'civicrm',
'js' => ['ang/crmUtil.js'],
'js' => [
'ang/crmUtil.js',
'ang/crmUtil/*.js',
],
'requires' => [],
];
21 changes: 21 additions & 0 deletions ang/crmUtil/crmCustomElementModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// https://civicrm.org/licensing
(function (angular, $, _) {
"use strict";

/**
* Allows using ng-model with a custom element like civi-rich-text-input
*/
angular.module("crmUtil").directive("crmCustomElementModel", () => ({
restrict: "A",
require: 'ngModel',
link: function ($scope, $element, attributes, ngModelController) {
// get the native element
const element = $element[0];
// update element value from angular in render hook
ngModelController.$render = () => element.value = ngModelController.$viewValue;
// update angular value from element when element is changed
element.addEventListener('change', () => ngModelController.$setViewValue(element.value));
}
}));

})(angular, CRM.$, CRM._);
5 changes: 5 additions & 0 deletions ext/afform/admin/ang/afGuiEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -713,3 +713,8 @@ body.af-gui-dragging {
#bootstrap-theme.af-gui-conditional-dialog .api4-operator {
width: 110px;
}

/* hide label token pickers until editing */
.af-gui-node-title:not(:focus-within) af-gui-token-select[field="label"] {
visibility: hidden;
}
65 changes: 64 additions & 1 deletion ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
mode: '@'
},
controllerAs: 'editor',
controller: function($scope, crmApi4, crmUiHelp, afGui, $parse, $timeout, $location, $route, $rootScope, formatForSelect2) {
controller: function($scope, $element, crmApi4, crmUiHelp, afGui, $parse, $timeout, $location, $route, $rootScope, formatForSelect2) {
const ts = $scope.ts = CRM.ts('org.civicrm.afform_admin');
$scope.hs = crmUiHelp({file: 'CRM/AfformAdmin/afformBuilder'});

Expand Down Expand Up @@ -711,6 +711,9 @@
};

$scope.save = function() {
// save and close any open rich text elements
$element[0].querySelectorAll('civi-rich-text-input[editing]').forEach((el) => el.saveAndCloseEditor());

const afform = JSON.parse(angular.toJson(editor.afform));
// This might be set to undefined by validation
afform.server_route = afform.server_route || '';
Expand Down Expand Up @@ -804,6 +807,66 @@
return $location.path(newPath);
}

this.getTokens = (includeSubmissionTokens = false) => {
const allTokens = [];
this.getEntities().forEach((entity) => {
const entityTokens = [];
const entityMeta = this.meta.entities[entity.type];
if (entityMeta.submissionTokens && includeSubmissionTokens) {
// Explicitly defined submission tokens e.g. by FormProcessor extension
entityMeta.submissionTokens.forEach((submissionToken) => {
entityTokens.push({
id: entity.name + '.0.' + submissionToken.token,
text: entity.label + ' ' + submissionToken.label,
description: submissionToken.description ?? '',
});
});
} else if (!entityMeta.submissionTokens) {
// Primary key token
// FIXME: not all entities use `id` for primary key
if (includeSubmissionTokens) {
entityTokens.push({
id: entity.name + '.0.id',
text: ts('%1 ID', {1: entity.label}),
});
}
// Tokens from entity data values
if (entity.data) {
Object.keys(entity.data).forEach((key) => {
if (entityMeta.fields[key]) {
entityTokens.push({
id: entity.name + '.0.' + key,
text: entity.label + ' ' + entityMeta.fields[key].label,
});
}
});
}
// Tokens from entity fields on the form
this.getEntityFields(entity.name).fields.forEach((field) => {
entityTokens.push({
id: entity.name + '.0.' + field.name,
text: entity.label + ' ' + field.label,
});
});
}
if (entityTokens.length) {
allTokens.push({
text: entity.label,
children: entityTokens,
});
}
});
if (includeSubmissionTokens) {
allTokens.push({
text: ts('Form'),
children: [
{id: 'token', text: ts('Submission JWT')},
],
});
}
return allTokens;
};

}
});

Expand Down
64 changes: 3 additions & 61 deletions ext/afform/admin/ang/afGuiEditor/afGuiTokenSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,73 +49,15 @@
}
};

this.getTokens = function() {
const allTokens = [];
ctrl.editor.getEntities().forEach((entity) => {
const entityTokens = [];
const entityMeta = ctrl.editor.meta.entities[entity.type];
if (entityMeta.submissionTokens && !ctrl.noSubmissionTokens) {
// Explicitly defined submission tokens e.g. by FormProcessor extension
entityMeta.submissionTokens.forEach((submissionToken) => {
entityTokens.push({
id: entity.name + '.0.' + submissionToken.token,
text: entity.label + ' ' + submissionToken.label,
description: submissionToken.description ?? '',
});
});
} else if (!entityMeta.submissionTokens) {
// Primary key token
if (!ctrl.noSubmissionTokens) {
entityTokens.push({
id: entity.name + '.0.id',
text: ts('%1 ID', {1: entity.label}),
});
}
// Tokens from entity data values
if (entity.data) {
Object.keys(entity.data).forEach((key) => {
if (entityMeta.fields[key]) {
entityTokens.push({
id: entity.name + '.0.' + key,
text: entity.label + ' ' + entityMeta.fields[key].label,
});
}
});
}
// Tokens from entity fields on the form
ctrl.editor.getEntityFields(entity.name).fields.forEach((field) => {
entityTokens.push({
id: entity.name + '.0.' + field.name,
text: entity.label + ' ' + field.label,
});
});
}
if (entityTokens.length) {
allTokens.push({
text: entity.label,
children: entityTokens,
});
}
});
if (!ctrl.noSubmissionTokens) {
allTokens.push({
text: ts('Form'),
children: [
{id: 'token', text: ts('Submission JWT')},
],
});
}
return {
results: allTokens
};
};
this.getTokens = () => ({
results: this.editor.getTokens(!this.noSubmissionTokens),
});

this.tokenSelectSettings = {
data: this.getTokens,
// The crm-action-menu icon doesn't show without a placeholder
placeholder: ' ',
};

}
});

Expand Down
10 changes: 8 additions & 2 deletions ext/afform/admin/ang/afGuiEditor/config-form.html
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,14 @@
<label for="af_config_confirmation_message">
{{:: editor.meta.afform_fields.confirmation_message.title }}
</label>
<af-gui-token-select class="pull-right" model="editor.afform" field="confirmation_message"></af-gui-token-select>
<textarea ng-model="editor.afform.confirmation_message" class="form-control" id="af_config_confirmation_message" ng-model-options="editor.debounceMode"></textarea>
<civi-rich-text-input
id="af_config_confirmation_message"
class="form-control"
crm-custom-element-model
ng-model="editor.afform.confirmation_message"
token-picker
include-submission-tokens
></civi-rich-text-input>
<p class="help-block">{{:: ts("This message wil be displayed when the form is submitted.") }}</p>
</div>

Expand Down
13 changes: 8 additions & 5 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiField.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
</div>
</div>
</div>
<label ng-style="{visibility: $ctrl.showLabel() ? 'visible' : 'hidden'}" ng-class="{'af-gui-field-required': getProp('required')}" class="af-gui-node-title">
<span crm-ui-editable ng-model="getSet('label')" ng-model-options="{getterSetter: true}" default-value="$ctrl.getDefn().label">{{ getProp('label') }}</span>
</label>
<div class="af-gui-node-title" ng-show="$ctrl.showLabel()">
<label ng-show="$ctrl.showLabel()" ng-class="{'af-gui-field-required': getProp('required')}">
<span crm-ui-editable ng-model="getSet('label')" ng-model-options="{getterSetter: true}" default-value="$ctrl.getDefn().label">{{ getProp('label') }}</span>
</label>
<af-gui-token-select model="getSet" field="label" no-submission-tokens="true"></af-gui-token-select>
</div>
<div class="af-gui-field-help" ng-if="propIsset('help_pre')">
<span crm-ui-editable ng-model="getSet('help_pre')" ng-model-options="{getterSetter: true}" default-value="$ctrl.getDefn().help_pre">{{ getProp('help_pre') }}</span>
<civi-rich-text-input token-picker crm-custom-element-model ng-model="getSet('help_pre')"></civi-rich-text-input>
</div>
<div class="af-gui-field-input af-gui-field-input-type-{{ getProp('input_type').toLowerCase() }}" ng-include="$ctrl.getInputTypeTemplate()"></div>
<div class="af-gui-field-help" ng-if="propIsset('help_post')">
<span crm-ui-editable ng-model="getSet('help_post')" ng-model-options="{getterSetter: true}" default-value="$ctrl.getDefn().help_post">{{ getProp('help_post') }}</span>
<civi-rich-text-input token-picker crm-custom-element-model ng-model="getSet('help_post')"></civi-rich-text-input>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<li>
<a href ng-click="edit()">{{:: ts('Edit content') }}</a>
<a href ng-click="$ctrl.edit()">{{:: ts('Edit content') }}</a>
</li>

<li role="separator" class="divider"></li>
Expand Down
43 changes: 7 additions & 36 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,19 @@
require: {
editor: '^^afGuiEditor',
},
controller: function($scope, $sce, $timeout) {
const ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'),
ctrl = this;
controller: function($element, $scope) {
const ts = $scope.ts = CRM.ts('org.civicrm.afform_admin');

this.$onInit = function() {
// CRM.wysiwyg doesn't work without a dom id
$scope.id = 'af-markup-editor-' + richtextId++;
this.$onInit = () => {

// When creating a new markup container, go straight to edit mode
$timeout(() => {
if (ctrl.node['#markup'] === false) {
$scope.edit();
}
});
};

$scope.getMarkup = function() {
return $sce.trustAsHtml(ctrl.node['#markup'] || '');
};

$scope.edit = function() {
$('#afGuiEditor').addClass('af-gui-editing-content');
$scope.editingMarkup = true;
CRM.wysiwyg.create('#' + $scope.id);
CRM.wysiwyg.setVal('#' + $scope.id, ctrl.node['#markup'] || '<p></p>');
if (!this.node['#markup']) {
this.edit();
}
};

$scope.save = function() {
ctrl.node['#markup'] = CRM.wysiwyg.getVal('#' + $scope.id);
$scope.close();
};
this.edit = () => $element[0].querySelector('civi-rich-text-input').openEditor();

$scope.close = function() {
CRM.wysiwyg.destroy('#' + $scope.id);
$('#afGuiEditor').removeClass('af-gui-editing-content');
// If a newly-added wysiwyg was canceled, just remove it
if (ctrl.node['#markup'] === false) {
$scope.container.removeElement(ctrl.node);
} else {
$scope.editingMarkup = false;
}
};
}
});

Expand Down
26 changes: 5 additions & 21 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div class="af-gui-bar">
<div class="form-inline pull-right">
<div class="form-inline">
<span>{{:: ts('Rich content') }}</span>
<div class="btn-group pull-right" af-gui-menu>
<button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button" data-toggle="dropdown" title="{{:: ts('Configure') }}">
<span><i class="crm-i fa-gear" role="img" aria-hidden="true"></i></span>
Expand All @@ -8,23 +9,6 @@
</div>
</div>
</div>
<div ng-if="!editingMarkup" ng-click="edit()" class="af-gui-markup-content crm-editable-enabled {{ $ctrl.node['class'] }}">
<div ng-bind-html="getMarkup()" style="{{ $ctrl.node.style }}"></div>
<div class="af-gui-markup-content-overlay"></div>
</div>
<div class="af-gui-content-editing-area">
<div class="af-gui-edit-options-bar" ng-if="editingMarkup">
<h4 class="pull-left">{{:: ts('Editing content') }}</h4>
<div class="btn-group-sm pull-right">
<button type="button" class="btn btn-default" ng-click="close()">
<i class="crm-i fa-times-circle" role="img" aria-hidden="true"></i>
{{:: ts('Cancel') }}
</button>
<button type="button" class="btn btn-success" ng-click="save()">
<i class="crm-i fa-check-circle" role="img" aria-hidden="true"></i>
{{:: ts('Done') }}
</button>
</div>
</div>
<textarea id="{{ id }}" ng-show="editingMarkup"></textarea>
</div>
<div class="af-gui-markup-content {{ $ctrl.node['class'] }}" style="{{ $ctrl.node.style }}">
<civi-rich-text-input token-picker crm-custom-element-model ng-model="$ctrl.node['#markup']"></civi-rich-text-input>
</div>
10 changes: 7 additions & 3 deletions ext/afform/core/ang/af/afField.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<label class="crm-af-field-label" ng-if=":: $ctrl.defn.label && !($ctrl.defn.input_type == 'CheckBox' && $ctrl.defn.data_type == 'Boolean')" for="{{:: fieldId }}">
{{:: $ctrl.defn.label }}
<af-markup markup="{{:: $ctrl.defn.label }}"></af-markup>
<span class="crm-marker" title="{{:: ts('Required') }}" ng-if=":: $ctrl.defn.required">*</span>
</label>
<p class="crm-af-field-help-pre" ng-if=":: $ctrl.defn.help_pre">{{:: $ctrl.defn.help_pre }}</p>
<p class="crm-af-field-help-pre" ng-if=":: $ctrl.defn.help_pre">
<af-markup markup="{{:: $ctrl.defn.help_pre }}" /></af-markup>
</p>
<div class="crm-af-field" ng-if="!$ctrl.defn.expose_operator" ng-include="$ctrl.defn.template"></div>
<div class="input-group" ng-if="$ctrl.defn.expose_operator" ng-include="'~/af/afFieldWithSearchOperator.html'"></div>
<p class="crm-af-field-help-post" ng-if=":: $ctrl.defn.help_post">{{:: $ctrl.defn.help_post }}</p>
<p class="crm-af-field-help-post" ng-if=":: $ctrl.defn.help_post">
<af-markup markup="{{:: $ctrl.defn.help_post }}" /></af-markup>
</p>
Loading