Some thoughts on implementing image drag-and-drop and paste with vue-simplemde
IntroThe project uses Vue and needed a markdown editor. I looked on npm, found simplemde (opens in 2018-4-12 03:0:12 Author: bugs.cc(查看原文) 阅读量:1 收藏

Intro

The project uses Vue and needed a markdown editor. I looked on npm, found simplemde (opens in a new tab) was decent. I’m pretty lazy, so I searched npm again, found the vue-simplemde (opens in a new tab) package, and started using it.

But vue-simplemde does not support drag-and-drop image upload or paste upload. You can’t really blame vue-simplemde; it is only a Vue wrapper around simplemde. So it comes down to simplemde not shipping this. For UX this feature is necessary, unless you drop the markdown editor and switch to a rich-text one, in which case a lot of the project would have to change. I looked up articles and some GitHub code. Analysis below.

Drag and drop

The core of the drag-and-drop API is the drop event: it fires when you drag a file from the desktop into the browser and release.

We all know that if you drag an image into the browser, it just opens the image. That is the default: dragging a file into the browser opens the file. We need to block that native behavior.

First, a snippet that suppresses the default:

window.addEventListener("drop", e => {

e = e || event

if (e.target.className === 'CodeMirror-scroll') { // if it enters the editor, prevent the default event

e.preventDefault()

}

}, false)

CodeMirror-scroll is the class name of the simplemde editor.

Now if we drag a file onto the editor and release, nothing happens. Outside the editor, the default still fires.

Next, get the simplemde instance and attach a drop handler.

// assume the page has three editor windows, so we loop to bind listeners

[ this.$refs.simplemde1,

this.$refs.simplemde2,

this.$refs.simplemde3

].map(({simplemde}) => {

simplemde.codemirror.on('drop', (editor, e) => {

if (!(e.dataTransfer && e.dataTransfer.files)) {

// alert that this browser does not support this operation

return

}

let dataList = e.dataTransfer.files

let imageFiles = [] // array of file instances to upload

// loop because several image files may be dragged at once

for (let i = 0; i < dataList.length; i++) {

// if not an image, warn that only dragging image files is supported

if (dataList[i].type.indexOf('image') === -1) {

// the continue below means: if the user drags 2 images and one document at once, the document is not uploaded while the images are uploaded as usual.

continue

}

imageFiles.push(dataList[i]) // push the current file into the array first, then upload them all together after the for loop ends.

}

// uploadImagesFile is the method that uploads images

// simplemde.codemirror is used to tell which editor the current image upload belongs to

this.uploadImagesFile(simplemde.codemirror, imageFiles)

// because the code below already exists, the default-event blocking code above is unnecessary

e.preventDefault()

})

})

At first glance it looks like a lot of code; that’s the comments. Here it is without comments. Read it and form your own take:

[ this.$refs.simplemde1,

this.$refs.simplemde2,

this.$refs.simplemde3

].map(({simplemde}) => {

simplemde.codemirror.on('drop', (editor, e) => {

if (!(e.dataTransfer && e.dataTransfer.files)) {

return

}

let dataList = e.dataTransfer.files

let imageFiles = []

for (let i = 0; i < dataList.length; i++) {

if (dataList[i].type.indexOf('image') === -1) {

continue

}

imageFiles.push(dataList[i])

}

this.uploadImagesFile(simplemde.codemirror, imageFiles)

e.preventDefault()

})

})

Paste

The paste API is the paste event. Unlike drop, paste does not need preventDefault. If you copy an image and press ctrl+v in the browser, nothing happens, so there is no need to block the default.

Code:

simplemde.codemirror.on('paste', (editor, e) => { // handler triggered when pasting an image

if (!(e.clipboardData && e.clipboardData.items)) {

// alert that this browser does not support this operation

return

}

try {

let dataList = e.clipboardData.items

if (dataList[0].kind === 'file' && dataList[0].getAsFile().type.indexOf('image') !== -1) {

this.uploadImagesFile(simplemde.codemirror, [dataList[0].getAsFile()])

}

} catch (e) {

// alert that only images can be pasted

}

})

The try...catch is there because if you paste a file, items is empty, and the if below reads dataList[0].kind, i.e. e.clipboardData.items[0].kind. Accessing kind on a missing item throws. So we need try...catch.

dataList[0].getAsFile().type.indexOf('image') !== -1 checks that what you pasted is actually an image, not something else.

The upload call inside if differs in [dataList[0].getAsFile()]. I wrap it in [] so the shape matches what uploadImagesFile expects. dataList[0].getAsFile() is the file instance.

Upload

Upload is a bit more of a hassle:

uploadImagesFile (simplemde, files) {

// wrap each file instance with FormData and return an array

let params = files.map(file => {

let param = new FormData()

param.append('file', file, file.name)

return param

})

let makeRequest = params => {

return this.$http.post('/Api/upload', params)

}

let requests = params.map(makeRequest)

this.$http.spread = callback => {

return arr => {

return callback.apply(null, arr)

}

}

// the server returns the format {state: Boolean, data: String}

// when state is false, data is the returned error message

// when state is true, data is the uploaded image's url, an absolute path relative to the site, like below:

// /static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png

Promise.all(requests)

.then(this.$http.spread((...resps) => {

for (let i = 0; i < resps.length; i++) {

let {state, data} = resps[i].data

if (!state) {

// alert showing the error message in data

continue

}

let url = `![](${location.origin + data})` // assemble into markdown syntax

let content = simplemde.getValue()

simplemde.setValue(content + url + '\n') // concatenate with the editor's existing content

}

}))

}

I wrapped axios as a Vue plugin, so this.$http is the instance, not axios itself. The axios maintainers’ fix is to import the axios package again. I don’t think that is needed. Internally axios.all is Promise.all. axios.spread is small, so I copied it and assigned it back onto axios.

So the snippet above is

Promise.all(requests)

.then(this.$http.spread((...resps) => {

// code

})

Which is equivalent to

axios.all(requests)

.then(axios.spread((...resps) => {

// code

})

For this, see the official thread: axios-all-is-not-a-function-inside-vue-component (opens in a new tab). You can also look at the axios source: axios.js#L45-L48 (opens in a new tab)

I won’t go further on this. Back to the topic.

When state is true, data is an absolute path on the site, e.g. /static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png

We need to concatenate, hence ![](${location.origin + data}). The last two lines get the previous content and append the url.

Wrap-up

Here is the final result:

Full code: Subject.vue#L378-L465 (opens in a new tab)

References && thanks

code from skecozo’s laravel-demo (opens in a new tab)

Lemon’s article “Implementing drag-and-drop and paste image upload in simplemde” (opens in a new tab)

f-loat’s vue-simplemde package (opens in a new tab)

wescossick’s simplemde package (opens in a new tab)


文章来源: https://bugs.cc/posts/simplemde-realizes-some-thoughts-on-drag-and-drop-and-paste-function/
如有侵权请联系:admin#unsafe.sh