Edit option lists
authorColeman Watts <coleman@civicrm.org>
Thu, 14 Nov 2019 19:07:37 +0000 (14:07 -0500)
committerCiviCRM <info@civicrm.org>
Wed, 16 Sep 2020 02:13:20 +0000 (19:13 -0700)
30 files changed:
ext/afform/core/afform.php
ext/afform/core/ang/afField/afField.js
ext/afform/core/ang/afField/widgets/CheckBox.html
ext/afform/core/ang/afField/widgets/Radio.html
ext/afform/core/ang/afField/widgets/Select.html
ext/afform/gui/afform_gui.php
ext/afform/gui/ang/afGuiEditor.css
ext/afform/gui/ang/afGuiEditor.js
ext/afform/gui/ang/afGuiEditor/block.html
ext/afform/gui/ang/afGuiEditor/button.html
ext/afform/gui/ang/afGuiEditor/editOptions.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/field.html
ext/afform/gui/ang/afGuiEditor/inputType/CheckBox.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/inputType/Date.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/inputType/Number.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/inputType/Radio.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/inputType/RichTextEditor.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/inputType/Select.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/inputType/Text.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/inputType/TextArea.html [new file with mode: 0644]
ext/afform/gui/ang/afGuiEditor/text.html
ext/afform/gui/ang/afGuiEditor/widgets/CheckBox.html [deleted file]
ext/afform/gui/ang/afGuiEditor/widgets/Date.html [deleted file]
ext/afform/gui/ang/afGuiEditor/widgets/Number.html [deleted file]
ext/afform/gui/ang/afGuiEditor/widgets/Radio.html [deleted file]
ext/afform/gui/ang/afGuiEditor/widgets/RichTextEditor.html [deleted file]
ext/afform/gui/ang/afGuiEditor/widgets/Select.html [deleted file]
ext/afform/gui/ang/afGuiEditor/widgets/Text.html [deleted file]
ext/afform/gui/ang/afGuiEditor/widgets/TextArea.html [deleted file]
ext/afform/mock/ang/testAfform.aff.html

index 20fe6b1e7767fc40960c15a7c74110593af44e1c..a2eebb33ee86b981400198ada6958722facf5f92 100644 (file)
@@ -329,6 +329,11 @@ function afform_civicrm_alterAngular($angular) {
             // If it's not an object, don't mess with it.
             continue;
           }
+          // TODO: Teach the api to return options in this format
+          if (!empty($fieldInfo['options'])) {
+            $fieldInfo['options'] = CRM_Utils_Array::makeNonAssociative($fieldInfo['options'], 'key', 'label');
+          }
+
           $fieldDefn = $existingFieldDefn ? CRM_Utils_JS::getRawProps($existingFieldDefn) : [];
           foreach ($fieldInfo as $name => $prop) {
             // Merge array props 1 level deep
index bd30e103d8f30452f7a731f1e6e8a30a18e7e428..f6004173236a971553dbce9db7e675907ceca638 100644 (file)
         $scope.fieldId = $scope.afFieldset.getDefn().modelName + '-' + $scope.fieldName;
         $scope.getData = $scope.afFieldset.getData;
 
+        $el.addClass('af-field-type-' + _.kebabCase($scope.defn.input_type));
+      },
+      controller: function($scope) {
+
         $scope.getOptions = function() {
+          return $scope.defn.options || [{key: '1', label: ts('Yes')}, {key: '0', label: ts('No')}];
+        };
+
+        $scope.select2Options = function() {
           return {
-            results: _.transform($scope.defn.options, function(result, val, key) {
-              result.push({id: key, text: val});
+            results: _.transform($scope.getOptions(), function(result, opt) {
+              result.push({id: opt.key, text: opt.label});
             }, [])
           };
         };
-
-        $el.addClass('af-field-type-' + _.kebabCase($scope.defn.input_type));
       }
     };
   });
index bf0bd8bb9e6a044a77f03b3ed029cf1ef59918ba..fbdab6b0b219f73a482b21e75cd9307829c5b7b3 100644 (file)
@@ -1,7 +1,7 @@
 <ul class="crm-checkbox-list" id="{{ fieldId }}" ng-if="defn.options">
-  <li ng-repeat="(key, val) in defn.options" >
-    <input type="checkbox" checklist-model="getData()[name]" id="{{ fieldId + key }}" checklist-value="key" />
-    <label for="{{ fieldId + key }}">{{ val }}</label>
+  <li ng-repeat="opt in defn.options" >
+    <input type="checkbox" checklist-model="getData()[name]" id="{{ fieldId + opt.key }}" checklist-value="opt.key" />
+    <label for="{{ fieldId + opt.key }}">{{ opt.label }}</label>
   </li>
 </ul>
