Afform - Refactor elements as components & move to their own files
authorColeman Watts <coleman@civicrm.org>
Fri, 8 Jan 2021 19:24:54 +0000 (14:24 -0500)
committerColeman Watts <coleman@civicrm.org>
Mon, 11 Jan 2021 14:49:06 +0000 (09:49 -0500)
21 files changed:
ext/afform/admin/ang/afGuiEditor.ang.php
ext/afform/admin/ang/afGuiEditor.css
ext/afform/admin/ang/afGuiEditor.js
ext/afform/admin/ang/afGuiEditor/afGuiEditorCanvas.html
ext/afform/admin/ang/afGuiEditor/container.html [deleted file]
ext/afform/admin/ang/afGuiEditor/elements/afGuiButton-menu.html [moved from ext/afform/admin/ang/afGuiEditor/button-menu.html with 80% similarity]
ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.component.js [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html [moved from ext/afform/admin/ang/afGuiEditor/button.html with 72% similarity]
ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html [moved from ext/afform/admin/ang/afGuiEditor/container-menu.html with 60% similarity]
ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html [moved from ext/afform/admin/ang/afGuiEditor/field-menu.html with 78% similarity]
ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/elements/afGuiField.html [moved from ext/afform/admin/ang/afGuiEditor/field.html with 85% similarity]
ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup-menu.html [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.component.js [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.html [moved from ext/afform/admin/ang/afGuiEditor/markup.html with 86% similarity]
ext/afform/admin/ang/afGuiEditor/elements/afGuiText-menu.html [moved from ext/afform/admin/ang/afGuiEditor/text-menu.html with 91% similarity]
ext/afform/admin/ang/afGuiEditor/elements/afGuiText.component.js [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/elements/afGuiText.html [moved from ext/afform/admin/ang/afGuiEditor/text.html with 62% similarity]
ext/afform/admin/ang/afGuiEditor/markup-menu.html [deleted file]

index 38d8ffea4838fac34368847f78ca1c2a839314df..99c6a5e70a21bef43e85431720f4f26f00e301e6 100644 (file)
@@ -4,6 +4,7 @@ return [
   'js' => [
     'ang/afGuiEditor.js',
     'ang/afGuiEditor/*.js',
+    'ang/afGuiEditor/*/*.js',
   ],
   'css' => ['ang/afGuiEditor.css'],
   'partials' => ['ang/afGuiEditor'],
index e99b30e90381ff36fb8adfaf3bfb8d300571dde1..fd9a9d133ce730b4042500191ea94bbf89d967f1 100644 (file)
 #afGuiEditor .af-gui-element {
   position: relative;
   padding: 0 3px 3px;
+  display: block;
 }
 
 #afGuiEditor .af-gui-container {
   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 {
index c3a8f905d79622a59e55ac8715e2ff29a7c44522..30db86b55d20b708a8437b8c37c6c616cb801992 100644 (file)
     });
   });
 
-  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,
     };
   });
 
-  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',
     };
   });
 
-  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'] || '<p></p>');
-        };
-
-        $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
index a5da54094e470958c04377428a9bc5df7f6542c9..53365daad9eb8cfa333578d7d64944b5ef5c21f7 100644 (file)
@@ -25,7 +25,7 @@
 
   </div>
   <div id="afGuiEditor-canvas-body" class="panel-body" ng-if="canvasTab === 'layout'">
-    <div ng-if="editor.layout" af-gui-container="editor.layout" entity-name="" />
+    <af-gui-container ng-if="editor.layout" node="editor.layout" entity-name="" ></af-gui-container>
   </div>
   <div class="panel-body" ng-if="canvasTab === 'markup'">
     <p class="help-block">{{:: ts('This is a read-only preview of the auto-generated markup.') }}</p>
