Tiptap extension: Embedded Media

This commit is contained in:
leekelleher
2024-09-19 17:04:23 +01:00
parent 4fb029de8e
commit e1121364d4
7 changed files with 127 additions and 16 deletions

View File

@@ -0,0 +1,54 @@
import { mergeAttributes, Node } from '@tiptap/core';
export const umbEmbeddedMedia = Node.create({
name: 'umbEmbeddedMedia',
group() {
return this.options.inline ? 'inline' : 'block';
},
inline() {
return this.options.inline;
},
atom: true,
marks: '',
draggable: true,
selectable: true,
addAttributes() {
return {
'data-embed-constrain': { default: false },
'data-embed-height': { default: 240 },
'data-embed-url': { default: null },
'data-embed-width': { default: 360 },
markup: { default: null },
};
},
parseHTML() {
return [{ tag: 'div', class: 'umb-embed-holder', getAttrs: (node) => ({ markup: node.innerHTML }) }];
},
renderHTML({ HTMLAttributes }) {
const { markup, ...attrs } = HTMLAttributes;
const embed = document.createRange().createContextualFragment(markup);
return ['div', mergeAttributes({ class: 'umb-embed-holder' }, attrs), embed];
},
addCommands() {
return {
setEmbeddedMedia:
(options) =>
({ commands }) => {
const attrs = { markup: options.markup, 'data-embed-url': options.url };
return commands.insertContent({ type: this.name, attrs });
},
};
},
});
declare module '@tiptap/core' {
interface Commands<ReturnType> {
umbEmbeddedMedia: {
setEmbeddedMedia: (options: { markup: string; url: string }) => ReturnType;
};
}
}

View File

@@ -24,4 +24,5 @@ export { TextAlign } from '@tiptap/extension-text-align';
export { Underline } from '@tiptap/extension-underline';
export { Image } from '@tiptap/extension-image';
// CUSTOM EXTENSIONS
export * from './extensions/tiptap-umb-embedded-media.extension.js';
export * from './extensions/tiptap-umb-image.extension.js';

View File

@@ -1,8 +1,9 @@
import { UmbOEmbedRepository } from '../repository/oembed.repository.js';
import type { UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue } from './embedded-media-modal.token.js';
import { css, html, unsafeHTML, when, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { umbFocus } from '@umbraco-cms/backoffice/lit-element';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UUIButtonState, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-embedded-media-modal')
@@ -27,6 +28,7 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement<
override 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.value = { ...this.value, constrain: this.data.constrain };
@@ -81,7 +83,7 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement<
<uui-box>
<umb-property-layout label=${this.localize.term('general_url')} orientation="vertical">
<div slot="editor">
<uui-input id="url" .value=${this._url} @input=${this.#onUrlChange} required="true">
<uui-input id="url" .value=${this._url} @input=${this.#onUrlChange} required="true" ${umbFocus()}>
<uui-button
slot="append"
look="primary"

View File

@@ -180,6 +180,32 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
margin-top: 0;
margin-bottom: 0.5em;
}
.umb-embed-holder {
display: inline-block;
position: relative;
}
.umb-embed-holder > * {
user-select: none;
pointer-events: none;
}
.umb-embed-holder.ProseMirror-selectednode {
outline: 2px solid var(--uui-palette-spanish-pink-light);
}
.umb-embed-holder::before {
z-index: 1000;
width: 100%;
height: 100%;
position: absolute;
content: ' ';
}
.umb-embed-holder.ProseMirror-selectednode::before {
background: rgba(0, 0, 0, 0.025);
}
}
`,
];

View File

@@ -14,6 +14,7 @@ const kinds: Array<UmbExtensionManifestKind> = [
},
];
// TODO: [LK] Move each of these to their corresponding packages, e.g. "code-editor", "embedded-media", "media", "multi-url-picker"
const umbExtensions: Array<ManifestTiptapExtension | ManifestTiptapExtensionButtonKind> = [
{
type: 'tiptapExtension',
@@ -33,11 +34,11 @@ const umbExtensions: Array<ManifestTiptapExtension | ManifestTiptapExtensionButt
kind: 'button',
alias: 'Umb.Tiptap.Embed',
name: 'Embed Tiptap Extension',
api: () => import('./umb/embed.extension.js'),
api: () => import('./umb/embedded-media.extension.js'),
meta: {
alias: 'umb-embed',
alias: 'umb-embedded-media',
icon: 'icon-embed',
label: 'Embed',
label: '#general_embed',
},
},
{

View File

@@ -1,11 +0,0 @@
import { UmbTiptapToolbarElementApiBase } from '../types.js';
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
export default class UmbTiptapEmbedExtensionApi extends UmbTiptapToolbarElementApiBase {
getTiptapExtensions = () => [];
override async execute(editor?: Editor) {
console.log('umb-embed.execute', editor);
// Research: https://github.com/ueberdosis/tiptap/tree/main/packages/extension-youtube
}
}

View File

@@ -0,0 +1,38 @@
import { UmbTiptapToolbarElementApiBase } from '../types.js';
import { umbEmbeddedMedia } from '@umbraco-cms/backoffice/external/tiptap';
import { UMB_EMBEDDED_MEDIA_MODAL } from '@umbraco-cms/backoffice/embedded-media';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
export default class UmbTiptapEmbedExtensionApi extends UmbTiptapToolbarElementApiBase {
getTiptapExtensions = () => [umbEmbeddedMedia.configure({ inline: true })];
override isActive = (editor: Editor) => editor.isActive(umbEmbeddedMedia.name) === true;
override async execute(editor?: Editor) {
const data = {
constrain: false,
height: 240,
width: 360,
url: '',
};
const attrs = editor?.getAttributes(umbEmbeddedMedia.name);
if (attrs) {
data.constrain = attrs['data-embed-constrain'];
data.height = attrs['data-embed-height'];
data.width = attrs['data-embed-width'];
data.url = attrs['data-embed-url'];
}
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalHandler = modalManager.open(this, UMB_EMBEDDED_MEDIA_MODAL, { data });
if (!modalHandler) return;
const result = await modalHandler.onSubmit().catch(() => undefined);
if (!result) return;
editor?.commands.setEmbeddedMedia(result);
}
}