1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLM系のサイトでCtrl+Enterで送信するようにした件について。

Posted at

Chrome 拡張機能を作るに当たって必要なもの

1.manifest.json
この記事を参考にしました。

・必須項目

{  
    “name”: “{拡張機能の名前}”, // 45字まで設定可能  
    “version”: “{拡張機能のバージョン}”, // 拡張機能の自動更新機能でバージョンチェックされるので、更新の際は既存verより大きい値にする 
    “manifest_version”: ”{マニフェストのバージョン}” // 2023年10月時点のバージョンは「3」  
}  

僕の場合はこう書きました

{
    "name": "Enter to Shift Enter",
    "version": "1.0",
    "manifest_version": 3,
}

マニフェストバージョンというのは、拡張機能を作るうえでの制約みたいなものです。
V2拡張機能は指定してももうロードできなくなってるので、3に指定しましょう。

・推奨項目

{  
    “description”: “{機能の説明}”, // 132字まで設定可能。Chromeウェブストアへ公開するための審査の際には必須 
    “default_locale”: ”{デフォルトに設定する言語のコード}”, // 「_locales」 ディレクトリを持つ拡張機能では必須  
    “icons”: { // インストール中及びChromeウェブストアで使用。表示サイズごとの指定も可能
        “{サイズ}”: ”{任意のファイルパス}” // PNGを基本とし、BMP、GIF、ICO、JPEGなども使用可能  
    }  
    “action”: { // 設定したアイコン(デフォルトはパズルのピース)をクリックしたときの挙動を設定できる
        “default_icon”: “{任意のファイルのパス}”, // ツールバーボタンで使用。表示サイズごとの指定も可能  
        “default_title”: “{任意のテキスト}”, // アイコンがフォーカスされた時に表示される  
        “default_popup”: “{任意のHTMLファイルのパス}” // アイコンクリック時に出現するポップアップ  
    }  
}  

僕のはこれだけです。

{
    "description": "EnterキーをShiftEnterにする拡張機能です",
}

(アイコン作るのめんどくさかったとか言えない...んで、後々アイコン画像が必要になったとか言えない...)

・content_script
特定のURLパターンに一致するサイトが開かれた時に使用されるJavaScript・CSSファイルを指定できます。

{  
    “content_scripts”: [  
        {  
            “matches”: [“{任意のURLパターン}”],  // 詳細は以下 
            “css”: [“{任意のcssファイルのパス}”], // 適用させたいcssのパス  
            “js”: [“{任意のjsファイルのパス}”], // 実行したいスクリプトのパス  
            “all_frames”: false or true, // デフォルトはfalse、詳細は以下  
            “run_at”: “{スクリプト挿入のタイミングを指定}”,  // デフォルトは"document_idle"、詳細は以下
        }  
    ]  
}  

僕のはこれです

{
    "content_scripts": [
        {
            "matches": [
                "*://gemini.google.com/*",
                "*://chatgpt.com/*",
                "*://www.genspark.ai/*"
                ],
            "js": ["content.js"],
            "all_frames": true
        }
    ]
}

geminiとchatgptとgensparkのサイトが開かれたときに、同階層にあるcontent.jsが動きます。
"all_frames":"true"とは、適用されるフレームをどこまでにするか指定します。
デフォルトはfalseで、表示されている一番上のフレームにのみ適用されます。
trueにすることでiframeを含むすべてのフレームに適用させることができます。

さあ!拡張機能を作ろう!!

・どう考えるか
Enterキーを禁止にして、それからctrl+Enterで送信のイベント発火させればよいかあって最初思ってた
けど、もっと簡単なのは
Enterキーを押されたときに、一緒にctrlも押されてた場合はそのままreturnさせ、それ以外では禁止にする
ってやるほうが良いなと思って最初に書いたコードがこちら!
どん!

document.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
        if (!e.ctrlKey) {
            e.preventDefault();
            alert('Enter禁止!')
        }
    }

});

1. keydownってなんだ
keydownとは、キーボードのキーを押された瞬間(文字が入る前)の段階でイベント発生させること。
他にも

・beforeinput: 入力が確定する直前

・input: 文字が入力された直後

・keyup: 指がキーから離れた瞬間

があります。

2. e(event)ってなんだ
関数の引数に設定されてるeはeventともかいてよい。
eventとは
イベントオブジェクトの内容である「イベント発生時の情報」。
多岐に渡るのですが、いくつかあげてみます⬇︎。

・ target
イベントを発生させた要素。ここでは button#btn1。

・ type
発生したイベントのタイプ。ここでは click。

・ pointerType
クリックなどの場合、イベントを起こしたデバイスの種類(マウス、ペン、タッチなど)

その他にもクリック位置に関するものなど、イベントが発生したときの情報が色々と詰まっています。

eventの中をのぞいてみると...
event.key: どの文字が入力されたか("a")
event.repeat: 長押し中かどうか(true)
event.shiftKey: Shiftキーを一緒に押しているか(false)
event.target: どこ(どの入力欄など)で起きたか(<body>)

3. preventDefaultってなんだ
event.preventDefault()でブラウザが本来行うはずの、決まった動作(デフォルトの動作)をキャンセルします。

まあ、これでも動かなかったんで、e.stopImmediatePropagation()とかいうのを追加しました。
さらに、true(キャプチャフェーズ)を指定しました。

document.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
        e.preventDefault();
        e.stopImmediatePropagation();
    }
}, true);

e.stopImmediatePropagation()と、addEventListenerの第三引数とは
e.stopImmediatePropagation()はイベントの伝播が上にいくのを防ぐためのものです。
イベント伝播についてはこちらの記事を参考にしてください

