Improve drag-n-drop placeholder & validation
authorColeman Watts <coleman@civicrm.org>
Sun, 5 Jan 2020 02:10:11 +0000 (21:10 -0500)
committerCiviCRM <info@civicrm.org>
Wed, 16 Sep 2020 02:13:21 +0000 (19:13 -0700)
ext/afform/gui/afform_gui.php
ext/afform/gui/ang/afGuiEditor.css
ext/afform/gui/ang/afGuiEditor.js
ext/afform/gui/ang/afGuiEditor/canvas.html
ext/afform/gui/ang/afGuiEditor/container.html
ext/afform/gui/ang/afGuiEditor/entity.html

index ff9db85758c38dc743d32988bc7d791498715ad1..8f0effff647915495f3a8d95f608840e4542199e 100644 (file)
@@ -261,6 +261,24 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
         ],
       ],
     ],
+    'fieldset' => [
+      'title' => ts('Fieldset'),
+      'element' => [
+        '#tag' => 'fieldset',
+        'af-fieldset' => NULL,
+        '#children' => [
+          [
+            '#tag' => 'legend',
+            'class' => 'af-text',
+            '#children' => [
+              [
+                '#text' => ts('Enter title'),
+              ],
+            ],
+          ],
+        ],
+      ],
+    ],
   ];
 
   // Reformat options
index 3399f74cd0bed4e7d7dbf966b87ee90d3b932698..7848835d6ab0548100e7b6566b8ba24a61ccdf16 100644 (file)
   opacity: 1;
   transition: opacity .2s;
 }
-#afGuiEditor-canvas .panel-body > div > .af-gui-bar {
+#afGuiEditor-canvas-body > div > .af-gui-bar {
   top: -5px;
 }
 
 #afGuiEditor [af-gui-edit-options] h5 {
   margin-left: 20px;
 }
+
+#afGuiEditor .af-gui-dropzone {
+  background-color: #e9eeff;
+  border: 2px solid #0071bd;
+  min-height: 30px;
+}
index d8316b713cb38a52ef5ee944e03eb9ad10a3b703..37336c281055cec3e3001507a04c41fa4f82bdfe 100644 (file)
           var pos = 1 + _.findLastIndex($scope.layout['#children'], {'#tag': 'af-entity'});
           $scope.layout['#children'].splice(pos, 0, $scope.entities[type + num]);
           // Create a new af-fieldset container for the entity
-          var fieldset = {
-            '#tag': 'fieldset',
-            'af-fieldset': type + num,
-            '#children': [
-              {
-                '#tag': 'legend',
-                'class': 'af-text',
-                '#children': [
-                  {
-                    '#text': meta.label + ' ' + num
-                  }
-                ]
-              }
-            ]
-          };
+          var fieldset = _.cloneDeep(editor.meta.elements.fieldset.element);
+          fieldset['af-fieldset'] = type + num;
+          fieldset['#children'][0]['#children'][0]['#text'] = meta.label + ' ' + num;
           // Add default contact name block
           if (meta.entity === 'Contact') {
             fieldset['#children'].push({'#tag': 'afblock-name-' + type.toLowerCase()});
           return $scope.selectedEntityName;
         };
 
+        // Validates that a drag-n-drop action is allowed
+        this.onDrop = function(event, ui) {
+          var sort = ui.item.sortable;
+          // Check if this is a callback for an item dropped into a different container
+          // @see https://github.com/angular-ui/ui-sortable notes on canceling
+          if (!sort.received && sort.source[0] !== sort.droptarget[0]) {
+            var $source = $(sort.source[0]),
+              $target = $(sort.droptarget[0]),
+              $item = $(ui.item[0]);
+            // Fields cannot be dropped outside their own entity
+            if ($item.is('[af-gui-field]') || $item.has('[af-gui-field]').length) {
+              if ($source.closest('[data-entity]').attr('data-entity') !== $target.closest('[data-entity]').attr('data-entity')) {
+                return sort.cancel();
+              }
+            }
+            // Entity-fieldsets cannot be dropped into other entity-fieldsets
+            if ((sort.model['af-fieldset'] || $item.has('.af-gui-fieldset').length) && $target.closest('.af-gui-fieldset').length) {
+              return sort.cancel();
+            }
+          }
+        };
+
         $scope.addEntity = function(entityType) {
           var entityName = editor.addEntity(entityType);
           editor.selectEntity(entityName);
           $scope.elementTitles.length = 0;
           _.each($scope.editor.meta.elements, function(element, name) {
             if (!search || _.contains(name, search) || _.contains(element.title.toLowerCase(), search)) {
-              $scope.elementList.push(_.cloneDeep(element.element));
-              $scope.elementTitles.push(element.title);
+              var node = _.cloneDeep(element.element);
+              if (name === 'fieldset') {
+                node['af-fieldset'] = $scope.entity.name;
+              }
+              $scope.elementList.push(node);
+              $scope.elementTitles.push(name === 'fieldset' ? ts('Fieldset for %1', {1: $scope.entity.label}) : element.title);
             }
           });
         }
           }
         };
 
