MediaWiki:Gadget-tidy.js
Hoppa till navigering
Hoppa till sök
OBS: Efter du har publicerat sidan kan du behöva tömma din webbläsares cache för att se ändringarna.
- Firefox / Safari: Håll ned Skift och klicka på Uppdatera sidan eller tryck Ctrl-F5 eller Ctrl-R (⌘-R på Mac)
- Google Chrome: Tryck Ctrl-Skift-R (⌘-Skift-R på Mac)
- Internet Explorer / Edge: Håll ned Ctrl och klicka på Uppdatera eller tryck Ctrl-F5
- Opera: Tryck Ctrl-F5.
// @ts-check
var processOnFocus = require("./tidy-on-focus.js");
var processOnKeydown = require("./tidy-on-keydown.js");
var processOnSave = require("./tidy-on-save.js");
var processSummary = require("./tidy-on-save-summary.js");
var getData = require("./tidy-data.js");
// Export items needed for testing.
/** @type {TidyExports} */
var exports = {
processOnSave: processOnSave,
processOnKeydown: processOnKeydown,
getData: getData,
};
/** @type {*} */ (window).tidy = exports;
// Declare types of global dependencies inline, to make this gadget self-contained.
/**
* @typedef {{
* processOnSave: typeof processOnSave,
* processOnKeydown: typeof processOnKeydown,
* getData: typeof getData,
* }} TidyExports
*
* @typedef {{
* on(event: string, cb: (this: HTMLElement, e: Event) => void): JQuery;
* trigger(event: string): JQuery;
* find(selector: string): JQuery;
* each(callback: (index: string, value: HTMLElement) => void): JQuery;
* parent(): JQuery;
* parents(selector: string): JQuery;
* text(): string;
* text(str: string): JQuery;
* val(): string;
* val(x: string): JQuery;
* css(props: Record<string, string | number>): JQuery;
* prop(props: Record<string, string | number | boolean>): JQuery;
* prop(prop: string): string | number | boolean;
* addClass(x: string): JQuery;
* prepend(...content: JQueryish[]): JQuery;
* append(...content: JQueryish[]): JQuery;
* remove(): JQuery;
* replaceWith(...content: JQueryish[]): JQuery;
* toggle(): JQuery;
* slideDown(): JQuery;
* slideUp(): JQuery;
* 0: HTMLElement & { CodeMirror?: CodeMirror };
* [index: number]: HTMLElement;
* length: number;
* textSelection(cmd: "getContents"): string;
* textSelection(cmd: "setContents", content: string): JQuery;
* textSelection(cmd: "getCaretPosition", opts?: { startAndEnd?: false }): number;
* textSelection(cmd: "getCaretPosition", opts?: { startAndEnd: true }): [number, number];
* textSelection(cmd: "encapsulateSelection", opts: { pre?: string; post?: string }): JQuery;
* textSelection(cmd: "setSelection", opts: { start: number; end?: number }): JQuery;
* }} JQuery
*
* @typedef {{
* (selectorOrHtml: string, props?: Record<string, string | number>): JQuery;
* (node: JQueryish): JQuery;
* parseHTML(html: string): Node[];
* }} JQueryStatic
*
* @typedef {string | JQuery | Node | (string | JQuery | Node)[]} JQueryish
*
* @typedef {{
* hook(name: "wikipage.content"): MwHook<[elem: JQuery]>;
* hook(name: "wikipage.editform"): MwHook<[elem: JQuery]>;
* hook(name: "ext.CodeMirror.switch"): MwHook<[isCodeMirror: boolean, elem: JQuery]>;
* loader: {
* using(dependencies: string[]): Promise<void>;
* implement(moduleName: string, fn: () => void): Promise<void>;
* };
* config: {
* get<T extends keyof MwConfig>(name: T): MwConfig[T];
* set<T extends keyof MwConfig>(name: T, value: MwConfig[T]): void;
* };
* user: {
* options: {
* get(): Record<string, any>;
* };
* };
* }} MediaWiki
*
* @typedef {{
* wgNamespaceNumber: number;
* wgPageName: string;
* wgUserGroups: ("*" | "bot")[];
* }} MwConfig
*
* @typedef {{
* display: {
* scroller: HTMLElement;
* };
* on<T extends keyof CmEventMap>(type: T, cb: CmEventMap[T]): void;
* }} CodeMirror Selected parts of the CodeMirror 5 API.
*
* @typedef {{
* change: (instance: CodeMirror, change: CmChange) => void;
* focus: (instance: CodeMirror, event: Event) => void;
* keydown: (instance: CodeMirror, event: Event) => void;
* }} CmEventMap
*
* @typedef {{
* origin: "+input" | "...";
* removed: string[];
* text: string[];
* from: unknown;
* to: unknown;
* }} CmChange
*
* @typedef {{
* value: string;
* setValue(x: string): void;
* pos?: number;
* }} NormalizedTextbox
*
* @typedef {{type: "warning"; message: string}} EditorWarning
*
* @typedef {{
* ui: {
* infuse(node: JQuery): OOElement;
* MessageWidget: {
* new (opts: {
* type: "success";
* inline: boolean;
* label: string;
* }): OOJQueryWrapper;
* };
* };
* }} OO
*
* @typedef {{ $element: JQuery }} OOJQueryWrapper
*
* @typedef {{
* disabled: boolean;
* setDisabled(x: boolean): void;
* }} OOElement
*
* @typedef {{
* message: string | HTMLElement | JQuery;
* seconds?: number;
* tag?: string;
* }} SessionStorageNotifyOptions
* @typedef {(opts: SessionStorageNotifyOptions) => void} SessionStorageNotify
*/
/**
* @template {any[]} Args
* @typedef {{
* add(cb: (...args: Args) => void): void;
* fire(...args: Args): void;
* }} MwHook
*/
/** @type {MediaWiki} */
var mw = /** @type {*} */ (window).mw;
if (mw.config.get("wgNamespaceNumber") === 0) {
mw.loader
.using([
"jquery.textSelection",
"ext.gadget.editorwarnings",
"ext.gadget.sessionStorageNotify",
"ext.gadget.data-lang",
"ext.gadget.data-lang-code-templates",
"ext.gadget.data-lang-replacements",
"ext.gadget.data-h3",
"ext.gadget.data-headings",
"oojs-ui-core",
])
.then(function () {
mw.hook("wikipage.editform").add(main);
});
}
/** @param {JQuery} form */
function main(form) {
polyfills();
/** @type {(category: string, warnings: EditorWarning[]) => void} */
var setEditorWarnings = /** @type {*} */ (window).setEditorWarnings;
/** @type {JQueryStatic} */
var $ = /** @type {*} */ (window).$;
/** @type {OO} */
var OO = /** @type {*} */ (window).OO;
/** @type {SessionStorageNotify} */
var sessionStorageNotify = /** @type {*} */ (window).sessionStorageNotify;
if (
["Övriga_tecken", "Övriga_uppslagsord"].includes(
mw.config.get("wgPageName")
)
) {
setEditorWarnings("tidy", [
{
type: "warning",
message:
"Finessen Tidy är avaktiverad för denna sida. Ingen automatisk korrigering av wikitext görs.",
},
]);
return;
}
var textbox = form.find("#wpTextbox1");
var textboxScroller = textbox;
if (form.find("[name=wpSection]").val() && /^===[^=]/.test(textbox.val())) {
setEditorWarnings("tidy", [
{
type: "warning",
message:
"Finessen Tidy är avaktiverad eftersom du redigerar ett ordklassavsnitt. Ingen automatisk korrigering av wikitext görs.",
},
]);
return;
}
var data = getData();
var save = form.find("#wpSave");
var ooSave = OO.ui.infuse(save.parent());
var summary = form.find("#wpSummary");
form.find("input[type=submit]").on("click", handleClick);
textbox
.on("input", handleInput)
.on("focus", handleFocus)
.on("keydown", handleKeydown);
/** @type {CodeMirror | undefined} */
var lastCmInstance;
mw.hook("ext.CodeMirror.switch").add(function (_isCm, elem) {
var cm = elem[0].CodeMirror;
textboxScroller = cm ? $(cm.display.scroller) : elem;
// We'll get a new CodeMirror instance every time the user switches to CM.
// We'll still verify that this, so we don't attach listeners to the same
// instance multiple times.
if (cm && cm !== lastCmInstance) {
cm.on("change", handleInput);
cm.on("focus", handleFocus);
cm.on("keydown", function (_cm, e) {
handleKeydown(e);
});
lastCmInstance = cm;
}
});
updateWarnings();
/** @type {{key: string; update: number; timeout: number} | undefined} */
var lastWarnings;
/** @type {number | undefined} */
var inputTimeout;
function handleInput() {
clearTimeout(inputTimeout);
if (lastWarnings) {
updateWarnings();
} else {
inputTimeout = setTimeout(updateWarnings, 3000);
}
}
function handleFocus() {
var tb = getTextbox();
var newValue = processOnFocus(tb.value);
if (tb.value !== newValue) {
tb.setValue(newValue);
}
}
/** @param {Event} e */
function handleClick(e) {
var isSave = e.target === save[0];
update(true, isSave);
if (isSave && ooSave.disabled) {
// It might have been disabled during the above `update()`.
e.preventDefault();
}
}
function updateWarnings() {
update(false, false);
}
/**
* @param {boolean} updateText
* @param {boolean} isSave
*/
function update(updateText, isSave) {
var tb = getTextbox();
var context = processOnSave(tb.value, data);
if (updateText) {
// Update textbox value.
var updated = context.unopaque(context.wikitext);
if (tb.value !== updated) {
tb.setValue(updated);
var cats = context.transformCats;
sessionStorageNotify({
message: new OO.ui.MessageWidget({
type: "success",
inline: true,
label:
"Wikitexten justerad" +
(cats.length && " (" + cats.join(", ") + ")"),
}).$element,
seconds: 4 + cats.length,
tag: "tidy",
});
}
// Update edit summary.
var newSummary = processSummary({
summary: summary.val(),
bot: mw.config.get("wgUserGroups").includes("bot"),
action: isSave ? "save" : "preview",
warn: !!context.warnings.size,
cats: context.transformCats,
});
if (newSummary !== undefined) {
summary.val(newSummary);
}
}
// Update warnings.
var warnArr = Array.from(context.warnings);
setEditorWarnings(
"tidy",
warnArr.map(function (text) {
return { type: "warning", message: text };
})
);
var warnKey = warnArr.join("");
if (warnKey) {
if (!lastWarnings || lastWarnings.key !== warnKey) {
ooSave.setDisabled(true);
if (lastWarnings) {
clearTimeout(lastWarnings.timeout);
}
lastWarnings = {
key: warnKey,
update: Date.now(),
timeout: setTimeout(function () {
ooSave.setDisabled(false);
}, 3000),
};
}
} else {
lastWarnings = undefined;
ooSave.setDisabled(false);
}
}
/** @param {Event} e */
function handleKeydown(e) {
var toInsert = processOnKeydown(
/** @type {KeyboardEvent} */ (e).key,
function () {
var tb = getTextbox();
if (tb.pos === undefined) {
return;
}
return {
text: tb.value,
cursor: tb.pos,
};
},
data
);
if (toInsert) {
e.preventDefault();
textbox.textSelection("encapsulateSelection", {
pre: toInsert[0],
post: toInsert[1],
});
}
}
/**
* Returns a normalized interface to a textbox.
* @returns {NormalizedTextbox}
*/
function getTextbox() {
var pos = textbox.textSelection("getCaretPosition", { startAndEnd: true });
return {
value: textbox.textSelection("getContents"),
setValue: function (x) {
var top = textboxScroller[0].scrollTop;
var left = textboxScroller[0].scrollLeft;
textbox.textSelection("setContents", x);
setTimeout(function () {
textboxScroller[0].scrollTop = top;
textboxScroller[0].scrollLeft = left;
});
},
pos: pos[0] === pos[1] ? pos[0] : undefined,
};
}
}
function polyfills() {
// Support IE and Edge <= 13.
if (!"".includes || ![].includes) {
String.prototype.includes = Array.prototype.includes = function (
search,
pos
) {
return this.indexOf(search, pos) !== -1;
};
}
// Support IE and Edge <= 13.
if (Object.entries) {
Object.entries = function (obj) {
return Object.keys(obj).map(function (k) {
return /** @type {[string, any]} */ ([k, obj[k]]);
});
};
}
// Support IE and Edge <= 18.
if (!window.globalThis) {
// @ts-expect-error
globalThis = window;
}
// Support IE.
if (!"".startsWith) {
/**
* @param {string} search
* @param {number} [pos]
*/
String.prototype.startsWith = function (search, pos) {
return this.slice(pos, (pos || 0) + search.length) === search;
};
}
}