Open10

Enterくんの開発メモ

ひげひげ

型アサーションを使うタイミング

型アサーションを使うのは気が引けるけど使わなきゃ無駄なコードが増えてしまってて悩んでいた。下の記事では開発者がコンパイラよりも型についてよく知っているときに型アサーションを使うべきと書かれていた。サンプルコードとしてquerySelectorを使ったものが挙げられていた。まさに、拡張機能開発ではサイト内のDOMを扱うことが多いから、querySelectorなどDOMを利用するときに型アサーションを使ってみる。
https://zenn.dev/mrbabyyy/articles/0be002fedafef4#ではいつ型アサーションを使うべきか

ひげひげ

content scriptが動かなくなった

  • ターミナルでctrl+c を入力しブラウザを再起動
  • 別のファイルを編集していないか確認
  • alert("hello")とかきどこで止まっているか確認
  • matches: ['<all_urls>'], とかき全てのURLに対してalertが機能するか確認し、urlのマッチパターンを書き直してみる
  • startUrlsのリンクが正しいか確認。再起動して開くページかstartUrlsのURLなので、ここで間違ったURLを指定してたら再起動ても意味ない。
  • コメントアウトしたまま忘れてないか?
ひげひげ

Enterで送信するのを止める実装

ChatGPTで機能したコードの一部。


    // ユーザーが押したキーイベント出ないなら無視(これによりKeyboardEventで作成したイベントを無視して無限ループを防いでいる。)
    if (!event.isTrusted) return;

    if (event.code === "Enter" && !event.ctrlKey && !event.metaKey) {
        event.preventDefault();
        let shiftEnter = new KeyboardEvent('keydown', {
            key: "Enter",
            code: "Enter",
            shiftKey: true,
            bubbles: true,
            cancelable: true,
        });
        target.dispatchEvent(shiftEnter);
    }

ポイントは4つ

  1. isTrustedプロパティを使い、ユーザーがキーボード入力して発生したイベントか、コード上で作成されたイベントかを識別している。
  2. Enterを押したときのみ実行する条件を書く。Ctrl、Win、Cmdキーと同時に押されたときはスキップ
  3. preventDefault()を使い、Enterを押したときの挙動を全てストップ。ブラウザデフォルトも開発者が書いたものも止める。
  4. 新たにShift+Enterが押されたことにするイベントを作成し、dispatchEvent()でイベントを実行

1がなければnew KeyboardEventで作成したShift+Enterのイベントが、2の条件をパスしてしまい無限ループに陥る。

2 ShiftやCtrlなどの装飾キーは専用のプロパティがあるのでそれを使っている。

3によってエンターを押して送信するイベントリスナーがどの要素に登録されているかわからなくても、とりあえず送信を止めることができる。サイト内を分析せずとも、自分で新しく作ってしまえばいいという発想はなかったので勉強になる
しかし変換確定の処理もpreventDefault()によりストップされてると思う。理屈はわからないがcomposingイベントが停止されて偶然に動いているだけ。2の条件で変換中だとスキップする条件をつけなければならないね。このIssueはそれが原因かと思う。

4によってサイト内でShift+Enterが改行に使われることを利用して、textareaが使われていなくても簡単に改行することができる。こう書けば簡単にしてるように思えるがなるほどなと思った。
ある程度リッチなテキストエディタを備えたサイトはテキストエリアを<textarea>で作られていない。ChatGPTも同様で、改行したら\nが挿入されるのではなく、新たなpタグが挿入される。なので末尾に\nを挿入するなど安直な方法が使えない。それならば、テキストエディタを分析して改行する仕組みを調査するのではなく、サイト内で使われている改行のショートカットを利用した方が圧倒的に簡単
preventDefaultはeventのメソッドだが、dispatchEventは要素のメソッド
dispatchEventは初めてみたのでメモ
https://ja.javascript.info/dispatch-events

ひげひげ

テキストエリアの探し方

リッチなエディタを実装しているサイトは単に<textarea>を使っているわけではない。大抵はdiv要素にcontentedittableを使ってCSSを駆使して作られている。なのでdocument.querySelector("textarea")を使っても現在見えているテキストエリアの挙動制御できるわけではない。

どうやってテキストエリアを探すのかというと次のコードをDevToolsのコンソールに入力する。

document.addEventListener("focusin", (event) => {
    console.log(event.target)
});

その後テキストエリアをクリックする。そうしたら現在フォーカスしているテキストエリアの情報が表示され、この情報からDOMを選択する。

ひげひげ

deepseekの対策を突破する

deepseekはコンピューターからの入力をさせない工夫を施しているようだ。Enterを押したら改行するということすら簡単にさせてくれない

失敗 KeybaordEventを送信する

ChatGPTのようにnew KeyboardEventでShift+Enter を押したときのイベントを発生させたが改行されない。おそらくisTrustedプロパティがtrueの時だけ、つまり人手で打ち込んだ入力だけをテキストエリアに反映させている。

失敗2 textarea.value += "\n" で改行記号を直接挿入する

