Skip to content

Commit 241ba50

Browse files
authored
Merge pull request #1978 from umbraco/v14/bugfix/tinymce-plugin-config
Feature: TinyMce Custom Configuration
2 parents 6e61968 + d502c40 commit 241ba50

File tree

4 files changed

+91
-54
lines changed

4 files changed

+91
-54
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default class UmbTinyMceMockPlugin {
2+
/**
3+
* @param {TinyMcePluginArguments} args
4+
*/
5+
constructor(args) {
6+
// Add your plugin code here
7+
console.log('editor initialized', args)
8+
}
9+
}

src/mocks/handlers/manifests.handlers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ const privateManifests: PackageManifestResponse = [
4545
propertyEditorSchema: 'Umbraco.TextBox',
4646
},
4747
},
48+
{
49+
type: 'tinyMcePlugin',
50+
alias: 'My.TinyMcePlugin.Custom',
51+
name: 'My Custom TinyMce Plugin',
52+
js: '/App_Plugins/tinyMcePlugin.js',
53+
meta: {
54+
config: {
55+
plugins: ['wordcount'],
56+
statusbar: true,
57+
},
58+
},
59+
},
4860
],
4961
},
5062
{

src/packages/core/extension-registry/models/tinymce-plugin.model.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/tiny-mce';
22
import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api';
3+
import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce';
34

45
export interface MetaTinyMcePlugin {
56
/**
@@ -26,6 +27,20 @@ export interface MetaTinyMcePlugin {
2627
*/
2728
icon?: string;
2829
}>;
30+
31+
/**
32+
* Sets the default configuration for the TinyMCE editor. This configuration will be used when the editor is initialized.
33+
*
34+
* @see [TinyMCE Configuration](https://www.tiny.cloud/docs/configure/) for more information.
35+
* @optional
36+
* @examples [
37+
* {
38+
* "plugins": "wordcount",
39+
* "statusbar": true
40+
* }
41+
* ]
42+
*/
43+
config?: RawEditorOptions;
2944
}
3045

3146
/**

src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { availableLanguages } from './input-tiny-mce.languages.js';
22
import { defaultFallbackConfig } from './input-tiny-mce.defaults.js';
33
import { pastePreProcessHandler } from './input-tiny-mce.handlers.js';
44
import { uriAttributeSanitizer } from './input-tiny-mce.sanitizer.js';
5-
import type { TinyMcePluginArguments, UmbTinyMcePluginBase } from './tiny-mce-plugin.js';
6-
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
7-
import { css, customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit';
8-
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
9-
import { getProcessedImageUrl } from '@umbraco-cms/backoffice/utils';
10-
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
5+
import type { UmbTinyMcePluginBase } from './tiny-mce-plugin.js';
6+
import { type ClassConstructor, loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
7+
import { css, customElement, html, property, query } from '@umbraco-cms/backoffice/external/lit';
8+
import { getProcessedImageUrl, umbDeepMerge } from '@umbraco-cms/backoffice/utils';
9+
import { type ManifestTinyMcePlugin, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
1110
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
1211
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
1312
import { UmbStylesheetDetailRepository, UmbStylesheetRuleManager } from '@umbraco-cms/backoffice/stylesheet';
@@ -53,10 +52,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
5352
@property({ attribute: false })
5453
configuration?: UmbPropertyEditorConfigCollection;
5554

56-
@state()
57-
private _tinyConfig: RawEditorOptions = {};
58-
59-
#plugins: Array<new (args: TinyMcePluginArguments) => UmbTinyMcePluginBase> = [];
55+
#plugins: Array<ClassConstructor<UmbTinyMcePluginBase> | undefined> = [];
6056
#editorRef?: Editor | null = null;
6157
#stylesheetRepository = new UmbStylesheetDetailRepository(this);
6258
#umbStylesheetRuleManager = new UmbStylesheetRuleManager();
@@ -85,15 +81,31 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
8581
return this.#editorRef;
8682
}
8783

88-
protected async firstUpdated(): Promise<void> {
89-
await Promise.all([...(await this.#loadPlugins())]);
90-
await this.#setTinyConfig();
84+
constructor() {
85+
super();
86+
87+
this.#loadEditor();
88+
}
89+
90+
async #loadEditor() {
91+
this.observe(umbExtensionsRegistry.byType('tinyMcePlugin'), async (manifests) => {
92+
this.#plugins.length = 0;
93+
this.#plugins = await this.#loadPlugins(manifests);
94+
95+
let config: RawEditorOptions = {};
96+
manifests.forEach((manifest) => {
97+
if (manifest.meta?.config) {
98+
config = umbDeepMerge(manifest.meta.config, config);
99+
}
100+
});
101+
102+
this.#setTinyConfig(config);
103+
});
91104
}
92105

93106
disconnectedCallback() {
94107
super.disconnectedCallback();
95108

96-
// TODO: Test if there is any problems with destroying the RTE here, but not initializing on connectedCallback. (firstUpdated is only called first time the element is rendered, not when it is reconnected)
97109
this.#editorRef?.destroy();
98110
}
99111

@@ -103,29 +115,14 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
103115
* setup method, the asynchronous nature means the editor is loaded before
104116
* the plugins are ready and so are not associated with the editor.
105117
*/
106-
async #loadPlugins() {
107-
const observable = umbExtensionsRegistry?.byType('tinyMcePlugin');
108-
const manifests = await firstValueFrom(observable);
109-
118+
async #loadPlugins(manifests: Array<ManifestTinyMcePlugin>) {
110119
const promises = [];
111120
for (const manifest of manifests) {
112121
if (manifest.js) {
113-
promises.push(
114-
loadManifestApi(manifest.js).then((plugin) => {
115-
if (plugin) {
116-
this.#plugins.push(plugin);
117-
}
118-
}),
119-
);
122+
promises.push(await loadManifestApi(manifest.js));
120123
}
121124
if (manifest.api) {
122-
promises.push(
123-
loadManifestApi(manifest.api).then((plugin) => {
124-
if (plugin) {
125-
this.#plugins.push(plugin);
126-
}
127-
}),
128-
);
125+
promises.push(await loadManifestApi(manifest.api));
129126
}
130127
}
131128
return promises;
@@ -181,7 +178,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
181178
return formatStyles;
182179
}
183180

