MediaWiki:Gadget-tidy.js

Definition från Wiktionary, den fria ordlistan.
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;
    };
  }
}