-<input type="checkbox" ng-if="!defn.options" id="{{ fieldId }}" ng-model="getData()[fieldName]" />
+<input type="checkbox" ng-if="!defn.options" id="{{ fieldId }}" ng-model="getData()[fieldName]" ng-true-value="'1'" ng-false-value="'0'" />
index c20d3daf038458871baa3a5dd1dbb0c44565d776..f914f64c4377c174cb575af5e85fc5152e9f85b5 100644 (file)
@@ -1,4 +1,4 @@
-<label ng-repeat="(key, val) in defn.options" >
-  <input class="crm-form-radio" type="radio" ng-model="getData()[fieldName]" value="{{ key }}" />
-  {{ val }}
+<label ng-repeat="(opt in getOptions()" >
+  <input class="crm-form-radio" type="radio" ng-model="getData()[fieldName]" value="{{ opt.key }}" />
+  {{ opt.label }}
 </label>
index c0812ab4e4cabd6a568adcc7c5df10a4870d9018..a89e8464ecf31878e15cbf19db51f16ce5fbbba0 100644 (file)
@@ -1 +1 @@
-<input crm-ui-select="{data: getOptions, multiple: defn.input_attrs.multiple, placeholder: defn.input_attrs.placeholder}" id="{{ fieldId }}" ng-model="getData()[fieldName]" />
+<input crm-ui-select="{data: select2Options, multiple: defn.input_attrs.multiple, placeholder: defn.input_attrs.placeholder}" id="{{ fieldId }}" ng-model="getData()[fieldName]" />
index e63047898f8e12606fa3cdb5c15880860e1bb8fc..19a71aa48ad31deae8b3c0aba27da1e1eecc6963 100644 (file)
@@ -185,10 +185,20 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
       ->setIncludeCustom(TRUE)
       ->setLoadOptions(TRUE)
       ->setAction('create')
-      ->setSelect(['name', 'title', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize'])
+      ->setSelect(['name', 'title', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type'])
       ->addWhere('input_type', 'IS NOT NULL')
       ->execute()
       ->indexBy('name');
+
+    // TODO: Teach the api to return options in this format
+    foreach ($data['fields'][$entityName] as $name => $field) {
+      if (!empty($field['options'])) {
+        $data['fields'][$entityName][$name]['options'] = CRM_Utils_Array::makeNonAssociative($field['options'], 'key', 'label');
+      }
+      else {
+        unset($data['fields'][$entityName][$name]['options']);
+      }
+    }
   }
 
   // Now adjust the field metadata
@@ -197,10 +207,10 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
 
   // Scan for input types
   // FIXME: Need a way to load this from other extensions too
-  foreach (glob(__DIR__ . '/ang/afGuiEditor/widgets/*.html') as $file) {
+  foreach (glob(__DIR__ . '/ang/afGuiEditor/inputType/*.html') as $file) {
     $matches = [];
     preg_match('/([-a-z_A-Z0-9]*).html/', $file, $matches);
-    $data['widgets'][$matches[1]] = $matches[1];
+    $data['inputType'][$matches[1]] = $matches[1];
   }
 
   $mimeType = 'text/javascript';
index 6ed42502d08b4473d436d9b00031291fda5da034..2b4667c1fd7b46ba6ab8e2ffa7fb73c76cc55878 100644 (file)
 
 #afGuiEditor .af-gui-block {
   border: 2px dashed transparent;
-  padding-top: 22px;
+  position: relative;
+  padding: 22px 3px 3px;
   min-height: 40px;
 }
 
 }
 /* grip handle */
 #afGuiEditor [ui-sortable] .af-gui-bar:before,
-#afGuiEditor .af-gui-field-select-list > div:not(.disabled):hover:before {
+#afGuiEditor .af-gui-field-select-list > div:not(.disabled):hover:before,
+#afGuiEditor [af-gui-edit-options] [ui-sortable] li:before {
   background-size: cover;
   background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1IiBoZWlnaHQ9IjUiPgo8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSIjODg4Ij48L3JlY3Q+Cjwvc3ZnPg==");
   width: 10px;
   cursor: default;
 }
 
-#afGuiEditor .af-gui-field-widget input[type=text].form-control {
+#afGuiEditor .af-gui-field-input input[type=text].form-control {
   color: #9a9a9a;
 }
 
-#afGuiEditor .af-gui-field-widget-number input[type=text].form-control {
+#afGuiEditor .af-gui-field-input-number input[type=text].form-control {
   background-image: url('../images/number.png');
   background-repeat: no-repeat;
   background-position: center right 6px;
 }
 
-#afGuiEditor .af-gui-field-widget input.crm-form-date {
+#afGuiEditor .af-gui-field-input input.crm-form-date {
   width: 140px;
   margin-right: -2px;
 }
