Browse Source

feat: default editor supports pasting or dragging pictures to upload (#825)

#### What type of PR is this?

/kind feature

#### What this PR does / why we need it:

添加复制或者拖拽图片到编辑器上传的支持。

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3109
Fixes https://github.com/halo-dev/halo/issues/2946

#### Screenshots:


#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?


```release-note
Console 端的默认编辑器支持拖拽或者粘贴图片上传
```
pull/836/head
Ryan Wang 2 years ago committed by GitHub
parent
commit
8f6d543f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      package.json
  2. 8
      pnpm-lock.yaml
  3. 143
      src/components/editor/DefaultEditor.vue

1
package.json

@ -63,6 +63,7 @@
"colorjs.io": "^0.4.2",
"dayjs": "^1.11.6",
"emoji-mart": "^5.3.3",
"fastq": "^1.15.0",
"floating-vue": "2.0.0-beta.20",
"fuse.js": "^6.6.2",
"lodash.clonedeep": "^4.5.0",

8
pnpm-lock.yaml

@ -62,6 +62,7 @@ importers:
eslint: ^8.28.0
eslint-plugin-cypress: ^2.12.1
eslint-plugin-vue: ^9.8.0
fastq: ^1.15.0
floating-vue: 2.0.0-beta.20
fuse.js: ^6.6.2
husky: ^8.0.2
@ -131,6 +132,7 @@ importers:
colorjs.io: 0.4.2
dayjs: 1.11.6
emoji-mart: 5.3.3
fastq: 1.15.0
floating-vue: 2.0.0-beta.20_vue@3.2.45
fuse.js: 6.6.2
lodash.clonedeep: 4.5.0
@ -2728,7 +2730,7 @@ packages:
engines: {node: '>= 8'}
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.13.0
fastq: 1.15.0
/@polka/url/1.0.0-next.21:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
@ -5977,8 +5979,8 @@ packages:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
dev: true
/fastq/1.13.0:
resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
/fastq/1.15.0:
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
dependencies:
reusify: 1.0.4

143
src/components/editor/DefaultEditor.vue

@ -81,6 +81,7 @@ import {
IconCharacterRecognition,
IconLink,
IconUserFollow,
Toast,
VTabItem,
VTabs,
} from "@halo-dev/components";
@ -104,6 +105,11 @@ import {
} from "vue";
import { formatDatetime } from "@/utils/date";
import { useAttachmentSelect } from "@/modules/contents/attachments/composables/use-attachment";
import { apiClient } from "@/utils/api-client";
import * as fastq from "fastq";
import type { queueAsPromised } from "fastq";
import type { Attachment } from "@halo-dev/api-client";
import { useFetchAttachmentPolicy } from "@/modules/contents/attachments/composables/use-attachment-policy";
const props = withDefaults(
defineProps<{
@ -168,6 +174,7 @@ const editor = useEditor({
ExtensionText,
ExtensionImage.configure({
inline: true,
allowBase64: false,
HTMLAttributes: {
loading: "lazy",
},
@ -250,8 +257,144 @@ const editor = useEditor({
handleGenerateTableOfContent();
});
},
editorProps: {
handleDrop: (view, event: DragEvent, _, moved) => {
if (!moved && event.dataTransfer && event.dataTransfer.files) {
const images = Array.from(event.dataTransfer.files).filter((file) =>
file.type.startsWith("image/")
) as File[];
if (images.length === 0) {
return;
}
event.preventDefault();
images.forEach((file, index) => {
uploadQueue.push({
file,
process: (url: string) => {
const { schema } = view.state;
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (!coordinates) return;
const node = schema.nodes.image.create({
src: url,
});
const transaction = view.state.tr.insert(
coordinates.pos + index,
node
);
editor.value?.view.dispatch(transaction);
},
});
});
return true;
}
return false;
},
handlePaste: (view, event: ClipboardEvent, slice) => {
const images = Array.from(event.clipboardData?.items || [])
.map((item) => {
return item.getAsFile();
})
.filter((file) => {
return file && file.type.startsWith("image/");
}) as File[];
if (images.length === 0) {
return;
}
event.preventDefault();
images.forEach((file) => {
uploadQueue.push({
file,
process: (url: string) => {
editor.value
?.chain()
.focus()
.insertContent([
{
type: "image",
attrs: {
src: url,
},
},
])
.run();
},
});
});
},
},
});
// image drag and paste upload
const { policies } = useFetchAttachmentPolicy({ fetchOnMounted: true });
type Task = {
file: File;
process: (permalink: string) => void;
};
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
async function asyncWorker(arg: Task): Promise<void> {
if (!policies.value.length) {
Toast.warning("目前没有可用的存储策略");
return;
}
const { data: attachmentData } = await apiClient.attachment.uploadAttachment({
file: arg.file,
policyName: policies.value[0].metadata.name,
});
const permalink = await handleFetchPermalink(attachmentData, 3);
if (permalink) {
arg.process(permalink);
}
}
const handleFetchPermalink = async (
attachment: Attachment,
maxRetry: number
): Promise<string | undefined> => {
if (maxRetry === 0) {
Toast.error(`获取附件永久链接失败:${attachment.spec.displayName}`);
return undefined;
}
const { data } =
await apiClient.extension.storage.attachment.getstorageHaloRunV1alpha1Attachment(
{
name: attachment.metadata.name,
}
);
if (data.status?.permalink) {
return data.status.permalink;
}
return await new Promise((resolve) => {
const timer = setTimeout(() => {
const permalink = handleFetchPermalink(attachment, maxRetry - 1);
clearTimeout(timer);
resolve(permalink);
}, 300);
});
};
const toolbarMenuItems = computed(() => {
if (!editor.value) return [];
return [

Loading…
Cancel
Save