From e1121364d47319a69acee58e32ccb1c7eddf4e6a Mon Sep 17 00:00:00 2001 From: leekelleher Date: Thu, 19 Sep 2024 17:04:23 +0100 Subject: [PATCH] Tiptap extension: Embedded Media --- .../tiptap-umb-embedded-media.extension.ts | 54 +++++++++++++++++++ .../src/external/tiptap/index.ts | 1 + .../modal/embedded-media-modal.element.ts | 6 ++- .../input-tiptap/input-tiptap.element.ts | 26 +++++++++ .../rte/tiptap/extensions/manifests.ts | 7 +-- .../tiptap/extensions/umb/embed.extension.ts | 11 ---- .../umb/embedded-media.extension.ts | 38 +++++++++++++ 7 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embed.extension.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embedded-media.extension.ts diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts new file mode 100644 index 0000000000..bcef5ee6eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts @@ -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 { + umbEmbeddedMedia: { + setEmbeddedMedia: (options: { markup: string; url: string }) => ReturnType; + }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts index 5c6fabf333..6f9245ee78 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts index fcf5ff01a6..62f8468c17 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts @@ -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<
- + * { + 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); + } } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts index cb135c4773..827e80d2fa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts @@ -14,6 +14,7 @@ const kinds: Array = [ }, ]; +// TODO: [LK] Move each of these to their corresponding packages, e.g. "code-editor", "embedded-media", "media", "multi-url-picker" const umbExtensions: Array = [ { type: 'tiptapExtension', @@ -33,11 +34,11 @@ const umbExtensions: Array 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', }, }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embed.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embed.extension.ts deleted file mode 100644 index f09ecfd084..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embed.extension.ts +++ /dev/null @@ -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 - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embedded-media.extension.ts new file mode 100644 index 0000000000..ccd8ee60e0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/umb/embedded-media.extension.ts @@ -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); + } +}