184-
async #setTinyConfig() {
181+
async #setTinyConfig(additionalConfig?: RawEditorOptions) {
185182
const dimensions = this.configuration?.getValueByAlias<{ width?: number; height?: number }>('dimensions');
186183

187184
const stylesheetPaths = this.configuration?.getValueByAlias<string[]>('stylesheets') ?? [];
@@ -230,7 +227,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
230227
}
231228

232229
// set the default values that will not be modified via configuration
233-
this._tinyConfig = {
230+
let config: RawEditorOptions = {
234231
autoresize_bottom_margin: 10,
235232
body_class: 'umb-rte',
236233
contextMenu: false,
@@ -244,27 +241,31 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
244241
setup: (editor) => this.#editorSetup(editor),
245242
target: this._editorElement,
246243
paste_data_images: false,
244+
language: this.#getLanguage(),
245+
promotion: false,
247246

248247
// Extend with configuration options
249248
...configurationOptions,
250249
};
251250

252-
this.#setLanguage();
253-
254-
if (this.#editorRef) {
255-
this.#editorRef.destroy();
251+
// Extend with additional configuration options
252+
if (additionalConfig) {
253+
config = umbDeepMerge(additionalConfig, config);
256254
}
257255

258-
const editors = await renderEditor(this._tinyConfig).catch((error) => {
256+
this.#editorRef?.destroy();
257+
258+
const editors = await renderEditor(config).catch((error) => {
259259
console.error('Failed to render TinyMCE', error);
260260
return [];
261261
});
262262
this.#editorRef = editors.pop();
263263
}
264264

265265
/**
266-
* Sets the language to use for TinyMCE */
267-
#setLanguage() {
266+
* Gets the language to use for TinyMCE
267+
**/
268+
#getLanguage() {
268269
const localeId = this.localize.lang();
269270
//try matching the language using full locale format
270271
let languageMatch = availableLanguages.find((x) => localeId?.localeCompare(x) === 0);
@@ -277,23 +278,12 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
277278
}
278279
}
279280

280-
// only set if language exists, will fall back to tiny default
281-
if (languageMatch) {
282-
this._tinyConfig.language = languageMatch;
283-
}
281+
return languageMatch;
284282
}
285283

286284
#editorSetup(editor: Editor) {
287285
editor.suffix = '.min';
288286

289-
// instantiate plugins - these are already loaded in this.#loadPlugins
290-
// to ensure they are available before setting up the editor.
291-
// Plugins require a reference to the current editor as a param, so can not
292-
// be instantiated until we have an editor
293-
for (const plugin of this.#plugins) {
294-
new plugin({ host: this, editor });
295-
}
296-
297287
// define keyboard shortcuts
298288
editor.addShortcut('Ctrl+S', '', () =>
299289
this.dispatchEvent(new CustomEvent('rte.shortcut.save', { composed: true, bubbles: true })),
@@ -336,13 +326,24 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
336326
}
337327
});
338328
});
339-
editor.on('init', () => editor.setContent(this.value?.toString() ?? ''));
329+
330+
// instantiate plugins to ensure they are available before setting up the editor.
331+
// Plugins require a reference to the current editor as a param, so can not
332+
// be instantiated until we have an editor
333+
for (const plugin of this.#plugins) {
334+
if (plugin) {
335+
// [v15]: This might be improved by changing to `createExtensionApi` and avoiding the `#loadPlugins` method altogether, but that would require a breaking change
336+
// because that function sends the UmbControllerHost as the first argument, which is not the case here.
337+
new plugin({ host: this, editor });
338+
}
339+
}
340340
}
341341

342342
#onInit(editor: Editor) {
343343
//enable browser based spell checking
344344
editor.getBody().setAttribute('spellcheck', 'true');
345345
uriAttributeSanitizer(editor);
346+
editor.setContent(this.value?.toString() ?? '');
346347
}
347348

348349
#onChange(value: string) {

0 commit comments

Comments
 (0)