Universal Figure with caption (#361)

Figure with caption for images and embed
This commit is contained in:
Ilya Y 2024-01-16 12:13:23 +03:00 committed by GitHub
parent ec5e55b10b
commit 2bb600c8c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 121 additions and 86 deletions

View File

@ -42,10 +42,10 @@ import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './Bubbl
import { EditorFloatingMenu } from './EditorFloatingMenu' import { EditorFloatingMenu } from './EditorFloatingMenu'
import Article from './extensions/Article' import Article from './extensions/Article'
import { CustomBlockquote } from './extensions/CustomBlockquote' import { CustomBlockquote } from './extensions/CustomBlockquote'
import { Embed } from './extensions/Embed'
import { Figcaption } from './extensions/Figcaption' import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure' import { Figure } from './extensions/Figure'
import { Footnote } from './extensions/Footnote' import { Footnote } from './extensions/Footnote'
import { Iframe } from './extensions/Iframe'
import { TrailingNode } from './extensions/TrailingNode' import { TrailingNode } from './extensions/TrailingNode'
import { TextBubbleMenu } from './TextBubbleMenu' import { TextBubbleMenu } from './TextBubbleMenu'
@ -130,11 +130,6 @@ export const Editor = (props: Props) => {
current: null, current: null,
} }
const ImageFigure = Figure.extend({
name: 'capturedImage',
content: 'figcaption image',
})
const handleClipboardPaste = async () => { const handleClipboardPaste = async () => {
try { try {
const clipboardItems = await navigator.clipboard.read() const clipboardItems = await navigator.clipboard.read()
@ -163,22 +158,16 @@ export const Editor = (props: Props) => {
.chain() .chain()
.focus() .focus()
.insertContent({ .insertContent({
type: 'capturedImage', type: 'figure',
attrs: { 'data-type': 'image' },
content: [ content: [
{
type: 'figcaption',
content: [
{
type: 'text',
text: result.originalFilename,
},
],
},
{ {
type: 'image', type: 'image',
attrs: { attrs: { src: result.url },
src: result.url,
}, },
{
type: 'figcaption',
content: [{ type: 'text', text: result.originalFilename }],
}, },
], ],
}) })
@ -250,11 +239,11 @@ export const Editor = (props: Props) => {
class: 'highlight', class: 'highlight',
}, },
}), }),
ImageFigure,
Image, Image,
Iframe,
Figure,
Figcaption, Figcaption,
Footnote, Footnote,
Embed,
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689 CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
BubbleMenu.configure({ BubbleMenu.configure({
pluginKey: 'textBubbleMenu', pluginKey: 'textBubbleMenu',
@ -265,8 +254,13 @@ export const Editor = (props: Props) => {
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection) const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
setIsCommonMarkup(e.isActive('figcaption')) setIsCommonMarkup(e.isActive('figcaption'))
const result = const result =
(view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')) || (view.hasFocus() &&
e.isActive('footnote') !empty &&
!isEmptyTextBlock &&
!e.isActive('image') &&
!e.isActive('figure')) ||
e.isActive('footnote') ||
e.isActive('figcaption')
setShouldShowTextBubbleMenu(result) setShouldShowTextBubbleMenu(result)
return result return result
}, },

View File

@ -26,7 +26,7 @@ const embedData = async (data) => {
const element = document.createRange().createContextualFragment(data) const element = document.createRange().createContextualFragment(data)
const { attributes } = element.firstChild as HTMLIFrameElement 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++) { for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i] const attribute = attributes[i]
@ -45,7 +45,28 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
const handleEmbedFormSubmit = async (value: string) => { const handleEmbedFormSubmit = async (value: string) => {
// TODO: add support instagram embed (blockquote) // TODO: add support instagram embed (blockquote)
const emb = await embedData(value) 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) => { const validateEmbed = async (value) => {

View File

@ -265,8 +265,26 @@ mark.highlight {
} }
} }
figure[data-type='capturedImage'] { .ProseMirror-hideselection figure[data-type='figure'] {
flex-direction: column-reverse; & > 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 */ /* stylelint-disable-next-line selector-type-no-unknown */

View File

@ -185,22 +185,16 @@ const SimplifiedEditor = (props: Props) => {
.chain() .chain()
.focus() .focus()
.insertContent({ .insertContent({
type: 'capturedImage', type: 'figure',
attrs: { 'data-type': 'image' },
content: [ content: [
{
type: 'figcaption',
content: [
{
type: 'text',
text: image.originalFilename,
},
],
},
{ {
type: 'image', type: 'image',
attrs: { attrs: { src: image.url },
src: image.url,
}, },
{
type: 'figcaption',
content: [{ type: 'text', text: image.originalFilename }],
}, },
], ],
}) })

View File

@ -81,7 +81,6 @@
.formHolder { .formHolder {
width: 100%; width: 100%;
margin-top: 24px; margin-top: 24px;
border-bottom: 1px solid #000;
} }
} }

View File

@ -16,24 +16,39 @@ export const Figure = Node.create({
} }
}, },
group: 'block', group: 'block',
content: 'block figcaption', content: '(image | iframe) figcaption',
draggable: true, draggable: true,
isolating: true, isolating: true,
atom: true,
addAttributes() { addAttributes() {
return { return {
'data-float': null, 'data-float': null,
'data-type': { default: null },
} }
}, },
parseHTML() { parseHTML() {
return [ 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 }) { renderHTML({ HTMLAttributes }) {
return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0] return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0]
}, },

