// ==UserScript==
// @name XenForo Profile Post Quote Button
// @namespace https://greasyfork.org/
// @version 1.7
// @description Proper Quote button for profile posts
// @author You
// @match *://soyjak.forum/*
// @match *://*.soyjak.forum/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
function addQuoteButtons() {
document.querySelectorAll('a').forEach(link => {
const text = (link.textContent || '').trim().toLowerCase();
const href = (link.getAttribute('href') || '').toLowerCase();
if (!text.includes('report') && !href.includes('report')) return;
if (link.dataset.quoteAdded) return;
const post = link.closest('.message, .messageSimple, .message--profilePost, li, article, .block-row');
if (!post) return;
if (post.dataset.quoteProcessed) return;
link.dataset.quoteAdded = '1';
post.dataset.quoteProcessed = '1';
// Username
const userEl = post.querySelector('a.username, .username, .message-name a, .messageSimple-name a');
const username = userEl ? userEl.textContent.trim() : 'User';
// Try to get a clean message body only
let contentEl = post.querySelector('.bbWrapper');
if (!contentEl) contentEl = post.querySelector('.message-content .bbWrapper, .messageSimple-content .bbWrapper');
if (!contentEl) contentEl = post.querySelector('.message-content, .messageSimple-content, .message-body');
let content = '';
if (contentEl) {
// Clone so we can remove unwanted parts
const clone = contentEl.cloneNode(true);
// Remove timestamps, attribution, post numbers, etc.
clone.querySelectorAll(
'.message-attribution, .message-attribution-main, .message-attribution-opposite, ' +
'.message-userDetails, time, .DateTime, .u-dt, .message-header, ' +
'.message-footer, .actionBar, .message-actionBar, .reactionsBar'
).forEach(el => el.remove());
content = clone.innerText.trim();
}
if (!content) return;
// Create button
const btn = document.createElement('a');
btn.href = 'javascript:;';
btn.textContent = 'Quote';
btn.className = link.className;
btn.style.marginLeft = '10px';
btn.style.cursor = 'pointer';
btn.title = 'Quote this profile post';
let hasRun = false;
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
if (hasRun) return;
hasRun = true;
// Click real Reply button to open the box
const replyBtn = post.querySelector(
'a[href*="reply"], a.actionBar-action--reply, a[data-xf-click*="reply"], a[title*="Reply" i]'
);
if (replyBtn) replyBtn.click();
setTimeout(() => {
let editor = post.querySelector('textarea[name="message"], .fr-element[contenteditable="true"], textarea.js-editor');
if (!editor) {
editor = document.querySelector('.message-responseRow textarea, .js-quickReply textarea, textarea[name="message"], .fr-element[contenteditable="true"]');
}
if (editor) {
// Clean quote format
const quote = `[QUOTE="${username}"]\n${content}\n[/QUOTE]\n\n`;
if (editor.isContentEditable) {
editor.innerHTML = '';
editor.focus();
document.execCommand('insertText', false, quote);
} else {
editor.value = quote;
editor.focus();
editor.dispatchEvent(new Event('input', { bubbles: true }));
}
editor.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
setTimeout(() => { hasRun = false; }, 1000);
}, 450);
});
link.parentNode.insertBefore(btn, link.nextSibling);
});
}
addQuoteButtons();
const observer = new MutationObserver(addQuoteButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();