-#afGuiEditor .af-gui-field-widget input.crm-form-time {
+#afGuiEditor .af-gui-field-input input.crm-form-time {
   width: 80px;
 }
 
+#afGuiEditor .af-gui-field-input-type-radio label.radio {
+  font-weight: normal;
+  margin-right: 10px;
+}
+#afGuiEditor .af-gui-field-input-type-radio label.radio input[type=radio] {
+  margin: 0;
+
+}
+
 #afGuiEditor .af-gui-text-h1 {
   font-weight: bolder;
   font-size: 16px;
 #afGuiEditor .af-gui-field-help {
   font-style: italic;
 }
+
+
+#afGuiEditor.af-gui-editing-options {
+  pointer-events: none;
+  cursor: default;
+}
+#afGuiEditor.af-gui-editing-options .panel-heading,
+#afGuiEditor.af-gui-editing-options .af-gui-element,
+.af-gui-editing-options #afGuiEditor-palette .panel-body > * {
+  opacity: .5;
+}
+#afGuiEditor.af-gui-editing-options .af-gui-block {
+  border: 2px solid transparent;
+}
+#afGuiEditor.af-gui-editing-options .af-gui-bar:not(.af-gui-edit-options-bar) {
+  visibility: hidden;
+}
+#afGuiEditor.af-gui-editing-options .af-gui-bar:before {
+  background: none;
+}
+
+#afGuiEditor [af-gui-edit-options] {
+  border: 2px solid #0071bd;
+  pointer-events: auto;
+  cursor: auto;
+  padding-top: 35px;
+  position: relative;
+}
+
+#afGuiEditor [af-gui-edit-options] .af-gui-edit-options-bar {
+  height: 30px;
+  font-family: "Courier New", Courier, monospace;
+  font-size: 12px;
+  width: 100%;
+  background-color: #f2f2f2;
+  position: absolute;
+  top: 0;
+  left: 0;
+  padding-left: 15px;
+}
+#afGuiEditor [af-gui-edit-options] ul[ui-sortable] {
+  padding: 5px 20px 0;
+}
+#afGuiEditor [af-gui-edit-options] ul[ui-sortable] li {
+  list-style: none;
+  padding-left: 15px;
+  position: relative;
+  background-color:#e7ecf1;
+  cursor: move;
+}
+#afGuiEditor [af-gui-edit-options] ul[ui-sortable] li:nth-child(even) {
+  background-color:#f2f2f2;
+}
+#afGuiEditor [af-gui-edit-options] ul[ui-sortable] li > div {
+  width: calc(100% - 30px);
+  display: inline-block;
+}
+#afGuiEditor [af-gui-edit-options] ul.af-gui-edit-options-deleted li > div {
+  text-decoration: line-through;
+}
+#afGuiEditor [af-gui-edit-options] ul[ui-sortable] li .btn-xs {
+  border: 0 none;
+}
+#afGuiEditor [af-gui-edit-options] h5 {
+  margin-left: 20px;
+}
index 542473f94bb7d584d6b74e27372e3984907d03f9..61eaabac63908f86e67441918e1a6250a7312f7e 100644 (file)
       },
       controller: function($scope) {
         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.entityName) : {};
         };
 
-        $scope.getDefn = function() {
+        $scope.getDefn = this.getDefn = function() {
           return $scope.editor ? $scope.editor.getField($scope.getEntity().type, $scope.node.name) : {};
         };
 
-        $scope.getOptions = function() {
+        $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.select2Options = function() {
           return {
-            results: _.transform($scope.getProp('options'), function(result, val, key) {
-              result.push({id: key, text: val});
+            results: _.transform($scope.getOptions(), function(result, opt) {
+              result.push({id: opt.key, text: opt.label});
             }, [])
           };
         };
 
+        $scope.resetOptions = function() {
+          delete $scope.node.defn.options;
+        };
+
+        $scope.editOptions = function() {
+          $scope.editingOptions = true;
+          $('#afGuiEditor').addClass('af-gui-editing-options');
+        };
+
+        $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(),
           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.toggleRequired = function() {
           getSet('required', !getSet('required'));
           return false;
         };
 
         $scope.toggleHelp = function(position) {
-          getSet('help_' + position, getSet('help_' + position) === null ? ($scope.getDefn()['help_' + position] || ts('Enter text')) : null);
+          getSet('help_' + position, $scope.propIsset('help_' + position) ? null : ($scope.getDefn()['help_' + position] || ts('Enter text')));
           return false;
         };
 
           return _.wrap(propName, getSet);
         };
 
-        // 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;
-        }
-
         // Getter/setter callback
         function getSet(propName, val) {
           if (arguments.length > 1) {
           }
           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;
+        }
+      }
+    };
+  });
 
