Jump to content

User:Polygnotus/Scripts/Buttons.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.
// <nowiki>
// Wikipedia Comment Navigator
// If the page was opened via a notification, navigates between new (highlighted) comments.
// Otherwise, navigates between comments made by the logged-in user.
// Buttons appear at the bottom-left only after scrolling down past the page header.

(function () {
    'use strict';

    // Only run on talk pages (odd namespace numbers)
    if (mw.config.get('wgNamespaceNumber') % 2 !== 1) {
        return;
    }

    mw.loader.using(['mediawiki.util'], function () {

        var username = mw.config.get('wgUserName');
        var comments  = [];
        var mode;

        // --- Mode detection ---------------------------------------------------

        // DiscussionTools highlights new comments with this class when arriving
        // via a notification link.
        var highlighted = Array.from(
            document.querySelectorAll('.ext-discussiontools-init-highlight')
        );

        if (highlighted.length > 0) {
            mode     = 'new';
            comments = highlighted;
        } else if (username) {
            mode = 'own';

            // Find comment anchors for comments authored by the current user.
            // DiscussionTools marks each comment with data-mw-comment-sig whose
            // value starts with "c-<username>-".
            var seen          = new Set();
            var usernameSafe  = username.replace(/ /g, '_');

            document.querySelectorAll('[data-mw-comment-sig]').forEach(function (sig) {
                var id = sig.getAttribute('data-mw-comment-sig');
                if (seen.has(id)) { return; }

                var match = id.match(/^c-([^-]+)-/);
                if (!match || match[1] !== usernameSafe) { return; }

                // The start anchor for a comment has data-mw-comment-start and id == commentId
                // CSS.escape handles any special characters in the id.
                var escapedId = typeof CSS !== 'undefined' && CSS.escape
                    ? CSS.escape(id)
                    : id.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');

                var anchor = document.querySelector(
                    '[data-mw-comment-start][id="' + escapedId + '"]'
                );
                if (!anchor) { return; }

                comments.push(anchor);
                seen.add(id);
            });

            // Sort by document order
            comments.sort(function (a, b) {
                return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1;
            });
        }

        if (comments.length === 0) { return; }

        // --- State ------------------------------------------------------------

        var currentIndex = 0;

        // --- Scroll helper ----------------------------------------------------

        // Scrolls so the target element sits near the top of the viewport.
        function scrollToComment(el) {
            var TOP_OFFSET = 10; // px gap from the top of the viewport
            var targetY = el.getBoundingClientRect().top + window.pageYOffset - TOP_OFFSET;
            window.scrollTo({ top: targetY, behavior: 'smooth' });
        }

        // --- Build the navigation widget --------------------------------------

        var nav = document.createElement('div');
        nav.id  = 'comment-nav-widget';
        Object.assign(nav.style, {
            position    : 'fixed',
            bottom      : '20px',
            left        : '20px',
            zIndex      : '9999',
            display     : 'none',          // hidden until scroll threshold is passed
            alignItems  : 'center',
            gap         : '6px',
            background  : '#f8f9fa',
            border      : '1px solid #a2a9b1',
            borderRadius: '4px',
            padding     : '5px 10px',
            boxShadow   : '0 2px 8px rgba(0,0,0,0.25)',
            fontSize    : '13px',
            userSelect  : 'none',
        });

        // Label shows mode
        var label = document.createElement('span');
        label.style.color = '#54595d';
        label.textContent = mode === 'new' ? 'New' : 'Mine';

        function makeButton(symbol, title) {
            var btn = document.createElement('button');
            btn.type      = 'button';
            btn.textContent = symbol;
            btn.title     = title;
            Object.assign(btn.style, {
                background  : '#0645ad',
                color       : '#fff',
                border      : 'none',
                borderRadius: '3px',
                padding     : '3px 8px',
                cursor      : 'pointer',
                fontSize    : '15px',
                lineHeight  : '1.2',
            });
            btn.addEventListener('mouseenter', function () {
                if (!btn.disabled) { btn.style.background = '#0b0080'; }
            });
            btn.addEventListener('mouseleave', function () {
                if (!btn.disabled) { btn.style.background = '#0645ad'; }
            });
            return btn;
        }

        var prevBtn  = makeButton('↑', 'Previous comment');
        var counter  = document.createElement('span');
        var nextBtn  = makeButton('↓', 'Next comment');

        counter.style.cssText = 'font-weight:bold;min-width:48px;text-align:center;color:#222;';

        nav.appendChild(label);
        nav.appendChild(prevBtn);
        nav.appendChild(counter);
        nav.appendChild(nextBtn);
        document.body.appendChild(nav);

        // --- Update UI state --------------------------------------------------

        function updateUI() {
            counter.textContent    = (currentIndex + 1) + '\u202f/\u202f' + comments.length;
            prevBtn.disabled       = (currentIndex === 0);
            nextBtn.disabled       = (currentIndex === comments.length - 1);
            prevBtn.style.opacity  = prevBtn.disabled ? '0.4' : '1';
            nextBtn.style.opacity  = nextBtn.disabled ? '0.4' : '1';
            prevBtn.style.cursor   = prevBtn.disabled ? 'default' : 'pointer';
            nextBtn.style.cursor   = nextBtn.disabled ? 'default' : 'pointer';
        }

        updateUI();

        // --- Button events ----------------------------------------------------

        prevBtn.addEventListener('click', function (e) {
            e.preventDefault();
            if (currentIndex > 0) {
                currentIndex--;
                scrollToComment(comments[currentIndex]);
                updateUI();
            }
        });

        nextBtn.addEventListener('click', function (e) {
            e.preventDefault();
            if (currentIndex < comments.length - 1) {
                currentIndex++;
                scrollToComment(comments[currentIndex]);
                updateUI();
            }
        });

        // --- Keyboard shortcuts -----------------------------------------------

        document.addEventListener('keydown', function (e) {
            if (!nav.offsetParent) { return; }                  // widget not visible
            if (/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) { return; }

            if (e.key === 'ArrowUp'   || e.key === 'ArrowLeft')  { e.preventDefault(); prevBtn.click(); }
            if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { e.preventDefault(); nextBtn.click(); }
        });

        // --- Show widget only after scrolling past the header -----------------
        // The threshold keeps the widget from appearing on top of the left-hand
        // sidebar / toolbox menus that are visible when the page is not scrolled.

        var SHOW_THRESHOLD = 250; // px — adjust if needed for your skin

        function onScroll() {
            nav.style.display = window.pageYOffset > SHOW_THRESHOLD ? 'flex' : 'none';
        }

        window.addEventListener('scroll', onScroll, { passive: true });
        onScroll(); // run once on load in case the page opens mid-scroll

    }); // end mw.loader.using

}());
// </nowiki>