From ab3c1d83f0a5a3b92cbec4d0ee296e51b9629bd4 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 8 Jan 2021 14:24:54 -0500 Subject: [PATCH] Afform - Refactor elements as components & move to their own files --- ext/afform/admin/ang/afGuiEditor.ang.php | 1 + ext/afform/admin/ang/afGuiEditor.css | 7 + ext/afform/admin/ang/afGuiEditor.js | 570 ------------------ .../ang/afGuiEditor/afGuiEditorCanvas.html | 2 +- .../admin/ang/afGuiEditor/container.html | 37 -- .../afGuiButton-menu.html} | 2 +- .../elements/afGuiButton.component.js | 41 ++ .../afGuiButton.html} | 8 +- .../afGuiContainer-menu.html} | 18 +- .../elements/afGuiContainer.component.js | 268 ++++++++ .../afGuiEditor/elements/afGuiContainer.html | 37 ++ .../afGuiField-menu.html} | 10 +- .../elements/afGuiField.component.js | 159 +++++ .../{field.html => elements/afGuiField.html} | 4 +- .../elements/afGuiMarkup-menu.html | 10 + .../elements/afGuiMarkup.component.js | 58 ++ .../afGuiMarkup.html} | 6 +- .../afGuiText-menu.html} | 2 +- .../elements/afGuiText.component.js | 56 ++ .../{text.html => elements/afGuiText.html} | 6 +- .../admin/ang/afGuiEditor/markup-menu.html | 10 - 21 files changed, 666 insertions(+), 646 deletions(-) delete mode 100644 ext/afform/admin/ang/afGuiEditor/container.html rename ext/afform/admin/ang/afGuiEditor/{button-menu.html => elements/afGuiButton-menu.html} (80%) create mode 100644 ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.component.js rename ext/afform/admin/ang/afGuiEditor/{button.html => elements/afGuiButton.html} (72%) rename ext/afform/admin/ang/afGuiEditor/{container-menu.html => elements/afGuiContainer-menu.html} (60%) create mode 100644 ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js create mode 100644 ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html rename ext/afform/admin/ang/afGuiEditor/{field-menu.html => elements/afGuiField-menu.html} (78%) create mode 100644 ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js rename ext/afform/admin/ang/afGuiEditor/{field.html => elements/afGuiField.html} (85%) create mode 100644 ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup-menu.html create mode 100644 ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.component.js rename ext/afform/admin/ang/afGuiEditor/{markup.html => elements/afGuiMarkup.html} (86%) rename ext/afform/admin/ang/afGuiEditor/{text-menu.html => elements/afGuiText-menu.html} (91%) create mode 100644 ext/afform/admin/ang/afGuiEditor/elements/afGuiText.component.js rename ext/afform/admin/ang/afGuiEditor/{text.html => elements/afGuiText.html} (62%) delete mode 100644 ext/afform/admin/ang/afGuiEditor/markup-menu.html diff --git a/ext/afform/admin/ang/afGuiEditor.ang.php b/ext/afform/admin/ang/afGuiEditor.ang.php index 38d8ffea48..99c6a5e70a 100644 --- a/ext/afform/admin/ang/afGuiEditor.ang.php +++ b/ext/afform/admin/ang/afGuiEditor.ang.php @@ -4,6 +4,7 @@ return [ 'js' => [ 'ang/afGuiEditor.js', 'ang/afGuiEditor/*.js', + 'ang/afGuiEditor/*/*.js', ], 'css' => ['ang/afGuiEditor.css'], 'partials' => ['ang/afGuiEditor'], diff --git a/ext/afform/admin/ang/afGuiEditor.css b/ext/afform/admin/ang/afGuiEditor.css index e99b30e903..fd9a9d133c 100644 --- a/ext/afform/admin/ang/afGuiEditor.css +++ b/ext/afform/admin/ang/afGuiEditor.css @@ -132,6 +132,7 @@ #afGuiEditor .af-gui-element { position: relative; padding: 0 3px 3px; + display: block; } #afGuiEditor .af-gui-container { @@ -139,6 +140,12 @@ position: relative; padding: 22px 3px 3px; min-height: 40px; + display: block; +} + +#afGuiEditor af-gui-markup, +#afGuiEditor af-gui-field { + display: block; } #afGuiEditor .af-gui-container-type-fieldset { diff --git a/ext/afform/admin/ang/afGuiEditor.js b/ext/afform/admin/ang/afGuiEditor.js index c3a8f905d7..30db86b55d 100644 --- a/ext/afform/admin/ang/afGuiEditor.js +++ b/ext/afform/admin/ang/afGuiEditor.js @@ -139,273 +139,6 @@ }); }); - angular.module('afGuiEditor').directive('afGuiContainer', function(crmApi4, dialogService, afAdmin) { - return { - restrict: 'A', - templateUrl: '~/afGuiEditor/container.html', - scope: { - node: '=afGuiContainer', - join: '=', - entityName: '=' - }, - require: ['^^afGuiEditor', '?^^afGuiContainer'], - link: function($scope, element, attrs, ctrls) { - var ts = $scope.ts = CRM.ts(); - $scope.editor = ctrls[0]; - $scope.parentContainer = ctrls[1]; - - $scope.isSelectedFieldset = function(entityName) { - return entityName === $scope.editor.getSelectedEntityName(); - }; - - $scope.selectEntity = function() { - if ($scope.node['af-fieldset']) { - $scope.editor.selectEntity($scope.node['af-fieldset']); - } - }; - - $scope.tags = { - div: ts('Container'), - fieldset: ts('Fieldset') - }; - - // Block settings - var block = {}; - $scope.block = null; - - $scope.getSetChildren = function(val) { - var collection = block.layout || ($scope.node && $scope.node['#children']); - return arguments.length ? (collection = val) : collection; - }; - - $scope.isRepeatable = function() { - return $scope.node['af-fieldset'] || (block.directive && $scope.editor.meta.blocks[block.directive].repeat) || $scope.join; - }; - - $scope.toggleRepeat = function() { - if ('af-repeat' in $scope.node) { - delete $scope.node.max; - delete $scope.node.min; - delete $scope.node['af-repeat']; - delete $scope.node['add-icon']; - } else { - $scope.node.min = '1'; - $scope.node['af-repeat'] = ts('Add'); - } - }; - - $scope.getSetMin = function(val) { - if (arguments.length) { - if ($scope.node.max && val > parseInt($scope.node.max, 10)) { - $scope.node.max = '' + val; - } - if (!val) { - delete $scope.node.min; - } - else { - $scope.node.min = '' + val; - } - } - return $scope.node.min ? parseInt($scope.node.min, 10) : null; - }; - - $scope.getSetMax = function(val) { - if (arguments.length) { - if ($scope.node.min && val && val < parseInt($scope.node.min, 10)) { - $scope.node.min = '' + val; - } - if (typeof val !== 'number') { - delete $scope.node.max; - } - else { - $scope.node.max = '' + val; - } - } - return $scope.node.max ? parseInt($scope.node.max, 10) : null; - }; - - $scope.pickAddIcon = function() { - afAdmin.pickIcon().then(function(val) { - $scope.node['add-icon'] = val; - }); - }; - - function getBlockNode() { - return !$scope.join ? $scope.node : ($scope.node['#children'] && $scope.node['#children'].length === 1 ? $scope.node['#children'][0] : null); - } - - function setBlockDirective(directive) { - if ($scope.join) { - $scope.node['#children'] = [{'#tag': directive}]; - } else { - delete $scope.node['#children']; - delete $scope.node['class']; - $scope.node['#tag'] = directive; - } - } - - function overrideBlockContents(layout) { - $scope.node['#children'] = layout || []; - if (!$scope.join) { - $scope.node['#tag'] = 'div'; - $scope.node['class'] = 'af-container'; - } - block.layout = block.directive = null; - } - - $scope.layouts = { - 'af-layout-rows': ts('Contents display as rows'), - 'af-layout-cols': ts('Contents are evenly-spaced columns'), - 'af-layout-inline': ts('Contents are arranged inline') - }; - - $scope.getLayout = function() { - if (!$scope.node) { - return ''; - } - return _.intersection(afAdmin.splitClass($scope.node['class']), _.keys($scope.layouts))[0] || 'af-layout-rows'; - }; - - $scope.setLayout = function(val) { - var classes = ['af-container']; - if (val !== 'af-layout-rows') { - classes.push(val); - } - afAdmin.modifyClasses($scope.node, _.keys($scope.layouts), classes); - }; - - $scope.selectBlockDirective = function() { - if (block.directive) { - block.layout = _.cloneDeep($scope.editor.meta.blocks[block.directive].layout); - block.original = block.directive; - setBlockDirective(block.directive); - } - else { - overrideBlockContents(block.layout); - } - }; - - if (($scope.node['#tag'] in $scope.editor.meta.blocks) || $scope.join) { - initializeBlockContainer(); - } - - function initializeBlockContainer() { - - // Cancel the below $watch expressions if already set - _.each(block.listeners, function(deregister) { - deregister(); - }); - - block = $scope.block = { - directive: null, - layout: null, - original: null, - options: [], - listeners: [] - }; - - _.each($scope.editor.meta.blocks, function(blockInfo, directive) { - if (directive === $scope.node['#tag'] || blockInfo.join === $scope.container.getFieldEntityType()) { - block.options.push({ - id: directive, - text: blockInfo.title - }); - } - }); - - if (getBlockNode() && getBlockNode()['#tag'] in $scope.editor.meta.blocks) { - block.directive = block.original = getBlockNode()['#tag']; - block.layout = _.cloneDeep($scope.editor.meta.blocks[block.directive].layout); - } - - block.listeners.push($scope.$watch('block.layout', function (layout, oldVal) { - if (block.directive && layout && layout !== oldVal && !angular.equals(layout, $scope.editor.meta.blocks[block.directive].layout)) { - overrideBlockContents(block.layout); - } - }, true)); - } - - $scope.saveBlock = function() { - var options = CRM.utils.adjustDialogDefaults({ - width: '500px', - height: '300px', - autoOpen: false, - title: ts('Save block') - }); - var model = { - title: '', - name: null, - layout: $scope.node['#children'] - }; - if ($scope.join) { - model.join = $scope.join; - } - if ($scope.block && $scope.block.original) { - model.title = $scope.editor.meta.blocks[$scope.block.original].title; - model.name = $scope.editor.meta.blocks[$scope.block.original].name; - model.block = $scope.editor.meta.blocks[$scope.block.original].block; - } - else { - model.block = $scope.container.getFieldEntityType() || '*'; - } - dialogService.open('saveBlockDialog', '~/afGuiEditor/saveBlock.html', model, options) - .then(function(block) { - $scope.editor.meta.blocks[block.directive_name] = block; - setBlockDirective(block.directive_name); - initializeBlockContainer(); - }); - }; - - }, - controller: function($scope, afAdmin) { - var container = $scope.container = this; - this.node = $scope.node; - - this.getNodeType = function(node) { - if (!node) { - return null; - } - if (node['#tag'] === 'af-field') { - return 'field'; - } - if (node['af-fieldset']) { - return 'fieldset'; - } - if (node['af-join']) { - return 'join'; - } - if (node['#tag'] && node['#tag'] in $scope.editor.meta.blocks) { - return 'container'; - } - var classes = afAdmin.splitClass(node['class']), - types = ['af-container', 'af-text', 'af-button', 'af-markup'], - type = _.intersection(types, classes); - return type.length ? type[0].replace('af-', '') : null; - }; - - this.removeElement = function(element) { - afAdmin.removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey}); - }; - - this.getEntityName = function() { - return $scope.entityName.split('-join-')[0]; - }; - - // Returns the primary entity type for this container e.g. "Contact" - this.getMainEntityType = function() { - return $scope.editor && $scope.editor.getEntity(container.getEntityName()).type; - }; - - // Returns the entity type for fields within this conainer (join entity type if this is a join, else the primary entity type) - this.getFieldEntityType = function() { - var joinType = $scope.entityName.split('-join-'); - return joinType[1] || ($scope.editor && $scope.editor.getEntity(joinType[0]).type); - }; - - } - }; - }); - angular.module('afGuiEditor').controller('afGuiSaveBlock', function($scope, crmApi4, dialogService) { var ts = $scope.ts = CRM.ts(), model = $scope.model, @@ -435,157 +168,6 @@ }; }); - angular.module('afGuiEditor').directive('afGuiField', function() { - return { - restrict: 'A', - templateUrl: '~/afGuiEditor/field.html', - scope: { - node: '=afGuiField' - }, - require: ['^^afGuiEditor', '^^afGuiContainer'], - link: function($scope, element, attrs, ctrls) { - $scope.editor = ctrls[0]; - $scope.container = ctrls[1]; - }, - controller: function($scope, afAdmin) { - var ts = $scope.ts = CRM.ts(); - $scope.editingOptions = false; - var yesNo = [ - {key: '1', label: ts('Yes')}, - {key: '0', label: ts('No')} - ]; - - $scope.getEntity = function() { - return $scope.editor ? $scope.editor.getEntity($scope.container.getEntityName()) : {}; - }; - - $scope.getDefn = this.getDefn = function() { - return $scope.editor ? afAdmin.getField($scope.container.getFieldEntityType(), $scope.node.name) : {}; - }; - - $scope.hasOptions = function() { - var inputType = $scope.getProp('input_type'); - return _.contains(['CheckBox', 'Radio', 'Select'], inputType) && !(inputType === 'CheckBox' && !$scope.getDefn().options); - }; - - $scope.getOptions = this.getOptions = function() { - if ($scope.node.defn && $scope.node.defn.options) { - return $scope.node.defn.options; - } - return $scope.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo); - }; - - $scope.resetOptions = function() { - delete $scope.node.defn.options; - }; - - $scope.editOptions = function() { - $scope.editingOptions = true; - $('#afGuiEditor').addClass('af-gui-editing-content'); - }; - - $scope.inputTypeCanBe = function(type) { - var defn = $scope.getDefn(); - switch (type) { - case 'CheckBox': - case 'Radio': - case 'Select': - return !(!defn.options && defn.data_type !== 'Boolean'); - - case 'TextArea': - case 'RichTextEditor': - return (defn.data_type === 'Text' || defn.data_type === 'String'); - } - return true; - }; - - // Returns a value from either the local field defn or the base defn - $scope.getProp = function(propName) { - var path = propName.split('.'), - item = path.pop(), - localDefn = drillDown($scope.node.defn || {}, path); - if (typeof localDefn[item] !== 'undefined') { - return localDefn[item]; - } - return drillDown($scope.getDefn(), path)[item]; - }; - - // Checks for a value in either the local field defn or the base defn - $scope.propIsset = function(propName) { - var val = $scope.getProp(propName); - return !(typeof val === 'undefined' || val === null); - }; - - $scope.toggleLabel = function() { - $scope.node.defn = $scope.node.defn || {}; - if ($scope.node.defn.label === false) { - delete $scope.node.defn.label; - } else { - $scope.node.defn.label = false; - } - }; - - $scope.toggleRequired = function() { - getSet('required', !getSet('required')); - return false; - }; - - $scope.toggleHelp = function(position) { - getSet('help_' + position, $scope.propIsset('help_' + position) ? null : ($scope.getDefn()['help_' + position] || ts('Enter text'))); - return false; - }; - - // Getter/setter for definition props - $scope.getSet = function(propName) { - return _.wrap(propName, getSet); - }; - - // Getter/setter callback - function getSet(propName, val) { - if (arguments.length > 1) { - var path = propName.split('.'), - item = path.pop(), - localDefn = drillDown($scope.node, ['defn'].concat(path)), - fieldDefn = drillDown($scope.getDefn(), path); - // Set the value if different than the field defn, otherwise unset it - if (typeof val !== 'undefined' && (val !== fieldDefn[item] && !(!val && !fieldDefn[item]))) { - localDefn[item] = val; - } else { - delete localDefn[item]; - clearOut($scope.node, ['defn'].concat(path)); - } - return val; - } - return $scope.getProp(propName); - } - this.getSet = getSet; - - this.setEditingOptions = function(val) { - $scope.editingOptions = val; - }; - - // Returns a reference to a path n-levels deep within an object - function drillDown(parent, path) { - var container = parent; - _.each(path, function(level) { - container[level] = container[level] || {}; - container = container[level]; - }); - return container; - } - - // Recursively clears out empty arrays and objects - function clearOut(parent, path) { - var item; - while (path.length && _.every(drillDown(parent, path), _.isEmpty)) { - item = path.pop(); - delete drillDown(parent, path)[item]; - } - } - } - }; - }); - angular.module('afGuiEditor').directive('afGuiEditOptions', function() { return { restrict: 'A', @@ -629,158 +211,6 @@ }; }); - angular.module('afGuiEditor').directive('afGuiText', function() { - return { - restrict: 'A', - templateUrl: '~/afGuiEditor/text.html', - scope: { - node: '=afGuiText' - }, - require: '^^afGuiContainer', - link: function($scope, element, attrs, container) { - $scope.container = container; - }, - controller: function($scope, afAdmin) { - var ts = $scope.ts = CRM.ts(); - - $scope.tags = { - p: ts('Normal Text'), - legend: ts('Fieldset Legend'), - h1: ts('Heading 1'), - h2: ts('Heading 2'), - h3: ts('Heading 3'), - h4: ts('Heading 4'), - h5: ts('Heading 5'), - h6: ts('Heading 6') - }; - - $scope.alignments = { - 'text-left': ts('Align left'), - 'text-center': ts('Align center'), - 'text-right': ts('Align right'), - 'text-justify': ts('Justify') - }; - - $scope.getAlign = function() { - return _.intersection(afAdmin.splitClass($scope.node['class']), _.keys($scope.alignments))[0] || 'text-left'; - }; - - $scope.setAlign = function(val) { - afAdmin.modifyClasses($scope.node, _.keys($scope.alignments), val === 'text-left' ? null : val); - }; - - $scope.styles = _.transform(CRM.afGuiEditor.styles, function(styles, val, key) { - styles['text-' + key] = val; - }); - - // Getter/setter for ng-model - $scope.getSetStyle = function(val) { - if (arguments.length) { - return afAdmin.modifyClasses($scope.node, _.keys($scope.styles), val === 'text-default' ? null : val); - } - return _.intersection(afAdmin.splitClass($scope.node['class']), _.keys($scope.styles))[0] || 'text-default'; - }; - - } - }; - }); - - var richtextId = 0; - angular.module('afGuiEditor').directive('afGuiMarkup', function($sce, $timeout) { - return { - restrict: 'A', - templateUrl: '~/afGuiEditor/markup.html', - scope: { - node: '=afGuiMarkup' - }, - require: '^^afGuiContainer', - link: function($scope, element, attrs, container) { - $scope.container = container; - // CRM.wysiwyg doesn't work without a dom id - $scope.id = 'af-markup-editor-' + richtextId++; - - // When creating a new markup container, go straight to edit mode - $timeout(function() { - if ($scope.node['#markup'] === false) { - $scope.edit(); - } - }); - }, - controller: function($scope) { - var ts = $scope.ts = CRM.ts(); - - $scope.getMarkup = function() { - return $sce.trustAsHtml($scope.node['#markup'] || ''); - }; - - $scope.edit = function() { - $('#afGuiEditor').addClass('af-gui-editing-content'); - $scope.editingMarkup = true; - CRM.wysiwyg.create('#' + $scope.id); - CRM.wysiwyg.setVal('#' + $scope.id, $scope.node['#markup'] || '

'); - }; - - $scope.save = function() { - $scope.node['#markup'] = CRM.wysiwyg.getVal('#' + $scope.id); - $scope.close(); - }; - - $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 ($scope.node['#markup'] === false) { - $scope.container.removeElement($scope.node); - } else { - $scope.editingMarkup = false; - } - }; - } - }; - }); - - - angular.module('afGuiEditor').directive('afGuiButton', function() { - return { - restrict: 'A', - templateUrl: '~/afGuiEditor/button.html', - scope: { - node: '=afGuiButton' - }, - require: '^^afGuiContainer', - link: function($scope, element, attrs, container) { - $scope.container = container; - }, - controller: function($scope, afAdmin) { - var ts = $scope.ts = CRM.ts(); - - // TODO: Add action selector to UI - // $scope.actions = { - // "afform.submit()": ts('Submit Form') - // }; - - $scope.styles = _.transform(CRM.afGuiEditor.styles, function(styles, val, key) { - styles['btn-' + key] = val; - }); - - // Getter/setter for ng-model - $scope.getSetStyle = function(val) { - if (arguments.length) { - return afAdmin.modifyClasses($scope.node, _.keys($scope.styles), ['btn', val]); - } - return _.intersection(afAdmin.splitClass($scope.node['class']), _.keys($scope.styles))[0] || ''; - }; - - $scope.pickIcon = function() { - afAdmin.pickIcon().then(function(val) { - $scope.node['crm-icon'] = val; - }); - }; - - } - }; - }); - // Connect bootstrap dropdown.js with angular // Allows menu content to be conditionally rendered only if open // This gives a large performance boost for a page with lots of menus diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditorCanvas.html b/ext/afform/admin/ang/afGuiEditor/afGuiEditorCanvas.html index a5da54094e..53365daad9 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditorCanvas.html +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditorCanvas.html @@ -25,7 +25,7 @@
-
+

{{:: ts('This is a read-only preview of the auto-generated markup.') }}

diff --git a/ext/afform/admin/ang/afGuiEditor/container.html b/ext/afform/admin/ang/afGuiEditor/container.html deleted file mode 100644 index 47708ca429..0000000000 --- a/ext/afform/admin/ang/afGuiEditor/container.html +++ /dev/null @@ -1,37 +0,0 @@ -
-
- {{ editor.getEntity(entityName).label }} - {{ join ? ts(join) + ':' : ts('Block:') }} - {{ tags[node['#tag']].toLowerCase() }} - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
diff --git a/ext/afform/admin/ang/afGuiEditor/button-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton-menu.html similarity index 80% rename from ext/afform/admin/ang/afGuiEditor/button-menu.html rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiButton-menu.html index 77d9e1c656..d1069acf94 100644 --- a/ext/afform/admin/ang/afGuiEditor/button-menu.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton-menu.html @@ -7,4 +7,4 @@
-
  • {{:: ts('Delete this button') }}
  • +
  • {{:: ts('Delete this button') }}
  • diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.component.js new file mode 100644 index 0000000000..65eb081f60 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.component.js @@ -0,0 +1,41 @@ +// https://civicrm.org/licensing +(function(angular, $, _) { + "use strict"; + + angular.module('afGuiEditor').component('afGuiButton', { + templateUrl: '~/afGuiEditor/elements/afGuiButton.html', + bindings: { + node: '=', + deleteThis: '&' + }, + controller: function($scope, afAdmin) { + var ts = $scope.ts = CRM.ts(), + ctrl = this; + + // TODO: Add action selector to UI + // $scope.actions = { + // "afform.submit()": ts('Submit Form') + // }; + + $scope.styles = _.transform(CRM.afGuiEditor.styles, function(styles, val, key) { + styles['btn-' + key] = val; + }); + + // Getter/setter for ng-model + $scope.getSetStyle = function(val) { + if (arguments.length) { + return afAdmin.modifyClasses(ctrl.node, _.keys($scope.styles), ['btn', val]); + } + return _.intersection(afAdmin.splitClass(ctrl.node['class']), _.keys($scope.styles))[0] || ''; + }; + + $scope.pickIcon = function() { + afAdmin.pickIcon().then(function(val) { + ctrl.node['crm-icon'] = val; + }); + }; + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/afform/admin/ang/afGuiEditor/button.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html similarity index 72% rename from ext/afform/admin/ang/afGuiEditor/button.html rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html index 68255f7632..21c168805c 100644 --- a/ext/afform/admin/ang/afGuiEditor/button.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html @@ -4,15 +4,15 @@ - +
    diff --git a/ext/afform/admin/ang/afGuiEditor/container-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html similarity index 60% rename from ext/afform/admin/ang/afGuiEditor/container-menu.html rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html index 97384cef80..dae4157cec 100644 --- a/ext/afform/admin/ang/afGuiEditor/container-menu.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html @@ -1,9 +1,9 @@ -
  • {{:: ts('Save as block') }}
  • - -
  • +
  • {{:: ts('Save as block') }}
  • + +
  • {{:: ts('Element:') }} -
    @@ -11,10 +11,10 @@
  • - + - @@ -30,7 +30,7 @@
  • -
  • -
  • +
  • +
  • -
  • {{ !block ? ts('Delete this container') : ts('Delete this block') }}
  • +
  • {{ !block ? ts('Delete this container') : ts('Delete this block') }}
  • diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js new file mode 100644 index 0000000000..5fb23982da --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js @@ -0,0 +1,268 @@ +// https://civicrm.org/licensing +(function(angular, $, _) { + "use strict"; + + angular.module('afGuiEditor').component('afGuiContainer', { + templateUrl: '~/afGuiEditor/elements/afGuiContainer.html', + bindings: { + node: '<', + join: '<', + entityName: '<', + deleteThis: '&' + }, + require: {editor: '^^afGuiEditor'}, + controller: function($scope, crmApi4, dialogService, afAdmin) { + var ts = $scope.ts = CRM.ts(), + ctrl = this; + + this.$onInit = function() { + if ((ctrl.node['#tag'] in afAdmin.meta.blocks) || ctrl.join) { + initializeBlockContainer(); + } + }; + + $scope.isSelectedFieldset = function(entityName) { + return entityName === ctrl.editor.getSelectedEntityName(); + }; + + $scope.selectEntity = function() { + if (ctrl.node['af-fieldset']) { + ctrl.editor.selectEntity(ctrl.node['af-fieldset']); + } + }; + + $scope.tags = { + div: ts('Container'), + fieldset: ts('Fieldset') + }; + + // Block settings + var block = {}; + $scope.block = null; + + $scope.getSetChildren = function(val) { + var collection = block.layout || (ctrl.node && ctrl.node['#children']); + return arguments.length ? (collection = val) : collection; + }; + + $scope.isRepeatable = function() { + return ctrl.node['af-fieldset'] || (block.directive && afAdmin.meta.blocks[block.directive].repeat) || ctrl.join; + }; + + $scope.toggleRepeat = function() { + if ('af-repeat' in ctrl.node) { + delete ctrl.node.max; + delete ctrl.node.min; + delete ctrl.node['af-repeat']; + delete ctrl.node['add-icon']; + } else { + ctrl.node.min = '1'; + ctrl.node['af-repeat'] = ts('Add'); + } + }; + + $scope.getSetMin = function(val) { + if (arguments.length) { + if (ctrl.node.max && val > parseInt(ctrl.node.max, 10)) { + ctrl.node.max = '' + val; + } + if (!val) { + delete ctrl.node.min; + } + else { + ctrl.node.min = '' + val; + } + } + return ctrl.node.min ? parseInt(ctrl.node.min, 10) : null; + }; + + $scope.getSetMax = function(val) { + if (arguments.length) { + if (ctrl.node.min && val && val < parseInt(ctrl.node.min, 10)) { + ctrl.node.min = '' + val; + } + if (typeof val !== 'number') { + delete ctrl.node.max; + } + else { + ctrl.node.max = '' + val; + } + } + return ctrl.node.max ? parseInt(ctrl.node.max, 10) : null; + }; + + $scope.pickAddIcon = function() { + afAdmin.pickIcon().then(function(val) { + ctrl.node['add-icon'] = val; + }); + }; + + function getBlockNode() { + return !ctrl.join ? ctrl.node : (ctrl.node['#children'] && ctrl.node['#children'].length === 1 ? ctrl.node['#children'][0] : null); + } + + function setBlockDirective(directive) { + if (ctrl.join) { + ctrl.node['#children'] = [{'#tag': directive}]; + } else { + delete ctrl.node['#children']; + delete ctrl.node['class']; + ctrl.node['#tag'] = directive; + } + } + + function overrideBlockContents(layout) { + ctrl.node['#children'] = layout || []; + if (!ctrl.join) { + ctrl.node['#tag'] = 'div'; + ctrl.node['class'] = 'af-container'; + } + block.layout = block.directive = null; + } + + $scope.layouts = { + 'af-layout-rows': ts('Contents display as rows'), + 'af-layout-cols': ts('Contents are evenly-spaced columns'), + 'af-layout-inline': ts('Contents are arranged inline') + }; + + $scope.getLayout = function() { + if (!ctrl.node) { + return ''; + } + return _.intersection(afAdmin.splitClass(ctrl.node['class']), _.keys($scope.layouts))[0] || 'af-layout-rows'; + }; + + $scope.setLayout = function(val) { + var classes = ['af-container']; + if (val !== 'af-layout-rows') { + classes.push(val); + } + afAdmin.modifyClasses(ctrl.node, _.keys($scope.layouts), classes); + }; + + $scope.selectBlockDirective = function() { + if (block.directive) { + block.layout = _.cloneDeep(afAdmin.meta.blocks[block.directive].layout); + block.original = block.directive; + setBlockDirective(block.directive); + } + else { + overrideBlockContents(block.layout); + } + }; + + function initializeBlockContainer() { + + // Cancel the below $watch expressions if already set + _.each(block.listeners, function(deregister) { + deregister(); + }); + + block = $scope.block = { + directive: null, + layout: null, + original: null, + options: [], + listeners: [] + }; + + _.each(afAdmin.meta.blocks, function(blockInfo, directive) { + if (directive === ctrl.node['#tag'] || blockInfo.join === ctrl.getFieldEntityType()) { + block.options.push({ + id: directive, + text: blockInfo.title + }); + } + }); + + if (getBlockNode() && getBlockNode()['#tag'] in afAdmin.meta.blocks) { + block.directive = block.original = getBlockNode()['#tag']; + block.layout = _.cloneDeep(afAdmin.meta.blocks[block.directive].layout); + } + + block.listeners.push($scope.$watch('block.layout', function (layout, oldVal) { + if (block.directive && layout && layout !== oldVal && !angular.equals(layout, afAdmin.meta.blocks[block.directive].layout)) { + overrideBlockContents(block.layout); + } + }, true)); + } + + $scope.saveBlock = function() { + var options = CRM.utils.adjustDialogDefaults({ + width: '500px', + height: '300px', + autoOpen: false, + title: ts('Save block') + }); + var model = { + title: '', + name: null, + layout: ctrl.node['#children'] + }; + if (ctrl.join) { + model.join = ctrl.join; + } + if ($scope.block && $scope.block.original) { + model.title = afAdmin.meta.blocks[$scope.block.original].title; + model.name = afAdmin.meta.blocks[$scope.block.original].name; + model.block = afAdmin.meta.blocks[$scope.block.original].block; + } + else { + model.block = ctrl.container.getFieldEntityType() || '*'; + } + dialogService.open('saveBlockDialog', '~/afGuiEditor/saveBlock.html', model, options) + .then(function(block) { + afAdmin.meta.blocks[block.directive_name] = block; + setBlockDirective(block.directive_name); + initializeBlockContainer(); + }); + }; + + this.node = ctrl.node; + + this.getNodeType = function(node) { + if (!node) { + return null; + } + if (node['#tag'] === 'af-field') { + return 'field'; + } + if (node['af-fieldset']) { + return 'fieldset'; + } + if (node['af-join']) { + return 'join'; + } + if (node['#tag'] && node['#tag'] in afAdmin.meta.blocks) { + return 'container'; + } + var classes = afAdmin.splitClass(node['class']), + types = ['af-container', 'af-text', 'af-button', 'af-markup'], + type = _.intersection(types, classes); + return type.length ? type[0].replace('af-', '') : null; + }; + + this.removeElement = function(element) { + afAdmin.removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey}); + }; + + this.getEntityName = function() { + return ctrl.entityName.split('-join-')[0]; + }; + + // Returns the primary entity type for this container e.g. "Contact" + this.getMainEntityType = function() { + return ctrl.editor && ctrl.editor.getEntity(ctrl.getEntityName()).type; + }; + + // Returns the entity type for fields within this conainer (join entity type if this is a join, else the primary entity type) + this.getFieldEntityType = function() { + var joinType = ctrl.entityName.split('-join-'); + return joinType[1] || (ctrl.editor && ctrl.editor.getEntity(joinType[0]).type); + }; + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html new file mode 100644 index 0000000000..5fab5d7258 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html @@ -0,0 +1,37 @@ +
    +
    + {{ $ctrl.editor.getEntity($ctrl.entityName).label }} + {{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }} + {{ tags[$ctrl.node['#tag']].toLowerCase() }} + + + + +
    +
    +
    +
    +
    + + + + + + + +
    +
    +
    +
    + +
    diff --git a/ext/afform/admin/ang/afGuiEditor/field-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html similarity index 78% rename from ext/afform/admin/ang/afGuiEditor/field-menu.html rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html index 31999fd057..853be65e92 100644 --- a/ext/afform/admin/ang/afGuiEditor/field-menu.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html @@ -2,7 +2,7 @@
    @@ -14,7 +14,7 @@
  • - + {{:: ts('Label') }}
  • @@ -33,19 +33,19 @@
  • - + {{:: ts('Default option list') }}
  • - + {{:: ts('Customize options') }}
  • - + {{:: ts('Delete this field') }}
  • diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js new file mode 100644 index 0000000000..d04dd5a96c --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js @@ -0,0 +1,159 @@ +// https://civicrm.org/licensing +(function(angular, $, _) { + "use strict"; + + angular.module('afGuiEditor').component('afGuiField', { + templateUrl: '~/afGuiEditor/elements/afGuiField.html', + bindings: { + node: '=', + deleteThis: '&' + }, + require: { + editor: '^^afGuiEditor', + container: '^^afGuiContainer' + }, + controller: function($scope, afAdmin) { + var ts = $scope.ts = CRM.ts(), + ctrl = this; + + $scope.editingOptions = false; + var yesNo = [ + {key: '1', label: ts('Yes')}, + {key: '0', label: ts('No')} + ]; + + this.$onInit = function() { + $scope.meta = afAdmin.meta; + }; + + $scope.getEntity = function() { + return ctrl.editor ? ctrl.editor.getEntity(ctrl.container.getEntityName()) : {}; + }; + + $scope.getDefn = this.getDefn = function() { + return ctrl.editor ? afAdmin.getField(ctrl.container.getFieldEntityType(), ctrl.node.name) : {}; + }; + + $scope.hasOptions = function() { + var inputType = $scope.getProp('input_type'); + return _.contains(['CheckBox', 'Radio', 'Select'], inputType) && !(inputType === 'CheckBox' && !$scope.getDefn().options); + }; + + $scope.getOptions = this.getOptions = function() { + if (ctrl.node.defn && ctrl.node.defn.options) { + return ctrl.node.defn.options; + } + return $scope.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo); + }; + + $scope.resetOptions = function() { + delete ctrl.node.defn.options; + }; + + $scope.editOptions = function() { + $scope.editingOptions = true; + $('#afGuiEditor').addClass('af-gui-editing-content'); + }; + + $scope.inputTypeCanBe = function(type) { + var defn = $scope.getDefn(); + switch (type) { + case 'CheckBox': + case 'Radio': + case 'Select': + return !(!defn.options && defn.data_type !== 'Boolean'); + + case 'TextArea': + case 'RichTextEditor': + return (defn.data_type === 'Text' || defn.data_type === 'String'); + } + return true; + }; + + // Returns a value from either the local field defn or the base defn + $scope.getProp = function(propName) { + var path = propName.split('.'), + item = path.pop(), + localDefn = drillDown(ctrl.node.defn || {}, path); + if (typeof localDefn[item] !== 'undefined') { + return localDefn[item]; + } + return drillDown($scope.getDefn(), path)[item]; + }; + + // Checks for a value in either the local field defn or the base defn + $scope.propIsset = function(propName) { + var val = $scope.getProp(propName); + return !(typeof val === 'undefined' || val === null); + }; + + $scope.toggleLabel = function() { + ctrl.node.defn = ctrl.node.defn || {}; + if (ctrl.node.defn.label === false) { + delete ctrl.node.defn.label; + } else { + ctrl.node.defn.label = false; + } + }; + + $scope.toggleRequired = function() { + getSet('required', !getSet('required')); + return false; + }; + + $scope.toggleHelp = function(position) { + getSet('help_' + position, $scope.propIsset('help_' + position) ? null : ($scope.getDefn()['help_' + position] || ts('Enter text'))); + return false; + }; + + // Getter/setter for definition props + $scope.getSet = function(propName) { + return _.wrap(propName, getSet); + }; + + // Getter/setter callback + function getSet(propName, val) { + if (arguments.length > 1) { + var path = propName.split('.'), + item = path.pop(), + localDefn = drillDown(ctrl.node, ['defn'].concat(path)), + fieldDefn = drillDown($scope.getDefn(), path); + // Set the value if different than the field defn, otherwise unset it + if (typeof val !== 'undefined' && (val !== fieldDefn[item] && !(!val && !fieldDefn[item]))) { + localDefn[item] = val; + } else { + delete localDefn[item]; + clearOut(ctrl.node, ['defn'].concat(path)); + } + return val; + } + return $scope.getProp(propName); + } + this.getSet = getSet; + + this.setEditingOptions = function(val) { + $scope.editingOptions = val; + }; + + // Returns a reference to a path n-levels deep within an object + function drillDown(parent, path) { + var container = parent; + _.each(path, function(level) { + container[level] = container[level] || {}; + container = container[level]; + }); + return container; + } + + // Recursively clears out empty arrays and objects + function clearOut(parent, path) { + var item; + while (path.length && _.every(drillDown(parent, path), _.isEmpty)) { + item = path.pop(); + delete drillDown(parent, path)[item]; + } + } + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/afform/admin/ang/afGuiEditor/field.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.html similarity index 85% rename from ext/afform/admin/ang/afGuiEditor/field.html rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiField.html index 547c0c974b..0ad20ad930 100644 --- a/ext/afform/admin/ang/afGuiEditor/field.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.html @@ -6,11 +6,11 @@ - +
    -
    -
    -
    +
    +
    diff --git a/ext/afform/admin/ang/afGuiEditor/text-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiText-menu.html similarity index 91% rename from ext/afform/admin/ang/afGuiEditor/text-menu.html rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiText-menu.html index 103ff14288..eaef3b9781 100644 --- a/ext/afform/admin/ang/afGuiEditor/text-menu.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiText-menu.html @@ -25,5 +25,5 @@
  • - {{:: ts('Delete this text') }} + {{:: ts('Delete this text') }}
  • diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiText.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiText.component.js new file mode 100644 index 0000000000..8ce16894c1 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiText.component.js @@ -0,0 +1,56 @@ +// https://civicrm.org/licensing +(function(angular, $, _) { + "use strict"; + + angular.module('afGuiEditor').component('afGuiText', { + templateUrl: '~/afGuiEditor/elements/afGuiText.html', + bindings: { + node: '=', + deleteThis: '&' + }, + controller: function($scope, afAdmin) { + var ts = $scope.ts = CRM.ts(), + ctrl = this; + + $scope.tags = { + p: ts('Normal Text'), + legend: ts('Fieldset Legend'), + h1: ts('Heading 1'), + h2: ts('Heading 2'), + h3: ts('Heading 3'), + h4: ts('Heading 4'), + h5: ts('Heading 5'), + h6: ts('Heading 6') + }; + + $scope.alignments = { + 'text-left': ts('Align left'), + 'text-center': ts('Align center'), + 'text-right': ts('Align right'), + 'text-justify': ts('Justify') + }; + + $scope.getAlign = function() { + return _.intersection(afAdmin.splitClass(ctrl.node['class']), _.keys($scope.alignments))[0] || 'text-left'; + }; + + $scope.setAlign = function(val) { + afAdmin.modifyClasses(ctrl.node, _.keys($scope.alignments), val === 'text-left' ? null : val); + }; + + $scope.styles = _.transform(CRM.afGuiEditor.styles, function(styles, val, key) { + styles['text-' + key] = val; + }); + + // Getter/setter for ng-model + $scope.getSetStyle = function(val) { + if (arguments.length) { + return afAdmin.modifyClasses(ctrl.node, _.keys($scope.styles), val === 'text-default' ? null : val); + } + return _.intersection(afAdmin.splitClass(ctrl.node['class']), _.keys($scope.styles))[0] || 'text-default'; + }; + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/afform/admin/ang/afGuiEditor/text.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiText.html similarity index 62% rename from ext/afform/admin/ang/afGuiEditor/text.html rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiText.html index 82eea2c0d9..e7b5d8cd27 100644 --- a/ext/afform/admin/ang/afGuiEditor/text.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiText.html @@ -4,10 +4,10 @@ - +
    -

    - {{ node['#children'][0]['#text'] }} +

    + {{ $ctrl.node['#children'][0]['#text'] }}

    diff --git a/ext/afform/admin/ang/afGuiEditor/markup-menu.html b/ext/afform/admin/ang/afGuiEditor/markup-menu.html deleted file mode 100644 index 4dacb07cc7..0000000000 --- a/ext/afform/admin/ang/afGuiEditor/markup-menu.html +++ /dev/null @@ -1,10 +0,0 @@ -
  • - {{:: ts('Edit content') }} -
  • - -
  • -
  • - -
  • - {{:: ts('Delete this content') }} -
  • -- 2.25.1