User:Chlod/Scripts/Deputy/AttributionNoticeTemplateEditor.js

/*!

*

* ATTRIBUTION NOTICE TEMPLATE EDITOR

*

* Graphically edit attribution notice templates on a page's talk page.

*

* ------------------------------------------------------------------------

*

* Copyright 2022 Chlod Aidan Alejandro

*

* Licensed under the Apache License, Version 2.0 (the "License");

* you may not use this file except in compliance with the License.

* You may obtain a copy of the License at

*

* http://www.apache.org/licenses/LICENSE-2.0

*

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*

* ------------------------------------------------------------------------

*

* NOTE TO USERS AND DEBUGGERS: This userscript is originally written in

* TypeScript. The original TypeScript code is converted to raw JavaScript

* during the build process. To view the original source code, visit

*

* https://github.com/ChlodAlejandro/deputy

*

*/

//

(function () {

'use strict';

/******************************************************************************

Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any

purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH

REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY

AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,

INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM

LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR

OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR

PERFORMANCE OF THIS SOFTWARE.

***************************************************************************** */

/* global Reflect, Promise, SuppressedError, Symbol, Iterator */

function __awaiter(thisArg, _arguments, P, generator) {

function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }

return new (P || (P = Promise))(function (resolve, reject) {

function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }

function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }

function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }

step((generator = generator.apply(thisArg, _arguments || [])).next());

});

}

typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {

var e = new Error(message);

return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;

};

var dist = {};

/* eslint-disable @typescript-eslint/no-unused-vars */

/**

* License: MIT

* @author Santo Pfingsten

* @see https://github.com/Lusito/tsx-dom

*/

Object.defineProperty(dist, "__esModule", { value: true });

var h_1 = dist.h = void 0;

function applyChild(element, child) {

if (child instanceof Element)

element.appendChild(child);

else if (typeof child === "string" || typeof child === "number")

element.appendChild(document.createTextNode(child.toString()));

else

console.warn("Unknown type to append: ", child);

}

function applyChildren(element, children) {

for (const child of children) {

if (!child && child !== 0)

continue;

if (Array.isArray(child))

applyChildren(element, child);

else

applyChild(element, child);

}

}

function transferKnownProperties(source, target) {

for (const key of Object.keys(source)) {

if (key in target)

target[key] = source[key];

}

}

function createElement(tag, attrs) {

const options = (attrs === null || attrs === void 0 ? void 0 : attrs.is) ? { is: attrs.is } : undefined;

if (attrs === null || attrs === void 0 ? void 0 : attrs.xmlns)

return document.createElementNS(attrs.xmlns, tag, options);

return document.createElement(tag, options);

}

function h(tag, attrs, ...children) {

if (typeof tag === "function")

return tag(Object.assign(Object.assign({}, attrs), { children }));

const element = createElement(tag, attrs);

if (attrs) {

for (const name of Object.keys(attrs)) {

// Ignore some debug props that might be added by bundlers

if (name === "__source" || name === "__self" || name === "is" || name === "xmlns")

continue;

const value = attrs[name];

if (name.startsWith("on")) {

const finalName = name.replace(/Capture$/, "");

const useCapture = name !== finalName;

const eventName = finalName.toLowerCase().substring(2);

element.addEventListener(eventName, value, useCapture);

}

else if (name === "style" && typeof value !== "string") {

// Special handler for style with a value of type CSSStyleDeclaration

transferKnownProperties(value, element.style);

}

else if (name === "dangerouslySetInnerHTML")

element.innerHTML = value;

else if (value === true)

element.setAttribute(name, name);

else if (value || value === 0)

element.setAttribute(name, value.toString());

}

}

applyChildren(element, children);

return element;

}

h_1 = dist.h = h;

/**

* Log errors to the console.

*

* @param {...any} data

*/

function error(...data) {

console.error('[Deputy]', ...data);

}

/**

* Unwraps an OOUI widget from its JQuery `$element` variable and returns it as an

* HTML element.

*

* @param el The widget to unwrap.

* @return The unwrapped widget.

*/

function unwrapWidget (el) {

if (el.$element == null) {

error(el);

throw new Error('Element is not an OOUI Element!');

}

return el.$element[0];

}

/**

* Gets the namespace ID from a canonical (not localized) namespace name.

*

* @param namespace The namespace to get

* @return The namespace ID

*/

function nsId(namespace) {

return mw.config.get('wgNamespaceIds')[namespace.toLowerCase().replace(/ /g, '_')];

}

/**

* Works like `Object.values`.

*

* @param obj The object to get the values of.

* @return The values of the given object as an array

*/

function getObjectValues(obj) {

return Object.keys(obj).map((key) => obj[key]);

}

/**

* Transforms the `redirects` object returned by MediaWiki's `query` action into an

* object instead of an array.

*

* @param redirects

* @param normalized

* @return Redirects as an object

*/

function toRedirectsObject(redirects, normalized) {

var _a;

if (redirects == null) {

return {};

}

const out = {};

for (const redirect of redirects) {

out[redirect.from] = redirect.to;

}

// Single-level redirect-normalize loop check

for (const normal of normalized) {

out[normal.from] = (_a = out[normal.to]) !== null && _a !== void 0 ? _a : normal.to;

}

return out;

}

