CoffeeScript: ファイル Drag&Drop

HTML へのローカルファイルの Drag & Drop のメモです。

JavaScript は CoffeeScript で記述しています。jQeury を使用しています。

# ondrop イベント
ファイル Drag&Drop の処理の要点をまとめると、以下のようでした。

1. ファイルをHTML要素にドロップすると、ondrop イベントがあがる
2. それをハンドルする
3. ただし、デフォルト動作(ファイルを表示するなど)が好ましくないので、それは無効にする

なので、それ用のハンドラのラッパを作りました。

```coffeescript:dnd.coffee
  dnd = (f = ()->) ->
    (e) ->
      e.stopPropagation()    # イベントの親要素への伝播を無効にする
      e.preventDefault()     # イベント発生時のデフォルト動作を無効にする
      f.apply $(this), [     # f はハンドラ本体
        e                    # 引数は扱いやすいように適当に並び替えました
        e.originalEvent.dataTransfer.files[0]
        e.originalEvent.dataTransfer.files
      ]
```

想定する使い方は以下のような感じです。
ondrop だけでなく dragover も抑止しないといけないので、その２つのイベントに上のラッパをかませます。

```coffeescript
  $ '#drop-here'                     
    .on 'dragover', dnd()
    .on 'drop',     dnd (e) -> console.log e     # この例の処理は適当
```

# ファイルの読み込み
上で Drag & Drop はできるようになったので、ファイルの中身を読む処理を追加しました。  
ファイルを読む処理の要点は以下です。

1. new FileReader してリーダオブジェクトを作る
2. ReadAsText などのメソッドでファイル読み込み処理を行う
3. 2. の処理は非同期処理なので、完了時等のハンドラを事前にセットしておく

リーダをつくる関数をつくりました。

```coffeescript:create_reader.coffee
  create_reader = (done, fail, method = 'readAsText') ->
    (e, file, files) ->
      rdr = new FileReader()
      rdr.onload  = (e) -> done e, file, e.target.result
      rdr.onerror = (e) -> fail e, file, e.target.result
      rdr[method] file
```

onload には成功時の、onerror には失敗時のハンドラをセットします。
ハンドラに渡されるイベントの e.target にリーダ自身がセットされています。
読み込んだ内容は、e.target.result に格納されています。
読み込んだテキストが JSON の場合、JSON.parse(text) でパースできます。

想定する使い方は以下です。

```coffeescript
  fail = (e, file, result) -> ... 失敗時の処理

  done = (e, file, result) -> ... 成功時の処理

  reader = create_reader done, fail

  $ '#drop-here'
    .on 'dragover', dnd()
    .on 'drop',     dnd reader
```

