Backlogで「リンク付きの課題キーと件名」を取得するボタンを追加するChrome拡張機能を作ってみた

2023.04.26

データアナリティクス事業本部のueharaです。

今回はBacklogで「リンク付きの課題キーと件名」を取得するボタンを追加するChrome拡張機能を作ってみたいと思います。

何がしたいのか?

Backlogには標準機能として課題キーと件名をコピーするボタンがあります。

しかし、少なくともMac Bookではこのボタンでコピーを行ったあと、Google Docsに貼り付けると次のようになります。

うーむ、、、

私としては、以下のようなプレーンなリンク付きテキストで貼り付けられるのが希望です。

今までは「①一旦書式無しテキストで貼り付け ⇒ ②課題チケットに戻りURLをコピー ⇒ ③テキストにリンクを紐付け」という手順を踏んでこの形にしていたのですが、流石に面倒になってきたので、今回Chromeの拡張機能を作ってみました。

どう対応したのか?

標準ボタンの隣に1つボタンを追加し、1クリックで上記の「プレーンなリンク付きテキスト」を取得することができるChromeの拡張機能を作成しました。

これにより今までのストレスが無くなり、快適にGoogle Docsに課題チケットを転記することができるようになりました。

実装

ファイル構成

今回作成した拡張機能のファイル構成は次の通りです。

my_extension
 ├ css
 | └ style.css
 ├ icons
 | ├ icon16.png
 | ├ icon48.png
 | └ icon128.png
 ├ js
 | └ content.js
 └ manifest.json

manifest.json

詳しい説明は省略しますが、Chromeの拡張機能を作る際にはまず manifest.json が必要になります。

現状使われているものとしてはmanifest V2とmanifest V3がありますが、V2の方は2023年6月に例外なく動作しなくなります

今から作成する場合はV3にしておく方が無難です。

ということで、以下のように書いてみました。

manifest.json

{
  "name": "BacklogLinkCopy",
  "version": "0.1.0",
  "manifest_version": 3,
  "description": "For Backlog Link Copy",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "host_permissions": [
    "https://<クラメソのサブドメイン>.backlog.jp/*"
  ],
  "permissions": [
    "clipboardWrite",
    "activeTab"
  ],
  "content_scripts": [{
    "matches": [
      "https://<クラメソのサブドメイン>.backlog.jp/*"
    ],
    "css": [
      "css/style.css"
    ],
    "js": [
      "js/content.js"
    ],
    "run_at": "document_end",
    "all_frames": true
  }]
}

今回、ターゲットのURLとして特定ドメインのBacklogに限定していますが、他のBacklogでも使いたい場合はURLを追加するであったり、*を使うなどで対応することができます。

それぞれの記載についてはなんとなく文字からイメージがつくかと思いますが、より詳しく知りたい方は公式のドキュメントをご確認ください。

style.css

追加するボタンのスタイルを決めます。

今回はできるだけ標準のボタンと近い形となるよう、以下のように設定しました。

style.css

.new-button {
    position: absolute;
    margin-left: 7px;
    background-color: #3ea8ff;
    color: white;
    line-height: 1;
    height: 32px;
    width: 32px;
    text-align: center;
    border: none;
    border-radius: 50%;
    font-size: 18px;
    cursor: pointer;
}

.new-button:active{
    background-color: #0f83fd;
    color: white;
}

content.js

こちらが今回のメインとなる部分です。

content.js

console.log("[DEBUG] load extension");

window.addEventListener('load', main, false);
window.addEventListener('popstate', main);
window.addEventListener('pushstate', main);
window.addEventListener('replacestate', main);

const loadFontAwesome = () => {
    let link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = 'https://use.fontawesome.com/releases/v5.13.1/css/all.css';
    document.head.insertAdjacentElement('beforeEnd', link);
};

function main(event) {
    const jsInitCheckTimer = setInterval(jsLoaded, 100);

    // fontawesomeのロード
    loadFontAwesome();
    
    function jsLoaded() {
        if (document.querySelector('#copyKey-help') != null) {
            clearInterval(jsInitCheckTimer);

            // ボタンを追加する場所を選択
            let existingButton = document.querySelector('#copyKey-help');

            // ボタン要素を作成
            let newButton = document.createElement('button');
            // ボタンのテキストを設定
            newButton.innerHTML = '<i class="far fa-copy"></i>';
            newButton.classList.add('new-button');

            // ボタンを追加
            existingButton.appendChild(newButton);

            // ボタンが押された時の処理
            newButton.addEventListener('click', function() {
                let currentUrl = window.location.href;
                let ticket_key = document.querySelector(".ticket__key-number").innerText;
                let subject = document.querySelector(".markdown-body").innerText;
                let title = ticket_key + " " + subject;

                let htmlLink = '<a href="' + currentUrl + '">' + title + '</a>';
                const blob = new Blob([htmlLink], { type: 'text/html' });
                const blobPlain = new Blob([htmlLink], { type: 'text/plain' });
                const data = [new window.ClipboardItem({ 'text/html': blob, 'text/plain': blobPlain })];

                navigator.clipboard.write(data)
                    .then(function() {
                        console.log('Title copied to clipboard');
                    })
                    .catch(function(error) {
                        console.error('Failed to copy title: ', error);
                    });
            });
        }
    }
}

そこまで難しい処理は無いですが、1点だけ注意したい部分があります。

今回標準機能のボタン #copyKey-help の隣に新しいボタンを追加したいのですが、標準ボタンは動的に生成される要素のため、単純に.appendChild(newButton) をしようとすると高確率でエラーとなります(標準ボタンの生成前にappend処理が動いてしまう)。

したがって、動的なページの読み込みが完了してからChrome拡張機能を実行するようにしています。

また、今回ボタンのアイコンにFontAwesomeを利用したかったので、ヘッダにスタイルシートを追加する処理も記載しています。

冒頭で示した「プレーンなリンク付きテキスト」は以下のような形で作成することができます。

let htmlLink = '<a href="' + currentUrl + '">' + title + '</a>';
const blob = new Blob([htmlLink], { type: 'text/html' });
const blobPlain = new Blob([htmlLink], { type: 'text/plain' });
const data = [new window.ClipboardItem({ 'text/html': blob, 'text/plain': blobPlain })];

作成した拡張機能の読み込み

作成したオレオレ拡張機能の読み込みは、Chromeの拡張機能のページにあるデベロッパーモードをONにし、「パッケージ化されていない拡張機能を読み込む」ボタンから実施することができます。

その後、my_extensionフォルダを選択すれば拡張機能の読み込みは完了です。

※私はアイコンをBacklogのロゴに設定したので、上記のような表示となっています。(my_extensionフォルダ配下のiconsに配置している.pngファイルが表示されるアイコンになります)

最後に

今回は、Backlogで「リンク付きの課題キーと件名」を取得するボタンを追加するChrome拡張機能を作ってみました。

本記事がどなたかの参考になりましたら幸いです。

参考文献