Jump to content

User:Overandoutnerd/Scripts/goToTop.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
mw.loader.using(['mediawiki.util']).then(function () {
  $(function () {
    'use strict';

    var BUTTON_ID = 'to-top';

    // --------------------------------------------------
    // Editing detection (desktop + mobile SPA)
    // --------------------------------------------------
    function isEditing() {
      // Mobile editor SPA route
      if (location.hash && location.hash.startsWith('#/editor')) {
        return true;
      }

      // Source editor
      var action = mw.config.get('wgAction');
      if (action === 'edit' || action === 'submit') {
        return true;
      }

      // VisualEditor / edit UI
      if (
        document.body.classList.contains('ve-activated') ||
        document.body.classList.contains('mw-editing') ||
        document.querySelector('.ve-ui-overlay') ||
        document.querySelector('.mw-editform')
      ) {
        return true;
      }

      return false;
    }

    // --------------------------------------------------
    // Button lifecycle
    // --------------------------------------------------
    function buttonExists() {
      return document.getElementById(BUTTON_ID);
    }

    function destroyButton() {
      $('#' + BUTTON_ID).remove();
      $(window).off('scroll.goToTop');
    }

    // --------------------------------------------------
    // Theme detection
    // --------------------------------------------------
    function isWikiDarkMode() {
      if (
        document.documentElement.classList.contains('skin-theme-clientpref-night') ||
        document.body.classList.contains('skin-theme-clientpref-night')
      ) {
        return true;
      }

      if (
        (
          document.documentElement.classList.contains('skin-theme-clientpref-os') ||
          document.body.classList.contains('skin-theme-clientpref-os')
        ) &&
        window.matchMedia('(prefers-color-scheme: dark)').matches
      ) {
        return true;
      }

      return false;
    }

    // --------------------------------------------------
    // Create button
    // --------------------------------------------------
    function createButton() {
      if (buttonExists() || isEditing()) {
        return;
      }

      $('body').append(
        '<span id="' + BUTTON_ID + '"' +
          ' role="button"' +
          ' tabindex="0"' +
          ' aria-label="' + mw.message('gototop-label').text() + '">' +
          '<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">' +
            '<path d="M7 14l5-5 5 5" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>' +
          '</svg>' +
        '</span>'
      );

      var $topButton = $('#' + BUTTON_ID);

      // Base styling
      $topButton.css({
        position: 'fixed',
        bottom: '-80px',
        right: '24px',
        width: '56px',
        height: '56px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        borderRadius: '16px',
        cursor: 'pointer',
        overflow: 'hidden',
        userSelect: 'none',
        WebkitUserSelect: 'none',
        transition: 'bottom 0.5s, box-shadow 0.2s, transform 0.2s',
        zIndex: 999
      });

      // Theme colors
      function applyThemeColor() {
        var dark = isWikiDarkMode();

        $topButton.css({
          backgroundColor: dark ? '#2a2d31' : '#1e88e5',
          boxShadow: dark
            ? '0 4px 12px rgba(0,0,0,0.6)'
            : '0 4px 12px rgba(0,0,0,0.25)'
        });

        $topButton.find('path').attr(
          'stroke',
          dark ? '#e0e0e0' : 'white'
        );
      }

      applyThemeColor();

      // Hover effects
      $topButton.on('mouseenter', function () {
        $(this).css({
          boxShadow: '0 6px 16px rgba(0,0,0,0.35)',
          transform: 'translateY(-2px)'
        });
      });

      $topButton.on('mouseleave', function () {
        $(this).css({
          boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
          transform: 'none'
        });
      });

      // Ripple effect
      $topButton.on('mousedown', function (e) {
        var offset = $topButton.offset();
        var x = e.pageX - offset.left;
        var y = e.pageY - offset.top;
        var size = Math.max($topButton.outerWidth(), $topButton.outerHeight());

        var $ripple = $('<span></span>').css({
          position: 'absolute',
          width: size,
          height: size,
          left: x - size / 2,
          top: y - size / 2,
          background: 'rgba(255,255,255,0.35)',
          borderRadius: '50%',
          transform: 'scale(0)',
          opacity: 1,
          pointerEvents: 'none'
        });

        $topButton.append($ripple);

        $ripple.animate(
          { opacity: 0 },
          {
            duration: 450,
            step: function (now) {
              $(this).css('transform', 'scale(' + (2 * (1 - now)) + ')');
            },
            complete: function () {
              $(this).remove();
            }
          }
        );
      });

      // Click + keyboard activation
      function scrollToTop() {
        if ('scrollBehavior' in document.documentElement.style) {
          window.scrollTo({ top: 0, behavior: 'smooth' });
        } else {
          $('html, body').animate({ scrollTop: 0 }, 'slow');
        }
      }

      $topButton.on('click', scrollToTop);

      $topButton.on('keydown', function (e) {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          scrollToTop();
        }
      });

      // Show / hide on scroll
      $(window).on('scroll.goToTop', function () {
        if ($(this).scrollTop() > 300) {
          $topButton.css('bottom', '64px');
        } else {
          $topButton.css('bottom', '-80px');
        }
      });

      // Observe theme changes
      var themeObserver = new MutationObserver(applyThemeColor);

      themeObserver.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ['class']
      });

      themeObserver.observe(document.body, {
        attributes: true,
        attributeFilter: ['class']
      });
    }

    // --------------------------------------------------
    // SPA route handling
    // --------------------------------------------------
    function handleRouteChange() {
      if (isEditing()) {
        destroyButton();
      } else {
        createButton();
      }
    }

    // Initial run
    handleRouteChange();

    // Mobile editor transitions
    window.addEventListener('hashchange', handleRouteChange);
  });
});