/**

* Copies text to the clipboard. Relies on the old style of clipboard copying

* (using `document.execCommand` due to a lack of support for `navigator`-based

* clipboard handling).

*

* @param text The text to copy to the clipboard.

*/

function copyToClipboard (text) {

const body = document.getElementsByTagName('body')[0];

const textarea = document.createElement('textarea');

textarea.value = text;

textarea.style.position = 'fixed';

textarea.style.left = '-100vw';

textarea.style.top = '-100vh';

body.appendChild(textarea);

textarea.select();

// noinspection JSDeprecatedSymbols

document.execCommand('copy');

body.removeChild(textarea);

}

/**

* Performs {{yesno}}-based string interpretation.

*

* @param value The value to check

* @param pull Depends which direction to pull unspecified values.

* @return If `pull` is true,

* any value that isn't explicitly a negative value will return `true`. Otherwise,

* any value that isn't explicitly a positive value will return `false`.

*/

function yesNo (value, pull = true) {

if (pull) {

return value !== false &&

value !== 'no' &&

value !== 'n' &&

value !== 'false' &&

value !== 'f' &&

value !== 'off' &&

+value !== 0;

}

else {

return !(value !== true &&

value !== 'yes' &&

value !== 'y' &&

value !== 't' &&

value !== 'true' &&

value !== 'on' &&

+value !== 1);

}

}

/**

* Normalizes the title into an mw.Title object based on either a given title or

* the current page.

*

* @param title The title to normalize. Default is current page.

* @return {mw.Title} A mw.Title object. `null` if not a valid title.

* @private

*/

function normalizeTitle(title) {

if (title instanceof mw.Title) {

return title;

}

else if (typeof title === 'string') {

return new mw.Title(title);

}

else if (title == null) {

// Null check goes first to avoid accessing properties of `null`.

return new mw.Title(mw.config.get('wgPageName'));

}

else if (title.title != null && title.namespace != null) {

return new mw.Title(title.title, title.namespace);

}

else {

return null;

}

}

/**

* Checks if two MediaWiki page titles are equal.

*

* @param title1

* @param title2

* @return `true` if `title1` and `title2` refer to the same page

*/

function equalTitle(title1, title2) {

return normalizeTitle(title1).getPrefixedDb() === normalizeTitle(title2).getPrefixedDb();

}

var version = "0.9.0";

var gitAbbrevHash = "317b503";

var gitBranch = "HEAD";

var gitDate = "Wed, 19 Feb 2025 00:13:53 +0800";

var gitVersion = "0.9.0+g317b503";

/**

*

*/

class MwApi {

/**

* @return A mw.Api for the current wiki.

*/

static get action() {

var _a;

return (_a = this._action) !== null && _a !== void 0 ? _a : (this._action = new mw.Api({

ajax: {

headers: {

'Api-User-Agent': `Deputy/${version} (https://w.wiki/7NWR; User:Chlod; wiki@chlod.net)`

}

},

parameters: {

format: 'json',

formatversion: 2,

utf8: true,

errorformat: 'html',

errorlang: mw.config.get('wgUserLanguage'),

errorsuselocal: true

}

}));

}

/**

* @return A mw.Rest for the current wiki.

*/

static get rest() {

var _a;

return (_a = this._rest) !== null && _a !== void 0 ? _a : (this._rest = new mw.Rest());

}

}

MwApi.USER_AGENT = `Deputy/${version} (https://w.wiki/7NWR; User:Chlod; wiki@chlod.net)`;

/**

* Get the API error text from an API response.

*

* @param errorData

* @param n Get the `n`th error. Defaults to 0 (first error).

*/

function getApiErrorText(errorData, n = 0) {

var _a, _b, _c, _d, _e, _f, _g;

// errorformat=html

return ((_b = (_a = errorData.errors) === null || _a === void 0 ? void 0 : _a[n]) === null || _b === void 0 ? void 0 : _b.html) ?

h_1("span", { dangerouslySetInnerHTML: (_d = (_c = errorData.errors) === null || _c === void 0 ? void 0 : _c[n]) === null || _d === void 0 ? void 0 : _d.html }) :

(

// errorformat=plaintext/wikitext

(_g = (_f = (_e = errorData.errors) === null || _e === void 0 ? void 0 : _e[n]) === null || _f === void 0 ? void 0 : _f.text) !== null && _g !== void 0 ? _g :

// errorformat=bc

errorData.info);

}

let InternalRevisionDateGetButton;

/**

* Initializes the process element.

*/

