WTForms の TextArea をファイルドロップに対応させる方法

# 経緯
ある案件をFlaskで実装したときのこと、
「Copy&Pasteしてね」と導いた公開鍵の登録フォーム(TextArea) に、
ファイルドロップをするエンドユーザさまがいらいしゃいました。
そんなことをする人がいるんだと思いながらも、なんとか対応できないものかと調べたところ...
[StackOverflow: Load Textfile via Drag and Drop on Textarea](https://stackoverflow.com/questions/58676967/load-textfile-via-drag-and-drop-on-textarea)
というのが見つかりました。

案外簡単なJavaScriptできることがわかったので、
Flaskで実装してみました。

## Flask
まずは、Flaskで公開鍵の登録フォームをFORMクラスRegistFormとして定義します。
公開鍵の検証は sshpubkeys ライブラリを使っていますが、これは本題ではありません。

フォームのデータを検証したデータを受け取って表示するだけのテストアプリです。

```
import os
from flask import (
    Flask,
    request,
    render_template,
    redirect,
    url_for,
)

from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField, ValidationError
from wtforms.validators import InputRequired
from sshpubkeys import SSHKey, InvalidKeyError, MalformedDataError

class RegistForm(FlaskForm):
    pubkey = TextAreaField("PublicKey",validators=[InputRequired()])
    submit = SubmitField("OK")
    reset = SubmitField("RESET")

    def validate_pubkey(self, field):
        if field.data.find('\n') >1:
            raise ValidationError("Too Many Publickey")

        sshpubkey = SSHKey(field.data, strict=True)
        if sshpubkey.key_type != b"ssh-rsa":
            raise ValidationError("Invalid Key Type")

        try:
            sshpubkey.parse()
        except InvalidKeyError as err:
            raise ValidationError("Invalid PublicKey")
        except MalformedDataError as err:
            raise ValidationError("Malformed key Error")
        except NotImplementedError as err:
            raise ValidationError("Undefined Error")


app = Flask(__name__, static_folder='static')
app.config['SECRET_KEY']='12345678901234567890'

@app.route("/", endpoint="register", methods=["GET", "POST"])
def register():
    form = RegistForm()
    if request.method == "GET":
        return render_template("test.html",
                               form=form,
                               endpoint="register")

    if form.validate_on_submit():
        return form.pubkey.data
    else:
        print(f'vaslidation error')
        return redirect(url_for("register"))


if __name__ == "__main__":
    app.run(
        host=os.getenv("APP_HOST", "localhost"),
        port=os.getenv("APP_PORT", 8080),
        debug=True,
    )
```

## JavaScript

static/functions.js に次の関数を用意します。


```
function dropfile(file) {
    var reader = new FileReader();
    reader.onload = function(e) {
        notepad.value = e.target.result.replace(/(?:\r\n|\r|\n)/g, '');
    };
    reader.readAsText(file, "UTF-8");
};

notepad.ondrop = function(e) {
    e.preventDefault();
    var file = e.dataTransfer.files[0];
    dropfile(file);
};
```

DataTransferオブジェクトを使っていますが、すべてのブラウザがオブジェクトを公開するわけではありません。モダンブラウザであれば問題ないはずですが...


## HTML

実は、コツらしいものといえばここぐらいなものです。
forms.py で定義したクラスのフィールドにアトリビュート id に "notepad" を与えています。

```
{% extends "base.html" %}

{% block contents %}

<form action="{{ url_for( endpoint ) }}" method="post">
{{ form.hidden_tag() }}
{{ form.pubkey(id="notepad", raws=6, cols=80,
               placeholder="Copy&Past or Drag&Drop here!") }}
{{ form.submit }}
{{ form.reset }}
</form>
{% endblock contents %}
```


`rows`や`cols`、`placeholder` はクラス定義のなかで  `render_kw` に辞書形式で与えることもできますが、描画に関わるものをあまりクラスで定義しない方がきれいだと思っています。 

```
render_kw={"rows": 6, "cols": 80, "placeholder": "Copy&Paste" }
```


これも本題とはあまり関係ないものですが、気になる方もおられるでしょうから、
とりあえずこんな base.html を用意します。

```
{% from "_defaults.html" import render_htmlattribs, render_styles, render_scripts %}

{% block doc -%}
<!DOCTYPE html>
<html{% block html_attribs %} {{ render_htmlattribs() }}{% endblock html_attribs %}>
  {%- block html %}
  <head>
    {%- block head %}
    <title>{% block title %}{{ render_title() }}{% endblock title %}</title>

      {%- block metas %}
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta charset="UTF-8">
      {%- endblock metas %}
      {%- block favicon %} {%- endblock favicon %}

      {%- block styles %}
      {{ render_styles() }}
      {%- endblock styles %}

    {%- endblock head %}
  </head>
  <body{% block body_attribs %}{% endblock body_attribs %}>
    {%- block body %}

    {% block contents %} {% endblock contents %}

    {% block footer %}
      {{ render_footer() }}
    {%- endblock footer %}

    {% block scripts %}
      {{ render_scripts() }}
    {%- endblock scripts %}

    {%- endblock body %}
  </body>
  {%- endblock html %}
</html>
{% endblock doc -%}
```

_defaults.html で定義したマクロは次のようなものです。
使いまわしているので bootstrap とか jquery とか読み込むようにしていますが、
これがなくても構いません。
static/functions.js さえ読み込んでいればOKです。

```
{% macro render_htmlattribs() -%}
lang="ja"
{%- endmacro %}

{% macro render_styles() %}
    <link href="/static/bootstrap-4.5.2/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static/base.css" rel="stylesheet">
{% endmacro %}


{% macro render_scripts() %}
    <script src="/static/jquery-3.5.1.min.js"></script>
    <script src="/static/bootstrap-4.5.2/js/bootstrap.min.js"></script>
    <script src="/static/functions.js"></script>
{% endmacro %}
```


## 参考資料
*　`<input type="file" />` でファイル選択させたいときはこちらが参考になります。
　 [アップロードしたファイルをテキストで読み込む](https://qiita.com/pirosikick/items/cf3d4852a1f2df4f552a)