Polymer サンプルコード (6) PWA [3] Markdownで画像自動アップロード貼り付け

# PWA [3] Markdownで画像自動アップロード貼り付け

<img src="https://www.polymer-project.org/images/logos/p-logo.png" height="26"> [Polymer](https://www.polymer-project.org/)と<img src="https://firebase.google.com/_static/images/firebase/touchicon-180.png" height="30"> [Firebase](https://firebase.google.com/) で、よくあるMarkdownを編集中に、画面キャプチャのコピーペーストやファイルのドラッグ&ドロップで画像を貼り付ける機能を実装してみます。

※ [Polymer サンプルコード (5) PWA [2]](http://qiita.com/howking/items/fce01f9768d4652290a2) の続きです。

<dl>
  <dt>動作確認</dt>
  <dd>Windows 10 : ○ Chrome60 Firefox56 × Edge40 IE11</dd>
  <dd>Mac 10.3 : ○ Chrome60 Safari10.1</dd>
</dl>

## 画面イメージ

![qiita-fb-pwa3-cp.gif](https://qiita-image-store.s3.amazonaws.com/0/90678/b71e6b0f-f903-ff70-5e9a-329867a12811.gif)

[:globe_with_meridians: デモサイト (Firebase)](https://sample-3-d794e.firebaseapp.com/)
**・Googleでログインすれば編集できますが、いたずら無しでお願いします**
**・定期的にデータクリアしてます**

### 機能

- コピーペーストやドラッグアンドドロップで画像のURLをMarkdownに挿入
- 対応画像ファイル形式の指定(jpg,gif,png)
- 複数ファイル同時アップロード

## 実行ステップ

ビルドツール(Polymer CLI)やFirebaseのプロジェクトの準備は[前回](http://qiita.com/howking/items/fce01f9768d4652290a2)と同じです。

### 1. ストレージの準備

Firebaseの[管理コンソール](https://console.firebase.google.com/)の左メニューから「Storage」を選択します。

Realtime Databaseと同じように初期状態では認証されたユーザしか読み書きできないので、「ルール」タブをクリックし、下記のルールをペーストして上書きして、「公開」ボタンをクリックしてください(`/files`以下のデータの読み込みを許可し、認証されたユーザのみ書きこみを許可します)。

```html:rules
service firebase.storage {
  match /b/{bucket}/o {
    match /files {
      match /{allFiles=**} {
        allow read;
      }
      match /{allFiles=**} {
        allow write : if request.auth != null;
      }
    }
  }
}
```

### 2. ファイルの取得とAPIキーの設定

ファイル一式を[こちら](https://github.com/howking/polymer-sample-pwa)からダウンロード(`sample-3`ブランチ)してください。
**`src/my-fire.html`の`<firebase-app>`タグの`api-key`、`auth-domain`、`storage-bucket`、`database-url`はご自分のプロジェクトのものを使用してください。**

```bash
# githubからダウンロードする場合
$ git clone -b sample-3 https://github.com/howking/polymer-sample-pwa sample-3

# 作業ディレクトリに移動
$ cd sample-3

# my-fire.htmlを修正
$ emacs src/my-fire.html
```

```html:src/my-fire.html
    <firebase-app
      api-key="AIzaSyDwrbEx_4RHcIUVfMAZgLN4edNoRo-tlvw"
      auth-domain="sample-3-d794e.firebaseapp.com"
      storage-bucket="sample-3-d794e.appspot.com"
      database-url="https://sample-3-d794e.firebaseio.com"></firebase-app>
```

書換える文字列は[前回](http://qiita.com/howking/items/fce01f9768d4652290a2)の方法を参考にして取得してください。

### 3. ビルドとファイルのアップロード

下記を入力するとビルドされ、サイトへアップロードされます。

**!!注意!!** polymerfireをビルドするとバグが発生するので、[ここ](https://github.com/Polymer/polymer-cli/issues/701#issuecomment-325925867)の修正が必要です。

```bash
# 必要なツールのインストール
$ npm install -g bower polymer-cli firebase-tools

# 利用するWebライブラリを取得
$ bower install

# ローカルでサーバを立ち上げて動作確認
$ polymer serve

# アップロードするファイルを構築
$ polymer build

# Firebaseにログイン
$ firebase login

# 使用するプロジェクトを指定
$ firebase use [プロジェクトID]

# ファイルをアップロード
$ firebase deploy
```

https://[プロジェクトID].firebaseapp.com/ で確認できます。 :tada:

## サンプルコードの説明

**[前回説明した部分](http://qiita.com/howking/items/fce01f9768d4652290a2)は省いてます。** 差分は[こちら](https://github.com/howking/polymer-sample-pwa/compare/sample-2...sample-3?expand=1)から確認できます。

### src/my-marked.html

```html:src/my-marked.html
<link rel="import" href="../bower_components/polymerfire/firebase-storage-multiupload.html">
<link rel="import" href="../bower_components/polymerfire/firebase-storage-upload-task.html">
...
    <firebase-storage-multiupload
      path="/files" upload-tasks="{{uploadTasks}}"></firebase-storage-multiupload>
    <template is="dom-repeat" items="[[uploadTasks]]">
      <firebase-storage-upload-task task="[[item]]" metadata="{{_metaData}}"></firebase-storage-upload-task>
    </template>
```

先週(9/6)リリースされた[polymerfire v2.2.0](https://github.com/firebase/polymerfire/releases/tag/v2.2.0)からとうとうStorageが使えるようになったので、さっそく使ってみます。
`<firebase-storage-multiupload>`でバケットの`/files`ディレクトリにアップロード処理がされ、`<firebase-storage-upload-task>`で個々のアップロードのステータスが取得されます。

```html:src/my-marked.html
        <paper-textarea value="{{contents}}" hidden="[[!_edit]]"
                        on-drop="_onDrop" on-paste="_onPaste"></paper-textarea>
```

テキスト編集エリアにドラッグ&ドロップされた時とペーストがされた時のイベントを拾う為に、それぞれ`on-drop`と`on-paste`で処理を指定します。

```js:src/my-marked.html
          allowedTypes: {
            type: Array,
            value: [
              'image/jpeg',
              'image/png',
              'image/jpg',
              'image/gif'
            ]
          },
          progressText: {
            type: String,
            value: '![Uploading file...]()'
          },
```

`allowedType`でアップロード対象とするファイル形式、`progressText`でアップロードしている最中に表示する文字を指定します。
プロパティとしているので、呼出し側で

```html
<my-marked allowed-type='["image/png","image/svg"]' progress-text="アップロード中..."></my-marked>
```

といったカスタマイズがしやすいです(配列の指定は`'`シングルクォーテーションを使用してください)

```js:src/my-marked.html
      _onDrop(e){
        e.stopPropagation()
        e.preventDefault()
        this.shadowRoot.querySelector('firebase-storage-multiupload').upload(
          Array.from(e.dataTransfer.files)
               .filter(f=>this.allowedTypes.indexOf(f.type) !== -1)
               .map(f=>{this._insertImageText(this.progressText); return f;}))
      }
```

ファイルがドラッグ&ドロップされた時の処理として、`e.stopPropagation()`と`e.preventDefault()`を指定してデフォルトの動作(別ページに画像を開いて遷移するなど)をキャンセルさせます。
`querySelector`で`<firebase-storage-multiupload>`を指定して`upload(files)`メソッドでファイルがアップロードされます。
ドラッグ&ドロップされたファイルは`e.dataTransfer.files`に格納されていますが`Array`(配列)ではないので、`Array.from`で配列にした上で`filter`で対象ファイルを制限し、`map`で戻しつつ、アップロード中を示す文字列を本文に挿入します。

```js:src/my-marked.html
      _onPaste(e){
        this.shadowRoot.querySelector('firebase-storage-multiupload').upload(
          Array.from(e.clipboardData.items || e.clipboardData.files || [])
               .filter(f=>this.allowedTypes.indexOf(f.type) !== -1)
               .map(i=>{this._insertImageText(this.progressText); return i.getAsFile();}))
      }
```

ペースト時も基本同じ要領ですが、`clipboardData`に入っているデータを最後に`File`として渡す`getAsFile()`を使用します。

```html:src/my-marked.html
      <firebase-storage-upload-task task="[[item]]" metadata="{{_metaData}}"></firebase-storage-upload-task>
         ...
          _metaData: {
            type: Object,
            observer: '_metaDataChanged'
          }
      ...
      _metaDataChanged(meta){
        if(meta) this._insertImageText('!['+meta.name+']('+meta.downloadURLs[0]+')'+"\n")
      }
```

個々のファイルアップロードがされる度に`_metadata`が更新されるので、最終的にそのファイル名(`meta.name`)とリンク先(`meta.downloadURLs[0]`)を取得してテキストを挿入をします。

```js:src/my-marked.html
      _insertImageText(text){
        const el = this.shadowRoot.querySelector('paper-textarea')
                       .shadowRoot.querySelector('iron-autogrow-textarea')
        const pos = el.selectionStart
        const ptext = this.progressText
        let offset = 0
        if(this.contents.substr(pos - ptext.length, ptext.length) == ptext)
          offset = ptext.length
        this.contents = this.contents.substring(0, pos - offset) + text +
                        this.contents.substring(pos, this.contents.length)
        el.selectionStart = pos + text.length - offset
        el.selectionEnd = pos + text.length - offset
        el.focus()
      }
```

`_insertImageText(text)`はテキストエリアに指定した文字列を挿入します。カーソルがある場所に挿入することと、複数アップロード時にすでにアップロード中を示す文字列(`progeressText`)がある場合は消す処理を加えています。
ちょっとわかりにくかったのはテキストエリアのカーソル位置を取得するのに、`<paper-textarea>`ではなく、その中にある`<iron-autogrow-textarea>`を指定しないと取得できなかったことですが、他にいい方法があるかもしれません。

## 最後に

イベントの取得やテキストエリアへの文字挿入など [`Inline Attachment 2.0.3`](https://github.com/Rovak/InlineAttachment) を参考にさせていただきました。

**:bow: コメント、編集リクエスト歓迎**

以上