From 972d33ccec5457a40292cdf39cc98e1604d4482d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Sep 2013 13:34:29 +1000 Subject: [PATCH] Got macro content loading in editor async updated more of the macro plugin to ensure the macro contents cannot be edited. --- src/Umbraco.Core/Macros/MacroTagParser.cs | 11 +- src/Umbraco.Tests/Macros/MacroParserTests.cs | 4 +- .../lib/tinymce/skins/umbraco/content.min.css | 10 ++ .../src/common/resources/macro.resource.js | 24 ++- .../src/common/services/tinymce.service.js | 78 +++++++--- src/Umbraco.Web.UI.Client/src/less/hacks.less | 1 - src/Umbraco.Web/Editors/MacroController.cs | 67 +++++++++ src/Umbraco.Web/UmbracoContext.cs | 2 + src/Umbraco.Web/UmbracoHelper.cs | 138 +++++++++++------- .../umbraco/macroResultWrapper.aspx.cs | 1 + 10 files changed, 256 insertions(+), 80 deletions(-) diff --git a/src/Umbraco.Core/Macros/MacroTagParser.cs b/src/Umbraco.Core/Macros/MacroTagParser.cs index 0b0949c978..3945454087 100644 --- a/src/Umbraco.Core/Macros/MacroTagParser.cs +++ b/src/Umbraco.Core/Macros/MacroTagParser.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Macros /// internal class MacroTagParser { - private static readonly Regex MacroRteContent = new Regex(@"(
.*?"); sb.Append("Macro alias: "); sb.Append(""); - sb.Append(match.Groups[1].Value); + sb.Append(alias); sb.Append("
"); return sb.ToString(); } diff --git a/src/Umbraco.Tests/Macros/MacroParserTests.cs b/src/Umbraco.Tests/Macros/MacroParserTests.cs index 5204729626..63924ff796 100644 --- a/src/Umbraco.Tests/Macros/MacroParserTests.cs +++ b/src/Umbraco.Tests/Macros/MacroParserTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Macros Assert.AreEqual(@"

asdfasdf

asdfsadf

-
+
Macro alias: Map

asdfasdf

", result); @@ -30,7 +30,7 @@ Macro alias: Map
var content = @"

asdfasdf

