/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = "./src/index.js"); /******/ }) /************************************************************************/ /******/ ({ /***/ "./node_modules/redux-thunk/dist/redux-thunk.js": /***/ (function(module, exports, __webpack_require__) { (function webpackUniversalModuleDefinition(root, factory) { if(true) module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["ReduxThunk"] = factory(); else root["ReduxThunk"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { module.exports = __webpack_require__(1); /***/ }, /* 1 */ /***/ function(module, exports) { 'use strict'; exports.__esModule = true; function createThunkMiddleware(extraArgument) { return function (_ref) { var dispatch = _ref.dispatch, getState = _ref.getState; return function (next) { return function (action) { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; }; }; } var thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; exports['default'] = thunk; /***/ } /******/ ]) }); ; /***/ }), /***/ "./node_modules/redux/dist/redux.js": /***/ (function(module, exports, __webpack_require__) { (function webpackUniversalModuleDefinition(root, factory) { if(true) module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["Redux"] = factory(); else root["Redux"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; exports.__esModule = true; exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined; var _createStore = __webpack_require__(2); var _createStore2 = _interopRequireDefault(_createStore); var _combineReducers = __webpack_require__(7); var _combineReducers2 = _interopRequireDefault(_combineReducers); var _bindActionCreators = __webpack_require__(6); var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators); var _applyMiddleware = __webpack_require__(5); var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware); var _compose = __webpack_require__(1); var _compose2 = _interopRequireDefault(_compose); var _warning = __webpack_require__(3); var _warning2 = _interopRequireDefault(_warning); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /* * This is a dummy function to check if the function name has been altered by minification. * If the function has been minified and NODE_ENV !== 'production', warn the user. */ function isCrushed() {} if (("development") !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') { (0, _warning2['default'])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.'); } exports.createStore = _createStore2['default']; exports.combineReducers = _combineReducers2['default']; exports.bindActionCreators = _bindActionCreators2['default']; exports.applyMiddleware = _applyMiddleware2['default']; exports.compose = _compose2['default']; /***/ }, /* 1 */ /***/ function(module, exports) { "use strict"; exports.__esModule = true; exports["default"] = compose; /** * Composes single-argument functions from right to left. The rightmost * function can take multiple arguments as it provides the signature for * the resulting composite function. * * @param {...Function} funcs The functions to compose. * @returns {Function} A function obtained by composing the argument functions * from right to left. For example, compose(f, g, h) is identical to doing * (...args) => f(g(h(...args))). */ function compose() { for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) { funcs[_key] = arguments[_key]; } if (funcs.length === 0) { return function (arg) { return arg; }; } if (funcs.length === 1) { return funcs[0]; } var last = funcs[funcs.length - 1]; var rest = funcs.slice(0, -1); return function () { return rest.reduceRight(function (composed, f) { return f(composed); }, last.apply(undefined, arguments)); }; } /***/ }, /* 2 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; exports.__esModule = true; exports.ActionTypes = undefined; exports['default'] = createStore; var _isPlainObject = __webpack_require__(4); var _isPlainObject2 = _interopRequireDefault(_isPlainObject); var _symbolObservable = __webpack_require__(12); var _symbolObservable2 = _interopRequireDefault(_symbolObservable); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /** * These are private action types reserved by Redux. * For any unknown actions, you must return the current state. * If the current state is undefined, you must return the initial state. * Do not reference these action types directly in your code. */ var ActionTypes = exports.ActionTypes = { INIT: '@@redux/INIT' }; /** * Creates a Redux store that holds the state tree. * The only way to change the data in the store is to call `dispatch()` on it. * * There should only be a single store in your app. To specify how different * parts of the state tree respond to actions, you may combine several reducers * into a single reducer function by using `combineReducers`. * * @param {Function} reducer A function that returns the next state tree, given * the current state tree and the action to handle. * * @param {any} [preloadedState] The initial state. You may optionally specify it * to hydrate the state from the server in universal apps, or to restore a * previously serialized user session. * If you use `combineReducers` to produce the root reducer function, this must be * an object with the same shape as `combineReducers` keys. * * @param {Function} enhancer The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. * * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */ function createStore(reducer, preloadedState, enhancer) { var _ref2; if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState; preloadedState = undefined; } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.'); } return enhancer(createStore)(reducer, preloadedState); } if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.'); } var currentReducer = reducer; var currentState = preloadedState; var currentListeners = []; var nextListeners = currentListeners; var isDispatching = false; function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice(); } } /** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */ function getState() { return currentState; } /** * Adds a change listener. It will be called any time an action is dispatched, * and some part of the state tree may potentially have changed. You may then * call `getState()` to read the current state tree inside the callback. * * You may call `dispatch()` from a change listener, with the following * caveats: * * 1. The subscriptions are snapshotted just before every `dispatch()` call. * If you subscribe or unsubscribe while the listeners are being invoked, this * will not have any effect on the `dispatch()` that is currently in progress. * However, the next `dispatch()` call, whether nested or not, will use a more * recent snapshot of the subscription list. * * 2. The listener should not expect to see all state changes, as the state * might have been updated multiple times during a nested `dispatch()` before * the listener is called. It is, however, guaranteed that all subscribers * registered before the `dispatch()` started will be called with the latest * state by the time it exits. * * @param {Function} listener A callback to be invoked on every dispatch. * @returns {Function} A function to remove this change listener. */ function subscribe(listener) { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.'); } var isSubscribed = true; ensureCanMutateNextListeners(); nextListeners.push(listener); return function unsubscribe() { if (!isSubscribed) { return; } isSubscribed = false; ensureCanMutateNextListeners(); var index = nextListeners.indexOf(listener); nextListeners.splice(index, 1); }; } /** * Dispatches an action. It is the only way to trigger a state change. * * The `reducer` function, used to create the store, will be called with the * current state tree and the given `action`. Its return value will * be considered the **next** state of the tree, and the change listeners * will be notified. * * The base implementation only supports plain object actions. If you want to * dispatch a Promise, an Observable, a thunk, or something else, you need to * wrap your store creating function into the corresponding middleware. For * example, see the documentation for the `redux-thunk` package. Even the * middleware will eventually dispatch plain object actions using this method. * * @param {Object} action A plain object representing “what changed”. It is * a good idea to keep actions serializable so you can record and replay user * sessions, or use the time travelling `redux-devtools`. An action must have * a `type` property which may not be `undefined`. It is a good idea to use * string constants for action types. * * @returns {Object} For convenience, the same action object you dispatched. * * Note that, if you use a custom middleware, it may wrap `dispatch()` to * return something else (for example, a Promise you can await). */ function dispatch(action) { if (!(0, _isPlainObject2['default'])(action)) { throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.'); } if (typeof action.type === 'undefined') { throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?'); } if (isDispatching) { throw new Error('Reducers may not dispatch actions.'); } try { isDispatching = true; currentState = currentReducer(currentState, action); } finally { isDispatching = false; } var listeners = currentListeners = nextListeners; for (var i = 0; i < listeners.length; i++) { listeners[i](); } return action; } /** * Replaces the reducer currently used by the store to calculate the state. * * You might need this if your app implements code splitting and you want to * load some of the reducers dynamically. You might also need this if you * implement a hot reloading mechanism for Redux. * * @param {Function} nextReducer The reducer for the store to use instead. * @returns {void} */ function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.'); } currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); } /** * Interoperability point for observable/reactive libraries. * @returns {observable} A minimal observable of state changes. * For more information, see the observable proposal: * https://github.com/zenparsing/es-observable */ function observable() { var _ref; var outerSubscribe = subscribe; return _ref = { /** * The minimal observable subscription method. * @param {Object} observer Any object that can be used as an observer. * The observer object should have a `next` method. * @returns {subscription} An object with an `unsubscribe` method that can * be used to unsubscribe the observable from the store, and prevent further * emission of values from the observable. */ subscribe: function subscribe(observer) { if (typeof observer !== 'object') { throw new TypeError('Expected the observer to be an object.'); } function observeState() { if (observer.next) { observer.next(getState()); } } observeState(); var unsubscribe = outerSubscribe(observeState); return { unsubscribe: unsubscribe }; } }, _ref[_symbolObservable2['default']] = function () { return this; }, _ref; } // When a store is created, an "INIT" action is dispatched so that every // reducer returns their initial state. This effectively populates // the initial state tree. dispatch({ type: ActionTypes.INIT }); return _ref2 = { dispatch: dispatch, subscribe: subscribe, getState: getState, replaceReducer: replaceReducer }, _ref2[_symbolObservable2['default']] = observable, _ref2; } /***/ }, /* 3 */ /***/ function(module, exports) { 'use strict'; exports.__esModule = true; exports['default'] = warning; /** * Prints a warning in the console if it exists. * * @param {String} message The warning message. * @returns {void} */ function warning(message) { /* eslint-disable no-console */ if (typeof console !== 'undefined' && typeof console.error === 'function') { console.error(message); } /* eslint-enable no-console */ try { // This error was thrown as a convenience so that if you enable // "break on all exceptions" in your console, // it would pause the execution at this line. throw new Error(message); /* eslint-disable no-empty */ } catch (e) {} /* eslint-enable no-empty */ } /***/ }, /* 4 */ /***/ function(module, exports, __webpack_require__) { var getPrototype = __webpack_require__(8), isHostObject = __webpack_require__(9), isObjectLike = __webpack_require__(11); /** `Object#toString` result references. */ var objectTag = '[object Object]'; /** Used for built-in method references. */ var funcProto = Function.prototype, objectProto = Object.prototype; /** Used to resolve the decompiled source of functions. */ var funcToString = funcProto.toString; /** Used to check objects for own properties. */ var hasOwnProperty = objectProto.hasOwnProperty; /** Used to infer the `Object` constructor. */ var objectCtorString = funcToString.call(Object); /** * Used to resolve the * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) * of values. */ var objectToString = objectProto.toString; /** * Checks if `value` is a plain object, that is, an object created by the * `Object` constructor or one with a `[[Prototype]]` of `null`. * * @static * @memberOf _ * @since 0.8.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. * @example * * function Foo() { * this.a = 1; * } * * _.isPlainObject(new Foo); * // => false * * _.isPlainObject([1, 2, 3]); * // => false * * _.isPlainObject({ 'x': 0, 'y': 0 }); * // => true * * _.isPlainObject(Object.create(null)); * // => true */ function isPlainObject(value) { if (!isObjectLike(value) || objectToString.call(value) != objectTag || isHostObject(value)) { return false; } var proto = getPrototype(value); if (proto === null) { return true; } var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor; return (typeof Ctor == 'function' && Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString); } module.exports = isPlainObject; /***/ }, /* 5 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; exports.__esModule = true; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; exports['default'] = applyMiddleware; var _compose = __webpack_require__(1); var _compose2 = _interopRequireDefault(_compose); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /** * Creates a store enhancer that applies middleware to the dispatch method * of the Redux store. This is handy for a variety of tasks, such as expressing * asynchronous actions in a concise manner, or logging every action payload. * * See `redux-thunk` package as an example of the Redux middleware. * * Because middleware is potentially asynchronous, this should be the first * store enhancer in the composition chain. * * Note that each middleware will be given the `dispatch` and `getState` functions * as named arguments. * * @param {...Function} middlewares The middleware chain to be applied. * @returns {Function} A store enhancer applying the middleware. */ function applyMiddleware() { for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) { middlewares[_key] = arguments[_key]; } return function (createStore) { return function (reducer, preloadedState, enhancer) { var store = createStore(reducer, preloadedState, enhancer); var _dispatch = store.dispatch; var chain = []; var middlewareAPI = { getState: store.getState, dispatch: function dispatch(action) { return _dispatch(action); } }; chain = middlewares.map(function (middleware) { return middleware(middlewareAPI); }); _dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch); return _extends({}, store, { dispatch: _dispatch }); }; }; } /***/ }, /* 6 */ /***/ function(module, exports) { 'use strict'; exports.__esModule = true; exports['default'] = bindActionCreators; function bindActionCreator(actionCreator, dispatch) { return function () { return dispatch(actionCreator.apply(undefined, arguments)); }; } /** * Turns an object whose values are action creators, into an object with the * same keys, but with every function wrapped into a `dispatch` call so they * may be invoked directly. This is just a convenience method, as you can call * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. * * For convenience, you can also pass a single function as the first argument, * and get a function in return. * * @param {Function|Object} actionCreators An object whose values are action * creator functions. One handy way to obtain it is to use ES6 `import * as` * syntax. You may also pass a single function. * * @param {Function} dispatch The `dispatch` function available on your Redux * store. * * @returns {Function|Object} The object mimicking the original object, but with * every action creator wrapped into the `dispatch` call. If you passed a * function as `actionCreators`, the return value will also be a single * function. */ function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch); } if (typeof actionCreators !== 'object' || actionCreators === null) { throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?'); } var keys = Object.keys(actionCreators); var boundActionCreators = {}; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var actionCreator = actionCreators[key]; if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch); } } return boundActionCreators; } /***/ }, /* 7 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; exports.__esModule = true; exports['default'] = combineReducers; var _createStore = __webpack_require__(2); var _isPlainObject = __webpack_require__(4); var _isPlainObject2 = _interopRequireDefault(_isPlainObject); var _warning = __webpack_require__(3); var _warning2 = _interopRequireDefault(_warning); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function getUndefinedStateErrorMessage(key, action) { var actionType = action && action.type; var actionName = actionType && '"' + actionType.toString() + '"' || 'an action'; return 'Given action ' + actionName + ', reducer "' + key + '" returned undefined. ' + 'To ignore an action, you must explicitly return the previous state.'; } function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) { var reducerKeys = Object.keys(reducers); var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer'; if (reducerKeys.length === 0) { return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.'; } if (!(0, _isPlainObject2['default'])(inputState)) { return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"'); } var unexpectedKeys = Object.keys(inputState).filter(function (key) { return !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]; }); unexpectedKeys.forEach(function (key) { unexpectedKeyCache[key] = true; }); if (unexpectedKeys.length > 0) { return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.'); } } function assertReducerSanity(reducers) { Object.keys(reducers).forEach(function (key) { var reducer = reducers[key]; var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT }); if (typeof initialState === 'undefined') { throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.'); } var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.'); if (typeof reducer(undefined, { type: type }) === 'undefined') { throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.'); } }); } /** * Turns an object whose values are different reducer functions, into a single * reducer function. It will call every child reducer, and gather their results * into a single state object, whose keys correspond to the keys of the passed * reducer functions. * * @param {Object} reducers An object whose values correspond to different * reducer functions that need to be combined into one. One handy way to obtain * it is to use ES6 `import * as reducers` syntax. The reducers may never return * undefined for any action. Instead, they should return their initial state * if the state passed to them was undefined, and the current state for any * unrecognized action. * * @returns {Function} A reducer function that invokes every reducer inside the * passed object, and builds a state object with the same shape. */ function combineReducers(reducers) { var reducerKeys = Object.keys(reducers); var finalReducers = {}; for (var i = 0; i < reducerKeys.length; i++) { var key = reducerKeys[i]; if (true) { if (typeof reducers[key] === 'undefined') { (0, _warning2['default'])('No reducer provided for key "' + key + '"'); } } if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; } } var finalReducerKeys = Object.keys(finalReducers); if (true) { var unexpectedKeyCache = {}; } var sanityError; try { assertReducerSanity(finalReducers); } catch (e) { sanityError = e; } return function combination() { var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; var action = arguments[1]; if (sanityError) { throw sanityError; } if (true) { var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache); if (warningMessage) { (0, _warning2['default'])(warningMessage); } } var hasChanged = false; var nextState = {}; for (var i = 0; i < finalReducerKeys.length; i++) { var key = finalReducerKeys[i]; var reducer = finalReducers[key]; var previousStateForKey = state[key]; var nextStateForKey = reducer(previousStateForKey, action); if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action); throw new Error(errorMessage); } nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; }; } /***/ }, /* 8 */ /***/ function(module, exports, __webpack_require__) { var overArg = __webpack_require__(10); /** Built-in value references. */ var getPrototype = overArg(Object.getPrototypeOf, Object); module.exports = getPrototype; /***/ }, /* 9 */ /***/ function(module, exports) { /** * Checks if `value` is a host object in IE < 9. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a host object, else `false`. */ function isHostObject(value) { // Many host objects are `Object` objects that can coerce to strings // despite having improperly defined `toString` methods. var result = false; if (value != null && typeof value.toString != 'function') { try { result = !!(value + ''); } catch (e) {} } return result; } module.exports = isHostObject; /***/ }, /* 10 */ /***/ function(module, exports) { /** * Creates a unary function that invokes `func` with its argument transformed. * * @private * @param {Function} func The function to wrap. * @param {Function} transform The argument transform. * @returns {Function} Returns the new function. */ function overArg(func, transform) { return function(arg) { return func(transform(arg)); }; } module.exports = overArg; /***/ }, /* 11 */ /***/ function(module, exports) { /** * Checks if `value` is object-like. A value is object-like if it's not `null` * and has a `typeof` result of "object". * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is object-like, else `false`. * @example * * _.isObjectLike({}); * // => true * * _.isObjectLike([1, 2, 3]); * // => true * * _.isObjectLike(_.noop); * // => false * * _.isObjectLike(null); * // => false */ function isObjectLike(value) { return !!value && typeof value == 'object'; } module.exports = isObjectLike; /***/ }, /* 12 */ /***/ function(module, exports, __webpack_require__) { module.exports = __webpack_require__(13); /***/ }, /* 13 */ /***/ function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(global) {'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _ponyfill = __webpack_require__(14); var _ponyfill2 = _interopRequireDefault(_ponyfill); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var root = undefined; /* global window */ if (typeof global !== 'undefined') { root = global; } else if (typeof window !== 'undefined') { root = window; } var result = (0, _ponyfill2['default'])(root); exports['default'] = result; /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }, /* 14 */ /***/ function(module, exports) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports['default'] = symbolObservablePonyfill; function symbolObservablePonyfill(root) { var result; var _Symbol = root.Symbol; if (typeof _Symbol === 'function') { if (_Symbol.observable) { result = _Symbol.observable; } else { result = _Symbol('observable'); _Symbol.observable = result; } } else { result = '@@observable'; } return result; }; /***/ } /******/ ]) }); ; /***/ }), /***/ "./src/actionTypes.js": /***/ (function(module, exports) { module.exports = { BOOT: 'BOOT', LINK_DWELL: 'LINK_DWELL', ABANDON_START: 'ABANDON_START', ABANDON_END: 'ABANDON_END', LINK_CLICK: 'LINK_CLICK', FETCH_START: 'FETCH_START', FETCH_END: 'FETCH_END', FETCH_COMPLETE: 'FETCH_COMPLETE', FETCH_FAILED: 'FETCH_FAILED', PREVIEW_DWELL: 'PREVIEW_DWELL', PREVIEW_SHOW: 'PREVIEW_SHOW', PREVIEW_CLICK: 'PREVIEW_CLICK', SETTINGS_SHOW: 'SETTINGS_SHOW', SETTINGS_HIDE: 'SETTINGS_HIDE', SETTINGS_CHANGE: 'SETTINGS_CHANGE', EVENT_LOGGED: 'EVENT_LOGGED', STATSV_LOGGED: 'STATSV_LOGGED' }; /***/ }), /***/ "./src/actions.js": /***/ (function(module, exports, __webpack_require__) { var $ = jQuery, mw = window.mediaWiki, actions = {}, types = __webpack_require__( "./src/actionTypes.js" ), wait = __webpack_require__( "./src/wait.js" ), // See the following for context around this value. // // * https://phabricator.wikimedia.org/T161284 // * https://phabricator.wikimedia.org/T70861#3129780 FETCH_START_DELAY = 150, // ms. // The delay after which a FETCH_COMPLETE action should be dispatched. // // If the API endpoint responds faster than 500 ms (or, say, the API // response is served from the UA's cache), then we introduce a delay of // 500 - t to make the preview delay consistent to the user. FETCH_COMPLETE_TARGET_DELAY = 500, // ms. ABANDON_END_DELAY = 300; // ms. /** * Mixes in timing information to an action. * * Warning: the `baseAction` parameter is modified and returned. * * @param {Object} baseAction * @return {Object} */ function timedAction( baseAction ) { baseAction.timestamp = mw.now(); return baseAction; } /** * Represents Page Previews booting. * * When a Redux store is created, the `@@INIT` action is immediately * dispatched to it. To avoid overriding the term, we refer to booting rather * than initializing. * * Page Previews persists critical pieces of information to local storage. * Since reading from and writing to local storage are synchronous, Page * Previews is booted when the browser is idle (using * [`mw.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)) * so as not to impact latency-critical events. * * @param {Boolean} isEnabled See `isEnabled.js` * @param {mw.user} user * @param {ext.popups.UserSettings} userSettings * @param {Function} generateToken * @param {mw.Map} config The config of the MediaWiki client-side application, * i.e. `mw.config` * @returns {Object} */ actions.boot = function ( isEnabled, user, userSettings, generateToken, config ) { var editCount = config.get( 'wgUserEditCount' ), previewCount = userSettings.getPreviewCount(); return { type: types.BOOT, isEnabled: isEnabled, isNavPopupsEnabled: config.get( 'wgPopupsConflictsWithNavPopupGadget' ), sessionToken: user.sessionId(), pageToken: generateToken(), page: { title: config.get( 'wgTitle' ), namespaceID: config.get( 'wgNamespaceNumber' ), id: config.get( 'wgArticleId' ) }, user: { isAnon: user.isAnon(), editCount: editCount, previewCount: previewCount } }; }; /** * Represents Page Previews fetching data via the gateway. * * @param {ext.popups.Gateway} gateway * @param {Element} el * @param {String} token The unique token representing the link interaction that * triggered the fetch * @return {Redux.Thunk} */ actions.fetch = function ( gateway, el, token ) { var title = $( el ).data( 'page-previews-title' ); return function ( dispatch ) { var request; dispatch( timedAction( { type: types.FETCH_START, el: el, title: title } ) ); request = gateway.getPageSummary( title ) .then( function ( result ) { dispatch( timedAction( { type: types.FETCH_END, el: el } ) ); return result; } ) .fail( function () { dispatch( { type: types.FETCH_FAILED, el: el } ); } ); $.when( request, wait( FETCH_COMPLETE_TARGET_DELAY - FETCH_START_DELAY ) ) .then( function ( result ) { dispatch( timedAction( { type: types.FETCH_COMPLETE, el: el, result: result, token: token } ) ); } ); }; }; /** * Represents the user dwelling on a link, either by hovering over it with * their mouse or by focussing it using their keyboard or an assistive device. * * @param {Element} el * @param {Event} event * @param {ext.popups.Gateway} gateway * @param {Function} generateToken * @return {Redux.Thunk} */ actions.linkDwell = function ( el, event, gateway, generateToken ) { var token = generateToken(); return function ( dispatch, getState ) { var action = timedAction( { type: types.LINK_DWELL, el: el, event: event, token: token } ); // Has the new generated token been accepted? function isNewInteraction() { return getState().preview.activeToken === token; } dispatch( action ); if ( !isNewInteraction() ) { return; } wait( FETCH_START_DELAY ) .then( function () { var previewState = getState().preview; if ( previewState.enabled && isNewInteraction() ) { dispatch( actions.fetch( gateway, el, token ) ); } } ); }; }; /** * Represents the user abandoning a link, either by moving their mouse away * from it or by shifting focus to another UI element using their keyboard or * an assistive device, or abandoning a preview by moving their mouse away * from it. * * @return {Redux.Thunk} */ actions.abandon = function () { return function ( dispatch, getState ) { var token = getState().preview.activeToken; dispatch( timedAction( { type: types.ABANDON_START, token: token } ) ); wait( ABANDON_END_DELAY ) .then( function () { dispatch( { type: types.ABANDON_END, token: token } ); } ); }; }; /** * Represents the user clicking on a link with their mouse, keyboard, or an * assistive device. * * @param {Element} el * @return {Object} */ actions.linkClick = function ( el ) { return timedAction( { type: types.LINK_CLICK, el: el } ); }; /** * Represents the user dwelling on a preview with their mouse. * * @return {Object} */ actions.previewDwell = function () { return { type: types.PREVIEW_DWELL }; }; /** * Represents a preview being shown to the user. * * This action is dispatched by the `./changeListeners/render.js` change * listener. * * @param {String} token * @return {Object} */ actions.previewShow = function ( token ) { return timedAction( { type: types.PREVIEW_SHOW, token: token } ); }; /** * Represents the user clicking either the "Enable previews" footer menu link, * or the "cog" icon that's present on each preview. * * @return {Object} */ actions.showSettings = function () { return { type: types.SETTINGS_SHOW }; }; /** * Represents the user closing the settings dialog and saving their settings. * * @return {Object} */ actions.hideSettings = function () { return { type: types.SETTINGS_HIDE }; }; /** * Represents the user saving their settings. * * N.B. This action returns a Redux.Thunk not because it needs to perform * asynchronous work, but because it needs to query the global state for the * current enabled state. In order to keep the enabled state in a single * place (the preview reducer), we query it and dispatch it as `wasEnabled` * so that other reducers (like settings) can act on it without having to * duplicate the `enabled` state locally. * See doc/adr/0003-keep-enabled-state-only-in-preview-reducer.md for more * details. * * @param {Boolean} enabled if previews are enabled or not * @return {Redux.Thunk} */ actions.saveSettings = function ( enabled ) { return function ( dispatch, getState ) { dispatch( { type: types.SETTINGS_CHANGE, wasEnabled: getState().preview.enabled, enabled: enabled } ); }; }; /** * Represents the queued event being logged `changeListeners/eventLogging.js` * change listener. * * @return {Object} */ actions.eventLogged = function () { return { type: types.EVENT_LOGGED }; }; /** * Represents the queued statsv event being logged. * See `mw.popups.changeListeners.statsv` change listener. * * @return {Object} */ actions.statsvLogged = function () { return { type: types.STATSV_LOGGED }; }; module.exports = actions; /***/ }), /***/ "./src/changeListener.js": /***/ (function(module, exports) { /** * @typedef {Function} ext.popups.ChangeListener * @param {Object} prevState The previous state * @param {Object} state The current state */ /** * Registers a change listener, which is bound to the * [store](http://redux.js.org/docs/api/Store.html). * * A change listener is a function that is only invoked when the state in the * [store](http://redux.js.org/docs/api/Store.html) changes. N.B. that there * may not be a 1:1 correspondence with actions being dispatched to the store * and the state in the store changing. * * See [Store#subscribe](http://redux.js.org/docs/api/Store.html#subscribe) * for more information about what change listeners may and may not do. * * @param {Redux.Store} store * @param {ext.popups.ChangeListener} callback */ module.exports = function ( store, callback ) { // This function is based on the example in [the documentation for // Store#subscribe](http://redux.js.org/docs/api/Store.html#subscribe), // which was written by Dan Abramov. var state; store.subscribe( function () { var prevState = state; state = store.getState(); if ( prevState !== state ) { callback( prevState, state ); } } ); }; /***/ }), /***/ "./src/changeListeners/eventLogging.js": /***/ (function(module, exports) { var $ = jQuery; /** * Creates an instance of the event logging change listener. * * When an event is enqueued to be logged it'll be logged using the schema. * Since it's the responsibility of EventLogging (and the UA) to deliver * logged events, the `EVENT_LOGGED` is immediately dispatched rather than * waiting for some indicator of completion. * * @param {Object} boundActions * @param {mw.eventLog.Schema} schema * @return {ext.popups.ChangeListener} */ module.exports = function ( boundActions, schema ) { return function ( _, state ) { var eventLogging = state.eventLogging, event = eventLogging.event; if ( event ) { schema.log( $.extend( true, {}, eventLogging.baseData, event ) ); boundActions.eventLogged(); } }; }; /***/ }), /***/ "./src/changeListeners/footerLink.js": /***/ (function(module, exports) { var mw = window.mediaWiki, $ = jQuery; /** * Creates the link element and appends it to the footer element. * * The following elements are considered to be the footer element (highest * priority to lowest): * * # `#footer-places` * # `#f-list` * # The parent element of `#footer li`, which is either an `ol` or `ul`. * * @return {jQuery} The link element */ function createFooterLink() { var $link = $( '
  • ' ).append( $( '' ) .attr( 'href', '#' ) .text( mw.message( 'popups-settings-enable' ).text() ) ), $footer; // As yet, we don't know whether the link should be visible. $link.hide(); // From https://en.wikipedia.org/wiki/MediaWiki:Gadget-ReferenceTooltips.js, // which was written by Yair rand . $footer = $( '#footer-places, #f-list' ); if ( $footer.length === 0 ) { $footer = $( '#footer li' ).parent(); } $footer.append( $link ); return $link; } /** * Creates an instance of the footer link change listener. * * The change listener covers the following behaviour: * * * The "Enable previews" link (the "link") is appended to the footer menu * (see `createFooterLink` above). * * When Page Previews are disabled, then the link is shown; otherwise, the * link is hidden. * * When the user clicks the link, then the `showSettings` bound action * creator is called. * * @param {Object} boundActions * @return {ext.popups.ChangeListener} */ module.exports = function ( boundActions ) { var $footerLink; return function ( prevState, state ) { if ( $footerLink === undefined ) { $footerLink = createFooterLink(); $footerLink.click( function ( e ) { e.preventDefault(); boundActions.showSettings(); } ); } if ( state.settings.shouldShowFooterLink ) { $footerLink.show(); } else { $footerLink.hide(); } }; }; /***/ }), /***/ "./src/changeListeners/index.js": /***/ (function(module, exports, __webpack_require__) { module.exports = { footerLink: __webpack_require__( "./src/changeListeners/footerLink.js" ), eventLogging: __webpack_require__( "./src/changeListeners/eventLogging.js" ), linkTitle: __webpack_require__( "./src/changeListeners/linkTitle.js" ), render: __webpack_require__( "./src/changeListeners/render.js" ), settings: __webpack_require__( "./src/changeListeners/settings.js" ), statsv: __webpack_require__( "./src/changeListeners/statsv.js" ), syncUserSettings: __webpack_require__( "./src/changeListeners/syncUserSettings.js" ) }; /***/ }), /***/ "./src/changeListeners/linkTitle.js": /***/ (function(module, exports) { var $ = jQuery; /** * Creates an instance of the link title change listener. * * While the user dwells on a link, then it becomes the active link. The * change listener will remove a link's `title` attribute while it's the * active link. * * @return {ext.popups.ChangeListener} */ module.exports = function () { var title; /** * Destroys the title attribute of the element, storing its value in local * state so that it can be restored later (see `restoreTitleAttr`). * * @param {Element} el */ function destroyTitleAttr( el ) { var $el = $( el ); // Has the user dwelled on a link? If we've already removed its title // attribute, then NOOP. if ( title ) { return; } title = $el.attr( 'title' ); $el.attr( 'title', '' ); } /** * Restores the title attribute of the element. * * @param {Element} el */ function restoreTitleAttr( el ) { $( el ).attr( 'title', title ); title = undefined; } return function ( prevState, state ) { var hasPrevActiveLink = prevState && prevState.preview.activeLink; if ( !state.preview.enabled ) { return; } if ( hasPrevActiveLink ) { // Has the user dwelled on a link immediately after abandoning another // (remembering that the ABANDON_END action is delayed by // ~10e2 ms). if ( prevState.preview.activeLink !== state.preview.activeLink ) { restoreTitleAttr( prevState.preview.activeLink ); } } if ( state.preview.activeLink ) { destroyTitleAttr( state.preview.activeLink ); } }; }; /***/ }), /***/ "./src/changeListeners/render.js": /***/ (function(module, exports, __webpack_require__) { var renderer = __webpack_require__( "./src/renderer.js" ); /** * Creates an instance of the render change listener. * * FIXME: Remove hard coupling with renderer, inject it as a parameter * * Wire it up in index.js * * Fix tests to remove require mocking * * @param {ext.popups.PreviewBehavior} previewBehavior * @return {ext.popups.ChangeListener} */ module.exports = function ( previewBehavior ) { var preview; return function ( prevState, state ) { if ( state.preview.shouldShow && !preview ) { preview = renderer.render( state.preview.fetchResponse ); preview.show( state.preview.activeEvent, previewBehavior, state.preview.activeToken ); } else if ( !state.preview.shouldShow && preview ) { preview.hide(); preview = undefined; } }; }; /***/ }), /***/ "./src/changeListeners/settings.js": /***/ (function(module, exports) { /** * Creates an instance of the settings change listener. * * @param {Object} boundActions * @param {Object} render function that renders a jQuery el with the settings * @return {ext.popups.ChangeListener} */ module.exports = function ( boundActions, render ) { var settings; return function ( prevState, state ) { if ( !prevState ) { // Nothing to do on initialization return; } // Update global modal visibility if ( prevState.settings.shouldShow === false && state.settings.shouldShow === true ) { // Lazily instantiate the settings UI if ( !settings ) { settings = render( boundActions ); settings.appendTo( document.body ); } // Update the UI settings with the current settings settings.setEnabled( state.preview.enabled ); settings.show(); } else if ( prevState.settings.shouldShow === true && state.settings.shouldShow === false ) { settings.hide(); } // Update help visibility if ( prevState.settings.showHelp !== state.settings.showHelp ) { settings.toggleHelp( state.settings.showHelp ); } }; }; /***/ }), /***/ "./src/changeListeners/statsv.js": /***/ (function(module, exports) { /** * Creates an instance of the statsv change listener. * * The listener will log events to a statsv endpoint by delegating the work * to the `ext.wikimediaEvents` module which is added to the output page * by the WikimediaEvents extension. * * @param {Object} boundActions * @param {bool} isLoggingEnabled * @param {Function} track mw.track * @return {ext.popups.ChangeListener} */ module.exports = function ( boundActions, isLoggingEnabled, track ) { return function ( _, state ) { var statsv = state.statsv; if ( isLoggingEnabled && statsv.action ) { track( statsv.action, statsv.data ); boundActions.statsvLogged(); } }; }; /***/ }), /***/ "./src/changeListeners/syncUserSettings.js": /***/ (function(module, exports) { /** * Creates an instance of the user settings sync change listener. * * This change listener syncs certain parts of the state tree to user * settings when they change. * * Used for: * * * Enabled state: If the previews are enabled or disabled. * * Preview count: When the user dwells on a link for long enough that * a preview is shown, then their preview count will be incremented (see * `reducers/eventLogging.js`, and is persisted to local storage. * * @param {ext.popups.UserSettings} userSettings * @return {ext.popups.ChangeListener} */ module.exports = function ( userSettings ) { return function ( prevState, state ) { syncIfChanged( prevState, state, 'eventLogging', 'previewCount', userSettings.setPreviewCount ); syncIfChanged( prevState, state, 'preview', 'enabled', userSettings.setIsEnabled ); }; }; /** * Given a state tree, reducer and property, safely return the value of the * property if the reducer and property exist * @param {Object} state tree * @param {String} reducer key to access on the state tree * @param {String} prop key to access on the reducer key of the state tree * @return {*} */ function get( state, reducer, prop ) { return state[ reducer ] && state[ reducer ][ prop ]; } /** * Calls a sync function if the property prop on the property reducer on * the state trees has changed value. * @param {Object} prevState * @param {Object} state * @param {String} reducer key to access on the state tree * @param {String} prop key to access on the reducer key of the state tree * @param {Function} sync function to be called with the newest value if * changed */ function syncIfChanged( prevState, state, reducer, prop, sync ) { var current = get( state, reducer, prop ); if ( prevState && ( get( prevState, reducer, prop ) !== current ) ) { sync( current ); } } /***/ }), /***/ "./src/constants.js": /***/ (function(module, exports) { module.exports = { THUMBNAIL_SIZE: 300 * $.bracketedDevicePixelRatio(), EXTRACT_LENGTH: 525 }; /***/ }), /***/ "./src/counts.js": /***/ (function(module, exports) { /** * Return count bucket for the number of edits a user has made. * * The buckets are defined as part of * [the Popups schema](https://meta.wikimedia.org/wiki/Schema:Popups). * * Extracted from `mw.popups.schemaPopups.getEditCountBucket`. * * @param {Number} count * @return {String} */ function getEditCountBucket( count ) { var bucket; if ( count === 0 ) { bucket = '0'; } else if ( count >= 1 && count <= 4 ) { bucket = '1-4'; } else if ( count >= 5 && count <= 99 ) { bucket = '5-99'; } else if ( count >= 100 && count <= 999 ) { bucket = '100-999'; } else if ( count >= 1000 ) { bucket = '1000+'; } return bucket + ' edits'; } /** * Return count bucket for the number of previews a user has seen. * * If local storage isn't available - because the user has disabled it * or the browser doesn't support it - then then "unknown" is returned. * * The buckets are defined as part of * [the Popups schema](https://meta.wikimedia.org/wiki/Schema:Popups). * * Extracted from `mw.popups.getPreviewCountBucket`. * * @param {Number} count * @return {String} */ function getPreviewCountBucket( count ) { var bucket; if ( count === -1 ) { return 'unknown'; } if ( count === 0 ) { bucket = '0'; } else if ( count >= 1 && count <= 4 ) { bucket = '1-4'; } else if ( count >= 5 && count <= 20 ) { bucket = '5-20'; } else if ( count >= 21 ) { bucket = '21+'; } return bucket + ' previews'; } module.exports = { getPreviewCountBucket: getPreviewCountBucket, getEditCountBucket: getEditCountBucket }; /***/ }), /***/ "./src/gateway/mediawiki.js": /***/ (function(module, exports, __webpack_require__) { // Public and private cache lifetime (5 minutes) var CACHE_LIFETIME = 300, createModel = __webpack_require__( "./src/preview/model.js" ).createModel; /** * MediaWiki API gateway factory * * @param {mw.Api} api * @param {mw.ext.constants} config * @returns {ext.popups.Gateway} */ function createMediaWikiApiGateway( api, config ) { /** * Fetch page data from the API * * @param {String} title * @return {jQuery.Promise} */ function fetch( title ) { return api.get( { action: 'query', prop: 'info|extracts|pageimages|revisions|info', formatversion: 2, redirects: true, exintro: true, exchars: config.EXTRACT_LENGTH, // There is an added geometric limit on .mwe-popups-extract // so that text does not overflow from the card. explaintext: true, piprop: 'thumbnail', pithumbsize: config.THUMBNAIL_SIZE, pilicense: 'any', rvprop: 'timestamp', inprop: 'url', titles: title, smaxage: CACHE_LIFETIME, maxage: CACHE_LIFETIME, uselang: 'content' }, { headers: { 'X-Analytics': 'preview=1' } } ); } /** * Get the page summary from the api and transform the data * * @param {String} title * @returns {jQuery.Promise} */ function getPageSummary( title ) { return fetch( title ) .then( extractPageFromResponse ) .then( convertPageToModel ); } return { fetch: fetch, extractPageFromResponse: extractPageFromResponse, convertPageToModel: convertPageToModel, getPageSummary: getPageSummary }; } /** * Extract page data from the MediaWiki API response * * @param {Object} data API response data * @throws {Error} Throw an error if page data cannot be extracted, * i.e. if the response is empty, * @returns {Object} */ function extractPageFromResponse( data ) { if ( data.query && data.query.pages && data.query.pages.length ) { return data.query.pages[ 0 ]; } throw new Error( 'API response `query.pages` is empty.' ); } /** * Transform the MediaWiki API response to a preview model * * @param {Object} page * @returns {ext.popups.PreviewModel} */ function convertPageToModel( page ) { return createModel( page.title, page.canonicalurl, page.pagelanguagehtmlcode, page.pagelanguagedir, page.extract, page.thumbnail ); } module.exports = createMediaWikiApiGateway; /***/ }), /***/ "./src/gateway/rest.js": /***/ (function(module, exports, __webpack_require__) { var RESTBASE_ENDPOINT = '/api/rest_v1/page/summary/', RESTBASE_PROFILE = 'https://www.mediawiki.org/wiki/Specs/Summary/1.0.0', createModel = __webpack_require__( "./src/preview/model.js" ).createModel, mw = window.mediaWiki, $ = jQuery; /** * RESTBase gateway factory * * @param {Function} ajax function from jQuery for example * @param {ext.popups.constants} config set of configuration values * @returns {ext.popups.Gateway} */ function createRESTBaseGateway( ajax, config ) { /** * Fetch page data from the API * * @param {String} title * @return {jQuery.Promise} */ function fetch( title ) { return ajax( { url: RESTBASE_ENDPOINT + encodeURIComponent( title ), headers: { Accept: 'application/json; charset=utf-8' + 'profile="' + RESTBASE_PROFILE + '"' } } ); } /** * Get the page summary from the api and transform the data * * Do not treat 404 as a failure as we want to show a generic * preview for missing pages. * * @param {String} title * @returns {jQuery.Promise} */ function getPageSummary( title ) { var result = $.Deferred(); fetch( title ) .then( function( page ) { result.resolve( convertPageToModel( page, config.THUMBNAIL_SIZE ) ); }, function ( jqXHR ) { if ( jqXHR.status === 404 ) { result.resolve( convertPageToModel( { title: title, lang: '', dir: '', extract: '' }, 0 ) ); } else { result.reject(); } } ); return result.promise(); } return { fetch: fetch, convertPageToModel: convertPageToModel, getPageSummary: getPageSummary }; } /** * Resizes the thumbnail to the requested width, preserving its aspect ratio. * * The requested width is limited to that of the original image unless the image * is an SVG, which can be scaled infinitely. * * This function is only intended to mangle the pretty thumbnail URLs used on * Wikimedia Commons. Once [an official thumb API](https://phabricator.wikimedia.org/T66214) * is fully specified and implemented, this function can be made more general. * * @param {Object} thumbnail The thumbnail image * @param {Object} original The original image * @param {int} thumbSize The requested size * @returns {Object} */ function generateThumbnailData( thumbnail, original, thumbSize ) { var parts = thumbnail.source.split( '/' ), lastPart = parts[ parts.length - 1 ], filename, width, height; // The last part, the thumbnail's full filename, is in the following form: // ${width}px-${filename}.${extension}. Splitting the thumbnail's filename // makes this function resilient to the thumbnail not having the same // extension as the original image, which is definitely the case for SVG's // where the thumbnail's extension is .svg.png. filename = lastPart.substr( lastPart.indexOf( 'px-' ) + 3 ); // Scale the thumbnail's largest dimension. if ( thumbnail.width > thumbnail.height ) { width = thumbSize; height = Math.floor( ( thumbSize / thumbnail.width ) * thumbnail.height ); } else { width = Math.floor( ( thumbSize / thumbnail.height ) * thumbnail.width ); height = thumbSize; } // If the image isn't an SVG, then it shouldn't be scaled past its original // dimensions. if ( width >= original.width && filename.indexOf( '.svg' ) === -1 ) { return original; } parts[ parts.length - 1 ] = width + 'px-' + filename; return { source: parts.join( '/' ), width: width, height: height }; } /** * Transform the rest API response to a preview model * * @param {Object} page * @param {int} thumbSize * @returns {ext.popups.PreviewModel} */ function convertPageToModel( page, thumbSize ) { return createModel( page.title, new mw.Title( page.title ).getUrl(), page.lang, page.dir, page.extract, page.thumbnail ? generateThumbnailData( page.thumbnail, page.originalimage, thumbSize ) : undefined ); } module.exports = createRESTBaseGateway; /***/ }), /***/ "./src/index.js": /***/ (function(module, exports, __webpack_require__) { var mw = mediaWiki, $ = jQuery, Redux = __webpack_require__( "./node_modules/redux/dist/redux.js" ), ReduxThunk = __webpack_require__( "./node_modules/redux-thunk/dist/redux-thunk.js" ), constants = __webpack_require__( "./src/constants.js" ), createRESTBaseGateway = __webpack_require__( "./src/gateway/rest.js" ), createMediaWikiApiGateway = __webpack_require__( "./src/gateway/mediawiki.js" ), createUserSettings = __webpack_require__( "./src/userSettings.js" ), createPreviewBehavior = __webpack_require__( "./src/previewBehavior.js" ), createSchema = __webpack_require__( "./src/schema.js" ), createSettingsDialogRenderer = __webpack_require__( "./src/settingsDialog.js" ), registerChangeListener = __webpack_require__( "./src/changeListener.js" ), createIsEnabled = __webpack_require__( "./src/isEnabled.js" ), processLinks = __webpack_require__( "./src/processLinks.js" ), renderer = __webpack_require__( "./src/renderer.js" ), statsvInstrumentation = __webpack_require__( "./src/statsvInstrumentation.js" ), changeListeners = __webpack_require__( "./src/changeListeners/index.js" ), actions = __webpack_require__( "./src/actions.js" ), reducers = __webpack_require__( "./src/reducers/index.js" ), BLACKLISTED_LINKS = [ '.extiw', '.image', '.new', '.internal', '.external', '.oo-ui-buttonedElement-button', '.cancelLink a' ]; /** * Creates a gateway with sensible values for the dependencies. * * @param {mw.Map} config * @return {ext.popups.Gateway} */ function createGateway( config ) { if ( config.get( 'wgPopupsAPIUseRESTBase' ) ) { return createRESTBaseGateway( $.ajax, constants ); } return createMediaWikiApiGateway( new mw.Api(), constants ); } /** * Subscribes the registered change listeners to the * [store](http://redux.js.org/docs/api/Store.html#store). * * @param {Redux.Store} store * @param {Object} actions * @param {ext.popups.UserSettings} userSettings * @param {Function} settingsDialog * @param {ext.popups.PreviewBehavior} previewBehavior * @param {bool} isStatsvLoggingEnabled * @param {Function} track mw.track */ function registerChangeListeners( store, actions, userSettings, settingsDialog, previewBehavior, isStatsvLoggingEnabled, track ) { registerChangeListener( store, changeListeners.footerLink( actions ) ); registerChangeListener( store, changeListeners.linkTitle() ); registerChangeListener( store, changeListeners.render( previewBehavior ) ); registerChangeListener( store, changeListeners.statsv( actions, isStatsvLoggingEnabled, track ) ); registerChangeListener( store, changeListeners.syncUserSettings( userSettings ) ); registerChangeListener( store, changeListeners.settings( actions, settingsDialog ) ); } /* * Initialize the application by: * 1. Creating the state store * 2. Binding the actions to such store * 3. Trigger the boot action to bootstrap the system * 4. When the page content is ready: * - Process the eligible links for page previews * - Initialize the renderer * - Bind hover and click events to the eligible links to trigger actions */ mw.requestIdleCallback( function () { var compose = Redux.compose, store, boundActions, // So-called "services". generateToken = mw.user.generateRandomSessionId, gateway = createGateway( mw.config ), userSettings, settingsDialog, isEnabled, schema, previewBehavior, isStatsvLoggingEnabled; userSettings = createUserSettings( mw.storage ); settingsDialog = createSettingsDialogRenderer(); isStatsvLoggingEnabled = statsvInstrumentation.isEnabled( mw.user, mw.config, mw.experiments ); isEnabled = createIsEnabled( mw.user, userSettings, mw.config, mw.experiments ); // If debug mode is enabled, then enable Redux DevTools. if ( mw.config.get( 'debug' ) === true ) { // eslint-disable-next-line no-underscore-dangle compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; } store = Redux.createStore( Redux.combineReducers( reducers ), compose( Redux.applyMiddleware( ReduxThunk.default ) ) ); boundActions = Redux.bindActionCreators( actions, store.dispatch ); previewBehavior = createPreviewBehavior( mw.config, mw.user, boundActions ); registerChangeListeners( store, boundActions, userSettings, settingsDialog, previewBehavior, isStatsvLoggingEnabled, mw.track ); // Load EventLogging schema if possible... mw.loader.using( 'ext.eventLogging.Schema' ).done( function () { schema = createSchema( mw.config, window ); registerChangeListener( store, changeListeners.eventLogging( boundActions, schema ) ); } ); boundActions.boot( isEnabled, mw.user, userSettings, generateToken, mw.config ); mw.hook( 'wikipage.content' ).add( function ( $container ) { var previewLinks = processLinks( $container, BLACKLISTED_LINKS, mw.config ); renderer.init(); previewLinks .on( 'mouseover keyup', function ( event ) { boundActions.linkDwell( this, event, gateway, generateToken ); } ) .on( 'mouseout blur', function () { boundActions.abandon( this ); } ) .on( 'click', function () { boundActions.linkClick( this ); } ); } ); } ); window.Redux = Redux; window.ReduxThunk = ReduxThunk; /***/ }), /***/ "./src/isEnabled.js": /***/ (function(module, exports) { /** * Given the global state of the application, creates a function that gets * whether or not the user should have Page Previews enabled. * * Page Preview is disabled when the Navigation Popups gadget is enabled. * * If Page Previews is configured as a beta feature (see * `$wgPopupsBetaFeature`), the user must be logged in and have enabled the * beta feature in order to see previews. Logged out users won't be able * to see the feature. * * If Page Previews is configured as a preference, then the user must either * be logged in and have enabled the preference or be logged out and have not * disabled previews via the settings modal. Logged out users who have not * disabled or enabled the previews via the settings modal are sampled at * the sampling rate defined by `wgPopupsAnonsEnabledSamplingRate`. * * @param {mw.user} user The `mw.user` singleton instance * @param {Object} userSettings An object returned by `userSettings.js` * @param {mw.Map} config * @param {mw.experiments} experiments The `mw.experiments` singleton instance * * @return {Boolean} */ module.exports = function ( user, userSettings, config, experiments ) { if ( config.get( 'wgPopupsConflictsWithNavPopupGadget' ) ) { return false; } if ( !user.isAnon() ) { return config.get( 'wgPopupsShouldSendModuleToUser' ); } if ( config.get( 'wgPopupsBetaFeature' ) ) { return false; } if ( !userSettings.hasIsEnabled() ) { return isUserSampled( user, config, experiments ); } return userSettings.getIsEnabled(); }; /** * Is the user sampled based on a sampling rate? * * The sampling rate is taken from `wgPopupsAnonsEnabledSamplingRate` and * defaults to 0.9. * * @param {mw.user} user The `mw.user` singleton instance * @param {mw.Map} config * @param {mw.experiments} experiments The `mw.experiments` singleton instance * * @return {Boolean} */ function isUserSampled( user, config, experiments ) { var samplingRate = config.get( 'wgPopupsAnonsEnabledSamplingRate', 0.9 ), bucket = experiments.getBucket( { name: 'ext.Popups.visibility', enabled: true, buckets: { control: 1 - samplingRate, A: samplingRate } }, user.sessionId() ); return bucket === 'A'; } /***/ }), /***/ "./src/preview/model.js": /***/ (function(module, exports) { var TYPE_GENERIC = 'generic', TYPE_PAGE = 'page'; /** * @typedef {Object} ext.popups.PreviewModel * @property {String} title * @property {String} url The canonical URL of the page being previewed * @property {String} languageCode * @property {String} languageDirection Either "ltr" or "rtl" * @property {String|undefined} extract `undefined` if the extract isn't * viable, e.g. if it's empty after having ellipsis and parentheticals * removed * @property {String} type Either "EXTRACT" or "GENERIC" * @property {Object|undefined} thumbnail */ /** * Creates a preview model. * * @param {String} title * @param {String} url The canonical URL of the page being previewed * @param {String} languageCode * @param {String} languageDirection Either "ltr" or "rtl" * @param {String} extract * @param {Object|undefined} thumbnail * @return {ext.popups.PreviewModel} */ function createModel( title, url, languageCode, languageDirection, extract, thumbnail ) { var processedExtract = processExtract( extract ), result = { title: title, url: url, languageCode: languageCode, languageDirection: languageDirection, extract: processedExtract, type: processedExtract === undefined ? TYPE_GENERIC : TYPE_PAGE, thumbnail: thumbnail }; return result; } /** * Processes the extract returned by the TextExtracts MediaWiki API query * module. * * @param {String|undefined} extract * @return {String|undefined} */ function processExtract( extract ) { var result; if ( extract === undefined || extract === '' ) { return undefined; } result = extract; result = removeParentheticals( result ); result = removeEllipsis( result ); return result.length > 0 ? result : undefined; } /** * Removes the trailing ellipsis from the extract, if it's there. * * This function was extracted from * `mw.popups.renderer.article#removeEllipsis`. * * @param {String} extract * @return {String} */ function removeEllipsis( extract ) { return extract.replace( /\.\.\.$/, '' ); } /** * Removes parentheticals from the extract. * * If the parenthesis are unbalanced or out of order, then the extract is * returned without further processing. * * This function was extracted from * `mw.popups.renderer.article#removeParensFromText`. * * @param {String} extract * @return {String} */ function removeParentheticals( extract ) { var ch, result = '', level = 0, i = 0; for ( i; i < extract.length; i++ ) { ch = extract.charAt( i ); if ( ch === ')' && level === 0 ) { return extract; } if ( ch === '(' ) { level++; continue; } else if ( ch === ')' ) { level--; continue; } if ( level === 0 ) { // Remove leading spaces before brackets if ( ch === ' ' && extract.charAt( i + 1 ) === '(' ) { continue; } result += ch; } } return ( level === 0 ) ? result : extract; } module.exports = { /** * @constant {String} */ TYPE_GENERIC: TYPE_GENERIC, /** * @constant {String} */ TYPE_PAGE: TYPE_PAGE, createModel: createModel }; /***/ }), /***/ "./src/previewBehavior.js": /***/ (function(module, exports) { var mw = window.mediaWiki, $ = jQuery; /** * A collection of event handlers specific to how the user interacts with all * previews. The event handlers are are agnostic to how/when they are bound * //but not to what they are bound//, i.e. the showSettings event handler is * written to be bound to either an `` or `