diff --git a/ext/afform/admin/ang/afGuiEditor/container.html b/ext/afform/admin/ang/afGuiEditor/container.html
deleted file mode 100644 (file)
index 47708ca..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<div class="af-gui-bar" ng-if="node['#tag'] !== 'af-form'" ng-click="selectEntity()" >
-  <div class="form-inline" af-gui-menu>
-    <span ng-if="container.getNodeType(node) == 'fieldset'">{{ editor.getEntity(entityName).label }}</span>
-    <span ng-if="block">{{ join ? ts(join) + ':' : ts('Block:') }}</span>
-    <span ng-if="!block">{{ tags[node['#tag']].toLowerCase() }}</span>
-    <select ng-if="block" ng-model="block.directive" ng-change="selectBlockDirective()">
-      <option value="">{{:: ts('Custom') }}</option>
-      <option ng-value="option.id" ng-repeat="option in block.options track by option.id">{{ option.text }}</option>
-    </select>
-    <button type="button" class="btn btn-default btn-xs" ng-if="block && !block.layout" ng-click="saveBlock()">{{:: ts('Save...') }}</button>
-    <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button pull-right" data-toggle="dropdown" title="{{:: ts('Configure') }}">
-      <span><i class="crm-i fa-gear"></i></span>
-    </button>
-    <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/container-menu.html'"></ul>
-  </div>
-</div>
-<div ui-sortable="{handle: '.af-gui-bar', connectWith: '[ui-sortable]', cancel: 'input,textarea,button,select,option,a,.dropdown-menu', placeholder: 'af-gui-dropzone', containment: '#afGuiEditor-canvas-body'}" ui-sortable-update="editor.onDrop" ng-model="getSetChildren" ng-model-options="{getterSetter: true}" class="af-gui-layout {{ getLayout() }}">
-  <div ng-repeat="item in getSetChildren()" >
-    <div ng-switch="container.getNodeType(item)">
-      <div ng-switch-when="fieldset" af-gui-container="item" style="{{ item.style }}" class="af-gui-container af-gui-fieldset af-gui-container-type-{{ item['#tag'] }}" ng-class="{'af-entity-selected': isSelectedFieldset(item['af-fieldset'])}" entity-name="item['af-fieldset']" data-entity="{{ item['af-fieldset'] }}" />
-      <div ng-switch-when="container" af-gui-container="item" style="{{ item.style }}" class="af-gui-container af-gui-container-type-{{ item['#tag'] }}" entity-name="entityName" data-entity="{{ entityName }}" />
-      <div ng-switch-when="join" af-gui-container="item"  style="{{ item.style }}" class="af-gui-container" join="item['af-join']" entity-name="entityName + '-join-' + item['af-join']" data-entity="{{ entityName + '-join-' + item['af-join'] }}" />
-      <div ng-switch-when="field" af-gui-field="item" />
-      <div ng-switch-when="text" af-gui-text="item" class="af-gui-element af-gui-text" />
-      <div ng-switch-when="markup" af-gui-markup="item" class="af-gui-markup" />
-      <div ng-switch-when="button" af-gui-button="item" class="af-gui-element af-gui-button" />
-    </div>
-  </div>
-</div>
-<div ng-if="node['af-repeat'] || node['af-repeat'] === ''" class="af-gui-button">
-  <button class="btn btn-xs btn-primary disabled">
-    <span class="crm-editable-enabled" ng-click="pickAddIcon()" >
-      <i class="crm-i {{ node['add-icon'] || 'fa-plus' }}"></i>
-    </span>
-    <span af-gui-editable ng-model="node['af-repeat']">{{ node['af-repeat'] }}</span>
-  </button>
-</div>
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 77d9e1c65628e766d2496e70f2d54f38021351f1..d1069acf94e2c27547cc2fa49c377979bf4c986d 100644 (file)
@@ -7,4 +7,4 @@
   </div>
 </li>
 <li role="separator" class="divider"></li>
-<li><a href ng-click="container.removeElement(node)"><span class="text-danger">{{:: ts('Delete this button') }}</span></a></li>
+<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{:: ts('Delete this button') }}</span></a></li>
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 (file)
index 0000000..65eb081
--- /dev/null
@@ -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._);
similarity index 72%
rename from ext/afform/admin/ang/afGuiEditor/button.html
rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html
index 68255f7632bf071e6598c4c921985dfd62c3fffc..21c168805c0ca061f3d52051c07683ca42b1727a 100644 (file)
@@ -4,15 +4,15 @@
       <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"></i></span>
       </button>
