diff --git a/src/Umbraco.Web.UI.Client/libs/utils/index.ts b/src/Umbraco.Web.UI.Client/libs/utils/index.ts index b7439f0c3a..b59f166a62 100644 --- a/src/Umbraco.Web.UI.Client/libs/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/utils/index.ts @@ -1,3 +1,4 @@ export * from './utils'; export * from './umbraco-path'; export * from './udi-service'; +export * from './media-helper.service'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/media-helper.service.ts b/src/Umbraco.Web.UI.Client/libs/utils/media-helper.service.ts similarity index 70% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/media-helper.service.ts rename to src/Umbraco.Web.UI.Client/libs/utils/media-helper.service.ts index c4b7727a0c..1ac97dba8d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/media-helper.service.ts +++ b/src/Umbraco.Web.UI.Client/libs/utils/media-helper.service.ts @@ -1,18 +1,10 @@ -// TODO => very much temporary. Need to determine where to provide share services like this one, which -// will be useful for downstream implementors... +// TODO => this is NOT a full reimplementation of the existing media helper service, currently +// contains only functions referenced by the TinyMCE editor import { Editor, EditorEvent } from "tinymce"; -import { UmbElementMixinInterface } from "@umbraco-cms/backoffice/element"; -import { UmbLitElement } from "@umbraco-cms/internal/lit-element"; -export class UmbMediaHelper { +export class UmbMediaHelper { - #host: UmbLitElement | UmbElementMixinInterface; - - constructor(host: UmbLitElement | UmbElementMixinInterface) { - this.#host = host; - } - /** * * @param editor @@ -198,67 +190,4 @@ export class UmbMediaHelper { e.target.setAttribute('data-mce-src', resizedPath); } - - /** - * - * @param blobInfo - * @param progress - * @returns - */ - uploadImageHandler(blobInfo: any, progress: any) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('POST', window.Umbraco?.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage'); - - xhr.onloadstart = () => this.#host.dispatchEvent(new CustomEvent('rte.file.uploading')); - - xhr.onloadend = () => this.#host.dispatchEvent(new CustomEvent('rte.file.uploaded')); - - xhr.upload.onprogress = (e) => progress((e.loaded / e.total) * 100); - - xhr.onerror = () => reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); - - xhr.onload = () => { - if (xhr.status < 200 || xhr.status >= 300) { - reject('HTTP Error: ' + xhr.status); - return; - } - - // TODO => confirm this is required given no more Angular handling XHR/HTTP - const data = xhr.responseText.split('\n'); - - if (data.length <= 1) { - reject('Unrecognized text string: ' + data); - return; - } - - let json: { [key: string]: string } = {}; - - try { - json = JSON.parse(data[1]); - } catch (e: any) { - reject('Invalid JSON: ' + data + ' - ' + e.message); - return; - } - - if (!json || typeof json.tmpLocation !== 'string') { - reject('Invalid JSON: ' + data); - return; - } - - // Put temp location into localstorage (used to update the img with data-tmpimg later on) - localStorage.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); - - // We set the img src url to be the same as we started - // The Blob URI is stored in TinyMce's cache - // so the img still shows in the editor - resolve(blobInfo.blobUri()); - }; - - const formData = new FormData(); - formData.append('file', blobInfo.blob(), blobInfo.blob().name); - - xhr.send(formData); - }); - } } diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 578829263e..7985ad8f49 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@tinymce/tinymce-webcomponent": "^2.0.1", "@umbraco-ui/uui": "^1.2.0-rc.0", "@umbraco-ui/uui-css": "^1.2.0-rc.0", "@umbraco-ui/uui-modal": "file:umbraco-ui-uui-modal-0.0.0.tgz", @@ -4748,11 +4747,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@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==" - }, "node_modules/@types/accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", @@ -21406,11 +21400,6 @@ "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", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index ae3981ecb6..b9df6254f6 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -60,7 +60,6 @@ "npm": ">=9.5 < 10" }, "dependencies": { - "@tinymce/tinymce-webcomponent": "^2.0.1", "@umbraco-ui/uui": "^1.2.0-rc.0", "@umbraco-ui/uui-css": "^1.2.0-rc.0", "@umbraco-ui/uui-modal": "file:umbraco-ui-uui-modal-0.0.0.tgz", 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 e0340c4da5..737e3f4c7c 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 @@ -5,18 +5,22 @@ import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { AstNode, Editor, EditorEvent } from 'tinymce'; import { firstValueFrom } from 'rxjs'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; -import type { UserDetails } from '@umbraco-cms/backoffice/models'; -import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { UmbMediaHelper } from '../../property-editors/uis/tiny-mce/media-helper.service'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, } from '../../../users/current-user/current-user.store'; import { TinyMcePluginArguments } from '../../property-editors/uis/tiny-mce/plugins/tiny-mce-plugin'; +import { UmbMediaHelper } from '@umbraco-cms/backoffice/utils'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; +import type { UserDetails } from '@umbraco-cms/backoffice/models'; +import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { ManifestTinyMcePlugin } from 'libs/extensions-registry/tinymce-plugin.model'; + +// TODO => determine optimal method for including tiny. Currently using public assets +// as we need to ship all core plugins to allow implementors to register these. Have not considered +// other locations for serving these assests - might make better sense in /libs import '../../../../../public-assets/tiny-mce/tinymce.min.js'; declare global { @@ -58,9 +62,10 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { @property() configuration: Array = []; - // TODO => type this + // TODO => create interface when we know what shape that will take + // TinyMCE provides the EditorOptions interface, but all props are required @state() - private _configObject: any = {}; + private _configObject = {}; private _styleFormats = [ { @@ -198,7 +203,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { #currentUserStore?: UmbCurrentUserStore; modalContext!: UmbModalContext; - #mediaHelper = new UmbMediaHelper(this); + #mediaHelper = new UmbMediaHelper(); currentUser?: UserDetails; protected getFormElement() { @@ -290,7 +295,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { if (this.#isMediaPickerEnabled()) { Object.assign(tinyConfig, { // Update the TinyMCE Config object to allow pasting - images_upload_handler: this.#mediaHelper.uploadImageHandler, + images_upload_handler: this.#uploadImageHandler, automatic_uploads: false, images_replace_blob_uris: false, // This allows images to be pasted in & stored as Base64 until they get uploaded to server @@ -309,6 +314,64 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { // convert i to em args.content = args.content.replace(/<\s*i([^>]*)>(.*?)<\s*\/\s*i([^>]*)>/g, '$2'); } + + // TODO => arg types + #uploadImageHandler(blobInfo: any, progress: any) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', window.Umbraco?.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage'); + + xhr.onloadstart = () => this.dispatchEvent(new CustomEvent('rte.file.uploading')); + + xhr.onloadend = () => this.dispatchEvent(new CustomEvent('rte.file.uploaded')); + + xhr.upload.onprogress = (e) => progress((e.loaded / e.total) * 100); + + xhr.onerror = () => reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); + + xhr.onload = () => { + if (xhr.status < 200 || xhr.status >= 300) { + reject('HTTP Error: ' + xhr.status); + return; + } + + // TODO => confirm this is required given no more Angular handling XHR/HTTP + const data = xhr.responseText.split('\n'); + + if (data.length <= 1) { + reject('Unrecognized text string: ' + data); + return; + } + + let json: { [key: string]: string } = {}; + + try { + json = JSON.parse(data[1]); + } catch (e: any) { + reject('Invalid JSON: ' + data + ' - ' + e.message); + return; + } + + if (!json || typeof json.tmpLocation !== 'string') { + reject('Invalid JSON: ' + data); + return; + } + + // Put temp location into localstorage (used to update the img with data-tmpimg later on) + localStorage.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); + + // We set the img src url to be the same as we started + // The Blob URI is stored in TinyMce's cache + // so the img still shows in the editor + resolve(blobInfo.blobUri()); + }; + + const formData = new FormData(); + formData.append('file', blobInfo.blob(), blobInfo.blob().name); + + xhr.send(formData); + }); + } /** * Returns the language to use for TinyMCE */ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts index 940b050746..66088f1efb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/manifests.ts @@ -36,7 +36,7 @@ const modals: Array = [ alias: 'Umb.Modal.CodeEditor', name: 'Code Editor Modal', loader: () => import('./code-editor/code-editor-modal.element'), - } + }, { type: 'modal', alias: 'Umb.Modal.EmbeddedMedia', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index 3227daad4d..89585a5ec2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -2,7 +2,7 @@ import type { UserDetails } from '@umbraco-cms/backoffice/models'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; import { UmbMediaPickerModalResult, UMB_MEDIA_PICKER_MODAL_TOKEN } from '../../../../../media/media/modals/media-picker'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from '../../../../../users/current-user/current-user.store'; -import { UmbMediaHelper } from '../media-helper.service'; +import { UmbMediaHelper } from '../../../../../../../libs/utils/media-helper.service'; import { TinyMcePluginArguments, TinyMcePluginBase } from './tiny-mce-plugin'; interface MediaPickerTargetData { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts index 04e09e4782..3f447e3f5e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts @@ -24,6 +24,7 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements #onChange(event: InputEvent) { this.value = (event.target as HTMLInputElement).value; + console.log(this.value); this.dispatchEvent(new CustomEvent('property-value-change')); }