V13: Rich text editor does not show its toolbar on grid layout rte's (#15595)

* synchronize normal rte with grid-rte

* restore pinToolbar and unpinToolbar from v10 and update to tinymce v6 and apply to grid-rte

* linting

* Reverting `pinToolbar` from v8

* remove unused variable

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
This commit is contained in:
Jacob Overgaard
2024-01-18 13:19:36 +01:00
parent 597d8553c0
commit 9df6e65552
4 changed files with 214 additions and 209 deletions

View File

@@ -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;
}
});
}
};
});

View File

@@ -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',

View File

@@ -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}}">
</grid-rte>
</div>

View File

@@ -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;
}
});
}