@finema/core 3.8.1 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/components/Form/InputWYSIWYG/EditorImagePasteExtension.d.ts +16 -0
- package/dist/runtime/components/Form/InputWYSIWYG/EditorImagePasteExtension.js +106 -0
- package/dist/runtime/components/Form/InputWYSIWYG/README.md +96 -0
- package/dist/runtime/components/Form/InputWYSIWYG/index.vue +2 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
3
|
+
export interface ImagePasteOptions {
|
|
4
|
+
requestOptions?: Omit<AxiosRequestConfig, 'baseURL'> & {
|
|
5
|
+
baseURL: string;
|
|
6
|
+
};
|
|
7
|
+
uploadPathURL?: string;
|
|
8
|
+
bodyKey?: string;
|
|
9
|
+
responseURL?: string;
|
|
10
|
+
responsePath?: string;
|
|
11
|
+
responseName?: string;
|
|
12
|
+
responseSize?: string;
|
|
13
|
+
responseID?: string;
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const ImagePaste: Extension<ImagePasteOptions, any>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
3
|
+
import { useUploadLoader } from "#core/composables/useUpload";
|
|
4
|
+
import { _get } from "#core/utils/lodash";
|
|
5
|
+
export const ImagePaste = Extension.create({
|
|
6
|
+
name: "imagePaste",
|
|
7
|
+
addOptions() {
|
|
8
|
+
return {
|
|
9
|
+
bodyKey: "file",
|
|
10
|
+
responseURL: "url",
|
|
11
|
+
responsePath: "path",
|
|
12
|
+
responseName: "name",
|
|
13
|
+
responseSize: "size",
|
|
14
|
+
responseID: "id"
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
addProseMirrorPlugins() {
|
|
18
|
+
const options = this.options;
|
|
19
|
+
return [
|
|
20
|
+
new Plugin({
|
|
21
|
+
key: new PluginKey("imagePaste"),
|
|
22
|
+
props: {
|
|
23
|
+
handlePaste: async (view, event) => {
|
|
24
|
+
const items = Array.from(event.clipboardData?.items || []);
|
|
25
|
+
const imageItems = items.filter((item) => item.type.includes("image"));
|
|
26
|
+
if (imageItems.length === 0) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
for (const item of imageItems) {
|
|
31
|
+
const file = item.getAsFile();
|
|
32
|
+
if (!file) continue;
|
|
33
|
+
if (options.maxSize && file.size > options.maxSize * 1024) {
|
|
34
|
+
console.warn(`Image size (${file.size} bytes) exceeds maximum allowed size (${options.maxSize} KB)`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!options.requestOptions) {
|
|
38
|
+
console.warn("ImagePaste: requestOptions is not configured");
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const {
|
|
42
|
+
schema
|
|
43
|
+
} = view.state;
|
|
44
|
+
const pos = view.state.selection.from;
|
|
45
|
+
if (schema.nodes.imageUpload) {
|
|
46
|
+
const transaction = view.state.tr.insert(pos, schema.nodes.imageUpload.create());
|
|
47
|
+
view.dispatch(transaction);
|
|
48
|
+
}
|
|
49
|
+
const request = {
|
|
50
|
+
requestOptions: options.requestOptions,
|
|
51
|
+
pathURL: options.uploadPathURL
|
|
52
|
+
};
|
|
53
|
+
const uploadLoader = useUploadLoader(request);
|
|
54
|
+
const formData = new FormData();
|
|
55
|
+
const bodyKey = options.bodyKey || "file";
|
|
56
|
+
formData.append(bodyKey, file);
|
|
57
|
+
await uploadLoader.run({
|
|
58
|
+
data: formData
|
|
59
|
+
});
|
|
60
|
+
if (uploadLoader.status.value.isSuccess && uploadLoader.data.value) {
|
|
61
|
+
const responseURL = options.responseURL || "url";
|
|
62
|
+
const url = _get(uploadLoader.data.value, responseURL);
|
|
63
|
+
if (!url) {
|
|
64
|
+
console.error("ImagePaste: Could not find URL in response", uploadLoader.data.value);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const currentState = view.state;
|
|
68
|
+
let placeholderPos = -1;
|
|
69
|
+
currentState.doc.descendants((node, nodePos) => {
|
|
70
|
+
if (node.type.name === "imageUpload" && placeholderPos === -1) {
|
|
71
|
+
placeholderPos = nodePos;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (placeholderPos !== -1 && schema.nodes.image) {
|
|
76
|
+
const imageNode = schema.nodes.image.create({
|
|
77
|
+
src: url
|
|
78
|
+
});
|
|
79
|
+
let tr = currentState.tr.delete(placeholderPos, placeholderPos + 1).insert(placeholderPos, imageNode);
|
|
80
|
+
const resolvedPos = tr.doc.resolve(placeholderPos + imageNode.nodeSize);
|
|
81
|
+
tr = tr.setSelection(TextSelection.near(resolvedPos));
|
|
82
|
+
view.dispatch(tr);
|
|
83
|
+
}
|
|
84
|
+
} else if (uploadLoader.status.value.isError) {
|
|
85
|
+
console.error("ImagePaste: Upload failed", uploadLoader.status.value.errorData);
|
|
86
|
+
const currentState = view.state;
|
|
87
|
+
let placeholderPos = -1;
|
|
88
|
+
currentState.doc.descendants((node, nodePos) => {
|
|
89
|
+
if (node.type.name === "imageUpload" && placeholderPos === -1) {
|
|
90
|
+
placeholderPos = nodePos;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (placeholderPos !== -1) {
|
|
95
|
+
const tr = currentState.tr.delete(placeholderPos, placeholderPos + 1);
|
|
96
|
+
view.dispatch(tr);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# InputWYSIWYG - TipTap WYSIWYG Editor
|
|
2
|
+
|
|
3
|
+
A rich text editor component built with TipTap that supports text formatting, images, and more.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Text Formatting**: Bold, italic, underline, strikethrough, code
|
|
8
|
+
- **Headings**: H1, H2, H3, H4
|
|
9
|
+
- **Lists**: Bullet lists and ordered lists
|
|
10
|
+
- **Text Alignment**: Left, center, right, justify
|
|
11
|
+
- **Blockquotes and Code Blocks**
|
|
12
|
+
- **Links**: Add and edit links with a popover interface
|
|
13
|
+
- **Image Upload**: Upload images via button click
|
|
14
|
+
- **Image Paste**: ✨ **NEW** - Copy and paste images directly from your computer
|
|
15
|
+
- **Horizontal Rules**
|
|
16
|
+
- **Undo/Redo**
|
|
17
|
+
|
|
18
|
+
## Image Paste Feature
|
|
19
|
+
|
|
20
|
+
The editor now supports pasting images directly from your clipboard! This works in two ways:
|
|
21
|
+
|
|
22
|
+
### 1. Copy from File System
|
|
23
|
+
- Copy an image file from your file explorer/finder
|
|
24
|
+
- Click into the editor
|
|
25
|
+
- Press `Cmd+V` (Mac) or `Ctrl+V` (Windows/Linux)
|
|
26
|
+
- The image will automatically upload and insert into the editor
|
|
27
|
+
|
|
28
|
+
### 2. Copy from Screenshot
|
|
29
|
+
- Take a screenshot (or copy an image from any application)
|
|
30
|
+
- Click into the editor
|
|
31
|
+
- Press `Cmd+V` (Mac) or `Ctrl+V` (Windows/Linux)
|
|
32
|
+
- The image will automatically upload and insert into the editor
|
|
33
|
+
|
|
34
|
+
### How It Works
|
|
35
|
+
|
|
36
|
+
The `ImagePaste` extension intercepts paste events and:
|
|
37
|
+
1. Detects if the clipboard contains image data
|
|
38
|
+
2. Validates the image size (if `maxSize` is configured)
|
|
39
|
+
3. Inserts a placeholder/loading node
|
|
40
|
+
4. Uploads the image to your configured endpoint
|
|
41
|
+
5. Replaces the placeholder with the actual image once uploaded
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```vue
|
|
46
|
+
<template>
|
|
47
|
+
<InputWYSIWYG
|
|
48
|
+
name="content"
|
|
49
|
+
label="Content"
|
|
50
|
+
:image="{
|
|
51
|
+
requestOptions: useRequestOptions().getFile(),
|
|
52
|
+
uploadPathURL: '/uploads',
|
|
53
|
+
maxSize: 5120, // 5MB in KB
|
|
54
|
+
bodyKey: 'file',
|
|
55
|
+
responseURL: 'url',
|
|
56
|
+
responsePath: 'path',
|
|
57
|
+
responseName: 'name',
|
|
58
|
+
responseSize: 'size',
|
|
59
|
+
responseID: 'id',
|
|
60
|
+
}"
|
|
61
|
+
/>
|
|
62
|
+
</template>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
### Image Upload Options
|
|
68
|
+
|
|
69
|
+
| Option | Type | Default | Description |
|
|
70
|
+
|--------|------|---------|-------------|
|
|
71
|
+
| `requestOptions` | `AxiosRequestConfig` | - | Axios configuration for upload requests |
|
|
72
|
+
| `uploadPathURL` | `string` | - | API endpoint path for uploads |
|
|
73
|
+
| `bodyKey` | `string` | `'file'` | Form data key for the file |
|
|
74
|
+
| `responseURL` | `string` | `'url'` | Path to URL in upload response |
|
|
75
|
+
| `responsePath` | `string` | `'path'` | Path to file path in response |
|
|
76
|
+
| `responseName` | `string` | `'name'` | Path to file name in response |
|
|
77
|
+
| `responseSize` | `string` | `'size'` | Path to file size in response |
|
|
78
|
+
| `responseID` | `string` | `'id'` | Path to file ID in response |
|
|
79
|
+
| `maxSize` | `number` | - | Maximum file size in KB |
|
|
80
|
+
|
|
81
|
+
## Extensions
|
|
82
|
+
|
|
83
|
+
The component uses the following TipTap extensions:
|
|
84
|
+
|
|
85
|
+
- **TextAlign**: For text alignment options
|
|
86
|
+
- **ImageUpload**: Custom extension for manual image uploads via button
|
|
87
|
+
- **ImagePaste**: Custom extension for pasting images from clipboard
|
|
88
|
+
|
|
89
|
+
## Files
|
|
90
|
+
|
|
91
|
+
- `index.vue` - Main component
|
|
92
|
+
- `EditorImageUploadExtension.ts` - Extension for manual image upload
|
|
93
|
+
- `EditorImageUploadNode.vue` - Vue component for image upload UI
|
|
94
|
+
- `EditorImagePasteExtension.ts` - Extension for paste image functionality
|
|
95
|
+
- `EditorLinkPopover.vue` - Link editing popover
|
|
96
|
+
- `types.ts` - TypeScript type definitions
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
types: ['heading', 'paragraph'],
|
|
15
15
|
alignments: ['left', 'center', 'right', 'justify']
|
|
16
16
|
}),
|
|
17
|
-
ImageUpload.configure(image)
|
|
17
|
+
...image.requestOptions ? [ImageUpload.configure(image), ImagePaste.configure(image)] : []
|
|
18
18
|
]"
|
|
19
19
|
:ui="{
|
|
20
20
|
content: '',
|
|
@@ -61,6 +61,7 @@ import { wysiwygTheme } from "#core/theme/wysiwyg";
|
|
|
61
61
|
import { useUiConfig } from "#core/composables/useConfig";
|
|
62
62
|
import { TextAlign } from "@tiptap/extension-text-align";
|
|
63
63
|
import { ImageUpload } from "./EditorImageUploadExtension";
|
|
64
|
+
import { ImagePaste } from "./EditorImagePasteExtension";
|
|
64
65
|
import EditorLinkPopover from "./EditorLinkPopover.vue";
|
|
65
66
|
const props = defineProps({
|
|
66
67
|
editable: { type: Boolean, required: false, default: () => true },
|