User:MSchottlender-WMF/WhoWroteThatGadget.js

From mediawiki.org

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]);