-
+
asdfasdf asdfas diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/skins/umbraco/content.min.css b/src/Umbraco.Web.UI.Client/lib/tinymce/skins/umbraco/content.min.css index 22702c1555..1c409a7758 100755 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/skins/umbraco/content.min.css +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/skins/umbraco/content.min.css @@ -83,3 +83,13 @@ td.mce-item-selected, th.mce-item-selected { display:block; margin:3px; } + +/* loader for macro loading in tinymce*/ + .mce-content-body .umb-macro-holder.loading { + background: url(img/loader.gif) right no-repeat; + -moz-background-size: 18px; + -o-background-size: 18px; + -webkit-background-size: 18px; + background-size: 18px; + background-position-x: 99%; + } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js index d678944062..a8c929118e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js @@ -11,7 +11,7 @@ function macroResource($q, $http, umbRequestHelper) { /** * @ngdoc method - * @name umbraco.resources.entityResource#getMacroParameters + * @name umbraco.resources.macroResource#getMacroParameters * @methodOf umbraco.resources.macroResource * * @description @@ -28,6 +28,28 @@ function macroResource($q, $http, umbRequestHelper) { "GetMacroParameters", [{ macroId: macroId }])), 'Failed to retreive macro parameters for macro with id ' + macroId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.macroResource#getMacroResult + * @methodOf umbraco.resources.macroResource + * + * @description + * Gets the result of a macro as html to display in the rich text editor + * + * @param {int} macroId The macro id to get parameters for + * @param {int} pageId The current page id + * + */ + getMacroResultAsHtmlForEditor: function (macroAlias, pageId) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "macroApiBaseUrl", + "GetMacroResultAsHtmlForEditor", + [{ macroAlias: macroAlias }, { pageId: pageId }])), + 'Failed to retreive macro result for macro with alias ' + macroAlias); } }; 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 580fa89a7b..479e912460 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 @@ -6,7 +6,7 @@ * @description * A service containing all logic for all of the Umbraco TinyMCE plugins */ -function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeout) { +function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeout, macroResource) { return { /** @@ -126,8 +126,7 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou * @param {Object} $scope the current controller scope */ createInsertMacro: function (editor, $scope) { - - + /** Adds custom rules for the macro plugin and custom serialization */ editor.on('preInit', function (args) { //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out @@ -141,13 +140,9 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou } } }); + }); - ///** Listens for the editor saving */ - //$scope.on("saving", function() { - - //}); - /** Adds the button instance */ editor.addButton('umbmacro', { icon: 'custom icon-settings-alt', @@ -194,7 +189,7 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou //remove the event listener before re-selecting editor.off('NodeChange', onNodeChanged); - + // move selection to top element to ensure we can't edit this editor.selection.select(macroElement); @@ -221,6 +216,31 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou } + /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */ + editor.on('BeforeExecCommand', function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + + /** This double checks and ensures you can't paste content into the rendered macro */ + editor.on("Paste", function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); //set onNodeChanged event listener editor.on('NodeChange', onNodeChanged); @@ -264,7 +284,8 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou }; //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left) - //supported keys to remove the macro (8-backspace, 46-delete) + //supported keys to remove the macro (8-backspace, 46-delete) + //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element? if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) { //move to next element moveSibling(macroElement, true); @@ -280,13 +301,13 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou moveSibling(macroElement, false); editor.dom.remove(macroElement); } - - return false; - + return ; } }); }, + + /** The insert macro button click event handler */ onclick: function () { dialogService.open({ @@ -298,16 +319,31 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou //put the macro syntax in comments, we will parse this out on the server side to be used //for persisting. var macroSyntaxComment = ""; + //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', + { + 'class': 'umb-macro-holder ' + data.macroAlias + ' mceNonEditable ' + uniqueId + }, + macroSyntaxComment + 'Macro alias: ' + data.macroAlias + ''); - editor.insertContent( - editor.dom.createHTML('div', - { - 'class': 'umb-macro-holder', - // indicates whether or not this should kick off the ajax request to load in the macro contents. - 'data-load-content': false - }, - macroSyntaxComment + 'Macro alias: ' + data.macroAlias + '')); + editor.selection.setNode(macroDiv); + + var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); + var $ins = $macroDiv.find("ins"); + //show the throbber + $macroDiv.addClass("loading"); + + macroResource.getMacroResultAsHtmlForEditor(data.macroAlias, 1234) + .then(function (htmlResult) { + + $macroDiv.removeClass("loading"); + htmlResult = htmlResult.trim(); + if (htmlResult !== "") { + $ins.html(htmlResult); + } + }); } }); diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index ba59d7b2c2..f850e90319 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -91,7 +91,6 @@ iframe, .content-column-body { .mce-panel{background: @grayLighter !important; border-color: @grayLight !important;} .mce-btn-group, .mce-btn{border: none !important; background: none !important;} .mce-ico{font-size: 12px !important; color: @blackLight !important;} - /* Special case to support helviticons for the tiny mce button controls */ .mce-ico.mce-i-custom[class^="icon-"], .mce-ico.mce-i-custom[class*=" icon-"] { diff --git a/src/Umbraco.Web/Editors/MacroController.cs b/src/Umbraco.Web/Editors/MacroController.cs index 828a4af7d0..da7b938f91 100644 --- a/src/Umbraco.Web/Editors/MacroController.cs +++ b/src/Umbraco.Web/Editors/MacroController.cs @@ -1,9 +1,19 @@ +using System; using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Net.Http; +using System.Text; +using System.Web; using System.Web.Http; using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.Routing; +using umbraco; namespace Umbraco.Web.Editors { @@ -31,5 +41,62 @@ namespace Umbraco.Web.Editors return Mapper.Map>(macro); } + + /// + /// Gets a rendered macro as html for rendering in the rich text editor + /// + /// + /// + /// + public HttpResponseMessage GetMacroResultAsHtmlForEditor(string macroAlias, int pageId) + { + var doc = Services.ContentService.GetById(pageId); + if (doc == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //need to get a legacy macro object - eventually we'll have a new format but nto yet + var macro = new macro(macroAlias); + if (macro == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //if it isn't supposed to be rendered in the editor then return an empty string + if (macro.DontRenderInEditor) + { + return new HttpResponseMessage() + { + //need to create a specific content result formatted as html since this controller has been configured + //with only json formatters. + Content = new StringContent(string.Empty, Encoding.UTF8, "text/html") + }; + } + + //because macro's are filled with insane legacy bits and pieces we need all sorts of wierdness to make them render. + //the 'easiest' way might be to create an IPublishedContent manually and populate the legacy 'page' object with that + //and then set the legacy parameters. + + var xml = doc.ToXml(); + var publishedContent = new XmlPublishedContent(xml.ToXmlElement()); + + var legacyPage = new global::umbraco.page(publishedContent); + UmbracoContext.HttpContext.Items["pageID"] = doc.Id; + UmbracoContext.HttpContext.Items["pageElements"] = legacyPage.Elements; + UmbracoContext.HttpContext.Items[global::Umbraco.Core.Constants.Conventions.Url.AltTemplate] = null; + + return new HttpResponseMessage() + { + //need to create a specific content result formatted as html since this controller has been configured + //with only json formatters. + Content = new StringContent( + Umbraco.RenderMacro(macro, new Dictionary(), legacyPage).ToString(), + Encoding.UTF8, + "text/html" + ) + }; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index a0d2dbf39d..be62950600 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -376,6 +376,8 @@ namespace Umbraco.Web // TODO - this is dirty old legacy tricks, we should clean it up at some point // also, what is a "custom page" and when should this be either null, or different // from PublishedContentRequest.PublishedContent.Id ?? + // SD: Have found out it can be different when rendering macro contents in the back office, but really youshould just be able + // to pass a page id to the macro renderer instead but due to all the legacy bits that's real difficult. get { try diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index c0b59819c4..bf4ccc3cd2 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -138,71 +138,107 @@ namespace Umbraco.Web /// public IHtmlString RenderMacro(string alias, IDictionary parameters) { - if (alias == null) throw new ArgumentNullException("alias"); - - var m = macro.GetMacro(alias); - if (_umbracoContext.PageId == null) - { - throw new InvalidOperationException("Cannot render a macro when UmbracoContext.PageId is null."); - } + if (_umbracoContext.PublishedContentRequest == null) { throw new InvalidOperationException("Cannot render a macro when there is no current PublishedContentRequest."); } - var macroProps = new Hashtable(); - foreach (var i in parameters) - { - //TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method of macro.cs - // looks for a lower case match. WTF. the whole macro concept needs to be rewritten. - macroProps.Add(i.Key.ToLower(), i.Value); - } - var macroControl = m.renderMacro(macroProps, - UmbracoContext.Current.PublishedContentRequest.UmbracoPage.Elements, - _umbracoContext.PageId.Value); - string html; - if (macroControl is LiteralControl) - { - // no need to execute, we already have text - html = (macroControl as LiteralControl).Text; - } - else - { - var containerPage = new FormlessPage(); - containerPage.Controls.Add(macroControl); + return RenderMacro(alias, parameters, _umbracoContext.PublishedContentRequest.UmbracoPage); + } - using (var output = new StringWriter()) - { - // .Execute() does a PushTraceContext/PopTraceContext and writes trace output straight into 'output' - // and I do not see how we could wire the trace context to the current context... so it creates dirty - // trace output right in the middle of the page. - // - // The only thing we can do is fully disable trace output while .Execute() runs and restore afterwards - // which means trace output is lost if the macro is a control (.ascx or user control) that is invoked - // from within Razor -- which makes sense anyway because the control can _not_ run correctly from - // within Razor since it will never be inserted into the page pipeline (which may even not exist at all - // if we're running MVC). - // + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The alias. + /// The parameters. + /// The legacy umbraco page object that is required for some macros + /// + internal IHtmlString RenderMacro(string alias, IDictionary parameters, page umbracoPage) + { + if (alias == null) throw new ArgumentNullException("alias"); + if (umbracoPage == null) throw new ArgumentNullException("umbracoPage"); + + var m = macro.GetMacro(alias); + if (m == null) + { + throw new KeyNotFoundException("Could not find macro with alias " + alias); + } + + return RenderMacro(m, parameters, umbracoPage); + } + + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The macro. + /// The parameters. + /// The legacy umbraco page object that is required for some macros + /// + internal IHtmlString RenderMacro(macro m, IDictionary parameters, page umbracoPage) + { + if (umbracoPage == null) throw new ArgumentNullException("umbracoPage"); + if (m == null) throw new ArgumentNullException("m"); + + if (_umbracoContext.PageId == null) + { + throw new InvalidOperationException("Cannot render a macro when UmbracoContext.PageId is null."); + } + + var macroProps = new Hashtable(); + foreach (var i in parameters) + { + //TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method of macro.cs + // looks for a lower case match. WTF. the whole macro concept needs to be rewritten. + macroProps.Add(i.Key.ToLower(), i.Value); + } + var macroControl = m.renderMacro(macroProps, + umbracoPage.Elements, + _umbracoContext.PageId.Value); + + string html; + if (macroControl is LiteralControl) + { + // no need to execute, we already have text + html = (macroControl as LiteralControl).Text; + } + else + { + var containerPage = new FormlessPage(); + containerPage.Controls.Add(macroControl); + + using (var output = new StringWriter()) + { + // .Execute() does a PushTraceContext/PopTraceContext and writes trace output straight into 'output' + // and I do not see how we could wire the trace context to the current context... so it creates dirty + // trace output right in the middle of the page. + // + // The only thing we can do is fully disable trace output while .Execute() runs and restore afterwards + // which means trace output is lost if the macro is a control (.ascx or user control) that is invoked + // from within Razor -- which makes sense anyway because the control can _not_ run correctly from + // within Razor since it will never be inserted into the page pipeline (which may even not exist at all + // if we're running MVC). + // // I'm sure there's more things that will get lost with this context changing but I guess we'll figure // those out as we go along. One thing we lose is the content type response output. // http://issues.umbraco.org/issue/U4-1599 if it is setup during the macro execution. So // here we'll save the content type response and reset it after execute is called. - var contentType = _umbracoContext.HttpContext.Response.ContentType; - var traceIsEnabled = containerPage.Trace.IsEnabled; - containerPage.Trace.IsEnabled = false; - _umbracoContext.HttpContext.Server.Execute(containerPage, output, false); - containerPage.Trace.IsEnabled = traceIsEnabled; + var contentType = _umbracoContext.HttpContext.Response.ContentType; + var traceIsEnabled = containerPage.Trace.IsEnabled; + containerPage.Trace.IsEnabled = false; + _umbracoContext.HttpContext.Server.Execute(containerPage, output, false); + containerPage.Trace.IsEnabled = traceIsEnabled; //reset the content type - _umbracoContext.HttpContext.Response.ContentType = contentType; + _umbracoContext.HttpContext.Response.ContentType = contentType; - //Now, we need to ensure that local links are parsed - html = TemplateUtilities.ParseInternalLinks(output.ToString()); - } - } + //Now, we need to ensure that local links are parsed + html = TemplateUtilities.ParseInternalLinks(output.ToString()); + } + } - return new HtmlString(html); - } + return new HtmlString(html); + } #endregion diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/macroResultWrapper.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/macroResultWrapper.aspx.cs index 50f9d852bc..f328f00f01 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/macroResultWrapper.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/macroResultWrapper.aspx.cs @@ -16,6 +16,7 @@ namespace umbraco.presentation /// /// Summary description for macroResultWrapper. /// + [Obsolete("This is no longer used and will be removed from the codebase")] public partial class macroResultWrapper : BasePages.UmbracoEnsuredPage {