User JavaScript and CSS (Chrome 拡張機能) を使って Redmine をカスタマイズしてみた

こちらの記事は [free_dev_com Advent Calendar 2019 - Adventar](https://adventar.org/calendars/4736) 7 日目の記事です。

## User JavaScript and CSS とは

Chrome の拡張機能として提供されている，既存のウェブサイトに独自の Javascript や CSS を埋め込むライブラリです．
[User JavaScript and CSS - Chrome ウェブストア](https://chrome.google.com/webstore/detail/user-javascript-and-css/nbhcbdghjpllgmfilhnhkllmkecfmpld)

これを利用して，ダークモードがないウェブサイトにダークモードを実装してみたり， Yahoo! のサイトに自分の好きなキャラクターのイラストを表示させたり，いろいろなことができます．
今回は，社内のタスク管理に使用している Redmine で，チケットの説明に出てくるスニペットの内容をクリップボードにコピーする機能を Javascript で実装してみました．

## 画面設計

スニペットの枠外，右下部分にコピーのアイコンを配置し，

![copy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/d835891f-2f19-db1e-f40e-b603657a360b.png)

アイコンをクリックすると「コピーしました！」のダイアログを表示することにします．

![copied.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/a24bbd4f-708f-5145-f2ee-62dc0550b743.png)

## 実装

### アイコンを読み込む

今回は Font Awesome のアイコンを使用します．
Font Awesome については[こちら](https://fontawesome.com/)

流れは次の通りです．

- Font Awesome を読み込む link タグを作成する
- head タグを取得し，その子要素として先ほど作成した link タグを追加する

```javascript:redmine.js
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', 'https://use.fontawesome.com/releases/v5.6.3/css/all.css');

document.getElementsByTagName('head')[0].appendChild(link);
```

### スニペットの枠外，右下部分に要素を配置する

流れは次の通りです．

#### スニペット (pre タグ) を取得する -> 各スニペットが配列で取れます

#### 各スニペットに対して，次の処理を行う
- コピーボタンを配置するための p タグを作成する (snippet)
- 後にコピーボタンとなる span タグを作成する (copyButton)
- (copyButton に title, innerHTML, className などの属性を設定する)
- copyButton にクリックのイベントリスナーを付与する (内容は下記)
  - コピー対象となる textarea タグを作成 (textarea タグでなくても行けるかもしれません)
  - スニペット内の文章を textarea タグに設定する
  - 作成した textarea タグを body タグの子要素として配置 -> 選択 -> コピー
  - textarea タグは不要になるので取り除く
  - コピーが完了した旨のメッセージを表示
- copyButton を snippet の直後に配置する

```javascript:redmine.js
// スニペットのタグを選択
const snippets = document.getElementsByTagName('pre');
// console.log(snippets); // HTMLCollection
// console.log(typeof snippets); // object

// コピーボタンを配置
Object.keys(snippets).forEach(function (index) {
    let snippet = snippets[index];
    let p = document.createElement('p');
    p.className = 'copy-snippet';

    let copyButton = document.createElement('span');
    copyButton.title = 'Copy to Clipboard';
    copyButton.innerHTML = '<i class="far fa-copy"></i> Copy';
    copyButton.className = 'copy-button';

    copyButton.addEventListener('click', function () {
        let copyTarget = document.createElement('textarea');
        copyTarget.textContent = snippet.innerText.trim(); // 見た目に合わせて文末改行削る？
        let body = document.getElementsByTagName('body')[0];
        body.appendChild(copyTarget);
        copyTarget.select();
        document.execCommand("copy");
        body.removeChild(copyTarget);
        // 後処理まで終わったらおまけ
        (function () {
            alert('コピーしました！');
        })();
    });
    p.appendChild(copyButton);
    snippet.parentNode.insertBefore(p, snippet.nextSibling);
});
```

### その他

画面右下にカエルのイラストを入れて，全体を CSS で整えます．

```javascript:redmine.js
// カエルがペコペコおじぎする GIF
const imageUrl = 'https://sozai-good.com/download?id=2451&type=7&subnumber=0&extention=gif';

// 画像挿入処理
let img = document.createElement('img');
img.id = 'inserted-image';
img.height = 105;
img.src = imageUrl;
const wrapper = document.getElementById('wrapper');
wrapper.parentNode.insertBefore(img, wrapper.nextSibling);
```

```css:redmine.css
#wrapper, #main {
    background-image: url("https://www.pakutaso.com/shared/img/thumb/mizuho1102dssd_TP_V.jpg");
    background-repeat: no-repeat;
    background-size: cover;
    background-attachment: fixed;
    opacity: 0.96;
}

#top-menu {
    /*background-color: darkgreen;*/
    background-color: rgba(10, 106, 10, 0.9);
}

#header, #footer {
    /*background-color: forestgreen;*/
    background-color: rgba(0, 0, 0, 0.5);
}

#main-menu li a {
    /*background-color: darkgreen;*/
    background-color: rgba(10, 106, 10, 0.6);
}

#main-menu li .selected {
    color: lightgreen;
}

#content {
    margin-bottom: 20px;
}

#sidebar {
    background-color: white;
}

#footer {
    position: fixed;
    background-color: black;
    right: 0;
    bottom: 0;
}

pre, .CodeRay {
    font-family: Monaco, monospace;
}

#inserted-image {
    position: fixed;
    right: 25px;
    bottom: 45px;
}

.copy-snippet {
    text-align: right;
}

.copy-button {
    cursor: pointer;
    background: none;
    border: 0;

    /* settings when hovered */
    /*transition: color 0.1s linear;*/
}

.copy-button:hover {
    color: red;
}
```

## Redmine の公式ページを表示してみる

[Defect #32525: CSV related tests fail with Rails 5.2.4 - Redmine](https://www.redmine.org/issues/32525)

![sample_page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/3316d4a9-cba0-f953-ca8e-e680b32b1cdb.png)

やったー！

## 効用

- 「コマンドはコピペして貼り付ける」のが暗黙のルールとなっている本番環境へのリリース作業において，逐一スニペットの中身をマウスで選択する手間が省けます．
- 自分の画面だけに実装されるので，他の人に自慢できます．
- フロント周りがちょっとだけ楽しくなります．