enh/refac: note image upload
This commit is contained in:
@@ -84,6 +84,10 @@
|
||||
import { ListKit } from '@tiptap/extension-list';
|
||||
import { Placeholder, CharacterCount } from '@tiptap/extensions';
|
||||
|
||||
import Image from './RichTextInput/Image/index.js';
|
||||
// import TiptapImage from '@tiptap/extension-image';
|
||||
|
||||
import FileHandler from '@tiptap/extension-file-handler';
|
||||
import Typography from '@tiptap/extension-typography';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||
@@ -106,11 +110,15 @@
|
||||
|
||||
export let socket = null;
|
||||
export let user = null;
|
||||
export let files = [];
|
||||
|
||||
export let documentId = '';
|
||||
|
||||
export let className = 'input-prose';
|
||||
export let placeholder = 'Type here...';
|
||||
export let link = false;
|
||||
export let image = false;
|
||||
export let fileHandler = false;
|
||||
|
||||
export let id = '';
|
||||
export let value = '';
|
||||
@@ -819,7 +827,9 @@
|
||||
editor = new Editor({
|
||||
element: element,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
StarterKit.configure({
|
||||
link: link
|
||||
}),
|
||||
Placeholder.configure({ placeholder }),
|
||||
|
||||
CodeBlockLowlight.configure({
|
||||
@@ -838,6 +848,60 @@
|
||||
}),
|
||||
CharacterCount.configure({}),
|
||||
|
||||
...(image ? [Image] : []),
|
||||
...(fileHandler
|
||||
? [
|
||||
FileHandler.configure({
|
||||
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
|
||||
onDrop: (currentEditor, files, pos) => {
|
||||
files.forEach((file) => {
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.readAsDataURL(file);
|
||||
fileReader.onload = () => {
|
||||
currentEditor
|
||||
.chain()
|
||||
.insertContentAt(pos, {
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: fileReader.result
|
||||
}
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
};
|
||||
});
|
||||
},
|
||||
onPaste: (currentEditor, files, htmlContent) => {
|
||||
files.forEach((file) => {
|
||||
if (htmlContent) {
|
||||
// if there is htmlContent, stop manual insertion & let other extensions handle insertion via inputRule
|
||||
// you could extract the pasted file from this url string and upload it to a server for example
|
||||
console.log(htmlContent); // eslint-disable-line no-console
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.readAsDataURL(file);
|
||||
fileReader.onload = () => {
|
||||
currentEditor
|
||||
.chain()
|
||||
.insertContentAt(currentEditor.state.selection.anchor, {
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: fileReader.result
|
||||
}
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
]
|
||||
: []),
|
||||
|
||||
...(autocomplete
|
||||
? [
|
||||
AIAutocompletion.configure({
|
||||
@@ -1093,6 +1157,11 @@
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
onBeforeCreate: ({ editor }) => {
|
||||
if (files) {
|
||||
editor.storage.files = files;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
197
src/lib/components/common/RichTextInput/Image/image.ts
Normal file
197
src/lib/components/common/RichTextInput/Image/image.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
||||
|
||||
export interface ImageOptions {
|
||||
/**
|
||||
* Controls if the image node should be inline or not.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
inline: boolean;
|
||||
|
||||
/**
|
||||
* Controls if base64 images are allowed. Enable this if you want to allow
|
||||
* base64 image urls in the `src` attribute.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
allowBase64: boolean;
|
||||
|
||||
/**
|
||||
* HTML attributes to add to the image element.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
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: {
|
||||
/**
|
||||
* Add an image
|
||||
* @param options The image attributes
|
||||
* @example
|
||||
* editor
|
||||
* .commands
|
||||
* .setImage({ src: 'https://tiptap.dev/logo.png', alt: 'tiptap', title: 'tiptap logo' })
|
||||
*/
|
||||
setImage: (options: SetImageOptions) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches an image to a  on input.
|
||||
*/
|
||||
export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/;
|
||||
|
||||
/**
|
||||
* This extension allows you to insert images.
|
||||
* @see https://www.tiptap.dev/api/nodes/image
|
||||
*/
|
||||
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', node.attrs.src || '');
|
||||
}
|
||||
} 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', node.attrs.src || '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 };
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
5
src/lib/components/common/RichTextInput/Image/index.ts
Normal file
5
src/lib/components/common/RichTextInput/Image/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Image } from './image.js';
|
||||
|
||||
export * from './image.js';
|
||||
|
||||
export default Image;
|
||||
Reference in New Issue
Block a user