From 2a3c0358b1c68fe2f3045acbaffa3a2af4a4dad7 Mon Sep 17 00:00:00 2001
From: jomehmet <jomehmet@gmail.com>
Date: Wed, 22 Feb 2023 15:21:53 +0100
Subject: [PATCH] Add support for Inline Macro- To enable: Add a Macro Parameter
 called: enableInlineMacro with Umbraco.TrueFalse set to True (value:"1")
 (#13628)

- Custom Macro Parameter with true as default value can be created for those prefering that in their project

Co-authored-by: Jo Mehmet Sollihagen <jomehmet@solsol.no>
---
 .../Macros/MacroTagParser.cs                  | 11 ++++--
 .../src/common/services/tinymce.service.js    | 38 +++++++++++--------
 .../src/less/rte-content.less                 |  6 ++-
 .../Macros/MacroParserTests.cs                | 21 ++++++++++
 4 files changed, 57 insertions(+), 19 deletions(-)

diff --git a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs
index 07109729b6..08559f786a 100644
--- a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs
+++ b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs
@@ -40,10 +40,15 @@ public class MacroTagParser
         {
             if (match.Groups.Count >= 3)
             {
+                var macroCanBeInlinedInParagraph = match.Value?.Contains("enableInlineMacro=\"1\"") ?? false;
+                var macroElementType = macroCanBeInlinedInParagraph ? "span" : "div";
                 // <div class="umb-macro-holder myMacro mceNonEditable">
                 var alias = match.Groups[2].Value;
-                var sb = new StringBuilder("<div class=\"umb-macro-holder ");
-
+                var sb = new StringBuilder($"<{macroElementType} class=\"umb-macro-holder ");
+                if (macroCanBeInlinedInParagraph)
+                {
+                    sb.Append("inlined-macro ");
+                }
                 // sb.Append(alias.ToSafeAlias());
                 sb.Append("mceNonEditable\"");
                 foreach (KeyValuePair<string, string> htmlAttribute in htmlAttributes)
@@ -64,7 +69,7 @@ public class MacroTagParser
                 sb.Append("Macro alias: ");
                 sb.Append("<strong>");
                 sb.Append(alias);
-                sb.Append("</strong></ins></div>");
+                sb.Append($"</strong></ins></{macroElementType}>");
                 return sb.ToString();
             }
 
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
index ea284ee2ce..e23e712046 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
@@ -766,14 +766,20 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
         }
 
         var e = $(element).closest(".umb-macro-holder");
+
         if (e.length > 0) {
-          if (e.get(0).parentNode.nodeName === "P") {
+          var macroHolder = e.get(0);
+          // In case of Inline Macro we don't need the be backward compliant
+          if(macroHolder.tagName === 'SPAN'){
+            return macroHolder;
+          }
+          if (macroHolder.parentNode.nodeName === "P") {
             //now check if we're the only element
             if (element.parentNode.childNodes.length === 1) {
-              return e.get(0).parentNode;
+              return macroHolder.parentNode;
             }
           }
-          return e.get(0);
+          return macroHolder;
         }
         return null;
       }
@@ -830,35 +836,37 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
       var macroSyntaxComment = "<!-- " + macroObject.syntax + " -->";
       //create an id class for this element so we can re-select it after inserting
       var uniqueId = "umb-macro-" + editor.dom.uniqueId();
-      var macroDiv = editor.dom.create('div',
+      var isInlined = macroObject.macroParamsDictionary["enableInlineMacro"] === "1";
+      var macroElementType = isInlined ? 'span' : 'div';
+      var macroElement = editor.dom.create(macroElementType,
         {
-          'class': 'umb-macro-holder ' + macroObject.macroAlias + " " + uniqueId + ' mceNonEditable',
+          'class': 'umb-macro-holder ' + macroObject.macroAlias + " " + uniqueId + ' mceNonEditable' + (isInlined ? ' inlined-macro' : ''),
           'contenteditable': 'false'
         },
         macroSyntaxComment + '<ins>Macro alias: <strong>' + macroObject.macroAlias + '</strong></ins>');
 
       //if there's an activeMacroElement then replace it, otherwise set the contents of the selected node
       if (activeMacroElement) {
-        activeMacroElement.replaceWith(macroDiv); //directly replaces the html node
+        activeMacroElement.replaceWith(macroElement); //directly replaces the html node
       }
       else {
-        editor.selection.setNode(macroDiv);
+        editor.selection.setNode(macroElement);
       }
 
-      var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId));
+      var $macroElement = $(editor.dom.select(".umb-macro-holder." + uniqueId));
       editor.setDirty(true);
 
       //async load the macro content