+  angular.module('afGuiEditor').directive('afGuiEditOptions', function() {
+    return {
+      restrict: 'A',
+      templateUrl: '~/afGuiEditor/editOptions.html',
+      scope: true,
+      require: '^^afGuiField',
+      link: function ($scope, element, attrs, afGuiField) {
+        $scope.field = afGuiField;
+        $scope.options = JSON.parse(angular.toJson(afGuiField.getOptions()));
+        var optionKeys = _.map($scope.options, 'key');
+        $scope.deletedOptions = _.filter(JSON.parse(angular.toJson(afGuiField.getDefn().options || [])), function(item) {
+          return !_.contains(optionKeys, item.key);
+        });
+        $scope.originalLabels = _.transform(afGuiField.getDefn().options || [], function(originalLabels, item) {
+          originalLabels[item.key] = item.label;
+        }, {});
+      },
+      controller: function ($scope) {
+        var ts = $scope.ts = CRM.ts();
+
+        $scope.deleteOption = function(option, $index) {
+          $scope.options.splice($index, 1);
+          $scope.deletedOptions.push(option);
+        };
+
+        $scope.restoreOption = function(option, $index) {
+          $scope.deletedOptions.splice($index, 1);
+          $scope.options.push(option);
+        };
+        
+        $scope.save = function() {
+          $scope.field.getSet('options', JSON.parse(angular.toJson($scope.options)));
+          $scope.close();
+        };
+
+        $scope.close = function() {
+          $scope.field.setEditingOptions(false);
+          $('#afGuiEditor').removeClass('af-gui-editing-options');
+        };
       }
     };
   });
     };
   });
 
