Enterくんの開発メモ
型アサーションを使うタイミング
型アサーションを使うのは気が引けるけど使わなきゃ無駄なコードが増えてしまってて悩んでいた。下の記事では開発者がコンパイラよりも型についてよく知っているときに型アサーションを使うべきと書かれていた。サンプルコードとしてquerySelector
を使ったものが挙げられていた。まさに、拡張機能開発ではサイト内のDOMを扱うことが多いから、querySelector
などDOMを利用するときに型アサーションを使ってみる。
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つ
-
isTrusted
プロパティを使い、ユーザーがキーボード入力して発生したイベントか、コード上で作成されたイベントかを識別している。 - Enterを押したときのみ実行する条件を書く。Ctrl、Win、Cmdキーと同時に押されたときはスキップ
-
preventDefault()
を使い、Enterを押したときの挙動を全てストップ。ブラウザデフォルトも開発者が書いたものも止める。 - 新たに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は初めてみたのでメモ
テキストエリアの探し方
リッチなエディタを実装しているサイトは単に<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
の時だけ、つまり人手で打ち込んだ入力だけをテキストエリアに反映させている。
textarea.value
+= "\n" で改行記号を直接挿入する
失敗2 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`を追記することで、リクエストヘッダーを修正することができるのでこの対策は簡単
stopPropagation
で親要素へのイベントバブリングを止める
対策2 ChatGPTは入力欄にEnterを押されたときに反応するイベントリスナーを登録してたっぽいのでpreventDefault()
でEnter送信を防ぐことができた。DeepSeekだとpreventDefault()
を使ってもEnter送信されてしまうので、入力欄以外の要素でイベントリスナーが登録されていることがわかる。そこでstopPropagation()
を使い、親要素へイベントが伝播することを防いだ。するとEnter送信を防ぐことができた。また嬉しい誤算だが、特にShiftEnterのイベントを発生させなくてもEnter改行ができている。
ちなみに入力欄でpreventDefault()
を設定すると改行されない。なのでEnter送信のイベントを入力欄以外に設定されてくれているのが功を奏した。
clipboardに\nをコピーしペースト
これもカスタムイベントを発生させるだけだったのでできなかった。やはりisTrustedを監視して入力かどうかを判断しているのか?
stopPropagation() と preventDefault() の違い
stopPropagationは自分のデフォルトの挙動は実行する。親要素にイベントの発生は伝えない
preventDefaultは自分のデフォルトの挙動は止める。親要素にイベントの発生は伝える。
対策3 isTrustedがtrueのイベントを作成する
chrome.debugger.sendCommand
でisTrusted
がtrueなイベントを発生させることができるらしい。今回はstopPropagation()
を利用してEnter改行を実現できたので、このAPI使わなかったが裏技用として頭の片隅に入れておきたい
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 }))
}
コンテンツスクリプトでactivetabの IDだけを取得したい
結局使わないがメモ。コンテンツスクリプトで空のメッセージをバックグラウンドに送る。メッセージの情報にはtabIdが含まれているからtabIdをそのままコンテンツスクリプトに送り返す。するとコンテンツスクリプトで自身のtabIDを得ることができる。
これはtabs
APIを使わずactivetab
permissionだけで完結するので権限を最小限に抑えたい時にいい。
https://stackoverflow.com/a/45600887
DOM探しに便利なメソッド
closest
チャットAIはプロンプトの編集ができるので送信ボタンが1つ以上存在する。着目しているテキストエリアに対応する送信ボタンを見つけるときはclosest
を使えばいい