-      <ul class="dropdown-menu" ng-if="menu.open" ng-include="'~/afGuiEditor/button-menu.html'"></ul>
+      <ul class="dropdown-menu" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiButton-menu.html'"></ul>
     </div>
   </div>
 </div>
 <button type="button" class="btn {{ getSetStyle() }} disabled">
   <span class="crm-editable-enabled" ng-click="pickIcon()" >
-    <i class="crm-i {{ node['crm-icon'] }}"></i>
+    <i class="crm-i {{ $ctrl.node['crm-icon'] }}"></i>
   </span>
-  <span af-gui-editable ng-model="node['#children'][0]['#text']" >
-    {{ node['#children'][0]['#text'] }}
+  <span af-gui-editable ng-model="$ctrl.node['#children'][0]['#text']" >
+    {{ $ctrl.node['#children'][0]['#text'] }}
   </span>
 </button>
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 97384cef80f0367bb0d4472712ec0442ebe9e437..dae4157cec2453523540a760bc130e6262114ac8 100644 (file)
@@ -1,9 +1,9 @@
-<li ng-if="!node['af-fieldset'] && !block.layout"><a href ng-click="saveBlock()">{{:: ts('Save as block') }}</a></li>
-<li ng-if="!node['af-fieldset'] && !block.layout" role="separator" class="divider"></li>
-<li ng-if="tags[node['#tag']]">
+<li ng-if="!$ctrl.node['af-fieldset'] && !block.layout"><a href ng-click="saveBlock()">{{:: ts('Save as block') }}</a></li>
+<li ng-if="!$ctrl.node['af-fieldset'] && !block.layout" role="separator" class="divider"></li>
+<li ng-if="tags[$ctrl.node['#tag']]">
   <div class="af-gui-field-select-in-dropdown form-inline" ng-click="$event.stopPropagation()">
     {{:: ts('Element:') }}
-    <select class="form-control" ng-model="node['#tag']" title="{{:: ts('Container type') }}">
+    <select class="form-control" ng-model="$ctrl.node['#tag']" title="{{:: ts('Container type') }}">
       <option ng-repeat="(opt, label) in tags" value="{{ opt }}">{{ label }}</option>
     </select>
   </div>
 <li ng-if="isRepeatable()" ng-click="$event.stopPropagation()">
   <div class="af-gui-field-select-in-dropdown form-inline">
     <label ng-click="toggleRepeat()">
-      <i class="crm-i fa-{{ node['af-repeat'] || node['af-repeat'] === '' ? 'check-' : '' }}square-o"></i>
+      <i class="crm-i fa-{{ $ctrl.node['af-repeat'] || $ctrl.node['af-repeat'] === '' ? 'check-' : '' }}square-o"></i>
       {{:: ts('Repeat') }}
     </label>
-    <span ng-style="{visibility: node['af-repeat'] || node['af-repeat'] === '' ? 'visible' : 'hidden'}">
+    <span ng-style="{visibility: $ctrl.node['af-repeat'] || $ctrl.node['af-repeat'] === '' ? 'visible' : 'hidden'}">
       <input type="number" class="form-control" ng-model="getSetMin" ng-model-options="{getterSetter: true}" placeholder="{{:: ts('min') }}" min="0" step="1" />
       - <input type="number" class="form-control" ng-model="getSetMax" ng-model-options="{getterSetter: true}" placeholder="{{:: ts('max') }}" min="2" step="1" />
     </span>
@@ -30,7 +30,7 @@
     </div>
   </div>
 </li>
-<li af-gui-menu-item-border="node"></li>
-<li af-gui-menu-item-background="node"></li>
+<li af-gui-menu-item-border="$ctrl.node"></li>
+<li af-gui-menu-item-background="$ctrl.node"></li>
 <li role="separator" class="divider"></li>