-        // Validates that a drag-n-drop action is allowed
-        $scope.onDrop = function(event, ui) {
-          var sort = ui.item.sortable;
-          // Check if this is a callback for an item dropped into a different container
-          // @see https://github.com/angular-ui/ui-sortable notes on canceling
-          if (!sort.received && sort.source[0] !== sort.droptarget[0]) {
-            var $source = $(sort.source[0]),
-              $target = $(sort.droptarget[0]),
-              $item = $(ui.item[0]);
-            // Dropping onto palette is ok; works like a trash can
-            if ($target.closest('#afGuiEditor-palette-config').length) {
-              return;
-            }
-            // Fields cannot be dropped outside their own entity
-            if ($item.is('[af-gui-field]') || $item.has('[af-gui-field]').length) {
-              if ($source.closest('[data-entity]').attr('data-entity') !== $target.closest('[data-entity]').attr('data-entity')) {
-                return sort.cancel();
-              }
-            }
-            // Entity-fieldsets cannot be dropped into other entity-fieldsets
-            if (($item.hasClass('af-gui-fieldset') || $item.has('.af-gui-fieldset').length) && $target.closest('.af-gui-fieldset').length) {
-              return sort.cancel();
-            }
-          }
-        };
-
         $scope.tags = {
           div: ts('Container'),
           fieldset: ts('Fieldset')
index 10e3950b050436998307721d1b87655f87dc35da..fc2553fb95858f579b94ecd1e3c49c81f1cb2be4 100644 (file)
@@ -12,7 +12,7 @@
       <div>{{ ts('Form Layout') }}</div>
     </form>
   </div>
-  <div class="panel-body">
+  <div id="afGuiEditor-canvas-body" class="panel-body">
     <div ng-if="layout" af-gui-container="layout" entity-name="" />
   </div>
 </div>
index 0f71f098d396049637d29fe19261f8798efca810..31c30b4715e34db4dcf309bf571798003b0522cd 100644 (file)
@@ -22,7 +22,7 @@
     <ul class="dropdown-menu" ng-if="menu.open" ng-include="'~/afGuiEditor/canvas-menu.html'"></ul>
   </div>
 </div>
-<div ui-sortable="{handle: '.af-gui-bar', update: onDrop, connectWith: '[ui-sortable]', cancel: 'input,textarea,button,select,option,a,.dropdown-menu'}" ng-model="getSetChildren" ng-model-options="{getterSetter: true}" class="af-gui-layout {{ getLayout() }}">
+<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'] }}" />
index a6c9b26aa83cc3a04afff9bc778abe4000fc6f7a..f709cff6bdc362da23c5eb5dbd6dc259fb3831b9 100644 (file)
@@ -22,7 +22,7 @@
     <div class="af-gui-entity-palette-select-list">
       <div ng-if="blockList.length">
         <label>{{ ts('Blocks') }}</label>
-        <div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[data-entity=' + entity.name + '] &gt; [ui-sortable]'}" ng-model="blockList">
+        <div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[data-entity=' + entity.name + '] &gt; [ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="editor.onDrop" ng-model="blockList">
           <div ng-repeat="block in blockList" ng-class="{disabled: blockInUse(block)}">
             {{ blockTitles[$index] }}
           </div>
@@ -30,7 +30,7 @@
       </div>
       <div ng-if="elementList.length">
         <label>{{ ts('Elements') }}</label>
-        <div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[ui-sortable]'}" ng-model="elementList">
+        <div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="editor.onDrop" ng-model="elementList">
           <div ng-repeat="element in elementList" >
             {{ elementTitles[$index] }}
           </div>
@@ -39,7 +39,7 @@
       <div ng-repeat="fieldGroup in fieldList">
         <div ng-if="fieldGroup.fields.length">
           <label>{{ fieldGroup.label }}</label>
-          <div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[data-entity=' + fieldGroup.entityName + '] &gt; [ui-sortable]'}" ng-model="fieldGroup.fields">
+          <div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[data-entity=' + fieldGroup.entityName + '] &gt; [ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="editor.onDrop" ng-model="fieldGroup.fields">
             <div ng-repeat="field in fieldGroup.fields" ng-class="{disabled: fieldInUse(field.name)}">
               {{ editor.getField(fieldGroup.entityType, field.name).title }}
             </div>