Zenn
🙆‍♂️

macOSのSafariでGrokを使うときの日本語入力問題を解決する方法

2025/03/02に公開

はじめに

macOSのSafariで日本語入力をしながらGrok(grok.com)を使っている方は、こんな問題に悩まされたことはありませんか?

  • 日本語入力中にEnterキーで文字を確定したら、入力途中のメッセージが送信されてしまう
  • 変換候補を選んだ直後に、意図せずメッセージが送信される
  • 思考を整理しながら入力できず、いちいち文章を別のエディタで作成してからコピペしている

これはmacOSの日本語IMEと、Webアプリケーションのイベント処理の相性問題です。特にAIチャットサービスでは、Enterキーが「送信」と「日本語確定」の両方に使われることで起こる問題です。この記事では、Tampermonkeyを使ってこの問題を解決する方法を紹介します。

問題の詳細

日本語入力で発生する問題を技術的に説明すると:

  1. IMEで日本語を入力中、「確定」のためにEnterキーを押す
  2. ブラウザはこのEnterキーイベントを「フォーム送信」として解釈してしまう
  3. 結果、入力途中のメッセージが送信されてしまう

この問題は特に以下のような状況で顕著です:

  • クライアントサイドのJavaScriptでEnterキーイベントをハンドリングしているサイト
  • ReactやVueなどのフレームワークを使った最新のWebアプリケーション
  • フォームの送信処理が非同期で行われるアプリケーション

解決策:Tampermonkeyスクリプト

この問題を解決するため、複数のアプローチを組み合わせた強力なTampermonkeyスクリプトを作成しました。このスクリプトは以下の手法で日本語入力中と確定直後のEnterキーによる誤送信を防ぎます:

  1. KeyboardEventの根本的な制御
  2. 送信ボタンの一時的な無効化
  3. フォーム送信イベントの制御
  4. イベントリスナーとディスパッチャーのオーバーライド

スクリプトの完全コード

// ==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を処理しないよう制御

これら複数の防御層により、どのような実装のサイトでも日本語入力の問題を解決できるようになっています。

インストール方法

  1. まずTampermonkey拡張機能をインストールします(Chrome、Firefox、Safariなど主要ブラウザに対応)
  2. Tampermonkeyのアイコンをクリックし、「新規スクリプトを作成」を選択
  3. エディタが開いたら、上記のコードをコピー&ペースト
  4. 「ファイル」→「保存」(またはCtrl+S)でスクリプトを保存
  5. 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)の連携に起因します。具体的には:

  1. イベントの順序: 日本語入力時、以下の順序でイベントが発生します

    • compositionstart: 日本語入力開始
    • (変換・編集作業)
    • compositionend: 日本語入力確定
    • keydown(Enter): 確定のためのキー
  2. イベントバブリング: キーイベントは要素から親要素へとバブルアップし、途中でフォーム送信処理に捕捉されてしまいます

  3. イベントタイミング: 多くのWebアプリケーションは、キーイベントのバブリングを適切に処理できておらず、日本語入力のコンテキストを考慮していません

このスクリプトは、これらの問題に対して複数のレベルで介入し、根本的に解決することを目指しています。

注意点

このスクリプトはかなり強力な方法でWebページの動作に介入しているため、一部のサイトでは意図しない動作を引き起こす可能性があります。もし問題が発生した場合は、スクリプトを無効化するか、調整が必要かもしれません。

まとめ

macOSの日本語入力とGrokなどのWebアプリケーションの相性問題は、多くのユーザーを悩ませています。このTampermonkeyスクリプトを使えば、日本語入力中のEnterキーによる誤送信を防ぎ、ストレスなくAIチャットサービスを利用できるようになります。

この解決策が、同じ問題で悩んでいる方の助けになれば幸いです。

Discussion

ログインするとコメントできます