-<li><a href ng-click="parentContainer.removeElement(node)"><span class="text-danger">{{ !block ? ts('Delete this container') : ts('Delete this block') }}</span></a></li>
+<li><a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{ !block ? ts('Delete this container') : ts('Delete this block') }}</span></a></li>
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 (file)
index 0000000..5fb2398
--- /dev/null
@@ -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 (file)
index 0000000..5fab5d7
--- /dev/null
@@ -0,0 +1,37 @@
+<div class="af-gui-bar" ng-if="$ctrl.node['#tag'] !== 'af-form'" ng-click="selectEntity()" >
+  <div class="form-inline" af-gui-menu>
+    <span ng-if="$ctrl.getNodeType($ctrl.node) == 'fieldset'">{{ $ctrl.editor.getEntity($ctrl.entityName).label }}</span>
+    <span ng-if="block">{{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }}</span>
+    <span ng-if="!block">{{ tags[$ctrl.node['#tag']].toLowerCase() }}</span>
+    <select ng-if="block" ng-model="block.directive" ng-change="selectBlockDirective()">
+      <option value="">{{:: ts('Custom') }}</option>
+      <option ng-value="option.id" ng-repeat="option in block.options track by option.id">{{ option.text }}</option>
+    </select>
+    <button type="button" class="btn btn-default btn-xs" ng-if="block && !block.layout" ng-click="saveBlock()">{{:: ts('Save...') }}</button>
+    <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button pull-right" data-toggle="dropdown" title="{{:: ts('Configure') }}">
+      <span><i class="crm-i fa-gear"></i></span>
+    </button>
+    <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiContainer-menu.html'"></ul>
+  </div>
+</div>
+<div ui-sortable="{handle: '.af-gui-bar', connectWith: '[ui-sortable]', cancel: 'input,textarea,button,select,option,a,.dropdown-menu', placeholder: 'af-gui-dropzone', containment: '#afGuiEditor-canvas-body'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="getSetChildren" ng-model-options="{getterSetter: true}" class="af-gui-layout {{ getLayout() }}">
+  <div ng-repeat="item in getSetChildren()" >
+    <div ng-switch="$ctrl.getNodeType(item)">
+      <af-gui-container ng-switch-when="fieldset" node="item" delete-this="$ctrl.removeElement(item)" style="{{ item.style }}" class="af-gui-container af-gui-fieldset af-gui-container-type-{{ item['#tag'] }}" ng-class="{'af-entity-selected': isSelectedFieldset(item['af-fieldset'])}" entity-name="item['af-fieldset']" data-entity="{{ item['af-fieldset'] }}" ></af-gui-container>
+      <af-gui-container ng-switch-when="container" node="item" delete-this="$ctrl.removeElement(item)" style="{{ item.style }}" class="af-gui-container af-gui-container-type-{{ item['#tag'] }}" entity-name="$ctrl.entityName" data-entity="{{ $ctrl.entityName }}" ></af-gui-container>
+      <af-gui-container ng-switch-when="join" node="item" delete-this="$ctrl.removeElement(item)" style="{{ item.style }}" class="af-gui-container" join="item['af-join']" entity-name="$ctrl.entityName + '-join-' + item['af-join']" data-entity="{{ $ctrl.entityName + '-join-' + item['af-join'] }}" ></af-gui-container>
+      <af-gui-field ng-switch-when="field" node="item" delete-this="$ctrl.removeElement(item)" ></af-gui-field>
+      <af-gui-text ng-switch-when="text" node="item" delete-this="$ctrl.removeElement(item)" class="af-gui-element af-gui-text" ></af-gui-text>
+      <af-gui-markup ng-switch-when="markup" node="item" delete-this="$ctrl.removeElement(item)" class="af-gui-markup" ></af-gui-markup>
+      <af-gui-button ng-switch-when="button" node="item" delete-this="$ctrl.removeElement(item)" class="af-gui-element af-gui-button" ></af-gui-button>
+    </div>
+  </div>
+</div>
+<div ng-if="$ctrl.node['af-repeat'] || $ctrl.node['af-repeat'] === ''" class="af-gui-button">
+  <button class="btn btn-xs btn-primary disabled">
+    <span class="crm-editable-enabled" ng-click="pickAddIcon()" >
+      <i class="crm-i {{ $ctrl.node['add-icon'] || 'fa-plus' }}"></i>
+    </span>
+    <span af-gui-editable ng-model="$ctrl.node['af-repeat']">{{ $ctrl.node['af-repeat'] }}</span>
+  </button>
+</div>
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 31999fd057a6f14ce26aff94535208b88bc6959b..853be65e928b06f79fd81975b3a0e786513e3d30 100644 (file)
@@ -2,7 +2,7 @@
   <div href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
     <label>{{:: ts('Type:') }}</label>
     <select class="form-control" ng-model="getSet('input_type')" ng-model-options="{getterSetter: true}" title="{{:: ts('Field type') }}">