function initRevisionDateGetButton() {

InternalRevisionDateGetButton = class RevisionDateGetButton extends OO.ui.ButtonWidget {

/**

* @param config Configuration to be passed to the element.

*/

constructor(config) {

super(Object.assign({

icon: 'download',

invisibleLabel: true,

disabled: true

}, config));

this.revisionInputWidget = config.revisionInputWidget;

this.dateInputWidget = config.dateInputWidget;

this.revisionInputWidget.on('change', this.updateButton.bind(this));

this.dateInputWidget.on('change', this.updateButton.bind(this));

this.on('click', this.setDateFromRevision.bind(this));

this.updateButton();

}

/**

* Update the disabled state of the button.

*/

updateButton() {

this.setDisabled(isNaN(+this.revisionInputWidget.getValue()) ||

!!this.dateInputWidget.getValue());

}

/**

* Set the date from the revision ID provided in the value of

* `this.revisionInputWidget`.

*/

setDateFromRevision() {

return __awaiter(this, void 0, void 0, function* () {

const revid = this.revisionInputWidget.getValue();

if (isNaN(+revid)) {

mw.notify(mw.msg('deputy.ante.dateAuto.invalid'), { type: 'error' });

this.updateButton();

return;

}

this

.setIcon('ellipsis')

.setDisabled(true);

this.dateInputWidget.setDisabled(true);

yield MwApi.action.get({

action: 'query',

prop: 'revisions',

revids: revid,

rvprop: 'timestamp'

}).then((data) => {

if (data.query.badrevids != null) {

mw.notify(mw.msg('deputy.ante.dateAuto.missing', revid), { type: 'error' });

this.updateButton();

return;

}

this.dateInputWidget.setValue(

// ISO-format date

data.query.pages[0].revisions[0].timestamp.split('T')[0]);

this.dateInputWidget.setDisabled(false);

this.setIcon('download');

this.updateButton();

}, (_error, errorData) => {

mw.notify(mw.msg('deputy.ante.dateAuto.failed', getApiErrorText(errorData)), { type: 'error' });

this.dateInputWidget.setDisabled(false);

this.setIcon('download');

this.updateButton();

});

});

}

};

}

/**

* Creates a new RevisionDateGetButton.

*

* @param config Configuration to be passed to the element.

* @return A RevisionDateGetButton object

*/

function RevisionDateGetButton (config) {

if (!InternalRevisionDateGetButton) {

initRevisionDateGetButton();

}

return new InternalRevisionDateGetButton(config);

}

let InternalSmartTitleInputWidget;

/**

* Initializes the process element.

*/

function initSmartTitleInputWidget() {

InternalSmartTitleInputWidget = class SmartTitleInputWidget extends mw.widgets.TitleInputWidget {

/**

* @param config Configuration to be passed to the element.

*/

constructor(config) {

super(Object.assign(config, {

// Force this to be true

allowSuggestionsWhenEmpty: true

}));

}

/**

* @inheritDoc

*/

getRequestQuery() {

const v = super.getRequestQuery();

return v || normalizeTitle().getSubjectPage().getPrefixedText();

}

/**

* @inheritDoc

*/

getQueryValue() {

const v = super.getQueryValue();

return v || normalizeTitle().getSubjectPage().getPrefixedText();

}

};

}

/**

* Creates a new SmartTitleInputWidget.

*

* @param config Configuration to be passed to the element.

* @return A SmartTitleInputWidget object

*/

function SmartTitleInputWidget (config) {

if (!InternalSmartTitleInputWidget) {

initSmartTitleInputWidget();

}

return new InternalSmartTitleInputWidget(config);

}

let InternalPageLatestRevisionGetButton;

/**

* Initializes the process element.

*/

function initPageLatestRevisionGetButton() {

InternalPageLatestRevisionGetButton = class PageLatestRevisionGetButton extends OO.ui.ButtonWidget {

/**

* @param config Configuration to be passed to the element.

*/

constructor(config) {

super(Object.assign({

icon: 'download',

invisibleLabel: true,

disabled: true

}, config));

this.titleInputWidget = config.titleInputWidget;

this.revisionInputWidget = config.revisionInputWidget;

this.titleInputWidget.on('change', this.updateButton.bind(this));

this.revisionInputWidget.on('change', this.updateButton.bind(this));

this.on('click', this.setRevisionFromPageLatestRevision.bind(this));

this.updateButton();

}

/**

* Update the disabled state of the button.

*/

updateButton() {

this.setDisabled(this.titleInputWidget.getValue().trim().length === 0 ||

this.revisionInputWidget.getValue().trim().length !== 0 ||

!this.titleInputWidget.isQueryValid());

}

/**

* Set the revision ID from the page provided in the value of

* `this.titleInputWidget`.

*/

setRevisionFromPageLatestRevision() {

return __awaiter(this, void 0, void 0, function* () {

this

.setIcon('ellipsis')

.setDisabled(true);

this.revisionInputWidget.setDisabled(true);

const title = this.titleInputWidget.getValue();

yield MwApi.action.get({

action: 'query',

prop: 'revisions',

titles: title,

rvprop: 'ids'

}).then((data) => {

if (data.query.pages[0].missing) {

mw.notify(mw.msg('deputy.ante.revisionAuto.missing', title), { type: 'error' });

this.updateButton();

return;

}

this.revisionInputWidget.setValue(data.query.pages[0].revisions[0].revid);

this.revisionInputWidget.setDisabled(false);

this.setIcon('download');

this.updateButton();

}, (_error, errorData) => {

mw.notify(mw.msg('deputy.ante.revisionAuto.failed', getApiErrorText(errorData)), { type: 'error' });

this.revisionInputWidget.setDisabled(false);

this.setIcon('download');

this.updateButton();

});

});

}

};

}

