2017-12-3 22:32
markdown vue

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

vueでマークダウン&シンタックスハイライト」の続き

ゴール

  • UXとして、マークダウン入力で画像を直接入力できるようにする。
  • 直接って?
    • ドラッグ&ドロップで画像を貼る
    • クリップボードから画像を貼る

ざっくりとした流れ

  • マークダウン入力エリアに画像をドラッグ&ドロップまたはクリップボードからペーストできる。
  • 入力された画像はサーバにアップロードされ、レスポンスとして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>

課題

  • ぺっぺぺっぺと画像をアップしたとして、最終的に使わなかった画像はどう処理したらいいのかな?