// ==UserScript== // @name Discourse Customizable Quick Replies // @namespace https://github.com/stevessr/bug-v3 // @version 2.1.0 // @description Adds customizable quick replies with a UI to Discourse forums, with enhanced UI theming. // @author stevessr (modified by an AI assistant) // @match https://linux.do/* // @match https://meta.discourse.org/* // @match https://*.discourse.org/* // @match http://localhost:5173/* // @exclude https://linux.do/a/* // @match https://idcflare.com/* // @grant none // @license MIT // @homepageURL https://github.com/stevessr/bug-v3 // @supportURL https://github.com/stevessr/bug-v3/issues // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/554981/Discourse%20Customizable%20Quick%20Replies.user.js // @updateURL https://update.greasyfork.icu/scripts/554981/Discourse%20Customizable%20Quick%20Replies.meta.js // ==/UserScript== (function () { 'use strict'; // ===== Settings Management ===== const SETTINGS_KEY = 'custom_quick_replies_settings'; let quickReplies = []; const DEFAULT_QUICK_REPLIES = [ { display: 'Info', insert: '>[!info]+\n', prefix: '[!info' }, { display: 'Tip', insert: '>[!tip]+\n', prefix: '[!tip' }, { display: 'Success', insert: '>[!success]+\n', prefix: '[!success' }, { display: 'Warning', insert: '>[!warning]+\n', prefix: '[!warning' }, { display: 'Danger', insert: '>[!danger]+\n', prefix: '[!danger' }, ]; function loadQuickReplies() { try { const settingsData = localStorage.getItem(SETTINGS_KEY); if (settingsData) { quickReplies = JSON.parse(settingsData); } else { quickReplies = DEFAULT_QUICK_REPLIES; } } catch (e) { console.warn('[Quick Replies] Failed to load settings:', e); quickReplies = DEFAULT_QUICK_REPLIES; } } function saveQuickReplies() { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(quickReplies)); } catch (e) { console.error('[Quick Replies] Failed to save settings:', e); } } // ===== Suggestion Box ===== let suggestionBox = null; let activeSuggestionIndex = 0; // ===== Entry Point ===== if (isDiscoursePage()) { console.log('[Quick Replies] Discourse detected, initializing...'); loadQuickReplies(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initQuickReplyFeatures(); initQuickInsertButton(); }); } else { initQuickReplyFeatures(); initQuickInsertButton(); } } // ===== Detailed Implementation ===== function isDiscoursePage() { return document.querySelector('#main-outlet, .ember-application, textarea.d-editor-input, .ProseMirror.d-editor-input'); } function initQuickReplyFeatures() { createSuggestionBox(); document.addEventListener('input', handleInput, true); document.addEventListener('keydown', handleKeydown, true); document.addEventListener('click', e => { if (e.target && e.target.tagName !== 'TEXTAREA' && suggestionBox && !suggestionBox.contains(e.target)) { hideSuggestionBox(); } }); console.log('[Quick Replies] Suggestion features initialized.'); } function initQuickInsertButton() { const observer = new MutationObserver(() => { const toolbars = document.querySelectorAll('.d-editor-button-bar, .chat-composer__inner-container'); toolbars.forEach(toolbar => { if (!toolbar.querySelector('.quick-reply-settings-btn')) { injectQuickInsertButton(toolbar); } }); }); observer.observe(document.body, { childList: true, subtree: true }); console.log('[Quick Replies] Quick insert button observer initialized.'); } function injectQuickInsertButton(toolbar) { const quickInsertButton = document.createElement('button'); quickInsertButton.className = 'btn no-text btn-icon toolbar__button quick-insert-button'; quickInsertButton.title = 'Quick Replies'; quickInsertButton.innerHTML = '⎘'; quickInsertButton.addEventListener('click', e => { e.stopPropagation(); toggleQuickInsertMenu(quickInsertButton); }); const settingsButton = document.createElement('button'); settingsButton.className = 'btn no-text btn-icon toolbar__button quick-reply-settings-btn'; settingsButton.title = 'Quick Reply Settings'; settingsButton.innerHTML = '⚙️'; settingsButton.addEventListener('click', e => { e.stopPropagation(); createSettingsUI(); }); toolbar.appendChild(quickInsertButton); toolbar.appendChild(settingsButton); } function toggleQuickInsertMenu(button) { let menu = document.getElementById('quick-insert-menu'); if (menu) { menu.remove(); return; } menu = createQuickInsertMenu(); document.body.appendChild(menu); const rect = button.getBoundingClientRect(); menu.style.position = 'fixed'; menu.style.top = `${rect.bottom + 5}px`; menu.style.left = `${rect.left}px`; const removeMenu = ev => { if (menu && !menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', removeMenu); } }; setTimeout(() => document.addEventListener('click', removeMenu), 100); } function createQuickInsertMenu() { const menu = document.createElement('div'); menu.id = 'quick-insert-menu'; menu.style.cssText = ` position: absolute; background-color: var(--secondary); border: 1px solid var(--primary-low-mid); border-radius: 6px; box-shadow: var(--d-shadow-medium); z-index: 10000; padding: 5px; `; quickReplies.forEach(reply => { const item = document.createElement('div'); item.textContent = reply.display; item.style.cssText = ` padding: 8px 12px; cursor: pointer; color: var(--primary-high); border-radius: 4px; `; item.onmouseover = () => item.style.backgroundColor = 'var(--d-hover)'; item.onmouseout = () => item.style.backgroundColor = 'transparent'; item.addEventListener('click', () => { insertIntoEditor(reply.insert); toggleQuickInsertMenu(); // Close menu }); menu.appendChild(item); }); return menu; } function insertIntoEditor(text) { const textarea = document.querySelector('textarea.d-editor-input, textarea.ember-text-area'); if (textarea) { const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + text.length; textarea.focus(); textarea.dispatchEvent(new Event('input', { bubbles: true })); } else { document.execCommand('insertText', false, text); } } // --- Suggestion Box Functions --- function createSuggestionBox() { if (suggestionBox) return; suggestionBox = document.createElement('div'); suggestionBox.id = 'callout-suggestion-box'; document.body.appendChild(suggestionBox); const style = document.createElement('style'); style.textContent = ` #callout-suggestion-box { position: absolute; background-color: var(--secondary); border: 1px solid var(--primary-low-mid); border-radius: 6px; box-shadow: var(--d-shadow-medium); z-index: 999999; padding: 5px; display: none; } .callout-suggestion-item { padding: 8px 12px; cursor: pointer; color: var(--primary-high); border-radius: 4px; } .callout-suggestion-item.active { background-color: var(--d-hover) !important; } `; document.head.appendChild(style); } function hideSuggestionBox() { if (suggestionBox) suggestionBox.style.display = 'none'; } function updateSuggestionBox(element, matches) { if (!suggestionBox || matches.length === 0) { hideSuggestionBox(); return; } suggestionBox.innerHTML = matches.map((reply, index) => `