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.

  1. // ==UserScript==
  2. // @name Fix Scrollbar Position on Page Up/Page Down in ContentEditable
  3. // @name:zh-TW 修正 ContentEditable 中按下 Page Up/Page Down 時滾動條位置不正確問題
  4. // @namespace Violentmonkey Scripts
  5. // @match https://chatgpt.com/c/*
  6. // @grant none
  7. // @version 1.0
  8. // @author JohnnyZhou@TW
  9. // @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.
  10. // @description:zh-TW 當在 contenteditable 元素的提示輸入區域按下 Page Up/Page Down 鍵時,通過處理按鍵事件並調整游標位置,防止滾動條位置錯位。
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. /**
  18. * 將游標移動到指定元素的開頭
  19. * @param {HTMLElement} element - 目標元素
  20. */
  21. function setCaretToStart(element) {
  22. const range = document.createRange();
  23. const sel = window.getSelection();
  24.  
  25. // 找到第一個可編輯的子節點
  26. let firstNode = element.querySelector('p, span, div');
  27. if (firstNode) {
  28. range.setStart(firstNode, 0);
  29. } else {
  30. range.setStart(element, 0);
  31. }
  32. range.collapse(true);
  33.  
  34. sel.removeAllRanges();
  35. sel.addRange(range);
  36. }
  37.  
  38. /**
  39. * 將游標移動到指定元素的結尾
  40. * @param {HTMLElement} element - 目標元素
  41. */
  42. function setCaretToEnd(element) {
  43. const range = document.createRange();
  44. const sel = window.getSelection();
  45.  
  46. // 找到最後一個可編輯的子節點
  47. let lastNode = getLastEditableNode(element);
  48. if (lastNode) {
  49. if (lastNode.nodeType === Node.TEXT_NODE) {
  50. range.setStart(lastNode, lastNode.textContent.length);
  51. } else {
  52. range.setStart(lastNode, lastNode.childNodes.length);
  53. }
  54. } else {
  55. range.setStart(element, element.childNodes.length);
  56. }
  57. range.collapse(true);
  58.  
  59. sel.removeAllRanges();
  60. sel.addRange(range);
  61. }
  62.  
  63. /**
  64. * 遞迴查找最後一個可編輯的子節點
  65. * @param {HTMLElement} element - 目標元素
  66. * @returns {Node} 最後一個可編輯的子節點
  67. */
  68. function getLastEditableNode(element) {
  69. if (!element) return null;
  70. if (element.lastChild) {
  71. return getLastEditableNode(element.lastChild);
  72. }
  73. return element;
  74. }
  75.  
  76. /**
  77. * 綁定鍵盤事件到目標元素
  78. * @param {HTMLElement} editableDiv - 可編輯的目標元素
  79. */
  80. function bindKeyEvents(editableDiv) {
  81. if (!editableDiv) return;
  82.  
  83. editableDiv.addEventListener('keydown', (event) => {
  84. if (event.key === 'PageUp') {
  85. event.preventDefault(); // 阻止預設行為
  86. setCaretToStart(editableDiv); // 將游標移動到開頭
  87. } else if (event.key === 'PageDown') {
  88. event.preventDefault(); // 阻止預設行為
  89. setCaretToEnd(editableDiv); // 將游標移動到結尾
  90. }
  91. });
  92. }
  93.  
  94. /**
  95. * 使用 MutationObserver 監聽目標元素的出現
  96. */
  97. function observeDOM() {
  98. const observer = new MutationObserver((mutations, obs) => {
  99. const editableDiv = document.getElementById('prompt-textarea');
  100. if (editableDiv) {
  101. bindKeyEvents(editableDiv);
  102. obs.disconnect(); // 停止監聽
  103. }
  104. });
  105.  
  106. observer.observe(document.body, {
  107. childList: true,
  108. subtree: true
  109. });
  110. }
  111.  
  112. // 立即嘗試綁定,如果元素已存在
  113. const existingDiv = document.getElementById('prompt-textarea');
  114. if (existingDiv) {
  115. bindKeyEvents(existingDiv);
  116. } else {
  117. // 如果元素尚未存在,開始監聽
  118. observeDOM();
  119. }
  120.  
  121. })();