Universal Figure with caption (#361)
Figure with caption for images and embed
This commit is contained in:
parent
ec5e55b10b
commit
2bb600c8c6
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -81,7 +81,6 @@
|
||||||
.formHolder {
|
.formHolder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
border-bottom: 1px solid #000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user