From 192695ae61597fb2b59327d9e8f1022ab37dc19b Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 24 Nov 2019 19:52:40 -0500 Subject: [PATCH] GUI: Add wysiwyg markup block --- ext/afform/core/CRM/Afform/ArrayHtml.php | 29 ++- ext/afform/gui/ang/afGuiEditor.css | 47 +++- ext/afform/gui/ang/afGuiEditor.js | 242 ++++++++++++------ .../gui/ang/afGuiEditor/block-menu.html | 31 +-- ext/afform/gui/ang/afGuiEditor/block.html | 2 + .../gui/ang/afGuiEditor/editOptions.html | 2 +- ext/afform/gui/ang/afGuiEditor/field.html | 2 +- .../gui/ang/afGuiEditor/markup-menu.html | 10 + ext/afform/gui/ang/afGuiEditor/markup.html | 30 +++ .../ang/afGuiEditor/menu-item-background.html | 7 + .../gui/ang/afGuiEditor/menu-item-border.html | 17 ++ 11 files changed, 290 insertions(+), 129 deletions(-) create mode 100644 ext/afform/gui/ang/afGuiEditor/markup-menu.html create mode 100644 ext/afform/gui/ang/afGuiEditor/markup.html create mode 100644 ext/afform/gui/ang/afGuiEditor/menu-item-background.html create mode 100644 ext/afform/gui/ang/afGuiEditor/menu-item-border.html diff --git a/ext/afform/core/CRM/Afform/ArrayHtml.php b/ext/afform/core/CRM/Afform/ArrayHtml.php index a8f55b2273..4705ea2731 100644 --- a/ext/afform/core/CRM/Afform/ArrayHtml.php +++ b/ext/afform/core/CRM/Afform/ArrayHtml.php @@ -115,6 +115,10 @@ class CRM_Afform_ArrayHtml { } } + if (isset($array['#markup'])) { + return $buf . '>' . $array['#markup'] . ''; + } + if (empty($children) && $this->isSelfClosing($tag)) { $buf .= ' />'; } @@ -175,12 +179,20 @@ class CRM_Afform_ArrayHtml { $arr = ['#tag' => $node->tagName]; foreach ($node->attributes as $attribute) { $txt = $attribute->textContent; - $type = $this->pickAttrType($node->tagName, $attribute->name); $arr[$attribute->name] = $this->decodeAttrValue($type, $txt); } if ($node->childNodes->length > 0) { - $arr['#children'] = $this->convertNodesToArray($node->childNodes); + // In shallow mode, return "af-markup" blocks as-is + if (!$this->deepCoding && !empty($arr['class']) && strpos($arr['class'], 'af-markup') !== FALSE) { + $arr['#markup'] = ''; + foreach ($node->childNodes as $child) { + $arr['#markup'] .= $child->ownerDocument->saveXML($child); + } + } + else { + $arr['#children'] = $this->convertNodesToArray($node->childNodes); + } } return $arr; } @@ -188,8 +200,7 @@ class CRM_Afform_ArrayHtml { return ['#text' => $node->textContent]; } elseif ($node instanceof DOMComment) { - $arr = ['#comment' => $node->nodeValue]; - return $arr; + return ['#comment' => $node->nodeValue]; } else { throw new \RuntimeException("Unrecognized DOM node"); @@ -235,15 +246,7 @@ class CRM_Afform_ArrayHtml { return 'text'; } - if (isset($this->protoSchema[$tag][$attrName])) { - return $this->protoSchema[$tag][$attrName]; - } - - if (isset($this->protoSchema['*'][$attrName])) { - return $this->protoSchema['*'][$attrName]; - } - - return $this->protoSchema['*']['*']; + return $this->protoSchema[$tag][$attrName] ?? $this->protoSchema['*'][$attrName] ?? $this->protoSchema['*']['*']; } /** diff --git a/ext/afform/gui/ang/afGuiEditor.css b/ext/afform/gui/ang/afGuiEditor.css index e13a9e16be..8517b3c39a 100644 --- a/ext/afform/gui/ang/afGuiEditor.css +++ b/ext/afform/gui/ang/afGuiEditor.css @@ -138,6 +138,29 @@ border: 2px dashed #757575; } +#afGuiEditor .af-gui-markup { + padding: 22px 3px 3px; + position: relative; +} + +#afGuiEditor div.af-gui-markup-content { + display: block; + position: relative; + padding: 0 !important; +} + +#afGuiEditor .af-gui-markup-content-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +#afGuiEditor .af-gui-markup-content:hover .af-gui-markup-content-overlay { + background-color: rgba(255, 255, 255, .2); +} + #afGuiEditor #afGuiEditor-canvas .af-entity-selected { border: 2px dashed #0071bd; } @@ -366,34 +389,38 @@ font-style: italic; } -#afGuiEditor.af-gui-editing-options { +#afGuiEditor.af-gui-editing-content { 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 > * { +#afGuiEditor.af-gui-editing-content .panel-heading, +#afGuiEditor.af-gui-editing-content .af-gui-element, +#afGuiEditor.af-gui-editing-content .af-gui-markup-content, +.af-gui-editing-content #afGuiEditor-palette .panel-body > * { opacity: .5; } -#afGuiEditor.af-gui-editing-options .af-gui-block { +#afGuiEditor.af-gui-editing-content .af-gui-block { border: 2px solid transparent; } -#afGuiEditor.af-gui-editing-options .af-gui-bar:not(.af-gui-edit-options-bar) { +#afGuiEditor.af-gui-editing-content .af-gui-bar { visibility: hidden; } -#afGuiEditor.af-gui-editing-options .af-gui-bar:before { +#afGuiEditor.af-gui-editing-content .af-gui-bar:before { background: none; } -#afGuiEditor [af-gui-edit-options] { - border: 2px solid #0071bd; +#afGuiEditor .af-gui-content-editing-area { pointer-events: auto; cursor: auto; padding-top: 35px; position: relative; } -#afGuiEditor [af-gui-edit-options] .af-gui-edit-options-bar { +#afGuiEditor [af-gui-edit-options] { + border: 2px solid #0071bd; +} + +#afGuiEditor .af-gui-content-editing-area .af-gui-edit-options-bar { height: 30px; font-family: "Courier New", Courier, monospace; font-size: 12px; diff --git a/ext/afform/gui/ang/afGuiEditor.js b/ext/afform/gui/ang/afGuiEditor.js index 16fe6b949c..2db100106c 100644 --- a/ext/afform/gui/ang/afGuiEditor.js +++ b/ext/afform/gui/ang/afGuiEditor.js @@ -337,17 +337,6 @@ var ts = $scope.ts = CRM.ts(); 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; @@ -368,6 +357,9 @@ if (_.contains(classes, 'af-button')) { return 'button'; } + if (_.contains(classes, 'af-markup')) { + return 'markup'; + } return null; }; @@ -376,8 +368,12 @@ var newBlock = _.defaults({ '#tag': classes.shift(), 'class': classes.join(' '), - '#children': classes[0] === 'af-block' ? [] : [{'#text': ts('Enter text')}] }, props); + if (classes[0] === 'af-block') { + newBlock['#children'] = []; + } else if (classes[0] === 'af-text' || classes[0] === 'af-button') { + newBlock['#children'] = [{'#text': ts('Enter text')}]; + } $scope.node['#children'].push(newBlock); }; @@ -440,66 +436,9 @@ if (val !== 'af-layout-rows') { classes.push(val); } - block.modifyClasses($scope.node, _.keys($scope.layouts), classes); - }; - - $scope.getSetBorderWidth = function(width) { - return getSetBorderProp(0, arguments.length ? width : null); - }; - - $scope.getSetBorderStyle = function(style) { - return getSetBorderProp(1, arguments.length ? style : null); - }; - - $scope.getSetBorderColor = function(color) { - return getSetBorderProp(2, arguments.length ? color : null); - }; - - $scope.getSetBackgroundColor = function(color) { - if (!arguments.length) { - return block.getStyles($scope.node)['background-color'] || '#ffffff'; - } - block.setStyle($scope.node, 'background-color', color); - }; - - function getSetBorderProp(idx, val) { - var border = getBorder() || ['1px', '', '#000000']; - if (val === null) { - return border[idx]; - } - border[idx] = val; - block.setStyle($scope.node, 'border', val ? border.join(' ') : null); - } - - this.getStyles = function(node) { - return !node || !node.style ? {} : _.transform(node.style.split(';'), function(styles, style) { - var keyVal = _.map(style.split(':'), _.trim); - if (keyVal.length > 1 && keyVal[1].length) { - styles[keyVal[0]] = keyVal[1]; - } - }, {}); - }; - - this.setStyle = function(node, name, val) { - var styles = block.getStyles(node); - styles[name] = val; - if (!val) { - delete styles[name]; - } - if (_.isEmpty(styles)) { - delete node.style; - } else { - node.style = _.transform(styles, function(combined, val, name) { - combined.push(name + ': ' + val); - }, []).join('; '); - } + modifyClasses($scope.node, _.keys($scope.layouts), classes); }; - function getBorder() { - var border = _.map((block.getStyles($scope.node).border || '').split(' '), _.trim); - return border.length > 2 ? border : null; - } - } }; }); @@ -551,7 +490,7 @@ $scope.editOptions = function() { $scope.editingOptions = true; - $('#afGuiEditor').addClass('af-gui-editing-options'); + $('#afGuiEditor').addClass('af-gui-editing-content'); }; $scope.inputTypeCanBe = function(type) { @@ -674,7 +613,7 @@ $scope.close = function() { $scope.field.setEditingOptions(false); - $('#afGuiEditor').removeClass('af-gui-editing-options'); + $('#afGuiEditor').removeClass('af-gui-editing-content'); }; } }; @@ -717,7 +656,7 @@ }; $scope.setAlign = function(val) { - $scope.block.modifyClasses($scope.node, _.keys($scope.alignments), val === 'text-left' ? null : val); + modifyClasses($scope.node, _.keys($scope.alignments), val === 'text-left' ? null : val); }; $scope.styles = _.transform(CRM.afformAdminData.styles, function(styles, val, key) { @@ -727,7 +666,7 @@ // Getter/setter for ng-model $scope.getSetStyle = function(val) { if (arguments.length) { - return $scope.block.modifyClasses($scope.node, _.keys($scope.styles), val === 'text-default' ? null : val); + return modifyClasses($scope.node, _.keys($scope.styles), val === 'text-default' ? null : val); } return _.intersection(splitClass($scope.node['class']), _.keys($scope.styles))[0] || 'text-default'; }; @@ -735,7 +674,62 @@ } }; }); - + + var richtextId = 0; + angular.module('afGuiEditor').directive('afGuiMarkup', function($sce, $timeout) { + return { + restrict: 'A', + templateUrl: '~/afGuiEditor/markup.html', + scope: { + node: '=afGuiMarkup' + }, + require: '^^afGuiBlock', + link: function($scope, element, attrs, block) { + $scope.block = block; + // CRM.wysiwyg doesn't work without a dom id + $scope.id = 'af-markup-editor-' + richtextId++; + + // When creating a new markup block, 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'] || '

'); + }; + + $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 block was canceled, just remove it + if ($scope.node['#markup'] === false) { + $scope.block.removeBlock($scope.node); + } else { + $scope.editingMarkup = false; + } + }; + } + }; + }); + + angular.module('afGuiEditor').directive('afGuiButton', function() { return { restrict: 'A', @@ -762,7 +756,7 @@ // Getter/setter for ng-model $scope.getSetStyle = function(val) { if (arguments.length) { - return $scope.block.modifyClasses($scope.node, _.keys($scope.styles), ['btn', val]); + return modifyClasses($scope.node, _.keys($scope.styles), ['btn', val]); } return _.intersection(splitClass($scope.node['class']), _.keys($scope.styles))[0] || ''; }; @@ -799,6 +793,67 @@ }; }); + // Menu item to control the border property of a node + angular.module('afGuiEditor').directive('afGuiMenuItemBorder', function() { + return { + restrict: 'A', + templateUrl: '~/afGuiEditor/menu-item-border.html', + scope: { + node: '=afGuiMenuItemBorder' + }, + link: function($scope, element, attrs) { + var ts = $scope.ts = CRM.ts(); + + $scope.getSetBorderWidth = function(width) { + return getSetBorderProp($scope.node, 0, arguments.length ? width : null); + }; + + $scope.getSetBorderStyle = function(style) { + return getSetBorderProp($scope.node, 1, arguments.length ? style : null); + }; + + $scope.getSetBorderColor = function(color) { + return getSetBorderProp($scope.node, 2, arguments.length ? color : null); + }; + + function getSetBorderProp(node, idx, val) { + var border = getBorder(node) || ['1px', '', '#000000']; + if (val === null) { + return border[idx]; + } + border[idx] = val; + setStyle(node, 'border', val ? border.join(' ') : null); + } + + function getBorder(node) { + var border = _.map((getStyles(node).border || '').split(' '), _.trim); + return border.length > 2 ? border : null; + } + } + }; + }); + + // Menu item to control the background property of a node + angular.module('afGuiEditor').directive('afGuiMenuItemBackground', function() { + return { + restrict: 'A', + templateUrl: '~/afGuiEditor/menu-item-background.html', + scope: { + node: '=afGuiMenuItemBackground' + }, + link: function($scope, element, attrs) { + var ts = $scope.ts = CRM.ts(); + + $scope.getSetBackgroundColor = function(color) { + if (!arguments.length) { + return getStyles($scope.node)['background-color'] || '#ffffff'; + } + setStyle($scope.node, 'background-color', color); + }; + } + }; + }); + // Editable titles using ngModel & html5 contenteditable // Cribbed from ContactLayoutEditor angular.module('afGuiEditor').directive("afGuiEditable", function() { @@ -937,4 +992,39 @@ }; }); + function getStyles(node) { + return !node || !node.style ? {} : _.transform(node.style.split(';'), function(styles, style) { + var keyVal = _.map(style.split(':'), _.trim); + if (keyVal.length > 1 && keyVal[1].length) { + styles[keyVal[0]] = keyVal[1]; + } + }, {}); + } + + function setStyle(node, name, val) { + var styles = getStyles(node); + styles[name] = val; + if (!val) { + delete styles[name]; + } + if (_.isEmpty(styles)) { + delete node.style; + } else { + node.style = _.transform(styles, function(combined, val, name) { + combined.push(name + ': ' + val); + }, []).join('; '); + } + } + + function modifyClasses(node, toRemove, toAdd) { + var classes = splitClass(node['class']); + if (toRemove) { + classes = _.difference(classes, splitClass(toRemove)); + } + if (toAdd) { + classes = _.unique(classes.concat(splitClass(toAdd))); + } + node['class'] = classes.join(' '); + } + })(angular, CRM.$, CRM._); diff --git a/ext/afform/gui/ang/afGuiEditor/block-menu.html b/ext/afform/gui/ang/afGuiEditor/block-menu.html index 4a01d9ee92..29d82ca653 100644 --- a/ext/afform/gui/ang/afGuiEditor/block-menu.html +++ b/ext/afform/gui/ang/afGuiEditor/block-menu.html @@ -1,34 +1,9 @@
  • {{ ts('Add block') }}
  • {{ ts('Add text box') }}
  • +
  • {{ ts('Add rich content') }}
  • {{ ts('Add button') }}
  • -
  • -
    - - - -
    -
  • -
  • -
    - - - - - -
    -
  • +
  • +
  • {{ ts('Delete this block') }}
  • diff --git a/ext/afform/gui/ang/afGuiEditor/block.html b/ext/afform/gui/ang/afGuiEditor/block.html index 1c7df59e4f..703147083a 100644 --- a/ext/afform/gui/ang/afGuiEditor/block.html +++ b/ext/afform/gui/ang/afGuiEditor/block.html @@ -24,6 +24,7 @@