Enter=Newline スクリプト[edit]

Version 0.0.2[edit]
// ==UserScript==
// @name         Gemini/ChatGPT: Enter=Newline, Ctrl+Enter=Send (stable CE)
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @run-at       document-start
// @grant        none
// @version      0.0.2
// @description  Enter is Newline; Ctrl+Enter is Send (Gemini/ChatGPT)
// ==/UserScript==

(function () {
 'use strict';
 console.log('[TM] key-remap loaded');

 // ---- selectors (Gemini is often Quill / rich-textarea) ----
 const GEMINI_EDITORS = [
   '.ql-editor[contenteditable="true"]',
   'div[contenteditable="true"][aria-label*="prompt"]',
   'rich-textarea [contenteditable="true"]',
 ].join(',');

 function isEditable(el) {
   if (!el) return false;
   if (el.tagName === 'TEXTAREA') return true;
   if (el.tagName === 'INPUT') return false;
   if (el.isContentEditable === true) return true;
   // sometimes focus is inside a child node; climb up
   if (el.closest && el.closest(GEMINI_EDITORS)) return true;
   return false;
 }

 function getEditorFromEvent(e) {
   // prefer event target; fallback to activeElement; then climb
   let el = e.target || document.activeElement;
   if (isEditable(el)) return el;

   if (el && el.closest) {
     const up = el.closest(GEMINI_EDITORS);
     if (up) return up;
   }
   const any = document.querySelector(GEMINI_EDITORS);
   return any || null;
 }

 // ---- newline insertion ----
 function insertNewlineTextarea(el) {
   const start = el.selectionStart ?? el.value.length;
   const end = el.selectionEnd ?? el.value.length;
   el.setRangeText('\n', start, end, 'end');
   el.dispatchEvent(new Event('input', { bubbles: true }));
 }

 // For Gemini (Quill), the safest newline is: synthesize Shift+Enter and let the app handle it.
 function dispatchShiftEnter(el) {
   const mk = (type) =>
     new KeyboardEvent(type, {
       key: 'Enter',
       code: 'Enter',
       shiftKey: true,
       bubbles: true,
       cancelable: true,
     });
   el.dispatchEvent(mk('keydown'));
   el.dispatchEvent(mk('keypress'));
   el.dispatchEvent(mk('keyup'));
 }

 function insertNewline(el) {
   if (!el) return;
   if (el.tagName === 'TEXTAREA') return insertNewlineTextarea(el);

   // Gemini / rich editors: prefer Shift+Enter synthesis
   dispatchShiftEnter(el);
 }

 // ---- send button ----
 function findSendButton(scope) {
   const sels = [
     'button[type="submit"]',
     'button[data-testid="send-button"]',
     'button[aria-label="Send"]',
     'button[aria-label="Send message"]',
     'button[aria-label*="Send"]',
     'button[aria-label*="送信"]',
     'button[title*="Send"]',
     'button[title*="送信"]',
   ];

   for (const sel of sels) {
     const btn = scope.querySelector(sel);
     if (btn && !btn.disabled) return btn;
   }

   // fallback: scan buttons by accessible name-ish signals
   const buttons = Array.from(scope.querySelectorAll('button'));
   for (const b of buttons) {
     if (b.disabled) continue;
     const a = (b.getAttribute('aria-label') || '').toLowerCase();
     const t = (b.getAttribute('title') || '').toLowerCase();
     const txt = (b.textContent || '').toLowerCase().trim();
     if (a.includes('send') || t.includes('send') || txt === 'send' || a.includes('送信') || t.includes('送信')) {
       return b;
     }
   }
   return null;
 }

 function clickSendButtonNear(editorEl) {
   // prefer nearest container (form / composer) to avoid clicking wrong "Send" elsewhere
   const scopes = [];
   if (editorEl && editorEl.closest) {
     scopes.push(editorEl.closest('form'));
     scopes.push(editorEl.closest('rich-textarea'));
     scopes.push(editorEl.closest('[role="textbox"]'));
     scopes.push(editorEl.parentElement);
   }
   scopes.push(document);

   for (const s of scopes) {
     if (!s) continue;
     const btn = findSendButton(s);
     if (btn) { btn.click(); return true; }
   }
   return false;
 }

 window.addEventListener(
   'keydown',
   (e) => {
     // avoid fighting with our own synthetic events
     if (!e.isTrusted) return;

     // IME変換確定中は介入しない
     if (e.isComposing) return;

     if (e.key !== 'Enter') return;

     const el = getEditorFromEvent(e);
     if (!isEditable(el)) return;

     // Ctrl+Enter → 送信
     if (e.ctrlKey) {
       e.preventDefault();
       e.stopImmediatePropagation();
       clickSendButtonNear(el);
       return;
     }

     // Enter単体 → 改行(GeminiはShift+Enterへ“すり替え”)
     if (!e.shiftKey && !e.altKey && !e.metaKey) {
       e.preventDefault();
       e.stopImmediatePropagation();
       insertNewline(el);
       return;
     }
     // Shift+Enter 等は既定挙動に任せる
   },
   true
 );
})();
* Version 0.0.1[edit]