-      <option ng-repeat="(type, label) in editor.meta.inputType" value="{{ type }}" ng-if="inputTypeCanBe(type)">{{ label }}</option>
+      <option ng-repeat="(type, label) in meta.inputType" value="{{ type }}" ng-if="inputTypeCanBe(type)">{{ label }}</option>
     </select>
   </div>
 </li>
@@ -14,7 +14,7 @@
 </li>
 <li>
   <a href ng-click="toggleLabel(); $event.stopPropagation();" title="{{:: ts('Show field label') }}">
-    <i class="crm-i" ng-class="{'fa-square-o': node.defn.title === false, 'fa-check-square-o': node.defn.title !== false}"></i>
+    <i class="crm-i" ng-class="{'fa-square-o': $ctrl.node.defn.title === false, 'fa-check-square-o': $ctrl.node.defn.title !== false}"></i>
     {{:: ts('Label') }}
   </a>
 </li>
 <li role="separator" class="divider" ng-if="hasOptions()"></li>
 <li ng-if="hasOptions()" ng-click="$event.stopPropagation()">
   <a href ng-click="resetOptions()" title="{{:: ts('Reset the option list for this field') }}">
-    <i class="crm-i fa-{{ node.defn.options ? '' : 'check-' }}circle-o"></i>
+    <i class="crm-i fa-{{ $ctrl.node.defn.options ? '' : 'check-' }}circle-o"></i>
     {{:: ts('Default option list') }}
   </a>
 </li>
 <li ng-if="hasOptions()">
   <a href ng-click="editOptions()" title="{{:: ts('Customize the option list for this field') }}">
-    <i class="crm-i fa-{{ !node.defn.options ? '' : 'check-' }}circle-o"></i>
+    <i class="crm-i fa-{{ !$ctrl.node.defn.options ? '' : 'check-' }}circle-o"></i>
     {{:: ts('Customize options') }}
   </a>
 </li>
 <li role="separator" class="divider"></li>
 <li>
-  <a href ng-click="container.removeElement(node)" title="{{:: ts('Remove field from form') }}">
+  <a href ng-click="$ctrl.deleteThis()" title="{{:: ts('Remove field from form') }}">
     <span class="text-danger">{{:: ts('Delete this field') }}</span>
   </a>
 </li>
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 (file)
index 0000000..d04dd5a
--- /dev/null
@@ -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._);
similarity index 85%
rename from ext/afform/admin/ang/afGuiEditor/field.html
rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiField.html
index 547c0c974bb27cec974cc30792d2de846461149c..0ad20ad9301510e48838bb0d85e27e8085f93f96 100644 (file)
@@ -6,11 +6,11 @@
         <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"></i></span>
         </button>
-        <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/field-menu.html'"></ul>
+        <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiField-menu.html'"></ul>
       </div>
     </div>
   </div>
-  <label ng-style="{visibility: node.defn.label === false ? 'hidden' : 'visible'}" ng-class="{'af-gui-field-required': getProp('required')}" class="af-gui-node-title">
+  <label ng-style="{visibility: $ctrl.node.defn.label === false ? 'hidden' : 'visible'}" ng-class="{'af-gui-field-required': getProp('required')}" class="af-gui-node-title">
     <span af-gui-editable ng-model="getSet('label')" ng-model-options="{getterSetter: true}" default-value="getDefn().label">{{ getProp('label') }}</span>
   </label>
   <div class="af-gui-field-help" ng-if="propIsset('help_pre')">
diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup-menu.html
new file mode 100644 (file)
index 0000000..7e1bb89
--- /dev/null
@@ -0,0 +1,10 @@
+<li>
+  <a href ng-click="edit()">{{:: ts('Edit content') }}</a>
+</li>
+<li role="separator" class="divider"></li>
+<li af-gui-menu-item-border="$ctrl.node"></li>
+<li af-gui-menu-item-background="$ctrl.node"></li>
+<li role="separator" class="divider"></li>
+<li>
+  <a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{:: ts('Delete this content') }}</span></a>
+</li>
diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.component.js
new file mode 100644 (file)
index 0000000..24482e4
--- /dev/null
@@ -0,0 +1,58 @@
+// https://civicrm.org/licensing
+(function(angular, $, _) {
+  "use strict";
+
+  var richtextId = 0;
+
+  angular.module('afGuiEditor').component('afGuiMarkup', {
+    templateUrl: '~/afGuiEditor/elements/afGuiMarkup.html',
+    bindings: {
+      node: '=',
+      deleteThis: '&'
+    },
+    controller: function($scope, $sce, $timeout) {
+      var ts = $scope.ts = CRM.ts(),
+        ctrl = this;
+
+      this.$onInit = function() {
+        // 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 (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>');
+      };
+
+      $scope.save = function() {
+        ctrl.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 (ctrl.node['#markup'] === false) {
+          $scope.container.removeElement(ctrl.node);
+        } else {
+          $scope.editingMarkup = false;
+        }
+      };
+    }
+  });
+
+})(angular, CRM.$, CRM._);
similarity index 86%
rename from ext/afform/admin/ang/afGuiEditor/markup.html
rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiMarkup.html
index 7c58dd65d93262af9899c6d84d529b3a739a3879..851bd70f75863e3c83e831d417cd980520e6c0e1 100644 (file)
@@ -4,12 +4,12 @@
       <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"></i></span>
       </button>
-      <ul class="dropdown-menu" ng-if="menu.open" ng-include="'~/afGuiEditor/markup-menu.html'"></ul>
+      <ul class="dropdown-menu" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiMarkup-menu.html'"></ul>
     </div>
   </div>
 </div>
-<div ng-if="!editingMarkup" ng-click="edit()" class="af-gui-markup-content crm-editable-enabled {{ node['class'] }}">
-  <div ng-bind-html="getMarkup()" style="{{ node.style }}"></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">
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 103ff1428892d5cc2904cf3df36f1cf573935c06..eaef3b97811d3dd3cb60a2ef57ebe1f1574aa931 100644 (file)
@@ -25,5 +25,5 @@
 </li>
 <li role="separator" class="divider"></li>
 <li>
-  <a href ng-click="container.removeElement(node)"><span class="text-danger">{{:: ts('Delete this text') }}</span></a>
+  <a href ng-click="$ctrl.deleteThis()"><span class="text-danger">{{:: ts('Delete this text') }}</span></a>
 </li>
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 (file)
index 0000000..8ce1689
--- /dev/null
@@ -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._);
similarity index 62%
rename from ext/afform/admin/ang/afGuiEditor/text.html
rename to ext/afform/admin/ang/afGuiEditor/elements/afGuiText.html
index 82eea2c0d9908905751901047615ee171c40aa63..e7b5d8cd2731c7ad7d245556ab2301e1e1a69046 100644 (file)
@@ -4,10 +4,10 @@
       <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"></i></span>
       </button>
-      <ul class="dropdown-menu" ng-if="menu.open" ng-include="'~/afGuiEditor/text-menu.html'"></ul>
+      <ul class="dropdown-menu" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiText-menu.html'"></ul>
     </div>
   </div>
 </div>
-<p class="af-gui-node-title {{ node['class'] + ' af-gui-text-' + node['#tag'] }}" >
-  <span af-gui-editable ng-model="node['#children'][0]['#text']">{{ node['#children'][0]['#text'] }}</span>
+<p class="af-gui-node-title {{ $ctrl.node['class'] + ' af-gui-text-' + $ctrl.node['#tag'] }}" >
+  <span af-gui-editable ng-model="$ctrl.node['#children'][0]['#text']">{{ $ctrl.node['#children'][0]['#text'] }}</span>
 </p>
diff --git a/ext/afform/admin/ang/afGuiEditor/markup-menu.html b/ext/afform/admin/ang/afGuiEditor/markup-menu.html
deleted file mode 100644 (file)
index 4dacb07..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<li>
-  <a href ng-click="edit()">{{:: ts('Edit content') }}</a>
-</li>
-<li role="separator" class="divider"></li>
-<li af-gui-menu-item-border="node"></li>
-<li af-gui-menu-item-background="node"></li>
-<li role="separator" class="divider"></li>
-<li>
-  <a href ng-click="container.removeElement(node)"><span class="text-danger">{{:: ts('Delete this content') }}</span></a>
-</li>