From 61654a370a09e2a5a2937cf5516eaca51d25075c Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 23 Sep 2013 12:15:15 +1000 Subject: [PATCH] Got TinyMCE insert macro button/dialog inserting into the rich text editor - now need to ensure it is written out correctly and then parsed on the server properly so that it works! --- .../lib/tinymce/skins/umbraco/content.min.css | 86 ++++++++- .../src/common/services/macro.service.js | 30 +++ .../src/common/services/tinymce.service.js | 181 ++++++++++++++++-- .../common/dialogs/insertmacro.controller.js | 10 +- .../propertyeditors/rte/rte.controller.js | 37 +++- .../common/services/macro-service.spec.js | 28 +++ .../umbraco_client/Editors/EditTemplate.js | 2 +- 7 files changed, 353 insertions(+), 21 deletions(-) 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 0b2726010d..22702c1555 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 @@ -1 +1,85 @@ -body.mce-content-body{background-color:#fff;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px;scrollbar-3dlight-color:#f0f0ee;scrollbar-arrow-color:#676662;scrollbar-base-color:#f0f0ee;scrollbar-darkshadow-color:#ddd;scrollbar-face-color:#e0e0dd;scrollbar-highlight-color:#f0f0ee;scrollbar-shadow-color:#f0f0ee;scrollbar-track-color:#f5f5f5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:11px}.mce-object{border:1px dotted #3a3a3a;background:#d5d5d5 url(img/object.gif) no-repeat center}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;width:9px!important;height:9px!important;border:1px dotted #3a3a3a;background:#d5d5d5 url(img/anchor.gif) no-repeat center}.mce-nbsp{background:#AAA}hr{cursor:default}.mce-match-marker{background:green;color:#fff}.mce-spellchecker-word{background:url(img/wline.gif) repeat-x bottom left;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td.mce-item-selected,th.mce-item-selected{background-color:#39f!important}.mce-edit-focus{outline:1px dotted #333} \ No newline at end of file +body.mce-content-body { + background-color: #fff; + font-family: Verdana,Arial,Helvetica,sans-serif; + font-size: 11px; + scrollbar-3dlight-color: #f0f0ee; + scrollbar-arrow-color: #676662; + scrollbar-base-color: #f0f0ee; + scrollbar-darkshadow-color: #ddd; + scrollbar-face-color: #e0e0dd; + scrollbar-highlight-color: #f0f0ee; + scrollbar-shadow-color: #f0f0ee; + scrollbar-track-color: #f5f5f5; +} + +td, th { + font-family: Verdana,Arial,Helvetica,sans-serif; + font-size: 11px; +} + +.mce-object { + border: 1px dotted #3a3a3a; + background: #d5d5d5 url(img/object.gif) no-repeat center; +} + +.mce-pagebreak { + cursor: default; + display: block; + border: 0; + width: 100%; + height: 5px; + border: 1px dashed #666; + margin-top: 15px; +} + +.mce-item-anchor { + cursor: default; + display: inline-block; + -webkit-user-select: all; + -webkit-user-modify: read-only; + -moz-user-select: all; + -moz-user-modify: read-only; + width: 9px!important; + height: 9px!important; + border: 1px dotted #3a3a3a; + background: #d5d5d5 url(img/anchor.gif) no-repeat center; +} + +.mce-nbsp { + background: #AAA; +} + +hr { + cursor: default; +} + +.mce-match-marker { + background: green; + color: #fff; +} + +.mce-spellchecker-word { + background: url(img/wline.gif) repeat-x bottom left; + cursor: default; +} + +.mce-item-table, .mce-item-table td, .mce-item-table th, .mce-item-table caption { + border: 1px dashed #BBB; +} + +td.mce-item-selected, th.mce-item-selected { + background-color: #39f!important; +} + +.mce-edit-focus { + outline: 1px dotted #333; +} + +/* TINYMCE Macro styles*/ +.mce-content-body .umb-macro-holder +{ + border: 3px dotted orange; + padding: 7px; + display:block; + margin:3px; +} diff --git a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js index 9a1f4b7483..ca80d4aa04 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js @@ -10,6 +10,36 @@ function macroService() { return { + /** + * @ngdoc function + * @name generateWebFormsSyntax + * @methodOf generateMacroSyntax.services.macroService + * @function + * + * @description + * generates the syntax for inserting a macro into a rich text editor - this is the very old umbraco style syntax + * + * @param {object} args an object containing the macro alias and it's parameter values + */ + generateMacroSyntax: function (args) { + + // + + var macroString = '"; + + return macroString; + }, + /** * @ngdoc function * @name generateWebFormsSyntax 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 7fa3bba2a6..6c27e5c5ca 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 @@ -8,7 +8,7 @@ */ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeout) { return { - + /** * @ngdoc method * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia @@ -45,7 +45,7 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou * @param {Object} editor the TinyMCE editor instance * @param {Object} $scope the current controller scope */ - createMediaPicker: function(editor, $scope) { + createMediaPicker: function (editor, $scope) { editor.addButton('umbmediapicker', { icon: 'media', tooltip: 'Media Picker', @@ -56,7 +56,7 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou if (img) { var imagePropVal = imageHelper.getImagePropertyValue({ imageModel: img, scope: $scope }); var data = { - src: (imagePropVal != null && imagePropVal != "") ? imagePropVal : "nothing.jpg", + src: (imagePropVal) ? imagePropVal : "nothing.jpg", id: '__mcenew' }; @@ -81,7 +81,7 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou } }); }, - + /** * @ngdoc method * @name umbraco.services.tinyMceService#createIconPicker @@ -126,21 +126,178 @@ 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 + editor.serializer.addRules('div'); + + /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + editor.serializer.addNodeFilter('div', function (nodes, name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { + nodes[i].parent.unwrap(); + } + } + }); + }); + + ///** Listens for the editor saving */ + //$scope.on("saving", function() { + + //}); + + /** Adds the button instance */ editor.addButton('umbmacro', { icon: 'custom icon-settings-alt', tooltip: 'Insert macro', + onPostRender: function () { + + var ctrl = this; + var isOnMacroElement = false; + + /** + * Because the macro gets wrapped in a P tag because of the way 'enter' works, this + * method will return the macro element if not wrapped in a p, or the p if the macro + * element is the only one inside of it even if we are deep inside an element inside the macro + */ + function getRealMacroElem(element) { + var e = $(element).closest(".umb-macro-holder"); + if (e.length > 0) { + if (e.get(0).parentNode.nodeName === "P") { + //now check if we're the only element + if (element.parentNode.childNodes.length === 1) { + return e.get(0).parentNode; + } + } + return e.get(0); + } + return null; + } + + /** + * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag. + * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves + * from the event listener before changing selection, however, it seems that putting a break point in this method + * will always cause an 'infinite' loop as the caret keeps changing. + */ + function onNodeChanged(evt) { + + //set our macro button active when on a node of class umb-macro-holder + var $macroElement = $(evt.element).closest(".umb-macro-holder"); + + ctrl.active($macroElement.length !== 0); + + if ($macroElement.length > 0) { + var macroElement = $macroElement.get(0); + + //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); + + // check if the current selection *is* the element (ie bug) + var currentSelection = editor.selection.getStart(); + if (tinymce.isIE) { + if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) { + while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) { + currentSelection = currentSelection.parentNode; + } + editor.selection.select(currentSelection); + } + } + + //set the flag + isOnMacroElement = true; + + //re-add the event listener + editor.on('NodeChange', onNodeChanged); + } + else { + isOnMacroElement = false; + } + + } + + + //set onNodeChanged event listener + editor.on('NodeChange', onNodeChanged); + + /** + * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so + * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request + * so the macro cannot be edited. + */ + editor.on('KeyDown', function (e) { + if (isOnMacroElement) { + var macroElement = editor.selection.getNode(); + + //get the 'real' element (either p or the real one) + macroElement = getRealMacroElem(macroElement); + + //prevent editing + e.preventDefault(); + e.stopPropagation(); + + var moveSibling = function (element, isNext) { + var $e = $(element); + var $sibling = isNext ? $e.next() : $e.prev(); + if ($sibling.length > 0) { + editor.selection.select($sibling.get(0)); + editor.selection.collapse(true); + } + else { + //if we're moving previous and there is no sibling, then lets recurse and just select the next one + if (!isNext) { + moveSibling(element, true); + return; + } + + //if there is no sibling we'll generate a new p at the end and select it + editor.setContent(editor.getContent() + "

 

"); + editor.selection.select($(editor.dom.getRoot()).children().last().get(0)); + editor.selection.collapse(true); + + } + }; + + //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) + if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) { + //move to next element + moveSibling(macroElement, true); + } + else if ($.inArray(e.keyCode, [27, 38, 37]) !== -1) { + //move to prev element + moveSibling(macroElement, false); + } + else if ($.inArray(e.keyCode, [8, 46]) !== -1) { + //delete macro element + + //move first, then delete + moveSibling(macroElement, false); + editor.dom.remove(macroElement); + } + + return false; + + } + }); + + }, onclick: function () { dialogService.open({ - show: true, template: "views/common/dialogs/insertmacro.html", scope: $scope, callback: function (c) { + show: true, + template: "views/common/dialogs/insertmacro.html", + scope: $scope, + callback: function(data) { + + editor.insertContent( + editor.dom.createHTML('div', { 'class': 'umb-macro-holder' }, 'Macro alias: ' + data.macroAlias + '')); - var data = { - style: 'font-size: 60px' - }; - - var i = editor.dom.createHTML('i', data); - tinyMCE.activeEditor.dom.addClass(i, c); - editor.insertContent(i); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.controller.js index 50f95647d6..2eee8dbcb8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.controller.js @@ -18,7 +18,8 @@ function InsertMacroController($scope, entityResource, macroResource, umbPropEdi //go to next page if there are params otherwise we can just exit if (!angular.isArray(data) || data.length === 0) { //we can just exist! - $scope.submit({ selectedMacro: $scope.selectedMacro }); + submitForm(); + } else { $scope.wizardStep = "paramSelect"; $scope.macroParams = data; @@ -44,11 +45,14 @@ function InsertMacroController($scope, entityResource, macroResource, umbPropEdi if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === "WebForms") { syntax = macroService.generateWebFormsSyntax({ macroAlias: macroAlias, macroParams: vals }); } - else { + else if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === "Mvc") { syntax = macroService.generateMvcSyntax({ macroAlias: macroAlias, macroParams: vals }); } + else { + syntax = macroService.generateMacroSyntax({ macroAlias: macroAlias, macroParams: vals }); + } - $scope.submit(syntax); + $scope.submit({syntax : syntax, macroAlias: macroAlias, macroParams: vals }); } $scope.macros = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index a4bd9eb266..183d842dca 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -2,6 +2,35 @@ angular.module("umbraco") .controller("Umbraco.Editors.RTEController", function ($rootScope, $scope, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService) { + //TODO: This should be configurable (i.e. from the config file we have and/or from pre-values) + var validElements = "@[id|class|style|title|dir"); + + }); + + it('can generate syntax for macros with no params', function () { + + var syntax = macroService.generateMacroSyntax({ + macroAlias: "myMacro", + macroParams: [] + }); + + expect(syntax). + toBe(""); + + }); + it('can generate syntax for webforms', function () { var syntax = macroService.generateWebFormsSyntax({ diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js index e77178ceca..77830eaad5 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js +++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js @@ -17,7 +17,7 @@ selectedAlias: alias }, callback: function(data) { - UmbEditor.Insert(data, '', self._opts.editorClientId); + UmbEditor.Insert(data.syntax, '', self._opts.editorClientId); } }); },