(function () {

 'use strict';
 console.log('[TM] ChatGPT key-remap loaded');
 function isEditable(el) {
   if (!el) return false;
   if (el.tagName === 'TEXTAREA') return true;
   if (el.tagName === 'INPUT') return false;
   return el.isContentEditable === true;
 }
 function insertNewlineTextarea(el) {
   const start = el.selectionStart ?? el.value.length;
   const end = el.selectionEnd ?? el.value.length;
   el.setRangeText('\n', start, end, 'end');
   el.dispatchEvent(new Event('input', { bubbles: true }));
 }
 function insertNewlineContentEditable(el) {
   // Selection/Range で <br> と ZWSP を挿入し,カーソルを次行へ
   const sel = window.getSelection();
   if (!sel || sel.rangeCount === 0) return;
   const range = sel.getRangeAt(0);
   range.deleteContents();
   const br = document.createElement('br');
   const zwsp = document.createTextNode('\u200B'); // 次行でタイピング可能にするおまじない
   range.insertNode(br);
   range.setStartAfter(br);
   range.insertNode(zwsp);
   // キャレットを ZWSP の後ろへ
   const after = document.createRange();
   after.setStartAfter(zwsp);
   after.collapse(true);
   sel.removeAllRanges();
   sel.addRange(after);
   // React/TipTap 等に変更通知
   el.dispatchEvent(new InputEvent('input', { bubbles: true, data: '\n' }));
 }
 function insertNewline(el) {
   if (el.tagName === 'TEXTAREA') return insertNewlineTextarea(el);
   // 念のため focus
   el.focus();
   insertNewlineContentEditable(el);
 }
 function clickSendButton() {
   const candidates = [
     'button[type="submit"]',
     'button[data-testid="send-button"]',
     'button[aria-label*="Send"]',
     'button[aria-label*="送信"]',
   ];
   for (const sel of candidates) {
     const btn = document.querySelector(sel);
     if (btn && !btn.disabled) { btn.click(); return true; }
   }
   return false;
 }
 window.addEventListener('keydown', (e) => {
   const el = document.activeElement;
   if (!isEditable(el)) return;
   // IME 変換確定中は介入しない
   if (e.isComposing) return;
   if (e.key !== 'Enter') return;
   // Ctrl+Enter → 送信
   if (e.ctrlKey) {
     e.preventDefault();
     e.stopImmediatePropagation();
     clickSendButton();
     return;
   }
   // Enter 単体 → 改行(誤送信防止)
   if (!e.shiftKey && !e.altKey && !e.metaKey) {
     e.preventDefault();
     e.stopImmediatePropagation();
     insertNewline(el);
     return;
   }
   // Shift+Enter 等は既定挙動(多くは改行)
 }, true);

})();

/pre>


トップ   新規 ページ一覧 検索 最終更新   ヘルプ   最終更新のRSS