Fix Scrollbar Position on Page Up/Page Down in ContentEditable

Prevents scrollbar misalignment when pressing Page Up/Page Down in the prompt input area of contenteditable elements by handling key events and adjusting cursor position accordingly.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name               Fix Scrollbar Position on Page Up/Page Down in ContentEditable
// @name:zh-TW         修正 ContentEditable 中按下 Page Up/Page Down 時滾動條位置不正確問題
// @namespace          Violentmonkey Scripts
// @match              https://chatgpt.com/c/*
// @grant              none
// @version            1.0
// @author             JohnnyZhou@TW
// @description        Prevents scrollbar misalignment when pressing Page Up/Page Down in the prompt input area of contenteditable elements by handling key events and adjusting cursor position accordingly.
// @description:zh-TW  當在 contenteditable 元素的提示輸入區域按下 Page Up/Page Down 鍵時,通過處理按鍵事件並調整游標位置,防止滾動條位置錯位。
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    /**
     * 將游標移動到指定元素的開頭
     * @param {HTMLElement} element - 目標元素
     */
    function setCaretToStart(element) {
        const range = document.createRange();
        const sel = window.getSelection();

        // 找到第一個可編輯的子節點
        let firstNode = element.querySelector('p, span, div');
        if (firstNode) {
            range.setStart(firstNode, 0);
        } else {
            range.setStart(element, 0);
        }
        range.collapse(true);

        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * 將游標移動到指定元素的結尾
     * @param {HTMLElement} element - 目標元素
     */
    function setCaretToEnd(element) {
        const range = document.createRange();
        const sel = window.getSelection();

        // 找到最後一個可編輯的子節點
        let lastNode = getLastEditableNode(element);
        if (lastNode) {
            if (lastNode.nodeType === Node.TEXT_NODE) {
                range.setStart(lastNode, lastNode.textContent.length);
            } else {
                range.setStart(lastNode, lastNode.childNodes.length);
            }
        } else {
            range.setStart(element, element.childNodes.length);
        }
        range.collapse(true);

        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * 遞迴查找最後一個可編輯的子節點
     * @param {HTMLElement} element - 目標元素
     * @returns {Node} 最後一個可編輯的子節點
     */
    function getLastEditableNode(element) {
        if (!element) return null;
        if (element.lastChild) {
            return getLastEditableNode(element.lastChild);
        }
        return element;
    }

    /**
     * 綁定鍵盤事件到目標元素
     * @param {HTMLElement} editableDiv - 可編輯的目標元素
     */
    function bindKeyEvents(editableDiv) {
        if (!editableDiv) return;

        editableDiv.addEventListener('keydown', (event) => {
            if (event.key === 'PageUp') {
                event.preventDefault(); // 阻止預設行為
                setCaretToStart(editableDiv); // 將游標移動到開頭
            } else if (event.key === 'PageDown') {
                event.preventDefault(); // 阻止預設行為
                setCaretToEnd(editableDiv); // 將游標移動到結尾
            }
        });
    }

    /**
     * 使用 MutationObserver 監聽目標元素的出現
     */
    function observeDOM() {
        const observer = new MutationObserver((mutations, obs) => {
            const editableDiv = document.getElementById('prompt-textarea');
            if (editableDiv) {
                bindKeyEvents(editableDiv);
                obs.disconnect(); // 停止監聽
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // 立即嘗試綁定,如果元素已存在
    const existingDiv = document.getElementById('prompt-textarea');
    if (existingDiv) {
        bindKeyEvents(existingDiv);
    } else {
        // 如果元素尚未存在,開始監聽
        observeDOM();
    }

})();