copilotを駆使してなんとかサイドパネルのChrome拡張を作ってみた

初投稿です。

個人用プログラムで巡回用のプログラムを作った時に、URLリストをいちいちマウスの左クリックやCTRL+Cでやっているのが面倒になり、ドラッグ・アンド・ドロップでなんとかできないかと言うことで、最初はただのHTMLファイルを作ったのですが、少々不便なのでChrome拡張にしてサイドパネルに設置するようにしました。
元々のURLリストを作るHTMLも含めてほぼcopilot頼りです。

***

copilotに頼んで当初出来上がったのが以下のHTMLファイル
~~~HTML:dandd.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>URLリスト作成</title>
    <style>
        #dropzone {
            width: 100%;
            height: 200px;
            border: 2px dashed #ccc;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
        }
        ul {
            list-style-type: none;
            padding: 0;
        }
        #copyButton {
            margin-top: 20px;
            padding: 10px 20px;
            border: none;
            background-color: #4CAF50;
            color: white;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div id="dropzone">ここにURLをドラッグ＆ドロップ</div>
    <ul id="urllist"></ul>
    <button id="copyButton">クリップボードにコピー</button>
    <script>
        const dropzone = document.getElementById('dropzone');
        const urllist = document.getElementById('urllist');
        const copyButton = document.getElementById('copyButton');

        dropzone.addEventListener('dragover', (event) => {
            event.preventDefault();
            dropzone.style.borderColor = '#aaa';
        });

        dropzone.addEventListener('dragleave', () => {
            dropzone.style.borderColor = '#ccc';
        });

        dropzone.addEventListener('drop', (event) => {
            event.preventDefault();
            dropzone.style.borderColor = '#ccc';

            const items = event.dataTransfer.items;
            for (let i = 0; i < items.length; i++) {
                if (items[i].kind === 'string' && items[i].type.match('^text/uri-list')) {
                    items[i].getAsString((url) => {
                        const li = document.createElement('li');
                        li.textContent = url;
                        urllist.appendChild(li);
                    });
                }
            }
        });

        copyButton.addEventListener('click', () => {
            const urls = [];
            const listItems = urllist.getElementsByTagName('li');
            for (let i = 0; i < listItems.length; i++) {
                urls.push(listItems[i].textContent);
            }
            const urlText = urls.join('\n');
            navigator.clipboard.writeText(urlText).then(() => {
                alert('URLリストがクリップボードにコピーされました！');
            }).catch((err) => {
                alert('コピーに失敗しました: ' + err);
            });
        });
    </script>
</body>
</html>
~~~

これはこれで良かったのですが、別タブを開いて分離してサイズ変更して画面の端へというのが手間になり、Chromeのサイドパネルで使えれば便利だなと思ったのが作った理由です。

***

先のHTMLをこのままChrome拡張にしようとJSONファイルをつくったもののCSPエラーが出たので、copilotに対処をしてもらいました。
最初にJavaScriptは別ファイルへ切り離し、それでもエラーが解消されないのでCSSも切り離し。
結局エラーは解消できず。
それでも期待通りの動作はするのと、自分専用なので問題はないかと思いそのまま完了することに。
ついでにcopilotに頼んでドラッグ・アンド・ドロップで作ったURLリストをクリアしてもらうボタンも追加。サイドパネルの再読み込みは一度閉じてから再度開く必要があるので。
さらに、拡張機能ボタンを押してサイドパネルが開くように設定しました。こちらはGoogle公式のサンプルファイルを参照して自力で付けています。

~~~JSON:manifest.json
{
    "manifest_version": 3,
    "name": "URLリストサイドパネル",
    "version": "1.0",
    "description": "URLリストを管理するサイドパネル",
    "permissions": ["sidePanel"],
    "background": {
        "service_worker": "service-worker.js"
    },
    "action": {
        "default_title": "URLリストサイドパネル\nクリックするとサイドパネルが開きます"
    },
    "side_panel": {
        "default_path": "index.html"
    },
    "content_security_policy": {
        "extension_pages": "script-src 'self'; object-src 'self'"
    }
}
~~~

~~~JavaScript:script.js
const dropzone = document.getElementById('dropzone');
const urllist = document.getElementById('urllist');
const copyButton = document.getElementById('copyButton');
const clearButton = document.getElementById('clearButton');

dropzone.addEventListener('dragover', (event) => {
    event.preventDefault();
    dropzone.style.borderColor = '#aaa';
});

dropzone.addEventListener('dragleave', () => {
    dropzone.style.borderColor = '#ccc';
});

dropzone.addEventListener('drop', (event) => {
    event.preventDefault();
    dropzone.style.borderColor = '#ccc';

    const items = event.dataTransfer.items;
    for (let i = 0; i < items.length; i++) {
        if (items[i].kind === 'string' && items[i].type.match('^text/uri-list')) {
            items[i].getAsString((url) => {
                const li = document.createElement('li');
                li.textContent = url;
                urllist.appendChild(li);
            });
        }
    }
});

copyButton.addEventListener('click', () => {
    const urls = [];
    const listItems = urllist.getElementsByTagName('li');
    for (let i = 0; i < listItems.length; i++) {
        urls.push(listItems[i].textContent);
    }
    const urlText = urls.join('\n');
    navigator.clipboard.writeText(urlText).then(() => {
        alert('URLリストがクリップボードにコピーされました！');
    }).catch((err) => {
        alert('コピーに失敗しました: ' + err);
    });
});

clearButton.addEventListener('click', () => {
    urllist.innerHTML = '';
});
~~~

~~~JavaScript:service-worker.js
chrome.runtime.onInstalled.addListener(() => {
    chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
});
~~~

~~~CSS:styles.css
#dropzone {
    width: 100%;
    height: 200px;
    border: 2px dashed #ccc;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
}
ul {
    list-style-type: none;
    padding: 0;
}
#copyButton,#clearButton {
    margin-top: 20px;
    padding: 10px 20px;
    border: none;
    background-color: #4CAF50;
    color: white;
    cursor: pointer;
}

~~~

~~~HTML:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>URLリスト作成</title>
    <link rel="stylesheet" href="styles.css"></head>
<body>
    <div id="dropzone">ここにURLをドラッグ＆ドロップ</div>
    <ul id="urllist"></ul>
    <button id="copyButton">クリップボードにコピー</button>
    <button id="clearButton">リストをクリア</button> 
    <script src="script.js"></script>
</body>
</html>
~~~

***

エラーは出たままですが、なんとか目的の機能は作れたかなと思います。
生成AIにコードを書いてもらうのが便利という話を聞いていましたが、ここまで楽だとは思いませんでした。