Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
This user script seems to have a documentation page at User:Pink Bee/ShortcutCopy.
//<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 " 📌"
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>