テキストエリアに画像をドラッグアンドドロップしてプレビュー表示しながら編集出来るUI


色々と調べたがまとまっている記事が無かったので、備忘録としてメモ。
まだまだ改善の余地、というかダメな部分も多々あるが、ひとまずそれぞれの仕組みの細かいことは置いておいて、手順だけ。

##全体の構成##
1.```<textarea>```は使わずに、```<div>```を使う。
```contenteditable="true"```を指定することで中身が編集できるようになるので、これをテキストエリアと見なす。

2.テキストエリアにドロップされた画像を[FileReader](https://developer.mozilla.org/ja/docs/Web/API/FileReader)で読み込み、```<div contenteditable="true">```の中に```<img>```要素を生成してプレビューする。

3.アップロードの際は、```<img>```要素を[FormData](https://developer.mozilla.org/ja/docs/Web/API/FormData)に組み込んで```XMLHttpRequest.send()```で送信する。

##ソース##

```html
<form action="upload/path" id="" method="post">
	<div id="alt_textarea" contenteditable="true" style="height: auto; min-height: 250px;">
  </div>
  <input type="submit" id="submit_btn" name="submit">
</form>
```

```js
/***
  ***********************************************
  ** ドラッグアンドドロップで画像をアップした時の動き **
  ***********************************************
****/
//ブラウザデフォルトのドラッグ＆ドロップ機能を無効化
document.addEventListener('dragover',function(event){
  event.preventDefault();
}, false);
document.addEventListener('drop',function(event){
  event.preventDefault();
}, false);
//div要素にドラッグしていった時の挙動
var div = document.getElementById('alt_textarea');
div.addEventListener('dragover', function(){
  div.style.border = '3px Aqua dashed';
}, false);
//ドラッグで離れた時の挙動
div.addEventListener('dragleave',function(){
  div.style.border = '';
}, false);
//ドロップした時の挙動
div.addEventListener('drop', function(event){
  //現在のカーソル位置
  var sel = window.getSelection();
  if (sel.anchorNode === null) {
    alert('テキストエリアにカーソルを合わせてください');
    div.style.border = '';
    return;
  }
  event.preventDefault();
  div.style.border = '';
  //DataTransfer オブジェクト、ファイルリストを取得する
  var files = event.dataTransfer.files;
  if(!files) { return; }
  for(var i = 0; i < files.length;i++){
    //ファイルを取得する
    if (!files[i].type.match('image.*')) {
      alert('画像をアップしてください');
      return;
    }
    var reader = new FileReader();
    //エラー処理
    reader.addEventListener('error', function (event){
      console.log('error' + event.target.error.code);
    }, false);
    //読み込み後の処理
    reader.addEventListener('load', function (event){
      var img = document.createElement('img');
      img.src = event.target.result;
      img.width = 300;
      var range = sel.getRangeAt(0);
      range.insertNode(img);
    }, false);
    reader.readAsDataURL(files[i]);
  }
}, false);

/***
  ***********************************************
  ** submitボタンを押された時の挙動
  ***********************************************
****/
var form = document.forms[0];
form.addEventListener('submit', function(event) {
  event.preventDefault();
  this.submit.disabled = true;
  var defaultValue = this.submit.value;
  this.submit.value = '送信中';
  var div = document.getElementById('alt_textarea');
  var div_tmp = document.createElement('div');
  div_tmp.innerHTML = div.innerHTML;
  var images = Array.prototype.slice.call(div_tmp.getElementsByTagName('img'));
  var form_data = new FormData(this);
  for (var i = 0; i < images.length; i++) {
    var altImage = document.createTextNode("articleImage_" + i);
    images[i].parentNode.insertBefore(altImage,images[i]);
    images[i].parentNode.removeChild(images[i]);
    form_data.append('eyecatch[]',images[i].src);
  };
  var xhr = new XMLHttpRequest();
  form_data.append('body',div_tmp.innerHTML);
  //エラー処理
  xhr.addEventListener('error', function (e){
    console.log('error' + e.target.error.code);
  },false);
  xhr.addEventListener('load', function (e){
    if (this.status == 200) {
      var url = 'redirect/url';
      window.location.href = url;
    } else {
      form.submit.disabled = false;
      form.submit.value = defaultValue;
      console.log('入力に間違いがあります。');
    }
  }, false);
  xhr.open("POST" , this.getAttribute('action'));
  xhr.send(form_data);
}, false);
```

##補足1##
```js
  for (var i = 0; i < images.length; i++) {
    var altImage = document.createTextNode("articleImage_" + i);
    images[i].parentNode.insertBefore(altImage,images[i]);
    images[i].parentNode.removeChild(images[i]);
    form_data.append('eyecatch[]',images[i].src);
  };
```

FormDataに画像を追加しつつ、テキストエリアの文字列を置き換えているのは、アップされた記事を後に表示する際にサーバーサイドで処理しやすくするためである。

```
  articleImage_1 => <img src="hogehoge1.jpg">
  articleImage_2 => <img src="hogehoge2.jpg">
```

表示する際は、記事の内容中の文字列を上記のように該当の```<img>```で置き換え、```<?php>echo htmlspecialchars_decode()<?>```とかで出力すれば画像が途中に埋め込まれた形で表示出来る。

何かもっと賢い方法が見つかったら変更予定。

##補足2##
```js
var images = Array.prototype.slice.call(div_tmp.getElementsByTagName('img'));
```
にて```Array.prototype.slice.call()```を使っているのは、取得した要素の追従をなくすため。
以前書いた[記事](http://qiita.com/kittaka/items/5b81b3e506ae35a96c89)で[ykzts](http://qiita.com/ykzts)さんに教えていただきました。ありがとうございます。

ここで追従を無くさないと、補足1で書いた操作をした時に```<img>```要素そのものが消えてしまい、FormDataに追加できなくなってしまう。

##まとめ##

ひとまずこれでそれっぽい動きはするようになった。

Laravelを使っているのでバリデーションはサーバーサイドでやりたいし、Flash Messageも使いたいのでできればAjaxでのsubmitではなく、もっと静的に送信したいが何か方法あるのかな？