2023-05-07 13:16:03 +00:00
|
|
|
import { findChildrenInRange, mergeAttributes, Node, nodeInputRule, Tracker } from '@tiptap/core'
|
|
|
|
|
|
|
|
export interface FigureOptions {
|
|
|
|
HTMLAttributes: Record<string, any>
|
|
|
|
}
|
|
|
|
|
|
|
|
declare module '@tiptap/core' {
|
|
|
|
interface Commands<ReturnType> {
|
|
|
|
figure: {
|
|
|
|
/**
|
|
|
|
* Add a figure element
|
|
|
|
*/
|
|
|
|
setFigure: (options: { src: string; alt?: string; title?: string; caption?: string }) => ReturnType
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts an image to a figure
|
|
|
|
*/
|
|
|
|
imageToFigure: () => ReturnType
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a figure to an image
|
|
|
|
*/
|
|
|
|
figureToImage: () => ReturnType
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const inputRegex = /!\[(.+|:?)]\((\S+)(?:\s+["'](\S+)["'])?\)/
|
|
|
|
|
|
|
|
export const Figure = Node.create<FigureOptions>({
|
|
|
|
name: 'figure',
|
|
|
|
|
|
|
|
addOptions() {
|
|
|
|
return {
|
|
|
|
HTMLAttributes: {}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
group: 'block',
|
|
|
|
|
|
|
|
content: 'inline*',
|
|
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
|
|
isolating: true,
|
|
|
|
|
|
|
|
addAttributes() {
|
|
|
|
return {
|
|
|
|
src: {
|
|
|
|
default: null,
|
|
|
|
parseHTML: (element) => element.querySelector('img')?.getAttribute('src')
|
|
|
|
},
|
|
|
|
|
|
|
|
alt: {
|
|
|
|
default: null,
|
|
|
|
parseHTML: (element) => element.querySelector('img')?.getAttribute('alt')
|
|
|
|
},
|
|
|
|
|
|
|
|
title: {
|
|
|
|
default: null,
|
|
|
|
parseHTML: (element) => element.querySelector('img')?.getAttribute('title')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
parseHTML() {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
tag: 'figure',
|
|
|
|
contentElement: 'figcaption'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
|
|
return [
|
|
|
|
'figure',
|
|
|
|
this.options.HTMLAttributes,
|
|
|
|
['img', mergeAttributes(HTMLAttributes, { draggable: false, contenteditable: false })],
|
|
|
|
['figcaption', 0]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
|
|
|
|
addCommands() {
|
|
|
|
return {
|
|
|
|
setFigure:
|
|
|
|
({ caption, ...attrs }) =>
|
|
|
|
({ chain }) => {
|
|
|
|
return (
|
|
|
|
chain()
|
|
|
|
.insertContent({
|
|
|
|
type: this.name,
|
|
|
|
attrs,
|
|
|
|
content: caption ? [{ type: 'text', text: caption }] : []
|
|
|
|
})
|
|
|
|
// set cursor at end of caption field
|
|
|
|
.command(({ tr, commands }) => {
|
|
|
|
const { doc, selection } = tr
|
|
|
|
const position = doc.resolve(selection.to - 2).end()
|
|
|
|
|
|
|
|
return commands.setTextSelection(position)
|
|
|
|
})
|
|
|
|
.run()
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
imageToFigure:
|
|
|
|
() =>
|
2023-05-07 13:47:10 +00:00
|
|
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
2023-05-07 13:16:03 +00:00
|
|
|
({ tr, commands }) => {
|
|
|
|
const { doc, selection } = tr
|
|
|
|
const { from, to } = selection
|
|
|
|
const images = findChildrenInRange(doc, { from, to }, (node) => node.type.name === 'image')
|
|
|
|
|
2023-05-07 13:47:10 +00:00
|
|
|
if (images.length === 0) {
|
2023-05-07 13:16:03 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const tracker = new Tracker(tr)
|
|
|
|
|
|
|
|
return commands.forEach(images, ({ node, pos }) => {
|
|
|
|
const mapResult = tracker.map(pos)
|
|
|
|
|
|
|
|
if (mapResult.deleted) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const range = {
|
|
|
|
from: mapResult.position,
|
|
|
|
to: mapResult.position + node.nodeSize
|
|
|
|
}
|
|
|
|
|
|
|
|
return commands.insertContentAt(range, {
|
|
|
|
type: this.name,
|
|
|
|
attrs: {
|
|
|
|
src: node.attrs.src
|
2023-05-07 15:32:28 +00:00
|
|
|
},
|
|
|
|
content: [{ type: 'text', text: node.attrs.src }]
|
2023-05-07 13:16:03 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
figureToImage:
|
|
|
|
() =>
|
2023-05-07 13:47:10 +00:00
|
|
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
2023-05-07 13:16:03 +00:00
|
|
|
({ tr, commands }) => {
|
|
|
|
const { doc, selection } = tr
|
|
|
|
const { from, to } = selection
|
|
|
|
const figures = findChildrenInRange(doc, { from, to }, (node) => node.type.name === this.name)
|
|
|
|
|
2023-05-07 13:47:10 +00:00
|
|
|
if (figures.length === 0) {
|
2023-05-07 13:16:03 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const tracker = new Tracker(tr)
|
|
|
|
|
|
|
|
return commands.forEach(figures, ({ node, pos }) => {
|
|
|
|
const mapResult = tracker.map(pos)
|
|
|
|
|
|
|
|
if (mapResult.deleted) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const range = {
|
|
|
|
from: mapResult.position,
|
|
|
|
to: mapResult.position + node.nodeSize
|
|
|
|
}
|
|
|
|
|
|
|
|
return commands.insertContentAt(range, {
|
|
|
|
type: 'image',
|
|
|
|
attrs: {
|
|
|
|
src: node.attrs.src
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
addInputRules() {
|
|
|
|
return [
|
|
|
|
nodeInputRule({
|
|
|
|
find: inputRegex,
|
|
|
|
type: this.type,
|
|
|
|
getAttributes: (match) => {
|
|
|
|
const [, src, alt, title] = match
|
|
|
|
|
|
|
|
return { src, alt, title }
|
|
|
|
}
|
|
|
|
})
|
|
|
|
]
|
|
|
|
}
|
|
|
|
})
|