/**

* Creates a new PageLatestRevisionGetButton.

*

* @param config Configuration to be passed to the element.

* @return A PageLatestRevisionGetButton object

*/

function PageLatestRevisionGetButton (config) {

if (!InternalPageLatestRevisionGetButton) {

initPageLatestRevisionGetButton();

}

return new InternalPageLatestRevisionGetButton(config);

}

/**

* Shows a confirmation dialog, if the user does not have danger mode enabled.

* If the user has danger mode enabled, this immediately resolves to true, letting

* the action run immediately.

*

* Do not use this with any action that can potentially break templates, user data,

* or cause irreversible data loss.

*

* @param config The user's configuration

* @param message See {@link OO.ui.MessageDialog}'s parameters.

* @param options See {@link OO.ui.MessageDialog}'s parameters.

* @return Promise resolving to a true/false boolean.

*/

function dangerModeConfirm(config, message, options) {

if (config.all.core.dangerMode.get()) {

return $.Deferred().resolve(true);

}

else {

return OO.ui.confirm(message, options);

}

}

/**

* Log warnings to the console.

*

* @param {...any} data

*/

function warn(...data) {

console.warn('[Deputy]', ...data);

}

/**

* What it says on the tin. Attempt to parse out a `title`, `diff`,

* or `oldid` from a URL. This is useful for converting diff URLs into actual

* diff information, and especially useful for {{copied}} templates.

*

* If diff parameters were not found (no `diff` or `oldid`), they will be `null`.

*

* @param url The URL to parse

* @return Parsed info: `diff` or `oldid` revision IDs, and/or the page title.

*/

function parseDiffUrl(url) {

if (typeof url === 'string') {

url = new URL(url);

}

// Attempt to get values from URL parameters (when using `/w/index.php?action=diff`)

let oldid = url.searchParams.get('oldid');

let diff = url.searchParams.get('diff');

let title = url.searchParams.get('title');

// Attempt to get information from this URL.

tryConvert: {

if (title && oldid && diff) {

// Skip if there's nothing else we need to get.

break tryConvert;

}

// Attempt to get values from Special:Diff short-link

const diffSpecialPageCheck =

// eslint-disable-next-line security/detect-unsafe-regex

/\/wiki\/Special:Diff\/(prev|next|\d+)(?:\/(prev|next|\d+))?/i.exec(url.pathname);

if (diffSpecialPageCheck != null) {

if (diffSpecialPageCheck[1] != null &&

diffSpecialPageCheck[2] == null) {

// Special:Diff/diff

diff = diffSpecialPageCheck[1];

}

else if (diffSpecialPageCheck[1] != null &&

diffSpecialPageCheck[2] != null) {

// Special:Diff/oldid/diff

oldid = diffSpecialPageCheck[1];

diff = diffSpecialPageCheck[2];

}

break tryConvert;

}

// Attempt to get values from Special:PermanentLink short-link

const permanentLinkCheck = /\/wiki\/Special:Perma(nent)?link\/(\d+)/i.exec(url.pathname);

if (permanentLinkCheck != null) {

oldid = permanentLinkCheck[2];

break tryConvert;

}

// Attempt to get values from article path with ?oldid or ?diff

// eslint-disable-next-line security/detect-non-literal-regexp

const articlePathRegex = new RegExp(mw.util.getUrl('(.*)'))

.exec(url.pathname);

if (articlePathRegex != null) {

title = decodeURIComponent(articlePathRegex[1]);

break tryConvert;

}

}

// Convert numbers to numbers

if (oldid != null && !isNaN(+oldid)) {

oldid = +oldid;

}

if (diff != null && !isNaN(+diff)) {

diff = +diff;

}

// Try to convert a page title

try {

title = new mw.Title(title).getPrefixedText();

}

catch (e) {

warn('Failed to normalize page title during diff URL conversion.');

}

return {

diff: diff,

oldid: oldid,

title: title

};

}

let InternalCopiedTemplateRowPage;

/**

* The UI representation of a {{copied}} template row. This refers to a set of `diff`, `to`,

* or `from` parameters on each {{copied}} template.

*

* Note that "Page" in the class title does not refer to a MediaWiki page, but rather

* a OOUI PageLayout.

*/