DeepSeekでは<textarea>で入力欄を実装してたので、textareaのvalueに改行記号を直接挿入すればいいと思い、次のコードをDevToolsのコンソールに入力した。これで入力欄に文字を挿入できるのだが、その後入力欄をクリックすると改行とaが消えてしまう。ただこの挙動はZennの入力欄でも見た気がする。悪意があって対策しているのではなく入力欄に保存している文字と、入力欄に表示している文字の2つを保管して、入力欄に何か変化があったら入力欄の反映させる?という挙動だった気がする。
つまりtextarea.valueを使って直接データを修正するのは悪手。確かReactでもそうだったはず。

    const textarea = document.querySelector("textarea")
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const value = textarea.value;

    textarea.value = value.slice(0, start) + "\n" + value.slice(end) + "a";
    textarea.selectionStart = textarea.selectionEnd = start + 1;

対策1 CSPを削除

コンテンツセキュリティーポリシーによりコンテンツスクリプトを挿入することが禁止されている。なのでalert("hello")といったコードすら外部から挿入することはできない。拡張機能だとmanifest.jsonのpermissionsにdeclarativeNetRequestと'declarativeNetRequestWithHostAccess`を追記することで、リクエストヘッダーを修正することができるのでこの対策は簡単

対策2 stopPropagationで親要素へのイベントバブリングを止める

ChatGPTは入力欄にEnterを押されたときに反応するイベントリスナーを登録してたっぽいのでpreventDefault()でEnter送信を防ぐことができた。DeepSeekだとpreventDefault()を使ってもEnter送信されてしまうので、入力欄以外の要素でイベントリスナーが登録されていることがわかる。そこでstopPropagation()を使い、親要素へイベントが伝播することを防いだ。するとEnter送信を防ぐことができた。また嬉しい誤算だが、特にShiftEnterのイベントを発生させなくてもEnter改行ができている。
ちなみに入力欄でpreventDefault()を設定すると改行されない。なのでEnter送信のイベントを入力欄以外に設定されてくれているのが功を奏した。

clipboardに\nをコピーしペースト

これもカスタムイベントを発生させるだけだったのでできなかった。やはりisTrustedを監視して入力かどうかを判断しているのか?

stopPropagation() と preventDefault() の違い

stopPropagationは自分のデフォルトの挙動は実行する。親要素にイベントの発生は伝えない
preventDefaultは自分のデフォルトの挙動は止める。親要素にイベントの発生は伝える。
https://stackoverflow.com/questions/5963669/whats-the-difference-between-event-stoppropagation-and-event-preventdefault

対策3 isTrustedがtrueのイベントを作成する

chrome.debugger.sendCommandisTrustedがtrueなイベントを発生させることができるらしい。今回はstopPropagation()を利用してEnter改行を実現できたので、このAPI使わなかったが裏技用として頭の片隅に入れておきたい
https://stackoverflow.com/questions/34853588/how-do-you-trigger-an-istrusted-true-click-event-using-javascript-in-a-chrome

ひげひげ

chrome.tabs はコンテンツスクリプトで使えない

chrome.tabsはコンテンツスクリプトでは使えないので、使おうとすると次のエラーが出る。
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'query')

いつも忘れてしまう。chrome.tabsはバックグラウンドで取得し、メッセージパッシングを通じてコンテンツスクリプトに渡す。

ひげひげ

入力欄にEnterのイベントリスナーが登録されてisTrusted=trueのときしか受け付けない時

困った。ChatGPTはisTrusted=falseでもキー入力を受け付けてくれてたのでKeyboardEventを作ったのを投げればよかった。
DeepSeekはisTrusted=falseのキー入力は受け付けてくれないが、入力欄の子孫にイベントリスナーが登録されているので、stopPropagationをするとフォームのデフォルト機能の改行が働いてくれて、かつEnter送信を止めてくれた
しかし、

ひげひげ

Reactで制御されたテキスト欄に文字列を入力する

このようにinnerTextやinnerHTMLで要素に文字列を入力した後、dispatchEventでイベントを発火させる。そしたら文字が入力されたとReactが認識して反映してくれる。このdispatchEventを書かなければ、画面上では文字列が反映されたように見えるがテキスト欄をfocusしたら新しく挿入した文字列が消えて、元の文字列が書き戻される。

この方法はシンプルでいい。Shift+Enterカスタムイベントを認識しないサイトであればこの方法でいく。

    const promptField = document.querySelector("textarea");
    if (promptField) {
        promptField.value = `hi \n doraemon`;
        promptField.dispatchEvent(new Event("input", { bubbles: true }))
    }

https://qiita.com/yyy_muu/items/b2df4bfc83d91882117d

ひげひげ

コンテンツスクリプトでactivetabの IDだけを取得したい

結局使わないがメモ。コンテンツスクリプトで空のメッセージをバックグラウンドに送る。メッセージの情報にはtabIdが含まれているからtabIdをそのままコンテンツスクリプトに送り返す。するとコンテンツスクリプトで自身のtabIDを得ることができる。
これはtabsAPIを使わずactivetabpermissionだけで完結するので権限を最小限に抑えたい時にいい。
https://stackoverflow.com/a/45600887

ひげひげ

DOM探しに便利なメソッド

closest
チャットAIはプロンプトの編集ができるので送信ボタンが1つ以上存在する。着目しているテキストエリアに対応する送信ボタンを見つけるときはclosestを使えばいい