addEventListenerの第三引数は、
true・・・キャプチャーフェーズ時に発火する。(つまり親から先に発火)
false・・・バブリングフェーズ時に発火する・(つまり子から先に発火)←初期値
という感じになってます
あ、この記事もよかったら

・次はEnterで改行できるように!

document.addEventListener("keydown", (e) => {

    if (e.key === "Enter") {

        if (!e.ctrlKey) {

            e.preventDefault();

            e.stopImmediatePropagation();

        }

        const shiftEnter = new KeyboardEvent("keydown",{

            key: "Enter",

            code: "Enter",

            shiftKey: true,

            bubbles: true

        });

        e.target.dispatchEvent(shiftEnter);

    }

},true);

const shiftEnter = new KeyboardEvent("keydown",{KeyboardEventオブジェクトのインスタンス化をする

{
            key: "Enter",
            code: "Enter",
            shiftKey: true,
            bubbles: true
        }

ここでは、shhiftEnterというインスタンスがもつ、イベントオプションを指定しています

e.target.dispatchEvent(shiftEnter)
作成したイベントを、実際に特定の要素(e.target)に対して発火(実行)させています。

e.target: 現在操作している要素(入力欄など)を指します。
e.targetととは、イベントが発生した要素そのものを指します。
dispatchEvent: 「このイベントを今すぐ実行せよ」という命令です。

・このコードの問題点
このコードはEnterというキーが押されるイベントが発生したら、動く関数ですが、
最終的にdispatchEventで発火させてるのは、Enterです。
つまり、Enterをキャンセルして、ctrl+Enterが押されたときにEnterが発火するようになってるので、またこの関数に入ってきて、無限ループになります。
なので、次のように改善しました。

document.addEventListener("keydown", (e) => {

    if (e.key === "Enter" && e.shiftKey) return;

    if (e.key === "Enter") {

        if (e.isComposing) return;

        if (!e.ctrlKey) {

            e.preventDefault();

            e.stopImmediatePropagation();

        }

        const shiftEnter = new KeyboardEvent("keydown",{

            key: "Enter",

            code: "Enter",

            shiftKey: true,

            bubbles: true

        });

        e.target.dispatchEvent(shiftEnter);

    }

},true); 

こうすることで、e.key === "Enter" && e.shiftKeyで、
押されたキーがEnterかつshiftKeyという変数が存在(true)しているときは早期リターンして関数を抜けること
ができます。

実は、これで動いたのはChatGPTだけでした。...なんで??

ChatGPTとGemini両方でできるように

原因をさぐると、geminiとchatgptでは入力欄がchatgptはtextareaなのに対して、geminiはdivタグにcontenteditable="true"属性を追加することで入力フォーム作ってた。いや、なにそれ!?
どうやら、リッチテキストエディタ(Quill.jsベース)というものらしい。

ということで書き直したのがこちら

document.addEventListener("keydown", (e) =>{
    if (e.key === "Enter" && !e.isComposing && !e.shiftKey) {
        if (e.ctrlKey) return;
        e.preventDefault();
        e.stopImmediatePropagation();
        const target = e.target;
        if (target.isContentEditable) {
            document.execCommand('insertLineBreak');
        }
        const shiftEnter = new KeyboardEvent("keydown",{
            key: "Enter",
            code: "Enter",
            shiftKey: true,
            bubbles: true
        });
        e.target.dispatchEvent(shiftEnter);
    }
}, true)

ここでは

if (target.isContentEditable) {
            document.execCommand('insertLineBreak');
        }
        const shiftEnter = new KeyboardEvent("keydown",{
            key: "Enter",
            code: "Enter",
            shiftKey: true,
            bubbles: true
        });
        e.target.dispatchEvent(shiftEnter);
    }
}, true)

で、前半のif文ではdocument.execCommand('insertLineBreak')というもので、強制的にイベント発火させています。
これはgeminiが、誰がイベント発火させたのかを厳しく監視しているので、JavaScriptの dispatchEvent で作ったイベントには isTrusted: false というフラグが自動で付きます。
Geminiの入力欄は「人間が実際にキーを押した(isTrusted: true)」イベントしか受け付けない設定になっている可能性が高いからです。

document.execCommand('insertLineBreak')はブラウザ標準の「改行を挿入せよ」というコマンドです。Geminiのような特殊な入力欄でも、このコマンドなら「人間が操作した」のと同様に扱ってくれます。

完成。

manifest.json

{
    "name": "Enter to Shift Enter",
    "version": "1.0",
    "manifest_version": 3,
    "description": "EnterキーをShiftEnterにする拡張機能です",
    "content_scripts": [
        {
            "matches": [
                "*://gemini.google.com/*",
                "*://chatgpt.com/*",
                "*://www.genspark.ai/*"
                ],
            "js": ["content.js"],
            "all_frames": true
        }
    ]
}

content.js

document.addEventListener("keydown", (e) =>{
    if (e.key === "Enter" && !e.isComposing && !e.shiftKey) {
        if (e.ctrlKey) return;
        e.preventDefault();
        e.stopImmediatePropagation();
        const target = e.target;
        if (target.isContentEditable) {
            document.execCommand('insertLineBreak');
        }
        const shiftEnter = new KeyboardEvent("keydown",{
            key: "Enter",
            code: "Enter",
            shiftKey: true,
            bubbles: true
        });
        e.target.dispatchEvent(shiftEnter);
    }
}, true)

みんなもやってみてね!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?