diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 812fec6e9c..7821c00e69 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -12,22 +12,18 @@ angular.module("umbraco.directives") replace: true, link: function (scope, element, attrs) { - // TODO: A lot of the code below should be shared between the grid rte and the normal rte - scope.isLoading = true; - var promises = []; - //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because // we have this mini content editor panel that can be launched with MNTP. scope.textAreaHtmlId = scope.uniqueId + "_" + String.CreateGuid(); - var editorConfig = scope.configuration ? scope.configuration : null; + let editorConfig = scope.configuration ? scope.configuration : null; if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); //for the grid by default, we don't want to include the macro or the block-picker toolbar - editorConfig.toolbar = _.without(editorConfig, "umbmacro", "umbblockpicker"); + editorConfig.toolbar = _.without(editorConfig.toolbar, "umbmacro", "umbblockpicker"); } //ensure the grid's global config is being passed up to the RTE, these 2 properties need to be in this format @@ -39,46 +35,50 @@ angular.module("umbraco.directives") scope.dataTypeKey = scope.datatypeKey; //Yes - this casing is rediculous, but it's because the var starts with `data` so it can't be `data-type-id` :/ //stores a reference to the editor - var tinyMceEditor = null; + let tinyMceEditor = null; + + const assetPromises = []; //queue file loading tinyMceAssets.forEach(function (tinyJsAsset) { - promises.push(assetsService.loadJs(tinyJsAsset, scope)); + assetPromises.push(assetsService.loadJs(tinyJsAsset, scope)); }); - promises.push(tinyMceService.getTinyMceEditorConfig({ - htmlId: scope.textAreaHtmlId, - stylesheets: editorConfig.stylesheets, - toolbar: editorConfig.toolbar, - mode: editorConfig.mode - })); - $q.all(promises).then(function (result) { + //wait for assets to load before proceeding + $q.all(assetPromises) + .then(function () { + return tinyMceService.getTinyMceEditorConfig({ + htmlId: scope.textAreaHtmlId, + stylesheets: editorConfig.stylesheets, + toolbar: editorConfig.toolbar, + mode: editorConfig.mode + }) + }) - var standardConfig = result[promises.length - 1]; + // Handle additional assets loading depending on the configuration before initializing the editor + .then(function (tinyMceConfig) { + // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified + if (tinyMceConfig.cloudApiKey) { + return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`) + .then(() => tinyMceConfig); + } + + return tinyMceConfig; + }) + + //wait for config to be ready after assets have loaded + .then(function (standardConfig) { //create a baseline Config to extend upon - var baseLineConfigObj = { - maxImageSize: editorConfig.maxImageSize, - toolbar_sticky: true + let baseLineConfigObj = { + maxImageSize: editorConfig.maxImageSize }; - Utilities.extend(baseLineConfigObj, standardConfig); - baseLineConfigObj.setup = function (editor) { //set the reference tinyMceEditor = editor; - //initialize the standard editor functionality for Umbraco - tinyMceService.initializeEditor({ - editor: editor, - toolbar: editorConfig.toolbar, - model: scope, - // Form is found in the scope of the grid controller above us, not in our isolated scope - // https://github.com/umbraco/Umbraco-CMS/issues/7461 - currentForm: angularHelper.getCurrentForm(scope.$parent) - }); - //custom initialization for this editor within the grid editor.on('init', function (e) { @@ -96,49 +96,52 @@ angular.module("umbraco.directives") }, 400); }); + + //initialize the standard editor functionality for Umbraco + tinyMceService.initializeEditor({ + editor: editor, + toolbar: editorConfig.toolbar, + model: scope, + // Form is found in the scope of the grid controller above us, not in our isolated scope + // https://github.com/umbraco/Umbraco-CMS/issues/7461 + currentForm: angularHelper.getCurrentForm(scope.$parent) + }); }; - /** Loads in the editor */ - function loadTinyMce() { - - //we need to add a timeout here, to force a redraw so TinyMCE can find - //the elements needed - $timeout(function () { - tinymce.init(baseLineConfigObj); - }, 150, false); - } - - loadTinyMce(); - - // TODO: This should probably be in place for all RTE, not just for the grid, which means - // this code can live in tinyMceService.initializeEditor - var tabShownListener = eventsService.on("app.tabChange", function (e, args) { - - var tabId = args.id; - var myTabId = element.closest(".umb-tab-pane").attr("rel"); - - if (String(tabId) === myTabId) { - //the tab has been shown, trigger the mceAutoResize (as it could have timed out before the tab was shown) - if (tinyMceEditor !== undefined && tinyMceEditor != null) { - tinyMceEditor.execCommand('mceAutoResize', false, null, null); - } - } - - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - scope.$on('$destroy', function () { - eventsService.unsubscribe(tabShownListener); - //ensure we unbind this in case the blur doesn't fire above - if (tinyMceEditor !== undefined && tinyMceEditor != null) { - tinyMceEditor.destroy() - } - }); + Utilities.extend(baseLineConfigObj, standardConfig); + //we need to add a timeout here, to force a redraw so TinyMCE can find + //the elements needed + $timeout(function () { + tinymce.init(baseLineConfigObj); + }, 150); }); + const tabShownListener = eventsService.on("app.tabChange", function (e, args) { + + const tabId = String(args.id); + const myTabId = element.closest(".umb-tab-pane").attr("rel"); + + if (tabId === myTabId) { + //the tab has been shown, trigger the mceAutoResize (as it could have timed out before the tab was shown) + if (tinyMceEditor !== undefined && tinyMceEditor != null) { + tinyMceEditor.execCommand('mceAutoResize', false, null, null); + } + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + scope.$on('$destroy', function () { + eventsService.unsubscribe(tabShownListener); + + //ensure we unbind this in case the blur doesn't fire above + if (tinyMceEditor) { + tinyMceEditor.destroy(); + tinyMceEditor = null; + } + }); } }; }); 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 cfea1d5594..ebf759c20c 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 @@ -797,6 +797,11 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); }); + // Do not add any further controls if the block editor is not available + if (!blockEditorApi) { + return; + } + editor.ui.registry.addToggleButton('umbblockpicker', { icon: 'visualblocks', tooltip: 'Insert Block', diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html index fb6f3b2a4b..2fbd0b2fe3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html @@ -4,8 +4,8 @@ configuration="model.config.rte" value="control.value" unique-id="control.$uniqueId" - datatype-key="{{model.dataTypeKey}}" - ignore-user-start-nodes="{{model.config.ignoreUserStartNodes}}"> + datatype-key="{{model.dataTypeKey}}" + ignore-user-start-nodes="{{model.config.ignoreUserStartNodes}}"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js index 4e9cf7d014..7f06215148 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js @@ -195,162 +195,148 @@ assetPromises.push(assetsService.loadJs(tinyJsAsset, $scope)); }); - const tinyMceConfigDeferred = $q.defer(); - //wait for assets to load before proceeding - $q.all(assetPromises).then(function () { - - tinyMceService.getTinyMceEditorConfig({ - htmlId: vm.textAreaHtmlId, - stylesheets: editorConfig.stylesheets, - toolbar: editorConfig.toolbar, - mode: editorConfig.mode + $q.all(assetPromises) + .then(function () { + return tinyMceService.getTinyMceEditorConfig({ + htmlId: vm.textAreaHtmlId, + stylesheets: editorConfig.stylesheets, + toolbar: editorConfig.toolbar, + mode: editorConfig.mode + }) }) - .then(function (tinyMceConfig) { - // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified - if (tinyMceConfig.cloudApiKey) { - return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`) - .then(() => tinyMceConfig); + + // Handle additional assets loading depending on the configuration before initializing the editor + .then(function (tinyMceConfig) { + // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified + if (tinyMceConfig.cloudApiKey) { + return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`) + .then(() => tinyMceConfig); + } + + return tinyMceConfig; + }) + + //wait for config to be ready after assets have loaded + .then(function (standardConfig) { + + if (height !== null) { + standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1); } - return tinyMceConfig; - }) - .then(function (tinyMceConfig) { - tinyMceConfigDeferred.resolve(tinyMceConfig); - }); - }); + //create a baseline Config to extend upon + let baseLineConfigObj = { + maxImageSize: editorConfig.maxImageSize, + width: width, + height: height + }; - //wait for config to be ready after assets have loaded - tinyMceConfigDeferred.promise.then(function (standardConfig) { + baseLineConfigObj.setup = function (editor) { - if (height !== null) { - standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1); - } + //set the reference + vm.tinyMceEditor = editor; - //create a baseline Config to extend upon - var baseLineConfigObj = { - maxImageSize: editorConfig.maxImageSize, - width: width, - height: height - }; - - baseLineConfigObj.setup = function (editor) { - - //set the reference - vm.tinyMceEditor = editor; - - vm.tinyMceEditor.on('init', function (e) { - $timeout(function () { - vm.rteLoading = false; - vm.updateLoading(); - }); - }); - vm.tinyMceEditor.on("focus", function () { - $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true})); - }); - vm.tinyMceEditor.on("blur", function () { - $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true})); - }); - - //initialize the standard editor functionality for Umbraco - tinyMceService.initializeEditor({ - //scope: $scope, - editor: editor, - toolbar: editorConfig.toolbar, - model: vm.model, - getValue: function () { - return vm.model.value.markup; - }, - setValue: function (newVal) { - vm.model.value.markup = newVal; - $scope.$evalAsync(); - }, - culture: vm.umbProperty?.culture ?? null, - segment: vm.umbProperty?.segment ?? null, - blockEditorApi: vm.noBlocksMode ? undefined : vm.blockEditorApi, - parentForm: vm.propertyForm, - valFormManager: vm.valFormManager, - currentFormInput: $scope.rteForm.modelValue - }); - - }; - - Utilities.extend(baseLineConfigObj, standardConfig); - - // Readonly mode - baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar; - baseLineConfigObj.readonly = vm.readonly ? 1 : baseLineConfigObj.readonly; - - // We need to wait for DOM to have rendered before we can find the element by ID. - $timeout(function () { - tinymce.init(baseLineConfigObj); - }, 50); - - //listen for formSubmitting event (the result is callback used to remove the event subscription) - unsubscribe.push($scope.$on("formSubmitting", function () { - if (vm.tinyMceEditor != null && !vm.rteLoading) { - - // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. - var blockElements = vm.tinyMceEditor.dom.select(`umb-rte-block, umb-rte-block-inline`); - const usedContentUdis = blockElements.map(blockElement => blockElement.getAttribute('data-content-udi')); - - const unusedBlocks = vm.layout.filter(x => usedContentUdis.indexOf(x.contentUdi) === -1); - unusedBlocks.forEach(blockLayout => { - deleteBlock(blockLayout.$block); - }); - - - // Remove Angular Classes from markup: - var parser = new DOMParser(); - var doc = parser.parseFromString(vm.model.value.markup, 'text/html'); - - // Get all elements in the parsed document - var elements = doc.querySelectorAll('*[class]'); - elements.forEach(element => { - var classAttribute = element.getAttribute("class"); - if (classAttribute) { - // Split the class attribute by spaces and remove "ng-scope" and "ng-isolate-scope" - var classes = classAttribute.split(" "); - var newClasses = classes.filter(function (className) { - return className !== "ng-scope" && className !== "ng-isolate-scope"; + vm.tinyMceEditor.on('init', function (e) { + $timeout(function () { + vm.rteLoading = false; + vm.updateLoading(); }); - - // Update the class attribute with the remaining classes - if (newClasses.length > 0) { - element.setAttribute('class', newClasses.join(' ')); - } else { - // If no remaining classes, remove the class attribute - element.removeAttribute('class'); - } - } + }); + vm.tinyMceEditor.on("focus", function () { + $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true})); + }); + vm.tinyMceEditor.on("blur", function () { + $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true})); }); - vm.model.value.markup = doc.body.innerHTML; + //initialize the standard editor functionality for Umbraco + tinyMceService.initializeEditor({ + //scope: $scope, + editor: editor, + toolbar: editorConfig.toolbar, + model: vm.model, + getValue: function () { + return vm.model.value.markup; + }, + setValue: function (newVal) { + vm.model.value.markup = newVal; + $scope.$evalAsync(); + }, + culture: vm.umbProperty?.culture ?? null, + segment: vm.umbProperty?.segment ?? null, + blockEditorApi: vm.noBlocksMode ? undefined : vm.blockEditorApi, + parentForm: vm.propertyForm, + valFormManager: vm.valFormManager, + currentFormInput: $scope.rteForm.modelValue + }); - } - })); + }; - vm.focusRTE = function () { - vm.tinyMceEditor.focus(); - } + Utilities.extend(baseLineConfigObj, standardConfig); + + // Readonly mode + baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar; + baseLineConfigObj.readonly = vm.readonly ? 1 : baseLineConfigObj.readonly; + + // We need to wait for DOM to have rendered before we can find the element by ID. + $timeout(function () { + tinymce.init(baseLineConfigObj); + }, 50); + + //listen for formSubmitting event (the result is callback used to remove the event subscription) + unsubscribe.push($scope.$on("formSubmitting", function () { + if (vm.tinyMceEditor != null && !vm.rteLoading) { + + // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. + var blockElements = vm.tinyMceEditor.dom.select(`umb-rte-block, umb-rte-block-inline`); + const usedContentUdis = blockElements.map(blockElement => blockElement.getAttribute('data-content-udi')); + + const unusedBlocks = vm.layout.filter(x => usedContentUdis.indexOf(x.contentUdi) === -1); + unusedBlocks.forEach(blockLayout => { + deleteBlock(blockLayout.$block); + }); + + + // Remove Angular Classes from markup: + var parser = new DOMParser(); + var doc = parser.parseFromString(vm.model.value.markup, 'text/html'); + + // Get all elements in the parsed document + var elements = doc.querySelectorAll('*[class]'); + elements.forEach(element => { + var classAttribute = element.getAttribute("class"); + if (classAttribute) { + // Split the class attribute by spaces and remove "ng-scope" and "ng-isolate-scope" + var classes = classAttribute.split(" "); + var newClasses = classes.filter(function (className) { + return className !== "ng-scope" && className !== "ng-isolate-scope"; + }); + + // Update the class attribute with the remaining classes + if (newClasses.length > 0) { + element.setAttribute('class', newClasses.join(' ')); + } else { + // If no remaining classes, remove the class attribute + element.removeAttribute('class'); + } + } + }); + + vm.model.value.markup = doc.body.innerHTML; - // When the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - $scope.$on('$destroy', function () { - if (vm.tinyMceEditor != null) { - if($element) { - $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true})); } - vm.tinyMceEditor.destroy(); - vm.tinyMceEditor = null; - } - }); + })); - }); + }); }; + vm.focusRTE = function () { + if (vm.tinyMceEditor) { + vm.tinyMceEditor.focus(); + } + } + // Called when we save the value, the server may return an updated data and our value is re-synced // we need to deal with that here so that our model values are all in sync so we basically re-initialize. function onServerValueChanged(newVal, oldVal) { @@ -978,6 +964,17 @@ for (const subscription of unsubscribe) { subscription(); } + + // When the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + if (vm.tinyMceEditor != null) { + if($element) { + $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true})); + } + vm.tinyMceEditor.destroy(); + vm.tinyMceEditor = null; + } }); }