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.

// ==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();
    }

})();