function initCopiedTemplateRowPage() {

InternalCopiedTemplateRowPage = class CopiedTemplateRowPage extends OO.ui.PageLayout {

/**

* @param config Configuration to be passed to the element.

*/

constructor(config) {

const { copiedTemplateRow, parent } = config;

if (parent == null) {

throw new Error('Parent dialog (CopiedTemplateEditorDialog) is required');

}

else if (copiedTemplateRow == null) {

throw new Error('Reference row (CopiedTemplateRow) is required');

}

const finalConfig = {

classes: ['cte-page-row']

};

super(copiedTemplateRow.id, finalConfig);

this.parent = parent;

this.copiedTemplateRow = copiedTemplateRow;

this.refreshLabel();

this.copiedTemplateRow.parent.addEventListener('destroy', () => {

parent.rebuildPages();

});

this.copiedTemplateRow.parent.addEventListener('rowDelete', () => {

parent.rebuildPages();

});

this.$element.append(this.render().$element);

}

/**

* Refreshes the page's label

*/

refreshLabel() {

if (this.copiedTemplateRow.from && equalTitle(this.copiedTemplateRow.from, normalizeTitle(this.copiedTemplateRow.parent.parsoid.getPage())

.getSubjectPage())) {

this.label = mw.message('deputy.ante.copied.entry.shortTo', this.copiedTemplateRow.to || '???').text();

}

else if (this.copiedTemplateRow.to && equalTitle(this.copiedTemplateRow.to, normalizeTitle(this.copiedTemplateRow.parent.parsoid.getPage())

.getSubjectPage())) {

this.label = mw.message('deputy.ante.copied.entry.shortFrom', this.copiedTemplateRow.from || '???').text();

}

else {

this.label = mw.message('deputy.ante.copied.entry.short', this.copiedTemplateRow.from || '???', this.copiedTemplateRow.to || '???').text();

}

if (this.outlineItem) {

this.outlineItem.setLabel(this.label);

}

}

/**

* Renders this page. Returns a FieldsetLayout OOUI widget.

*

* @return An OOUI FieldsetLayout

*/

render() {

this.layout = new OO.ui.FieldsetLayout({

icon: 'parameter',

label: mw.msg('deputy.ante.copied.entry.label'),

classes: ['cte-fieldset']

});

this.layout.$element.append(this.renderButtons());

this.layout.addItems(this.renderFields());

return this.layout;

}

/**

* Renders a set of buttons used to modify a specific {{copied}} template row.

*

* @return An array of OOUI FieldLayouts

*/

renderButtons() {

const deleteButton = new OO.ui.ButtonWidget({

icon: 'trash',

title: mw.msg('deputy.ante.copied.entry.remove'),

framed: false,

flags: ['destructive']

});

deleteButton.on('click', () => {

this.copiedTemplateRow.parent.deleteRow(this.copiedTemplateRow);

});

const copyButton = new OO.ui.ButtonWidget({

icon: 'quotes',

title: mw.msg('deputy.ante.copied.entry.copy'),

framed: false

});

copyButton.on('click', () => {

// TODO: Find out a way to l10n-ize this.

let attributionString = `Attribution: Content ${this.copiedTemplateRow.merge ? 'merged' : 'partially copied'}`;

let lacking = false;

if (this.copiedTemplateRow.from != null &&

this.copiedTemplateRow.from.length !== 0) {

attributionString += ` from ${this.copiedTemplateRow.from}`;

}

else {

lacking = true;

if (this.copiedTemplateRow.from_oldid != null) {

attributionString += ' from a page';

}

}

if (this.copiedTemplateRow.from_oldid != null) {

attributionString += ` as of revision ${this.copiedTemplateRow.from_oldid}`;

}

if (this.copiedTemplateRow.to_diff != null ||

this.copiedTemplateRow.to_oldid != null) {

// Shifting will ensure that `to_oldid` will be used if `to_diff` is missing.

const diffPart1 = this.copiedTemplateRow.to_oldid ||

this.copiedTemplateRow.to_diff;

const diffPart2 = this.copiedTemplateRow.to_diff ||

this.copiedTemplateRow.to_oldid;

attributionString += ` with this edit`;

}

if (this.copiedTemplateRow.from != null &&

this.copiedTemplateRow.from.length !== 0) {

attributionString += `; refer to that page's edit history for additional attribution`;

}

attributionString += '.';

copyToClipboard(attributionString);

if (lacking) {

mw.notify(mw.msg('deputy.ante.copied.entry.copy.lacking'), { title: mw.msg('deputy.ante'), type: 'warn' });

}

else {

mw.notify(mw.msg('deputy.ante.copied.entry.copy.success'), { title: mw.msg('deputy.ante') });

}

});

return h_1("div", { style: {

float: 'right',

position: 'absolute',

top: '0.5em',

right: '0.5em'

} },

unwrapWidget(copyButton),

unwrapWidget(deleteButton));

}

/**

* Renders a set of OOUI InputWidgets and FieldLayouts, eventually returning an

* array of each FieldLayout to append to the FieldsetLayout.

*

* @return An array of OOUI FieldLayouts

*/

renderFields() {

const copiedTemplateRow = this.copiedTemplateRow;

const parsedDate = (copiedTemplateRow.date == null || copiedTemplateRow.date.trim().length === 0) ?

undefined : (!isNaN(new Date(copiedTemplateRow.date.trim() + ' UTC').getTime()) ?

(new Date(copiedTemplateRow.date.trim() + ' UTC')) : (!isNaN(new Date(copiedTemplateRow.date.trim()).getTime()) ?

new Date(copiedTemplateRow.date.trim()) : null));

this.inputs = {

from: SmartTitleInputWidget({

$overlay: this.parent.$overlay,

placeholder: mw.msg('deputy.ante.copied.from.placeholder'),

value: copiedTemplateRow.from,

validate: /^.+$/g

}),

from_oldid: new OO.ui.TextInputWidget({

placeholder: mw.msg('deputy.ante.copied.from_oldid.placeholder'),

value: copiedTemplateRow.from_oldid,

validate: /^\d*$/

}),

to: SmartTitleInputWidget({

$overlay: this.parent.$overlay,

placeholder: mw.msg('deputy.ante.copied.to.placeholder'),

value: copiedTemplateRow.to

}),

to_diff: new OO.ui.TextInputWidget({

placeholder: mw.msg('deputy.ante.copied.to_diff.placeholder'),

value: copiedTemplateRow.to_diff,

validate: /^\d*$/

}),

// Advanced options

to_oldid: new OO.ui.TextInputWidget({

placeholder: mw.msg('deputy.ante.copied.to_oldid.placeholder'),

value: copiedTemplateRow.to_oldid,

validate: /^\d*$/

}),

diff: new OO.ui.TextInputWidget({

placeholder: mw.msg('deputy.ante.copied.diff.placeholder'),

value: copiedTemplateRow.diff

}),

merge: new OO.ui.CheckboxInputWidget({

selected: yesNo(copiedTemplateRow.merge)

}),

afd: new OO.ui.TextInputWidget({

placeholder: mw.msg('deputy.ante.copied.afd.placeholder'),

value: copiedTemplateRow.afd,

disabled: copiedTemplateRow.merge === undefined,

// Prevent people from adding the WP:AFD prefix.

validate: /^((?!W(iki)?p(edia)?:(A(rticles)?[ _]?f(or)?[ _]?d(eletion)?\/)).+|$)/gi

}),

date: new mw.widgets.DateInputWidget({

$overlay: this.parent.$overlay,

icon: 'calendar',

value: parsedDate ? `${parsedDate.getUTCFullYear()}-${parsedDate.getUTCMonth() + 1}-${parsedDate.getUTCDate()}` : undefined,

placeholder: mw.msg('deputy.ante.copied.date.placeholder'),

calendar: {

verticalPosition: 'above'

}

}),

toggle: new OO.ui.ToggleSwitchWidget()

};

const diffConvert = new OO.ui.ButtonWidget({

label: mw.msg('deputy.ante.copied.convert')

});

const dateAuto = RevisionDateGetButton({

label: mw.msg('deputy.ante.dateAuto', 'to_diff'),

revisionInputWidget: this.inputs.to_diff,

dateInputWidget: this.inputs.date

});

const revisionAutoFrom = PageLatestRevisionGetButton({

invisibleLabel: false,

label: mw.msg('deputy.ante.revisionAuto'),

title: mw.msg('deputy.ante.revisionAuto.title', 'from'),

titleInputWidget: this.inputs.from,

revisionInputWidget: this.inputs.from_oldid

});

const revisionAutoTo = PageLatestRevisionGetButton({

invisibleLabel: false,

label: mw.msg('deputy.ante.revisionAuto'),

title: mw.msg('deputy.ante.revisionAuto.title', 'to'),

titleInputWidget: this.inputs.to,

revisionInputWidget: this.inputs.to_diff

});

this.fieldLayouts = {

from: new OO.ui.FieldLayout(this.inputs.from, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.from.label'),

align: 'top',

help: mw.msg('deputy.ante.copied.from.help')

}),

from_oldid: new OO.ui.ActionFieldLayout(this.inputs.from_oldid, revisionAutoFrom, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.from_oldid.label'),

align: 'left',

help: mw.msg('deputy.ante.copied.from_oldid.help')

}),

to: new OO.ui.FieldLayout(this.inputs.to, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.to.label'),

align: 'top',

help: mw.msg('deputy.ante.copied.to.help')

}),

to_diff: new OO.ui.ActionFieldLayout(this.inputs.to_diff, revisionAutoTo, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.to_diff.label'),

align: 'left',

help: mw.msg('deputy.ante.copied.to_diff.help')

}),

