Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Playground.ru Comment Floater
- // @match *://*.playground.ru/*
- // @version 0.1
- // @description Добавляет плавающую панель редактора, дублирующую кнопки (включая эмуляцию Ctrl+B/I/J), которая появляется и исчезает вместе с оригинальной панелью.
- // @grant GM_addStyle
- // @run-at document-idle
- // ==/UserScript==
- (function() {
- 'use strict';
- // --- СЕКЦИЯ 1: СТИЛИЗАЦИЯ ---
- // Этот блок добавляет CSS в страницу для нашей плавающей панели.
- // - #pg-floating-toolbar: Определяет позицию (fixed), внешний вид (темный),
- // z-index (чтобы быть поверх других элементов) и начальное состояние (display: none).
- // - .btn: Стилизует кнопки внутри панели, чтобы они соответствовали теме сайта.
- GM_addStyle(`
- #pg-floating-toolbar {
- position: fixed;
- top: 150px;
- right: 20px;
- background: #2f2f2f;
- border: 1px solid #444;
- border-radius: 5px;
- padding: 5px;
- z-index: 9998;
- display: none; /* Изначально панель скрыта */
- flex-direction: column;
- gap: 5px;
- box-shadow: 0 4px 10px rgba(0,0,0,0.5);
- }
- #pg-floating-toolbar .btn {
- background: #3c3c3c;
- color: #ddd;
- border: 1px solid #555;
- padding: 8px 10px;
- margin: 0;
- line-height: 1.2;
- }
- #pg-floating-toolbar .btn:hover {
- background: #4a4a4a;
- color: #fff;
- }
- `);
- // --- СЕКЦИЯ 2: СОЗДАНИЕ HTML ПАНЕЛИ ---
- // Этот блок создает <div> элемент плавающей панели,
- // добавляет в него HTML-код для всех кнопок (с иконками)
- // и встраивает готовую панель в <body> страницы.
- const floater = document.createElement('div');
- floater.id = 'pg-floating-toolbar';
- floater.innerHTML = `
- <button class="btn" data-action="bold" title="Выделенный (Ctrl+B)">
- <span class="glyphicon glyphicon-bold"></span>
- </button>
- <button class="btn" data-action="italic" title="Наклонный (Ctrl+I)">
- <span class="glyphicon glyphicon-italic"></span>
- </button>
- <button class="btn" data-action="quote" title="Цитата">
- <span class="icon-quote"></span>
- </button>
- <button class="btn" data-action="spoiler" title="Спойлер">
- <span class="glyphicon glyphicon-eye-close"></span>
- </button>
- <button class="btn" data-action="link" title="Ссылка">
- <span class="glyphicon glyphicon-link"></span>
- </button>
- <button class="btn" data-action="image" title="Изображение">
- <span class="glyphicon glyphicon-picture"></span>
- </button>
- <button class="btn" data-action="code" title="Код (Ctrl+J)">
- <span class="glyphicon glyphicon-list-alt"></span>
- </button>
- `;
- document.body.appendChild(floater);
- // --- СЕКЦИЯ 3: ОСНОВНЫЕ УТИЛИТЫ ---
- // Эти функции выполняют "грязную" работу по эмуляции действий пользователя.
- /**
- * Эмулирует ПОЛНЫЙ клик мыши (mousedown -> mouseup -> click).
- * Это необходимо, т.к. React-компоненты сайта (на которых построен редактор)
- * часто игнорируют простое .click().
- *
- * @param {HTMLElement} element - Кнопка, на которую нужно "нажать".
- */
- function emulateClick(element) {
- if (!element) {
- return;
- }
- // Используем `unsafeWindow` вместо `window`.
- // Так как скрипт выполняется в "песочнице" (@grant GM_addStyle),
- // `window` ссылается на песочницу, а не на окно страницы.
- // `view: unsafeWindow` жизненно важен, иначе new MouseEvent() вызовет ошибку.
- const commonEventProps = { bubbles: true, cancelable: true, view: unsafeWindow };
- element.dispatchEvent(new MouseEvent('mousedown', commonEventProps));
- element.dispatchEvent(new MouseEvent('mouseup', commonEventProps));
- element.dispatchEvent(new MouseEvent('click', commonEventProps));
- }
- /**
- * Эмулирует нажатие комбинации клавиш Ctrl + [клавиша].
- * Вместо клика по кнопке, эта функция создает и отправляет событие
- * нажатия клавиш прямо в активное поле ввода.
- *
- * @param {string} key - Символ клавиши (напр. 'b', 'i', 'j')
- * @param {string} code - Код клавиши (напр. 'KeyB', 'KeyI', 'KeyJ')
- * @param {number} keyCode - keyCode клавиши (напр. 66, 73, 74)
- */
- function emulateCtrlKey(key, code, keyCode) {
- // 1. Находим элемент, в котором сейчас находится курсор.
- const activeEditor = document.activeElement;
- // 2. Проверяем, что это действительно поле ввода редактора.
- if (!activeEditor || activeEditor.getAttribute('contenteditable') !== 'true') {
- return; // Если курсор не в редакторе, ничего не делаем.
- }
- // 3. Описываем "фальшивое" событие нажатия клавиши.
- const keyEventProps = {
- key: key,
- code: code,
- keyCode: keyCode,
- which: keyCode,
- ctrlKey: true, // Главное — указываем, что Ctrl зажат.
- bubbles: true,
- cancelable: true,
- view: unsafeWindow // Опять используем unsafeWindow.
- };
- // 4. Отправляем "фальшивое" событие 'keydown' прямо в поле ввода.
- activeEditor.dispatchEvent(new KeyboardEvent('keydown', keyEventProps));
- }
- /**
- * Находит *активную* панель инструментов, к которой привязан *активный* редактор.
- * Это ключевая функция, чтобы скрипт "не промахнулся" мимо нужной панели,
- * когда на странице их несколько (например, основной редактор и редактор ответа).
- *
- * @returns {HTMLElement|null} - Элемент .comments-editor-tools-panel или null.
- */
- function getActiveToolsPanel() {
- // 1. Находим, где сейчас курсор.
- const activeEditor = document.activeElement;
- if (!activeEditor || activeEditor.getAttribute('contenteditable') !== 'true') {
- return null; // Курсор не в редакторе
- }
- // 2. От курсора "поднимаемся" вверх до родительского компонента редактора.
- const editorRoot = activeEditor.closest('.comments-editor.js-comments-editor');
- if (!editorRoot) {
- return null; // Не удалось найти корень редактора
- }
- // 3. Внутри этого компонента ищем *его* панель инструментов.
- const originalPanel = editorRoot.querySelector('.comments-editor-tools-panel');
- return originalPanel; // Возвращаем панель или null
- }
- // --- СЕКЦИЯ 4: ЛОГИКА ОБРАБОТЧИКОВ ---
- /**
- * Главная функция-диспетчер, вызываемая при клике (кроме "Код", "Жирный", "Курсив").
- * Находит нужную кнопку на оригинальной панели и эмулирует клик по ней.
- *
- * @param {string} action - Действие ('quote', 'spoiler' и т.д.), взятое из data-action.
- */
- function triggerOriginalClick(action) {
- // 1. Получаем *только* ту панель, которая активна СЕЙЧАС.
- const originalPanel = getActiveToolsPanel();
- if (!originalPanel) {
- return; // Не в редакторе, ничего не делаем.
- }
- let targetButton;
- // 2. Ищем нужную кнопку по ее тексту-подсказке (data-balloon).
- // Это надежнее, чем искать по классам иконок.
- switch(action) {
- // 'bold' и 'italic' теперь обрабатываются через emulateCtrlKey
- case 'quote': targetButton = originalPanel.querySelector('button[data-balloon*="цитата"]'); break;
- case 'spoiler': targetButton = originalPanel.querySelector('button[data-balloon*="спойлер"]'); break;
- case 'link':
- targetButton = originalPanel.querySelector('button[data-balloon*="ссылку"]');
- // Не эмулируем клик.
- // Вместо этого, вручную открываем модальное окно, на которое
- // ссылается кнопка. Это единственный способ избежать
- // "прыжка" скроллбара, который вызывает Bootstrap/браузер.
- if (targetButton && targetButton.dataset.target) {
- const modal = document.querySelector(targetButton.dataset.target);
- // Проверяем, что jQuery ($) и метод .modal() существуют
- if (modal && typeof $ === 'function' && typeof $.fn.modal === 'function') {
- // Добавляем одноразовый слушатель события 'shown.bs.modal'.
- // Это событие срабатывает ПОСЛЕ того, как модальное окно
- // полностью появилось (завершило анимацию).
- $(modal).one('shown.bs.modal', function() {
- // Ищем инпут внутри *этого* модального окна.
- // Используем [name="url-input"] и [type="url"] вместо динамического ID.
- const urlInput = modal.querySelector('input[type="url"][name="url-input"]');
- if (urlInput) {
- // Если нашли, ставим на него фокус.
- urlInput.focus();
- }
- });
- // $(modal).modal('show') — это jQuery-метод Bootstrap
- $(modal).modal('show');
- }
- }
- return; // Выходим из функции, клик не нужен.
- case 'image': targetButton = originalPanel.querySelector('label[data-balloon*="изображение"]'); break;
- // 'code' обрабатывается отдельно в слушателе.
- }
- // 3. Если кнопка найдена...
- if (targetButton) {
- // Для "image" (т.к. "link" теперь обрабатывается выше)
- // используем простой .click().
- if (action === 'image') {
- targetButton.click();
- } else {
- // Для всех остальных кнопок (quote, spoiler)
- // "тяжелая" эмуляция жизненно необходима, чтобы React их "увидел".
- emulateClick(targetButton);
- }
- }
- }
- /**
- * Основной слушатель событий для *нашей* плавающей панели.
- * Используем 'mousedown', а не 'click', чтобы применить e.preventDefault().
- */
- floater.addEventListener('mousedown', (e) => {
- // Находим кнопку, на которую нажали (даже если попали по иконке внутри нее).
- const button = e.target.closest('button[data-action]');
- if (!button) return; // Клик мимо кнопки
- const action = button.dataset.action;
- // 1. ПРЕДОТВРАЩАЕМ КРАЖУ ФОКУСА (e.preventDefault()):
- // Это КРИТИЧНО. Без этого, при нажатии на нашу панель,
- // редактор теряет фокус, и выделение текста сбрасывается.
- // Не делаем этого ТОЛЬКО для "image" (это <label>).
- // Для "link" (это <button>) мы *должны* отменить e.preventDefault(),
- // чтобы остановить прыжок скролла.
- if (action !== 'image') {
- e.preventDefault();
- }
- // В любом случае останавливаем всплытие, чтобы не кликнуть "сквозь" панель.
- e.stopPropagation();
- // 2. Логика обработки действий
- switch (action) {
- case 'bold':
- // Эмулируем Ctrl+B
- emulateCtrlKey('b', 'KeyB', 66);
- break;
- case 'italic':
- // Эмулируем Ctrl+I
- emulateCtrlKey('i', 'KeyI', 73);
- break;
- case 'code':
- // Эмулируем Ctrl+J
- emulateCtrlKey('j', 'KeyJ', 74);
- break;
- default:
- // 3. Стандартная логика: эмулируем клик по оригинальной кнопке.
- triggerOriginalClick(action);
- }
- });
- // --- СЕКЦИЯ 5: НАБЛЮДАТЕЛЬ (OBSERVER) ---
- // Эта часть отвечает за автоматическое появление и скрытие плавающей панели
- // синхронно с оригинальной панелью редактора.
- // Переменная для хранения состояния (чтобы не дергать .style зря).
- let isFloaterVisible = false;
- /**
- * Проверяет, видна ли *хотя бы одна* оригинальная панель редактора на странице.
- * Вызывается при любом изменении DOM (см. observer.observe).
- */
- function checkEditorVisibility() {
- // 1. Находим *все* панели инструментов на странице.
- const allPanels = document.querySelectorAll('.comments-editor-tools-panel');
- let isPanelVisible = false;
- for (const panel of allPanels) {
- // 2. `.offsetParent !== null` — это самый надежный способ проверить,
- // видим ли элемент (включая проверку `display: none` у родительских элементов).
- if (panel.offsetParent !== null) {
- isPanelVisible = true;
- break; // Нашли хотя бы одну видимую, выходим.
- }
- }
- // 3. Синхронизируем состояние нашей панели с состоянием оригинальной.
- if (isPanelVisible && !isFloaterVisible) {
- // Показываем нашу панель
- floater.style.display = 'flex';
- isFloaterVisible = true;
- } else if (!isPanelVisible && isFloaterVisible) {
- // Скрываем нашу панель
- floater.style.display = 'none';
- isFloaterVisible = false;
- }
- }
- // 1. Создаем MutationObserver, который будет следить за изменениями на странице.
- const observer = new MutationObserver(checkEditorVisibility);
- // 2. Настраиваем его на отслеживание:
- // - childList: добавление/удаление элементов (например, появление всего редактора).
- // - subtree: следить за всеми дочерними элементами.
- // - attributes: следить за изменениями атрибутов (важно для 'style' и 'class').
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['style', 'class'] // Следим только за важными атрибутами
- });
- // 3. Запускаем проверку один раз при загрузке скрипта,
- // на случай, если редактор уже был открыт в момент старта.
- checkEditorVisibility();
- })();
Advertisement
Add Comment
Please, Sign In to add comment