From: Coleman Watts Date: Fri, 18 Mar 2022 13:42:49 +0000 (-0400) Subject: Afform - add collapsible title as directive X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=be125358b0a81655a190b6c390c13d3b1f0f8cf2;p=civicrm-core.git Afform - add collapsible title as directive Before: A fieldset `` was treated as its own element. This was more flexible but more complex. After: Augenerated ` for fieldsets or `

` for other containers based on new `af-title` directive. This allows central control of titles for e.g. collapsible styles. Fixes dev/core#3110 --- diff --git a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php index 7038b42321..8ca5ec909c 100644 --- a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php +++ b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php @@ -249,17 +249,8 @@ class AfformAdminMeta { 'element' => [ '#tag' => 'fieldset', 'af-fieldset' => NULL, - '#children' => [ - [ - '#tag' => 'legend', - 'class' => 'af-text', - '#children' => [ - [ - '#text' => E::ts('Enter title'), - ], - ], - ], - ], + 'af-title' => E::ts('Enter title'), + '#children' => [], ], ], ]; diff --git a/ext/afform/admin/ang/afGuiEditor.css b/ext/afform/admin/ang/afGuiEditor.css index 35ff7e113d..3b60112dd1 100644 --- a/ext/afform/admin/ang/afGuiEditor.css +++ b/ext/afform/admin/ang/afGuiEditor.css @@ -142,7 +142,8 @@ font-family: "Courier New", Courier, monospace; font-size: 12px; } -#afGuiEditor-canvas:not(.af-gui-menu-open) .af-gui-bar { +#afGuiEditor-canvas:not(.af-gui-menu-open) .af-gui-bar, +#afGuiEditor-canvas:not(.af-gui-menu-open) .af-gui-container-title span:empty { opacity: 0; } #afGuiEditor-canvas [ui-sortable] .af-gui-bar { @@ -153,11 +154,13 @@ left: 0; padding-left: 15px; } -#afGuiEditor:not(.af-gui-dragging *) #afGuiEditor-canvas:hover .af-gui-bar { +#afGuiEditor:not(.af-gui-dragging *) #afGuiEditor-canvas:hover .af-gui-bar, +#afGuiEditor:not(.af-gui-dragging *) #afGuiEditor-canvas:hover .af-gui-container-title span:empty { opacity: 1; transition: opacity .2s; } -#afGuiEditor #afGuiEditor-canvas .af-gui-dragtarget > .af-gui-bar { +#afGuiEditor #afGuiEditor-canvas .af-gui-dragtarget > .af-gui-bar, +#afGuiEditor #afGuiEditor-canvas .af-gui-dragtarget > .af-gui-container-title span:empty { background-color: #d7e6ff; opacity: 1; transition: opacity .1s; @@ -263,7 +266,8 @@ body.af-gui-dragging { } /* Fix button colors when bar is highlighted */ #afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > .btn-group > .btn-group > button > span, -#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > span { +#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > span, +#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-node-title { color: white; } #afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > .btn-group > .btn-group > button:hover > span, @@ -381,6 +385,12 @@ body.af-gui-dragging { margin-right: 20px; position: relative; } +#afGuiEditor .af-gui-container-title { + top: -21px; +} +#afGuiEditor .af-gui-container-title span:empty { + font-weight: lighter; +} #afGuiEditor .af-gui-field-required:after { content: '*'; diff --git a/ext/afform/admin/ang/afGuiEditor.js b/ext/afform/admin/ang/afGuiEditor.js index 60391af1f2..13ec8bc1ed 100644 --- a/ext/afform/admin/ang/afGuiEditor.js +++ b/ext/afform/admin/ang/afGuiEditor.js @@ -56,6 +56,16 @@ return str ? _.unique(_.trim(str).split(/\s+/g)) : []; } + // Check if a node has class(es) + function hasClass(node, className) { + if (!node['class']) { + return false; + } + var classes = splitClass(node['class']), + classNames = className.split(' '); + return _.intersection(classes, classNames).length === classNames.length; + } + function modifyClasses(node, toRemove, toAdd) { var classes = splitClass(node['class']); if (toRemove) { @@ -64,7 +74,11 @@ if (toAdd) { classes = _.unique(classes.concat(splitClass(toAdd))); } - node['class'] = classes.join(' '); + if (classes.length) { + node['class'] = classes.join(' '); + } else if ('class' in node) { + delete node['class']; + } } return { @@ -202,6 +216,7 @@ }, splitClass: splitClass, + hasClass: hasClass, modifyClasses: modifyClasses, getStyles: getStyles, setStyle: setStyle, diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js index 5aa0a44944..a7b93544d8 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js @@ -135,7 +135,7 @@ // Create a new af-fieldset container for the entity var fieldset = _.cloneDeep(afGui.meta.elements.fieldset.element); fieldset['af-fieldset'] = type + num; - fieldset['#children'][0]['#children'][0]['#text'] = meta.label + ' ' + num; + fieldset['af-title'] = meta.label + ' ' + num; // Add boilerplate contents _.each(meta.boilerplate, function (tag) { fieldset['#children'].push(tag); @@ -274,6 +274,7 @@ var fieldset = { '#tag': 'div', 'af-fieldset': '', + 'af-title': display.label, '#children': [ { '#tag': display.tag, diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiMenuItemCollapsible.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiMenuItemCollapsible.component.js new file mode 100644 index 0000000000..0beec12e19 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/afGuiMenuItemCollapsible.component.js @@ -0,0 +1,43 @@ +// https://civicrm.org/licensing +(function(angular, $, _) { + "use strict"; + + // Menu item to control the border property of a node + angular.module('afGuiEditor').component('afGuiMenuItemCollapsible', { + templateUrl: '~/afGuiEditor/afGuiMenuItemCollapsible.html', + bindings: { + node: '=' + }, + controller: function($scope, afGui) { + var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'), + ctrl = this; + + this.isCollapsible = function() { + return afGui.hasClass(ctrl.node, 'af-collapsible'); + }; + + this.isCollapsed = function() { + return afGui.hasClass(ctrl.node, 'af-collapsible af-collapsed'); + }; + + this.toggleCollapsible = function() { + // Node must have a title to be collapsible + if (ctrl.isCollapsible() || !ctrl.node['af-title']) { + afGui.modifyClasses(ctrl.node, 'af-collapsible af-collapsed'); + } else { + afGui.modifyClasses(ctrl.node, null, 'af-collapsible'); + } + }; + + this.toggleCollapsed = function() { + if (ctrl.isCollapsed()) { + afGui.modifyClasses(ctrl.node, 'af-collapsed'); + } else { + afGui.modifyClasses(ctrl.node, null, 'af-collapsed'); + } + }; + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiMenuItemCollapsible.html b/ext/afform/admin/ang/afGuiEditor/afGuiMenuItemCollapsible.html new file mode 100644 index 0000000000..da105691c9 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/afGuiMenuItemCollapsible.html @@ -0,0 +1,8 @@ + + + + {{ $ctrl.isCollapsed() ? ts('Closed') : ts('Open') }} + diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html index 77d2e5ceae..9afec26ebd 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer-menu.html @@ -30,6 +30,7 @@ +
  • diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js index 1c4a724263..4942691352 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js @@ -122,6 +122,12 @@ } }; + this.getCollapsibleIcon = function() { + if (afGui.hasClass(ctrl.node, 'af-collapsible')) { + return afGui.hasClass(ctrl.node, 'af-collapsed') ? 'fa-caret-right' : 'fa-caret-down'; + } + }; + // Sets min value for af-repeat as a string, returns it as an int $scope.getSetMin = function(val) { if (arguments.length) { @@ -332,6 +338,33 @@ return type.length ? type[0].replace('af-', '') : null; }; + this.getSetTitle = function(value) { + if (arguments.length) { + if (value.length) { + ctrl.node['af-title'] = value; + } else { + delete ctrl.node['af-title']; + // With no title, cannot be collapsible + afGui.modifyClasses(ctrl.node, 'af-collapsible af-collapsed'); + } + } + return ctrl.node['af-title']; + }; + + this.getToolTip = function() { + var text = '', nodeType; + if (!$scope.block) { + nodeType = ctrl.getNodeType(ctrl.node); + if (nodeType === 'fieldset') { + text = ctrl.editor.getEntity(ctrl.entityName).label; + } else if (nodeType === 'searchFieldset') { + text = ts('Search Display'); + } + text += ' ' + $scope.tags[ctrl.node['#tag']]; + } + return text; + }; + this.removeElement = function(element) { afGui.removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey}); ctrl.editor.onRemoveElement(); diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html index 302043892f..6e3fe030db 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html @@ -1,15 +1,12 @@ -
    -
    - {{ $ctrl.editor.getEntity($ctrl.entityName).label }} - {{:: ts('Search Display') }} +
    +
    {{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }} - {{ tags[$ctrl.node['#tag']] }} - - -
    + +
    +
    diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchContainer-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchContainer-menu.html index 84db1151f5..ac3b8b7c30 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchContainer-menu.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiSearchContainer-menu.html @@ -6,6 +6,7 @@
    +
  • diff --git a/ext/afform/core/ang/af/afTitle.directive.js b/ext/afform/core/ang/af/afTitle.directive.js new file mode 100644 index 0000000000..b74a2abe72 --- /dev/null +++ b/ext/afform/core/ang/af/afTitle.directive.js @@ -0,0 +1,28 @@ +(function(angular, $, _) { + "use strict"; + angular.module('af').directive('afTitle', function() { + return { + restrict: 'A', + bindToController: { + title: '@afTitle' + }, + controller: function($scope, $element) { + var ctrl = this; + + $scope.$watch(function() {return ctrl.title;}, function(text) { + var tag = $element.is('fieldset') ? 'legend' : 'h4', + $title = $element.children(tag + '.af-title'); + if (!$title.length) { + $title = $('<' + tag + ' class="af-title" />').prependTo($element); + if ($element.hasClass('af-collapsible')) { + $title.click(function() { + $element.toggleClass('af-collapsed'); + }); + } + } + $title.text(text); + }); + } + }; + }); +})(angular, CRM.$, CRM._); diff --git a/ext/afform/core/ang/afCore.css b/ext/afform/core/ang/afCore.css index 554758de1f..edc6012437 100644 --- a/ext/afform/core/ang/afCore.css +++ b/ext/afform/core/ang/afCore.css @@ -30,3 +30,20 @@ af-form { top: 0; right: 0; } + +/* Collapsible containers */ +.af-collapsible > .af-title { + cursor: pointer; +} +.af-collapsible > .af-title:before { + font-family: "FontAwesome"; + display: inline-block; + width: 1em; + content: "\f0d7"; +} +.af-collapsible.af-collapsed > .af-title:before { + content: "\f0da"; +} +.af-collapsible.af-collapsed > .af-title + * { + display: none; +}