#author("2025-12-25T12:20:53+00:00","default:iseki","iseki")
#author("2025-12-25T12:21:22+00:00","default:iseki","iseki")
*** Enter=Newline スクリプト
**** Version 0.0.2
<pre>
// ==UserScript==
// @name         Gemini/ChatGPT: Enter=Newline, Ctrl+Enter=Send (stable CE)
// @name         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
  );
})();
</pre>

***** Version 0.0.1
<pre>
// ==UserScript==
// @name         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.1
// @description  Enter is Newline
// ==/UserScript==

(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