# サンプル: ドロップされた JSON データをプロットする
- [「プログラムでシダを描画する」を CoffeeScript (HTML5 Canvas) で描画する](http://qiita.com/lnznt/items/f61538a4ff716cb9c166)

上の記事で作ったスクリプトを流用して、JSON形式でドロップされたデータをプロットするスクリプトを作りました。(CoffeeScript と Haml で記述しています)
渡される JSON形式のデータは、X座標とY座標の組(配列)の配列を想定しています。

    [ [x1,y1], [x2,y2], ..., [xn, yx]]

実は、どこかの座標変換が何かがバグっているようで、少し太ったシダになります。(色をランダムにだしているのでぼやけて見えます) 
ですが、ここではファイルDrop&Dragのテストが目的なので、そのバグ追跡は打ち切りました。

```coffeescript:myscript.coffee
$ ->
  # ---- 色 --------------------------------------------------
  green = () -> [0, 0xff, 0, 0xff]    # 単色 緑

  rand = (max = 0xff, min = 0) -> min + Math.random() * (max - min + 1) |0
  random_color = () -> [rand()/4, rand(), rand()/2, 0xff - rand()/8]

  #
  # ---- プロッタ (Canvas) -----------------------------------
  #
  plotter = (color = random_color, canvas = $('#drop-here')[0]) ->
    ctx = canvas.getContext '2d'
    img = ctx.createImageData canvas.width, canvas.height

    put = (xy) ->
      x = xy[0] * (+1) * (canvas.width  - 10) + (canvas.width / 2) |0
      y = xy[1] * (-1) * (canvas.height - 10) + (canvas.height   ) |0
      offset = (canvas.width * y + x) * 4
      color().forEach (c, i) -> img.data[offset + i] = c |0

    plot: (data) ->
      [0...(canvas.width * canvas.height * 4)].forEach (i) -> img.data[i] = 0
      data.forEach put
      ctx.putImageData img, 0, 0

  #
  # ---- ファイル Drag & Drop  ------------------------------
  #
  dnd = (f = ()->) ->
    (e) ->
      e.stopPropagation()
      e.preventDefault()
      f.apply $(this), [
        e
        e.originalEvent.dataTransfer.files[0]
        e.originalEvent.dataTransfer.files
      ]

  create_reader = (done, fail, method = 'readAsText') ->
    (e, file, files) ->
      rdr = new FileReader()
      rdr.onload  = (e) -> done e, file, e.target.result
      rdr.onerror = (e) -> fail e, file, e.target.result
      rdr[method] file

  fail = (e, file, result) -> console.log 'Fail: ' + e

  done = (e, file, result) ->
    console.log 'ファイル読み込み: ' + file.size + ' バイト'
    console.log '描画開始'
    plotter().plot JSON.parse result
    console.log '描画終了'

  reader = create_reader done, fail

  $ '#drop-here'
    .on 'dragenter' +'.style', () -> $(this).addClass    'active'
    .on 'dragleave' +'.style', () -> $(this).removeClass 'active'
    .on 'drop'      +'.style', () -> $(this).removeClass 'active'
    .on 'dragover', dnd()
    .on 'drop',     dnd reader
```

```haml:sample.haml
!!!
%meta(charset="UTF-8")
%title ファイル Drag & Drop

:css
  canvas {
    background-color: #000;
  }
  .active {
    background-color: #333;
  }

%p データファイルをドロップしてください。
%canvas#drop-here(style="width: 500px; height: 500px")
  このブラウザは HTML5 Canvas に対応していません。

%script(src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js")
%script(src="myscript.js")
```

Haml->HTML、CoffeeScript -> JavaScript の変換方法です。

```shell-session
$ haml -q sample.haml sample.html
$ coffee -b -c myscript.coffee
````
---
ドロップするデータの作成には以下のいずれかの方法があります。

ひとつは、以下の RubyスクリプトでJSONを出力する方法です。

```rb:fern.rb
#
# ---- 「シダ」のアルゴリズム ----------------------------
#
module Fern
  extend Enumerable
  extend self

  def each(k = 20)
    w1x = -> x, y {  0.836 * x + 0.044 * y }
    w1y = -> x, y { -0.044 * x + 0.836 * y + 0.169 }
    w2x = -> x, y { -0.141 * x + 0.302 * y }
    w2y = -> x, y {  0.302 * x + 0.141 * y + 0.127 }
    w3x = -> x, y {  0.141 * x - 0.302 * y }
    w3y = -> x, y {  0.302 * x + 0.141 * y + 0.169 }
    w4x = -> x, y {  0.0 }
    w4y = -> x, y {  0.175337 * y }

    f = -> k, x, y do
      if 0 < k
        f.(k - 1, w1x.(x, y), w1y.(x, y))
        f.(k - 1, w2x.(x, y), w2y.(x, y)) if Random.rand < 0.3
        f.(k - 1, w3x.(x, y), w3y.(x, y)) if Random.rand < 0.3
        f.(k - 1, w4x.(x, y), w4y.(x, y)) if Random.rand < 0.3
      else
        yield [x, y]
      end
    end

    f.(k, 0, 0)
  end
end

if $0 == __FILE__
  require 'json'

  puts JSON.dump Fern.entries
end
```

使い方。

```shell-session
$ ruby fern.rb >shida.json
```

---
もうひとつは、以下の記事で作った Erlang プログラムで「シダ」データを YAMLで出力して、それをJSONに変換する方法です。

- [「Erlang でシダを描画する」(ただし、Ruby(RMagick)と合わせ技)](http://qiita.com/lnznt/items/6a351fd6c1d276604dee)
- [YAML <-> JSON 変換 (Rubyで実装)](http://qiita.com/lnznt/items/d4798cb42d463cf1acf1)

```shell-session
$ escript shida.erl | yj >shida.json
```