// ==UserScript==
// @name TopAreaNav
// @namespace top.areaNav
// @version 1.2.2
// @description Moves the Area Navigation Menu to the top
// @grant none
// @run-at document-end
// @match https://www.torn.com/*
// @author AndersAngstrom [3690608]
// @license Private to AndersAngstrom [3690608] – cannot be used or duplicated in any form
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// ==/UserScript==
(() => {
'use strict';
// Configuration options
const config = {
scrollAmount: 400, // Pixels to scroll on arrow click
arrowWidth: '24px', // Width of navigation arrows
arrowColor: 'white', // Color of arrow text
borderColor: '#000', // Border color between nav elements
buttonBgColor: '#222', // Background color for arrow buttons
safetyTimeout: 10000, // Maximum time to wait for elements before stopping observation (ms)
stickyPosition: 'top', // Where to stick the navigation ('top' or 'bottom')
stickyZIndex: 999, // Z-index for sticky navigation
stickyBackground: '#222', // Background color for sticky navigation
toggleBtnColor: '#222', // Background color for toggle button
toggleTextColor: 'white', // Text color for toggle button
storageKey: 'topAreaNavSticky' // Local storage key to remember the toggle state
};
// Get the toggle state from localStorage or default to enabled
const isNavigationEnabled = () => {
const storedValue = localStorage.getItem(config.storageKey);
return storedValue === null ? true : storedValue === 'true';
};
// Toggle the navigation state
const toggleNavigation = (stickyWrapper) => {
const currentState = isNavigationEnabled();
const newState = !currentState;
// Update local storage
localStorage.setItem(config.storageKey, newState);
// Update UI
if (stickyWrapper) {
updateNavigationVisibility(stickyWrapper, newState);
}
return newState;
};
// Update the visibility of the navigation based on state
const updateNavigationVisibility = (stickyWrapper, isEnabled) => {
if (isEnabled) {
// Enable sticky navigation
stickyWrapper.style.position = 'sticky';
stickyWrapper.classList.add('torn-nav-sticky');
} else {
// Disable sticky navigation but keep visible when scrolled to top
stickyWrapper.style.position = 'static';
stickyWrapper.classList.remove('torn-nav-sticky');
}
// Update toggle button text if it exists
const toggleBtn = document.getElementById('torn-nav-toggle');
if (toggleBtn) {
toggleBtn.textContent = isEnabled ? '📌' : '📍';
toggleBtn.title = isEnabled ? 'Unpin' : 'Pin';
}
};
// Create "Go to Top" button
const createGoToTopButton = () => {
const goToTopBtn = document.createElement('button');
goToTopBtn.id = 'torn-nav-top-btn';
goToTopBtn.setAttribute('type', 'button');
goToTopBtn.setAttribute('aria-label', 'Go to top of page');
goToTopBtn.title = 'Go to top of page';
// Style the button
Object.assign(goToTopBtn.style, {
cursor: 'pointer',
width: '50px',
height: '42px',
padding: '0',
margin: '0 0 0 4px',
//backgroundColor: config.buttonBgColor,
border: 'none',
borderRadius: '4px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
backgroundImage: 'url(https://www.torn.com/images/v2/svg_icons/globals/go_to_top.svg)',
backgroundPosition: '-4px -4px',
backgroundRepeat: 'no-repeat',
filter: 'invert(100%) brightness(50%)',
opacity: '1',
cursor: 'not-allowed',
});
// Add click event listener
goToTopBtn.addEventListener('click', () => {
if (window.scrollY > 300) {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
});
// Show/hide button based on scroll position
window.addEventListener('scroll', () => {
if (window.scrollY > 300) {
//goToTopBtn.style.opacity = '1';
goToTopBtn.style.backgroundPosition = '-4px -54px';
goToTopBtn.style.filter = 'invert(100%) brightness(100%)';
goToTopBtn.style.cursor = 'pointer';
} else {
goToTopBtn.style.backgroundPosition = '-4px -4px';
goToTopBtn.style.filter = 'invert(100%) brightness(50%)';
goToTopBtn.style.cursor = 'not-allowed';
}
}, { passive: true });
return goToTopBtn;
};
// Create the UI components once the target element is found
const setupNavigationUI = () => {
const wrapper = document.querySelector('.areasWrapper');
if (!wrapper) return false;
const content = wrapper.querySelector('.toggle-content___BJ9Q9');
if (!content) return false;
// Clone the original content
const clonedContent = content.cloneNode(true);
// Style the cloned content container
Object.assign(clonedContent.style, {
display: 'flex',
overflowX: 'auto', // 'auto' is more cross-browser compatible than 'scroll'
width: '100%',
flex: '1',
msOverflowStyle: 'none', // IE and Edge
scrollbarWidth: 'thin', // Firefox
WebkitOverflowScrolling: 'touch' // Smooth scrolling for iOS Safari
});
//Clone the "Go to Top" Button
const goTop = document.querySelector('#go-to-top-btn-root')
if (!goTop) return false;
const goToTopBtn = createGoToTopButton();
// Hide the scrollbar in WebKit browsers while keeping functionality
clonedContent.classList.add('custom-scrollbar');
// Style all navigation elements
const navElements = clonedContent.querySelectorAll('[id^="nav-"]');
navElements.forEach(nav => {
Object.assign(nav.style, {
flex: '0 0 auto',
width: 'auto',
height: 'auto',
boxSizing: 'border-box',
borderRight: `2px solid ${config.borderColor}`
});
// Style area rows
const areaRow = nav.querySelector('[class^="area-row"]');
if (areaRow) {
areaRow.style.borderRadius = '0';
// Style desktop links
const desktopLink = areaRow.querySelector('.desktopLink___SG2RU');
if (desktopLink) {
desktopLink.style.flexDirection = 'column';
desktopLink.style.padding = '8px';
}
}
});
// Create parent container with flex layout
const parentContainer = document.createElement('div');
Object.assign(parentContainer.style, {
position: 'relative',
width: '100%',
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 0',
boxSizing: 'border-box'
});
// Create outer wrapper for sticky positioning
const stickyWrapper = document.createElement('div');
Object.assign(stickyWrapper.style, {
width: '100%',
backgroundColor: config.stickyBackground,
padding: '4px 10px',
boxSizing: 'border-box'
});
// Apply sticky positioning based on current toggle state
const isEnabled = isNavigationEnabled();
Object.assign(stickyWrapper.style, {
width: '100%',
backgroundColor: config.stickyBackground,
padding: '4px 10px',
boxSizing: 'border-box',
boxShadow: '0 2px 4px rgba(0,0,0,0.3)',
zIndex: config.stickyZIndex.toString(),
[config.stickyPosition]: '0'
});
if (isEnabled) {
stickyWrapper.style.position = 'sticky';
stickyWrapper.classList.add('torn-nav-sticky');
} else {
stickyWrapper.style.position = 'static';
}
// Create navigation arrows with improved styling
const createArrowButton = (text, direction) => {
// Use button for semantic correctness but also create a div for Safari compatibility
const button = document.createElement('button');
button.setAttribute('type', 'button'); // Explicitly set type for accessibility
button.setAttribute('aria-label', direction < 0 ? 'Scroll left' : 'Scroll right');
button.textContent = text;
// Apply styles that work across browsers
Object.assign(button.style, {
cursor: 'pointer',
height: '100%',
width: config.arrowWidth,
color: config.arrowColor,
backgroundColor: config.buttonBgColor,
border: 'none',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0',
fontSize: '24px',
fontWeight: 'bold',
userSelect: 'none', // Prevent text selection
WebkitUserSelect: 'none', // Safari
MozUserSelect: 'none', // Firefox
msUserSelect: 'none', // IE/Edge
webkitAppearance: 'none', // Remove default styling in WebKit browsers
appearance: 'none' // Remove default styling in modern browsers
});
// Add click event listener
button.addEventListener('click', () => {
// Cross-browser smooth scrolling
if ('scrollBehavior' in document.documentElement.style) {
// Modern browsers that support smooth scrolling
clonedContent.scrollBy({
left: direction * config.scrollAmount,
behavior: 'smooth'
});
} else {
// Fallback for browsers that don't support ScrollToOptions with behavior
smoothScrollPolyfill(clonedContent, direction * config.scrollAmount);
}
});
return button;
};
// Create left and right navigation buttons
const leftArrow = createArrowButton('◀', -1);
const rightArrow = createArrowButton('▶', 1);
// Create toggle button
const toggleBtn = document.createElement('button');
toggleBtn.id = 'torn-nav-toggle';
toggleBtn.textContent = isEnabled ? '📌' : '📍';
toggleBtn.title = isEnabled ? 'Unpin' : 'Pin';
toggleBtn.setAttribute('type', 'button');
toggleBtn.setAttribute('aria-label', 'Toggle sticky navigation');
// Style the toggle button
Object.assign(toggleBtn.style, {
cursor: 'pointer',
padding: '8px',
margin: '0 0 0 4px',
backgroundColor: config.toggleBtnColor,
color: config.toggleTextColor,
border: 'none',
borderRadius: '4px',
fontSize: '18px',
fontWeight: 'bold',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
userSelect: 'none', // Prevent text selection
WebkitUserSelect: 'none', // Safari
MozUserSelect: 'none', // Firefox
msUserSelect: 'none', // IE/Edge
});
// Add click event listener to toggle button
toggleBtn.addEventListener('click', () => {
toggleNavigation(stickyWrapper);
});
// Create a container for right-side elements (right arrow + toggle button)
const leftSideContainer = document.createElement('div');
Object.assign(leftSideContainer.style, {
display: 'flex',
alignItems: 'center',
gap: '4px',
});
leftSideContainer.appendChild(goToTopBtn);
leftSideContainer.appendChild(leftArrow);
const rightSideContainer = document.createElement('div');
Object.assign(rightSideContainer.style, {
display: 'flex',
alignItems: 'center',
gap: '4px',
});
rightSideContainer.appendChild(rightArrow);
rightSideContainer.appendChild(toggleBtn);
// Assemble the components
parentContainer.appendChild(leftSideContainer);
parentContainer.appendChild(clonedContent);
parentContainer.appendChild(rightSideContainer);
stickyWrapper.appendChild(parentContainer);
// Add the custom navigation to the page
const targetElement = document.querySelector('.content');
if (targetElement) {
// Add the stickyWrapper to the page
targetElement.insertAdjacentElement('beforebegin', stickyWrapper);
// Hide the original wrapper and go-to-btn to avoid duplication
wrapper.style.display = 'none';
goTop.style.display = 'none';
// Add scroll tracking to enhance sticky visual effect
const handleScroll = () => {
if (window.scrollY > 10) {
stickyWrapper.classList.add('scrolled');
} else {
stickyWrapper.classList.remove('scrolled');
}
};
// Add scroll event listener with performance optimization
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
}, { passive: true });
// Initial call to set the correct state
handleScroll();
// Add touch swipe support for mobile browsers
let touchStartX = 0;
let touchEndX = 0;
clonedContent.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
clonedContent.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, { passive: true });
const handleSwipe = () => {
const swipeThreshold = 100; // Minimum distance for a swipe
if (touchEndX < touchStartX - swipeThreshold) {
// Swipe left, scroll right
rightArrow.click();
} else if (touchEndX > touchStartX + swipeThreshold) {
// Swipe right, scroll left
leftArrow.click();
}
};
return true;
}
return false;
};
// Polyfill for smooth scrolling in browsers that don't support scrollBy with behavior option
const smoothScrollPolyfill = (element, amount) => {
const startTime = performance.now();
const startScrollLeft = element.scrollLeft;
const duration = 300; // Duration of animation in milliseconds
const animateScroll = (currentTime) => {
const elapsedTime = currentTime - startTime;
if (elapsedTime < duration) {
// Easing function - easeInOutQuad
let progress = elapsedTime / duration;
progress = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
const newScrollLeft = startScrollLeft + amount * progress;
element.scrollLeft = newScrollLeft;
window.requestAnimationFrame(animateScroll);
} else {
// Ensure we end at the exact destination
element.scrollLeft = startScrollLeft + amount;
}
};
window.requestAnimationFrame(animateScroll);
};
// Add custom CSS to the page
const addCustomStyles = () => {
const style = document.createElement('style');
style.textContent = `
/* Hide scrollbar while maintaining functionality - cross-browser approach */
.custom-scrollbar {
scrollbar-width: thin; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.custom-scrollbar::-webkit-scrollbar {
height: 4px; /* Chrome, Safari, newer versions of Opera */
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #666;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #222;
}
/* For older browsers and Safari - hide scrollbar visually */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.custom-scrollbar {
overflow-y: overlay;
}
}
/* Sticky navigation styles */
@supports ((position: -webkit-sticky) or (position: sticky)) {
.torn-nav-sticky {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 999;
}
}
/* Transition effects for sticky state */
.torn-nav-sticky {
transition: box-shadow 0.3s ease;
}
.torn-nav-sticky.scrolled {
box-shadow: 0 3px 10px rgba(0,0,0,0.5);
}
/* Responsive adjustments */
@media screen and (max-width: 768px) {
.torn-nav-toggle-btn {
display: block !important;
}
}
/* Toggle button styles */
#torn-nav-toggle {
transition: background-color 0.2s ease;
}
#torn-nav-toggle:hover {
background-color: #333333;
}
/* Highlight when navigation is sticky */
#torn-nav-toggle.active {
box-shadow: 0 0 0 2px rgba(255,255,255,0.3);
}
`;
document.head.appendChild(style);
};
// Use a more efficient observer setup with safety timeout
const setupObserver = () => {
// Add custom styles first
addCustomStyles();
// Feature detection for MutationObserver
const MutationObserverImpl = window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver;
if (!MutationObserverImpl) {
console.warn('Torn Navigation Enhancer: MutationObserver not supported. Falling back to interval check.');
// Fallback for older browsers that don't support MutationObserver
const checkInterval = setInterval(() => {
if (setupNavigationUI()) {
clearInterval(checkInterval);
console.log('Torn Navigation Enhancer: Navigation UI created through interval check');
}
}, 500);
// Safety timeout to avoid infinite checking
setTimeout(() => {
clearInterval(checkInterval);
console.warn('Torn Navigation Enhancer: Timed out waiting for elements');
}, config.safetyTimeout);
return;
}
// Create an observer instance
const observer = new MutationObserverImpl((mutations, obs) => {
if (setupNavigationUI()) {
// Disconnect observer once the navigation is set up
obs.disconnect();
console.log('Torn Navigation Enhancer: Navigation UI successfully created');
}
});
// Safety timeout to prevent infinite observation
const safetyTimer = setTimeout(() => {
observer.disconnect();
console.warn('Torn Navigation Enhancer: Timed out waiting for elements');
}, config.safetyTimeout);
// Start observing with optimized settings
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
// Attempt to set up navigation immediately in case the elements are already loaded
if (setupNavigationUI()) {
clearTimeout(safetyTimer);
observer.disconnect();
console.log('Torn Navigation Enhancer: Navigation UI created immediately');
}
};
// Check browser compatibility and initialize the script
const isBrowserCompatible = () => {
// Feature detection for essential features
const hasQuerySelector = !!document.querySelector;
const hasEventListener = !!window.addEventListener;
const hasCreateElement = !!document.createElement;
return hasQuerySelector && hasEventListener && hasCreateElement;
};
// Run the script only if browser is compatible
if (isBrowserCompatible()) {
// Handle older browsers that don't have console.log
if (typeof console === 'undefined') {
window.console = {
log: function(){},
warn: function(){},
error: function(){}
};
}
// Wait for DOM to be ready in a cross-browser way
if (document.readyState === 'loading') {
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', setupObserver);
} else {
window.attachEvent('onload', setupObserver);
}
} else {
// DOM already loaded
setupObserver();
}
} else {
// Log error for incompatible browsers
if (window.console && console.error) {
console.error('Torn Navigation Enhancer: Your browser lacks required features to run this script.');
}
}
})();