zetlnd

Untitled

Nov 16th, 2025 (edited)
15
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.91 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Playground.ru Comment Floater
  3. // @match *://*.playground.ru/*
  4. // @version 0.1
  5. // @description Добавляет плавающую панель редактора, дублирующую кнопки (включая эмуляцию Ctrl+B/I/J), которая появляется и исчезает вместе с оригинальной панелью.
  6. // @grant GM_addStyle
  7. // @run-at document-idle
  8. // ==/UserScript==
  9.  
  10. (function() {
  11. 'use strict';
  12.  
  13. // --- СЕКЦИЯ 1: СТИЛИЗАЦИЯ ---
  14. // Этот блок добавляет CSS в страницу для нашей плавающей панели.
  15. // - #pg-floating-toolbar: Определяет позицию (fixed), внешний вид (темный),
  16. // z-index (чтобы быть поверх других элементов) и начальное состояние (display: none).
  17. // - .btn: Стилизует кнопки внутри панели, чтобы они соответствовали теме сайта.
  18. GM_addStyle(`
  19. #pg-floating-toolbar {
  20. position: fixed;
  21. top: 150px;
  22. right: 20px;
  23. background: #2f2f2f;
  24. border: 1px solid #444;
  25. border-radius: 5px;
  26. padding: 5px;
  27. z-index: 9998;
  28. display: none; /* Изначально панель скрыта */
  29. flex-direction: column;
  30. gap: 5px;
  31. box-shadow: 0 4px 10px rgba(0,0,0,0.5);
  32. }
  33. #pg-floating-toolbar .btn {
  34. background: #3c3c3c;
  35. color: #ddd;
  36. border: 1px solid #555;
  37. padding: 8px 10px;
  38. margin: 0;
  39. line-height: 1.2;
  40. }
  41. #pg-floating-toolbar .btn:hover {
  42. background: #4a4a4a;
  43. color: #fff;
  44. }
  45. `);
  46.  
  47. // --- СЕКЦИЯ 2: СОЗДАНИЕ HTML ПАНЕЛИ ---
  48. // Этот блок создает <div> элемент плавающей панели,
  49. // добавляет в него HTML-код для всех кнопок (с иконками)
  50. // и встраивает готовую панель в <body> страницы.
  51. const floater = document.createElement('div');
  52. floater.id = 'pg-floating-toolbar';
  53. floater.innerHTML = `
  54. <button class="btn" data-action="bold" title="Выделенный (Ctrl+B)">
  55. <span class="glyphicon glyphicon-bold"></span>
  56. </button>
  57. <button class="btn" data-action="italic" title="Наклонный (Ctrl+I)">
  58. <span class="glyphicon glyphicon-italic"></span>
  59. </button>
  60. <button class="btn" data-action="quote" title="Цитата">
  61. <span class="icon-quote"></span>
  62. </button>
  63. <button class="btn" data-action="spoiler" title="Спойлер">
  64. <span class="glyphicon glyphicon-eye-close"></span>
  65. </button>
  66. <button class="btn" data-action="link" title="Ссылка">
  67. <span class="glyphicon glyphicon-link"></span>
  68. </button>
  69. <button class="btn" data-action="image" title="Изображение">
  70. <span class="glyphicon glyphicon-picture"></span>
  71. </button>
  72. <button class="btn" data-action="code" title="Код (Ctrl+J)">
  73. <span class="glyphicon glyphicon-list-alt"></span>
  74. </button>
  75. `;
  76. document.body.appendChild(floater);
  77.  
  78. // --- СЕКЦИЯ 3: ОСНОВНЫЕ УТИЛИТЫ ---
  79. // Эти функции выполняют "грязную" работу по эмуляции действий пользователя.
  80.  
  81. /**
  82. * Эмулирует ПОЛНЫЙ клик мыши (mousedown -> mouseup -> click).
  83. * Это необходимо, т.к. React-компоненты сайта (на которых построен редактор)
  84. * часто игнорируют простое .click().
  85. *
  86. * @param {HTMLElement} element - Кнопка, на которую нужно "нажать".
  87. */
  88. function emulateClick(element) {
  89. if (!element) {
  90. return;
  91. }
  92.  
  93. // Используем `unsafeWindow` вместо `window`.
  94. // Так как скрипт выполняется в "песочнице" (@grant GM_addStyle),
  95. // `window` ссылается на песочницу, а не на окно страницы.
  96. // `view: unsafeWindow` жизненно важен, иначе new MouseEvent() вызовет ошибку.
  97. const commonEventProps = { bubbles: true, cancelable: true, view: unsafeWindow };
  98.  
  99. element.dispatchEvent(new MouseEvent('mousedown', commonEventProps));
  100. element.dispatchEvent(new MouseEvent('mouseup', commonEventProps));
  101. element.dispatchEvent(new MouseEvent('click', commonEventProps));
  102. }
  103.  
  104. /**
  105. * Эмулирует нажатие комбинации клавиш Ctrl + [клавиша].
  106. * Вместо клика по кнопке, эта функция создает и отправляет событие
  107. * нажатия клавиш прямо в активное поле ввода.
  108. *
  109. * @param {string} key - Символ клавиши (напр. 'b', 'i', 'j')
  110. * @param {string} code - Код клавиши (напр. 'KeyB', 'KeyI', 'KeyJ')
  111. * @param {number} keyCode - keyCode клавиши (напр. 66, 73, 74)
  112. */
  113. function emulateCtrlKey(key, code, keyCode) {
  114. // 1. Находим элемент, в котором сейчас находится курсор.
  115. const activeEditor = document.activeElement;
  116.  
  117. // 2. Проверяем, что это действительно поле ввода редактора.
  118. if (!activeEditor || activeEditor.getAttribute('contenteditable') !== 'true') {
  119. return; // Если курсор не в редакторе, ничего не делаем.
  120. }
  121.  
  122. // 3. Описываем "фальшивое" событие нажатия клавиши.
  123. const keyEventProps = {
  124. key: key,
  125. code: code,
  126. keyCode: keyCode,
  127. which: keyCode,
  128. ctrlKey: true, // Главное — указываем, что Ctrl зажат.
  129. bubbles: true,
  130. cancelable: true,
  131. view: unsafeWindow // Опять используем unsafeWindow.
  132. };
  133.  
  134. // 4. Отправляем "фальшивое" событие 'keydown' прямо в поле ввода.
  135. activeEditor.dispatchEvent(new KeyboardEvent('keydown', keyEventProps));
  136. }
  137.  
  138.  
  139. /**
  140. * Находит *активную* панель инструментов, к которой привязан *активный* редактор.
  141. * Это ключевая функция, чтобы скрипт "не промахнулся" мимо нужной панели,
  142. * когда на странице их несколько (например, основной редактор и редактор ответа).
  143. *
  144. * @returns {HTMLElement|null} - Элемент .comments-editor-tools-panel или null.
  145. */
  146. function getActiveToolsPanel() {
  147. // 1. Находим, где сейчас курсор.
  148. const activeEditor = document.activeElement;
  149. if (!activeEditor || activeEditor.getAttribute('contenteditable') !== 'true') {
  150. return null; // Курсор не в редакторе
  151. }
  152.  
  153. // 2. От курсора "поднимаемся" вверх до родительского компонента редактора.
  154. const editorRoot = activeEditor.closest('.comments-editor.js-comments-editor');
  155. if (!editorRoot) {
  156. return null; // Не удалось найти корень редактора
  157. }
  158.  
  159. // 3. Внутри этого компонента ищем *его* панель инструментов.
  160. const originalPanel = editorRoot.querySelector('.comments-editor-tools-panel');
  161. return originalPanel; // Возвращаем панель или null
  162. }
  163.  
  164. // --- СЕКЦИЯ 4: ЛОГИКА ОБРАБОТЧИКОВ ---
  165.  
  166. /**
  167. * Главная функция-диспетчер, вызываемая при клике (кроме "Код", "Жирный", "Курсив").
  168. * Находит нужную кнопку на оригинальной панели и эмулирует клик по ней.
  169. *
  170. * @param {string} action - Действие ('quote', 'spoiler' и т.д.), взятое из data-action.
  171. */
  172. function triggerOriginalClick(action) {
  173. // 1. Получаем *только* ту панель, которая активна СЕЙЧАС.
  174. const originalPanel = getActiveToolsPanel();
  175. if (!originalPanel) {
  176. return; // Не в редакторе, ничего не делаем.
  177. }
  178.  
  179. let targetButton;
  180. // 2. Ищем нужную кнопку по ее тексту-подсказке (data-balloon).
  181. // Это надежнее, чем искать по классам иконок.
  182. switch(action) {
  183. // 'bold' и 'italic' теперь обрабатываются через emulateCtrlKey
  184. case 'quote': targetButton = originalPanel.querySelector('button[data-balloon*="цитата"]'); break;
  185. case 'spoiler': targetButton = originalPanel.querySelector('button[data-balloon*="спойлер"]'); break;
  186. case 'link':
  187. targetButton = originalPanel.querySelector('button[data-balloon*="ссылку"]');
  188. // Не эмулируем клик.
  189. // Вместо этого, вручную открываем модальное окно, на которое
  190. // ссылается кнопка. Это единственный способ избежать
  191. // "прыжка" скроллбара, который вызывает Bootstrap/браузер.
  192. if (targetButton && targetButton.dataset.target) {
  193. const modal = document.querySelector(targetButton.dataset.target);
  194. // Проверяем, что jQuery ($) и метод .modal() существуют
  195. if (modal && typeof $ === 'function' && typeof $.fn.modal === 'function') {
  196.  
  197. // Добавляем одноразовый слушатель события 'shown.bs.modal'.
  198. // Это событие срабатывает ПОСЛЕ того, как модальное окно
  199. // полностью появилось (завершило анимацию).
  200. $(modal).one('shown.bs.modal', function() {
  201. // Ищем инпут внутри *этого* модального окна.
  202. // Используем [name="url-input"] и [type="url"] вместо динамического ID.
  203. const urlInput = modal.querySelector('input[type="url"][name="url-input"]');
  204. if (urlInput) {
  205. // Если нашли, ставим на него фокус.
  206. urlInput.focus();
  207. }
  208. });
  209.  
  210. // $(modal).modal('show') — это jQuery-метод Bootstrap
  211. $(modal).modal('show');
  212. }
  213. }
  214. return; // Выходим из функции, клик не нужен.
  215. case 'image': targetButton = originalPanel.querySelector('label[data-balloon*="изображение"]'); break;
  216. // 'code' обрабатывается отдельно в слушателе.
  217. }
  218.  
  219. // 3. Если кнопка найдена...
  220. if (targetButton) {
  221. // Для "image" (т.к. "link" теперь обрабатывается выше)
  222. // используем простой .click().
  223. if (action === 'image') {
  224. targetButton.click();
  225. } else {
  226. // Для всех остальных кнопок (quote, spoiler)
  227. // "тяжелая" эмуляция жизненно необходима, чтобы React их "увидел".
  228. emulateClick(targetButton);
  229. }
  230. }
  231. }
  232.  
  233. /**
  234. * Основной слушатель событий для *нашей* плавающей панели.
  235. * Используем 'mousedown', а не 'click', чтобы применить e.preventDefault().
  236. */
  237. floater.addEventListener('mousedown', (e) => {
  238. // Находим кнопку, на которую нажали (даже если попали по иконке внутри нее).
  239. const button = e.target.closest('button[data-action]');
  240. if (!button) return; // Клик мимо кнопки
  241.  
  242. const action = button.dataset.action;
  243.  
  244. // 1. ПРЕДОТВРАЩАЕМ КРАЖУ ФОКУСА (e.preventDefault()):
  245. // Это КРИТИЧНО. Без этого, при нажатии на нашу панель,
  246. // редактор теряет фокус, и выделение текста сбрасывается.
  247. // Не делаем этого ТОЛЬКО для "image" (это <label>).
  248. // Для "link" (это <button>) мы *должны* отменить e.preventDefault(),
  249. // чтобы остановить прыжок скролла.
  250. if (action !== 'image') {
  251. e.preventDefault();
  252. }
  253.  
  254. // В любом случае останавливаем всплытие, чтобы не кликнуть "сквозь" панель.
  255. e.stopPropagation();
  256.  
  257. // 2. Логика обработки действий
  258. switch (action) {
  259. case 'bold':
  260. // Эмулируем Ctrl+B
  261. emulateCtrlKey('b', 'KeyB', 66);
  262. break;
  263. case 'italic':
  264. // Эмулируем Ctrl+I
  265. emulateCtrlKey('i', 'KeyI', 73);
  266. break;
  267. case 'code':
  268. // Эмулируем Ctrl+J
  269. emulateCtrlKey('j', 'KeyJ', 74);
  270. break;
  271. default:
  272. // 3. Стандартная логика: эмулируем клик по оригинальной кнопке.
  273. triggerOriginalClick(action);
  274. }
  275. });
  276.  
  277. // --- СЕКЦИЯ 5: НАБЛЮДАТЕЛЬ (OBSERVER) ---
  278. // Эта часть отвечает за автоматическое появление и скрытие плавающей панели
  279. // синхронно с оригинальной панелью редактора.
  280.  
  281. // Переменная для хранения состояния (чтобы не дергать .style зря).
  282. let isFloaterVisible = false;
  283.  
  284. /**
  285. * Проверяет, видна ли *хотя бы одна* оригинальная панель редактора на странице.
  286. * Вызывается при любом изменении DOM (см. observer.observe).
  287. */
  288. function checkEditorVisibility() {
  289. // 1. Находим *все* панели инструментов на странице.
  290. const allPanels = document.querySelectorAll('.comments-editor-tools-panel');
  291. let isPanelVisible = false;
  292.  
  293. for (const panel of allPanels) {
  294. // 2. `.offsetParent !== null` — это самый надежный способ проверить,
  295. // видим ли элемент (включая проверку `display: none` у родительских элементов).
  296. if (panel.offsetParent !== null) {
  297. isPanelVisible = true;
  298. break; // Нашли хотя бы одну видимую, выходим.
  299. }
  300. }
  301.  
  302. // 3. Синхронизируем состояние нашей панели с состоянием оригинальной.
  303. if (isPanelVisible && !isFloaterVisible) {
  304. // Показываем нашу панель
  305. floater.style.display = 'flex';
  306. isFloaterVisible = true;
  307. } else if (!isPanelVisible && isFloaterVisible) {
  308. // Скрываем нашу панель
  309. floater.style.display = 'none';
  310. isFloaterVisible = false;
  311. }
  312. }
  313.  
  314. // 1. Создаем MutationObserver, который будет следить за изменениями на странице.
  315. const observer = new MutationObserver(checkEditorVisibility);
  316.  
  317. // 2. Настраиваем его на отслеживание:
  318. // - childList: добавление/удаление элементов (например, появление всего редактора).
  319. // - subtree: следить за всеми дочерними элементами.
  320. // - attributes: следить за изменениями атрибутов (важно для 'style' и 'class').
  321. observer.observe(document.body, {
  322. childList: true,
  323. subtree: true,
  324. attributes: true,
  325. attributeFilter: ['style', 'class'] // Следим только за важными атрибутами
  326. });
  327.  
  328. // 3. Запускаем проверку один раз при загрузке скрипта,
  329. // на случай, если редактор уже был открыт в момент старта.
  330. checkEditorVisibility();
  331.  
  332. })();
Advertisement
Add Comment
Please, Sign In to add comment