Feature: oEmbed implementation
This commit is contained in:
@@ -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: `<iframe width="${width}" height="${height}" src="https://www.youtube.com/embed/wJNbtYdr-Hg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Sleep Token - The Summoning"></iframe>`,
|
||||
oEmbedStatus: OEmbedStatus.Success,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
|
||||
@@ -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<
|
||||
<uui-box>
|
||||
<umb-property-layout label="URL" orientation="vertical">
|
||||
<div slot="editor">
|
||||
<uui-input .value=${this._model.url} type="text" @change=${this.#onUrlChange} required="true">
|
||||
<uui-input .value=${this._url} type="text" @input=${this.#onUrlChange} required="true">
|
||||
<uui-button
|
||||
slot="append"
|
||||
look="primary"
|
||||
color="positive"
|
||||
@click=${this.#getPreview}
|
||||
?disabled=${!this._model.url}
|
||||
label="Retrieve"></uui-button>
|
||||
</uui-input>
|
||||
</div>
|
||||
</umb-property-layout>
|
||||
|
||||
${when(
|
||||
this.#embedResult?.oEmbedStatus === OEmbedStatus.Success || this._model.a11yInfo,
|
||||
this._loading !== undefined,
|
||||
() =>
|
||||
html` <umb-property-layout label="Preview" orientation="vertical">
|
||||
<div slot="editor">
|
||||
${when(this.#loading, () => html`<uui-loader-circle></uui-loader-circle>`)}
|
||||
${when(this.#embedResult?.markup, () => html`${unsafeHTML(this.#embedResult.markup)}`)}
|
||||
${when(this._model.info, () => html` <p aria-hidden="true">${this._model.info}</p>`)}
|
||||
${when(
|
||||
this._model.a11yInfo,
|
||||
() => html` <p class="sr-only" role="alert">${this._model.a11yInfo}</p>`,
|
||||
)}
|
||||
${when(this._loading === 'waiting', () => html`<uui-loader-circle></uui-loader-circle>`)}
|
||||
${when(this.value?.markup, () => html`${unsafeHTML(this.value.markup)}`)}
|
||||
</div>
|
||||
</umb-property-layout>`,
|
||||
)}
|
||||
|
||||
<umb-property-layout label="Width" orientation="vertical">
|
||||
<uui-input
|
||||
slot="editor"
|
||||
.value=${this._model.width}
|
||||
type="number"
|
||||
?disabled=${this.#dimensionControlsDisabled()}
|
||||
@change=${this.#onWidthChange}></uui-input>
|
||||
<umb-property-layout label="Max width" orientation="vertical">
|
||||
<uui-input slot="editor" .value=${this._width} type="number" @change=${this.#onWidthChange}></uui-input>
|
||||
</umb-property-layout>
|
||||
|
||||
<umb-property-layout label="Height" orientation="vertical">
|
||||
<uui-input
|
||||
slot="editor"
|
||||
.value=${this._model.height}
|
||||
type="number"
|
||||
?disabled=${this.#dimensionControlsDisabled()}
|
||||
@change=${this.#onHeightChange}></uui-input>
|
||||
<umb-property-layout label="Max height" orientation="vertical">
|
||||
<uui-input slot="editor" .value=${this._height} type="number" @change=${this.#onHeightChange}></uui-input>
|
||||
</umb-property-layout>
|
||||
|
||||
<umb-property-layout label="Constrain" orientation="vertical">
|
||||
<uui-toggle
|
||||
slot="editor"
|
||||
@change=${this.#onConstrainChange}
|
||||
?disabled=${this.#dimensionControlsDisabled()}
|
||||
.checked=${this._model.constrain}></uui-toggle>
|
||||
<uui-toggle slot="editor" @change=${this.#onConstrainChange} .checked=${this._constrain}></uui-toggle>
|
||||
</umb-property-layout>
|
||||
</uui-box>
|
||||
|
||||
<uui-button slot="actions" id="cancel" label="Cancel" @click=${this.#handleCancel}>Cancel</uui-button>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
id="cancel"
|
||||
label=${this.localize.term('buttons_confirmActionCancel')}
|
||||
@click=${() => this.modalContext?.reject()}></uui-button>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
id="submit"
|
||||
color="positive"
|
||||
look="primary"
|
||||
label="Submit"
|
||||
@click=${this.#handleConfirm}></uui-button>
|
||||
label=${this.localize.term('buttons_confirmActionConfirm')}
|
||||
@click=${() => this.modalContext?.submit()}></uui-button>
|
||||
</umb-body-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './repository/index.js';
|
||||
@@ -0,0 +1,13 @@
|
||||
import { manifests as repositories } from './repository/manifests.js';
|
||||
import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
const modals: Array<ManifestModal> = [
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.EmbeddedMedia',
|
||||
name: 'Embedded Media Modal',
|
||||
element: () => import('./embedded-media-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests = [...modals, ...repositories];
|
||||
@@ -0,0 +1,2 @@
|
||||
export { UmbDocumentCultureAndHostnamesRepository } from './oembed.repository.js';
|
||||
export { UMB_DOCUMENT_CULTURE_AND_HOSTNAMES_REPOSITORY_ALIAS } from './manifests.js';
|
||||
@@ -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<ManifestTypes> = [repository];
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './embedded-media/index.js';
|
||||
@@ -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<UmbEmbeddedMediaDimensionsModel> {
|
||||
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<UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue>(
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user