Greasy Fork is available in English.
Adds an autocompleting user field to forum search so you can filter by poster. Cmd/Ctrl+Click or Cmd/Ctrl+Enter opens results in a new tab.
// ==UserScript==
// @name Search Torn Forums by User
// @namespace https://www.torn.com/
// @version 1.0
// @author Kalends [2032147]
// @license MIT
// @description Adds an autocompleting user field to forum search so you can filter by poster. Cmd/Ctrl+Click or Cmd/Ctrl+Enter opens results in a new tab.
// @match https://www.torn.com/forums.php*
// @run-at document-idle
// @grant none
// @noframes
// ==/UserScript==
(function () {
'use strict';
const IDS = {
style: 'tm-forum-user-search-style',
li: 'tm-native-user-search-li',
wrap: 'tm-native-user-search-wrap',
input: 'tm-native-user-search',
cont: 'tm-native-user-search-cont'
};
const SELECTORS = {
panel: '.search-wrap .panel.fm-list',
lastLi: '.search-wrap .panel.fm-list > li.last',
searchInput: '.search-wrap input[name="searchword"]',
forumSelect: '.search-wrap select[name="forum"]',
typeSelect: '.search-wrap select[name="type"]',
searchButton: '.search-wrap .btn input[type="submit"]'
};
const OBSERVER_ROOT_SELECTORS = ['.content-wrapper', '#forums-page-wrap', '.forums-main-wrap'];
const FORUM_SEARCH_CACHE_KEY = 'forumSearchCache';
const FORUM_SEARCH_CACHE_LIMIT = 5;
const MAX_INIT_RETRIES = 20;
let pendingRefreshFrame = 0;
let pendingMountFrame = 0;
let initRetries = 0;
let observer = null;
let observerRoot = null;
let resizeBound = false;
function extractUserId(value) {
const text = String(value || '').trim();
const bracketed = text.match(/\[(\d+)\]\s*$/);
if (bracketed) return bracketed[1];
return /^\d+$/.test(text) ? text : '';
}
function getSearchParts() {
const searchInput = document.querySelector(SELECTORS.searchInput);
const forumSelect = document.querySelector(SELECTORS.forumSelect);
const typeSelect = document.querySelector(SELECTORS.typeSelect);
const userInput = document.getElementById(IDS.input);
if (!searchInput || !forumSelect || !typeSelect || !userInput) return null;
const text = String(searchInput.value || '').trim();
const userId = extractUserId(userInput.value);
let query = text;
if (userId && text) query = `by:${userId} ${text}`;
else if (userId) query = `by:${userId}`;
const params = new URLSearchParams({
p: 'search',
q: query,
f: String(forumSelect.value ?? 0),
y: String(typeSelect.value ?? 0)
});
const hash = params.toString().replace(/%3A/gi, ':');
return {
hash,
text,
url: `${location.origin}/forums.php#/${hash}`
};
}
function rememberKeywordSearch(text) {
const keyword = String(text || '').trim();
if (!keyword) return;
try {
const current = JSON.parse(localStorage.getItem(FORUM_SEARCH_CACHE_KEY) || '[]');
const next = [keyword]
.concat(current.filter((value) => value !== keyword))
.slice(0, FORUM_SEARCH_CACHE_LIMIT);
localStorage.setItem(FORUM_SEARCH_CACHE_KEY, JSON.stringify(next));
} catch (e) {}
}
function runSearch(openInNewTab) {
const parts = getSearchParts();
if (!parts) return;
rememberKeywordSearch(parts.text);
if (openInNewTab) {
const a = document.createElement('a');
a.href = parts.url;
a.target = '_blank';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
} else if (window.hasher && typeof window.hasher.setHash === 'function') {
window.hasher.setHash(parts.hash);
} else {
location.href = parts.url;
}
}
function findObserverRoot() {
for (const selector of OBSERVER_ROOT_SELECTORS) {
const root = document.querySelector(selector);
if (root) return root;
}
return document.body;
}
function clearUserAutocompleteResults() {
const dropdown = document.getElementById(IDS.cont);
const menu = dropdown && dropdown.querySelector('.ui-autocomplete');
if (menu) menu.textContent = '';
}
function closeUserAutocomplete() {
const input = document.getElementById(IDS.input);
const dropdown = document.getElementById(IDS.cont);
const menu = dropdown && dropdown.querySelector('.ui-autocomplete');
if (dropdown) dropdown.classList.remove('open', 'ac-focus-input');
if (input) input.classList.remove('open', 'chosen', 'ui-autocomplete-loading');
if (window.jQuery && input) {
try {
const $input = window.jQuery(input);
if ($input.data('ui-autocomplete')) {
$input.autocomplete('close');
}
} catch (e) {}
}
if (menu) {
menu.style.display = 'none';
}
}
function resetUserAutocomplete() {
closeUserAutocomplete();
clearUserAutocompleteResults();
}
function ensureAutocompleteInitialized(input) {
if (!input) return false;
if (!document.getElementById(IDS.cont) && typeof window.initializeAutocompleteSearch === 'function') {
try {
window.initializeAutocompleteSearch({ type: 'all', isNotLink: true });
} catch (e) {}
}
if (!document.getElementById(IDS.cont)) return false;
initRetries = 0;
input.classList.add('ui-autocomplete-input');
input.classList.remove('ac-search');
return true;
}
function autocompleteMenuWidth(input) {
const style = getComputedStyle(input);
const px = (value) => Number.parseFloat(value) || 0;
const padding = px(style.paddingLeft) + px(style.paddingRight);
const border = px(style.borderLeftWidth) + px(style.borderRightWidth);
let contentWidth = px(style.width);
if (style.boxSizing === 'border-box') {
contentWidth = Math.max(0, contentWidth - padding - border);
}
return Math.round(contentWidth + padding);
}
function normalizeInputStyles(input, dropdown) {
input.style.removeProperty('background');
input.style.removeProperty('color');
input.style.removeProperty('border-color');
input.style.removeProperty('border-radius');
dropdown.style.removeProperty('top');
}
function syncAutocompleteWidth(input, dropdown) {
const width = autocompleteMenuWidth(input);
if (width) {
dropdown.style.left = '0px';
dropdown.style.width = `${width}px`;
}
}
function isAutocompleteMenuOpen(menu) {
return !!(
menu &&
menu.children.length &&
getComputedStyle(menu).display !== 'none'
);
}
function syncAutocompleteOpenState(input, dropdown) {
const menu = dropdown.querySelector('.ui-autocomplete');
const hasText = !!String(input.value || '').trim();
const menuOpen = hasText && isAutocompleteMenuOpen(menu);
if (menu) {
menu.style.width = 'auto';
menu.style.zIndex = '1';
}
input.classList.toggle('open', menuOpen);
dropdown.classList.toggle('open', menuOpen);
}
function syncAutocompleteBox() {
const input = document.getElementById(IDS.input);
const dropdown = document.getElementById(IDS.cont);
if (!input || !dropdown) return false;
normalizeInputStyles(input, dropdown);
syncAutocompleteWidth(input, dropdown);
syncAutocompleteOpenState(input, dropdown);
return true;
}
function bindNativeSearchControls(searchInput, searchButton) {
if (searchInput && searchInput.dataset.tmUserSearchBound !== '1') {
searchInput.dataset.tmUserSearchBound = '1';
searchInput.addEventListener('keydown', function (event) {
if (event.key !== 'Enter') return;
event.preventDefault();
runSearch(event.metaKey || event.ctrlKey);
}, true);
}
if (searchButton && searchButton.dataset.tmUserSearchBound !== '1') {
searchButton.dataset.tmUserSearchBound = '1';
searchButton.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
runSearch(event.metaKey || event.ctrlKey);
}, true);
}
}
function bindResize() {
if (resizeBound) return;
resizeBound = true;
window.addEventListener('resize', function () {
refreshUi(false);
}, true);
}
function scheduleMount() {
if (pendingMountFrame) return;
pendingMountFrame = requestAnimationFrame(() => {
pendingMountFrame = 0;
mount();
bindObserver();
});
}
function bindObserver() {
const nextRoot = findObserverRoot();
if (!nextRoot || nextRoot === observerRoot) return;
if (observer) observer.disconnect();
observerRoot = nextRoot;
observer = new MutationObserver(scheduleMount);
observer.observe(observerRoot, { childList: true, subtree: true });
if (nextRoot === document.body) {
setTimeout(bindObserver, 500);
}
}
function refreshUi(reopen) {
if (pendingRefreshFrame) cancelAnimationFrame(pendingRefreshFrame);
pendingRefreshFrame = requestAnimationFrame(() => {
pendingRefreshFrame = 0;
const input = document.getElementById(IDS.input);
const hasText = !!String(input?.value || '').trim();
if (!hasText) {
resetUserAutocomplete();
} else if (reopen && window.jQuery) {
try {
const $input = window.jQuery(input);
if ($input.data('ui-autocomplete')) {
$input.autocomplete('search', input.value || '');
}
} catch (e) {}
}
syncAutocompleteBox();
});
}
function mount() {
const panel = document.querySelector(SELECTORS.panel);
const lastLi = document.querySelector(SELECTORS.lastLi);
const searchInput = document.querySelector(SELECTORS.searchInput);
const searchButton = document.querySelector(SELECTORS.searchButton);
if (!panel || !lastLi || !searchInput || !searchButton) return;
if (!document.getElementById(IDS.style)) {
const style = document.createElement('style');
style.id = IDS.style;
style.textContent = `
#${IDS.li} .ac-options {
display: none !important;
}
#${IDS.li} .autocomplete-wrap .viewport,
#${IDS.li} .autocomplete-wrap .scrollbar {
top: 0 !important;
}
#${IDS.li} {
border: 0;
}
#${IDS.wrap},
#${IDS.wrap} .ac-wrapper {
width: 100%;
}
#${IDS.wrap} {
position: relative;
display: inline-block;
}
#${IDS.input},
#${IDS.input}:hover,
#${IDS.input}:focus,
#${IDS.input}.ui-autocomplete-input {
width: calc(100% - 22px) !important;
height: 14px !important;
min-height: 14px !important;
box-sizing: content-box !important;
line-height: 14px !important;
padding: 9px 10px !important;
background: #fff !important;
background: var(--input-background-color) !important;
color: #000 !important;
color: var(--input-color) !important;
border-color: #ccc !important;
border-color: var(--input-border-color) !important;
border-radius: 5px !important;
box-shadow: none;
outline: none;
}
#${IDS.input}:hover {
border-color: #999 !important;
border-color: var(--input-hover-border-color) !important;
box-shadow: var(--input-hover-box-shadow) !important;
}
#${IDS.input}:focus,
#${IDS.input}.ac-focus {
border-color: #1864AB80 !important;
border-color: var(--input-focus-border-color) !important;
box-shadow: var(--input-focus-box-shadow) !important;
}
#${IDS.input}.open {
border-radius: 5px 5px 0 0 !important;
}
#${IDS.cont} {
pointer-events: none;
}
#${IDS.cont}.open {
pointer-events: auto;
}
#${IDS.cont}.search-autocomplete-wrap.open .viewport {
box-sizing: border-box;
border: 1px solid #fff;
border: 1px solid var(--default-panel-divider-inner-side-color);
box-shadow: 0 1px 2px 1px #ccc;
box-shadow: var(--autocomplete-box-shadow);
}
#${IDS.cont} .ui-autocomplete {
width: auto !important;
}
`;
document.head.appendChild(style);
}
if (!document.getElementById(IDS.li)) {
const li = document.createElement('li');
li.id = IDS.li;
const wrap = document.createElement('div');
wrap.id = IDS.wrap;
wrap.className = 'left fm-search-input';
const userInput = document.createElement('input');
userInput.id = IDS.input;
userInput.className = 'ac-search';
userInput.type = 'text';
userInput.placeholder = 'User';
userInput.autocomplete = 'off';
userInput.dataset.action = 'autocompleteUserAjaxAction';
wrap.appendChild(userInput);
li.appendChild(wrap);
panel.insertBefore(li, lastLi);
}
const input = document.getElementById(IDS.input);
if (!input) return;
const autocompleteReady = ensureAutocompleteInitialized(input);
if (!autocompleteReady && input.isConnected && initRetries < MAX_INIT_RETRIES) {
initRetries += 1;
setTimeout(scheduleMount, 250);
}
const dropdown = document.getElementById(IDS.cont);
if (dropdown) {
dropdown.classList.add('search-autocomplete-wrap');
}
refreshUi(false);
bindNativeSearchControls(searchInput, searchButton);
bindResize();
if (input.dataset.tmBound !== '1') {
input.dataset.tmBound = '1';
input.addEventListener('keydown', function (event) {
if (event.key !== 'Enter') return;
const dropdown = document.getElementById(IDS.cont);
const menu = dropdown && dropdown.querySelector('.ui-autocomplete');
const hasChosenId = !!extractUserId(input.value);
const menuOpen = isAutocompleteMenuOpen(menu);
if (!hasChosenId && menuOpen) return;
event.preventDefault();
runSearch(event.metaKey || event.ctrlKey);
}, true);
input.addEventListener('focus', function () {
refreshUi(true);
}, true);
input.addEventListener('click', function () {
refreshUi(true);
}, true);
input.addEventListener('input', function () {
refreshUi(false);
}, true);
input.addEventListener('blur', function () {
setTimeout(() => refreshUi(false), 0);
}, true);
}
}
mount();
bindObserver();
})();