// Advanced options

to_oldid: new OO.ui.FieldLayout(this.inputs.to_oldid, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.to_oldid.label'),

align: 'left',

help: mw.msg('deputy.ante.copied.to_oldid.help')

}),

diff: new OO.ui.ActionFieldLayout(this.inputs.diff, diffConvert, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.diff.label'),

align: 'inline',

help: new OO.ui.HtmlSnippet(mw.message('deputy.ante.copied.diff.help').plain())

}),

merge: new OO.ui.FieldLayout(this.inputs.merge, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.merge.label'),

align: 'inline',

help: mw.msg('deputy.ante.copied.merge.help')

}),

afd: new OO.ui.FieldLayout(this.inputs.afd, {

$overlay: this.parent.$overlay,

label: mw.msg('deputy.ante.copied.afd.label'),

align: 'left',

help: mw.msg('deputy.ante.copied.afd.help')

}),

date: new OO.ui.ActionFieldLayout(this.inputs.date, dateAuto, {

align: 'inline',

classes: ['cte-fieldset-date']

}),

toggle: new OO.ui.FieldLayout(this.inputs.toggle, {

label: mw.msg('deputy.ante.copied.advanced'),

align: 'inline',

classes: ['cte-fieldset-advswitch']

})

};

if (parsedDate === null) {

this.fieldLayouts.date.setWarnings([

mw.msg('deputy.ante.copied.dateInvalid', copiedTemplateRow.date)

]);

}

