TopAreaNav

Moves the Area Navigation Menu to the top

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.');
    }
  }
})();