| | import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core'; |
| |
|
| | export interface ImageOptions { |
| | |
| | |
| | |
| | |
| | |
| | inline: boolean; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | allowBase64: boolean; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | HTMLAttributes: Record<string, any>; |
| | } |
| |
|
| | export interface SetImageOptions { |
| | src: string; |
| | alt?: string; |
| | title?: string; |
| | width?: number; |
| | height?: number; |
| | } |
| |
|
| | declare module '@tiptap/core' { |
| | interface Commands<ReturnType> { |
| | image: { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | setImage: (options: SetImageOptions) => ReturnType; |
| | }; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/; |
| |
|
| | |
| | |
| | |
| | |
| | export const Image = Node.create<ImageOptions>({ |
| | name: 'image', |
| |
|
| | addOptions() { |
| | return { |
| | inline: false, |
| | allowBase64: false, |
| | HTMLAttributes: {} |
| | }; |
| | }, |
| |
|
| | inline() { |
| | return this.options.inline; |
| | }, |
| |
|
| | group() { |
| | return this.options.inline ? 'inline' : 'block'; |
| | }, |
| |
|
| | draggable: true, |
| |
|
| | addAttributes() { |
| | return { |
| | file: { |
| | default: null |
| | }, |
| | src: { |
| | default: null |
| | }, |
| | alt: { |
| | default: null |
| | }, |
| | title: { |
| | default: null |
| | }, |
| | width: { |
| | default: null |
| | }, |
| | height: { |
| | default: null |
| | } |
| | }; |
| | }, |
| |
|
| | parseHTML() { |
| | return [ |
| | { |
| | tag: this.options.allowBase64 ? 'img[src]' : 'img[src]:not([src^="data:"])' |
| | } |
| | ]; |
| | }, |
| |
|
| | renderHTML({ HTMLAttributes }) { |
| | if (HTMLAttributes.file) { |
| | delete HTMLAttributes.file; |
| | } |
| |
|
| | return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; |
| | }, |
| |
|
| | addNodeView() { |
| | return ({ node, editor }) => { |
| | const domImg = document.createElement('img'); |
| | domImg.setAttribute('src', node.attrs.src || ''); |
| | domImg.setAttribute('alt', node.attrs.alt || ''); |
| | domImg.setAttribute('title', node.attrs.title || ''); |
| |
|
| | const container = document.createElement('div'); |
| | const img = document.createElement('img'); |
| |
|
| | const fileId = node.attrs.src.replace('data://', ''); |
| | img.setAttribute('id', `image:${fileId}`); |
| |
|
| | img.classList.add('rounded-md', 'max-h-72', 'w-fit', 'object-contain'); |
| |
|
| | const editorFiles = editor.storage?.files || []; |
| |
|
| | if (editorFiles && node.attrs.src.startsWith('data://')) { |
| | const file = editorFiles.find((f) => f.id === fileId); |
| | if (file) { |
| | img.setAttribute('src', file.url || ''); |
| | } else { |
| | img.setAttribute('src', '/image-placeholder.png'); |
| | } |
| | } else { |
| | img.setAttribute('src', node.attrs.src || ''); |
| | } |
| |
|
| | img.setAttribute('alt', node.attrs.alt || ''); |
| | img.setAttribute('title', node.attrs.title || ''); |
| |
|
| | img.addEventListener('data', (e) => { |
| | const files = e?.files || []; |
| | if (files && node.attrs.src.startsWith('data://')) { |
| | const file = editorFiles.find((f) => f.id === fileId); |
| | if (file) { |
| | img.setAttribute('src', file.url || ''); |
| | } else { |
| | img.setAttribute('src', '/image-placeholder.png'); |
| | } |
| | } |
| | }); |
| |
|
| | container.append(img); |
| | return { |
| | dom: img, |
| | contentDOM: domImg |
| | }; |
| | }; |
| | }, |
| |
|
| | addCommands() { |
| | return { |
| | setImage: |
| | (options) => |
| | ({ commands }) => { |
| | return commands.insertContent({ |
| | type: this.name, |
| | attrs: options |
| | }); |
| | } |
| | }; |
| | }, |
| |
|
| | addInputRules() { |
| | return [ |
| | nodeInputRule({ |
| | find: inputRegex, |
| | type: this.type, |
| | getAttributes: (match) => { |
| | const [, , alt, src, title] = match; |
| |
|
| | return { src, alt, title }; |
| | } |
| | }) |
| | ]; |
| | } |
| | }); |
| |
|