diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index f630f0758a..b69099e314 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -21,6 +21,7 @@ "lodash-es": "4.17.21", "router-slot": "file:router-slot-1.6.1.tgz", "rxjs": "^7.8.0", + "tinymce": "^6.3.2", "uuid": "^9.0.0" }, "devDependencies": { @@ -3102,26 +3103,6 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", - "integrity": "sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.1.0", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^2.42.0" - } - }, "node_modules/@rollup/pluginutils": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", @@ -17491,6 +17472,11 @@ "globrex": "^0.1.2" } }, + "node_modules/tinymce": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.3.2.tgz", + "integrity": "sha512-nOVHk4FhHKQj48hi7fEptS1Se6CNzPtfIcDzTO70KoTcSiQIFzhhZjS5bPotSzFnQ4dIQJ4QPOd7sqNs6fXUrA==" + }, "node_modules/title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -20786,29 +20772,15 @@ } } }, - "@rollup/plugin-node-resolve": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", - "integrity": "sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.1.0", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - } - }, "@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", "dev": true, "requires": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" } }, "@sinclair/typebox": { @@ -22197,6 +22169,11 @@ "magic-string": "^0.27.0" } }, + "@tinymce/tinymce-webcomponent": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tinymce/tinymce-webcomponent/-/tinymce-webcomponent-2.0.1.tgz", + "integrity": "sha512-17rbpsggiRqfDTKaAvCFlt9LWfb3JBXXhCZtGprb/Rk707ZMJAjCMMrrobPdCqc71r7BTMCFRH5PL4sP87lbdw==" + }, "@types/accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", @@ -22367,9 +22344,9 @@ "dev": true }, "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, "@types/express": { @@ -22657,15 +22634,6 @@ "csstype": "^3.0.2" } }, - "@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -24009,6 +23977,52 @@ "whatwg-url": "^11.0.0" }, "dependencies": { + "@rollup/plugin-node-resolve": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", + "integrity": "sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", @@ -26591,9 +26605,9 @@ "dev": true }, "estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, "esutils": { @@ -30915,34 +30929,11 @@ "jsonc-parser": "^3.2.0" }, "dependencies": { - "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", - "dev": true - }, "es-module-lexer": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.0.tgz", "integrity": "sha512-2BMfqBDeVCcOlLaL1ZAfp+D868SczNpKArrTM3dhpd7dK/OVlogzY15qpUngt+LMTq5UC/csb9vVQAgupucSbA==", "dev": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true } } }, @@ -31814,6 +31805,11 @@ "globrex": "^0.1.2" } }, + "tinymce": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.3.2.tgz", + "integrity": "sha512-nOVHk4FhHKQj48hi7fEptS1Se6CNzPtfIcDzTO70KoTcSiQIFzhhZjS5bPotSzFnQ4dIQJ4QPOd7sqNs6fXUrA==" + }, "title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 474739d2c9..185b5d156e 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -69,6 +69,7 @@ "lodash-es": "4.17.21", "router-slot": "file:router-slot-1.6.1.tgz", "rxjs": "^7.8.0", + "tinymce": "^6.3.2", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tiny-mce/input-tiny-mce.element.ts index cf248f9bf3..09bc2585fa 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tiny-mce/input-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tiny-mce/input-tiny-mce.element.ts @@ -3,20 +3,24 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { AstNode } from 'tinymce'; +import { AstNode, Editor, EditorEvent, TinyMCE } from 'tinymce'; import { UmbMediaHelper } from '../../property-editors/uis/tiny-mce/media-helper.service'; import { AcePlugin } from '../../property-editors/uis/tiny-mce/plugins/ace.plugin'; import { LinkPickerPlugin } from '../../property-editors/uis/tiny-mce/plugins/linkpicker.plugin'; import { MacroPlugin } from '../../property-editors/uis/tiny-mce/plugins/macro.plugin'; +import { MediaPickerPlugin } from '../../property-editors/uis/tiny-mce/plugins/mediapicker.plugin'; +import { + UmbCurrentUserStore, + UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, +} from '../../../users/current-user/current-user.store'; import { UmbLitElement } from '@umbraco-cms/element'; -import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import type { UserDetails } from '@umbraco-cms/models'; +import { DataTypePropertyModel } from '@umbraco-cms/backend-api'; /// TINY MCE // import 'tinymce'; import '@tinymce/tinymce-webcomponent'; -import { MediaPickerPlugin } from '../../property-editors/uis/tiny-mce/plugins/mediapicker.plugin'; -import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from 'src/backoffice/users/current-user/current-user.store'; -import type { UserDetails } from '@umbraco-cms/models'; // /* Default icons are required. After that, import custom icons if applicable */ // import 'tinymce/icons/default'; @@ -46,8 +50,8 @@ import type { UserDetails } from '@umbraco-cms/models'; declare global { interface Window { - tinyConfig: any; - tinymce: any; + tinyConfig: { [key: string]: string | number | boolean | object | (() => void) }; + tinymce: TinyMCE; Umbraco: any; } } @@ -57,7 +61,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { static styles = [UUITextStyles]; @property() - configuration: Array = []; + configuration: Array = []; @property() private _dimensions?: { [key: string]: number }; @@ -98,7 +102,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { // private _contentStyle: string = contentUiSkinCss.toString() + '\n' + contentCss.toString(); #currentUserStore?: UmbCurrentUserStore; - modalService?: UmbModalService; + modalContext?: UmbModalContext; #mediaHelper = new UmbMediaHelper(); currentUser?: UserDetails; @@ -109,8 +113,8 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { constructor() { super(); - this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => { - this.modalService = instance; + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.modalContext = instance; this.#setTinyConfig(); }); @@ -156,16 +160,16 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { //skin: false, statusbar: false, style_formats: this._styleFormats, - setup: (editor: any) => this.#editorSetup(editor), + setup: (editor: Editor) => this.#editorSetup(editor), }; } - #editorSetup(editor: any) { + #editorSetup(editor: Editor) { // initialise core plugins - new AcePlugin(editor, this.modalService); - new LinkPickerPlugin(editor, this.modalService, this.configuration); - new MacroPlugin(editor, this.modalService); - new MediaPickerPlugin(editor, this.configuration, this.modalService, this.currentUser); + new AcePlugin(editor, this.modalContext); + new LinkPickerPlugin(editor, this.modalContext, this.configuration); + new MacroPlugin(editor, this.modalContext); + new MediaPickerPlugin(editor, this.configuration, this.modalContext, this.currentUser); // register custom option maxImageSize editor.options.register('maxImageSize', { processor: 'number', default: 500 }); @@ -176,10 +180,12 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { // To update the icon to show you can NOT drop something into the editor if (this._toolbar && !this.#isMediaPickerEnabled()) { // Wire up the event listener - editor.on('dragstart dragend dragover draggesture dragdrop drop drag', (e: any) => { + editor.on('dragstart dragend dragover draggesture dragdrop drop drag', (e: EditorEvent) => { e.preventDefault(); - e.dataTransfer.effectAllowed = 'none'; - e.dataTransfer.dropEffect = 'none'; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'none'; + e.dataTransfer.dropEffect = 'none'; + } e.stopPropagation(); }); } @@ -197,33 +203,39 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { editor.on('Dirty', () => this.#onChange(editor.getContent())); editor.on('Keyup', () => this.#onChange(editor.getContent())); editor.on('SetContent', () => this.#uploadBlobImages(editor)); - editor.on('ObjectResized', (e: any) => { - this.#onResize(e); + editor.on('ObjectResized', (e) => { + this.#onResize(e); this.#onChange(editor.getContent()); }); - } - async #onResize(e: any) { + async #onResize( + e: EditorEvent<{ + target: HTMLElement; + width: number; + height: number; + origin: string; + }> + ) { const srcAttr = e.target.getAttribute('src'); - if (!srcAttr) { - return; - } + if (!srcAttr) { + return; + } + + const path = srcAttr.split('?')[0]; + const resizedPath = await this.#mediaHelper.getProcessedImageUrl(path, { + width: e.width, + height: e.height, + mode: 'max', + }); - const path = srcAttr.split('?')[0]; - const resizedPath = await this.#mediaHelper.getProcessedImageUrl(path, { - width: e.width, - height: e.height, - mode: 'max', - }); - e.target.setAttribute('data-mce-src', resizedPath); } - #onInit(editor: any) { + #onInit(editor: Editor) { //enable browser based spell checking - editor.getBody().setAttribute('spellcheck', true); + editor.getBody().setAttribute('spellcheck', 'true'); /** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes: * https://github.com/advisories/GHSA-w7jx-j77m-wp65 @@ -278,20 +290,21 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { })(); if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) { - editor.serializer.addAttributeFilter(uriAttributesToSanitize, (nodes: AstNode[]) => { - nodes.forEach((node: AstNode) => { - node.attributes?.forEach((attr) => { - const attrName = attr.name.toLowerCase(); - if (uriAttributesToSanitize.indexOf(attrName) !== -1) { - attr.value = parseUri(attr.value, node.name) ?? ''; - } + uriAttributesToSanitize.forEach((attribute) => { + editor.serializer.addAttributeFilter(attribute, (nodes: AstNode[]) => { + nodes.forEach((node: AstNode) => { + node.attributes?.forEach((attr) => { + if (uriAttributesToSanitize.includes(attr.name.toLowerCase())) { + attr.value = parseUri(attr.value, node.name) ?? ''; + } + }); }); }); }); } } - async #uploadBlobImages(editor: any) { + async #uploadBlobImages(editor: Editor) { const content = editor.getContent(); // Upload BLOB images (dragged/pasted ones) @@ -300,7 +313,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { if (content.search(/src=["']blob:.*?["']/gi) !== -1) { const data = await editor.uploadImages(); // Once all images have been uploaded - data.forEach((item: any) => { + data.forEach((item) => { // Skip items that failed upload if (item.status === false) { return; @@ -330,7 +343,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { const blobImageWithNoTmpImgAttribute = editor.dom.select('img[src^="blob:"]:not([data-tmpimg])'); //For each of these selected items - blobImageWithNoTmpImgAttribute.forEach((imageElement: any) => { + blobImageWithNoTmpImgAttribute.forEach((imageElement) => { const blobSrcUri = editor.dom.getAttrib(imageElement, 'src'); // Find the same image uploaded (Should be in LocalStorage) @@ -348,7 +361,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) { /** prevent injecting arbitrary JavaScript execution in on-attributes. */ - const allNodes = Array.prototype.slice.call(editor.dom.doc.getElementsByTagName('*')); + const allNodes = Array.from(editor.dom.doc.getElementsByTagName('*')); allNodes.forEach((node) => { for (let i = 0; i < node.attributes.length; i++) { if (node.attributes[i].name.startsWith('on')) { @@ -359,7 +372,6 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { } } - #onChange(value: string) { super.value = value; this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));