User:MSchottlender-WMF/WhoWroteThatGadget.js

(function{function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})({1:[function(require,module,exports){ "use strict";

Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0;

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

/** * An activation singleton, responsible for activating and attaching the * button that activates the system when it is applicable. * * @class */ var ActivationSingleton = /*#__PURE__*/ function { /**  * Initialize if it is the first time, and cache the * initialization for the next times. */ function ActivationSingleton { _classCallCheck(this, ActivationSingleton);

if (!ActivationSingleton.instance) { this.initialized = false; this.link = null; this.namespace = null; this.mainPage = false; this.$contentWrapper = null; this.$originalContent = null; this.lang = 'en'; this.translations = {}; ActivationSingleton.instance = this; }

return ActivationSingleton.instance; } /**   * Check if the current page is valid * for the system to display. *  * @return {boolean} Page is valid */

_createClass(ActivationSingleton, [{   key: "isValidPage",    value: function isValidPage {      return !!( // Initialization did not already happen !this.initialized && // Has the needed parser content this.$contentWrapper.length && // Is in the main namespace this.namespace === '' && // Is not main page !this.mainPage);   }    /**     * Set the class properties.     * This is separated from initialization for cleanliness     * and to make sure tests can run with different parameters.     *     * @param {jQuery} $content Page content     * @param {Object} config Configuration options     * @param {string} [config.lang="en"] User interface language     * @param {Object} [config.translations={}] Object with all translations     *  organized by language key with data of translation key/value pairs.     * @param {string} [config.namespace] Page namespace. Falls back to reading     *  directly from mw.config     * @param {string} [config.mainPage] Whether the current page is the main page     *  of the wiki. Falls back to reading directly from mw.config     */

}, {   key: "setProperties", value: function setProperties($content, config) { this.$contentWrapper = $content; this.$originalContent = this.$contentWrapper.clone; this.lang = config.lang || 'en'; this.namespace = config.namespace || ''; this.mainPage = !!config.mainPage; this.translations = config.translations || {}; }   /**     * Initialize the process *    * @param {jQuery} $content Page content * @param {Object} config Configuration options */

}, {   key: "initialize", value: function initialize($content, config) { this.setProperties($content, config);

if (!this.isValidPage) { return; } // This validation is for tests, where // mw is not defined. We don't care to test // whether messages are set properly, since that // has its own tests in the mw.messsages bundle

if (window.mw) { // Load all messages mw.messages.set(Object.assign({}, // Manually create fallback on English this.translations.en, this.translations[this.lang])); // Add a portlet link to 'tools'

this.link = mw.util.addPortletLink('p-tb', '#', mw.msg('ext-whowrotethat-activation-link'), 't-whowrotethat', mw.msg('ext-whowrotethat-activation-link-tooltip')); this.initialized = true; }   }    /**     * Load the required dependencies for the full script *    * @return {jQuery.Promise} Promise that is resolved when * all dependencies are ready and loaded. */

}, {   key: "loadDependencies", value: function loadDependencies { return $.when($.ready, // jQuery's document.ready     mw.loader.using([// MediaWiki dependencies 'oojs-ui', 'oojs-ui.styles.icons-user', 'oojs-ui.styles.icons-interactions'])); }   /**     * Get the jQuery object representing the activation button *    * @return {jQuery} Activation button */

}, {   key: "getButton", value: function getButton { return $(this.link); }   /**     * Get the content wrapper for the current page *    * @return {jQuery} Content wrapper */

}, {   key: "getContentWrapper", value: function getContentWrapper { return this.$contentWrapper; }   /**     * Get the original content of the article *    * @return {jQuery} Original content */

}, {   key: "getOriginalContent", value: function getOriginalContent { return this.$originalContent; } }]);

return ActivationSingleton; }; // Initialize the singleton

var activationInstance = new ActivationSingleton; var _default = activationInstance; exports["default"] = _default;

},{}],2:[function(require,module,exports){ "use strict";

Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0;

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

/** * Interface to the [WikiWho](https://www.wikiwho.net/) WhoColor API. * * @class */ var Api = /*#__PURE__*/ function { /** * @param {Object} config * @cfg config.url The WikiWho base URL. * @constructor */ function Api { var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

_classCallCheck(this, Api);

this.url = config.url || ''; // Remove trailing slash.

if (this.url && this.url.slice(-1) === '/') { this.url = this.url.slice(0, -1); }

this.results = null; } /**   * Get the value of a parameter from the given URL query string. *  * @protected * @param {string} querystring URL query string * @param {string} param Parameter name * @return {string|null} Parameter value; null if not found */

_createClass(Api, [{   key: "getQueryParameter",    value: function getQueryParameter(querystring, param) {      var urlParams, regex, results;

if (querystring === '') { return null; }

try { urlParams = new URLSearchParams(querystring); return urlParams.get(param); } catch (err) { // Fallback for IE and Edge // eslint-disable-next-line no-useless-escape param = param.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); regex = new RegExp("[?&]".concat(param, "=([^&#]*)")); results = regex.exec(querystring); return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); }   }    /**     * Get a WhoColor API URL based on a given wiki URL. *    * @param  {string} wikiUrl URL of the wiki page that we want to analyze. * @return {string} Ajax URL for the data from WhoColor. */

}, {   key: "getAjaxURL", value: function getAjaxURL(wikiUrl) { var parts, oldId, title, lang, matches, queryString, linkNode = document.createElement('a'); linkNode.href = wikiUrl; queryString = linkNode.search; title = this.getQueryParameter(queryString, 'title');

if (title) { // URL is like: https://en.wikipedia.org/w/index.php?title=Foo&oldid=123 matches = linkNode.hostname.match(/([a-z]+)\.wiki.*/i); lang = matches[1]; } else { // URL is like: https://en.wikipedia.org/wiki/Foo matches = wikiUrl.match(/:\/\/([a-z]+).wikipedia.org\/wiki\/([^#?]*)/i); lang = matches[1]; title = matches[2]; }

parts = [this.url, lang, 'whocolor/v1.0.0-beta', title]; // Add oldid if it's present.

oldId = this.getQueryParameter(queryString, 'oldid');

if (oldId) { parts.push(oldId); } // Compile the full URL.

return parts.join('/') + '/'; }   /**     * Extract token and editor IDs from a WikiWho span element with `id='token-X'` and * `class='token-editor-Y'` attributes. *    * @param {HTMLElement} element * @return {Object} An object with two parameters: tokenId and editorId (string). */

}, {   key: "getIdsFromElement", value: function getIdsFromElement(element) { var out = { tokenId: false, editorId: false },         tokenMatches = element.id.match(/token-(\d+)/), editorMatches = element.className.match(/token-editor-([^\s]+)/);

if (tokenMatches && tokenMatches[1]) { out.tokenId = parseInt(tokenMatches[1]); }

if (editorMatches && editorMatches[1]) { out.editorId = editorMatches[1]; }

return out; }   /**     * Get the WikiWho replacement for `.mw-parser-output` HTML. * @return {string} */

}, {   key: "getReplacementHtml", value: function getReplacementHtml { return this.results.extended_html; }   /**     * Get user and revision information for a given token. *    * @param {number} tokenId * @return {{revisionId: *, score: *, userId: *, username: *, revisionTime: *}|boolean} Object * that represents the token info or false if a token wasn't found. */

}, {   key: "getTokenInfo", value: function getTokenInfo(tokenId) { var revId, revision, username, score; // Get the token information. results.tokens structure: // [ [ conflict_score, str, o_rev_id, in, out, editor/class_name, age ], ... ]     // e.g. Array(7) [ 0, "indicate", 769691068, [], [], "18201938", 76652371.587203 ]

var token = this.results.tokens[tokenId];

if (!token) { return false; } // Get revision information. results.revisions structure: // { rev_id: [ timestamp, parent_rev_id, user_id, editor_name ], ... }     // e.g. Array(4) [ "2017-03-11T02:12:47Z", 769315355, "18201938", "Biogeographist" ]

revId = token[2]; revision = this.results.revisions[revId]; username = revision[3]; // Get the user's edit score (percentage of content edited). // results.present_editors structure: // [ [ username, user_id, score ], ... ]

for (var i = 0; i < this.results.present_editors.length; i++) { if (this.results.present_editors[i][0] === username) { score = parseFloat(this.results.present_editors[i][2]).toFixed(1); break; }     } // Put it all together.

return { username: username, userId: token[5], revisionId: revId, revisionTime: new Date(revision[0]), score: score };   }    /**     * Get the WikiWho data for a given wiki page. *    * @param {string} wikiUrl URL of the wiki page that we want to analyze. * @return {Promise} A promise that resolves when the data is ready, * or rejects if there was an error. */

}, {   key: "getData", value: function getData(wikiUrl) { var api = this;

if (this.resultsPromise) { return this.resultsPromise; }

this.resultsPromise = $.getJSON(this.getAjaxURL(wikiUrl)).then(function (result) {       // Handle error response.        if (!result.success && result.info) {          // The API gives us an error message, but we don't use it because it's only          // in English.          return $.Deferred.reject('ext-whowrotethat-api-error-wikiwho-generic');        } // Store all results.

api.results = result; });     return this.resultsPromise;    }  }]);

return Api; };

var _default = Api; exports["default"] = _default;

},{}],3:[function(require,module,exports){ "use strict";

Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0;

var _config = _interopRequireDefault(require("./config"));

var _Api = _interopRequireDefault(require("./Api"));

var _InfoBarWidget = _interopRequireDefault(require("./InfoBarWidget"));

var _ActivationSingleton = _interopRequireDefault(require("./ActivationSingleton"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

/** * Application class, responsible for running, activating, * and toggling the entire application. * * @class */ var App = /*#__PURE__*/ function { /**  * Only instantiate once, so the initialization doesn't run again * even if this is called on multiple clicks/calls *  * @constructor * @param {App} A class instance */ function App { _classCallCheck(this, App);

// Instantiate only once if (!App.instance) { this.initialized = false; App.instance = this; }

return App.instance; } /**   * Initialize the application */

_createClass(App, [{   key: "initialize",    value: function initialize {      // Only initialize once      if (this.initialized) {        return;      }

this.widget = new _InfoBarWidget["default"]({       state: 'pending'      }); this.api = new _Api["default"]({       url: _config["default"].wikiWhoUrl      }); this.widget.setState('pending'); // Attach widget

if ($('body').hasClass('skin-timeless')) { $('#mw-content-wrapper').prepend(this.widget.$element); } else { $('#content').prepend(this.widget.$element); } // Attach events

this.widget.on('close', this.onWidgetClose.bind(this)); this.initialized = true; }   /**     * Run the application. * This performs the initialization for the first time * and then can do the toggling when and if the activation * button is clicked multiple times, toggling the state * on and off and on again. */

}, {   key: "start", value: function start { var self = this; this.initialize; this.widget.toggle(true); this.api.getData(window.location.href).then( // Success handler.     function  {        // Insert modified HTML.        $('.mw-parser-output').html(self.api.getReplacementHtml); // Highlight when hover a user's contributions.        // @TODO This is just testing code and should be        // replaced by the proper behaviour.

$('.mw-parser-output .editor-token').on('mouseenter', function {          var ids = self.api.getIdsFromElement(this); // Activate all this user's contribution spans.

$('.token-editor-' + ids.editorId).addClass('active'); // Information for popup. // eslint-disable-next-line no-console

console.log(self.api.getTokenInfo(ids.tokenId)); }).on('mouseleave', function { // Deactivate all spans. $('.mw-parser-output .editor-token').removeClass('active'); });       self.widget.setState('ready');      }, // Error handler.      function (errorCode) {        self.widget.setState('err');        self.widget.setErrorMessage(errorCode);      }); }   /**     * Repond to the close event that the widget emits. * Toggle the application off, and replace the content * to the original dom of the original article. */

}, {   key: "onWidgetClose", value: function onWidgetClose { // Close button; revert back to the original content _ActivationSingleton["default"].getContentWrapper.replaceWith(_ActivationSingleton["default"].getOriginalContent); // Hide the widget

this.widget.toggle(false); } }]);

return App; }; // This should be able to load with 'require'

module.exports = App; var _default = App; exports["default"] = _default;

},{"./ActivationSingleton":1,"./Api":2,"./InfoBarWidget":4,"./config":6}],4:[function(require,module,exports){ "use strict";

Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0;

/** * @class * @param {Object} config Configuration options. * @param {string} config.state The state of the infobar; see {@link InfoBarWidget#setState}. * @constructor */ var InfoBarWidget = function InfoBarWidget { var _this = this;

var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Parent constructor OO.ui.ButtonWidget.parent.call(this, config); OO.ui.mixin.IconElement.call(this, config); OO.ui.mixin.LabelElement.call(this, config); OO.ui.mixin.TitledElement.call(this, config); OO.ui.mixin.FlaggedElement.call(this, config); this.closeIcon = new OO.ui.IconWidget({   icon: 'clear',    flags: ['invert'],    classes: ['ext-wwt-infoBarWidget-close']  }); this.userInfoLabel = new OO.ui.LabelWidget({   label: mw.msg('ext-whowrotethat-ready-general'),    classes: ['ext-wwt-infoBarWidget-info']  }); this.$pendingAnimation = $(' ').addClass('ext-wwt-infoBarWidget-spinner').append($(' ').addClass('ext-wwt-infoBarWidget-spinner-bounce')); // Set properties

this.setState(config.state || 'pending'); this.setLabel($(' ').append(mw.msg('ext-whowrotethat-state-pending')).contents); // Close event

this.closeIcon.$element.on('click', function {    return _this.emit('close');  }); // Initialize

this.$element.addClass('ext-wwt-infoBarWidget').append(this.$pendingAnimation, this.$icon, this.$label, this.userInfoLabel.$element, this.closeIcon.$element); }; /* Setup */

OO.inheritClass(InfoBarWidget, OO.ui.Widget); OO.mixinClass(InfoBarWidget, OO.ui.mixin.IconElement); OO.mixinClass(InfoBarWidget, OO.ui.mixin.LabelElement); OO.mixinClass(InfoBarWidget, OO.ui.mixin.TitledElement); OO.mixinClass(InfoBarWidget, OO.ui.mixin.FlaggedElement); /** * Define legal states for the widget * * @type {Array} */

InfoBarWidget["static"].legalFlags = ['pending', 'ready', 'err']; /** * Change the state of the widget * * @param {string} state Widget state; 'pending', 'ready' or 'error' */

InfoBarWidget.prototype.setState = function (state) { var flags = {};

if (this.state !== state) { this.constructor["static"].legalFlags.forEach(function (flag) {     flags[flag] = flag === state;    }); flags.invert = true; this.setFlags(flags);

if (state === 'ready') { this.setLabel($(' ').append(mw.msg('ext-whowrotethat-ready-title')).contents); this.userInfoLabel.setLabel($(' ').append(mw.msg('ext-whowrotethat-ready-general')).contents); this.setIcon('userAvatar'); } else if (state === 'pending') { this.setIcon(''); } else { this.setIcon('error'); this.setErrorMessage; }

this.$pendingAnimation.toggle(state === 'pending'); this.userInfoLabel.toggle(state === 'ready'); this.closeIcon.toggle(state !== 'pending'); this.state = state; } }; /** * Set an error with a specific label * * @param {string} message Error label message key */

InfoBarWidget.prototype.setErrorMessage = function { var message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'ext-whowrotethat-state-error-generic'; this.setLabel(mw.msg('ext-whowrotethat-state-error', mw.msg(message))); };

var _default = InfoBarWidget; exports["default"] = _default;

},{}],5:[function(require,module,exports){ "use strict";

Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0;

/** * A popup meant to display as a welcome message after the * extension is installed for the first time. * @class * * @constructor * @param {Object} [config={}] Configuration options */ var WelcomePopupWidget = function WelcomePopupWidget { var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var $content = $(' ').addClass('ext-wwt-welcomePopupWidget-content'); // Parent constructor

WelcomePopupWidget.parent.call(this, Object.assign({ padded: true, autoClose: true, position: 'after', $content: $content }, config)); this.dismissButton = new OO.ui.ButtonWidget({   flags: ['primary', 'progressive'],    label: mw.msg('ext-whowrotethat-tour-welcome-dismiss')  }); $content.append($(' ').addClass('ext-wwt-welcomePopupWidget-title').append(mw.msg('ext-whowrotethat-tour-welcome-title')), $(' ').addClass('ext-wwt-welcomePopupWidget-description').append(mw.msg('ext-whowrotethat-tour-welcome-description')), $(' ').addClass('ext-wwt-welcomePopupWidget-button').append(this.dismissButton.$element)); // Events

this.dismissButton.connect(this, {   click: ['emit', 'dismiss']  }); // Initialize

this.$element.addClass('ext-wwt-welcomePopupWidget'); }; /* Setup */

OO.inheritClass(WelcomePopupWidget, OO.ui.PopupWidget); module.exports = WelcomePopupWidget; var _default = WelcomePopupWidget; exports["default"] = _default;

},{}],6:[function(require,module,exports){ "use strict";

Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var config = { wikiWhoUrl: 'https://www.wikiwho.net/' }; var _default = config; exports["default"] = _default;

},{}],7:[function(require,module,exports){ "use strict";

var _ActivationSingleton = _interopRequireDefault(require("../ActivationSingleton"));

var _languages = _interopRequireDefault(require("../../temp/languages"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

// This is generated during the build process (function {  var onActivateButtonClick = function onActivateButtonClick(e) {    _ActivationSingleton["default"].loadDependencies.then(function  { // Only load after dependencies are loaded var App = require('../App'), a = new App;

a.start; });

e.preventDefault; return false; },     loadWhoWroteThat = function loadWhoWroteThat { _ActivationSingleton["default"].initialize($('.mw-parser-output'), {     lang: $('html').attr('lang'),      translations: _languages["default"],      namespace: mw.config.get('wgCanonicalNamespace'),      mainPage: mw.config.get('wgIsMainPage')    });

_ActivationSingleton["default"].getButton.on('click', onActivateButtonClick); };

$(document).ready(function {    var welcomeTourSeen = window.localStorage.getItem('WelcomeTourSeen');    loadWhoWroteThat;

if (welcomeTourSeen) { // Do not show the tour if it was previously dismissed return; }

$.when($.ready, mw.loader.using(['oojs-ui', 'mediawiki.jqueryMsg'])).then(function {      var WelcomePopupWidget = require('../WelcomePopupWidget'),          welcome = new WelcomePopupWidget({ $floatableContainer: $('#t-whowrotethat') });

welcome.on('dismiss', function {        // This is a gadget that may also work for        // non logged in users, so we can't trust        // the mw.user.options storage.        // Store the fact that the tour was        // dismissed in localStorage        window.localStorage.setItem('WelcomeTourSeen', true); // Hide the popup

welcome.toggle(false); }); // Show the popup

$('body').append(welcome.$element); welcome.toggle(true); }); }); // For debugging purposes, export methods to the window global

window.wwtDebug = { resetWelcomePopup: function resetWelcomePopup { window.localStorage.removeItem('WelcomeTourSeen'); window.console.log('WhoWroteThat Extension: Welcome tour reset. Please refresh.'); } }; });

},{"../../temp/languages":8,"../ActivationSingleton":1,"../App":3,"../WelcomePopupWidget":5}],8:[function(require,module,exports){ "use strict";

Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var languageBlob = { "en": { "ext-whowrotethat-activation-link": "Who Wrote That?", "ext-whowrotethat-activation-link-tooltip": "Activate Who Wrote That?", "ext-whowrotethat-state-pending": " Who Wrote That? is loading. This might take a while", "ext-whowrotethat-state-error": "Error: $1", "ext-whowrotethat-state-error-generic": "Could not load WhoWroteThat. Please refresh and try again.", "ext-whowrotethat-ready-title": "Who Wrote That?", "ext-whowrotethat-ready-general": "Hover to see contributions by the same author. Click for more details.", "ext-whowrotethat-api-error-wikiwho-generic": "WikiWho data is not currently available for this article. Please try again soon.", "ext-whowrotethat-tour-welcome-title": " Who Wrote That? is enabled!", "ext-whowrotethat-tour-welcome-description": "Activate the the tool from the sidebar. Hover over the text to see the author and their other contributions to this page. Click to get more details.", "ext-whowrotethat-tour-welcome-dismiss": "Got it!" } }; var _default = languageBlob; exports["default"] = _default;

},{}]},{},[7]);