macOSのSafariでGrokを使うときの日本語入力問題を解決する方法
はじめに
macOSのSafariで日本語入力をしながらGrok(grok.com)を使っている方は、こんな問題に悩まされたことはありませんか?
- 日本語入力中にEnterキーで文字を確定したら、入力途中のメッセージが送信されてしまう
- 変換候補を選んだ直後に、意図せずメッセージが送信される
- 思考を整理しながら入力できず、いちいち文章を別のエディタで作成してからコピペしている
これはmacOSの日本語IMEと、Webアプリケーションのイベント処理の相性問題です。特にAIチャットサービスでは、Enterキーが「送信」と「日本語確定」の両方に使われることで起こる問題です。この記事では、Tampermonkeyを使ってこの問題を解決する方法を紹介します。
問題の詳細
日本語入力で発生する問題を技術的に説明すると:
- IMEで日本語を入力中、「確定」のためにEnterキーを押す
- ブラウザはこのEnterキーイベントを「フォーム送信」として解釈してしまう
- 結果、入力途中のメッセージが送信されてしまう
この問題は特に以下のような状況で顕著です:
- クライアントサイドのJavaScriptでEnterキーイベントをハンドリングしているサイト
- ReactやVueなどのフレームワークを使った最新のWebアプリケーション
- フォームの送信処理が非同期で行われるアプリケーション
解決策:Tampermonkeyスクリプト
この問題を解決するため、複数のアプローチを組み合わせた強力なTampermonkeyスクリプトを作成しました。このスクリプトは以下の手法で日本語入力中と確定直後のEnterキーによる誤送信を防ぎます:
- KeyboardEventの根本的な制御
- 送信ボタンの一時的な無効化
- フォーム送信イベントの制御
- イベントリスナーとディスパッチャーのオーバーライド
スクリプトの完全コード
// ==UserScript==
// @name Grok日本語入力修正 (強化版)
// @namespace http://tampermonkey.net/
// @version 0.2
// @description grok.comで日本語入力確定時に誤送信されるのを防止します
// @author You
// @match https://grok.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// オリジナルのイベントディスパッチャーを保存
const originalEventDispatcher = EventTarget.prototype.dispatchEvent;
const originalAddEventListener = EventTarget.prototype.addEventListener;
// 送信ボタンの要素を識別するためのセレクタ
const SEND_BUTTON_SELECTORS = [
'button[type="submit"]',
'button.send-button',
'button[aria-label*="送信"]',
'button[aria-label*="Send"]'
];
// 1. KeyboardEventをオーバーライド
const originalKeyboardEvent = window.KeyboardEvent;
let isComposing = false;
let lastCompositionEndTime = 0;
// 日本語入力状態を監視するグローバルハンドラー
document.addEventListener('compositionstart', function(e) {
isComposing = true;
}, true);
document.addEventListener('compositionend', function(e) {
isComposing = false;
lastCompositionEndTime = Date.now();
}, true);
// 特定の入力要素にフォーカスがあるか確認する関数
function isInputFocused() {
const activeElement = document.activeElement;
return activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.isContentEditable
);
}
// KeyboardEventをモンキーパッチ
window.KeyboardEvent = function(type, eventInitDict) {
// オリジナルのイベントを作成
const event = new originalKeyboardEvent(type, eventInitDict);
// Enterキーの処理
if (type === 'keydown' &&
eventInitDict &&
eventInitDict.key === 'Enter' &&
!eventInitDict.shiftKey &&
isInputFocused()) {
// 日本語入力中または入力直後の場合、キーコードを変更
if (isComposing || (Date.now() - lastCompositionEndTime < 200)) {
// キーコードをTab(9)に変更してEnterイベントを無効化
Object.defineProperty(event, 'keyCode', { get: function() { return 9; } });
Object.defineProperty(event, 'key', { get: function() { return 'Tab'; } });
Object.defineProperty(event, 'code', { get: function() { return 'Tab'; } });
Object.defineProperty(event, 'which', { get: function() { return 9; } });
}
}
return event;
};
// KeyboardEventのプロトタイプをオリジナルのものから継承
window.KeyboardEvent.prototype = originalKeyboardEvent.prototype;
// 2. 送信ボタンをクリック不可にする
function disableSendButtonIfComposing() {
if (isComposing || (Date.now() - lastCompositionEndTime < 200)) {
SEND_BUTTON_SELECTORS.forEach(selector => {
const buttons = document.querySelectorAll(selector);
buttons.forEach(button => {
if (!button.hasAttribute('data-original-disabled')) {
button.setAttribute('data-original-disabled', button.disabled ? 'true' : 'false');
button.disabled = true;
// 200ms後に元に戻す
setTimeout(() => {
if (button.getAttribute('data-original-disabled') === 'false') {
button.disabled = false;
}
button.removeAttribute('data-original-disabled');
}, 200);
}
});
});
}
}
// 日本語入力確定後にボタンを一時的に無効化
document.addEventListener('compositionend', disableSendButtonIfComposing, true);
// 3. フォーム送信イベントを制御
const originalSubmit = HTMLFormElement.prototype.submit;
HTMLFormElement.prototype.submit = function() {
if (isComposing || (Date.now() - lastCompositionEndTime < 200)) {
console.log('日本語入力中またはその直後のため、フォーム送信をブロックしました');
return;
}
return originalSubmit.apply(this, arguments);
};
// 4. イベントディスパッチャーをオーバーライド
EventTarget.prototype.dispatchEvent = function(event) {
// 入力要素でのEnterキーイベントまたはsubmitイベントをキャンセル
if ((event.type === 'keydown' || event.type === 'keypress') &&
event.key === 'Enter' &&
!event.shiftKey &&
isInputFocused() &&
(isComposing || (Date.now() - lastCompositionEndTime < 200))) {
return false;
}
if (event.type === 'submit' &&
(isComposing || (Date.now() - lastCompositionEndTime < 200))) {
return false;
}
return originalEventDispatcher.apply(this, arguments);
};
// 5. イベントリスナーをオーバーライド
EventTarget.prototype.addEventListener = function(type, listener, options) {
if (type === 'keydown' || type === 'keypress' || type === 'keyup') {
// Enterキーのイベントハンドラーをラップ
const wrappedListener = function(event) {
if (event.key === 'Enter' &&
!event.shiftKey &&
isInputFocused() &&
(isComposing || (Date.now() - lastCompositionEndTime < 200))) {
// イベントをキャンセル
event.stopImmediatePropagation();
event.preventDefault();
return false;
}
return listener.apply(this, arguments);
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
if (type === 'submit') {
// submitイベントハンドラーをラップ
const wrappedListener = function(event) {
if (isComposing || (Date.now() - lastCompositionEndTime < 200)) {
event.stopImmediatePropagation();
event.preventDefault();
return false;
}
return listener.apply(this, arguments);
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.apply(this, arguments);
};
// MutationObserverでDOM変更を監視
const observer = new MutationObserver(function(mutations) {
if (isComposing || (Date.now() - lastCompositionEndTime < 200)) {
disableSendButtonIfComposing();
}
});
// ページ読み込み完了後に監視開始
window.addEventListener('load', function() {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['disabled']
});
// 既存の入力フィールドにカスタムイベントリスナーを追加
document.querySelectorAll('input, textarea').forEach(function(input) {
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey &&
(isComposing || (Date.now() - lastCompositionEndTime < 200))) {
e.stopImmediatePropagation();
e.preventDefault();
}
}, true);
});
});
// デバッグ用ログ
console.log('Grok日本語入力修正スクリプトが読み込まれました(強化版)');
})();
スクリプトの仕組み解説
1. KeyboardEventのオーバーライド
このスクリプトの最も重要な部分は、KeyboardEventそのものをオーバーライドしている点です。これにより、日本語入力中や確定直後のEnterキーが押された場合、そのキーコードを「Tab」に置き換えることで、送信イベントを根本から防いでいます。
window.KeyboardEvent = function(type, eventInitDict) {
// オリジナルのイベントを作成
const event = new originalKeyboardEvent(type, eventInitDict);
// 日本語入力中または入力直後の場合、キーコードを変更
if (isComposing || (Date.now() - lastCompositionEndTime < 200)) {
// キーコードをTab(9)に変更
Object.defineProperty(event, 'keyCode', { get: function() { return 9; } });
Object.defineProperty(event, 'key', { get: function() { return 'Tab'; } });
// 省略...
}
return event;
};
2. 多層防御による安全確保
一つの方法だけでは対応できない可能性があるため、複数のアプローチを組み合わせています:
- 送信ボタンの一時的無効化: 日本語入力確定後200ミリ秒間、送信ボタンを無効化
- フォーム送信メソッドの直接制御: HTMLFormElement.prototypeをオーバーライド
- イベントディスパッチャーの制御: Enterキーや送信イベントをブロック
- イベントリスナーのラッピング: 既存のイベントリスナーが日本語入力中のEnterを処理しないよう制御
これら複数の防御層により、どのような実装のサイトでも日本語入力の問題を解決できるようになっています。
インストール方法
- まずTampermonkey拡張機能をインストールします(Chrome、Firefox、Safariなど主要ブラウザに対応)
- Tampermonkeyのアイコンをクリックし、「新規スクリプトを作成」を選択
- エディタが開いたら、上記のコードをコピー&ペースト
- 「ファイル」→「保存」(またはCtrl+S)でスクリプトを保存
- grok.comを開いて使用開始
他のサイトでも使用するには
このスクリプトはgrok.com向けに設定されていますが、他のサイトでも同様の問題が発生する場合は、スクリプトの8行目の@match
の値を変更するだけで使用できます:
// @match https://grok.com/*
例えば、特定のサイトで使用する場合:
// @match https://example.com/*
複数のサイトで使用する場合は、以下のように複数行の@match
を追加します:
// @match https://grok.com/*
// @match https://example.com/*
// @match https://anothersite.com/*
カスタマイズと調整
タイミングの調整
日本語入力確定後のブロック時間(現在は200ミリ秒)が長すぎたり短すぎたりする場合は、以下の箇所の数値を調整してください:
if (isComposing || (Date.now() - lastCompositionEndTime < 200)) {
送信ボタンのセレクタ
サイトによって送信ボタンのHTMLが異なる場合は、以下のセレクタリストを調整してください:
const SEND_BUTTON_SELECTORS = [
'button[type="submit"]',
'button.send-button',
'button[aria-label*="送信"]',
'button[aria-label*="Send"]'
];
技術的な解説:なぜこの問題が起きるのか
日本語入力問題は、ブラウザとOSのIME(Input Method Editor)の連携に起因します。具体的には:
-
イベントの順序: 日本語入力時、以下の順序でイベントが発生します
- compositionstart: 日本語入力開始
- (変換・編集作業)
- compositionend: 日本語入力確定
- keydown(Enter): 確定のためのキー
-
イベントバブリング: キーイベントは要素から親要素へとバブルアップし、途中でフォーム送信処理に捕捉されてしまいます
-
イベントタイミング: 多くのWebアプリケーションは、キーイベントのバブリングを適切に処理できておらず、日本語入力のコンテキストを考慮していません
このスクリプトは、これらの問題に対して複数のレベルで介入し、根本的に解決することを目指しています。
注意点
このスクリプトはかなり強力な方法でWebページの動作に介入しているため、一部のサイトでは意図しない動作を引き起こす可能性があります。もし問題が発生した場合は、スクリプトを無効化するか、調整が必要かもしれません。
まとめ
macOSの日本語入力とGrokなどのWebアプリケーションの相性問題は、多くのユーザーを悩ませています。このTampermonkeyスクリプトを使えば、日本語入力中のEnterキーによる誤送信を防ぎ、ストレスなくAIチャットサービスを利用できるようになります。
この解決策が、同じ問題で悩んでいる方の助けになれば幸いです。
Discussion