WIP canvas
authorColeman Watts <coleman@civicrm.org>
Thu, 31 Oct 2019 00:08:47 +0000 (20:08 -0400)
committerCiviCRM <info@civicrm.org>
Wed, 16 Sep 2020 02:13:19 +0000 (19:13 -0700)
ext/afform/gui/ang/afGuiEditor.css
ext/afform/gui/ang/afGuiEditor.js
ext/afform/gui/ang/afGuiEditor/block.html
ext/afform/gui/ang/afGuiEditor/canvas.html
ext/afform/gui/ang/afGuiEditor/config-entity.html
ext/afform/gui/ang/afGuiEditor/field.html
ext/afform/gui/ang/afGuiEditor/fieldset.html [deleted file]
ext/afform/gui/ang/afGuiEditor/palette.html
ext/afform/gui/ang/afGuiEditor/text.html [new file with mode: 0644]
ext/afform/mock/ang/testAfform.aff.html

index 93afb919b58588ecb93bf756619d43a1851e858c..424e5aad54ee3fcd706bd5f4b516dad73afef13f 100644 (file)
@@ -12,7 +12,7 @@
 }
 
 #afGuiEditor #afGuiEditor-canvas {
-  flex: 1;
+  flex: 1.5;
   margin-left: 5px;
 }
 
 #afGuiEditor-palette-config .form-inline label {
   min-width: 110px;
 }
+
+#afGuiEditor .af-gui-bar {
+  cursor: move;
+  visibility: hidden;
+  background-color: #efefef;
+  height: 22px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  padding-left:15px;
+}
+/* grip handle */
+#afGuiEditor .af-gui-bar:before {
+  background-size: cover;
+  background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1IiBoZWlnaHQ9IjUiPgo8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSIjODg4Ij48L3JlY3Q+Cjwvc3ZnPg==");
+  width: 10px;
+  height: 15px;
+  content: ' ';
+  display: block;
+  position: absolute;
+  left: 4px;
+  top: 5px;
+}
+
+#afGuiEditor-canvas:hover .af-gui-bar {
+  visibility: visible;
+}
+
+#afGuiEditor .af-gui-field,
+#afGuiEditor .af-gui-text,
+#afGuiEditor .af-gui-block {
+  position: relative;
+  padding: 32px 3px 3px;
+  min-height: 40px;
+  border: 2px dashed transparent;
+}
+
+#afGuiEditor #afGuiEditor-canvas .af-entity-selected {
+  border: 2px dashed #0071bd;
+}
+#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar {
+  background-color: #0071bd;
+  visibility: visible;
+  color: white;
+}
+
+#afGuiEditor .af-gui-block {
+  padding-top: 20px;
+}
+
+#afGuiEditor .af-gui-block:hover {
+  border: 2px dashed #757575;
+}
index d5b38680d962fa16e5e3fa2d0184f2f8853691b3..45ef072bf635743ff7751dea1f952580ffe9c035 100644 (file)
@@ -8,12 +8,13 @@
       scope: {
         afGuiEditor: '='
       },
