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>
mw.loader.using('mediawiki.util', function () {
    mw.util.addCSS(
        `.scc-shortcut-button {
            cursor: pointer;
            ${window.scc_noMonospace ? "" : "font-family: monospace;"}
            ${window.scc_selectOnClick ? "user-select: all;" : ""}
        }

        .module-shortcutboxplain .plainlist ul li {
            font-weight: normal;
            white-space: nowrap;
        }

        .wp-rsp-sc {
            font-weight: normal !important;
            white-space: nowrap;
            padding-top: 5px !important;
            padding-bottom: 5px !important;
        }

        table.shortcutbox-compact th {
            font-weight: normal;
            white-space: nowrap;
        }`
    );

    function createShortcutButton(text, plain) {
        return $("<button>", {
            class: "scc-shortcut-button",
            text: text,
        }).attr("type", "button").attr(
            "aria-label",
            `Copy ${plain} shortcut to clipboard`,
        );
    }

    function createPageOrTemplateShortcutButton(
        plainShortcut,
        decoratedShortcut = plainShortcut,
        isSubstTemplate = false,
    ) {
        const isDecorated = decoratedShortcut !== plainShortcut;
        const isTemplateShortcut = isDecorated && decoratedShortcut === `{{${plainShortcut}}}`;

        if (isTemplateShortcut && isSubstTemplate) {
            decoratedShortcut = `{{subst:${plainShortcut}}}`;
        }

        // If there is decoration around the link, use that for the button. If not, use the markup
        // for a wikilink to the shortcut, or just the plain shortcut name if requested.
        const markup = isDecorated ? decoratedShortcut : `[[${plainShortcut}]]`;

        return createShortcutButton(markup, plainShortcut);
    }

    function addButtonHandler(/** @type {JQuery} */ $elem, handleStatus, reset) {
        let timeoutId;

        $elem.on("click", ".scc-shortcut-button", function () {
            clearTimeout(timeoutId);

            const buttonText = this.textContent;

            navigator.clipboard.writeText(buttonText).then(() => {
                handleStatus(true);
            }).catch(() => {
                handleStatus(false);
            }).finally(() => {
                timeoutId = setTimeout(() => {
                    reset();
                }, 1000);
            });
        });
    }

    function buttoniseLink(/** @type {JQuery} */ $link, /** @type {JQuery} */ $button) {
        $link
            .text("v")
            .before($button)
            .before(document.createTextNode("\u00A0["))
            .after(document.createTextNode("]"));
    }

    // Looks for "always substitute"/"never substitute" message boxes around `$box`, returning
    // `true` if the results of this search suggest that any template shortcuts inside the box
    // should be presented as substitutions, and `false` otherwise.
    function decideSubstitution(/** @type {JQuery} */ $box) {
        let foundAlwaysSubst = false;
        let foundNeverSubst = false;

        $box
            .siblings("table.box-Subst_only")
            .each(function () {
                const text = this.textContent.trim();

                if (text.startsWith("This template should always be substituted")) {
                    foundAlwaysSubst = true;
                } else if (text.startsWith("This template should not be substituted")) {
                    foundNeverSubst = true;
                } else {
                    console.warn(`SCC: Not sure what "${text}" means in`, this);
                }
            });

        // We assume (reasonably) that the shortcut box and the substitution boxes refer to the
        // same thing (i.e. any template shortcuts in the box link to the template whose
        // substitution or non-substitution is discussed in the message boxes).

        // Almost always, if the "always substitute" box is present, then the template should be
        // substituted. An exception is the documentation for [[:Template:Always substitute]]
        // itself, which displays a demo "always substitute" box, but also has the "never
        // substitute" box, because it's not actually supposed to be substituted. We generalise and
        // ignore any "always substitute" that appears alongside a "never substitute".
        return foundAlwaysSubst && !foundNeverSubst;
    }

    function processBigBox(/** @type {JQuery} */ $box) {
        const $listItems = $box.find("div.plainlist > ul > li");

        const $title = $box.find("div.module-shortcutlist > a");

        if ($title.length === 0) {
            // No title link. Could be a [[:Template:Shortcut-style further links]]. The user is
            // more likely to want to click the links in these boxes than copy them, so we leave
            // them alone.
            return;
        }

        const $titleListItem = $title.closest($listItems);

        if ($titleListItem.length !== 0) {
            // If the title is inside a list item, this is a [[:Template:Short URL box]].
            const $link = $titleListItem.find("> span.plainlinks > a");

            // Short URL boxes don't centre the title, but that looks weird with the added width of
            // the button.
            $titleListItem.css("text-align", "center");

            // There's not much point having a "view" link for a short URL, because it doesn't go
            // to an editable redirect page.
            $link.replaceWith(createShortcutButton($link.text(), $link.text()));
        } else {
            // todo: We could also check for the "Wikipedia substituted templates" category. That
            // does not work on template documentation pages, though. (E.g. [[:TM:Degree/doc]]).
            let isSubstitution = decideSubstitution($box);

            $listItems.each(function () {
                // The list items contain at least the shortcut link, and possibly some extra
                // (text) nodes around it (hereinafter called "decoration").
                const $listItem = $(this);

                const $relevantChildren = $listItem.contents().filter(function () {
                    // [[:Module:Shortcut]] often uses [[:Template:No redirect]], which includes a
                    // backlink that is hidden with the inline CSS "display: none". As it's
                    // invisible to the user, we have to filter it out before we can find out what
                    // the visible shortcut text is.
                    return this.nodeType !== Node.ELEMENT_NODE || this.style["display"] !== "none";
                });

                const decoratedShortcut = $relevantChildren.text();

                let $link = $listItem.find("> span.plainlinks > a, > a").first();

                const plainShortcut = $link.text();

                // Remove decoration.
                {
                    const $linkParent = $link.parent();

                    if ($linkParent[0] === $listItem[0]) {
                        // `$link` matched "> a"; the parent is the list item. Make the link the
                        // only child.
                        $listItem.empty().append($link);
                    } else {
                        // The parent is the `span.plainlinks`. Any decoration will be around that,
                        // so make it the only child of the list item.
                        $listItem.empty().append($linkParent);
                    }
                }

                buttoniseLink(
                    $link,
                    createPageOrTemplateShortcutButton(
                        plainShortcut,
                        decoratedShortcut,
                        isSubstitution,
                    ),
                );
            });
        }

        const originalTitle = $title.text();

        // We indicate the result by changing the box title (often "Shortcuts") because it's very
        // likely to be narrower than the shortcuts themselves after we've made our changes, so
        // changing (reducing, really) the title width probably won't change the width of the box
        // and cause text to reflow around it.
        addButtonHandler($box, (ok) => {
            $title.text(ok ? "Copied!" : "Failed!");
        }, () => {
            $title.text(originalTitle);
        });
    }

    function processBigBoxes(/** @type {JQuery} */ $content) {
        $content.find(".module-shortcutboxplain").each(function () {
            processBigBox($(this));
        });
    }

    function processMiniBoxes(/** @type {JQuery} */ $content) {
        $content.find("span.wp-rsp-sc").each(function () {
            const $miniBox = $(this);

            const $emojiNode = $miniBox
                .contents()
                .filter(function () { return this.nodeType === Node.TEXT_NODE; })
                .last();

            if ($emojiNode.length === 0) {
                console.warn(`SCC: No emoji node in mini box`, this);
                return;
            }

            // Should be "&nbsp;📌"
            const originalEmojiText = $emojiNode[0].nodeValue;

            const $link = $miniBox.find(".plainlinks a").first();

            buttoniseLink($link, createPageOrTemplateShortcutButton($link.text()));

            addButtonHandler($miniBox, (ok) => {
                $emojiNode[0].nodeValue = ok ? "\u00A0✅" : "\u00A0⚠️";
            }, () => {
                $emojiNode[0].nodeValue = originalEmojiText;
            });
        });
    }

    function processCompactBoxes(/** @type {JQuery} */ $content) {
        $content.find("table.shortcutbox-compact").each(function () {
            const $compactBox = $(this);

            // The box consists of two table headers: one for the "Shortcut:" text, and another for
            // the shortcut link(s).
            const $halves = $compactBox.find("th");

            const $titleHalf = $halves.first();
            const $title = $titleHalf.find("a");

            const $links = $halves.eq(1).find("a");

            const titleChanger = {
                // There is a text node containing a colon (":") after the "Shortcut" link, and one
                // containing a nbsp before the link. We don't care about the nbsp, but when we
                // change "Shortcut" to "Copied!" or "Failed!", we hide the colon.
                $colonNode: $title.parent()
                    .contents()
                    .filter(function () { return this.nodeType === Node.TEXT_NODE; })
                    .last(),

                // I lied. We replace the colon with another nbsp.
                $nbspNode: $(document.createTextNode("\u00A0")),

                originalText: $title.text(),

                // Keep track of whether the current title is one that we've put there so that we
                // don't try to edit our own stuff (beyond the title, which we still change).
                isTitleModified: false,

                changeForStatus(ok) {
                    // Just changing the label would change the width of the box, and could reflow
                    // text around it. In most fonts, "Copied!" and "Failed!" are both narrower
                    // than "Shortcut:", so we fix the width of the title half to whatever it is
                    // currently, change the text, and unfix the width on reset. Visually, the
                    // width never changes, and no text is reflowed. (This will fix the width even
                    // if the user does something else that would usually change the width, but
                    // that's fine; the width will still change after we reset.)
                    if (!this.isTitleModified) {
                        $titleHalf.width($titleHalf.width());
                        this.$colonNode.replaceWith(this.$nbspNode);

                        this.isTitleModified = true;
                    }

                    $title.text(ok ? "Copied!" : "Failed!");
                },

                reset() {
                    if (this.isTitleModified) {
                        $titleHalf.width("");
                        this.$nbspNode.replaceWith(this.$colonNode);

                        this.isTitleModified = false;
                    }

                    $title.text(this.originalText);
                }
            };

            $links.each(function () {
                const $link = $(this);
                buttoniseLink($link, createPageOrTemplateShortcutButton($link.text()));
            });

            addButtonHandler(
                $compactBox,
                (ok) => titleChanger.changeForStatus(ok),
                () => titleChanger.reset(),
            );
        });
    }

    mw.hook("wikipage.content").add(function (/** @type {JQuery} */ $content) {
        // [[:TM:Shortcut]]
        // [[:TM:Template shortcut]]
        // [[:TM:Template redirect]]
        // [[:TM:Short URL box]]
        processBigBoxes($content);

        // [[:TM:Shortcut mini]]
        processMiniBoxes($content);

        // [[:TM:Shortcut compact]]
        processCompactBoxes($content);
    });
});
//</nowiki>