CURRENT MISSION: Conclude the 2026 soyjak.party hack page, aswell as any pages relating to it, such as the Summer 2026 Crisis page.
Update Quotecord with any relevant info. See the blackboard for more info.

User:Tar/common.js: Difference between revisions

From Soyjak Wiki, the free ensoyclopedia
Jump to navigationJump to search
No edit summary
Tag: Reverted
No edit summary
Tag: Reverted
Line 1: Line 1:
(function () {
mw.loader.using('mediawiki.api', function () {
     'use strict';
     'use strict';
 
     var api = new mw.Api();
     function isOnFilePage() {
        const path = location.pathname;
        const params = new URLSearchParams(location.search);
        return (
            /^\/File:/.test(path) ||
            /^\/Special:Moderation/i.test(path) ||
          /^\/Special:Moderation/i.test(path) ||
            (
                params.get("title") === "Special:Moderation" &&
                params.get("modaction") === "show"
            )
        );
    }
 
    function extractImageData(img, callback) {
        const tmpImg = new Image();
        tmpImg.crossOrigin = 'anonymous';
        tmpImg.onload = function () {
            const canvas = document.createElement('canvas');
            canvas.width = tmpImg.width;
            canvas.height = tmpImg.height;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(tmpImg, 0, 0);
            callback(ctx, canvas);
        };
        tmpImg.src = img.dataset.originalSrc;
    }
 
    function revealLSB(imageData, bits) {
        const data = imageData.data;
        const maskShift = 8 - bits;
        for (let i = 0; i < data.length; i += 4) {
            data[i] = (data[i] << maskShift) & 0xff;
            data[i + 1] = (data[i + 1] << maskShift) & 0xff;
            data[i + 2] = (data[i + 2] << maskShift) & 0xff;
        }
    }
 
    function decodeLSB(imageData, maxChars = 5000) {
        const pixels = imageData.data;
        const bits = [];
        for (let i = 0; i < pixels.length; i += 4) {
            bits.push(pixels[i] & 1, pixels[i + 1] & 1, pixels[i + 2] & 1);
        }
        let output = '';
        for (let i = 0; i + 7 < bits.length && output.length < maxChars; i += 8) {
            let c = 0;
            for (let j = 0; j < 8; j++) c = (c << 1) | bits[i + j];
            if (c === 0) break;
            output += String.fromCharCode(c);
        }
        return output;
    }
 
    function addStegUIForImg(img) {
        if (!img || img.dataset.stegscanInit) return;
 
        img.dataset.stegscanInit = '1';
        img.dataset.originalSrc = img.dataset.originalSrc || img.src;
 
        const container = document.createElement('div');
        Object.assign(container.style, {
            margin: '6px 0 12px',
            display: 'block',
            maxWidth: img.width ? img.width + 'px' : '422px'
        });
 
        const label = document.createElement('div');
        label.textContent = 'Steganography Scan:';
        Object.assign(label.style, {
            fontSize: '12px',
            fontWeight: '600',
            marginBottom: '4px'
        });
 
        const slider = document.createElement('input');
        Object.assign(slider, { type: 'range', min: 1, max: 8, value: 1 });
        slider.style.width = '99%';
 
        const buttonWrapper = document.createElement('div');
        Object.assign(buttonWrapper.style, {
            display: 'flex',
            justifyContent: 'center',
            marginTop: '6px'
        });
 
        const decodeBtn = document.createElement('button');
        decodeBtn.textContent = 'Decode';
        Object.assign(decodeBtn.style, {
            cursor: 'pointer',
            padding: '4px 12px',
        });
 
        buttonWrapper.appendChild(decodeBtn);
 
        container.appendChild(label);
        container.appendChild(slider);
        container.appendChild(buttonWrapper);
 
        const parent = img.parentNode;
        (parent.tagName.toLowerCase() === 'a' ? parent : img)
            .insertAdjacentElement('afterend', container);
 
        let lastBits = 1;
 
        slider.addEventListener('input', () => {
            const bits = Number(slider.value);
            if (bits === lastBits) return;
            lastBits = bits;
            if (bits <= 1) {
                img.src = img.dataset.originalSrc;
                return;
            }
            extractImageData(img, (ctx, canvas) => {
                const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
                revealLSB(data, bits - 1);
                img.removeAttribute('srcset');
                ctx.putImageData(data, 0, 0);
                img.src = canvas.toDataURL();
            });
        });
 
        decodeBtn.addEventListener('click', () => {
            decodeBtn.disabled = true;
            decodeBtn.textContent = 'Decoding...';
            extractImageData(img, (ctx, canvas) => {
                const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
                const message = decodeLSB(data);
                decodeBtn.textContent = 'Decode';
                decodeBtn.disabled = false;
                if (message) {
                    navigator.clipboard.writeText(message)
                        .then(() => alert('Decoded message copied to clipboard'))
                        .catch(() => prompt('Decoded message:', message));
                } else {
                    alert('No message detected.');
                }
            });
        });
    }
 
    function scanAllImages() {
        if (!isOnFilePage()) return; // Only run on File pages or media viewer
        const content = document.querySelector('#content');
        if (!content) return;
        content.querySelectorAll('img').forEach(img => {
            // Skip file history thumbnails
            if (img.closest('.filehistory')) return;
            addStegUIForImg(img);
        });
    }
 
    scanAllImages();
 
    const observer = new MutationObserver(() => {
        if (isOnFilePage()) scanAllImages();
        else observer.disconnect();
    });
 
    observer.observe(document.body, { childList: true, subtree: true });
 
})();
 
 
(function () {
    'use strict';
 
    if (!['edit', 'submit'].includes(mw.config.get('wgAction'))) {
        return;
    }
 
     const config = {
     const config = {
         minimumCharacters: 2,
         minimumCharacters: 2,
         maximumResults: 5,
         maximumResults: 5,
         searchDelay: 200,
         searchDelay: 200,
 
         insertFormat: '@[[User:$1]] '
         usernameInsertFormat: '@[[User:$1]] ',
        emojiInsertFormat: '{{e|$1}} '
     };
     };
 
     let codeMirror = null;
    const emojis = [
     let activeMention = null;
        'abdul', 'ack', 'alien', 'angel', 'brainlet', 'caca', 'calm',
        'cat', 'chang', 'cheeky', 'cheers', 'clittycel', 'clown',
        'coal', 'cold', 'concerned', 'cool', 'coomer', 'coping',
        'disgusted', 'diva', 'dog', 'dreamy', 'esl', 'euro', 'fact',
        'false', 'feelsgoodman', 'flushed', 'fruity', 'fuming', 'gem',
        'giga', 'glowie', 'heartbroken', 'hearts', 'holdit', 'horny',
        'hot', 'hunk', 'imp', 'informative', 'irritated', 'itsover',
        'jamming', 'jarty', 'jsid', 'kaching', 'kek', 'keyed', 'kumar',
        'locked', 'looking', 'marge', 'meds', 'mexi', 'mutt',
        'nailbiter', 'neutral', 'nophono', 'omgsisa', 'pleading',
        'poon', 'projecting', 'rage', 'reddit', 'robot', 'sad',
        'schizo', 'serious', 'shush', 'dough', 'sick', 'skull',
        'sleeping', 'slf', 'smart', 'smoking', 'smug', 'smugjak',
        'snca', 'sob', 'soyboy', 'soytan', 'spade', 'steiner',
        'surprised', 'suspicious', 'tendie', 'thinking', 'though',
        'thumbsdown', 'thumbsup', 'tired', 'tism', 'true', 'tsmt',
        'turk', 'tyrone', 'unc', 'uncomfortable', 'wholesome',
        'xitter', 'yum', 'zany', 'dodis', 'arrow', 'ijgs', 'genius'
    ];
 
     let codeMirror;
     let activeCompletion;
     let results = [];
     let results = [];
     let selectedIndex = 0;
     let selectedIndex = 0;
     let debounceTimer;
     let debounceTimer = null;
     let apiPromise = null;
     let requestNumber = 0;
 
     const menu = document.createElement('div');
     const menu = document.createElement('div');
     menu.className = 'username-autocomplete';
     menu.className = 'username-autocomplete';
     menu.hidden = true;
     menu.hidden = true;
     document.body.appendChild(menu);
     document.body.appendChild(menu);
 
     const style = document.createElement('style');
     function getApi() {
    style.textContent = `
         if (!apiPromise) {
         .username-autocomplete {
             apiPromise = mw.loader
             position: absolute;
                .using('mediawiki.api')
            z-index: 999999;
                .then(function () {
            min-width: 200px;
                    return new mw.Api();
            max-width: 350px;
                });
            overflow-y: auto;
            background: var(--background-color-base, #ffffff);
            color: var(--color-base, #202122);
            border: 1px solid var(--border-color-base, #a2a9b1);
            border-radius: 3px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.25);
            font-family: sans-serif;
            font-size: 14px;
         }
         }
 
        .username-autocomplete-result {
         return apiPromise;
            padding: 7px 10px;
     }
            cursor: pointer;
 
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .username-autocomplete-result:hover,
        .username-autocomplete-result.selected {
            background: var(
                --background-color-interactive-subtle,
                #eaecf0
            );
        }
         .username-autocomplete-empty {
            padding: 7px 10px;
            color: var(--color-subtle, #72777d);
            font-style: italic;
        }
     `;
    document.head.appendChild(style);
     function getCodeMirrorFromEvent(event) {
     function getCodeMirrorFromEvent(event) {
         const wrapper = event.target instanceof Element && event.target.closest('.CodeMirror');
         if (!(event.target instanceof Element)) {
 
            return null;
         return wrapper?.CodeMirror || null;
        }
        const wrapper = event.target.closest('.CodeMirror');
        if (!wrapper || !wrapper.CodeMirror) {
            return null;
        }
         return wrapper.CodeMirror;
     }
     }
 
     function getMention(cm) {
     function getCompletion(cm) {
         const cursor = cm.getCursor();
         const cursor = cm.getCursor();
         const before = cm.getLine(cursor.line).slice(0, cursor.ch);
         const lineText = cm.getLine(cursor.line);
 
        const textBeforeCursor = lineText.slice(0, cursor.ch);
         let match = before.match(
         const match = textBeforeCursor.match(
             /(?:^|[\s([{"'])@([^@\n\r\t<>{}\[\]|#/:;,!?]*)$/
             /(?:^|[\s([{"'])@([^@\n\r\t<>{}\[\]|#/:;,!?]*)$/
         );
         );
        let type = 'username';
        if (!match) {
            match = before.match(
                /(?:^|[\s([{"']):([a-zA-Z0-9_-]*)$/
            );
            type = 'emoji';
        }
         if (!match) {
         if (!match) {
             return null;
             return null;
         }
         }
         const query = match[1];
         const query = match[1];
         return {
         return {
            type,
             query,
             query,
             start: {
             start: {
                 line: cursor.line,
                 line: cursor.line,
                 ch: cursor.ch - query.length - 1
                 ch: cursor.ch - query.length - 1
             },
             },
 
             end: {
             end: cursor
                line: cursor.line,
                ch: cursor.ch
            }
         };
         };
     }
     }
 
     function mentionsMatch(first, second) {
     function completionsMatch(a, b) {
         return (
         return (
             a &&
             first &&
             b &&
             second &&
             a.type === b.type &&
             first.query === second.query &&
            a.query === b.query &&
             first.start.line === second.start.line &&
             a.start.line === b.start.line &&
             first.start.ch === second.start.ch &&
             a.start.ch === b.start.ch &&
             first.end.line === second.end.line &&
             a.end.line === b.end.line &&
             first.end.ch === second.end.ch
             a.end.ch === b.end.ch
         );
         );
     }
     }
     function hideMenu() {
     function hideMenu() {
         menu.hidden = true;
         menu.hidden = true;
         menu.replaceChildren();
         menu.replaceChildren();
 
         activeMention = null;
         activeCompletion = null;
         results = [];
         results = [];
         selectedIndex = 0;
         selectedIndex = 0;
     }
     }
     function positionMenu() {
     function positionMenu() {
         if (!codeMirror) {
         if (!codeMirror) {
             return;
             return;
         }
         }
 
         const coordinates = codeMirror.cursorCoords(null, 'page');
         const pos = codeMirror.cursorCoords(null, 'page');
         menu.style.left = `${coordinates.left}px`;
 
         menu.style.top = `${coordinates.bottom}px`;
         menu.style.left = pos.left + 'px';
         menu.style.top = pos.bottom + 'px';
     }
     }
     function setSelectedIndex(index) {
     function setSelectedIndex(index) {
         const items = menu.querySelectorAll(
         const items = menu.querySelectorAll(
             '.username-autocomplete-result'
             '.username-autocomplete-result'
         );
         );
         if (!items.length) {
         if (!items.length) {
             return;
             return;
         }
         }
 
         selectedIndex =
         selectedIndex = (index + items.length) % items.length;
            ((index % items.length) + items.length) %
 
            items.length;
         items.forEach((item, i) => {
         items.forEach(function (item, itemIndex) {
             item.classList.toggle(
             item.classList.toggle(
                 'selected',
                 'selected',
                 i === selectedIndex
                 itemIndex === selectedIndex
             );
             );
         });
         });
if (items.item(selectedIndex)) {
    items.item(selectedIndex).scrollIntoView({
        block: 'nearest'
    });
}
     }
     }
 
     function insertUsername(username) {
     function insertResult(value) {
         if (!codeMirror || !activeMention) {
         if (!codeMirror || !activeCompletion) {
             return;
             return;
         }
         }
 
         const insertion = config.insertFormat.replaceAll(
         const format =
            '$1',
            activeCompletion.type === 'emoji'
            username
                ? config.emojiInsertFormat
        );
                : config.usernameInsertFormat;
 
        const insertion = format.replaceAll('$1', value);
 
         codeMirror.replaceRange(
         codeMirror.replaceRange(
             insertion,
             insertion,
             activeCompletion.start,
             activeMention.start,
             activeCompletion.end,
             activeMention.end,
             '+autocomplete'
             '+username-autocomplete'
         );
         );
         codeMirror.setCursor({
         codeMirror.setCursor({
             line: activeCompletion.start.line,
             line: activeMention.start.line,
             ch: activeCompletion.start.ch + insertion.length
             ch: activeMention.start.ch + insertion.length
         });
         });
         codeMirror.focus();
         codeMirror.focus();
         hideMenu();
         hideMenu();
     }
     }
 
     function renderResults(usernames) {
     function renderResults(values) {
         menu.replaceChildren();
         menu.replaceChildren();
 
         results = usernames;
         results = values;
         selectedIndex = 0;
         selectedIndex = 0;
 
         if (!usernames.length) {
         if (!values.length) {
             const empty = document.createElement('div');
             const empty = document.createElement('div');
             empty.className = 'username-autocomplete-empty';
             empty.className = 'username-autocomplete-empty';
             empty.textContent =
             empty.textContent = 'No matching users';
                activeCompletion.type === 'emoji'
                    ? 'No matching emojis'
                    : 'No matching users';
 
             menu.appendChild(empty);
             menu.appendChild(empty);
         } else {
            menu.hidden = false;
            values.forEach((value, index) => {
            positionMenu();
                const item = document.createElement('div');
            return;
 
         }
                item.className = 'username-autocomplete-result';
        usernames.forEach(function (username, index) {
 
            const item = document.createElement('div');
                if (index === 0) {
            item.className = 'username-autocomplete-result';
                    item.classList.add('selected');
            item.textContent = username;
                }
            if (index === 0) {
 
                item.classList.add('selected');
                if (activeCompletion.type === 'emoji') {
            }
                    const img = document.createElement('img');
            item.addEventListener('mouseenter', function () {
 
                 setSelectedIndex(index);
                    img.src =
            });
                        mw.config.get('wgScriptPath') +
            item.addEventListener('mousedown', function (event) {
                        '/index.php?title=Special:Redirect/file/' +
                event.preventDefault();
                        encodeURIComponent( 'Nuuru ' + value.toLowerCase() + '.png')
                 insertUsername(username);
 
                    img.alt = value;
 
                    item.append(img, document.createTextNode(' ' + value));
                } else {
                    item.textContent = value;
                 }
 
                item.addEventListener(
                    'mouseenter',
                    () => setSelectedIndex(index)
                );
 
                item.addEventListener(
                    'mousedown',
                    event => {
                        event.preventDefault();
                        insertResult(value);
                    }
                 );
 
                menu.appendChild(item);
             });
             });
         }
            menu.appendChild(item);
 
         });
         menu.hidden = false;
         menu.hidden = false;
         positionMenu();
         positionMenu();
     }
     }
 
     function searchUsers(cm, mention) {
     function searchUsers(cm, completion) {
         const currentRequest = ++requestNumber;
         getApi()
        api.get({
            .then(function (api) {
            action: 'query',
                return api.get({
            list: 'allusers',
                    action: 'query',
            auprefix: mention.query,
                    list: 'allusers',
            aulimit: config.maximumResults,
                    auprefix: completion.query,
            formatversion: 2
                    aulimit: config.maximumResults,
        }).then(function (data) {
                    formatversion: 2
            if (currentRequest !== requestNumber) {
                });
                 return;
            })
            }
            .then(function (data) {
            const currentMention = getMention(cm);
                 const current = getCompletion(cm);
            if (!mentionsMatch(currentMention, mention)) {
 
                return;
                if (!completionsMatch(current, completion)) {
            }
                    return;
            codeMirror = cm;
                }
            activeMention = currentMention;
 
            const usernames = (
                codeMirror = cm;
                 data.query.allusers || []
                activeCompletion = current;
             ).map(function (user) {
 
                 return user.name;
                 renderResults(
                    (data.query.allusers || []).map(
                        user => user.name
                    )
                );
             })
            .catch(function (error) {
                 console.error(
                    'Username autocomplete API error:',
                    error
                );
 
                hideMenu();
             });
             });
            renderResults(usernames);
        }).catch(function (error) {
            console.error(
                'Username autocomplete API error:',
                error
            );
            hideMenu();
        });
     }
     }
    function searchEmojis(cm, completion) {
        const current = getCompletion(cm);
        if (!completionsMatch(current, completion)) {
            return;
        }
        codeMirror = cm;
        activeCompletion = current;
        const query = completion.query.toLowerCase();
        renderResults(
            emojis
                .filter(emoji =>
                    emoji.startsWith(query)
                )
                .slice(0, config.maximumResults)
        );
    }
     function processEditor(cm) {
     function processEditor(cm) {
         const completion = getCompletion(cm);
         const mention = getMention(cm);
 
         if (
         if (
             !completion ||
             !mention ||
             completion.query.length <
             mention.query.length < config.minimumCharacters
                config.minimumCharacters
         ) {
         ) {
            clearTimeout(debounceTimer);
             hideMenu();
             hideMenu();
             return;
             return;
         }
         }
         codeMirror = cm;
         codeMirror = cm;
         activeCompletion = completion;
         activeMention = mention;
 
         clearTimeout(debounceTimer);
         clearTimeout(debounceTimer);
 
         debounceTimer = setTimeout(function () {
         if (completion.type === 'emoji') {
             searchUsers(cm, mention);
             searchEmojis(cm, completion);
         }, config.searchDelay);
            return;
         }
 
        debounceTimer = setTimeout(
            () => searchUsers(cm, completion),
            config.searchDelay
        );
     }
     }
     document.addEventListener(
     document.addEventListener(
         'keyup',
         'keyup',
         event => {
         function (event) {
             const cm = getCodeMirrorFromEvent(event);
             const cm = getCodeMirrorFromEvent(event);
 
            if (!cm) {
                return;
            }
             if (
             if (
                !cm ||
                 [
                 [
                     'ArrowUp',
                     'ArrowUp',
Line 516: Line 255:
                 return;
                 return;
             }
             }
             processEditor(cm);
             processEditor(cm);
         },
         },
         true
         true
     );
     );
     document.addEventListener(
     document.addEventListener(
         'keydown',
         'keydown',
         event => {
         function (event) {
             if (
             if (menu.hidden || !codeMirror) {
                menu.hidden ||
                 return;
                 getCodeMirrorFromEvent(event) !== codeMirror
            }
            ) {
            const cm = getCodeMirrorFromEvent(event);
            if (cm !== codeMirror) {
                 return;
                 return;
             }
             }
             switch (event.key) {
             switch (event.key) {
                 case 'ArrowDown':
                 case 'ArrowDown':
Line 538: Line 275:
                     setSelectedIndex(selectedIndex + 1);
                     setSelectedIndex(selectedIndex + 1);
                     break;
                     break;
                 case 'ArrowUp':
                 case 'ArrowUp':
                     event.preventDefault();
                     event.preventDefault();
Line 544: Line 280:
                     setSelectedIndex(selectedIndex - 1);
                     setSelectedIndex(selectedIndex - 1);
                     break;
                     break;
                 case 'Enter':
                 case 'Enter':
                 case 'Tab':
                 case 'Tab':
Line 550: Line 285:
                         event.preventDefault();
                         event.preventDefault();
                         event.stopPropagation();
                         event.stopPropagation();
 
                         insertUsername(results[selectedIndex]);
                         insertResult(
                            results[selectedIndex]
                        );
                     }
                     }
                     break;
                     break;
                 case 'Escape':
                 case 'Escape':
                     event.preventDefault();
                     event.preventDefault();
Line 566: Line 297:
         true
         true
     );
     );
 
     document.addEventListener('mousedown', function (event) {
     document.addEventListener('mousedown', event => {
         if (
         if (
             !menu.contains(event.target) &&
             !menu.contains(event.target) &&
Line 578: Line 308:
         }
         }
     });
     });
 
    document.addEventListener(
     window.addEventListener('resize', () => {
        'scroll',
        function () {
            if (!menu.hidden) {
                positionMenu();
            }
        },
        true
    );
     window.addEventListener('resize', function () {
         if (!menu.hidden) {
         if (!menu.hidden) {
             positionMenu();
             positionMenu();
         }
         }
     });
     });
}());
});

Revision as of 00:19, 11 August 2026

mw.loader.using('mediawiki.api', function () {
    'use strict';
    var api = new mw.Api();
    const config = {
        minimumCharacters: 2,
        maximumResults: 5,
        searchDelay: 200,
        insertFormat: '@[[User:$1]] '
    };
    let codeMirror = null;
    let activeMention = null;
    let results = [];
    let selectedIndex = 0;
    let debounceTimer = null;
    let requestNumber = 0;
    const menu = document.createElement('div');
    menu.className = 'username-autocomplete';
    menu.hidden = true;
    document.body.appendChild(menu);
    const style = document.createElement('style');
    style.textContent = `
        .username-autocomplete {
            position: absolute;
            z-index: 999999;
            min-width: 200px;
            max-width: 350px;
            overflow-y: auto;
            background: var(--background-color-base, #ffffff);
            color: var(--color-base, #202122);
            border: 1px solid var(--border-color-base, #a2a9b1);
            border-radius: 3px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.25);
            font-family: sans-serif;
            font-size: 14px;
        }
        .username-autocomplete-result {
            padding: 7px 10px;
            cursor: pointer;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .username-autocomplete-result:hover,
        .username-autocomplete-result.selected {
            background: var(
                --background-color-interactive-subtle,
                #eaecf0
            );
        }
        .username-autocomplete-empty {
            padding: 7px 10px;
            color: var(--color-subtle, #72777d);
            font-style: italic;
        }
    `;
    document.head.appendChild(style);
    function getCodeMirrorFromEvent(event) {
        if (!(event.target instanceof Element)) {
            return null;
        }
        const wrapper = event.target.closest('.CodeMirror');
        if (!wrapper || !wrapper.CodeMirror) {
            return null;
        }
        return wrapper.CodeMirror;
    }
    function getMention(cm) {
        const cursor = cm.getCursor();
        const lineText = cm.getLine(cursor.line);
        const textBeforeCursor = lineText.slice(0, cursor.ch);
        const match = textBeforeCursor.match(
            /(?:^|[\s([{"'])@([^@\n\r\t<>{}\[\]|#/:;,!?]*)$/
        );
        if (!match) {
            return null;
        }
        const query = match[1];
        return {
            query,
            start: {
                line: cursor.line,
                ch: cursor.ch - query.length - 1
            },
            end: {
                line: cursor.line,
                ch: cursor.ch
            }
        };
    }
    function mentionsMatch(first, second) {
        return (
            first &&
            second &&
            first.query === second.query &&
            first.start.line === second.start.line &&
            first.start.ch === second.start.ch &&
            first.end.line === second.end.line &&
            first.end.ch === second.end.ch
        );
    }
    function hideMenu() {
        menu.hidden = true;
        menu.replaceChildren();
        activeMention = null;
        results = [];
        selectedIndex = 0;
    }
    function positionMenu() {
        if (!codeMirror) {
            return;
        }
        const coordinates = codeMirror.cursorCoords(null, 'page');
        menu.style.left = `${coordinates.left}px`;
        menu.style.top = `${coordinates.bottom}px`;
    }
    function setSelectedIndex(index) {
        const items = menu.querySelectorAll(
            '.username-autocomplete-result'
        );
        if (!items.length) {
            return;
        }
        selectedIndex =
            ((index % items.length) + items.length) %
            items.length;
        items.forEach(function (item, itemIndex) {
            item.classList.toggle(
                'selected',
                itemIndex === selectedIndex
            );
        });
		if (items.item(selectedIndex)) {
		    items.item(selectedIndex).scrollIntoView({
		        block: 'nearest'
		    });
		}
    }
    function insertUsername(username) {
        if (!codeMirror || !activeMention) {
            return;
        }
        const insertion = config.insertFormat.replaceAll(
            '$1',
            username
        );
        codeMirror.replaceRange(
            insertion,
            activeMention.start,
            activeMention.end,
            '+username-autocomplete'
        );
        codeMirror.setCursor({
            line: activeMention.start.line,
            ch: activeMention.start.ch + insertion.length
        });
        codeMirror.focus();
        hideMenu();
    }
    function renderResults(usernames) {
        menu.replaceChildren();
        results = usernames;
        selectedIndex = 0;
        if (!usernames.length) {
            const empty = document.createElement('div');
            empty.className = 'username-autocomplete-empty';
            empty.textContent = 'No matching users';
            menu.appendChild(empty);
            menu.hidden = false;
            positionMenu();
            return;
        }
        usernames.forEach(function (username, index) {
            const item = document.createElement('div');
            item.className = 'username-autocomplete-result';
            item.textContent = username;
            if (index === 0) {
                item.classList.add('selected');
            }
            item.addEventListener('mouseenter', function () {
                setSelectedIndex(index);
            });
            item.addEventListener('mousedown', function (event) {
                event.preventDefault();
                insertUsername(username);
            });
            menu.appendChild(item);
        });
        menu.hidden = false;
        positionMenu();
    }
    function searchUsers(cm, mention) {
        const currentRequest = ++requestNumber;
        api.get({
            action: 'query',
            list: 'allusers',
            auprefix: mention.query,
            aulimit: config.maximumResults,
            formatversion: 2
        }).then(function (data) {
            if (currentRequest !== requestNumber) {
                return;
            }
            const currentMention = getMention(cm);
            if (!mentionsMatch(currentMention, mention)) {
                return;
            }
            codeMirror = cm;
            activeMention = currentMention;
            const usernames = (
                data.query.allusers || []
            ).map(function (user) {
                return user.name;
            });
            renderResults(usernames);
        }).catch(function (error) {
            console.error(
                'Username autocomplete API error:',
                error
            );
            hideMenu();
        });
    }
    function processEditor(cm) {
        const mention = getMention(cm);
        if (
            !mention ||
            mention.query.length < config.minimumCharacters
        ) {
            hideMenu();
            return;
        }
        codeMirror = cm;
        activeMention = mention;
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(function () {
            searchUsers(cm, mention);
        }, config.searchDelay);
    }
    document.addEventListener(
        'keyup',
        function (event) {
            const cm = getCodeMirrorFromEvent(event);
            if (!cm) {
                return;
            }
            if (
                [
                    'ArrowUp',
                    'ArrowDown',
                    'Enter',
                    'Tab',
                    'Escape'
                ].includes(event.key)
            ) {
                return;
            }
            processEditor(cm);
        },
        true
    );
    document.addEventListener(
        'keydown',
        function (event) {
            if (menu.hidden || !codeMirror) {
                return;
            }
            const cm = getCodeMirrorFromEvent(event);
            if (cm !== codeMirror) {
                return;
            }
            switch (event.key) {
                case 'ArrowDown':
                    event.preventDefault();
                    event.stopPropagation();
                    setSelectedIndex(selectedIndex + 1);
                    break;
                case 'ArrowUp':
                    event.preventDefault();
                    event.stopPropagation();
                    setSelectedIndex(selectedIndex - 1);
                    break;
                case 'Enter':
                case 'Tab':
                    if (results.length) {
                        event.preventDefault();
                        event.stopPropagation();
                        insertUsername(results[selectedIndex]);
                    }
                    break;
                case 'Escape':
                    event.preventDefault();
                    event.stopPropagation();
                    hideMenu();
                    break;
            }
        },
        true
    );
    document.addEventListener('mousedown', function (event) {
        if (
            !menu.contains(event.target) &&
            !(
                event.target instanceof Element &&
                event.target.closest('.CodeMirror')
            )
        ) {
            hideMenu();
        }
    });
    document.addEventListener(
        'scroll',
        function () {
            if (!menu.hidden) {
                positionMenu();
            }
        },
        true
    );
    window.addEventListener('resize', function () {
        if (!menu.hidden) {
            positionMenu();
        }
    });
});