ZenhubのタイトルをコピーするChrome拡張機能を作ってみた

2023.07.07

はじめに

私のプロジェクトではZenhubでissueの管理を行っています。そして、進捗報告の文章を書くときなどに、issueのタイトルを貼り付けることが多いです。

もちろん、タイトルをこのように選択してCtrl + Cを押せばコピーできます。

しかし、左端を選択して右端までドラッグしてコピーするという行為が少し手間に感じます。また、コピーしたタイトルをそのままNotionにペーストすると、このようにissue番号のところで改行されたり、太字になったりします。

そこで、ワンクリックでZenhubのタイトルをコピーしてくれるChrome拡張機能を作ってみました。

manifest.json

まず初めにマニフェストファイルを作成します。

マニフェストファイルとは、拡張機能についての情報など様々な設定を記述するファイルです。

必ず必要な項目は以下の3項目です。

  • manifest_version:マニフェストファイルのバージョン(2023年7月現在は3)
  • name:拡張機能の名前
  • version:拡張機能のバージョン

他にはどんな設定項目があるのか、各設定項目の詳細についてはドキュメントを参照してください。

Manifest file format - Chrome Developers

今回は以下のようなマニフェストファイルを作成しました。

{
    "manifest_version": 3,
    "name": "copy zenhub title",
    "description": "zenhubのタイトルをコピーするボタンを表示する",
    "version": "1.0.0",
    "permissions": [
        "activeTab"
    ],
    "content_scripts": [
        {
            "matches": ["https://app.zenhub.com/workspaces/*"],
            "js": ["script.js"],
            "css": ["script.css"]
        }
    ]
}

content_scriptsのところで、埋め込むスクリプト(js、css)と、どのページに埋め込むかを指定しています。

script.js

実際にページを開いたときに埋め込んで動作させるJavaScriptファイルです。

DOMを見てみると、issueのタイトルが拾える属性はdata-testid="issue-title”ぐらいしかなさそうでした。そこで、この属性をセレクタとして使いました。

※data-testidは本来テスト用の属性みたいです。

【Rspec】data-testidを使ってテスト対象を明確にする

コードは以下のようになりました。

issueのタイトル部分の近くにコピーボタンを表示し、ボタンのクリックイベントとしてクリップボードに書き込む処理を登録しています。

function buttonClick() {
    // ブラウザで使えない場合は終了
    if (!navigator.clipboard) {
        console.log("このブラウザは対応していません")
        return;
    }
    const title = document.querySelector('[data-testid="issue-title"]').innerText
    // クリップボード書き込み
    navigator.clipboard.writeText(title)
}

// オプション
const options = {
    childList: true,
    characterData: true,
    characterDataOldValue: true,
    attributes: true,
    subtree: true,
}

// DOMの変更を検知した場合のコールバック関数
function domChanged() {
    const title_dom = document.querySelector('[data-testid="issue-title"]')
    if(title_dom){
        const copyButton = title_dom.parentNode.querySelector('button')
        if(!copyButton){
            const button = document.createElement('button')
            button.type = 'button'
            button.onclick = buttonClick
            button.className = 'gg-copy'
            // 最後の子要素として追加
            title_dom.parentNode.appendChild(button)
        }
    }
}

// body以下に変更があった場合に通知
const target = document.querySelector('body');

// DOM変更監視を開始
const observer = new MutationObserver(domChanged);
observer.observe(target, options);

苦労した点としては、最初はwindow.onload時にissueのタイトルを取得しようとしたのですが、うまくいきませんでした。

というのも、ZenHubは動的にページを読み込んでいて、window.onloadではDOMを検知できませんでした。また、ZenHubは各issueのページに遷移するのではなく、画面の右端からモーダルウインドウが表示されるので、その場合は何というイベントでトリガーすればいいのだろうと悩みました。

setTimeoutで何秒か待って…など試行錯誤しましたが、結果的にMutationObserverというDOMの変更を監視する機能を使ってうまくいきました。

script.css

ボタンのアイコンはこちらのサイトのものを使用しました。

700+ CSS Icons, Customizable, Retina Ready & API

Copy - CSS Icons

こちらに記載されているcssをそのままコピーさせて頂きました。

拡張機能を読み込む

拡張機能のページchrome://extensions/を開きます。右上に「デベロッパーモード」と書かれたトグルがあるので、ONにします。

すると、このようにメニューが表示されます。「パッケージ化されていない拡張機能を読み込む」をクリックして、拡張機能のファイルが格納されているフォルダを選択します。

このように作成した拡張機能が表示されます。読み込んだ後にコードを修正した場合は、更新アイコンをクリックすれば最新状態に更新されます。

拡張機能を読み込んだ状態でZenHubのissueを開いてみると、このようにコピーボタンが表示されました。見た目は不格好ですが、クリックするとちゃんとクリップボードにタイトルがコピーされました。

おわりに

Chrome拡張機能はなんだか難しそうなイメージがありましたが、意外と気軽に作れるものだということがわかりました。

今後も何かちょっとした手間を削減するのに活用していきたいです。