-      link: function($scope, $el, $attr) {
+      controller: function($scope) {
         $scope.ts = CRM.ts();
         $scope.afform = null;
         $scope.selectedEntity = null;
         $scope.meta = CRM.afformAdminData;
         $scope.controls = {};
+        $scope.editor = this;
         var newForm = {
           title: ts('Untitled Form'),
           layout: [{
@@ -51,7 +52,7 @@
           $scope.fields = getAllFields($scope.layout['#children']);
         }
 
-        $scope.addEntity = function(entityType) {
+        this.addEntity = function(entityType) {
           var existingEntitiesofThisType = _.map(_.filter($scope.entities, {type: entityType}), 'name'),
             num = existingEntitiesofThisType.length + 1;
           // Give this new entity a unique name
             label: entityType + ' ' + num
           };
           $scope.layout['#children'].unshift($scope.entities[entityType + num]);
-          $scope.selectEntity(entityType + num);
+          this.selectEntity(entityType + num);
         };
 
-        $scope.removeEntity = function(entityName) {
+        this.removeEntity = function(entityName) {
           delete $scope.entities[entityName];
           _.remove($scope.layout['#children'], {'#tag': 'af-entity', name: entityName});
-          $scope.selectEntity(null);
+          this.selectEntity(null);
         };
 
-        $scope.selectEntity = function(entityName) {
+        this.selectEntity = function(entityName) {
           $scope.selectedEntity = entityName;
         };
 
-        $scope.getField = function(entityName, fieldName) {
-          return _.filter($scope.meta.fields[entityName], {name: fieldName})[0];
+        this.getField = function(entityType, fieldName) {
+          return _.filter($scope.meta.fields[entityType], {name: fieldName})[0];
+        };
+
+        this.getEntity = function(entityName) {
+          return $scope.entities[entityName];
+        };
+
+        this.getSelectedEntity = function() {
+          return $scope.selectedEntity;
         };
 
         $scope.valuesFields = function() {
     return allFields;
   }
 
+  // Turns a space-separated list (e.g. css classes) into an array
+  function splitClass(str) {
+    if (_.isArray(str)) {
+      return str;
+    }
+    return str ? _.unique(_.trim(str).split(/\s+/g)) : [];
+  }
+
   angular.module('afGuiEditor').directive('afGuiBlock', function() {
     return {
       restrict: 'A',
       templateUrl: '~/afGuiEditor/block.html',
       scope: {
-        block: '=afGuiBlock'
+        node: '=afGuiBlock',
+        entityName: '='
       },
-      link: function($scope, element, attrs) {
-        $scope.isItemVisible = function(block) {
-          return (block['#tag'] === 'af-fieldset') || (block['#tag'] === 'div' && _.contains(block['class'], 'af-block'));
+      require: '^^afGuiEditor',
+      link: function($scope, element, attrs, editor) {
+        $scope.editor = editor;
+      },
+      controller: function($scope) {
+        $scope.block = this;
+        this.node = $scope.node;
+
+        this.modifyClasses = function(item, toRemove, toAdd) {
+          var classes = splitClass(item['class']);
+          if (toRemove) {
+            classes = _.difference(classes, splitClass(toRemove));
+          }
+          if (toAdd) {
+            classes = _.unique(classes.concat(splitClass(toAdd)));
+          }
+          item['class'] = classes.join(' ');
+        };
+
+        this.getNodeType = function(node) {
+          if (!node) {
+            return null;
+          }
+          if (node['#tag'] === 'af-field') {
+            return 'field';
+          }
+          if (node['af-fieldset']) {
+            return 'fieldset';
+          }
+          var classes = splitClass(node['class']);
+          if (_.contains(classes, 'af-block')) {
+            return 'block';
+          }
+          if (_.contains(classes, 'af-text')) {
+            return 'text';
+          }
+          return null;
+        };
+
+        $scope.isSelectedFieldset = function(entityName) {
+          return entityName === $scope.editor.getSelectedEntity();
+        };
+
+        $scope.selectEntity = function() {
+          if ($scope.node['af-fieldset']) {
+            $scope.editor.selectEntity($scope.node['af-fieldset']);
+          }
         };
+
+        $scope.tags = {
+          div: ts('Block'),
+          fieldset: ts('Fieldset')
+        };
+
       }
     };
   });
 
-  angular.module('afGuiEditor').directive('afGuiFieldset', function() {
+  angular.module('afGuiEditor').directive('afGuiField', function() {
     return {
       restrict: 'A',
-      templateUrl: '~/afGuiEditor/fieldset.html',
+      templateUrl: '~/afGuiEditor/field.html',
       scope: {
-        fieldset: '=afGuiFieldset'
+        node: '=afGuiField',
+        entityName: '='
+      },
+      require: '^^afGuiEditor',
+      link: function($scope, element, attrs, editor) {
+        $scope.editor = editor;
       },
-      link: function($scope, element, attrs) {
-        $scope.isItemVisible = function(block) {
-          return (block['#tag'] === 'af-field') || (block['#tag'] === 'div' && _.contains(block['class'], 'af-block'));
+      controller: function($scope) {
+
+        $scope.getEntity = function() {
+          return $scope.editor.getEntity($scope.entityName);
+        };
+
+        $scope.getDefn = function() {
+          return $scope.editor.getField($scope.getEntity().type, $scope.node.name);
         };
       }
     };
   });
 
-  angular.module('afGuiEditor').directive('afGuiField', function() {
+  angular.module('afGuiEditor').directive('afGuiText', function() {
     return {
       restrict: 'A',
-      templateUrl: '~/afGuiEditor/field.html',
+      templateUrl: '~/afGuiEditor/text.html',
       scope: {
-        field: '=afGuiField'
+        node: '=afGuiText'
       },
-      link: function($scope, element, attrs) {
+      require: '^^afGuiBlock',
+      link: {
+        pre: function($scope, element, attrs, block) {
+          $scope.block = block;
+        },
+        post: function($scope, element, attrs) {
+          if ($scope.block.node && $scope.block.node['#tag'] === 'fieldset') {
+            $scope.tags.legend = ts('Fieldset Legend');
+          }
+        }
+      },
+      controller: function($scope) {
+        $scope.tags = {
+          p: ts('Normal Text'),
+          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(splitClass($scope.node['class']), _.keys($scope.alignments))[0];
+        };
+
+        $scope.setAlign = function(val) {
+          $scope.block.modifyClasses($scope.node, _.keys($scope.alignments), val);
+        };
       }
     };
   });
index 8ceeee2056448b9ab9d3b5e6b8170081eff78721..6aca722e1dce8674dfa64b4ecd83a702c5556979 100644 (file)
@@ -1,6 +1,17 @@
-<div ui-sortable ng-model="block['#children']">
-  <div ng-repeat="item in block['#children']">
-    <div ng-if="isItemVisible(item) && item['#tag'] === 'af-fieldset'" af-gui-fieldset="item" />
-    <div ng-if="isItemVisible(item) && item['#tag'] === 'div'" af-gui-block="item" />
+<div class="af-gui-bar" ng-if="node['#tag'] != 'af-form'" ng-click="selectEntity()" >
+  <span ng-if="block.getNodeType(node) == 'fieldset'">{{ editor.getEntity(entityName).label }}</span>
+  {{ node['#tag'] }}
+  <select class="pull-right" ng-model="node['#tag']">
+    <option ng-repeat="(opt, label) in tags" value="{{ opt }}">{{ label }}</option>
+  </select>
+</div>
+<div ui-sortable="{handle: '.af-gui-bar'}" ng-model="node['#children']">
+  <div ng-repeat="item in node['#children']">
+    <div ng-switch="block.getNodeType(item)">
+      <div ng-switch-when="fieldset" af-gui-block="item" class="af-gui-block af-gui-fieldset" ng-class="{'af-entity-selected': isSelectedFieldset(item['af-fieldset'])}" entity-name="item['af-fieldset']" />
+      <div ng-switch-when="block" af-gui-block="item" class="af-gui-block" entity-name="entityName" />
+      <div ng-switch-when="field" af-gui-field="item" class="af-gui-field" entity-name="entityName" />
+      <div ng-switch-when="text" af-gui-text="item" class="af-gui-text" />
+    </div>
   </div>
 </div>
index f1d862d582b8fadd2a8cfb722574e481be5ea63e..8d0eda2044cbd3ec07c4f0097ece41ff1381ab71 100644 (file)
@@ -3,6 +3,6 @@
     {{ ts('Form Layout') }}
   </div>
   <div class="panel-body">
-    <div af-gui-block="layout" />
+    <div af-gui-block="layout" entity-name="" />
   </div>
 </div>
index 4dd21cd271773fc01e17c90ef9bf21923038e3b2..495319c9075524fc4b374e4f0c5b322ca63ab9bc 100644 (file)
@@ -1,13 +1,13 @@
 <div>
-  <a href ng-click="removeEntity(selectedEntity)" class="btn btn-sm btn-danger pull-right">
+  <a href ng-click="editor.removeEntity(selectedEntity)" class="btn btn-sm btn-danger pull-right">
     <i class="crm-i fa-trash"></i>
   </a>
 
   <fieldset>
     <legend>{{ ts('Values') }}</legend>
     <div class="form-inline" ng-if="entity.data" ng-repeat="(fieldName, value) in entity.data">
-      <label>{{ getField(entity.type, fieldName).title }}:</label>
-      <input class="form-control" af-gui-field-value="getField(entity.type, fieldName)" ng-model="entity.data[fieldName]" />
+      <label>{{ editor.getField(entity.type, fieldName).title }}:</label>
+      <input class="form-control" af-gui-field-value="editor.getField(entity.type, fieldName)" ng-model="entity.data[fieldName]" />
       <a href class="pull-right" ng-click="removeValue(entity, fieldName)">
         <i class="crm-i fa-times"></i>
       </a>
index 0f236440c8c8ffbd966c6e90b27a62e0701a1310..b67b7730c24307e3155e8dbecaf6a204b4faad1b 100644 (file)
@@ -1,3 +1,6 @@
+<div class="af-gui-bar">
+  <span>{{ getEntity().label + ': ' + getDefn().title }}</span>
+</div>
 <div>
-  {{ field.name }}
+  {{ node.name }}
 </div>
diff --git a/ext/afform/gui/ang/afGuiEditor/fieldset.html b/ext/afform/gui/ang/afGuiEditor/fieldset.html
deleted file mode 100644 (file)
index b2c85fe..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<div ui-sortable ng-model="fieldset['#children']">
-  <div ng-repeat="item in fieldset['#children']">
-    <div ng-if="isItemVisible(item) && item['#tag'] === 'af-field'" af-gui-field="item" />
-    <div ng-if="isItemVisible(item) && item['#tag'] === 'div'" af-gui-block="item" />
-  </div>
-</div>
index 78ca04333451cca76248676cb1f3410268453940..5278c642fc491434de66495f03def5f04c8c4d31 100644 (file)
@@ -1,12 +1,12 @@
 <div id="afGuiEditor-palette-config" class="panel panel-default">
     <ul id="afGuiEditor-palette-tabs" class="panel-heading nav nav-tabs">
       <li role="presentation" ng-class="{active: selectedEntity === null}">
-        <a href ng-click="selectEntity(null)">
+        <a href ng-click="editor.selectEntity(null)">
           <span>{{ ts('Form Settings') }}</span>
         </a>
       </li>
       <li role="presentation" ng-repeat="entity in entities" ng-class="{active: selectedEntity === entity.name}">
-        <a href ng-click="selectEntity(entity.name)">
+        <a href ng-click="editor.selectEntity(entity.name)">
           <span af-gui-editable ng-model="entity.label">{{ entity.label }}</span>
         </a>
       </li>
@@ -16,7 +16,7 @@
         </a>
         <ul class="dropdown-menu">
           <li ng-repeat="entity in meta.entities">
-            <a href ng-click="addEntity(entity.name)">{{ entity.name }}</a>
+            <a href ng-click="editor.addEntity(entity.name)">{{ entity.name }}</a>
           </li>
         </ul>
       </li>
diff --git a/ext/afform/gui/ang/afGuiEditor/text.html b/ext/afform/gui/ang/afGuiEditor/text.html
new file mode 100644 (file)
index 0000000..4babf01
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="af-gui-bar">
+  {{ node['#tag'] }}
+  <select class="pull-right" ng-model="node['#tag']">
+    <option ng-repeat="(opt, label) in tags" value="{{ opt }}">{{ label }}</option>
+  </select>
+  <div class="btn-group btn-group-xs pull-right" role="group">
+    <button type="button" class="btn btn-default" ng-class="{active: (opt === getAlign()) || (opt === 'text-left' && !getAlign())}" ng-repeat="(opt, label) in alignments" ng-click="setAlign(opt)" title="{{ label }}">
+      <i class="crm-i fa-{{ opt.replace('text', 'align') }}" ></i>
+    </button>
+  </div>
+</div>
+<p af-gui-editable ng-model="node['#children'][0]['#text']" class="{{ getAlign() + ' af-gui-text-' + node['#tag'] }}" >
+  {{ node['#children'][0]['#text'] }}
+</p>
index 8e5e52ea73e519b92cdaf091fd69da511b4b3a29..6dd4ac78752cbf08567c9c30333ef8cb749a3506 100644 (file)
@@ -4,8 +4,8 @@
   <af-entity type="Contact" data="{contact_type: 'Individual'}" name="parent" label="Parent" url-autofill="1" autofill="user" />
   <af-entity type="Contact" data="{contact_type: 'Individual'}" name="spouse" label="Spouse" contact-relationship="['Spouse of', 'parent']" />
 
-  <div af-fieldset="parent">
-    <h3>About You</h3>
+  <fieldset af-fieldset="parent">
+    <legend class="af-text">About You</legend>
 
     <div class="af-block">
       <af-field name="first_name" />
     <af-field name="constituent_information.Marital_Status" />
     <af-field name="constituent_information.Marriage_Date" />
     <af-field name="constituent_information.Most_Important_Issue" />
-  </div>
+  </fieldset>
 
   <div af-fieldset="spouse" ng-if="modelListCtrl.getData('parent')['constituent_information.Marital_Status'] == 'M'">
-    <h3>About Your Spouse</h3>
-
+    <h3 class="af-text text-center">About Your Spouse</h3>
+    <p class="af-text">Only visible if you are married.</p>
     <af-field name="first_name" defn='{title: ts("Spouse First Name")}' />
     <af-field name="last_name" defn='{title: ts("Spouse Last Name")}' />
     <af-field name="do_not_email" />