JSを使ってクリップボードへコピーする機能を実装した

# 前文

　しばらく使わないうちにQiitaのエディタが新しくなっていた。書き心地がいい。

　個人開発アプリに、日報に登録した文面をクリップボードへコピーする機能を追加した。twitterやSlackなどへシェアしたいときに便利だ。実装の仕方をここにまとめる。

# 0. 流れ

```
 1.DBにコピー用の文章を保存するカラムを追加
 2.日報を登録時にコピー用の文章も保存
 3.日報一覧画面にコピーボタンを作る
 4.JSでクリップボードへコピー
```


# 1.DBにコピー用の文章を保存するカラムを追加

 日報のマイグレーションファイルはこのようになった。日報の詳細文章である `content` の他に`content_for_share`を追加している。

```rb:
class CreateReports < ActiveRecord::Migration[6.0]
  def change
    create_table :reports do |t|
      t.references :user, null: false, foreign_key: true
      t.datetime :reported_on, null: false
      t.string :content, null: false
      t.string :content_for_share
      t.timestamps
    end
  end
end
```

# 2.日報を登録時にコピー用の文章も保存

　日報を登録するときに、コピー用の文章を整形して、`content_for_share`へ保存する。

コントローラーは以下のようになった。

```rb:app/controllers/reports_controller.rb
#　略

  def create
    # 略

    formatted_para = report_params
    formatted_para[:report_items_attributes] = para
    genres_set = get_genre_nameset

    share_content = Report.convert_content_shared(formatted_para, genres_set)
    @report.content_for_share = share_content
    # 以降、日報の保存処理

  end
```


formatted_paraの中身はちょっとわかりにくい。最終的にはこのようになっている。

```rb
> formatted_para
=> <ActionController::Parameters {
    "content"=>"本日のコメント", 
    "reported_on"=>"2023-04-23", 
    "report_items_attributes"=><ActionController::Parameters {
        "0"=><ActionController::Parameters {
            "content"=>"やったこと1", 
            "genre_name"=>"Rails", 
            "genre_id"=>1
        } permitted: true>, 
        "1682190268018"=><ActionController::Parameters {
            "content"=>"やったこと2",
            "genre_name"=>"個人開発",
            "genre_id"=>10
        } permitted: true>
    }
     permitted: true>
} permitted: true>
```
genres_setはこう

```rb
> genres_set
=> [[1, "Rails"], [2, "英語"], [3, "Vue"], [10, "個人開発"]]
```

`convert_content_shared`メソッドはReportモデルに書いている。

```rb:app/models/report.rb
#略

  def self.convert_content_shared(formatted_para, genres_set)
    # すっごい、いろいろ省略
    date + "\n" + formatted_items + "\n" + content
  end
```

整形された文字を戻り値としている。

# 3.日報一覧画面にコピーボタンを作る

　viewファイルにボタンを作る。

```erb:app/views/reports/_reportlist.html.erb
<!-- 略 -->
      <div class="col-4 col-sm-3 col-xl-2">
        <button type="button" class="btn btn-sm btn-outline-primary js-copy-report" data-report-id="<%= report.id %>"><i class="far fa-clipboard mr-1"></i>COPY</button>
        <div class="d-none" share-report-id="<%= report.id %>"><%= report.content_for_share %></div>
      </div>
<!-- 略 -->
```

　ポイントはコピー用の文章を非表示で載せること。これを後の処理で使う。bootstrapを使っているので`btn-sm` のようなclass名になっている。


 # 4.JSでクリップボードへコピー
　ボタンが押されたときにイベントを作る。


```js:app/javascript/home.js
  const copyReportButtons = document.querySelectorAll('.js-copy-report');
  if (document.querySelectorAll('.js-copy-report') !== null){
    copyReportButtons.forEach(function(button) {
      const reportId = button.dataset.reportId;

      button.addEventListener('click', function(event) {
        event.preventDefault();
        button.textContent = 'Copied!';
        setTimeout(() => {
          button.innerHTML = `<i class="far fa-clipboard mr-1"></i>COPY`;
        }, 1000);
        copyText = document.querySelector(`[share-report-id="${reportId}"]`).textContent;
        copyToClipboard(copyText);
      });
    });
  }


function copyToClipboard(text) {
  navigator.clipboard.writeText(text)
    .then(() => {
      // console.log('Copied to clipboard');
    })
    .catch((error) => {
      // console.error('Failed to copy to clipboard', error);
    });
```

`js-copy-report`がついているボタン要素ひとつひとつにイベント追加していく。ボタンのreportidを読み取り、viewファイルに記述されている日報のデータと紐づける。非表示だが記入はされているので、わざわざDBに取得に行かなくて良い。

　ボタンが押されたら少しの間、文字がCopied!になる。
　
　クリップボードへのコピーは`navigator.clipboard.writeText(text)` を使うだけで良い。


# おわり

　これでコピーする機能を実装できた。これから詰まったところを紹介していく。書いておくと、自分で忘れた時も便利でいいなあ。