2017-02-02 02:24:12 +00:00
/******/ ( 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 ] = {
2017-03-03 10:24:45 +00:00
/******/ i : moduleId ,
/******/ l : false ,
/******/ exports : { }
2017-02-02 02:24:12 +00:00
/******/ } ;
/******/
/******/ // Execute the module function
/******/ modules [ moduleId ] . call ( module . exports , module , module . exports , _ _webpack _require _ _ ) ;
/******/
/******/ // Flag the module as loaded
2017-03-03 10:24:45 +00:00
/******/ module . l = true ;
2017-02-02 02:24:12 +00:00
/******/
/******/ // 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 ;
/******/
2017-03-03 10:24:45 +00:00
/******/ // 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 ) ; } ;
/******/
2017-02-02 02:24:12 +00:00
/******/ // __webpack_public_path__
/******/ _ _webpack _require _ _ . p = "" ;
/******/
2017-03-03 10:24:45 +00:00
/******/ // 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 = "" ;
2017-02-02 02:24:12 +00:00
/******/ // Load entry module and return exports
/******/ return _ _webpack _require _ _ ( 0 ) ;
/******/ } )
/************************************************************************/
/******/ ( [
/* 0 */
/***/ function ( module , exports , _ _webpack _require _ _ ) {
2016-11-08 19:42:21 +00:00
2017-03-03 10:24:45 +00:00
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 ) ;
} ;
} ;
} ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
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.' ) ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
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 ;
2017-02-14 20:19:12 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* 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 .
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* @ 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 ) ) ) .
2017-02-14 20:19:12 +00:00
* /
2016-11-08 19:42:21 +00:00
2017-03-03 10:24:45 +00:00
function compose ( ) {
for ( var _len = arguments . length , funcs = Array ( _len ) , _key = 0 ; _key < _len ; _key ++ ) {
funcs [ _key ] = arguments [ _key ] ;
}
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
if ( funcs . length === 0 ) {
return function ( arg ) {
return arg ;
} ;
}
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
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 ) ) ;
} ;
}
2017-02-14 20:19:12 +00:00
/***/ } ,
2017-03-03 10:14:37 +00:00
/* 2 */
/***/ function ( module , exports , _ _webpack _require _ _ ) {
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
'use strict' ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
exports . _ _esModule = true ;
exports . ActionTypes = undefined ;
exports [ 'default' ] = createStore ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
var _isPlainObject = _ _webpack _require _ _ ( 4 ) ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
var _isPlainObject2 = _interopRequireDefault ( _isPlainObject ) ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
var _symbolObservable = _ _webpack _require _ _ ( 12 ) ;
var _symbolObservable2 = _interopRequireDefault ( _symbolObservable ) ;
function _interopRequireDefault ( obj ) { return obj && obj . _ _esModule ? obj : { 'default' : obj } ; }
2017-02-14 20:19:12 +00:00
2017-02-14 20:17:32 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* 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 .
2017-02-14 20:17:32 +00:00
* /
2017-03-03 10:24:45 +00:00
var ActionTypes = exports . ActionTypes = {
INIT : '@@redux/INIT'
} ;
2017-02-14 20:17:32 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* 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 .
2017-02-14 20:17:32 +00:00
*
2017-03-03 10:24:45 +00:00
* 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 ` .
2017-02-28 14:28:40 +00:00
*
2017-03-03 10:24:45 +00:00
* @ param { Function } reducer A function that returns the next state tree , given
* the current state tree and the action to handle .
2017-02-28 14:28:40 +00:00
*
2017-03-03 10:24:45 +00:00
* @ 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() ` .
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* @ returns { Store } A Redux store that lets you read the state , dispatch actions
* and subscribe to changes .
2017-02-14 20:19:12 +00:00
* /
2017-03-03 10:24:45 +00:00
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 ;
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
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 ;
}
2017-02-02 02:24:12 +00:00
/***/ } ,
2017-03-03 10:24:45 +00:00
/* 3 */
2017-02-02 02:24:12 +00:00
/***/ function ( module , exports ) {
2017-03-03 10:24:45 +00:00
'use strict' ;
exports . _ _esModule = true ;
exports [ 'default' ] = warning ;
2017-02-14 20:19:12 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* Prints a warning in the console if it exists .
2017-02-02 02:24:12 +00:00
*
2017-03-03 10:24:45 +00:00
* @ param { String } message The warning message .
* @ returns { void }
2017-02-02 02:24:12 +00:00
* /
2017-03-03 10:24:45 +00:00
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 */
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
/***/ } ,
/* 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 ) ;
2017-02-14 20:16:25 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* Used to resolve the
* [ ` toStringTag ` ] ( http : //ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values .
2017-02-14 20:16:25 +00:00
* /
2017-03-03 10:24:45 +00:00
var objectToString = objectProto . toString ;
2017-02-14 20:19:12 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* 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
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* function Foo ( ) {
* this . a = 1 ;
* }
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* _ . isPlainObject ( new Foo ) ;
* // => false
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* _ . isPlainObject ( [ 1 , 2 , 3 ] ) ;
* // => false
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* _ . isPlainObject ( { 'x' : 0 , 'y' : 0 } ) ;
* // => true
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* _ . isPlainObject ( Object . create ( null ) ) ;
* // => true
2017-02-14 20:19:12 +00:00
* /
2017-03-03 10:24:45 +00:00
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 ) ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
module . exports = isPlainObject ;
2017-02-14 20:16:25 +00:00
2017-02-20 18:58:06 +00:00
/***/ } ,
2017-03-03 10:24:45 +00:00
/* 5 */
2017-02-20 18:58:06 +00:00
/***/ function ( module , exports , _ _webpack _require _ _ ) {
2017-03-03 10:24:45 +00:00
'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 } ; }
2017-02-20 18:58:06 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* 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 .
2017-02-20 18:58:06 +00:00
*
2017-03-03 10:24:45 +00:00
* See ` redux-thunk ` package as an example of the Redux middleware .
2017-02-20 18:58:06 +00:00
*
2017-03-03 10:24:45 +00:00
* 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 .
2017-02-20 18:58:06 +00:00
*
2017-03-03 10:24:45 +00:00
* @ param { ... Function } middlewares The middleware chain to be applied .
* @ returns { Function } A store enhancer applying the middleware .
2017-02-20 18:58:06 +00:00
* /
2017-03-03 10:24:45 +00:00
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 ) ;
2017-02-20 18:58:06 +00:00
2017-03-03 10:24:45 +00:00
return _extends ( { } , store , {
dispatch : _dispatch
} ) ;
} ;
} ;
}
2017-02-20 18:58:06 +00:00
2017-02-14 20:16:25 +00:00
/***/ } ,
2017-03-03 10:24:45 +00:00
/* 6 */
2017-02-14 20:16:25 +00:00
/***/ function ( module , exports ) {
2017-03-03 10:24:45 +00:00
'use strict' ;
exports . _ _esModule = true ;
exports [ 'default' ] = bindActionCreators ;
function bindActionCreator ( actionCreator , dispatch ) {
return function ( ) {
return dispatch ( actionCreator . apply ( undefined , arguments ) ) ;
} ;
}
2017-02-14 20:16:25 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* 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 .
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* For convenience , you can also pass a single function as the first argument ,
* and get a function in return .
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* @ 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 .
2017-02-14 20:16:25 +00:00
* /
2017-03-03 10:24:45 +00:00
function bindActionCreators ( actionCreators , dispatch ) {
if ( typeof actionCreators === 'function' ) {
return bindActionCreator ( actionCreators , dispatch ) ;
}
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
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 ;
}
2017-02-14 20:19:12 +00:00
/***/ } ,
2017-03-03 10:24:45 +00:00
/* 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.' ) ;
}
} ) ;
}
2017-02-14 20:19:12 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* 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 .
2017-02-14 20:16:40 +00:00
*
2017-03-03 10:24:45 +00:00
* @ 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 .
2017-02-14 20:16:40 +00:00
*
2017-03-03 10:24:45 +00:00
* @ returns { Function } A reducer function that invokes every reducer inside the
* passed object , and builds a state object with the same shape .
2017-02-14 20:16:40 +00:00
* /
2017-03-03 10:24:45 +00:00
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 ;
2017-02-14 20:16:25 +00:00
/***/ } ,
2017-03-03 10:14:37 +00:00
/* 9 */
2017-02-14 20:16:25 +00:00
/***/ function ( module , exports ) {
/ * *
2017-03-03 10:24:45 +00:00
* Checks if ` value ` is a host object in IE < 9.
2017-02-14 20:16:25 +00:00
*
2017-03-03 10:24:45 +00:00
* @ private
* @ param { * } value The value to check .
* @ returns { boolean } Returns ` true ` if ` value ` is a host object , else ` false ` .
2017-02-14 20:16:25 +00:00
* /
2017-03-03 10:24:45 +00:00
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 ;
2017-02-14 20:19:12 +00:00
/***/ } ,
2017-03-03 10:14:37 +00:00
/* 10 */
2017-02-14 20:19:12 +00:00
/***/ function ( module , exports ) {
2017-02-14 20:17:06 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* Creates a unary function that invokes ` func ` with its argument transformed .
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* @ private
* @ param { Function } func The function to wrap .
* @ param { Function } transform The argument transform .
* @ returns { Function } Returns the new function .
2017-02-14 20:19:12 +00:00
* /
2017-03-03 10:24:45 +00:00
function overArg ( func , transform ) {
return function ( arg ) {
return func ( transform ( arg ) ) ;
} ;
2017-02-14 20:19:12 +00:00
}
2017-02-14 20:16:25 +00:00
2017-03-03 10:24:45 +00:00
module . exports = overArg ;
2017-02-14 20:16:25 +00:00
/***/ } ,
2017-03-03 10:14:37 +00:00
/* 11 */
2017-02-14 20:16:25 +00:00
/***/ function ( module , exports ) {
/ * *
2017-03-03 10:24:45 +00:00
* Checks if ` value ` is object - like . A value is object - like if it ' s not ` null `
* and has a ` typeof ` result of "object" .
2017-02-14 20:16:25 +00:00
*
2017-03-03 10:24:45 +00:00
* @ 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
2017-02-14 20:16:25 +00:00
*
2017-03-03 10:24:45 +00:00
* _ . isObjectLike ( [ 1 , 2 , 3 ] ) ;
* // => true
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* _ . isObjectLike ( _ . noop ) ;
* // => false
*
* _ . isObjectLike ( null ) ;
* // => false
2017-02-14 20:16:25 +00:00
* /
2017-03-03 10:24:45 +00:00
function isObjectLike ( value ) {
return ! ! value && typeof value == 'object' ;
}
module . exports = isObjectLike ;
2017-02-14 20:16:25 +00:00
/***/ } ,
2017-03-03 10:14:37 +00:00
/* 12 */
2017-03-03 10:24:45 +00:00
/***/ function ( module , exports , _ _webpack _require _ _ ) {
2017-02-14 20:16:25 +00:00
2017-03-03 10:24:45 +00:00
module . exports = _ _webpack _require _ _ ( 13 ) ;
2017-02-14 20:16:25 +00:00
/***/ } ,
2017-03-03 10:14:37 +00:00
/* 13 */
2017-03-03 10:24:45 +00:00
/***/ function ( module , exports , _ _webpack _require _ _ ) {
2017-02-14 20:16:25 +00:00
2017-03-03 10:24:45 +00:00
/* 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 ;
2017-03-03 11:19:46 +00:00
}
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
var result = ( 0 , _ponyfill2 [ 'default' ] ) ( root ) ;
exports [ 'default' ] = result ;
/* WEBPACK VAR INJECTION */ } . call ( exports , ( function ( ) { return this ; } ( ) ) ) )
2017-02-14 20:19:12 +00:00
/***/ } ,
2017-03-03 10:14:37 +00:00
/* 14 */
2017-03-03 10:24:45 +00:00
/***/ function ( module , exports ) {
2017-02-14 20:16:25 +00:00
2017-03-03 10:24:45 +00:00
'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 ;
2017-02-14 20:19:12 +00:00
} else {
2017-03-03 10:24:45 +00:00
result = _Symbol ( 'observable' ) ;
_Symbol . observable = result ;
2017-02-14 20:19:12 +00:00
}
} else {
2017-03-03 10:24:45 +00:00
result = '@@observable' ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
2017-02-14 20:19:12 +00:00
return result ;
2017-02-02 02:24:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
/***/ }
/******/ ] )
} ) ;
;
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
/***/ } ) ,
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
/***/ "./src/actionTypes.js" :
/***/ ( function ( module , exports ) {
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
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 _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' ,
2017-03-07 00:27:38 +00:00
EVENT _LOGGED : 'EVENT_LOGGED' ,
STATSV _LOGGED : 'STATSV_LOGGED'
2017-03-03 10:24:45 +00:00
} ;
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
/***/ } ) ,
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
/***/ "./src/actions.js" :
/***/ ( function ( module , exports , _ _webpack _require _ _ ) {
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
var $ = jQuery ,
mw = window . mediaWiki ,
actions = { } ,
types = _ _webpack _require _ _ ( "./src/actionTypes.js" ) ,
wait = _ _webpack _require _ _ ( "./src/wait.js" ) ,
2017-03-24 05:33:50 +00:00
// See the following for context around this value.
//
// * https://phabricator.wikimedia.org/T161284
// * https://phabricator.wikimedia.org/T70861#3129780
FETCH _START _DELAY = 150 , // ms.
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
// The delay after which a FETCH_END 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
// 300 - t to make the preview delay consistent to the user.
FETCH _END _TARGET _DELAY = 500 , // ms.
2017-02-02 02:24:12 +00:00
2017-03-03 10:24:45 +00:00
ABANDON _END _DELAY = 300 ; // ms.
2016-11-15 18:18:13 +00:00
2017-03-03 10:24:45 +00:00
/ * *
* 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 ( ) ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
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
}
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
/ * *
* Represents Page Previews fetching data via the gateway .
*
* @ param { ext . popups . Gateway } gateway
* @ param { Element } el
* @ param { Date } started The time at which the interaction started .
* @ return { Redux . Thunk }
* /
actions . fetch = function ( gateway , el , started ) {
var title = $ ( el ) . data ( 'page-previews-title' ) ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
return function ( dispatch ) {
2017-03-07 00:27:38 +00:00
dispatch ( timedAction ( {
2017-03-03 10:24:45 +00:00
type : types . FETCH _START ,
el : el ,
title : title
2017-03-07 00:27:38 +00:00
} ) ) ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
gateway . getPageSummary ( title )
. fail ( function ( ) {
dispatch ( {
type : types . FETCH _FAILED ,
el : el
} ) ;
} )
. done ( function ( result ) {
var now = mw . now ( ) ,
delay ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
// If the API request has taken longer than the target delay, then
// don't delay any further.
delay = Math . max (
FETCH _END _TARGET _DELAY - Math . round ( now - started ) ,
0
) ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
wait ( delay )
. then ( function ( ) {
2017-03-07 00:27:38 +00:00
dispatch ( timedAction ( {
2017-03-03 10:24:45 +00:00
type : types . FETCH _END ,
el : el ,
2017-03-07 00:27:38 +00:00
result : result ,
delay : delay
} ) ) ;
2017-03-03 10:24:45 +00:00
} ) ;
} ) ;
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
/ * *
* 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 ( ) ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
return function ( dispatch , getState ) {
var action = timedAction ( {
type : types . LINK _DWELL ,
el : el ,
event : event ,
token : token
} ) ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
// Has the new generated token been accepted?
function isNewInteraction ( ) {
return getState ( ) . preview . activeToken === token ;
}
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
dispatch ( action ) ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
if ( ! isNewInteraction ( ) ) {
return ;
}
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
wait ( FETCH _START _DELAY )
. then ( function ( ) {
var previewState = getState ( ) . preview ;
if ( previewState . enabled && isNewInteraction ( ) ) {
dispatch ( actions . fetch ( gateway , el , action . timestamp ) ) ;
}
} ) ;
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
/ * *
* 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 ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
dispatch ( timedAction ( {
type : types . ABANDON _START ,
token : token
} ) ) ;
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
wait ( ABANDON _END _DELAY )
. then ( function ( ) {
dispatch ( {
type : types . ABANDON _END ,
token : token
2017-02-14 20:19:12 +00:00
} ) ;
} ) ;
} ;
2017-03-03 10:24:45 +00:00
} ;
/ * *
* 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
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
/ * *
* Represents a preview being shown to the user .
*
* This action is dispatched by the ` ./changeListeners/render.js ` change
* listener .
*
* @ return { Object }
* /
actions . previewShow = function ( ) {
return timedAction ( {
type : types . PREVIEW _SHOW
} ) ;
} ;
/ * *
* 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
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
/ * *
* Represents the user closing the settings dialog and saving their settings .
*
* @ return { Object }
* /
actions . hideSettings = function ( ) {
return {
type : types . SETTINGS _HIDE
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
/ * *
* 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
2017-02-14 20:19:12 +00:00
} ) ;
} ;
2017-03-03 10:24:45 +00:00
} ;
/ * *
* Represents the queued event being logged ` changeListeners/eventLogging.js `
* change listener .
*
* @ return { Object }
* /
actions . eventLogged = function ( ) {
return {
type : types . EVENT _LOGGED
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
2017-02-02 22:49:42 +00:00
2017-03-07 00:27:38 +00:00
/ * *
* Represents the queued statsv event being logged .
* See ` mw.popups.changeListeners.statsv ` change listener .
*
* @ return { Object }
* /
actions . statsvLogged = function ( ) {
return {
type : types . STATSV _LOGGED
} ;
} ;
2017-03-03 10:24:45 +00:00
module . exports = actions ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/***/ } ) ,
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/***/ "./src/changeListener.js" :
/***/ ( function ( module , exports ) {
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/ * *
* @ typedef { Function } ext . popups . ChangeListener
* @ param { Object } prevState The previous state
* @ param { Object } state The current state
* /
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/ * *
* 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.
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
var state ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
store . subscribe ( function ( ) {
var prevState = state ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
state = store . getState ( ) ;
if ( prevState !== state ) {
callback ( prevState , state ) ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
} ) ;
} ;
/***/ } ) ,
/***/ "./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 ( ) ;
2017-02-14 20:19:12 +00:00
}
} ;
2017-03-03 10:24:45 +00:00
} ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/***/ } ) ,
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/***/ "./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 = $ ( '<li>' ) . append (
$ ( '<a>' )
. 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 <https://en.wikipedia.org/wiki/User: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 ( ) ;
} ) ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
if ( state . settings . shouldShowFooterLink ) {
$footerLink . show ( ) ;
} else {
$footerLink . hide ( ) ;
2017-02-02 22:49:42 +00:00
}
} ;
2017-03-03 10:24:45 +00:00
} ;
2017-02-02 22:49:42 +00:00
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
/***/ } ) ,
/***/ "./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" ) ,
2017-03-07 00:27:38 +00:00
statsv : _ _webpack _require _ _ ( "./src/changeListeners/statsv.js" ) ,
2017-03-03 10:24:45 +00:00
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 ;
2017-02-02 22:49:42 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* Destroys the title attribute of the element , storing its value in local
* state so that it can be restored later ( see ` restoreTitleAttr ` ) .
2017-02-02 22:49:42 +00:00
*
2017-03-03 10:24:45 +00:00
* @ param { Element } el
2017-02-02 22:49:42 +00:00
* /
2017-03-03 10:24:45 +00:00
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 ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
title = $el . attr ( 'title' ) ;
$el . attr ( 'title' , '' ) ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
2017-02-14 20:19:12 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* Restores the title attribute of the element .
2017-02-14 20:19:12 +00:00
*
2017-03-03 10:24:45 +00:00
* @ param { Element } el
2017-02-14 20:19:12 +00:00
* /
2017-03-03 10:24:45 +00:00
function restoreTitleAttr ( el ) {
$ ( el ) . attr ( 'title' , title ) ;
title = undefined ;
2017-02-02 22:49:42 +00:00
}
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
return function ( prevState , state ) {
var hasPrevActiveLink = prevState && prevState . preview . activeLink ;
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
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 ) ;
} 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 ) ;
}
} ;
} ;
2017-03-07 00:27:38 +00:00
/***/ } ) ,
/***/ "./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 ( ) ;
}
} ;
} ;
2017-03-03 10:24:45 +00:00
/***/ } ) ,
/***/ "./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 = {
2017-02-16 20:38:26 +00:00
THUMBNAIL _SIZE : 300 * $ . bracketedDevicePixelRatio ( ) ,
EXTRACT _LENGTH : 525
2017-03-03 10:24:45 +00:00
} ;
/***/ } ) ,
/***/ "./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 _ _ ) {
2017-02-16 20:38:26 +00:00
// Public and private cache lifetime (5 minutes)
var CACHE _LIFETIME = 300 ,
2017-03-03 10:24:45 +00:00
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 ) {
2017-02-02 22:49:42 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* Fetch page data from the API
2017-02-02 22:49:42 +00:00
*
2017-03-03 10:24:45 +00:00
* @ param { String } title
* @ return { jQuery . Promise }
2017-02-02 22:49:42 +00:00
* /
2017-03-03 10:24:45 +00:00
function fetch ( title ) {
return api . get ( {
action : 'query' ,
prop : 'info|extracts|pageimages|revisions|info' ,
formatversion : 2 ,
redirects : true ,
exintro : true ,
2017-02-16 20:38:26 +00:00
exchars : config . EXTRACT _LENGTH ,
2017-03-03 10:24:45 +00:00
// 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 < ext . popups . PreviewModel > }
* /
function getPageSummary ( title ) {
return fetch ( title )
. then ( extractPageFromResponse )
. then ( convertPageToModel ) ;
}
return {
fetch : fetch ,
extractPageFromResponse : extractPageFromResponse ,
convertPageToModel : convertPageToModel ,
getPageSummary : getPageSummary
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
}
/ * *
* 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 ] ;
}
2017-02-14 20:19:12 +00:00
2017-03-03 10:24:45 +00:00
throw new Error ( 'API response `query.pages` is empty.' ) ;
}
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/ * *
* 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 ;
/ * *
* 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 ) {
2017-02-02 22:49:42 +00:00
/ * *
2017-03-03 10:24:45 +00:00
* Fetch page data from the API
2017-02-02 22:49:42 +00:00
*
2017-03-03 10:24:45 +00:00
* @ param { String } title
* @ return { jQuery . Promise }
2017-02-02 22:49:42 +00:00
* /
2017-03-03 10:24:45 +00:00
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
*
* @ param { String } title
* @ returns { jQuery . Promise < ext . popups . PreviewModel > }
* /
function getPageSummary ( title ) {
return fetch ( title )
. then ( function ( page ) {
return convertPageToModel ( page , config . THUMBNAIL _SIZE ) ;
} ) ;
}
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 ] ,
2017-03-10 10:21:53 +00:00
filename ,
width ,
height ;
2017-03-03 10:24:45 +00:00
// 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 ) ;
2017-03-10 10:21:53 +00:00
// 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
2017-03-03 10:24:45 +00:00
// dimensions.
2017-03-10 10:21:53 +00:00
if ( width >= original . width && filename . indexOf ( '.svg' ) === - 1 ) {
2017-03-03 10:24:45 +00:00
return original ;
}
2017-03-10 10:21:53 +00:00
parts [ parts . length - 1 ] = width + 'px-' + filename ;
2017-03-03 10:24:45 +00:00
return {
source : parts . join ( '/' ) ,
2017-03-10 10:21:53 +00:00
width : width ,
height : height
2017-03-03 10:24:45 +00:00
} ;
}
/ * *
* 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" ) ,
2017-03-07 00:27:38 +00:00
statsvInstrumentation = _ _webpack _require _ _ ( "./src/statsvInstrumentation.js" ) ,
2017-03-03 10:24:45 +00:00
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
2017-03-07 00:27:38 +00:00
* @ param { bool } isStatsvLoggingEnabled
* @ param { Function } track mw . track
2017-03-03 10:24:45 +00:00
* /
2017-03-16 21:03:21 +00:00
function registerChangeListeners ( store , actions , userSettings , settingsDialog , previewBehavior , isStatsvLoggingEnabled , track ) {
2017-03-03 10:24:45 +00:00
registerChangeListener ( store , changeListeners . footerLink ( actions ) ) ;
registerChangeListener ( store , changeListeners . linkTitle ( ) ) ;
registerChangeListener ( store , changeListeners . render ( previewBehavior ) ) ;
2017-03-07 00:27:38 +00:00
registerChangeListener ( store , changeListeners . statsv ( actions , isStatsvLoggingEnabled , track ) ) ;
2017-03-03 10:24:45 +00:00
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 ,
2017-03-14 11:28:58 +00:00
boundActions ,
2017-03-03 10:24:45 +00:00
// So-called "services".
generateToken = mw . user . generateRandomSessionId ,
gateway = createGateway ( mw . config ) ,
userSettings ,
settingsDialog ,
isEnabled ,
schema ,
2017-03-07 00:27:38 +00:00
previewBehavior ,
isStatsvLoggingEnabled ;
2017-03-03 10:24:45 +00:00
userSettings = createUserSettings ( mw . storage ) ;
settingsDialog = createSettingsDialogRenderer ( ) ;
2017-03-07 00:27:38 +00:00
isStatsvLoggingEnabled = statsvInstrumentation . isEnabled ( mw . user , mw . config , mw . experiments ) ;
2017-03-03 10:24:45 +00:00
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 (
2017-03-14 11:33:15 +00:00
Redux . combineReducers ( reducers ) ,
2017-03-03 10:24:45 +00:00
compose ( Redux . applyMiddleware (
ReduxThunk . default
) )
) ;
2017-03-14 11:28:58 +00:00
boundActions = Redux . bindActionCreators ( actions , store . dispatch ) ;
2017-03-03 10:24:45 +00:00
2017-03-14 11:28:58 +00:00
previewBehavior = createPreviewBehavior ( mw . config , mw . user , boundActions ) ;
2017-03-03 10:24:45 +00:00
2017-03-07 00:27:38 +00:00
registerChangeListeners (
2017-03-16 21:03:21 +00:00
store , boundActions , userSettings , settingsDialog ,
2017-03-07 00:27:38 +00:00
previewBehavior , isStatsvLoggingEnabled , mw . track
) ;
2017-03-03 10:24:45 +00:00
2017-03-16 21:03:21 +00:00
// Load EventLogging schema if possible...
mw . loader . using ( 'ext.eventLogging.Schema' ) . done ( function ( ) {
schema = createSchema ( mw . config , window ) ;
registerChangeListener ( store , changeListeners . eventLogging ( boundActions , schema ) ) ;
} ) ;
2017-03-14 11:28:58 +00:00
boundActions . boot (
2017-03-03 10:24:45 +00:00
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
2017-03-08 17:08:16 +00:00
. on ( 'mouseover keyup' , function ( event ) {
2017-03-14 11:28:58 +00:00
boundActions . linkDwell ( this , event , gateway , generateToken ) ;
2017-03-03 10:24:45 +00:00
} )
. on ( 'mouseout blur' , function ( ) {
2017-03-14 11:28:58 +00:00
boundActions . abandon ( this ) ;
2017-03-03 10:24:45 +00:00
} )
. on ( 'click' , function ( ) {
2017-03-14 11:28:58 +00:00
boundActions . linkClick ( this ) ;
2017-03-03 10:24:45 +00:00
} ) ;
} ) ;
} ) ;
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 .
*
* 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 ( ! 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 ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
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 ;
2017-02-14 20:19:12 +00:00
}
2017-03-03 10:24:45 +00:00
}
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 ;
/ * *
* @ typedef { Object } ext . popups . PreviewBehavior
* @ property { String } settingsUrl
* @ property { Function } showSettings
* @ property { Function } previewDwell
* @ property { Function } previewAbandon
2017-03-22 14:54:04 +00:00
* @ property { Function } click handler for the entire preview
2017-03-03 10:24:45 +00:00
* /
/ * *
* Creates an instance of ` ext.popups.PreviewBehavior ` .
*
* If the user is logged out , then clicking the cog should show the settings
* modal .
*
* If the user is logged in , then clicking the cog should send them to the
* Special : Preferences page with the "Beta features" tab open if Page Previews
* is enabled as a beta feature , or the "Appearance" tab otherwise .
*
* @ param { mw . Map } config
* @ param { mw . User } user
* @ param { Object } actions The action creators bound to the Redux store
* @ return { ext . popups . PreviewBehavior }
* /
module . exports = function ( config , user , actions ) {
var isBetaFeature = config . get ( 'wgPopupsBetaFeature' ) ,
rawTitle ,
settingsUrl ,
showSettings = $ . noop ;
if ( user . isAnon ( ) ) {
showSettings = function ( event ) {
event . preventDefault ( ) ;
actions . showSettings ( ) ;
} ;
} else {
rawTitle = 'Special:Preferences#mw-prefsection-' ;
rawTitle += isBetaFeature ? 'betafeatures' : 'rendering' ;
settingsUrl = mw . Title . newFromText ( rawTitle )
. getUrl ( ) ;
}
return {
settingsUrl : settingsUrl ,
showSettings : showSettings ,
previewDwell : actions . previewDwell ,
previewAbandon : actions . abandon ,
2017-03-22 14:54:04 +00:00
previewShow : actions . previewShow ,
click : actions . linkClick
2017-02-14 20:19:12 +00:00
} ;
2017-03-03 10:24:45 +00:00
} ;
2017-02-14 20:19:12 +00:00
2017-02-02 22:49:42 +00:00
2017-03-03 10:24:45 +00:00
/***/ } ) ,
/***/ "./src/processLinks.js" :
/***/ ( function ( module , exports ) {
var mw = window . mediaWiki ,
$ = jQuery ;
/ * *
* @ private
*
* Gets the title of a local page from an href given some configuration .
*
* @ param { String } href
* @ param { mw . Map } config
* @ return { String | undefined }
* /
function getTitle ( href , config ) {
var linkHref ,
matches ,
queryLength ,
titleRegex = new RegExp ( mw . RegExp . escape ( config . get ( 'wgArticlePath' ) )
. replace ( '\\$1' , '(.+)' ) ) ;
// Skip every URI that mw.Uri cannot parse
try {
linkHref = new mw . Uri ( href ) ;
} catch ( e ) {
return undefined ;
}
// External links
if ( linkHref . host !== location . hostname ) {
return undefined ;
}
queryLength = Object . keys ( linkHref . query ) . length ;
// No query params (pretty URL)
if ( ! queryLength ) {
matches = titleRegex . exec ( linkHref . path ) ;
return matches ? decodeURIComponent ( matches [ 1 ] ) : undefined ;
} else if ( queryLength === 1 && linkHref . query . hasOwnProperty ( 'title' ) ) {
// URL is not pretty, but only has a `title` parameter
return linkHref . query . title ;
}
return undefined ;
}
/ * *
* Processes and returns link elements ( or "`<a>`s" ) that are eligible for
* previews in a given container .
*
* An ` <a> ` is eligible for a preview if :
*
* * It has an href and a title , i . e . ` <a href="/wiki/Foo" title="Foo" /> ` .
* * It doesn ' t have any blacklisted CSS classes .
* * Its href is a valid URI of a page on the local wiki .
*
* If an ` <a> ` is eligible , then the title of the page on the local wiki is
* stored in the ` data-previews-page-title ` attribute for later reuse .
*
* @ param { jQuery } $container
* @ param { String [ ] } blacklist If an ` <a> ` has one or more of these CSS
* classes , then it will be ignored .
* @ param { mw . Map } config
*
* @ return { jQuery }
* /
function processLinks ( $container , blacklist , config ) {
var contentNamespaces ;
contentNamespaces = config . get ( 'wgContentNamespaces' ) ;
return $container
. find ( 'a[href][title]:not(' + blacklist . join ( ', ' ) + ')' )
. filter ( function ( ) {
var title ,
titleText = getTitle ( this . href , config ) ;
if ( ! titleText ) {
return false ;
}
// Is titleText in a content namespace?
title = mw . Title . newFromText ( titleText ) ;
if ( title && ( $ . inArray ( title . namespace , contentNamespaces ) >= 0 ) ) {
$ ( this ) . data ( 'page-previews-title' , titleText ) ;
return true ;
}
} ) ;
}
module . exports = processLinks ;
// Add processLinks to a global namespace to be tested in
// tests/qunit/ext.popups/processLinks.test.js
mw . popups = mw . popups || { } ;
mw . popups . processLinks = processLinks ;
/***/ } ) ,
/***/ "./src/reducers/eventLogging.js" :
/***/ ( function ( module , exports , _ _webpack _require _ _ ) {
var actionTypes = _ _webpack _require _ _ ( "./src/actionTypes.js" ) ,
nextState = _ _webpack _require _ _ ( "./src/reducers/nextState.js" ) ,
counts = _ _webpack _require _ _ ( "./src/counts.js" ) ;
/ * *
* Initialize the data that ' s shared between all events logged with [ the Popups
* schema ] ( https : //meta.wikimedia.org/wiki/Schema:Popups).
*
* @ param { Object } bootAction
* @ return { Object }
* /
function getBaseData ( bootAction ) {
var result = {
pageTitleSource : bootAction . page . title ,
namespaceIdSource : bootAction . page . namespaceID ,
pageIdSource : bootAction . page . id ,
isAnon : bootAction . user . isAnon ,
popupEnabled : bootAction . isEnabled ,
pageToken : bootAction . pageToken ,
sessionToken : bootAction . sessionToken ,
previewCountBucket : counts . getPreviewCountBucket ( bootAction . user . previewCount ) ,
hovercardsSuppressedByGadget : bootAction . isNavPopupsEnabled
} ;
if ( ! bootAction . user . isAnon ) {
result . editCountBucket = counts . getEditCountBucket ( bootAction . user . editCount ) ;
}
return result ;
}
2017-03-23 11:47:23 +00:00
/ * *
* Creates an event that , when mixed into the base data ( see ` getBaseData ` ) ,
* represents the user abandoning a link or preview .
*
* @ param { Object } interaction
* @ param { Number } endTimestamp
* @ return { Object }
* /
function createAbandonEvent ( interaction ) {
var result = {
linkInteractionToken : interaction . token ,
totalInteractionTime : Math . round ( interaction . finished - interaction . started )
} ;
// Has the preview been shown? If so, then, in the context of the
// instrumentation, then the preview has been dismissed by the user
// rather than the user has abandoned the link.
if ( interaction . timeToPreviewShow !== undefined ) {
result . action = 'dismissed' ;
result . previewType = interaction . previewType ;
} else {
result . action = 'dwelledButAbandoned' ;
}
return result ;
}
2017-03-03 10:24:45 +00:00
/ * *
* Reducer for actions that may result in an event being logged with the
* Popups schema via Event Logging .
*
* TODO : For obvious reasons , this reducer and the associated change listener
* are tightly bound to the Popups schema . This reducer must be
* renamed / moved if we introduce additional instrumentation .
*
* The base data represents data that ' s shared between all events . Very nearly
* all of it is initialized during the BOOT action ( see ` getBaseData ` ) and
* doesn ' t change between link interactions , e . g . the user being an anon or
* the number of edits they ' ve made .
*
* The user ' s number of previews , however , does change between link
* interactions and the associated bucket ( a computed property ) is what is
* logged . This is reflected in the state tree : the ` previewCount ` property is
* used to store the user ' s number of previews and the
* ` baseData.previewCountBucket ` property is used to store the associated
* bucket .
*
* @ param { Object } state
* @ param { Object } action
* @ return { Object } The state as a result of processing the action
* /
module . exports = function ( state , action ) {
2017-03-23 11:47:23 +00:00
var nextCount ;
2017-03-03 10:24:45 +00:00
if ( state === undefined ) {
state = {
previewCount : undefined ,
baseData : { } ,
interaction : undefined ,
event : undefined
} ;
}
switch ( action . type ) {
case actionTypes . BOOT :
return nextState ( state , {
previewCount : action . user . previewCount ,
baseData : getBaseData ( action ) ,
event : {
action : 'pageLoaded'
}
} ) ;
case actionTypes . EVENT _LOGGED :
return nextState ( state , {
event : undefined
} ) ;
case actionTypes . FETCH _END :
return nextState ( state , {
interaction : nextState ( state . interaction , {
previewType : action . result . type
} )
} ) ;
case actionTypes . PREVIEW _SHOW :
nextCount = state . previewCount + 1 ;
return nextState ( state , {
previewCount : nextCount ,
baseData : nextState ( state . baseData , {
previewCountBucket : counts . getPreviewCountBucket ( nextCount )
} ) ,
interaction : nextState ( state . interaction , {
timeToPreviewShow : action . timestamp - state . interaction . started
} )
} ) ;
case actionTypes . LINK _DWELL :
2017-03-21 12:37:58 +00:00
// Not a new interaction?
if ( state . interaction && action . el === state . interaction . link ) {
return nextState ( state , {
interaction : nextState ( state . interaction , {
isUserDwelling : true
} )
} ) ;
}
2017-03-03 10:24:45 +00:00
return nextState ( state , {
2017-03-21 12:37:58 +00:00
// TODO: Extract this object into a module that can be shared between
// this and the preview reducer.
2017-03-03 10:24:45 +00:00
interaction : {
2017-03-21 12:37:58 +00:00
link : action . el ,
2017-03-03 10:24:45 +00:00
token : action . token ,
2017-03-21 12:37:58 +00:00
started : action . timestamp ,
isUserDwelling : true
2017-03-03 10:24:45 +00:00
}
} ) ;
2017-03-21 12:37:58 +00:00
case actionTypes . PREVIEW _DWELL :
return nextState ( state , {
interaction : nextState ( state . interaction , {
isUserDwelling : true
} )
} ) ;
2017-03-03 10:24:45 +00:00
case actionTypes . LINK _CLICK :
return nextState ( state , {
event : {
action : 'opened' ,
linkInteractionToken : state . interaction . token ,
totalInteractionTime : Math . round ( action . timestamp - state . interaction . started )
}
} ) ;
case actionTypes . ABANDON _START :
return nextState ( state , {
interaction : nextState ( state . interaction , {
2017-03-21 12:37:58 +00:00
finished : action . timestamp ,
isUserDwelling : false
2017-03-03 10:24:45 +00:00
} )
} ) ;
case actionTypes . ABANDON _END :
2017-03-21 12:37:58 +00:00
if ( action . token === state . interaction . token && ! state . interaction . isUserDwelling ) {
return nextState ( state , {
interaction : undefined ,
2017-03-23 11:47:23 +00:00
event : createAbandonEvent ( state . interaction )
2017-03-21 12:37:58 +00:00
} ) ;
2017-03-03 10:24:45 +00:00
}
2017-03-21 12:37:58 +00:00
return state ;
2017-03-03 10:24:45 +00:00
2017-03-22 17:00:52 +00:00
case actionTypes . SETTINGS _SHOW :
return nextState ( state , {
event : {
action : 'tapped settings cog'
}
} ) ;
2017-03-03 10:24:45 +00:00
default :
return state ;
}
} ;
/***/ } ) ,
/***/ "./src/reducers/index.js" :
/***/ ( function ( module , exports , _ _webpack _require _ _ ) {
module . exports = {
eventLogging : _ _webpack _require _ _ ( "./src/reducers/eventLogging.js" ) ,
preview : _ _webpack _require _ _ ( "./src/reducers/preview.js" ) ,
2017-03-07 00:27:38 +00:00
settings : _ _webpack _require _ _ ( "./src/reducers/settings.js" ) ,
statsv : _ _webpack _require _ _ ( "./src/reducers/statsv.js" )
2017-03-03 10:24:45 +00:00
} ;
/***/ } ) ,
/***/ "./src/reducers/nextState.js" :
/***/ ( function ( module , exports ) {
/ * *
* Creates the next state tree from the current state tree and some updates .
*
* N . B . OO . copy doesn ' t copy Element instances , whereas $ . extend does .
* However , OO . copy does copy properties whose values are undefined or null ,
* whereas $ . extend doesn ' t . Since the state tree contains an Element instance
* - the preview . activeLink property - and we want to copy undefined / null into
* the state we need to manually iterate over updates and check with
* hasOwnProperty to copy over to the new state .
*
* In [ change listeners ] ( / d o c / c h a n g e _ l i s t e n e r s . m d ) , f o r e x a m p l e , w e t a l k a b o u t
* the previous state and the current state ( the ` prevState ` and ` state `
* parameters , respectively ) . Since
* [ reducers ] ( http : //redux.js.org/docs/basics/Reducers.html) take the current
* state and an action and make updates , "next state" seems appropriate .
*
* @ param { Object } state
* @ param { Object } updates
* @ return { Object }
* /
module . exports = function ( state , updates ) {
var result = { } ,
key ;
for ( key in state ) {
if ( state . hasOwnProperty ( key ) && ! updates . hasOwnProperty ( key ) ) {
result [ key ] = state [ key ] ;
}
}
for ( key in updates ) {
if ( updates . hasOwnProperty ( key ) ) {
result [ key ] = updates [ key ] ;
}
}
return result ;
} ;
/***/ } ) ,
/***/ "./src/reducers/preview.js" :
/***/ ( function ( module , exports , _ _webpack _require _ _ ) {
var actionTypes = _ _webpack _require _ _ ( "./src/actionTypes.js" ) ,
nextState = _ _webpack _require _ _ ( "./src/reducers/nextState.js" ) ;
/ * *
* Reducer for actions that modify the state of the preview model
*
* @ param { Object } state before action
* @ param { Object } action Redux action that modified state .
* Must have ` type ` property .
* @ return { Object } state after action
* /
module . exports = function ( state , action ) {
if ( state === undefined ) {
state = {
enabled : undefined ,
activeLink : undefined ,
activeEvent : undefined ,
activeToken : '' ,
shouldShow : false ,
isUserDwelling : false
} ;
}
switch ( action . type ) {
case actionTypes . BOOT :
return nextState ( state , {
enabled : action . isEnabled
} ) ;
case actionTypes . SETTINGS _CHANGE :
return nextState ( state , {
enabled : action . enabled
} ) ;
case actionTypes . LINK _DWELL :
// New interaction
if ( action . el !== state . activeLink ) {
return nextState ( state , {
activeLink : action . el ,
activeEvent : action . event ,
activeToken : action . token ,
// When the user dwells on a link with their keyboard, a preview is
// renderered, and then dwells on another link, the ABANDON_END
// action will be ignored.
//
// Ensure that all the preview is hidden.
shouldShow : false ,
isUserDwelling : true
} ) ;
} else {
// Dwelling back into the same link
return nextState ( state , {
isUserDwelling : true
} ) ;
}
case actionTypes . ABANDON _END :
if ( action . token === state . activeToken && ! state . isUserDwelling ) {
return nextState ( state , {
activeLink : undefined ,
activeToken : undefined ,
activeEvent : undefined ,
fetchResponse : undefined ,
shouldShow : false
} ) ;
}
return state ;
case actionTypes . PREVIEW _DWELL :
return nextState ( state , {
isUserDwelling : true
} ) ;
case actionTypes . ABANDON _START :
return nextState ( state , {
isUserDwelling : false
} ) ;
case actionTypes . FETCH _START :
return nextState ( state , {
fetchResponse : undefined
} ) ;
case actionTypes . FETCH _END :
if ( action . el === state . activeLink ) {
return nextState ( state , {
fetchResponse : action . result ,
shouldShow : true
} ) ;
}
/* falls through */
default :
return state ;
}
} ;
/***/ } ) ,
/***/ "./src/reducers/settings.js" :
/***/ ( function ( module , exports , _ _webpack _require _ _ ) {
var actionTypes = _ _webpack _require _ _ ( "./src/actionTypes.js" ) ,
nextState = _ _webpack _require _ _ ( "./src/reducers/nextState.js" ) ;
/ * *
* Reducer for actions that modify the state of the settings
*
* @ param { Object } state
* @ param { Object } action
* @ return { Object } state after action
* /
module . exports = function ( state , action ) {
if ( state === undefined ) {
state = {
shouldShow : false ,
showHelp : false ,
shouldShowFooterLink : false
} ;
}
switch ( action . type ) {
case actionTypes . SETTINGS _SHOW :
return nextState ( state , {
shouldShow : true ,
showHelp : false
} ) ;
case actionTypes . SETTINGS _HIDE :
return nextState ( state , {
shouldShow : false ,
showHelp : false
} ) ;
case actionTypes . SETTINGS _CHANGE :
return action . wasEnabled === action . enabled ?
// If the setting is the same, just hide the dialogs
nextState ( state , {
shouldShow : false
} ) :
// If the settings have changed...
nextState ( state , {
// If we enabled, we just hide directly, no help
// If we disabled, keep it showing and let the ui show the help.
shouldShow : ! action . enabled ,
showHelp : ! action . enabled ,
// Since the footer link is only ever shown to anonymous users (see
// the BOOT case below), state.userIsAnon is always truthy here.
shouldShowFooterLink : ! action . enabled
} ) ;
case actionTypes . BOOT :
return nextState ( state , {
shouldShowFooterLink : action . user . isAnon && ! action . isEnabled
} ) ;
default :
return state ;
}
} ;
2017-03-07 00:27:38 +00:00
/***/ } ) ,
/***/ "./src/reducers/statsv.js" :
/***/ ( function ( module , exports , _ _webpack _require _ _ ) {
var actionTypes = _ _webpack _require _ _ ( "./src/actionTypes.js" ) ,
nextState = _ _webpack _require _ _ ( "./src/reducers/nextState.js" ) ;
/ * *
* Reducer for actions that may result in an event being logged via statsv .
*
* @ param { Object } state
* @ param { Object } action
* @ return { Object } state after action
* /
module . exports = function ( state , action ) {
state = state || { } ;
switch ( action . type ) {
case actionTypes . FETCH _START :
return nextState ( state , {
fetchStartedAt : action . timestamp
} ) ;
case actionTypes . FETCH _END :
return nextState ( state , {
action : 'timing.PagePreviewsApiResponse' ,
data : action . timestamp - state . fetchStartedAt - action . delay
} ) ;
case actionTypes . FETCH _FAILED :
return nextState ( state , {
action : 'counter.PagePreviewsApiFailure' ,
data : 1
} ) ;
case actionTypes . LINK _DWELL :
return nextState ( state , {
linkDwellStartedAt : action . timestamp
} ) ;
case actionTypes . PREVIEW _SHOW :
return nextState ( state , {
action : 'timing.PagePreviewsPreviewShow' ,
data : action . timestamp - state . linkDwellStartedAt
} ) ;
case actionTypes . STATSV _LOGGED :
return nextState ( state , {
action : null ,
data : null
} ) ;
default :
return state ;
}
} ;
2017-03-03 10:24:45 +00:00
/***/ } ) ,
/***/ "./src/renderer.js" :
/***/ ( function ( module , exports , _ _webpack _require _ _ ) {
var mw = window . mediaWiki ,
$ = jQuery ,
isSafari = navigator . userAgent . match ( /Safari/ ) !== null ,
wait = _ _webpack _require _ _ ( "./src/wait.js" ) ,
SIZES = {
portraitImage : {
h : 250 , // Exact height
w : 203 // Max width
} ,
landscapeImage : {
h : 200 , // Max height
w : 300 // Exact Width
} ,
landscapePopupWidth : 450 ,
portraitPopupWidth : 300 ,
pokeySize : 8 // Height of the pokey.
} ,
$window = $ ( window ) ;
/ * *
* Extracted from ` mw.popups.createSVGMasks ` .
* /
function createPokeyMasks ( ) {
$ ( '<div>' )
. attr ( 'id' , 'mwe-popups-svg' )
. html (
'<svg width="0" height="0">' +
'<defs>' +
'<clippath id="mwe-popups-mask">' +
'<polygon points="0 8, 10 8, 18 0, 26 8, 1000 8, 1000 1000, 0 1000"/>' +
'</clippath>' +
'<clippath id="mwe-popups-mask-flip">' +
'<polygon points="0 8, 274 8, 282 0, 290 8, 1000 8, 1000 1000, 0 1000"/>' +
'</clippath>' +
'<clippath id="mwe-popups-landscape-mask">' +
'<polygon points="0 8, 174 8, 182 0, 190 8, 1000 8, 1000 1000, 0 1000"/>' +
'</clippath>' +
'<clippath id="mwe-popups-landscape-mask-flip">' +
'<polygon points="0 0, 1000 0, 1000 242, 190 242, 182 250, 174 242, 0 242"/>' +
'</clippath>' +
'</defs>' +
'</svg>'
)
. appendTo ( document . body ) ;
}
/ * *
* Initializes the renderer .
* /
function init ( ) {
createPokeyMasks ( ) ;
}
/ * *
* The model of how a view is rendered , which is constructed from a response
* from the gateway .
*
* TODO : Rename ` isTall ` to ` isPortrait ` .
*
* @ typedef { Object } ext . popups . Preview
* @ property { jQuery } el
* @ property { Boolean } hasThumbnail
* @ property { Object } thumbnail
* @ property { Boolean } isTall Sugar around
* ` preview.hasThumbnail && thumbnail.isTall `
* /
/ * *
* Renders a preview given data from the { @ link gateway ext . popups . Gateway } .
* The preview is rendered and added to the DOM but will remain hidden until
* the ` show ` method is called .
*
* Previews are rendered at :
*
* # The position of the mouse when the user dwells on the link with their
* mouse .
* # The centermost point of the link when the user dwells on the link with
* their keboard or other assistive device .
*
* Since the content of the preview doesn ' t change but its position might , we
* distinguish between "rendering" - generating HTML from a MediaWiki API
* response - and "showing/hiding" - positioning the layout and changing its
* orientation , if necessary .
*
* @ param { ext . popups . PreviewModel } model
* @ return { ext . popups . Preview }
* /
function render ( model ) {
var preview = model . extract === undefined ? createEmptyPreview ( model ) : createPreview ( model ) ;
return {
/ * *
* Shows the preview given an event representing the user ' s interaction
* with the active link , e . g . an instance of
* [ MouseEvent ] ( https : //developer.mozilla.org/en/docs/Web/API/MouseEvent).
*
* See ` show ` for more detail .
*
* @ param { Event } event
* @ param { Object } boundActions The
* [ bound action creators ] ( http : //redux.js.org/docs/api/bindActionCreators.html)
* that were ( likely ) created in [ boot . js ] ( . / boot . js ) .
* @ return { jQuery . Promise }
* /
show : function ( event , boundActions ) {
return show ( preview , event , boundActions ) ;
} ,
/ * *
* Hides the preview .
*
* See ` hide ` for more detail .
*
* @ return { jQuery . Promise }
* /
hide : function ( ) {
return hide ( preview ) ;
}
} ;
}
/ * *
* Creates an instance of the DTO backing a preview .
*
* @ param { ext . popups . PreviewModel } model
* @ return { ext . popups . Preview }
* /
function createPreview ( model ) {
var templateData ,
thumbnail = createThumbnail ( model . thumbnail ) ,
hasThumbnail = thumbnail !== null ,
// FIXME: This should probably be moved into the gateway as we'll soon be
// fetching HTML from the API. See
// https://phabricator.wikimedia.org/T141651 for more detail.
extract = renderExtract ( model . extract , model . title ) ,
$el ;
templateData = $ . extend ( { } , model , {
hasThumbnail : hasThumbnail
} ) ;
$el = mw . template . get ( 'ext.popups' , 'preview.mustache' )
. render ( templateData ) ;
if ( hasThumbnail ) {
$el . find ( '.mwe-popups-discreet' ) . append ( thumbnail . el ) ;
}
if ( extract . length ) {
$el . find ( '.mwe-popups-extract' ) . append ( extract ) ;
}
return {
el : $el ,
hasThumbnail : hasThumbnail ,
thumbnail : thumbnail ,
isTall : hasThumbnail && thumbnail . isTall
} ;
}
/ * *
* Creates an instance of the DTO backing a preview . In this case the DTO
* represents a generic preview , which covers the following scenarios :
*
* * The page doesn ' t exist , i . e . the user hovered over a redlink or a
* redirect to a page that doesn ' t exist .
* * The page doesn ' t have a viable extract .
*
* @ param { ext . popups . PreviewModel } model
* @ return { ext . popups . Preview }
* /
function createEmptyPreview ( model ) {
var templateData ,
$el ;
templateData = $ . extend ( { } , model , {
extractMsg : mw . msg ( 'popups-preview-no-preview' ) ,
readMsg : mw . msg ( 'popups-preview-footer-read' )
} ) ;
$el = mw . template . get ( 'ext.popups' , 'preview-empty.mustache' )
. render ( templateData ) ;
return {
el : $el ,
hasThumbnail : false ,
isTall : false
} ;
}
/ * *
* Converts the extract into a list of elements , which correspond to fragments
* of the extract . Fragements that match the title verbatim are wrapped in a
* ` <b> ` element .
*
* Using the bolded elements of the extract of the page directly is covered by
* [ T141651 ] ( https : //phabricator.wikimedia.org/T141651).
*
* Extracted from ` mw.popups.renderer.article.getProcessedElements ` .
*
* @ param { String } extract
* @ param { String } title
* @ return { Array }
* /
function renderExtract ( extract , title ) {
var regExp , escapedTitle ,
elements = [ ] ,
boldIdentifier = '<bi-' + Math . random ( ) + '>' ,
snip = '<snip-' + Math . random ( ) + '>' ;
title = title . replace ( /\s+/g , ' ' ) . trim ( ) ; // Remove extra white spaces
escapedTitle = mw . RegExp . escape ( title ) ; // Escape RegExp elements
regExp = new RegExp ( '(^|\\s)(' + escapedTitle + ')(|$)' , 'i' ) ;
// Remove text in parentheses along with the parentheses
extract = extract . replace ( /\s+/ , ' ' ) ; // Remove extra white spaces
// Make title bold in the extract text
// As the extract is html escaped there can be no such string in it
// Also, the title is escaped of RegExp elements thus can't have "*"
extract = extract . replace ( regExp , '$1' + snip + boldIdentifier + '$2' + snip + '$3' ) ;
extract = extract . split ( snip ) ;
$ . each ( extract , function ( index , part ) {
if ( part . indexOf ( boldIdentifier ) === 0 ) {
elements . push ( $ ( '<b>' ) . text ( part . substring ( boldIdentifier . length ) ) ) ;
} else {
elements . push ( document . createTextNode ( part ) ) ;
}
} ) ;
return elements ;
}
/ * *
* Shows the preview .
*
* Extracted from ` mw.popups.render.openPopup ` .
*
* TODO : From the perspective of the client , there ' s no need to distinguish
* between renderering and showing a preview . Merge # render and Preview # show .
*
* @ param { ext . popups . Preview } preview
* @ param { Event } event
* @ param { ext . popups . PreviewBehavior } behavior
* @ return { jQuery . Promise } A promise that resolves when the promise has faded
* in
* /
function show ( preview , event , behavior ) {
var layout = createLayout ( preview , event ) ;
preview . el . appendTo ( document . body ) ;
// Hack to "refresh" the SVG so that it's displayed.
//
// Elements get added to the DOM and not to the screen because of different
// namespaces of HTML and SVG.
//
// See http://stackoverflow.com/a/13654655/366138 for more detail.
//
// TODO: Find out how early on in the render that this could be done, e.g.
// createThumbnail?
preview . el . html ( preview . el . html ( ) ) ;
layoutPreview ( preview , layout ) ;
preview . el . hover ( behavior . previewDwell , behavior . previewAbandon ) ;
2017-03-22 14:54:04 +00:00
preview . el . click ( behavior . click ) ;
2017-03-03 10:24:45 +00:00
preview . el . find ( '.mwe-popups-settings-icon' )
. attr ( 'href' , behavior . settingsUrl )
2017-03-22 14:54:04 +00:00
. click ( function ( event ) {
event . stopPropagation ( ) ;
behavior . showSettings ( ) ;
} ) ;
2017-03-03 10:24:45 +00:00
preview . el . show ( ) ;
return wait ( 200 )
. then ( behavior . previewShow ) ;
}
/ * *
* Extracted from ` mw.popups.render.closePopup ` .
*
* @ param { ext . popups . Preview } preview
* @ return { jQuery . Promise } A promise that resolves when the preview has faded
* out
* /
function hide ( preview ) {
var fadeInClass ,
fadeOutClass ;
// FIXME: This method clearly needs access to the layout of the preview.
fadeInClass = ( preview . el . hasClass ( 'mwe-popups-fade-in-up' ) ) ?
'mwe-popups-fade-in-up' :
'mwe-popups-fade-in-down' ;
fadeOutClass = ( fadeInClass === 'mwe-popups-fade-in-up' ) ?
'mwe-popups-fade-out-down' :
'mwe-popups-fade-out-up' ;
preview . el
. removeClass ( fadeInClass )
. addClass ( fadeOutClass ) ;
return wait ( 150 ) . then ( function ( ) {
preview . el . remove ( ) ;
} ) ;
}
/ * *
* @ typedef { Object } ext . popups . Thumbnail
* @ property { Element } el
* @ property { Boolean } isTall Whether or not the thumbnail is portrait
* /
/ * *
* Creates a thumbnail from the representation of a thumbnail returned by the
* PageImages MediaWiki API query module .
*
* If there 's no thumbnail, the thumbnail is too small, or the thumbnail' s URL
* contains characters that could be used to perform an
* [ XSS attack via CSS ] ( https : //www.owasp.org/index.php/Testing_for_CSS_Injection_(OTG-CLIENT-005)),
* then ` null ` is returned .
*
* Extracted from ` mw.popups.renderer.article.createThumbnail ` .
*
* @ param { Object } rawThumbnail
* @ return { ext . popups . Thumbnail | null }
* /
function createThumbnail ( rawThumbnail ) {
var tall , thumbWidth , thumbHeight ,
x , y , width , height , clipPath ,
devicePixelRatio = $ . bracketedDevicePixelRatio ( ) ;
if ( ! rawThumbnail ) {
return null ;
}
tall = rawThumbnail . width < rawThumbnail . height ;
thumbWidth = rawThumbnail . width / devicePixelRatio ;
thumbHeight = rawThumbnail . height / devicePixelRatio ;
if (
// Image too small for landscape display
( ! tall && thumbWidth < SIZES . landscapeImage . w ) ||
// Image too small for portrait display
( tall && thumbHeight < SIZES . portraitImage . h ) ||
// These characters in URL that could inject CSS and thus JS
(
rawThumbnail . source . indexOf ( '\\' ) > - 1 ||
rawThumbnail . source . indexOf ( '\'' ) > - 1 ||
rawThumbnail . source . indexOf ( '\"' ) > - 1
)
) {
return null ;
}
if ( tall ) {
x = ( thumbWidth > SIZES . portraitImage . w ) ?
( ( thumbWidth - SIZES . portraitImage . w ) / - 2 ) :
( SIZES . portraitImage . w - thumbWidth ) ;
y = ( thumbHeight > SIZES . portraitImage . h ) ?
( ( thumbHeight - SIZES . portraitImage . h ) / - 2 ) : 0 ;
width = SIZES . portraitImage . w ;
height = SIZES . portraitImage . h ;
} else {
x = 0 ;
y = ( thumbHeight > SIZES . landscapeImage . h ) ?
( ( thumbHeight - SIZES . landscapeImage . h ) / - 2 ) : 0 ;
width = SIZES . landscapeImage . w + 3 ;
height = ( thumbHeight > SIZES . landscapeImage . h ) ?
SIZES . landscapeImage . h : thumbHeight ;
clipPath = 'mwe-popups-mask' ;
}
return {
el : createThumbnailElement (
tall ? 'mwe-popups-is-tall' : 'mwe-popups-is-not-tall' ,
rawThumbnail . source ,
x ,
y ,
thumbWidth ,
thumbHeight ,
width ,
height ,
clipPath
) ,
isTall : tall ,
width : thumbWidth ,
height : thumbHeight
} ;
}
/ * *
* Creates the SVG image element that represents the thumbnail .
*
* This function is distinct from ` createThumbnail ` as it abstracts away some
* browser issues that are uncovered when manipulating elements across
* namespaces .
*
* @ param { String } className
* @ param { String } url
* @ param { Number } x
* @ param { Number } y
* @ param { Number } thumbnailWidth
* @ param { Number } thumbnailHeight
* @ param { Number } width
* @ param { Number } height
* @ param { String } clipPath
* @ return { jQuery }
* /
function createThumbnailElement ( className , url , x , y , thumbnailWidth , thumbnailHeight , width , height , clipPath ) {
var $thumbnailSVGImage , $thumbnail ,
ns = 'http://www.w3.org/2000/svg' ,
// Use createElementNS to create the svg:image tag as jQuery uses
// createElement instead. Some browsers mistakenly map the image tag to
// img tag.
svgElement = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'image' ) ;
$thumbnailSVGImage = $ ( svgElement ) ;
$thumbnailSVGImage
. addClass ( className )
. attr ( {
x : x ,
y : y ,
width : thumbnailWidth ,
height : thumbnailHeight ,
'clip-path' : 'url(#' + clipPath + ')'
} ) ;
// Certain browsers, e.g. IE9, will not correctly set attributes from
// foreign namespaces using Element#setAttribute (see T134979). Apart from
// Safari, all supported browsers can set them using Element#setAttributeNS
// (see T134979).
if ( isSafari ) {
svgElement . setAttribute ( 'xlink:href' , url ) ;
} else {
svgElement . setAttributeNS ( ns , 'xlink:href' , url ) ;
}
$thumbnail = $ ( '<svg>' )
. attr ( {
xmlns : ns ,
width : width ,
height : height
} )
. append ( $thumbnailSVGImage ) ;
return $thumbnail ;
}
/ * *
* Represents the layout of a preview , which consists of a position ( ` offset ` )
* and whether or not the preview should be flipped horizontally or
* vertically ( ` flippedX ` and ` flippedY ` respectively ) .
*
* @ typedef { Object } ext . popups . PreviewLayout
* @ property { Object } offset
* @ property { Boolean } flippedX
* @ property { Boolean } flippedY
* /
/ * *
* Extracted from ` mw.popups.renderer.article.getOffset ` .
*
* @ param { ext . popups . Preview } preview
* @ param { Object } event
* @ return { ext . popups . PreviewLayout }
* /
function createLayout ( preview , event ) {
var flippedX = false ,
flippedY = false ,
link = $ ( event . target ) ,
offsetTop = ( event . pageY ) ? // If it was a mouse event
// Position according to mouse
// Since client rectangles are relative to the viewport,
// take scroll position into account.
getClosestYPosition (
event . pageY - $window . scrollTop ( ) ,
link . get ( 0 ) . getClientRects ( ) ,
false
) + $window . scrollTop ( ) + SIZES . pokeySize :
// Position according to link position or size
link . offset ( ) . top + link . height ( ) + SIZES . pokeySize ,
clientTop = ( event . clientY ) ?
event . clientY :
offsetTop ,
offsetLeft = ( event . pageX ) ?
event . pageX :
link . offset ( ) . left ;
// X Flip
if ( offsetLeft > ( $window . width ( ) / 2 ) ) {
offsetLeft += ( ! event . pageX ) ? link . width ( ) : 0 ;
offsetLeft -= ! preview . isTall ?
SIZES . portraitPopupWidth :
SIZES . landscapePopupWidth ;
flippedX = true ;
}
if ( event . pageX ) {
offsetLeft += ( flippedX ) ? 20 : - 20 ;
}
// Y Flip
if ( clientTop > ( $window . height ( ) / 2 ) ) {
flippedY = true ;
// Mirror the positioning of the preview when there's no "Y flip": rest
// the pokey on the edge of the link's bounding rectangle. In this case
// the edge is the top-most.
offsetTop = link . offset ( ) . top - SIZES . pokeySize ;
// Change the Y position to the top of the link
if ( event . pageY ) {
// Since client rectangles are relative to the viewport,
// take scroll position into account.
offsetTop = getClosestYPosition (
event . pageY - $window . scrollTop ( ) ,
link . get ( 0 ) . getClientRects ( ) ,
true
) + $window . scrollTop ( ) ;
}
}
return {
offset : {
top : offsetTop ,
left : offsetLeft
} ,
flippedX : flippedX ,
flippedY : flippedY
} ;
}
/ * *
* Generates a list of declarative CSS classes that represent the layout of
* the preview .
*
* @ param { ext . popups . Preview } preview
* @ param { ext . popups . PreviewLayout } layout
* @ return { String [ ] }
* /
function getClasses ( preview , layout ) {
var classes = [ ] ;
if ( layout . flippedY ) {
classes . push ( 'mwe-popups-fade-in-down' ) ;
} else {
classes . push ( 'mwe-popups-fade-in-up' ) ;
}
if ( layout . flippedY && layout . flippedX ) {
classes . push ( 'flipped_x_y' ) ;
}
if ( layout . flippedY && ! layout . flippedX ) {
classes . push ( 'flipped_y' ) ;
}
if ( layout . flippedX && ! layout . flippedY ) {
classes . push ( 'flipped_x' ) ;
}
if ( ( ! preview . hasThumbnail || preview . isTall ) && ! layout . flippedY ) {
classes . push ( 'mwe-popups-no-image-tri' ) ;
}
if ( ( preview . hasThumbnail && ! preview . isTall ) && ! layout . flippedY ) {
classes . push ( 'mwe-popups-image-tri' ) ;
}
if ( preview . isTall ) {
classes . push ( 'mwe-popups-is-tall' ) ;
} else {
classes . push ( 'mwe-popups-is-not-tall' ) ;
}
return classes ;
}
/ * *
* Lays out the preview given the layout .
*
* If the preview should be oriented differently , then the pokey is updated ,
* e . g . if the preview should be flipped vertically , then the pokey is
* removed .
*
* If the thumbnail is landscape and isn ' t the full height of the thumbnail
* container , then pull the extract up to keep whitespace consistent across
* previews .
*
* @ param { ext . popups . Preview } preview
* @ param { ext . popups . PreviewLayout } layout
* /
function layoutPreview ( preview , layout ) {
var popup = preview . el ,
isTall = preview . isTall ,
hasThumbnail = preview . hasThumbnail ,
thumbnail = preview . thumbnail ,
flippedY = layout . flippedY ,
flippedX = layout . flippedX ,
offsetTop = layout . offset . top ;
if ( ! flippedY && ! isTall && hasThumbnail && thumbnail . height < SIZES . landscapeImage . h ) {
$ ( '.mwe-popups-extract' ) . css (
'margin-top' ,
thumbnail . height - SIZES . pokeySize
) ;
}
popup . addClass ( getClasses ( preview , layout ) . join ( ' ' ) ) ;
if ( flippedY ) {
offsetTop -= popup . outerHeight ( ) ;
}
popup . css ( {
top : offsetTop ,
left : layout . offset . left + 'px'
} ) ;
if ( flippedY && hasThumbnail ) {
popup . find ( 'image' ) [ 0 ]
2017-03-13 10:09:38 +00:00
. removeAttribute ( 'clip-path' ) ;
2017-03-03 10:24:45 +00:00
}
if ( flippedY && flippedX && hasThumbnail && isTall ) {
popup . find ( 'image' ) [ 0 ]
. setAttribute ( 'clip-path' , 'url(#mwe-popups-landscape-mask-flip)' ) ;
}
if ( flippedX && ! flippedY && hasThumbnail && ! isTall ) {
popup . find ( 'image' ) [ 0 ]
. setAttribute ( 'clip-path' , 'url(#mwe-popups-mask-flip)' ) ;
}
if ( flippedX && ! flippedY && hasThumbnail && isTall ) {
popup . removeClass ( 'mwe-popups-no-image-tri' )
. find ( 'image' ) [ 0 ]
. setAttribute ( 'clip-path' , 'url(#mwe-popups-landscape-mask)' ) ;
}
}
/ * *
* Given the rectangular box ( es ) find the 'y' boundary of the closest
* rectangle to the point 'y' . The point 'y' is the location of the mouse
* on the 'y' axis and the rectangular box ( es ) are the borders of the
* element over which the mouse is located . There will be more than one
* rectangle in case the element spans multiple lines .
*
* In the majority of cases the mouse pointer will be inside a rectangle .
* However , some browsers ( i . e . Chrome ) trigger a hover action even when
* the mouse pointer is just outside a bounding rectangle . That ' s why
* we need to look at all rectangles and not just the rectangle that
* encloses the point .
*
* @ param { Number } y the point for which the closest location is being
* looked for
* @ param { ClientRectList } rects list of rectangles defined by four edges
* @ param { Boolean } [ isTop ] should the resulting rectangle 's top ' y '
* boundary be returned . By default the bottom 'y' value is returned .
* @ return { Number }
* /
function getClosestYPosition ( y , rects , isTop ) {
var result ,
deltaY ,
minY = null ;
$ . each ( rects , function ( i , rect ) {
deltaY = Math . abs ( y - rect . top + y - rect . bottom ) ;
if ( minY === null || minY > deltaY ) {
minY = deltaY ;
// Make sure the resulting point is at or outside the rectangle
// boundaries.
result = ( isTop ) ? Math . floor ( rect . top ) : Math . ceil ( rect . bottom ) ;
}
} ) ;
return result ;
}
module . exports = {
render : render ,
init : init
} ;
/***/ } ) ,
/***/ "./src/schema.js" :
/***/ ( function ( module , exports ) {
var mw = window . mediaWiki ,
$ = jQuery ;
/ * *
* Creates an instance of an EventLogging schema that can be used to log
* Popups events .
*
* @ param { mw . Map } config
* @ param { Window } window
* @ return { mw . eventLog . Schema }
* /
module . exports = function ( config , window ) {
var samplingRate = config . get ( 'wgPopupsSchemaSamplingRate' , 0 ) ;
if (
! window . navigator ||
! $ . isFunction ( window . navigator . sendBeacon ) ||
window . QUnit
) {
samplingRate = 0 ;
}
return new mw . eventLog . Schema ( 'Popups' , samplingRate ) ;
} ;
/***/ } ) ,
/***/ "./src/settingsDialog.js" :
/***/ ( function ( module , exports ) {
var mw = window . mediaWiki ,
$ = jQuery ;
/ * *
* Creates a render function that will create the settings dialog and return
* a set of methods to operate on it
* @ returns { Function } render function
* /
module . exports = function ( ) {
/ * *
* Cached settings dialog
*
* @ type { jQuery }
* /
var $dialog ,
/ * *
* Cached settings overlay
*
* @ type { jQuery }
* /
$overlay ;
/ * *
* Renders the relevant form and labels in the settings dialog
* @ param { Object } boundActions
* @ returns { Object } object with methods to affect the rendered UI
* /
return function ( boundActions ) {
if ( ! $dialog ) {
$dialog = createSettingsDialog ( ) ;
$overlay = $ ( '<div>' ) . addClass ( 'mwe-popups-overlay' ) ;
// Setup event bindings
$dialog . find ( '.save' ) . click ( function ( ) {
// Find the selected value (simple|advanced|off)
var selected = getSelectedSetting ( $dialog ) ,
// Only simple means enabled, advanced is disabled in favor of
// NavPops and off means disabled.
enabled = selected === 'simple' ;
boundActions . saveSettings ( enabled ) ;
} ) ;
$dialog . find ( '.close, .okay' ) . click ( boundActions . hideSettings ) ;
}
return {
/ * *
* Append the dialog and overlay to a DOM element
* @ param { HTMLElement } el
* /
appendTo : function ( el ) {
$overlay . appendTo ( el ) ;
$dialog . appendTo ( el ) ;
} ,
/ * *
* Show the settings element and position it correctly
* /
show : function ( ) {
var h = $ ( window ) . height ( ) ,
w = $ ( window ) . width ( ) ;
$overlay . show ( ) ;
// FIXME: Should recalc on browser resize
$dialog
. show ( )
. css ( 'left' , ( w - $dialog . outerWidth ( true ) ) / 2 )
. css ( 'top' , ( h - $dialog . outerHeight ( true ) ) / 2 ) ;
} ,
/ * *
* Hide the settings dialog .
* /
hide : function ( ) {
$overlay . hide ( ) ;
$dialog . hide ( ) ;
} ,
/ * *
* Toggle the help dialog on or off
* @ param { Boolean } visible if you want to show or hide the help dialog
* /
toggleHelp : function ( visible ) {
toggleHelp ( $dialog , visible ) ;
} ,
/ * *
* Update the form depending on the enabled flag
*
* If false and no navpops , then checks 'off'
* If true , then checks 'on'
* If false , and there are navpops , then checks 'advanced'
*
* @ param { Boolean } enabled if page previews are enabled
* /
setEnabled : function ( enabled ) {
var name = 'off' ;
if ( enabled ) {
name = 'simple' ;
} else if ( isNavPopupsEnabled ( ) ) {
name = 'advanced' ;
}
// Check the appropiate radio button
$dialog . find ( '#mwe-popups-settings-' + name )
. prop ( 'checked' , true ) ;
}
} ;
} ;
} ;
/ * *
* Create the settings dialog
*
* @ return { jQuery } settings dialog
* /
function createSettingsDialog ( ) {
var $el ,
path = mw . config . get ( 'wgExtensionAssetsPath' ) + '/Popups/resources/ext.popups/images/' ,
choices = [
{
id : 'simple' ,
name : mw . msg ( 'popups-settings-option-simple' ) ,
description : mw . msg ( 'popups-settings-option-simple-description' ) ,
image : path + 'hovercard.svg' ,
isChecked : true
} ,
{
id : 'advanced' ,
name : mw . msg ( 'popups-settings-option-advanced' ) ,
description : mw . msg ( 'popups-settings-option-advanced-description' ) ,
image : path + 'navpop.svg'
} ,
{
id : 'off' ,
name : mw . msg ( 'popups-settings-option-off' )
}
] ;
if ( ! isNavPopupsEnabled ( ) ) {
// remove the advanced option
choices . splice ( 1 , 1 ) ;
}
// render the template
$el = mw . template . get ( 'ext.popups' , 'settings.mustache' ) . render ( {
heading : mw . msg ( 'popups-settings-title' ) ,
closeLabel : mw . msg ( 'popups-settings-cancel' ) ,
saveLabel : mw . msg ( 'popups-settings-save' ) ,
helpText : mw . msg ( 'popups-settings-help' ) ,
okLabel : mw . msg ( 'popups-settings-help-ok' ) ,
descriptionText : mw . msg ( 'popups-settings-description' ) ,
choices : choices
} ) ;
return $el ;
}
/ * *
* Get the selected value on the radio button
*
* @ param { jQuery . Object } $el the element to extract the setting from
* @ return { String } Which should be ( simple | advanced | off )
* /
function getSelectedSetting ( $el ) {
return $el . find (
'input[name=mwe-popups-setting]:checked, #mwe-popups-settings'
) . val ( ) ;
}
/ * *
* Toggles the visibility between a form and the help
* @ param { jQuery . Object } $el element that contains form and help
* @ param { Boolean } visible if the help should be visible , or the form
* /
function toggleHelp ( $el , visible ) {
var $dialog = $ ( '#mwe-popups-settings' ) ,
formSelectors = 'main, .save, .close' ,
helpSelectors = '.mwe-popups-settings-help, .okay' ;
if ( visible ) {
$dialog . find ( formSelectors ) . hide ( ) ;
$dialog . find ( helpSelectors ) . show ( ) ;
} else {
$dialog . find ( formSelectors ) . show ( ) ;
$dialog . find ( helpSelectors ) . hide ( ) ;
}
}
/ * *
* Checks if the NavigationPopups gadget is enabled by looking at the global
* variables
* @ returns { Boolean } if navpops was found to be enabled
* /
function isNavPopupsEnabled ( ) {
/* global pg: false*/
return typeof pg !== 'undefined' && pg . fn . disablePopups !== undefined ;
}
2017-03-07 00:27:38 +00:00
/***/ } ) ,
/***/ "./src/statsvInstrumentation.js" :
/***/ ( function ( module , exports ) {
/ * *
* Whether statsv logging is enabled
*
* @ param { mw . user } user The ` mw.user ` singleton instance
* @ param { mw . Map } config The ` mw.config ` singleton instance
* @ param { mw . experiments } experiments The ` mw.experiments ` singleton instance
* @ returns { bool } Whether the statsv logging is enabled for the user
* given the sampling rate .
* /
function isEnabled ( user , config , experiments ) {
var samplingRate = config . get ( 'wgPopupsStatsvSamplingRate' , 0 ) ,
bucket = experiments . getBucket ( {
name : 'ext.Popups.statsv' ,
enabled : true ,
buckets : {
control : 1 - samplingRate ,
A : samplingRate
}
} , user . sessionId ( ) ) ;
return bucket === 'A' ;
}
module . exports = {
isEnabled : isEnabled
} ;
2017-03-03 10:24:45 +00:00
/***/ } ) ,
/***/ "./src/userSettings.js" :
/***/ ( function ( module , exports ) {
/ * *
* @ typedef { Object } ext . popups . UserSettings
* /
var IS _ENABLED _KEY = 'mwe-popups-enabled' ,
PREVIEW _COUNT _KEY = 'ext.popups.core.previewCount' ;
/ * *
* Given the global state of the application , creates an object whose methods
* encapsulate all interactions with the given User Agent ' s storage .
*
* @ param { mw . storage } storage The ` mw.storage ` singleton instance
*
* @ return { ext . popups . UserSettings }
* /
module . exports = function ( storage ) {
return {
/ * *
* Gets whether or not the user has previously enabled Page Previews .
*
* N . B . that if the user hasn ' t previously enabled or disabled Page
* Previews , i . e . userSettings . setIsEnabled ( true ) , then they are treated as
* if they have enabled them .
*
* @ return { Boolean }
* /
getIsEnabled : function ( ) {
return storage . get ( IS _ENABLED _KEY ) !== '0' ;
} ,
/ * *
* Sets whether or not the user has enabled Page Previews .
*
* @ param { Boolean } isEnabled
* /
setIsEnabled : function ( isEnabled ) {
storage . set ( IS _ENABLED _KEY , isEnabled ? '1' : '0' ) ;
} ,
/ * *
* Gets whether or not the user has previously enabled * * or disabled * *
* Page Previews .
*
* @ return { Boolean }
* /
hasIsEnabled : function ( ) {
var value = storage . get ( IS _ENABLED _KEY ) ;
return Boolean ( value ) !== false ;
} ,
/ * *
* Gets the number of Page Previews that the user has seen .
*
* If the storage isn ' t available , then - 1 is returned .
*
* @ return { Number }
* /
getPreviewCount : function ( ) {
var result = storage . get ( PREVIEW _COUNT _KEY ) ;
if ( result === false ) {
return - 1 ;
} else if ( result === null ) {
return 0 ;
}
return parseInt ( result , 10 ) ;
} ,
/ * *
* Sets the number of Page Previews that the user has seen .
*
* @ param { Number } count
* /
setPreviewCount : function ( count ) {
storage . set ( PREVIEW _COUNT _KEY , count . toString ( ) ) ;
}
} ;
} ;
/***/ } ) ,
/***/ "./src/wait.js" :
/***/ ( function ( module , exports ) {
var $ = jQuery ;
/ * *
* Sugar around ` window.setTimeout ` .
*
* @ example
* function continueProcessing ( ) {
* // ...
* }
*
* wait ( 150 ) . then ( continueProcessing ) ;
*
* @ param { Number } delay The number of milliseconds to wait
* @ return { jQuery . Promise }
* /
module . exports = function ( delay ) {
var result = $ . Deferred ( ) ;
setTimeout ( function ( ) {
result . resolve ( ) ;
} , delay ) ;
return result . promise ( ) ;
} ;
/***/ } )
/******/ } ) ;
2017-02-02 02:24:12 +00:00
//# sourceMappingURL=index.js.map