// Define options that get hidden when advanced options are toggled

const advancedOptions = [

this.fieldLayouts.to_oldid,

this.fieldLayouts.diff,

this.fieldLayouts.merge,

this.fieldLayouts.afd

];

// Self-imposed deprecation notice in order to steer away from plain URL

// diff links. This will, in the long term, make it easier to parse out

// and edit {{copied}} templates.

const diffDeprecatedNotice = new OO.ui.HtmlSnippet(mw.message('deputy.ante.copied.diffDeprecate').plain());

// Hide advanced options

advancedOptions.forEach((e) => {

e.toggle(false);

});

// ...except for `diff` if it's supplied (legacy reasons)

if (copiedTemplateRow.diff) {

this.fieldLayouts.diff.toggle(true);

this.fieldLayouts.diff.setWarnings([diffDeprecatedNotice]);

}

else {

diffConvert.setDisabled(true);

}

// Attach event listeners

this.inputs.diff.on('change', () => {

if (this.inputs.diff.getValue().length > 0) {

try {

// Check if the diff URL is from this wiki.

if (new URL(this.inputs.diff.getValue(), window.location.href).host === window.location.host) {

// Prefer `to_oldid` and `to_diff`

this.fieldLayouts.diff.setWarnings([diffDeprecatedNotice]);

diffConvert.setDisabled(false);

}

else {

this.fieldLayouts.diff.setWarnings([]);

diffConvert.setDisabled(true);

}

}

catch (e) {

// Clear warnings just to be safe.

this.fieldLayouts.diff.setWarnings([]);

diffConvert.setDisabled(true);

}

}

else {

this.fieldLayouts.diff.setWarnings([]);

diffConvert.setDisabled(true);

}

});

this.inputs.merge.on('change', (value) => {

this.inputs.afd.setDisabled(!value);

});

this.inputs.toggle.on('change', (value) => {

advancedOptions.forEach((e) => {

e.toggle(value);

});

this.fieldLayouts.to_diff.setLabel(value ? 'Ending revision ID' : 'Revision ID');

});

this.inputs.from.on('change', () => {

this.refreshLabel();

});

this.inputs.to.on('change', () => {

this.refreshLabel();

});

for (const _field in this.inputs) {

if (_field === 'toggle') {

continue;

}

const field = _field;

const input = this.inputs[field];

// Attach the change listener

input.on('change', (value) => {

if (input instanceof OO.ui.CheckboxInputWidget) {

// Specific to `merge`. Watch out before adding more checkboxes.

this.copiedTemplateRow[field] = value ? 'yes' : '';

}

else if (input instanceof mw.widgets.DateInputWidget) {

this.copiedTemplateRow[field] = value ?

window.moment(value, 'YYYY-MM-DD')

.locale(mw.config.get('wgContentLanguage'))

.format('D MMMM Y') : undefined;

if (value.length > 0) {

this.fieldLayouts[field].setWarnings([]);

}

}

else {

this.copiedTemplateRow[field] = value;

}

copiedTemplateRow.parent.save();

this.refreshLabel();

});

if (input instanceof OO.ui.TextInputWidget) {

// Rechecks the validity of the field.

input.setValidityFlag();

}

}

// Diff convert click handler

diffConvert.on('click', this.convertDeprecatedDiff.bind(this));

return getObjectValues(this.fieldLayouts);

}

/**

* Converts a raw diff URL on the same wiki as the current to use `to` and `to_oldid`

* (and `to_diff`, if available).

*/

convertDeprecatedDiff() {

return __awaiter(this, void 0, void 0, function* () {

const value = this.inputs.diff.getValue();

try {

const url = new URL(value, window.location.href);

if (!value) {

return;

}

if (url.host !== window.location.host) {

if (!(yield OO.ui.confirm(mw.msg('deputy.ante.copied.diffDeprecate.warnHost')))) {

return;

}

}

// From the same wiki, accept deprecation immediately.

// Parse out info from this diff URL

const parseInfo = parseDiffUrl(url);

let { diff, oldid } = parseInfo;

const { title } = parseInfo;

// If only an oldid was provided, and no diff

if (oldid && !diff) {

diff = oldid;

oldid = undefined;

}

const confirmProcess = new OO.ui.Process();

// Looping over the row name and the value that will replace it.

for (const [_rowName, newValue] of [

['to_oldid', oldid],

['to_diff', diff],

['to', title]

]) {

const rowName = _rowName;

if (newValue == null) {

continue;

}

if (

// Field has an existing value

this.copiedTemplateRow[rowName] != null &&

this.copiedTemplateRow[rowName].length > 0 &&

this.copiedTemplateRow[rowName] !== newValue) {

confirmProcess.next(() => __awaiter(this, void 0, void 0, function* () {

const confirmPromise = dangerModeConfirm(window.CopiedTemplateEditor.config, mw.message('deputy.ante.copied.diffDeprecate.replace', rowName, this.copiedTemplateRow[rowName], newValue).text());

confirmPromise.done((confirmed) => {

if (confirmed) {

this.inputs[rowName].setValue(newValue);

this.fieldLayouts[rowName].toggle(true);

}

});

return confirmPromise;

}));

}

else {

this.inputs[rowName].setValue(newValue);

this.fieldLayouts[rowName].toggle(true);

}

}

confirmProcess.next(() => {

this.copiedTemplateRow.parent.save();

this.inputs.diff.setValue('');

if (!this.inputs.toggle.getValue()) {

this.fieldLayouts.diff.toggle(false);

}

});

confirmProcess.execute();

}

catch (e) {

error('Cannot convert `diff` parameter to URL.', e);

OO.ui.alert(mw.msg('deputy.ante.copied.diffDeprecate.failed'));

}

});

}

