マークダウンエリアへの画像の直接入力

imatomix
2017年12月3日 13:44

ゴール

  • マークダウン入力で画像を以下の手段で入力できるようにする。
  • ドラッグ&ドロップで画像を貼る
  • クリップボードから画像を貼る

ざっくりとした流れ

  • マークダウン入力エリアに画像をドラッグ&ドロップまたはクリップボードからペーストできる。
  • 入力された画像はサーバにアップロードされ、レスポンスとしてURLが返ってくる。
  • URLを元に、マークダウン入力エリアに画像表示のマークダウン記法を入力する

スクリプト

イベント

  • vueなので、@drop@pasteかなと試したらいけた。
  • @dropはそのままだと画像を表示する画面に遷移しちゃうので、.preventをつける。
<textarea placeholder="# 本文" rows=24 :value="this.post.body" @input="update" @drop.prevent="handleDrop" @paste="handlePaste"></textarea>

ドロップ時の処理

handleDrop: function(e) { // ファイルがドロップされたときだけ処理する var files = e.dataTransfer.files if (files.length > 0) { var formData = new FormData() for (var file of files) { if (file.type.match('image.*')) { formData.append('note', file) // 画像のみ追加 } } this.postImage(formData) // 画像をアップロード }

クリップボードからペースト時の処理

  • event.clipboardData.items でクリップボードのデータを受け取る
  • getAsFile() でデータをファイルとして取得する
handlePaste: function(e) { var items = e.clipboardData.items var formData = new FormData() for (var item of items) { if (item.type.match('image.*')) { formData.append('note', item.getAsFile()) } } if (formData.has('note')) { this.postImage(formData) }

画像アップロード&マークダウン挿入

  • 画像をアップロードしたらレスポンスで画像のURLを受け取る
  • textarea内のキャレットの位置はtextarea.selectionStartまたはtextarea.selectionEndで取得する。
  • それぞれ選択範囲の開始位置と終了位置を返すものだが、選択がない場合はどちらもキャレットの位置を返す
  • textarea.valueの文字列を操作してキャレット位置に画像表示のマークダウンを挿入する。
postImage: function(formData) { axios .post(URL, formData) // 画像をアップロード .then(response => { var textarea = document.querySelector('textarea') var sentence = textarea.value var len = sentence.length var pos = textarea.selectionStart var before = sentence.substr(0, pos) var after = sentence.substr(pos, len) for (var image of response.data.images) { var word = '![image](' + image + ')' sentence = before + word + after // マークダウンを挿入 this.post.body = sentence } }) .catch(error => { console.log(error) }) },

全体

markdown.vue
<template> <form> <textarea placeholder="# 本文" rows="24" @input="update" @drop.prevent="handleDrop" @paste="handlePaste"> <div v-html="compiledMarkdown"></div> </form> </template> <script> import marked from 'marked' import Prism from 'prismjs' import lodash from 'lodash' export default{ data: function(){ return{ title: '', body: '', } }, computed: { compiledMarkdown: function () { return marked(this.body, { sanitize: true }) } }, methods: { update: lodash.debounce(function (e) { this.body = e.target.value }, 500), // 追加箇所 postImage: function(formData) { axios .post(URL + 'upload/image/', formData) .then(response => { var textarea = document.querySelector('textarea') var sentence = textarea.value var len = sentence.length var pos = textarea.selectionStart var before = sentence.substr(0, pos) var after = sentence.substr(pos, len) for (var image of response.data.images) { var word = '![image](' + image + ')' sentence = before + word + after this.post.body = sentence } }) .catch(error => { console.log(error) }) }, handleDrop: function(e) { var files = e.dataTransfer.files if (files.length > 0) { var formData = new FormData() for (var file of files) { if (file.type.match('image.*')) { formData.append('note', file) } } this.postImage(formData) } }, handlePaste: function(e) { var items = e.clipboardData.items var formData = new FormData() for (var item of items) { if (item.type.match('image.*')) { formData.append('note', item.getAsFile()) } } if (formData.has('note')) { this.postImage(formData) } } } } </script>

課題

  • 画像をアップしたとして、最終的に使わなかった画像はどう処理するか。