From 823a42629a42c7fa01080bc7971d3113653e54ba Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 2 May 2024 23:57:51 +0200 Subject: [PATCH] Feature: oEmbed implementation --- .../src/mocks/handlers/rte-embed.handlers.ts | 9 +- .../embedded-media-modal.element.ts | 205 ++++++------------ .../core/modal/common/embedded-media/index.ts | 1 + .../modal/common/embedded-media/manifests.ts | 13 ++ .../common/embedded-media/repository/index.ts | 2 + .../embedded-media/repository/manifests.ts | 13 ++ .../repository/oembed.repository.ts | 20 ++ .../repository/oembed.server.data.ts | 32 +++ .../src/packages/core/modal/common/index.ts | 1 + .../modal/token/embedded-media-modal.token.ts | 27 +-- .../plugins/tiny-mce-embeddedmedia.plugin.ts | 7 +- 11 files changed, 161 insertions(+), 169 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.server.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/modal/common/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts index 16719a60b6..592d1c6435 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts @@ -1,6 +1,5 @@ const { rest } = window.MockServiceWorker; -import type { OEmbedResult} from '@umbraco-cms/backoffice/modal'; -import { OEmbedStatus } from '@umbraco-cms/backoffice/modal'; +import type { OEmbedResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ @@ -11,12 +10,8 @@ export const handlers = [ const heightParam = req.url.searchParams.get('height'); const height = heightParam ? parseInt(heightParam) : 240; - const response: OEmbedResult = { - supportsDimensions: true, + const response: OEmbedResponseModel = { markup: ``, - oEmbedStatus: OEmbedStatus.Success, - width, - height, }; return res(ctx.status(200), ctx.json(response)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts index a0177a7f7a..7011b6044d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts @@ -1,127 +1,81 @@ -import { css, html, unsafeHTML, when, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbOEmbedRepository } from './repository/oembed.repository.js'; +import { css, html, unsafeHTML, when, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { - OEmbedResult, - UmbEmbeddedMediaModalData, - UmbEmbeddedMediaModalValue} from '@umbraco-cms/backoffice/modal'; -import { - OEmbedStatus, - UmbModalBaseElement, -} from '@umbraco-cms/backoffice/modal'; -import { umbracoPath } from '@umbraco-cms/backoffice/utils'; - -interface UmbEmbeddedMediaModalModel { - url?: string; - info?: string; - a11yInfo?: string; - originalWidth: number; - originalHeight: number; - width: number; - height: number; - constrain: boolean; -} +import type { UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue } from '@umbraco-cms/backoffice/modal'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import type { UUIButtonState, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-embedded-media-modal') export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue > { - #loading = false; - #embedResult!: OEmbedResult; - - #handleConfirm() { - this.value = { - preview: this.#embedResult.markup, - originalWidth: this._model.width, - originalHeight: this._model.originalHeight, - width: this.#embedResult.width, - height: this.#embedResult.height, - }; - this.modalContext?.submit(); - } - - #handleCancel() { - this.modalContext?.reject(); - } + #oEmbedRepository = new UmbOEmbedRepository(this); @state() - private _model: UmbEmbeddedMediaModalModel = { - url: '', - width: 360, - height: 240, - constrain: true, - info: '', - a11yInfo: '', - originalHeight: 240, - originalWidth: 360, - }; + private _loading?: UUIButtonState; + + @state() + private _width = 360; + + @state() + private _height = 240; + + @state() + private _url = ''; + + @state() + private _constrain = false; connectedCallback() { super.connectedCallback(); + if (this.data?.width) this._width = this.data.width; + if (this.data?.height) this._height = this.data.height; + if (this.data?.constrain) this._constrain = this.data.constrain; + if (this.data?.url) { - Object.assign(this._model, this.data); + this._url = this.data.url; this.#getPreview(); } } async #getPreview() { - this._model.info = ''; - this._model.a11yInfo = ''; + this._loading = 'waiting'; - this.#loading = true; - this.requestUpdate('_model'); + const { data } = await this.#oEmbedRepository.requestOEmbed({ + url: this._url, + maxWidth: this._width, + maxHeight: this._height, + }); - try { - // TODO => use backend cli when available - const result = await fetch( - umbracoPath('/rteembed?') + - new URLSearchParams({ - url: this._model.url, - width: this._model.width?.toString(), - height: this._model.height?.toString(), - } as { [key: string]: string }), - ); - - this.#embedResult = await result.json(); - - switch (this.#embedResult.oEmbedStatus) { - case 0: - this.#onPreviewFailed('Not supported'); - break; - case 1: - this.#onPreviewFailed('Could not embed media - please ensure the URL is valid'); - break; - case 2: - this._model.info = ''; - this._model.a11yInfo = 'Retrieved URL'; - break; - } - } catch (e) { - this.#onPreviewFailed('Could not embed media - please ensure the URL is valid'); + if (!data) { + this._loading = 'failed'; + return; } - this.#loading = false; - this.requestUpdate('_model'); + this.value = { ...this.value, markup: data.markup, url: this._url }; + this._loading = 'success'; } - #onPreviewFailed(message: string) { - this._model.info = message; - this._model.a11yInfo = message; + #onUrlChange(e: UUIInputEvent) { + this._url = e.target.value as string; } - #onUrlChange(e: InputEvent) { - this._model.url = (e.target as HTMLInputElement).value; - this.requestUpdate('_model'); + #onWidthChange(e: UUIInputEvent) { + this._width = parseInt(e.target.value as string, 10); + //this.#getPreview(); + //this.#changeSize('width'); } - #onWidthChange(e: InputEvent) { - this._model.width = parseInt((e.target as HTMLInputElement).value, 10); - this.#changeSize('width'); + #onHeightChange(e: UUIInputEvent) { + this._height = parseInt(e.target.value as string, 10); + //this.#getPreview(); + //this.#changeSize('height'); } - #onHeightChange(e: InputEvent) { - this._model.height = parseInt((e.target as HTMLInputElement).value, 10); - this.#changeSize('height'); + #onConstrainChange() { + this._constrain = !this._constrain; + this.value = { ...this.value, constrain: this._constrain }; } /** @@ -129,6 +83,7 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< * If constrain is false, axis change independently * @param axis {string} */ + /* #changeSize(axis: 'width' | 'height') { const resize = this._model.originalWidth !== this._model.width || this._model.originalHeight !== this._model.height; @@ -147,19 +102,7 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< this.#getPreview(); } } - - #onConstrainChange() { - this._model.constrain = !this._model.constrain; - } - - /** - * If the embed does not support dimensions, or was not requested successfully - * the width, height and constrain controls are disabled - * @returns {boolean} - */ - #dimensionControlsDisabled() { - return !this.#embedResult?.supportsDimensions || this.#embedResult?.oEmbedStatus !== OEmbedStatus.Success; - } + */ render() { return html` @@ -167,69 +110,53 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement<
- +
${when( - this.#embedResult?.oEmbedStatus === OEmbedStatus.Success || this._model.a11yInfo, + this._loading !== undefined, () => html`
- ${when(this.#loading, () => html``)} - ${when(this.#embedResult?.markup, () => html`${unsafeHTML(this.#embedResult.markup)}`)} - ${when(this._model.info, () => html` `)} - ${when( - this._model.a11yInfo, - () => html` `, - )} + ${when(this._loading === 'waiting', () => html``)} + ${when(this.value?.markup, () => html`${unsafeHTML(this.value.markup)}`)}
`, )} - - + + - - + + - +
- Cancel + this.modalContext?.reject()}> + label=${this.localize.term('buttons_confirmActionConfirm')} + @click=${() => this.modalContext?.submit()}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/index.ts new file mode 100644 index 0000000000..3d76f338dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/index.ts @@ -0,0 +1 @@ +export * from './repository/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/manifests.ts new file mode 100644 index 0000000000..9cf9968724 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/manifests.ts @@ -0,0 +1,13 @@ +import { manifests as repositories } from './repository/manifests.js'; +import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.EmbeddedMedia', + name: 'Embedded Media Modal', + element: () => import('./embedded-media-modal.element.js'), + }, +]; + +export const manifests = [...modals, ...repositories]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/index.ts new file mode 100644 index 0000000000..c3f58d36ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/index.ts @@ -0,0 +1,2 @@ +export { UmbDocumentCultureAndHostnamesRepository } from './oembed.repository.js'; +export { UMB_DOCUMENT_CULTURE_AND_HOSTNAMES_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/manifests.ts new file mode 100644 index 0000000000..580f3e1026 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/manifests.ts @@ -0,0 +1,13 @@ +import { UmbOEmbedRepository } from './oembed.repository.js'; +import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_OEMBED_REPOSITORY_ALIAS = 'Umb.Repository.OEmbed'; + +const repository: ManifestRepository = { + type: 'repository', + alias: UMB_OEMBED_REPOSITORY_ALIAS, + name: 'OEmbed Repository', + api: UmbOEmbedRepository, +}; + +export const manifests: Array = [repository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.repository.ts new file mode 100644 index 0000000000..344c067453 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.repository.ts @@ -0,0 +1,20 @@ +import { UmbOEmbedServerDataSource } from './oembed.server.data.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; + +export class UmbOEmbedRepository extends UmbControllerBase implements UmbApi { + #dataSource = new UmbOEmbedServerDataSource(this); + + constructor(host: UmbControllerHost) { + super(host); + } + + async requestOEmbed({ url, maxWidth, maxHeight }: { url?: string; maxWidth?: number; maxHeight?: number }) { + const { data, error } = await this.#dataSource.getOEmbedQuery({ url, maxWidth, maxHeight }); + if (!error) { + return { data }; + } + return { error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.server.data.ts new file mode 100644 index 0000000000..0f59a79736 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.server.data.ts @@ -0,0 +1,32 @@ +import { DocumentService, OEmbedService } from '@umbraco-cms/backoffice/external/backend-api'; +import type { OembedData, UpdateDomainsRequestModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * A data source for the OEmbed that fetches data from a given URL. + * @export + * @class UmbOEmbedServerDataSource + * @implements {RepositoryDetailDataSource} + */ +export class UmbOEmbedServerDataSource { + #host: UmbControllerHost; + + /** + * Creates an instance of UmbOEmbedServerDataSource. + * @param {UmbControllerHost} host + * @memberof UmbOEmbedServerDataSource + */ + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Fetches markup for the given URL. + * @param {string} unique + * @memberof UmbOEmbedServerDataSource + */ + async getOEmbedQuery({ url, maxWidth, maxHeight }: { url?: string; maxWidth?: number; maxHeight?: number }) { + return tryExecuteAndNotify(this.#host, OEmbedService.getOembedQuery({ url, maxWidth, maxHeight })); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/index.ts new file mode 100644 index 0000000000..5a1c0033a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/index.ts @@ -0,0 +1 @@ +export * from './embedded-media/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts index 7eca2cf44c..0fb7df92ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts @@ -1,31 +1,18 @@ import { UmbModalToken } from './modal-token.js'; -export enum OEmbedStatus { - NotSupported, - Error, - Success, -} - -export interface UmbEmbeddedMediaDimensions { - width: number; - height: number; - constrain?: boolean; -} - -export interface UmbEmbeddedMediaModalData extends UmbEmbeddedMediaDimensions { +export interface UmbEmbeddedMediaModalData extends Partial { url?: string; } -export interface OEmbedResult extends UmbEmbeddedMediaDimensions { - oEmbedStatus: OEmbedStatus; - supportsDimensions: boolean; - markup?: string; +export interface UmbEmbeddedMediaDimensionsModel { + constrain: boolean; + width: number; + height: number; } export interface UmbEmbeddedMediaModalValue extends UmbEmbeddedMediaModalData { - preview?: string; - originalWidth: number; - originalHeight: number; + markup: string; + url: string; } export const UMB_EMBEDDED_MEDIA_MODAL = new UmbModalToken( diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts index 7d389d2281..df36043308 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts @@ -44,17 +44,18 @@ export default class UmbTinyMceEmbeddedMediaPlugin extends UmbTinyMcePluginBase #insertInEditor(embed: UmbEmbeddedMediaModalValue, activeElement: HTMLElement) { // Wrap HTML preview content here in a DIV with non-editable class of .mceNonEditable // This turns it into a selectable/cutable block to move about + const wrapper = this.editor.dom.create( 'div', { class: 'mceNonEditable umb-embed-holder', 'data-embed-url': embed.url ?? '', - 'data-embed-height': embed.height, - 'data-embed-width': embed.width, + 'data-embed-height': embed.height!, + 'data-embed-width': embed.width!, 'data-embed-constrain': embed.constrain ?? false, contenteditable: false, }, - embed.preview, + embed.markup, ); // Only replace if activeElement is an Embed element.