View File

@ -1,4 +1,4 @@
import { mergeAttributes, Node } from '@tiptap/core' import { Node } from '@tiptap/core'
export interface IframeOptions { export interface IframeOptions {
allowFullscreen: boolean allowFullscreen: boolean
@ -15,19 +15,35 @@ declare module '@tiptap/core' {
} }
} }
export const Embed = Node.create<IframeOptions>({ export const Iframe = Node.create<IframeOptions>({
name: 'embed', name: 'iframe',
group: 'block', group: 'block',
selectable: true,
atom: true, atom: true,
draggable: true,
addAttributes() { addOptions() {
return { return {
src: { default: null }, allowFullscreen: true,
width: { default: null }, HTMLAttributes: {
height: { default: null }, class: 'iframe-wrapper',
},
} }
}, },
addAttributes() {
return {
src: {
default: null,
},
frameborder: {
default: 0,
},
allowfullscreen: {
default: this.options.allowFullscreen,
parseHTML: () => this.options.allowFullscreen,
},
}
},
parseHTML() { parseHTML() {
return [ return [
{ {
@ -35,28 +51,15 @@ export const Embed = Node.create<IframeOptions>({
}, },
] ]
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['iframe', mergeAttributes(HTMLAttributes)] return ['div', this.options.HTMLAttributes, ['iframe', 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,
}
}
}, },
addCommands() { addCommands() {
return { return {
setIframe: setIframe:
(options) => (options: { src: string }) =>
({ tr, dispatch }) => { ({ tr, dispatch }) => {
const { selection } = tr const { selection } = tr
const node = this.type.create(options) const node = this.type.create(options)

View File

@ -197,8 +197,8 @@
margin-bottom: 4rem; margin-bottom: 4rem;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
flex-direction: column-reverse; margin: 1rem 0 0;
align-items: flex-start; flex-direction: column;
gap: 1rem; gap: 1rem;
} }

View File

@ -582,8 +582,8 @@ figure {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: fit-content; width: fit-content;
gap: 16px;
margin: 2em auto; margin: 2em auto;
gap: 0.6rem;
img { img {
display: block; display: block;
@ -596,11 +596,8 @@ figure {
figure { figure {
figcaption { figcaption {
color: rgb(0 0 0 / 60%); color: rgb(0 0 0 / 60%);
@include font-size(1.2rem); @include font-size(1.2rem);
line-height: 1.5; line-height: 1.5;
margin-top: 0.5em;
} }
} }

View File

@ -8,22 +8,16 @@ export const renderUploadedImage = (editor: Editor, image: UploadedFile) => {
.chain() .chain()
.focus() .focus()
.insertContent({ .insertContent({
type: 'capturedImage', type: 'figure',
attrs: { 'data-type': 'image' },
content: [ content: [
{
type: 'figcaption',
content: [
{
type: 'text',
text: image.originalFilename ?? '',
},
],
},
{ {
type: 'image', type: 'image',
attrs: { attrs: { src: image.url },
src: image.url,
}, },
{
type: 'figcaption',
content: [{ type: 'text', text: image.originalFilename }],
}, },
], ],
}) })