User:MSchottlender-WMF/WhoWroteThatGadget.js
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)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
(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){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var objectCreate = Object.create || objectCreatePolyfill
var objectKeys = Object.keys || objectKeysPolyfill
var bind = Function.prototype.bind || functionBindPolyfill
function EventEmitter() {
if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) {
this._events = objectCreate(null);
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
}
module.exports = EventEmitter;
// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
var defaultMaxListeners = 10;
var hasDefineProperty;
try {
var o = {};
if (Object.defineProperty) Object.defineProperty(o, 'x', { value: 0 });
hasDefineProperty = o.x === 0;
} catch (err) { hasDefineProperty = false }
if (hasDefineProperty) {
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
enumerable: true,
get: function() {
return defaultMaxListeners;
},
set: function(arg) {
// check whether the input is a positive number (whose value is zero or
// greater and not a NaN).
if (typeof arg !== 'number' || arg < 0 || arg !== arg)
throw new TypeError('"defaultMaxListeners" must be a positive number');
defaultMaxListeners = arg;
}
});
} else {
EventEmitter.defaultMaxListeners = defaultMaxListeners;
}
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
if (typeof n !== 'number' || n < 0 || isNaN(n))
throw new TypeError('"n" argument must be a positive number');
this._maxListeners = n;
return this;
};
function $getMaxListeners(that) {
if (that._maxListeners === undefined)
return EventEmitter.defaultMaxListeners;
return that._maxListeners;
}
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
return $getMaxListeners(this);
};
// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
function emitNone(handler, isFn, self) {
if (isFn)
handler.call(self);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self);
}
}
function emitOne(handler, isFn, self, arg1) {
if (isFn)
handler.call(self, arg1);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1);
}
}
function emitTwo(handler, isFn, self, arg1, arg2) {
if (isFn)
handler.call(self, arg1, arg2);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2);
}
}
function emitThree(handler, isFn, self, arg1, arg2, arg3) {
if (isFn)
handler.call(self, arg1, arg2, arg3);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2, arg3);
}
}
function emitMany(handler, isFn, self, args) {
if (isFn)
handler.apply(self, args);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].apply(self, args);
}
}
EventEmitter.prototype.emit = function emit(type) {
var er, handler, len, args, i, events;
var doError = (type === 'error');
events = this._events;
if (events)
doError = (doError && events.error == null);
else if (!doError)
return false;
// If there is no 'error' event listener then throw.
if (doError) {
if (arguments.length > 1)
er = arguments[1];
if (er instanceof Error) {
throw er; // Unhandled 'error' event
} else {
// At least give some kind of context to the user
var err = new Error('Unhandled "error" event. (' + er + ')');
err.context = er;
throw err;
}
return false;
}
handler = events[type];
if (!handler)
return false;
var isFn = typeof handler === 'function';
len = arguments.length;
switch (len) {
// fast cases
case 1:
emitNone(handler, isFn, this);
break;
case 2:
emitOne(handler, isFn, this, arguments[1]);
break;
case 3:
emitTwo(handler, isFn, this, arguments[1], arguments[2]);
break;
case 4:
emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
break;
// slower
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
emitMany(handler, isFn, this, args);
}
return true;
};
function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
events = target._events;
if (!events) {
events = target._events = objectCreate(null);
target._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener) {
target.emit('newListener', type,
listener.listener ? listener.listener : listener);
// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = target._events;
}
existing = events[type];
}
if (!existing) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener;
++target._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events[type] =
prepend ? [listener, existing] : [existing, listener];
} else {
// If we've already got an array, just append.
if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
}
// Check for listener leak
if (!existing.warned) {
m = $getMaxListeners(target);
if (m && m > 0 && existing.length > m) {
existing.warned = true;
var w = new Error('Possible EventEmitter memory leak detected. ' +
existing.length + ' "' + String(type) + '" listeners ' +
'added. Use emitter.setMaxListeners() to ' +
'increase limit.');
w.name = 'MaxListenersExceededWarning';
w.emitter = target;
w.type = type;
w.count = existing.length;
if (typeof console === 'object' && console.warn) {
console.warn('%s: %s', w.name, w.message);
}
}
}
}
return target;
}
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
};
function onceWrapper() {
if (!this.fired) {
this.target.removeListener(this.type, this.wrapFn);
this.fired = true;
switch (arguments.length) {
case 0:
return this.listener.call(this.target);
case 1:
return this.listener.call(this.target, arguments[0]);
case 2:
return this.listener.call(this.target, arguments[0], arguments[1]);
case 3:
return this.listener.call(this.target, arguments[0], arguments[1],
arguments[2]);
default:
var args = new Array(arguments.length);
for (var i = 0; i < args.length; ++i)
args[i] = arguments[i];
this.listener.apply(this.target, args);
}
}
}
function _onceWrap(target, type, listener) {
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
var wrapped = bind.call(onceWrapper, state);
wrapped.listener = listener;
state.wrapFn = wrapped;
return wrapped;
}
EventEmitter.prototype.once = function once(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.on(type, _onceWrap(this, type, listener));
return this;
};
EventEmitter.prototype.prependOnceListener =
function prependOnceListener(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.prependListener(type, _onceWrap(this, type, listener));
return this;
};
// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener =
function removeListener(type, listener) {
var list, events, position, i, originalListener;
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
events = this._events;
if (!events)
return this;
list = events[type];
if (!list)
return this;
if (list === listener || list.listener === listener) {
if (--this._eventsCount === 0)
this._events = objectCreate(null);
else {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, list.listener || listener);
}
} else if (typeof list !== 'function') {
position = -1;
for (i = list.length - 1; i >= 0; i--) {
if (list[i] === listener || list[i].listener === listener) {
originalListener = list[i].listener;
position = i;
break;
}
}
if (position < 0)
return this;
if (position === 0)
list.shift();
else
spliceOne(list, position);
if (list.length === 1)
events[type] = list[0];
if (events.removeListener)
this.emit('removeListener', type, originalListener || listener);
}
return this;
};
EventEmitter.prototype.removeAllListeners =
function removeAllListeners(type) {
var listeners, events, i;
events = this._events;
if (!events)
return this;
// not listening for removeListener, no need to emit
if (!events.removeListener) {
if (arguments.length === 0) {
this._events = objectCreate(null);
this._eventsCount = 0;
} else if (events[type]) {
if (--this._eventsCount === 0)
this._events = objectCreate(null);
else
delete events[type];
}
return this;
}
// emit removeListener for all listeners on all events
if (arguments.length === 0) {
var keys = objectKeys(events);
var key;
for (i = 0; i < keys.length; ++i) {
key = keys[i];
if (key === 'removeListener') continue;
this.removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._events = objectCreate(null);
this._eventsCount = 0;
return this;
}
listeners = events[type];
if (typeof listeners === 'function') {
this.removeListener(type, listeners);
} else if (listeners) {
// LIFO order
for (i = listeners.length - 1; i >= 0; i--) {
this.removeListener(type, listeners[i]);
}
}
return this;
};
function _listeners(target, type, unwrap) {
var events = target._events;
if (!events)
return [];
var evlistener = events[type];
if (!evlistener)
return [];
if (typeof evlistener === 'function')
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
}
EventEmitter.prototype.listeners = function listeners(type) {
return _listeners(this, type, true);
};
EventEmitter.prototype.rawListeners = function rawListeners(type) {
return _listeners(this, type, false);
};
EventEmitter.listenerCount = function(emitter, type) {
if (typeof emitter.listenerCount === 'function') {
return emitter.listenerCount(type);
} else {
return listenerCount.call(emitter, type);
}
};
EventEmitter.prototype.listenerCount = listenerCount;
function listenerCount(type) {
var events = this._events;
if (events) {
var evlistener = events[type];
if (typeof evlistener === 'function') {
return 1;
} else if (evlistener) {
return evlistener.length;
}
}
return 0;
}
EventEmitter.prototype.eventNames = function eventNames() {
return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
};
// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
list[i] = list[k];
list.pop();
}
function arrayClone(arr, n) {
var copy = new Array(n);
for (var i = 0; i < n; ++i)
copy[i] = arr[i];
return copy;
}
function unwrapListeners(arr) {
var ret = new Array(arr.length);
for (var i = 0; i < ret.length; ++i) {
ret[i] = arr[i].listener || arr[i];
}
return ret;
}
function objectCreatePolyfill(proto) {
var F = function() {};
F.prototype = proto;
return new F;
}
function objectKeysPolyfill(obj) {
var keys = [];
for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) {
keys.push(k);
}
return k;
}
function functionBindPolyfill(context) {
var fn = this;
return function () {
return fn.apply(context, arguments);
};
}
},{}],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.
* @cfg config.mwApi The mw.Api instance.
* @constructor
*/
function Api() {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_classCallCheck(this, Api);
// Remove trailing slash from URL.
this.url = (config.url || '').replace(/\/$/, '');
this.mwApi = config.mwApi;
this.results = null;
}
/**
* Fetch core messages needed for the revision popup, etc., making them available to mw.msg().
* This is called just after the script is first loaded, and the request completes very quickly,
* so we shouldn't need to bother with promises and such.
*/
_createClass(Api, [{
key: "fetchMessages",
value: function fetchMessages() {
this.mwApi.loadMessages(['parentheses-start', 'talkpagelinktext', 'pipe-separator', 'contribslink', 'parentheses-end']);
}
/**
* Get parsed edit summary for the given revision.
* @param {number} revId
* @return {Promise} Resolving Object with keys 'comment' and 'size'.
*/
}, {
key: "fetchEditSummary",
value: function fetchEditSummary(revId) {
return this.mwApi.ajax({
action: 'compare',
fromrev: revId,
torelative: 'prev',
prop: 'parsedcomment|size',
formatversion: 2
}).then(function (data) {
if (data.compare) {
return {
comment: data.compare.toparsedcomment,
size: data.compare.tosize - (data.compare.fromsize || 0)
};
}
return $.Deferred().reject();
}, function (failData) {
return $.Deferred().reject(failData);
});
}
/**
* 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
*/
}, {
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('/') + '/';
}
/**
* 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, isIP, 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]; // WikiWho prefixes IP addresses with '0|'.
isIP = username.slice(0, 2) === '0|';
username = isIP ? username.slice(2) : username; // 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],
isIP: isIP,
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 _this = this;
var _getJsonData,
retry = 1,
retries = 4;
if (this.resultsPromise) {
return this.resultsPromise;
}
_getJsonData = function getJsonData() {
return $.getJSON(_this.getAjaxURL(wikiUrl)).then(function (result) {
// Handle error response.
if (!result.success) {
// The API gives us an error message, but we don't use it because it's only
// in English. Some of the error messages are:
// * result.info: "Requested data is not currently available in WikiWho
// database. It will be available soon."
// * result.error: "The article (x) you are trying to request does not exist
// in english Wikipedia."
// We do add the full error details to the console, for easier debugging.
var errCode = result.info && result.info.match(/data is not currently available/i) ? 'refresh' : 'contact';
window.console.error('WhoWroteThat encountered a "' + errCode + '" error:', result);
if (errCode === 'refresh' && retry <= retries) {
// Return an intermediate Promise to handle the wait.
// The time to wait gets progressively longer for each retry.
return new Promise(function (resolve) {
return setTimeout(resolve, 1000 * retry);
}).then(function () {
// Followed by a (recursive) Promise to do the next request.
window.console.log('WhoWroteThat Api::getData() retry ' + retry);
retry++;
return _getJsonData();
});
}
return $.Deferred().reject(errCode);
} // Report retry count.
if (retry > 1) {
window.console.info('WhoWroteThat Api::getData() total retries: ' + (retry - 1));
} // Store all results.
_this.results = result;
}, function (jqXHR) {
// All other errors are likely to be 4xx and 5xx, and the only one that the user
// might be able to recover from is 429 Too Many Requests.
var errCode = 'contact';
if (jqXHR.status === 429) {
errCode = 'later';
}
return $.Deferred().reject(errCode);
});
};
this.resultsPromise = _getJsonData();
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 _InfoBarWidget = _interopRequireDefault(require("./InfoBarWidget"));
var _RevisionPopupWidget = _interopRequireDefault(require("./RevisionPopupWidget"));
var _Controller = _interopRequireDefault(require("./Controller"));
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 managing the WWT ui
* and the actions on the DOM returned by the API
*
* @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() {
var _this = this;
_classCallCheck(this, App);
// Instantiate only once
if (!App.instance) {
App.instance = this;
}
this.model = _Controller["default"].getModel();
this.revisionPopup = new _RevisionPopupWidget["default"]();
this.widget = new _InfoBarWidget["default"](); // Attach widget
if ($('body').hasClass('skin-timeless')) {
$('#mw-content-wrapper').prepend(this.widget.$element);
} else {
$('#content').prepend(this.widget.$element);
} // Attach popup
$('body').append(this.revisionPopup.$element); // Attach events
this.model.on('state', function (state) {
if (state === 'ready') {
// TODO: We need to sort something out here where either
// we detach those events on 'dismiss' or we make sure
// that we do not reattach events if they were previously
// already attached.
// This problem existed before the MVC refactoring, but is
// more visible now, and may have more potential challenges
// when we allow for VE to be dismissed without save (in which
// case no need to reattach events to the DOM we're launching)
// vs redoing the operation after a VE save without reload (in
// which case we need to re-attach those events).
_this.attachContentListeners(_this.model.getContentWrapper());
_this.scrollDown();
}
});
return App.instance;
}
/**
* Scroll down to the content on a diff page
* if it is below or in the lower third of the viewport.
*/
_createClass(App, [{
key: "scrollDown",
value: function scrollDown() {
var viewBottom = window.scrollY + window.innerHeight * (2 / 3),
$diffHead = $('h2.diff-currentversion-title');
if ($diffHead.length === 1) {
var contentTop = $diffHead.offset().top,
infobarHeight = this.widget.$element.outerHeight(true);
if (contentTop > viewBottom) {
// Scroll to below the WWT info bar. Redundant selector is for Safari.
$('html, body').animate({
scrollTop: contentTop - infobarHeight
});
}
}
}
/**
* Activate all the spans belonging to the given user.
*
* @param {jQuery} $content The content to apply events on
* @param {number} editorId
*/
}, {
key: "activateSpans",
value: function activateSpans($content, editorId) {
$content.find('.token-editor-' + editorId).addClass('active');
}
/**
* Deactivate all spans.
*
* @param {jQuery} $content The content to apply events on
*/
}, {
key: "deactivateSpans",
value: function deactivateSpans($content) {
$content.find('.mw-parser-output .editor-token').removeClass('active');
}
/**
* 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;
}
/**
* Add listeners to:
* - highlight attribution;
* - show the RevisionPopupWidget; and
* - scroll to the right place for fragment links.
*
* @param {jQuery} $content The content to apply events on
*/
}, {
key: "attachContentListeners",
value: function attachContentListeners($content) {
var _this2 = this;
$content.find('.mw-parser-output .editor-token').on('mouseenter', function (e) {
if (_this2.revisionPopup.isVisible()) {
return;
}
var ids = _this2.getIdsFromElement(e.currentTarget),
tokenInfo = _Controller["default"].getApi().getTokenInfo(ids.tokenId);
_this2.activateSpans($content, ids.editorId);
_this2.widget.setUsernameInfo(tokenInfo.username);
}).on('mouseleave', function () {
if (_this2.revisionPopup.isVisible()) {
return;
}
_this2.deactivateSpans($content);
_this2.widget.clearUsernameInfo();
});
$content.find('.editor-token').on('click', function (e) {
var ids = _this2.getIdsFromElement(e.currentTarget),
tokenInfo = _Controller["default"].getApi().getTokenInfo(ids.tokenId);
_this2.activateSpans($content, ids.editorId);
_this2.widget.setUsernameInfo(tokenInfo.username);
_this2.revisionPopup.show(tokenInfo, $(e.currentTarget));
_this2.revisionPopup.once('toggle', function () {
_this2.deactivateSpans($content);
_this2.widget.clearUsernameInfo();
}); // eslint-disable-next-line one-var
var reqStartTime = Date.now(); // Fetch edit summary then re-render the popup.
_Controller["default"].getApi().fetchEditSummary(tokenInfo.revisionId).then(function (successData) {
var delayTime = Date.now() - reqStartTime < 250 ? 250 : 0;
Object.assign(tokenInfo, successData);
setTimeout(function () {
_this2.revisionPopup.show(tokenInfo, $(e.target));
}, delayTime);
}, function () {
// Silently fail. The revision info provided by WikiWho is still present, which is
// the important part, so we'll just show what we have and throw a console warning.
mw.log.warn("WhoWroteThat failed to fetch the summary for revision ".concat(tokenInfo.revisionId));
});
});
/*
* Modify fragment link scrolling behaviour to take into account the width of the infobar at
* the top of the screen, to prevent the targeted heading or citation from being hidden.
*/
$content.find("a[href^='#']").on('click', function (event) {
var targetId, linkOffset, infobarHeight;
if (!_this2.widget.isVisible()) {
// Use the default if WWT is not active.
return;
}
targetId = decodeURIComponent(event.currentTarget.hash).replace(/^#/, '');
event.preventDefault(); // After preventing the default event from doing it, set the URL bar fragment manually.
window.location.hash = targetId; // After setting that, manually scroll to the correct place.
linkOffset = $(document.getElementById(targetId)).offset().top;
infobarHeight = _this2.widget.$element.outerHeight(true);
window.scrollTo(0, linkOffset - infobarHeight);
});
}
}]);
return App;
}();
var _default = App; // This should be able to load with 'require'
exports["default"] = _default;
module.exports = App;
},{"./Controller":4,"./InfoBarWidget":5,"./RevisionPopupWidget":7}],4:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _Api = _interopRequireDefault(require("./Api"));
var _Model = _interopRequireDefault(require("./Model"));
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; }
var $ = window.$ || require('jquery');
/**
* An activation singleton, responsible for activating and attaching the
* button that activates the system when it is applicable.
*
* @class
*/
var Controller =
/*#__PURE__*/
function () {
/**
* Initialize if it is the first time, and cache the
* initialization for the next times.
*/
function Controller() {
var _this = this;
_classCallCheck(this, Controller);
if (!Controller.instance) {
this.initialized = false;
this.link = null;
this.namespace = null;
this.mainPage = false;
this.translations = {};
Controller.instance = this;
this.app = null;
this.api = null;
this.model = new _Model["default"](); // Events
this.model.on('active', function (isActive) {
_this.toggleLinkActiveState(isActive); // Toggle a class for CSS to style links appropriately.
_this.model.getContentWrapper().toggleClass('wwt-active', isActive);
}); // Events
this.model.on('enabled', function (isEnabled) {
_this.getButton().toggle(isEnabled);
});
}
return Controller.instance;
}
/**
* Get the model attached to this controller
*
* @return {Model} Current model
*/
_createClass(Controller, [{
key: "getModel",
value: function getModel() {
return this.model;
}
/**
* Get the API class attached to this controller
*
* @return {Api} Current API
*/
}, {
key: "getApi",
value: function getApi() {
return this.api;
}
/**
* Initialize the process
*
* @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: "initialize",
value: function initialize($content, config) {
if (this.model.isInitialized()) {
return;
}
this.model.initialize($content, config);
if (!this.model.isValidPage()) {
return;
}
this.translations = config.translations || {}; // 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) {
this.api = new _Api["default"]({
url: config.wikiWhoUrl,
mwApi: new mw.Api()
});
this.api.fetchMessages(); // Load all messages
mw.messages.set(Object.assign({}, // Manually create fallback on English
this.translations.en, this.translations[this.model.getLang()])); // Add a portlet link to 'tools'
this.link = mw.util.addPortletLink('p-tb', '#', mw.msg('whowrotethat-activation-link'), 't-whowrotethat', mw.msg('whowrotethat-activation-link-tooltip'));
this.initialized = true;
}
}
/**
* Launch WWT application
*
* @return {jQuery.Promise} Promise that is resolved when the system
* has finished launching, or is rejected if the system has failed to launch
*/
}, {
key: "launch",
value: function launch() {
var _this2 = this;
if (!this.model.isEnabled()) {
window.console.log('Who Wrote That: Could not launch. System is disabled.');
return $.Deferred().reject();
} // Cache the current version.
// We might replace it in a minute, but if the user will
// shut the system down while we fetch, we want the
// original available.
this.model.cacheOriginal();
return this.loadDependencies().then(function () {
if (!_this2.app) {
// Only load after dependencies are loaded
// And only load once
// We do this trick because our widgets are dependent
// on OOUI, which is not available at construction time.
// When we launch, we load dependencies and the first run
// should make sure we also initialize the widget app
var App = require('./App');
_this2.app = new App();
}
_this2.model.toggleActive(true);
_this2.model.setState('pending');
return _this2.api.getData(window.location.href).then( // Success handler.
function () {
// There could be time that passed between
// activating the promise request and getting the
// answer. During that time, the user may
// have dismissed the system.
// We should only replace the DOM and declare
// ready if the system is actually ready to be
// replaced.
// On subsequent launches, this promise will run
// again (no-op as an already-resolved promise)
// and the operation below will be re-triggered
// with the replacements
if (_this2.model.isActive()) {
// Insert modified HTML.
_this2.model.cacheOriginal();
_this2.model.getContentWrapper().html(_this2.api.getReplacementHtml());
_this2.model.setState('ready');
}
return _this2.model.getContentWrapper();
}, // Error handler.
function (errorCode) {
_this2.model.setState('err', errorCode);
});
});
}
/**
* Close the WWT application
*/
}, {
key: "dismiss",
value: function dismiss() {
if (!this.model.isActive()) {
window.console.log('Who Wrote That: Could not dismiss. System is not active.');
return;
}
this.model.toggleActive(false); // Revert back to the original content
this.model.getContentWrapper().html(this.model.getOriginalContent().html());
}
/**
* Toggle the system; if already launched, dismiss it, and vise versa.
*/
}, {
key: "toggle",
value: function toggle() {
if (this.model.isActive()) {
this.dismiss();
} else {
this.launch();
}
}
/**
* Set the link text and tooltip.
* @param {boolean} [active] The state to toggle to.
*/
}, {
key: "toggleLinkActiveState",
value: function toggleLinkActiveState(active) {
var anchor = this.link.querySelector('a');
if (active) {
anchor.textContent = mw.msg('whowrotethat-deactivation-link');
anchor.title = '';
} else {
anchor.textContent = mw.msg('whowrotethat-activation-link');
anchor.title = mw.msg('whowrotethat-activation-link-tooltip');
}
}
/**
* 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() {
if (!window.mw) {
// This is for test environment only, where mw
// is not defined.
return $.Deferred().resolve();
}
return $.when($.ready, // jQuery's document.ready
mw.loader.using([// MediaWiki dependencies
'oojs-ui', 'oojs-ui.styles.icons-user', 'oojs-ui.styles.icons-interactions', 'mediawiki.interface.helpers.styles', 'mediawiki.special.changeslist', 'mediawiki.widgets.datetime', 'moment']));
}
/**
* Get the jQuery object representing the activation button
*
* @return {jQuery} Activation button
*/
}, {
key: "getButton",
value: function getButton() {
return $(this.link);
}
}]);
return Controller;
}(); // Initialize the singleton
// eslint-disable-next-line one-var
var wwtController = new Controller();
var _default = wwtController;
exports["default"] = _default;
},{"./Api":2,"./App":3,"./Model":6,"jquery":undefined}],5:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _Tools = _interopRequireDefault(require("./Tools"));
var _Controller = _interopRequireDefault(require("./Controller"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
/**
* @class
* @param {Object} config Configuration options.
* @constructor
*/
var InfoBarWidget = function InfoBarWidget() {
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: ['wwt-infoBarWidget-close']
});
this.userInfoUsernameLabel = new OO.ui.LabelWidget();
this.userInfoLabel = new OO.ui.LabelWidget({
label: mw.msg('whowrotethat-ready-general'),
classes: ['wwt-infoBarWidget-info']
});
this.$pendingAnimation = $('<div>').addClass('wwt-infoBarWidget-spinner').append($('<div>').addClass('wwt-infoBarWidget-spinner-bounce')); // Set properties
this.setState(_Controller["default"].getModel().getState());
this.toggle(_Controller["default"].getModel().isActive()); // Events
this.closeIcon.$element.on('click', _Controller["default"].dismiss.bind(_Controller["default"]));
_Controller["default"].getModel().on('state', this.setState.bind(this));
_Controller["default"].getModel().on('active', this.toggle.bind(this)); // Initialize
this.$element.addClass('wwt-infoBarWidget').append(this.$pendingAnimation, this.$icon, this.$label, this.userInfoUsernameLabel.$element, 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'
* @param {string} [errorCode] Error code, if applicable
*/
InfoBarWidget.prototype.setState = function (state) {
var errorCode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
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.setIcon('userAvatar');
this.setLabel($('<span>').append(mw.msg('whowrotethat-ready-title')).contents());
this.userInfoLabel.setLabel($('<span>').append(mw.msg('whowrotethat-ready-general')).contents());
} else if (state === 'pending') {
this.setIcon('');
this.setLabel($('<span>').append(mw.msg('whowrotethat-state-pending')).contents());
} else {
this.setIcon('error');
this.setErrorMessage(errorCode);
}
this.$pendingAnimation.toggle(state === 'pending');
this.userInfoLabel.toggle(state === 'ready');
this.state = state;
}
};
/**
* Set an error with a specific label
*
* @param {string} errCode Error code as defined by the Api class.
*/
InfoBarWidget.prototype.setErrorMessage = function () {
var errCode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'refresh';
// Messages used here:
// whowrotethat-error-refresh
// whowrotethat-error-later
// whowrotethat-error-contact
var errorMessage = mw.msg('whowrotethat-error-' + errCode);
if (errCode === 'contact') {
// The contact error message is the only with with a different signature, so we handle it.
var link = document.createElement('a');
link.href = 'https://meta.wikimedia.org/wiki/Talk:Community_Tech/Who_Wrote_That_tool';
link.text = mw.msg('whowrotethat-error-contact-link');
errorMessage = mw.message('whowrotethat-error-contact', link).parse();
}
this.setLabel(new OO.ui.HtmlSnippet(mw.msg('whowrotethat-state-error', errorMessage)));
};
/**
* Show the given username information
*
* @param {string} username
*/
InfoBarWidget.prototype.setUsernameInfo = function (username) {
this.userInfoUsernameLabel.setLabel($('<span>').append(_Tools["default"].bidiIsolate(username)).contents());
}; // Clear the username information
InfoBarWidget.prototype.clearUsernameInfo = function () {
this.userInfoUsernameLabel.setLabel('');
};
var _default = InfoBarWidget;
exports["default"] = _default;
},{"./Controller":4,"./Tools":8}],6:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(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; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
var EventEmitter = require('events');
/**
* A class managing and storing the state of the system
*
* @extends EventEmitter
*/
var Model =
/*#__PURE__*/
function (_EventEmitter) {
_inherits(Model, _EventEmitter);
/**
* Construct with the default values
*/
function Model() {
var _this;
_classCallCheck(this, Model);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Model).call(this));
_this.initialized = false;
_this.enabled = true;
_this.active = false;
_this.state = 'pending';
_this.$originalContent = null;
_this.$contentWrapper = null;
_this.lang = 'en';
_this.namespace = '';
_this.mainPage = false;
return _this;
}
/**
* Initialize the model with the given content and configuration
* This is done after the class is instantiated, when we are certain
* that the DOM loaded with all gadgets ready.
*
* @param {jQuery} $content jQuery object representing the content
* @param {Object} config Configuration object
* @param {string} [config.lang='en'] Interface language used
* @param {string} [config.namespace] Namespace of the current article
* @param {boolean} [config.mainPage] Whether the current page is the
* wiki's main page
* @fires Model#initialized
*/
_createClass(Model, [{
key: "initialize",
value: function initialize($content, config) {
this.$contentWrapper = $content;
this.lang = config.lang || 'en';
this.namespace = config.namespace || '';
this.mainPage = !!config.mainPage;
this.initialized = true;
/**
* Initialization event
*
* @event Model#initialized
*/
this.emit('initialized');
}
/**
* Cache the original content we are about to replace
*
* @param {jQuery} [$content=this.$contentWrapper] A jQuery
* object to cache. If not given the $contentWrapper will
* be cached. In most cases this should remain empty,
* as $contentWrapper contains the original content before
* it is replaced.
*/
}, {
key: "cacheOriginal",
value: function cacheOriginal() {
var $content = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.$contentWrapper;
$content = $content || this.$contentWrapper;
this.$originalContent = $content.clone(true, true);
}
/**
* 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;
}
/**
* Check if the current page is valid
* for the system to display.
*
* @return {boolean} Page is valid
*/
}, {
key: "isValidPage",
value: function isValidPage() {
return !!( // Has the needed parser content
this.getContentWrapper().length && // Is in the main namespace
this.namespace === '' && // Is not main page
!this.mainPage);
}
/**
* Check whether the model was initialized
*
* @return {boolean} Model was initialized
*/
}, {
key: "isInitialized",
value: function isInitialized() {
return this.initialized;
}
/**
* Check whether the system is enabled on this page
*
* @return {boolean} System is enabled
*/
}, {
key: "isEnabled",
value: function isEnabled() {
return this.enabled;
}
/**
* Check whether the system is currently active
*
* @return {boolean} System is active
*/
}, {
key: "isActive",
value: function isActive() {
return this.active;
}
/**
* Get the current state of the system.
* Possible values are 'pending', 'ready' or 'error'.
*
* @return {string} State of the system
*/
}, {
key: "getState",
value: function getState() {
return this.state;
}
/**
* Get the interface language used
*
* @return {string} Interface language
*/
}, {
key: "getLang",
value: function getLang() {
return this.lang;
}
/**
* Toggle the active state of the system
*
* @fires Model#active
* @param {boolean} [state] System is active
*/
}, {
key: "toggleActive",
value: function toggleActive(state) {
// If state isn't given, use the flipped value of the current state
state = state !== undefined ? !!state : !this.active;
if (this.active !== state) {
this.active = state;
/**
* Activation event
*
* @event Model#active
* @type {boolean}
* @param {boolean} isActive System is currently active
*/
this.emit('active', this.active);
}
}
/**
* Toggle whether the system is currently enabled
*
* @fires Model#enabled
* @param {boolean} [state] System is enabled
*/
}, {
key: "toggleEnabled",
value: function toggleEnabled(state) {
// If state isn't given, use the flipped value of the current state
state = state !== undefined ? !!state : !this.active;
if (this.enabled !== state) {
this.enabled = state;
/**
* Activation event
*
* @event Model#enabled
* @type {boolean}
* @param {boolean} isEnabled System is currently active
*/
this.emit('enabled', this.enabled);
}
}
/**
* Set the state of the system. Legal values are 'pending', 'ready', or 'err'
* For 'err' state, an error code is optionally given.
*
* @fires Model#state
* @param {string} state System state
* @param {string} [errorCode=''] Error code type, if the state is an error
*/
}, {
key: "setState",
value: function setState(state) {
var errorCode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
if (['pending', 'ready', 'err'].indexOf(state) > -1 && this.state !== state) {
this.state = state;
/**
* State change event
*
* @event Model#state
* @type {boolean}
* @param {string} System changed its state
* @param {string} Error code, if provided
*/
this.emit('state', this.state, errorCode);
}
}
}]);
return Model;
}(EventEmitter);
var _default = Model;
exports["default"] = _default;
},{"events":1}],7:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _Tools = _interopRequireDefault(require("./Tools"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
/**
* @class
* @constructor
*/
var RevisionPopupWidget = function RevisionPopupWidget() {
this.$popupContent = $('<div>').addClass('wwt-revisionPopupWidget-content'); // Parent constructor
RevisionPopupWidget.parent.call(this, {
padded: true,
autoClose: true,
position: 'above',
// FIXME: 'force-left' for RTL languages
align: 'force-right',
hideWhenOutOfView: false,
$content: this.$popupContent
});
};
/* Setup */
OO.inheritClass(RevisionPopupWidget, OO.ui.PopupWidget);
/**
* Get markup for the diff size.
* @param {number} size
* @return {string}
*/
function getSizeHtml(size) {
var sizeClass;
if (size > 0) {
sizeClass = 'mw-plusminus-pos';
} else if (size < 0) {
sizeClass = 'mw-plusminus-neg';
} else {
sizeClass = 'mw-plusminus-null';
}
return "\n\t\t<span class=\"".concat(sizeClass, " mw-diff-bytes\">") + "".concat(size > 0 ? '+' : '').concat(mw.language.convertNumber(size)) + '</span>';
}
/**
* Get markup for the edit summary.
* @param {Object} data As returned by Api.prototype.getTokenInfo().
* @return {string}
*/
function getCommentHtml(data) {
if (data.comment === '') {
// No edit summary.
return '';
} else if (data.comment === undefined) {
// Not yet available.
return "\n\t\t\t<div class=\"wwt-revisionPopupWidget-comment\">\n\t\t\t\t<div class=\"wwt-shimmer www-shimmer-animation\"></div>\n\t\t\t\t<div class=\"wwt-shimmer www-shimmer-animation\"></div>\n\t\t\t</div>";
}
return "\n\t\t<div class=\"wwt-revisionPopupWidget-comment wwt-revisionPopupWidget-comment-transparent\">\n\t\t\t<span class=\"comment comment--without-parentheses wwt-revisionPopupWidget-comment\">".concat(_Tools["default"].bidiIsolate(data.comment, true), "</span>\n\t\t\t").concat(getSizeHtml(data.size), "\n\t\t</div>");
}
/**
* Get jQuery objects for the user links.
* @param {Object} data As returned by Api.prototype.getTokenInfo().
* @return {jQuery}
*/
function getUserLinksHtml(data) {
var contribsUrl = mw.util.getUrl("Special:Contributions/".concat(data.username)),
// We typically link to Special:Contribs for IPs.
userPageUrl = data.isIP ? contribsUrl : mw.util.getUrl("User:".concat(data.username));
if (!data.username) {
// Username was apparently suppressed.
return $('<span>').addClass('history-deleted') // Note we can't use the native MediaWiki 'rev-deleted-user'
// message because it can include parser functions.
.text(mw.msg('whowrotethat-revision-deleted-username'));
}
return $([]).add($('<a>').attr('href', userPageUrl).append(_Tools["default"].bidiIsolate(data.username))).add(document.createTextNode(' ' + mw.msg('parentheses-start'))).add( // Talk page
$('<a>').attr('href', mw.util.getUrl("User talk:".concat(data.username))).text(mw.msg('talkpagelinktext'))).add(document.createTextNode(' ' + mw.msg('pipe-separator') + ' ')).add($('<a>').attr('href', contribsUrl).text(mw.msg('contribslink'))).add(document.createTextNode(mw.msg('parentheses-end')));
}
/**
* Show the revision popup based on the given token data, above the given element.
* Note that the English namespaces will normalize to the wiki's local namespaces.
* @param {Object} data As returned by Api.prototype.getTokenInfo().
* @param {jQuery} $target Element the popup should be attached to.
*/
RevisionPopupWidget.prototype.show = function (data, $target) {
var $userLinks = getUserLinksHtml(data),
dateStr = moment(data.revisionTime).locale(mw.config.get('wgUserLanguage')).format('LLL'),
// Use jQuery to make sure attributes are properly escaped
$diffLink = $('<a>').attr('href', mw.util.getUrl("Special:Diff/".concat(data.revisionId))).text(dateStr),
addedMsg = mw.message('whowrotethat-revision-added', $userLinks, $diffLink).parse(),
scoreMsgKey = Number(data.score) >= 1 ? 'whowrotethat-revision-attribution' : 'whowrotethat-revision-attribution-lessthan',
attributionMsg = "<div class=\"wwt-revisionPopupWidget-attribution\">".concat(mw.message(scoreMsgKey, data.score).parse(), "</div>"),
html = $.parseHTML("\n\t\t\t".concat(addedMsg.trim(), "\n\t\t\t").concat(getCommentHtml(data), "\n\t\t\t").concat(attributionMsg, "\n\t\t"));
this.$popupContent.html(html);
if ($target.find('.thumb').length) {
$target = $target.find('.thumb');
} // Make sure all links in the popup (including in the edit summary) open in new tabs.
this.$popupContent.find('a').attr('target', '_blank');
this.setFloatableContainer($target);
this.toggle(true); // Animate edit summary, if present.
if (data.comment) {
$('.wwt-revisionPopupWidget-comment').removeClass('wwt-revisionPopupWidget-comment-transparent');
}
};
var _default = RevisionPopupWidget;
exports["default"] = _default;
},{"./Tools":8}],8:[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; }
var $ = window.$ || require('jquery');
/**
* Class to hold some global helper tools
*/
var Tools =
/*#__PURE__*/
function () {
function Tools() {
_classCallCheck(this, Tools);
}
_createClass(Tools, null, [{
key: "bidiIsolate",
/**
* Encompass strings with BiDi isolation for RTL/LTR support.
* This should be used on any string that is content language,
* to make sure that if the user uses a different directionality
* in the interface language, the output is shown properly
* within a bidi isolation block.
*
* @param {jQuery|string} $content Content to isolate
* @param {boolean} [returnRawHtml] Force the return value to
* be raw html. If set to false, will return the encompassed
* jQuery object.
* @return {jQuery|string} BiDi isolated jQuery object or HTML
*/
value: function bidiIsolate($content) {
var returnRawHtml = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var $result = $('<bdi>');
if (typeof $content === 'string') {
$content = $($.parseHTML($content));
}
$result.append($content);
if (returnRawHtml) {
// `html()` sends the inner HTML, so we wrap the node
// and send the result to include the new <bdi> wrap
return $('<div>').append($result).html();
}
return $result;
}
}]);
return Tools;
}();
var _default = Tools;
exports["default"] = _default;
},{"jquery":undefined}],9:[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
* @extends OO.ui.PopupWidget
*
* @constructor
* @param {Object} [config={}] Configuration options
*/
var WelcomePopupWidget = function WelcomePopupWidget() {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var $content = $('<div>').addClass('wwt-welcomePopupWidget-content'); // Parent constructor
WelcomePopupWidget.parent.call(this, Object.assign({
padded: true,
autoClose: true,
position: 'after',
hideWhenOutOfView: false,
$content: $content
}, config));
this.dismissButton = new OO.ui.ButtonWidget({
flags: ['primary', 'progressive'],
label: mw.msg('whowrotethat-tour-welcome-dismiss')
});
$content.append($('<div>').addClass('wwt-welcomePopupWidget-title').append(mw.msg('whowrotethat-tour-welcome-title')), $('<div>').addClass('wwt-welcomePopupWidget-description').append(mw.msg('whowrotethat-tour-welcome-description')), $('<div>').addClass('wwt-welcomePopupWidget-button').append(this.dismissButton.$element)); // Events
this.dismissButton.connect(this, {
click: ['emit', 'dismiss']
}); // Initialize
this.$element.addClass('wwt-welcomePopupWidget');
};
/* Setup */
OO.inheritClass(WelcomePopupWidget, OO.ui.PopupWidget);
/* Methods */
/**
* @inheritdoc
*/
WelcomePopupWidget.prototype.toggle = function (show) {
// Parent method
WelcomePopupWidget.parent.prototype.toggle.call(this, show); // HACK: Prevent clipping; we don't want (or need) the
// popup to clip itself when it's outside the viewport
// or close to the viewport.
// Unfortunately, the toggle operation calls toggleClipping
// multiple times on and off, so we have to insist on it
// being actually off at the end of it.
this.toggleClipping(false);
};
module.exports = WelcomePopupWidget;
var _default = WelcomePopupWidget;
exports["default"] = _default;
},{}],10:[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;
},{}],11:[function(require,module,exports){
"use strict";
var _config = _interopRequireDefault(require("../config"));
var _Controller = _interopRequireDefault(require("../Controller"));
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) {
_Controller["default"].toggle();
e.preventDefault();
return false;
},
loadWhoWroteThat = function loadWhoWroteThat() {
_Controller["default"].initialize($('.mw-parser-output'), {
lang: $('html').attr('lang'),
translations: _languages["default"],
namespace: mw.config.get('wgCanonicalNamespace'),
mainPage: mw.config.get('wgIsMainPage'),
wikiWhoUrl: _config["default"].wikiWhoUrl
});
_Controller["default"].getButton().on('click', onActivateButtonClick);
};
$(document).ready(function () {
var welcomeTourSeen = window.localStorage.getItem('WelcomeTourSeen'),
$overlay = $('<div>').addClass('wwt-overlay');
loadWhoWroteThat();
if (welcomeTourSeen || !_Controller["default"].getModel().isValidPage()) {
// Do not show the tour if it was previously dismissed
// Or if the page is invalid
return;
}
$.when($.ready, mw.loader.using(['oojs-ui', 'mediawiki.jqueryMsg'])).then(function () {
var WelcomePopupWidget = require('../WelcomePopupWidget'),
welcome = new WelcomePopupWidget({
$floatableContainer: $('#t-whowrotethat'),
$overlay: $overlay
});
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
$('html').addClass('wwt-popup');
$('body').append($overlay);
$overlay.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.');
},
launch: _Controller["default"].launch.bind(_Controller["default"]),
dismiss: _Controller["default"].dismiss.bind(_Controller["default"])
};
})();
},{"../../temp/languages":12,"../Controller":4,"../WelcomePopupWidget":9,"../config":10}],12:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var languageBlob = {
"en": {
"whowrotethat-activation-link": "Who Wrote That?",
"whowrotethat-activation-link-tooltip": "Activate Who Wrote That?",
"whowrotethat-deactivation-link": "Close Who Wrote That?",
"whowrotethat-state-pending": "<strong><em>Who Wrote That?</em></strong> is loading. This might take a while.",
"whowrotethat-state-error": "API Error: $1",
"whowrotethat-error-refresh": "Please refresh the page or try again later.",
"whowrotethat-error-later": "Please try again later.",
"whowrotethat-error-contact": "Please $1 about this issue.",
"whowrotethat-error-contact-link": "contact us",
"whowrotethat-ready-title": "Who Wrote That?",
"whowrotethat-ready-general": "Hover to see contributions by the same author. Click for more details.",
"whowrotethat-tour-welcome-title": "<strong><em>Who Wrote That?</em></strong> is enabled!",
"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.",
"whowrotethat-tour-welcome-dismiss": "Got it!",
"whowrotethat-revision-added": "$1 added this on $2.",
"whowrotethat-revision-attribution": "They have written <strong>$1%</strong> of the page.",
"whowrotethat-revision-attribution-lessthan": "They have written <strong>less than 1%</strong> of the page.",
"whowrotethat-revision-deleted-username": "(username or IP removed)"
}
};
var _default = languageBlob;
exports["default"] = _default;
},{}]},{},[11]);