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:
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user