+  // 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
+  angular.module('afGuiEditor').directive('afGuiMenu', function() {
+    return {
+      restrict: 'A',
+      link: function($scope, element, attrs) {
+        $scope.menu = {};
+        element
+          .on('show.bs.dropdown', function() {
+            $scope.$apply(function() {
+              $scope.menu.open = true;
+            });
+          })
+          .on('hidden.bs.dropdown', function() {
+            $scope.$apply(function() {
+              $scope.menu.open = false;
+            });
+          });
+      }
+    };
+  });
+
   // Editable titles using ngModel & html5 contenteditable
   // Cribbed from ContactLayoutEditor
   angular.module('afGuiEditor').directive("afGuiEditable", function() {
index 11c28be13b15fc29b1be5fbb181697c5f67f8446..d8c862e92ab66c8aeefe68ab69a58b5f7ba95002 100644 (file)
@@ -1,7 +1,7 @@
 <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>
   <span>{{ node['#tag'] }}</span>
-  <div class="form-inline pull-right">
+  <div class="form-inline pull-right" af-gui-menu>
     <div class="btn-group btn-group-xs" role="group">
       <button type="button" class="btn btn-default" ng-class="{active: opt === getLayout()}" ng-repeat="(opt, label) in layouts" ng-click="setLayout(opt)" title="{{ label }}">
         <i class="af-gui-layout-icon {{ opt }}" ></i>
     <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-block-button" data-toggle="dropdown" title="{{ ts('Add block') }}">
       <span><i class="crm-i fa-plus"></i></span>
     </button>
-    <ul class="dropdown-menu">
+    <ul class="dropdown-menu" ng-if="menu.open">
       <li><a href ng-click="addBlock('div.af-block')">{{ ts('Add block') }}</a></li>
       <li><a href ng-click="addBlock('p.af-text')">{{ ts('Add text box') }}</a></li>
       <li><a href ng-click="addBlock('button.af-button.btn-primary', {'crm-icon': 'fa-check', 'ng-click': 'modelListCtrl.submit()'})">{{ ts('Add button') }}</a></li>
+      <li role="separator" class="divider"></li>
       <li><a href ng-click="block.removeBlock(node)"><span class="text-danger">{{ ts('Delete this block') }}</span></a></li>
     </ul>
   </div>
 </div>
 <div class="af-gui-bar" ng-if="node['#tag'] === 'af-form'" >
-  <div class="form-inline pull-right">
+  <div class="form-inline pull-right" af-gui-menu>
     <button type="button" class="btn  btn-default btn-sm dropdown-toggle af-gui-add-canvas-button" data-toggle="dropdown" title="{{ ts('Add block') }}">
       <span>Add <i class="crm-i fa-plus"></i></span>
     </button>
-    <ul class="dropdown-menu">
+    <ul class="dropdown-menu" ng-if="menu.open">
       <li><a href ng-click="addBlock('div.af-block')">{{ ts('Add block') }}</a></li>
       <li><a href ng-click="addBlock('p.af-text')">{{ ts('Add text box') }}</a></li>
       <li><a href ng-click="addBlock('button.af-button.btn-primary', {'crm-icon': 'fa-check', 'ng-click': 'modelListCtrl.submit()'})">{{ ts('Add button') }}</a></li>
@@ -40,9 +41,9 @@
 <div ui-sortable="{handle: '.af-gui-bar', connectWith: '[ui-sortable]', cancel: 'input,textarea,button,select,option,a'}" ng-model="node['#children']" class="af-gui-layout {{ getLayout() }}">
   <div ng-repeat="item in node['#children']" ng-show="block.getNodeType(item)">
     <div ng-switch="block.getNodeType(item)">
-      <div ng-switch-when="fieldset" af-gui-block="item" class="af-gui-element af-gui-block af-gui-fieldset af-gui-block-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="block" af-gui-block="item" class="af-gui-element af-gui-block af-gui-block-type-{{ item['#tag'] }}" entity-name="entityName" />
-      <div ng-switch-when="field" af-gui-field="item" class="af-gui-element af-gui-field" entity-name="entityName" />
+      <div ng-switch-when="fieldset" af-gui-block="item" class="af-gui-block af-gui-fieldset af-gui-block-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="block" af-gui-block="item" class="af-gui-block af-gui-block-type-{{ item['#tag'] }}" entity-name="entityName" />
+      <div ng-switch-when="field" af-gui-field="item" entity-name="entityName" />
       <div ng-switch-when="text" af-gui-text="item" class="af-gui-element af-gui-text" />
       <div ng-switch-when="button" af-gui-button="item" class="af-gui-element af-gui-button" />
     </div>
index f566c9b1874eb1156f4377bf5ea306ca40599fbc..fe35de767484114262375e1c528fa60d9baf5a8c 100644 (file)
@@ -1,10 +1,10 @@
 <div class="af-gui-bar">
   <div class="form-inline pull-right">
-    <div class="btn-group pull-right">
+    <div class="btn-group pull-right" af-gui-menu>
       <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-block-button" data-toggle="dropdown" title="{{ ts('Configure') }}">
         <span><i class="crm-i fa-gear"></i></span>
       </button>
-      <ul class="dropdown-menu">
+      <ul class="dropdown-menu" ng-if="menu.open">
         <li title="{{ ts('Button style') }}">
           <a href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown" >
             <select class="form-control {{ getSetStyle().replace('btn', 'text') }}" ng-model="getSetStyle" ng-model-options="{getterSetter: true}">
@@ -12,6 +12,7 @@
             </select>
           </a>
         </li>
+        <li role="separator" class="divider"></li>
         <li><a href ng-click="block.removeBlock(node)"><span class="text-danger">{{ ts('Delete this button') }}</span></a></li>
       </ul>
     </div>
diff --git a/ext/afform/gui/ang/afGuiEditor/editOptions.html b/ext/afform/gui/ang/afGuiEditor/editOptions.html
new file mode 100644 (file)
index 0000000..c576262
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="af-gui-edit-options-bar" >
+  <h4 class="pull-left">{{ ts('Edit option list') }}</h4>
+  <div class="btn-group-sm pull-right">
+    <button type="button" class="btn btn-default" ng-click="close()">
+      <i class="crm-i fa-times-circle"></i>
+      {{ ts('Cancel') }}
+    </button>
+    <button type="button" class="btn btn-success" ng-click="save()">
+      <i class="crm-i fa-check-circle"></i>
+      {{ ts('Save') }}
+    </button>
+  </div>
+</div>
+<ul ui-sortable="{connectWith: '.af-gui-edit-options-deleted', cancel: 'input,textarea,button,select,option,a,[contenteditable]'}" ng-model="options" class="af-gui-edit-options-enabled">
+  <li ng-repeat="option in options">
+    <div af-gui-editable ng-model="option.label" default-value="originalLabels[option.key]" >{{ option.label }}</div>
+    <button type="button" class="btn btn-danger-outline btn-xs pull-right" ng-click="deleteOption(option, $index)" title="{{ ts('Remove option') }}">
+      <i class="crm-i fa-trash"></i>
+    </button>
+  </li>
+</ul>
+<h5 ng-show="deletedOptions.length">{{ ts('Deleted options') }}</h5>
+<ul ng-if="deletedOptions.length" ui-sortable="{connectWith: '.af-gui-edit-options-enabled'}" ng-model="deletedOptions" class="af-gui-edit-options-deleted">
+  <li ng-repeat="option in deletedOptions">
+    <div>{{ option.label }}</div>
+    <button type="button" class="btn btn-success-outline btn-xs pull-right" ng-click="restoreOption(option, $index)" title="{{ ts('Restore option') }}">
+      <i class="crm-i fa-arrow-circle-o-up"></i>
+    </button>
+  </li>
+</ul>
index 0085c9f8d4565d73ed807e29fb27e0770d6cae8f..b04caaf519559ce1ef513ce102d016ba9ed564c5 100644 (file)
@@ -1,51 +1,67 @@
-<div class="af-gui-bar" title="{{ getEntity().label + ': ' + getDefn().title }}">
-  <div class="form-inline pull-right">
-
-    <div class="btn-group">
-      <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-block-button" data-toggle="dropdown" title="{{ ts('Configure') }}">
-        <span><i class="crm-i fa-gear"></i></span>
-      </button>
-      <ul class="dropdown-menu dropdown-menu-right">
-        <li title="{{ ts('Field type') }}">
-          <a href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
-            <select class="form-control" ng-model="getSet('input_type')" ng-model-options="{getterSetter: true}">
-              <option ng-repeat="(opt, label) in editor.meta.widgets" value="{{ opt }}">{{ label }}</option>
-            </select>
-          </a>
-        </li>
-        <li title="{{ ts('Require this field') }}">
-          <a href ng-click="toggleRequired(); $event.stopPropagation();">
-            <i class="crm-i" ng-class="{'fa-square-o': !getProp('required'), 'fa-check-square-o': getProp('required')}"></i>
-            {{ ts('Required') }}
-          </a>
-        </li>
-        <li title="{{ ts('Show help text above this field') }}">
-          <a href ng-click="toggleHelp('pre'); $event.stopPropagation();">
-            <i class="crm-i" ng-class="{'fa-square-o': !getProp('help_pre'), 'fa-check-square-o': getProp('help_pre')}"></i>
-            {{ ts('Pre help text') }}
-          </a>
-        </li>
-        <li title="{{ ts('Show help text below this field') }}">
-          <a href ng-click="toggleHelp('post'); $event.stopPropagation();">
-            <i class="crm-i" ng-class="{'fa-square-o': !getProp('help_post'), 'fa-check-square-o': getProp('help_post')}"></i>
-            {{ ts('Post help text') }}
-          </a>
-        </li>
-        <li title="{{ ts('Remove field from form') }}">
-          <a href ng-click="block.removeBlock(node)"><span class="text-danger">{{ ts('Delete this field') }}</span></a>
-        </li>
-      </ul>
+<div af-gui-edit-options ng-if="editingOptions"></div>
+<div ng-if="!editingOptions" class="af-gui-element af-gui-field" >
+  <div class="af-gui-bar" title="{{ getEntity().label + ': ' + getDefn().title }}">
+    <div class="form-inline pull-right">
+      <div class="btn-group" af-gui-menu >
+        <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-block-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">
+          <li title="{{ ts('Field type') }}">
+            <a href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
+              <select class="form-control" ng-model="getSet('input_type')" ng-model-options="{getterSetter: true}">
+                <option ng-repeat="(type, label) in editor.meta.inputType" value="{{ type }}" ng-if="inputTypeCanBe(type)">{{ label }}</option>
+              </select>
+            </a>
+          </li>
+          <li title="{{ ts('Require this field') }}">
+            <a href ng-click="toggleRequired(); $event.stopPropagation();">
+              <i class="crm-i" ng-class="{'fa-square-o': !getProp('required'), 'fa-check-square-o': getProp('required')}"></i>
+              {{ ts('Required') }}
+            </a>
+          </li>
+          <li title="{{ ts('Show help text above this field') }}">
+            <a href ng-click="toggleHelp('pre'); $event.stopPropagation();">
+              <i class="crm-i" ng-class="{'fa-square-o': !propIsset('help_pre'), 'fa-check-square-o': propIsset('help_pre')}"></i>
+              {{ ts('Pre help text') }}
+            </a>
+          </li>
+          <li title="{{ ts('Show help text below this field') }}">
+            <a href ng-click="toggleHelp('post'); $event.stopPropagation();">
+              <i class="crm-i" ng-class="{'fa-square-o': !propIsset('help_post'), 'fa-check-square-o': propIsset('help_post')}"></i>
+              {{ ts('Post help text') }}
+            </a>
+          </li>
+          <li role="separator" class="divider" ng-if="hasOptions()"></li>
+          <li title="{{ ts('Reset the option list for this field') }}" ng-if="hasOptions()" ng-click="$event.stopPropagation()">
+            <a href ng-click="resetOptions()">
+              <i class="crm-i fa-{{ node.defn.options ? '' : 'check-' }}circle-o"></i>
+              {{ ts('Use default option list') }}
+            </a>
+          </li>
+          <li title="{{ ts('Customize the option list for this field') }}" ng-if="hasOptions()">
+            <a href ng-click="editOptions()">
+              <i class="crm-i fa-{{ !node.defn.options ? '' : 'check-' }}circle-o"></i>
+              {{ ts('Customize field options') }}
+            </a>
+          </li>
+          <li role="separator" class="divider"></li>
+          <li title="{{ ts('Remove field from form') }}">
+            <a href ng-click="block.removeBlock(node)"><span class="text-danger">{{ ts('Delete this field') }}</span></a>
+          </li>
+        </ul>
+      </div>
     </div>
   </div>
-</div>
-<label ng-class="{'af-gui-field-required': getProp('required')}" class="af-gui-node-title">
-  <span af-gui-editable ng-model="node.defn.title" default-value="getDefn().title">{{ getProp('title') }}</span>
-</label>
-<div class="af-gui-field-help" ng-if="getProp('help_pre') !== null">
-  <span af-gui-editable ng-model="node.defn.help_pre" default-value="getDefn().help_pre">{{ getProp('help_pre') }}</span>
-</div>
-<div class="af-gui-field-widget af-gui-field-widget-{{ getProp('input_type').toLowerCase() }}" ng-include="'~/afGuiEditor/widgets/' + getProp('input_type') + '.html'"></div>
-<div class="af-gui-field-help" ng-if="getProp('help_post') !== null">
-  <span af-gui-editable ng-model="node.defn.help_post" default-value="getDefn().help_post">{{ getProp('help_post') }}</span>
+  <label ng-class="{'af-gui-field-required': getProp('required')}" class="af-gui-node-title">
+    <span af-gui-editable ng-model="node.defn.title" default-value="getDefn().title">{{ getProp('title') }}</span>
+  </label>
+  <div class="af-gui-field-help" ng-if="propIsset('help_pre')">
+    <span af-gui-editable ng-model="node.defn.help_pre" default-value="getDefn().help_pre">{{ getProp('help_pre') }}</span>
+  </div>
+  <div class="af-gui-field-input af-gui-field-input-type-{{ getProp('input_type').toLowerCase() }}" ng-include="'~/afGuiEditor/inputType/' + getProp('input_type') + '.html'"></div>
+  <div class="af-gui-field-help" ng-if="propIsset('help_post')">
+    <span af-gui-editable ng-model="node.defn.help_post" default-value="getDefn().help_post">{{ getProp('help_post') }}</span>
+  </div>
 </div>
 
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/CheckBox.html b/ext/afform/gui/ang/afGuiEditor/inputType/CheckBox.html
new file mode 100644 (file)
index 0000000..08baba1
--- /dev/null
@@ -0,0 +1,7 @@
+<ul class="crm-checkbox-list" id="{{ fieldId }}" ng-if="getOptions()">
+  <li ng-repeat="opt in getOptions()" >
+    <input type="checkbox" disabled />
+    <label>{{ opt.label }}</label>
+  </li>
+</ul>
+<input type="checkbox" disabled ng-if="!getOptions()" />
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/Date.html b/ext/afform/gui/ang/afGuiEditor/inputType/Date.html
new file mode 100644 (file)
index 0000000..32ce511
--- /dev/null
@@ -0,0 +1,5 @@
+<div class="form-inline">
+  <input autocomplete="off" class="form-control crm-form-date crm-placeholder-icon" placeholder="&#xF073" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" />
+  <span class="addon fa fa-calendar"></span>
+  <input autocomplete="off" ng-if="getProp('input_attrs.time')" placeholder="&#xF017" class="form-control crm-form-time crm-placeholder-icon" ng-model="getSet('input_attrs.timePlaceholder')" ng-model-options="{getterSetter: true}" type="text" />
+</div>
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/Number.html b/ext/afform/gui/ang/afGuiEditor/inputType/Number.html
new file mode 100644 (file)
index 0000000..812d08d
--- /dev/null
@@ -0,0 +1 @@
+<input autocomplete="off" class="form-control" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" />
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/Radio.html b/ext/afform/gui/ang/afGuiEditor/inputType/Radio.html
new file mode 100644 (file)
index 0000000..d1c47b0
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="form-inline">
+  <label ng-repeat="opt in getOptions()" class="radio" >
+    <input class="crm-form-radio" type="radio" disabled />
+    {{ opt.label }}
+  </label>
+</div>
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/RichTextEditor.html b/ext/afform/gui/ang/afGuiEditor/inputType/RichTextEditor.html
new file mode 100644 (file)
index 0000000..8c4a77d
--- /dev/null
@@ -0,0 +1 @@
+<textarea autocomplete="off" class="crm-form-textarea" disabled ></textarea>
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/Select.html b/ext/afform/gui/ang/afGuiEditor/inputType/Select.html
new file mode 100644 (file)
index 0000000..549e7a5
--- /dev/null
@@ -0,0 +1 @@
+<input autocomplete="off" crm-ui-select="{data: select2Options, multiple: defn.input_attrs.multiple}" />
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/Text.html b/ext/afform/gui/ang/afGuiEditor/inputType/Text.html
new file mode 100644 (file)
index 0000000..812d08d
--- /dev/null
@@ -0,0 +1 @@
+<input autocomplete="off" class="form-control" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" />
diff --git a/ext/afform/gui/ang/afGuiEditor/inputType/TextArea.html b/ext/afform/gui/ang/afGuiEditor/inputType/TextArea.html
new file mode 100644 (file)
index 0000000..8c4a77d
--- /dev/null
@@ -0,0 +1 @@
+<textarea autocomplete="off" class="crm-form-textarea" disabled ></textarea>
index 0b65461790030f844b53343f288fc42b530fd303..7c69daac6416f9a6021fd5c246f3cffd70c82c1a 100644 (file)
@@ -1,10 +1,10 @@
 <div class="af-gui-bar">
   <div class="form-inline pull-right">
-    <div class="btn-group pull-right">
+    <div class="btn-group pull-right" af-gui-menu>
       <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-block-button" data-toggle="dropdown" title="{{ ts('Configure') }}">
         <span><i class="crm-i fa-gear"></i></span>
       </button>
-      <ul class="dropdown-menu">
+      <ul class="dropdown-menu" ng-if="menu.open">
         <li title="{{ ts('Text style') }}">
           <a href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
             <select class="form-control" ng-model="node['#tag']">
@@ -21,6 +21,7 @@
             </div>
           </a>
         </li>
+        <li role="separator" class="divider"></li>
         <li>
           <a href ng-click="block.removeBlock(node)"><span class="text-danger">{{ ts('Delete this text') }}</span></a>
         </li>
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/CheckBox.html b/ext/afform/gui/ang/afGuiEditor/widgets/CheckBox.html
deleted file mode 100644 (file)
index 27086e1..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<ul class="crm-checkbox-list" id="{{ fieldId }}" ng-if="getProp('options')">
-  <li ng-repeat="(key, val) in getProp('options')" >
-    <input type="checkbox" disabled />
-    <label >{{ val }}</label>
-  </li>
-</ul>
-<input type="checkbox" disabled ng-if="!getProp('options')" />
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/Date.html b/ext/afform/gui/ang/afGuiEditor/widgets/Date.html
deleted file mode 100644 (file)
index aebd73d..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<div class="form-inline">
-  <input class="form-control crm-form-date" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" />
-  <span class="addon fa fa-calendar"></span>
-  <input ng-if="getProp('input_attrs.time')" class="form-control crm-form-time" ng-model="getSet('input_attrs.timePlaceholder')" ng-model-options="{getterSetter: true}" type="text" />
-</div>
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/Number.html b/ext/afform/gui/ang/afGuiEditor/widgets/Number.html
deleted file mode 100644 (file)
index bfb984a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<input class="form-control" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" />
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/Radio.html b/ext/afform/gui/ang/afGuiEditor/widgets/Radio.html
deleted file mode 100644 (file)
index 4050077..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<label ng-repeat="(key, val) in getProp('options')" >
-  <input class="crm-form-radio" type="radio" disabled value="{{ key }}" />
-  {{ val }}
-</label>
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/RichTextEditor.html b/ext/afform/gui/ang/afGuiEditor/widgets/RichTextEditor.html
deleted file mode 100644 (file)
index 81acfcd..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<textarea class="crm-form-textarea" disabled ></textarea>
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/Select.html b/ext/afform/gui/ang/afGuiEditor/widgets/Select.html
deleted file mode 100644 (file)
index d096b8a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<input crm-ui-select="{data: getOptions, multiple: defn.input_attrs.multiple}" />
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/Text.html b/ext/afform/gui/ang/afGuiEditor/widgets/Text.html
deleted file mode 100644 (file)
index bfb984a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<input class="form-control" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}" type="text" />
diff --git a/ext/afform/gui/ang/afGuiEditor/widgets/TextArea.html b/ext/afform/gui/ang/afGuiEditor/widgets/TextArea.html
deleted file mode 100644 (file)
index 81acfcd..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<textarea class="crm-form-textarea" disabled ></textarea>
index 512bc15303a8f39cedd3b26c6405923766748e5d..9e459aefe56618854c5f3ded0fa10f1b52288ed8 100644 (file)
@@ -11,7 +11,7 @@
       <af-field name="first_name" />
       <af-field name="last_name" />
     </div>
-    <af-field name="gender_id" />
+    <af-field name="gender_id" defn="{options: [{key: 1, label: 'Girl'}, {key: 2, label: 'Boy'}, {key: 3, label: 'Other'}]}"/>
     <af-field name="constituent_information.Marital_Status" />
     <af-field name="constituent_information.Marriage_Date" />
     <af-field name="constituent_information.Most_Important_Issue" />