From 2bb600c8c6dc2732ecf88f7c8291c0d94ee62880 Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:13:23 +0300 Subject: [PATCH] Universal Figure with caption (#361) Figure with caption for images and embed --- src/components/Editor/Editor.tsx | 38 ++++++------- .../EditorFloatingMenu/EditorFloatingMenu.tsx | 25 ++++++++- src/components/Editor/Prosemirror.scss | 22 +++++++- src/components/Editor/SimplifiedEditor.tsx | 18 ++---- .../UploadModalContent.module.scss | 1 - src/components/Editor/extensions/Figure.ts | 21 ++++++- .../Editor/extensions/{Embed.ts => Iframe.ts} | 55 ++++++++++--------- src/components/Views/Feed/Feed.module.scss | 4 +- src/styles/app.scss | 5 +- src/utils/renderUploadedImage.ts | 18 ++---- 10 files changed, 121 insertions(+), 86 deletions(-) rename src/components/Editor/extensions/{Embed.ts => Iframe.ts} (51%) diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 5720c807..18882032 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -42,10 +42,10 @@ import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './Bubbl import { EditorFloatingMenu } from './EditorFloatingMenu' import Article from './extensions/Article' import { CustomBlockquote } from './extensions/CustomBlockquote' -import { Embed } from './extensions/Embed' import { Figcaption } from './extensions/Figcaption' import { Figure } from './extensions/Figure' import { Footnote } from './extensions/Footnote' +import { Iframe } from './extensions/Iframe' import { TrailingNode } from './extensions/TrailingNode' import { TextBubbleMenu } from './TextBubbleMenu' @@ -130,11 +130,6 @@ export const Editor = (props: Props) => { current: null, } - const ImageFigure = Figure.extend({ - name: 'capturedImage', - content: 'figcaption image', - }) - const handleClipboardPaste = async () => { try { const clipboardItems = await navigator.clipboard.read() @@ -163,22 +158,16 @@ export const Editor = (props: Props) => { .chain() .focus() .insertContent({ - type: 'capturedImage', + type: 'figure', + attrs: { 'data-type': 'image' }, content: [ { - type: 'figcaption', - content: [ - { - type: 'text', - text: result.originalFilename, - }, - ], + type: 'image', + attrs: { src: result.url }, }, { - type: 'image', - attrs: { - src: result.url, - }, + type: 'figcaption', + content: [{ type: 'text', text: result.originalFilename }], }, ], }) @@ -250,11 +239,11 @@ export const Editor = (props: Props) => { class: 'highlight', }, }), - ImageFigure, Image, + Iframe, + Figure, Figcaption, Footnote, - Embed, CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689 BubbleMenu.configure({ pluginKey: 'textBubbleMenu', @@ -265,8 +254,13 @@ export const Editor = (props: Props) => { const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection) setIsCommonMarkup(e.isActive('figcaption')) const result = - (view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')) || - e.isActive('footnote') + (view.hasFocus() && + !empty && + !isEmptyTextBlock && + !e.isActive('image') && + !e.isActive('figure')) || + e.isActive('footnote') || + e.isActive('figcaption') setShouldShowTextBubbleMenu(result) return result }, diff --git a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx index de7a2e65..a348a430 100644 --- a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx +++ b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx @@ -26,7 +26,7 @@ const embedData = async (data) => { const element = document.createRange().createContextualFragment(data) const { attributes } = element.firstChild as HTMLIFrameElement - const result: { src: string } = { src: '' } + const result: { src: string; width?: string; height?: string } = { src: '' } for (let i = 0; i < attributes.length; i++) { const attribute = attributes[i] @@ -45,7 +45,28 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => { const handleEmbedFormSubmit = async (value: string) => { // TODO: add support instagram embed (blockquote) const emb = await embedData(value) - props.editor.chain().focus().setIframe(emb).run() + props.editor + .chain() + .focus() + .insertContent({ + type: 'figure', + attrs: { 'data-type': 'iframe' }, + content: [ + { + type: 'iframe', + attrs: { + src: emb.src, + width: emb.width, + height: emb.height, + }, + }, + { + type: 'figcaption', + content: [{ type: 'text', text: t('Description') }], + }, + ], + }) + .run() } const validateEmbed = async (value) => { diff --git a/src/components/Editor/Prosemirror.scss b/src/components/Editor/Prosemirror.scss index 3529cb48..d84e01da 100644 --- a/src/components/Editor/Prosemirror.scss +++ b/src/components/Editor/Prosemirror.scss @@ -265,8 +265,26 @@ mark.highlight { } } -figure[data-type='capturedImage'] { - flex-direction: column-reverse; +.ProseMirror-hideselection figure[data-type='figure'] { + & > figcaption { + --selection-color: rgb(0 0 0 / 60%); + } +} + +figure[data-type='figure'] { + width: 100% !important; + + .iframe-wrapper { + position: relative; + overflow: hidden; + width: 100%; + height: auto; + + iframe { + display: block; + width: 100%; + } + } } /* stylelint-disable-next-line selector-type-no-unknown */ diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index 7d4783c4..a4aa4d7b 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -185,22 +185,16 @@ const SimplifiedEditor = (props: Props) => { .chain() .focus() .insertContent({ - type: 'capturedImage', + type: 'figure', + attrs: { 'data-type': 'image' }, content: [ { - type: 'figcaption', - content: [ - { - type: 'text', - text: image.originalFilename, - }, - ], + type: 'image', + attrs: { src: image.url }, }, { - type: 'image', - attrs: { - src: image.url, - }, + type: 'figcaption', + content: [{ type: 'text', text: image.originalFilename }], }, ], }) diff --git a/src/components/Editor/UploadModalContent/UploadModalContent.module.scss b/src/components/Editor/UploadModalContent/UploadModalContent.module.scss index c8cbb0ee..f34ec66b 100644 --- a/src/components/Editor/UploadModalContent/UploadModalContent.module.scss +++ b/src/components/Editor/UploadModalContent/UploadModalContent.module.scss @@ -81,7 +81,6 @@ .formHolder { width: 100%; margin-top: 24px; - border-bottom: 1px solid #000; } } diff --git a/src/components/Editor/extensions/Figure.ts b/src/components/Editor/extensions/Figure.ts index 1a252b18..c67bebc2 100644 --- a/src/components/Editor/extensions/Figure.ts +++ b/src/components/Editor/extensions/Figure.ts @@ -16,24 +16,39 @@ export const Figure = Node.create({ } }, group: 'block', - content: 'block figcaption', + content: '(image | iframe) figcaption', draggable: true, isolating: true, + atom: true, addAttributes() { return { 'data-float': null, + 'data-type': { default: null }, } }, parseHTML() { return [ { - tag: `figure[data-type="${this.name}"]`, + tag: 'figure', + getAttrs: (node) => { + if (!(node instanceof HTMLElement)) { + return + } + const img = node.querySelector('img') + const iframe = node.querySelector('iframe') + let dataType = null + if (img) { + dataType = 'image' + } else if (iframe) { + dataType = 'iframe' + } + return { 'data-type': dataType } + }, }, ] }, - renderHTML({ HTMLAttributes }) { return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0] }, diff --git a/src/components/Editor/extensions/Embed.ts b/src/components/Editor/extensions/Iframe.ts similarity index 51% rename from src/components/Editor/extensions/Embed.ts rename to src/components/Editor/extensions/Iframe.ts index ce842a26..63ebab2f 100644 --- a/src/components/Editor/extensions/Embed.ts +++ b/src/components/Editor/extensions/Iframe.ts @@ -1,4 +1,4 @@ -import { mergeAttributes, Node } from '@tiptap/core' +import { Node } from '@tiptap/core' export interface IframeOptions { allowFullscreen: boolean @@ -15,19 +15,35 @@ declare module '@tiptap/core' { } } -export const Embed = Node.create({ - name: 'embed', +export const Iframe = Node.create({ + name: 'iframe', group: 'block', - selectable: true, atom: true, - draggable: true, - addAttributes() { + + addOptions() { return { - src: { default: null }, - width: { default: null }, - height: { default: null }, + allowFullscreen: true, + HTMLAttributes: { + class: 'iframe-wrapper', + }, } }, + + addAttributes() { + return { + src: { + default: null, + }, + frameborder: { + default: 0, + }, + allowfullscreen: { + default: this.options.allowFullscreen, + parseHTML: () => this.options.allowFullscreen, + }, + } + }, + parseHTML() { return [ { @@ -35,28 +51,15 @@ export const Embed = Node.create({ }, ] }, + renderHTML({ HTMLAttributes }) { - return ['iframe', mergeAttributes(HTMLAttributes)] - }, - addNodeView() { - return ({ node }) => { - const div = document.createElement('div') - div.className = 'embed-wrapper' - const iframe = document.createElement('iframe') - iframe.width = node.attrs.width - iframe.height = node.attrs.height - iframe.allowFullscreen = node.attrs.allowFullscreen - iframe.src = node.attrs.src - div.append(iframe) - return { - dom: div, - } - } + return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]] }, + addCommands() { return { setIframe: - (options) => + (options: { src: string }) => ({ tr, dispatch }) => { const { selection } = tr const node = this.type.create(options) diff --git a/src/components/Views/Feed/Feed.module.scss b/src/components/Views/Feed/Feed.module.scss index f2905bc2..27a3854b 100644 --- a/src/components/Views/Feed/Feed.module.scss +++ b/src/components/Views/Feed/Feed.module.scss @@ -197,8 +197,8 @@ margin-bottom: 4rem; @include media-breakpoint-down(sm) { - flex-direction: column-reverse; - align-items: flex-start; + margin: 1rem 0 0; + flex-direction: column; gap: 1rem; } diff --git a/src/styles/app.scss b/src/styles/app.scss index ef3ea022..2d3a7ac2 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -582,8 +582,8 @@ figure { display: flex; flex-direction: column; width: fit-content; - gap: 16px; margin: 2em auto; + gap: 0.6rem; img { display: block; @@ -596,11 +596,8 @@ figure { figure { figcaption { color: rgb(0 0 0 / 60%); - @include font-size(1.2rem); - line-height: 1.5; - margin-top: 0.5em; } } diff --git a/src/utils/renderUploadedImage.ts b/src/utils/renderUploadedImage.ts index a6b2ac78..dfadf611 100644 --- a/src/utils/renderUploadedImage.ts +++ b/src/utils/renderUploadedImage.ts @@ -8,22 +8,16 @@ export const renderUploadedImage = (editor: Editor, image: UploadedFile) => { .chain() .focus() .insertContent({ - type: 'capturedImage', + type: 'figure', + attrs: { 'data-type': 'image' }, content: [ { - type: 'figcaption', - content: [ - { - type: 'text', - text: image.originalFilename ?? '', - }, - ], + type: 'image', + attrs: { src: image.url }, }, { - type: 'image', - attrs: { - src: image.url, - }, + type: 'figcaption', + content: [{ type: 'text', text: image.originalFilename }], }, ], })