-      this.loadMacroContent($macroDiv, macroObject, editor);
+      this.loadMacroContent($macroElement, macroObject, editor);
 
     },
 
     /** loads in the macro content async from the server */
-    loadMacroContent: function ($macroDiv, macroData, editor) {
+    loadMacroContent: function ($macroElement, macroData, editor) {
 
       //if we don't have the macroData, then we'll need to parse it from the macro div
       if (!macroData) {
-        var contents = $macroDiv.contents();
+        var contents = $macroElement.contents();
         var comment = _.find(contents, function (item) {
           return item.nodeType === 8;
         });
@@ -870,15 +878,15 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
         macroData = parsed;
       }
 
-      var $ins = $macroDiv.find("ins");
+      var $ins = $macroElement.find("ins");
 
       //show the throbber
-      $macroDiv.addClass("loading");
+      $macroElement.addClass("loading");
 
       // Add the contenteditable="false" attribute
       // As just the CSS class of .mceNonEditable is not working by itself?!
       // TODO: At later date - use TinyMCE editor DOM manipulation as opposed to jQuery
-      $macroDiv.attr("contenteditable", "false");
+      $macroElement.attr("contenteditable", "false");
 
       var contentId = $routeParams.id;
 
@@ -887,7 +895,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
         macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary)
           .then(function (htmlResult) {
 
-            $macroDiv.removeClass("loading");
+            $macroElement.removeClass("loading");
             htmlResult = htmlResult.trim();
             if (htmlResult !== "") {
               var wasDirty = editor.isDirty();
diff --git a/src/Umbraco.Web.UI.Client/src/less/rte-content.less b/src/Umbraco.Web.UI.Client/src/less/rte-content.less
index 3fe4e52f92..f19b1edb98 100644
--- a/src/Umbraco.Web.UI.Client/src/less/rte-content.less
+++ b/src/Umbraco.Web.UI.Client/src/less/rte-content.less
@@ -4,9 +4,13 @@
 .mce-content-body .umb-macro-holder {
     border: 3px dotted @pinkLight;
     padding: 7px;
-    display: block;
     margin: 3px;
 }
+.mce-content-body .umb-macro-holder.inlined-macro {
+  border: 1px dotted @pinkLight;
+  padding: 1px;
+  margin: 0px;
+}
 
 
 .umb-rte .mce-content-body .umb-macro-holder.loading {
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Macros/MacroParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Macros/MacroParserTests.cs
index bbfcf2103e..63300eaa1b 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Macros/MacroParserTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Macros/MacroParserTests.cs
@@ -396,4 +396,25 @@ asdfsdf
             @"<?UMBRACO_MACRO macroAlias=""Test"" content=""1089"" textArea=""asdfasdf"" title="""" bool=""0"" number="""" contentType="""" multiContentType="""" multiProperties="""" properties="""" tabs="""" multiTabs="""" />",
             result);
     }
+    
+    [Test]
+    public void Format_RTE_WhenMacroContainsParameter_EnableInlineMacro_WithValue_1_ItShouldBeInASpan()
+    {
+        var content = @"<p>asdfasdf</p>
+<p>asdfsadf</p>
+<?UMBRACO_MACRO macroAlias=""My.Map.isCool eh[boy!]"" test1=""value1"" enableInlineMacro=""1""/>
+<p>asdfasdf</p>";
+        var result = MacroTagParser.FormatRichTextPersistedDataForEditor(
+            content,
+            new Dictionary<string, string> { { "test1", "value1" }, { "enableInlineMacro",  "1"} });
+
+        Assert.AreEqual(
+            @"<p>asdfasdf</p>
+<p>asdfsadf</p>
+<span class=""umb-macro-holder inlined-macro mceNonEditable"" test1=""value1"" enableInlineMacro=""1"">
+<!-- <?UMBRACO_MACRO macroAlias=""My.Map.isCool eh[boy!]"" test1=""value1"" enableInlineMacro=""1"" /> -->
+<ins>Macro alias: <strong>My.Map.isCool eh[boy!]</strong></ins></span>
+<p>asdfasdf</p>".StripNewLines(),
+            result.StripNewLines());
+    }
 }