/**

* Sets up the outline item of this page. Used in the BookletLayout.

*/

setupOutlineItem() {

if (this.outlineItem !== undefined) {

this.outlineItem

.setMovable(true)

.setRemovable(true)

.setIcon('parameter')

.setLevel(1)

.setLabel(this.label);

}

}

};

}

/**

* Creates a new CopiedTemplateRowPage.

*

* @param config Configuration to be passed to the element.

* @return A CopiedTemplateRowPage object

*/

function CopiedTemplateRowPage (config) {

if (!InternalCopiedTemplateRowPage) {

initCopiedTemplateRowPage();

}

return new InternalCopiedTemplateRowPage(config);

}

/**

* An attribution notice's row or entry.

*/

class AttributionNoticeRow {

/**

* @return The parent of this attribution notice row.

*/

get parent() {

return this._parent;

}

/**

* Sets the parent. Automatically moves this template from one

* parent's row set to another.

*

* @param newParent The new parent.

*/

set parent(newParent) {

this._parent.deleteRow(this);

newParent.addRow(this);

this._parent = newParent;

}

/**

*

* @param parent

*/

constructor(parent) {

this._parent = parent;

const r = window.btoa((Math.random() * 10000).toString()).slice(0, 6);

this.name = this.parent.name + '#' + r;

this.id = window.btoa(parent.node.getTarget().wt) + '-' + this.name;

}

/**

* Clones this row.

*

* @param parent The parent of this new row.

* @return The cloned row

*/

clone(parent) {

// Odd constructor usage here allows cloning from subclasses without having

// to re-implement the cloning function.

// noinspection JSCheckFunctionSignatures

return new this.constructor(this, parent);

}

}

const copiedTemplateRowParameters = [

'from', 'from_oldid', 'to', 'to_diff',

'to_oldid', 'diff', 'date', 'afd', 'merge'

];

/**

* Represents a row/entry in a {{copied}} template.

*/

class CopiedTemplateRow extends AttributionNoticeRow {

// noinspection JSDeprecatedSymbols

/**

* Creates a new RawCopiedTemplateRow

*

* @param rowObjects

* @param parent

*/

constructor(rowObjects, parent) {

super(parent);

this.from = rowObjects.from;

// eslint-disable-next-line camelcase

this.from_oldid = rowObjects.from_oldid;

this.to = rowObjects.to;

// eslint-disable-next-line camelcase

this.to_diff = rowObjects.to_diff;

// eslint-disable-next-line camelcase

this.to_oldid = rowObjects.to_oldid;

this.diff = rowObjects.diff;

this.date = rowObjects.date;

this.afd = rowObjects.afd;

this.merge = rowObjects.merge;

}

/**

* @inheritDoc

*/

clone(parent) {

return super.clone(parent);

}

/**

* @inheritDoc

*/

generatePage(dialog) {

return CopiedTemplateRowPage({

copiedTemplateRow: this,

parent: dialog

});

}

}

/**

* Merges templates together. Its own class to avoid circular dependencies.

*/

class TemplateMerger {

/**

* Merge an array of CopiedTemplates into one big CopiedTemplate. Other templates

* will be destroyed.

*

* @param templateList The list of templates to merge

* @param pivot The template to merge into. If not supplied, the first template

* in the list will be used.

*/

static merge(templateList, pivot) {

pivot = pivot !== null && pivot !== void 0 ? pivot : templateList[0];

while (templateList.length > 0) {

const template = templateList[0];

if (template !== pivot) {

if (template.node.getTarget().href !== pivot.node.getTarget().href) {

throw new Error("Attempted to merge incompatible templates.");

}

pivot.merge(template, { delete: true });

}

// Pop the pivot template out of the list.

templateList.shift();

}

}

}

/**

* Renders the panel used to merge multiple {{split article}} templates.

*

* @param type

* @param parentTemplate

* @param mergeButton

* @return A

element

*/

function renderMergePanel(type, parentTemplate, mergeButton) {

const mergePanel = new OO.ui.FieldsetLayout({

classes: ['cte-merge-panel'],

icon: 'tableMergeCells',

label: mw.msg('deputy.ante.merge.title')

});

unwrapWidget(mergePanel).style.padding = '16px';

unwrapWidget(mergePanel).style.zIndex = '20';

// Hide by default

mergePanel.toggle(false);

//