Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* PageCreator/code.js
* @file Displays information related to page's creator
* @author Eizen <dev.fandom.com/wiki/User_talk:Eizen>
* @license CC-BY-SA 3.0
* @external "mediawiki.util"
* @external "I18n-js"
*/
/**
* <pre>
* <em>Table of contents</em> <em>Summary</em>
* - Pseudo-enums Storage for PageCreator utility consts
* - Utility methods Helper methods for validation, etc.
* - Assembly methods Builder functions assembling on-page HTML
* - Main method Main functionality/app logic method
* - Setup methods Initialization methods for loading, setup
* </pre>
*/
/* jshint -W030, undef: true, unused: true, eqnull: true, laxbreak: true,
bitwise: false */
;(function (module, window, $, mw) {
"use strict";
// Prevent double loads and respect prior double load check formatting
if (!window || !$ || !mw || module.isLoaded || window.isPageCreatorLoaded) {
return;
}
module.isLoaded = true;
/****************************************************************************/
/* Pseudo-enums */
/****************************************************************************/
// Namespace protected properties
Object.defineProperties(this, {
/**
* @description This pseudo-enum used to initialize the script stores data
* related to the external dependencies and core modules required by the
* script. It consists of two properties. The former, a constant
* <code>object</code> called "ARTICLES," originally contained key/value
* pairs wherein the key was the specific name of the <code>mw.hook</code>
* and the value was the script's location for use by
* <code>importArticles.articles</code>. However, this system was eventually
* replaced in favor of an array of <code>object</code>s containing
* properties for hook, <code>window.dev</code> alias, and script for more
* efficient, readable loading of dependencies. The latter array, a constant
* array named <code>MODULES</code>, contains a listing of the core modules
* required for use by <code>mw.loader.using</code>.
* <br />
* <br />
* The key for the <code>ARTICLES</code> array entries is as follows:
* <pre>
* - DEV/WINDOW: The name and location of the <code>window</code> property
* - HOOK: The name of the <code>mw.hook</code> event
* - ARTICLE: The location of the script or stylesheet on the Dev wiki
* - TYPE: Either "script" for JS scripts or "style" for CSS stylesheets
* </pre>
*
* @readonly
* @enum {object}
*/
Dependencies: {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze({
ARTICLES: Object.freeze([
Object.freeze({
DEV: "i18n",
HOOK: "dev.i18n",
ARTICLE: "u:dev:MediaWiki:I18n-js/code.js",
TYPE: "script"
}),
Object.freeze({
DEV: null,
HOOK: null,
ARTICLE: "u:dev:MediaWiki:PageCreator.css",
TYPE: "style"
}),
]),
MODULES: Object.freeze([
"mediawiki.util",
]),
}),
},
/**
* @description This pseudo-enum of the <code>this</code> namespace object
* is used to store all CSS selectors in a single place in the event that
* one or more need to be changed. The formatting of the object literal key
* naming is type (id or class), location (placement, modal, content,
* preview), and either the name for ids or the type of element (div, span,
* etc.). This system was adopted, like many aspects of this script, from
* the author's MassEdit script.
*
* @readonly
* @enum {string}
*/
Selectors: {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze({
ID_LASTEDITED_WRAPPER: "lastEdited",
ID_PAGECREATOR_WRAPPER: "page-creator", // Legacy, bleh
ID_PAGECREATOR_USERPAGE: "pageCreator-userpage",
ID_PAGECREATOR_TALKPAGE: "pageCreator-talkpage",
ID_PAGECREATOR_CONTRIBUTIONS: "pageCreator-contribs",
ID_PAGECREATOR_TIMESTAMP: "pageCreator-timestamp",
CLASS_PAGEHEADER_WRAPPER: "page-header",
CLASS_PAGECREATOR_WRAPPER: "pageCreator",
CLASS_PAGECREATOR_IMAGE: "pageCreator-image",
CLASS_PAGECREATOR_LINK: "pageCreator-link",
CLASS_MW_USERTOOLLINKS: "mw-usertoollinks",
CLASS_PAGEHEADER_TITLE: "page-header__title",
CLASS_USERPAGE_BUTTON: "UserProfileActionButton",
}),
},
/**
* @description This pseudo-enum is used to store the <code>string</code>
* names of the various <code>WikipediaGlobal</code> (wg) variables required
* for use in the script. These are fetched within the body of the
* <code>this.preload</code> function via a <code>mw.config.get</code>
* invocation and stored in a namespace property named <code>globals</code>
* for subsequent usage. This approach replaces the deprecated approach
* previously used in the script of assuming the relevant wg variables exist
* as properties of the <code>window</code> object, an assumption that is
* discouraged in more recent versions of MediaWiki.
*
* @readonly
* @enum {string}
*/
Globals: {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze([
"wgAction",
"wgArticleId",
"wgArticlePath",
"wgFormattedNamespaces",
"wgNamespaceNumber",
"wgPageName"
]),
},
/**
* @description This pseudo-enum is used to store information related to the
* various custom config options available for use and their associated
* default values. Originally, custom config was specified by individual
* fields that were attached to the <code>window</code> object as properties
* in their own right. However, this system was eventually replaced by the
* bundling of all config options into a single <code>window</code> property
* called <code>pageCreatorConfig</code>. To ensure that both the updated
* and legacy config formats are respected and supported, each option housed
* in this pseudo-enum has both its <code>pageCreatorConfig</code> property
* name and legacy <code>window</code> property name so that the validator
* function <code>this.generateValidConfig</code> can check both formats for
* user config before assigning the default value if no config is found for
* that particular field.
*
* @readonly
* @enum {object}
*/
Config: {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze([
Object.freeze({
PROPERTY_NAME: "namespaces",
LEGACY_NAME: "pageCreatorNamespaces",
DEFAULT_VALUE: Object.freeze([0, 4, 8, 10, 14]),
}),
Object.freeze({
PROPERTY_NAME: "useUsernameAndLinks",
LEGACY_NAME: null,
DEFAULT_VALUE: true,
}),
Object.freeze({
PROPERTY_NAME: "useAvatar",
LEGACY_NAME: "pageCreatorAvatar",
DEFAULT_VALUE: true,
}),
Object.freeze({
PROPERTY_NAME: "useTimestamp",
LEGACY_NAME: "pageCreatorTimestamp",
DEFAULT_VALUE: true,
}),
Object.freeze({
PROPERTY_NAME: "useUTC",
LEGACY_NAME: "pageCreatorUTC",
DEFAULT_VALUE: true,
}),
Object.freeze({
PROPERTY_NAME: "useTimeago",
LEGACY_NAME: null,
DEFAULT_VALUE: false,
}),
Object.freeze({
PROPERTY_NAME: "avatarsize",
LEGACY_NAME: null,
DEFAULT_VALUE: 15,
}),
]),
},
/**
* @description This catchall pseudo-enum of the <code>this</code> namespace
* object is used to house assorted values of various data types that don't
* fit well into other pseudo-enums. It contains the I18n-js language cache
* version <code>number</code>, a <code>string</code> constant denoting the
* name of the script, another <code>string</code> for the name of the
* <code>mw.hook</code> event, and a <code>boolean</code> flag setting the
* default value for debug mode.
*
* @readonly
* @enum {string|boolean|number}
*/
Utility: {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze({
SCRIPT: "PageCreator",
HOOK_NAME: "PageCreator.render",
DEBUG: false,
CACHE_VERSION: 2,
}),
},
});
/****************************************************************************/
/* Utility methods */
/****************************************************************************/
/**
* @description As the name implies, this helper function capitalizes the
* first character of the input string and returns the altered, adjusted
* string. it is generally used in the dynamic construction of i18n messages
* in various assembly methods.
*
* @param {string} paramTarget - <code>string</code> to be capitalized
* @returns {string} - Capitalized <code>string</code>
*/
this.capitalize = function (paramTarget) {
return paramTarget.charAt(0).toUpperCase() + paramTarget.slice(1);
};
/**
* @description This helper method is used to check whether the target object
* is one of several types of <code>object</code>. It is most often used to
* determine if the target is an <code>array</code> or a straight-up
* <code>object</code>.
*
* @param {string} paramType - Either "Object" or "Array"
* @param {string} paramTarget - Target to check
* @returns {boolean} - Flag denoting the nature of the target
*/
this.isThisAn = function (paramType, paramTarget) {
return Object.prototype.toString.call(paramTarget) === "[object " +
this.capitalize.call(this, paramType.toLowerCase()) + "]";
};
/**
* @description This method is used to query the API for information
* pertaining to the initial revision of the page being viewed. In the event
* that the user wants to display the full range of information pertaining to
* the page creator, timestamp data is returned alongwith the userid value
* used to fetch and display the page creator's avatar in the appropriate
* <code>this.buildAvatar</code> method.
*
* @returns {object} - <code>$.Deferred</code> instance
*/
this.getRevisions = function () {
return $.ajax({
type: "GET",
url: mw.util.wikiScript("api"),
data: {
action: "query",
prop: "revisions",
titles: this.globals.wgPageName,
rvprop: "ids|timestamp|user|userid",
rvlimit: "1",
rvdir: "newer",
format: "json"
}
});
};
/**
* @description This helper method is used to assemble a wellformed user
* config <code>object</code> containing properties related to the display of
* certain functionality. The method validates any inputted user config
* conformant to either the modern or legacy formats and applies the default
* value specified in the <code>this.Config</code> pseudo-enum if no
* appropriate values are found.
* <br />
* <br />
* Modern config objects are formatted as follows:
* <br />
* <pre>
* window.pageCreatorConfig = {
* namespaces: [0, 4, 8, 10, 14],
* useUsernameAndLinks: false,
* useAvatar: false,
* useTimestamp: false,
* useUTC: false,
* useTimeago: false
* };
* </pre>
* <br />
* <br />
* Legacy config is formatted as follows:
* <br />
* <pre>
* window.pageCreatorNamespaces = [0, 4, 8, 10, 14];
* window.pageCreatorAvatar = false;
* window.pageCreatorTimestamp = false;
* window.pageCreatorUTC = false;
* </pre>
*
* @returns {object} results - A wellformed user config <code>object</code>
*/
this.generateValidConfig = function () {
// Declarations
var i, n, results, defaults, userConfig, options, input;
// Definitions
results = {};
defaults = this.Config;
userConfig = window.pageCreatorConfig || {};
if (this.Utility.DEBUG) {
window.console.log("window.pageCreatorConfig:", userConfig);
}
for (i = 0, n = defaults.length; i < n; i++) {
options = defaults[i];
// Check for both modern config and legacy window property approach
input = (userConfig[options.PROPERTY_NAME] != null)
? userConfig[options.PROPERTY_NAME]
: window[options.LEGACY_NAME];
if (this.Utility.DEBUG) {
window.console.log(
options.PROPERTY_NAME,
userConfig[options.PROPERTY_NAME],
window[options.LEGACY_NAME],
input
);
}
// Handle namespaces rulebreaker/check for type equivalence, apply default
results[options.PROPERTY_NAME] =
(options.PROPERTY_NAME === "namespaces" && input === "all")
? Object.keys(this.globals.wgFormattedNamespaces).map(Number)
: (typeof input === typeof options.DEFAULT_VALUE)
? input
: options.DEFAULT_VALUE;
}
if (this.Utility.DEBUG) {
window.console.log("results:", results);
}
return results;
};
/****************************************************************************/
/* Assembly methods */
/****************************************************************************/
/**
* @description This function is a simple recursive <code>string</code> HTML
* generator that makes use of <code>mw.html</code>'s assembly methods to
* construct wellformed HTML strings from a set of nested input arrays. This
* allows for a more readable means of producing proper HTML than the default
* <code>jQuery</code> approach or the hardcoded HTML <code>string</code>
* approach employed in earlier iterations of this script. Through the use of
* nested arrays, this function permits the laying out of parent/child DOM
* nodes in array form in a fashion similar to actual HTML, enhancing both
* readability and usability.
* <br />
* <br />
* Furthermore, as the <code>assembleElement</code> function returns a
* <code>string</code>, nested invocations of the method within parameter
* arrays is permitted, as evidenced in certain, more specialized assembly
* methods elsewhere in the script.
* <br />
* <br />
* An example of wellformed input is shown below:
* <br />
* <pre>
* this.assembleElement(
* ["div", {id: "foo-id", class: "foo-class"},
* ["button", {id: "bar-id", class: "bar-class"},
* "Button text",
* ],
* ["li", {class: "overflow"},
* ["a", {href: "#"},
* "Link text",
* ],
* ],
* ],
* );
* </pre>
*
* @param {Array<string>} paramArray - Wellformed array representing DOM nodes
* @returns {string} - Assembled <code>string</code> HTML
*/
this.assembleElement = function (paramArray) {
// Declarations
var type, attributes, counter, content;
// Make sure input argument is a well-formatted array
if (!this.isThisAn("Array", paramArray)) {
return this.assembleElement.call(this,
Array.prototype.slice.call(arguments));
}
// Definitions
counter = 0;
content = "";
type = paramArray[counter++];
// mw.html.element requires an object for the second param
attributes = (this.isThisAn("Object", paramArray[counter]))
? paramArray[counter++]
: {};
while (counter < paramArray.length) {
// Check if recursive assembly is required for another inner DOM element
content += (this.isThisAn("Array", paramArray[counter]))
? this.assembleElement(paramArray[counter++])
: paramArray[counter++];
}
return mw.html.element(type, attributes, new mw.html.Raw(content));
};
/**
* @description This builder function is used to assemble the optional infobar
* module related to the display of the page creator's avatar, one of the
* various bits of optional functionality available to users via the script's
* custom options config. This method makes use of a somewhat-antiquated "pure
* HTML" approach to handle cases of nonexistent/corrupted avatar URLs and/or
* failed query requests. The address linking to the page creator's avatar is
* contained in a <code>object</code> tag; if this image is not found, the
* inner contained <code>img</code> tag displays the default
* <code>Avatar.jpg</code> image shown for anons and users who have not set an
* avatar. An alternative, JS approach that might be implemented in future
* would involve using jQuery or the vanilla <code>onerror</code>
* <code>img</code> attribute to load the default avatar if an error is
* encountered in the display of the actual avatar.
*
* @param {string} paramUserId - The page creator's userid (data.userid)
* @returns {string} - Assembled modular <code>string</code> HTML
*/
this.buildAvatar = function (paramUserId) {
return this.assembleElement(
["object", {
class: this.Selectors.CLASS_PAGECREATOR_IMAGE,
data: "https://services.fandom.com/user-avatar/user/" + paramUserId +
"/avatar",
type: "image/png",
width: this.config.avatarsize,
height: this.config.avatarsize,
},
["img", {
class: this.Selectors.CLASS_PAGECREATOR_IMAGE,
src: "https://vignette.wikia.nocookie.net/messaging/" +
"images/1/19/Avatar.jpg/revision/latest",
width: this.config.avatarsize,
height: this.config.avatarsize,
}]
]
);
};
/**
* @description This builder function is responsible for assembling the
* optional-but-not-actually-optional <code>mw-usertoollinks</code>
* parenthetical section containing links to the page creator's talk page and
* associated contributions page. This section, though treated like one of the
* optional infobar modules like avatar and timestamp, will always be added to
* the infobar framework; if not built by <code>this.main</code> for some
* reason, the builder method will be invoked by
* <code>this.buildFramework</code> as a precaution to ensure that these links
* are always displayed.
*
* @param {string} paramUser - The page creator's <code>string</code> username
* @returns {string} - Assembled modular <code>string</code> HTML
*/
this.buildUserToolLinks = function (paramUser) {
return this.assembleElement(
["span", {class: this.Selectors.CLASS_MW_USERTOOLLINKS},
"(",
["a", {
id: this.Selectors.ID_PAGECREATOR_TALKPAGE,
class: this.Selectors.CLASS_PAGECREATOR_LINK,
href: mw.util.getUrl(this.globals.wgFormattedNamespaces[3] + ":" +
paramUser),
title: this.i18n.msg("talk").plain()
},
this.i18n.msg("talk").plain(),
],
" | ",
["a", {
id: this.Selectors.ID_PAGECREATOR_CONTRIBUTIONS,
class: this.Selectors.CLASS_PAGECREATOR_LINK,
href: mw.util.getUrl("Special:Contributions/" + paramUser),
title: this.i18n.msg("contribs").plain()
},
this.i18n.msg("contribs").plain(),
],
")",
]
);
};
/**
* @description The <code>buildTimeago</code> function is a helper method that
* is little more than a UCP-friendly adapation of the standard
* <code>$.timeago.inWords</code> function provided by default. For whatever
* reason, the UCP version of this function includes a conditional check for
* time values and only provides a fuzzy date for values smaller than 30 days.
* To work around this, this function provides the same functionality without
* the conditional, a method used in LastEdit to likewise circumvent this
* restriction.
*
* @param {string} paramDate The date timestamp to be fuzzyified
* @returns {string} Fuzzy date to be displayed as first revision link text
*/
this.buildTimeago = function (paramDate) {
// Declarations
var t, e, r, a, i, n, o;
// Definitions 1
t = new Date().getTime() - $.timeago.parse(paramDate).getTime();
e = false;
// Determine if system clock is not synced (10 minutes from now)
$.timeago.settings.allowFuture && (t < 0 && (e = !0), t = Math.abs(t));
// Definitions 2
r = t / 1e3;
a = r / 60;
i = a / 60;
n = i / 24;
o = n / 365;
// Helper function for mw.message fetching
function u(t, r) {
return mw.message(e
? "timeago-" + t + "-from-now"
: "timeago-" + t, r
).text();
}
return (
r < 45 && u("second", Math.round(r)) ||
r < 90 && u("minute", 1) ||
a < 45 && u("minute", Math.round(a)) ||
a < 90 && u("hour", 1) ||
i < 24 && u("hour", Math.round(i)) ||
i < 48 && u("day", 1) ||
n < 30 && u("day", Math.floor(n)) ||
n < 60 && u("month", 1) ||
n < 365 && u("month", Math.floor(n / 30)) ||
o < 2 && u("year", 1) ||
u("year", Math.floor(o))
);
};
/**
* @description This builder function handles the assembly of the optional
* timestamp link, a module of the infobar framework that displays either a
* <code>$.timeago</code>-generated estimation of the time since the page's
* initial revision or the exact time and date of creation depending on the
* user's preference. On UCP wikis, the exact time/date combo is always
* displayed, as <code>$.timeago</code> always returns <code>undefined</code>
* when invoked with a well-formed timestamp. Regardless of which version is
* displayed, the timestamp will also serve as a link to the initial revision
* of the page.
* <br />
* <br />
* As of the 07/10/2021 update, the <code>$.timeago</code> issue has been
* worked around via the helper function <code>this.buildTimeago</code>, which
* is little more than a modified version of the standard
* <code>$.timeago.inWords</code> function provided by default that removes
* the inexplicable restriction preventing values over 30 days from being
* rendered as fuzzy dates.
*
* @param {string} paramDate - Formatted timestamp for display
* @param {string} paramRevision - Initial revision id/url
* @returns {string} - Assembled modular <code>string</code> HTML
*/
this.buildTimestamp = function (paramDate, paramRevision) {
// Declarations
var shouldUseTimeago, linkText, revisionLink;
// Repaired $.timeago may now be used on UCP wikis
shouldUseTimeago = this.config.useTimeago;
if (this.Utility.DEBUG) {
window.console.log("shouldUseTimeago:", shouldUseTimeago);
}
// Definitions
linkText = (shouldUseTimeago) ? this.buildTimeago(paramDate) : paramDate;
revisionLink = this.assembleElement(
["a", {
id: this.Selectors.ID_PAGECREATOR_TIMESTAMP,
class: this.Selectors.CLASS_PAGECREATOR_LINK,
href: this.globals.wgArticlePath.replace(/\$1/g, paramRevision),
title: linkText,
},
linkText
]
);
if (this.Utility.DEBUG) {
window.console.log("linkText:", linkText);
}
// Non-timeago estimations are prefixed with "on"
return (shouldUseTimeago)
? revisionLink
: this.i18n.msg("on").escape().replace(/\$2/g, revisionLink);
};
/**
* @description As the name implies, this builder function is used to build
* the basic framework of the PageCreator infobar thingie to which are added
* as modules various optional pieces like the avatar and timestamp depending
* on the preferences of the user. It follows the same basic layout as its
* older cousin, LastEdited, though it manifests a few basic formatting
* differences. The only module it will always build is the default
* <code>mw-usertoollinks</code> parenthetical section containing links to the
* page creator's talk page and associated contributions page; if this module
* is somehow not built by <code>this.main</code>, the framework builder will
* construct it and add it regardless.
*
* @param {string} paramUser - Username of the page creator
* @param {object} paramArgs - <code>object</code> containing modules
* @returns {string} - Formatted <code>string</code> HTML for appending, etc.
*/
this.buildFramework = function (paramUser, paramArgs) {
return this.assembleElement(
["div", {
id: this.Selectors.ID_PAGECREATOR_WRAPPER,
class: this.Selectors.CLASS_PAGECREATOR_WRAPPER,
},
((paramArgs.links)
? this.i18n.msg("main").escape().replace(/\$1/g,
((paramArgs.hasOwnProperty("avatar"))
? paramArgs.avatar + " "
: "") +
this.assembleElement(
["a", {
id: this.Selectors.ID_PAGECREATOR_USERPAGE,
class: this.Selectors.CLASS_PAGECREATOR_LINK,
href: mw.util.getUrl(this.globals.wgFormattedNamespaces[2] +
":" + paramUser),
title: paramUser,
},
paramUser
]
)
) +
" " +
(paramArgs.links || this.buildUserToolLinks(paramUser))
: this.i18n.msg("created").escape()
),
((paramArgs.hasOwnProperty("timestamp"))
? " " + paramArgs.timestamp
: "")
]
);
};
/****************************************************************************/
/* Main method */
/****************************************************************************/
/**
* @description The main method is invoked as the associated callback handler
* that is called once <code>this.getRevisions</code> successfully resolves
* with data pertaining to the earliest revision of the viewed page and its
* initial author, the page creator. Using this data passed as an argument,
* the <code>this.main</code> function coordinates the validation of this
* data and the construction of the page header infobar to display the desired
* information indicated by the user's <code>window</code> config object. The
* method is responsible for formatting the timestamp (if desired), building
* the optional avatar, and appending the completed result to the page header
* atop LastEdited (if loaded).
*
* @param {object} paramData - Returned from <code>this.getRevisions</code>
* @returns {void}
*/
this.main = function (paramData) {
// Declarations
var data, revisionURL, infobar, time, formattedDate, args;
// Definitions
args = {};
data = paramData.query.pages[this.globals.wgArticleId].revisions[0];
// Link to first revision page
revisionURL = this.globals.wgPageName + "?" + $.param({
oldid: data.revid,
});
if (this.Utility.DEBUG) {
window.console.log(revisionURL);
}
// Build parenthetical links section if selected
if (this.config.useUsernameAndLinks) {
args.links = this.buildUserToolLinks(data.user);
}
// Avatar is optional, now displayed by default to match LastEdited
if (this.config.useAvatar && this.config.useUsernameAndLinks) {
args.avatar = this.buildAvatar(data.userid);
}
// Optional timestamp includes link to first revision
if (this.config.useTimestamp || !this.config.useUsernameAndLinks) {
// Timestamp in either local or UTC time
time = new Date(data.timestamp)[
(this.config.useUTC) ? "toUTCString" : "toString"
]();
// Formatted creation date
formattedDate = (this.config.useUTC)
? time.slice(0, 3) + ", " + time.slice(4, 16) + ", " +
time.slice(17, 25) + " (" + time.slice(26) + ")"
: time.slice(0, 3) + ", " + time.slice(4, 15) + ", " +
time.slice(16, 24) + " " + time.slice(34);
if (this.Utility.DEBUG) {
window.console.log(time, formattedDate);
}
// this.buildTimestamp handles use of $.timeago, not this.main
args.timestamp = this.buildTimestamp(formattedDate, revisionURL);
}
// Depending on user's preferences, args may be of length 1
infobar = this.buildFramework(data.user, args);
// PageCreator should always precede LastEdited if that script is loaded
if ($("#" + this.Selectors.ID_LASTEDITED_WRAPPER).length) {
$(infobar).insertBefore("#" + this.Selectors.ID_LASTEDITED_WRAPPER);
} else {
$(infobar).insertAfter(
"." + this.Selectors.CLASS_PAGEHEADER_WRAPPER + " " +
"." + this.Selectors.CLASS_PAGEHEADER_TITLE + ", " +
"." + this.Selectors.CLASS_USERPAGE_BUTTON
);
}
// Fire render hook once added - legacy
mw.hook(this.Utility.HOOK_NAME).fire(
$("#" + this.Selectors.ID_PAGECREATOR_WRAPPER));
};
/****************************************************************************/
/* Setup methods */
/****************************************************************************/
/**
* @description The <code>this.init</code> initialization method is called
* once all external dependencies and ResourceLoader modules have been loaded.
* The method is responsible for validating the user's config (if applicable),
* setting the i18n language for the script, and defining a new protected
* <code>module</code> property <code>exports</code> containing exposed public
* methods for post-load invocation. At present, the only method publicly
* accessible is <code>observeScript</code>, which allows the user to view the
* layout of the script and the namespace object's various properties via a
* <code>console.dir</code> invocation. The method returns after firing the
* PageCreator <code>mw.hook</code> event, querying the API for information
* pertaining to the page's earliest revision, and attaching
* <code>this.main</code> as a listener callback.
*
* @param {undefined|function} paramRequire - Either function or undefined
* @param {object} paramLang - I18n-js data content
* @returns {void}
*/
this.init = function (paramRequire, paramLang) {
// Validate user-input config elements
this.config = Object.freeze(this.generateValidConfig());
// Fetch, define, and cache globals
this.globals = Object.freeze(mw.config.get(this.Globals));
if (
this.globals.wgNamespaceNumber === -1 ||
this.globals.wgArticleId === 0 ||
this.globals.wgAction === "edit" ||
$("#" + this.Selectors.ID_PAGECREATOR_WRAPPER).length ||
$.inArray(this.globals.wgNamespaceNumber, this.config.namespaces) === -1
) {
return;
}
// Add i18n data as local property
(this.i18n = paramLang).useContentLang();
// Expose public methods for external debugging
Object.defineProperty(module, "exports", {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze({
observeScript: window.console.dir.bind(this, this),
})
});
// Get data related to initial revision and its author
this.getRevisions()
.then(this.main.bind(this))
.fail(window.console.error.bind(null, this.Utility.SCRIPT));
};
/**
* @description Over its lifetime, the <code>load</code> function has
* undergone a number of changes and evolutions to account for the transition
* from Wikia's fork of MediaWiki 1.19 to the current MediaWiki 1.33 UCP
* platform. During the transition period in which <code>importArticles</code>
* only worked for legacy wikis, the function made use of a fallback approach
* that created new ResourceLoader modules from external dependencies,
* permitting an alternate means of loading required resources. However, with
* the reinstitution of <code>importArticles</code> functionality to the UCP,
* this approach was scrapped and a simpler implementation employed instead to
* load Dev resources via several <code>importArticles</code> calls.
*
* @param {object} paramDeferred - <code>$.Deferred</code> instance
* @returns {void}
*/
this.load = function (paramDeferred) {
// Declarations
var articles, numArticles, unloadedScripts, isLoaded;
// Definitions
articles = this.Dependencies.ARTICLES;
numArticles = articles.length;
/**
* @description To collate a listing of unloaded scripts to import en bulk,
* three chained higher-order looping functions are called in conjunction.
* In addition to assembly a list of unloaded scripts with hooks, these
* function callbacks likewise address and handle cases of already-loaded
* scripts with hooks, unloaded scripts without hooks, and CSS stylesheets.
* The higher-order functions called are a pair of
* <code>Array.prototype.filter</code> invocations and a final
* <code>Array.prototype.map</code> call.
*/
unloadedScripts = articles.filter(function (current) {
// Determine if the script has been loaded
isLoaded = Boolean(
(current.DEV && window.dev.hasOwnProperty(current.DEV)) ||
(current.WINDOW && window.hasOwnProperty(current.WINDOW))
);
// If script has been loaded and has a dedicated hook
if (isLoaded && current.HOOK) {
if (this.Utility.DEBUG) {
window.console.log("isLoaded", current.ARTICLE);
}
// Use progress as handler
// (coerce to task from microtask w/ setTimeout of 0)
mw.hook(current.HOOK).add(window.setTimeout.bind(null,
paramDeferred.notify.bind(null, current.ARTICLE)
));
}
// Pass along unloaded scripts
return !isLoaded;
}.bind(this)).filter(function (current) {
// Unloaded scripts with dedicated hooks are passed along to map
if (current.TYPE === "script" && current.HOOK) {
// Use progress as handler
// (coerce to task from microtask w/ setTimeout of 0)
return mw.hook(current.HOOK).add(window.setTimeout.bind(null,
paramDeferred.notify.bind(null, current.ARTICLE)
));
}
// Unloaded scripts w/o hooks and stylesheets imported here
window.importArticle({
type: current.TYPE,
article: current.ARTICLE
}).then(paramDeferred.notify.bind(null, current.ARTICLE));
}.bind(this)).map(function (current) {
return current.ARTICLE;
});
// Unloaded scripts with hooks are imported en bulk here
if (unloadedScripts.length) {
if (this.Utility.DEBUG) {
window.console.log("unloadedScripts", unloadedScripts);
}
window.importArticles({
type: "script",
articles: unloadedScripts
});
}
/**
* @description The <code>$.Deferred.progress</code> handler is responsible
* for determining whether all required external Dev dependencies have been
* loaded, thus allowing for the resolution of the <code>$.Deferred</code>
* and permitting execution to continue to <code>this.init</code>.
*/
paramDeferred.progress(function (paramArticle) {
if (this.Utility.DEBUG) {
window.console.log(paramArticle, numArticles);
}
if (--numArticles === 0) {
window.dev.i18n.loadMessages(this.Utility.SCRIPT, {
cacheVersion: this.Utility.CACHE_VERSION,
}).then(paramDeferred.resolve).fail(paramDeferred.reject);
}
}.bind(this));
};
// Coordinate loading of all relevant dependencies
$.when(
mw.loader.using(this.Dependencies.MODULES),
new $.Deferred(this.load.bind(this)).promise())
.then(this.init.bind(this))
.fail(window.console.error.bind(null, this.Utility.SCRIPT));
}.call(Object.create(null), (this.dev = this.dev || {}).pageCreator =
this.dev.pageCreator || {}, this, this.jQuery, this.mediaWiki));