"const icons = require( '../mobile.startup/icons' );\n\n/* global $ */\nmodule.exports = function fakeToolbar() {\n\tconst $goBack = icons.cancel().$el;\n\n\t// FIXME: Should not be a button, instead should be an icon with text\n\tconst $loadingMessage = icons.spinner( {\n\t\ttagName: 'span',\n\t\tisIconOnly: false,\n\t\tlabel: mw.msg( 'mobile-frontend-editor-loading' )\n\t} ).$el;\n\n\t// Wrappers similar to .overlay-header-container, .overlay-header and .oo-ui-toolbar\n\tconst $fakeToolbar = $( '<div>' )\n\t\t.addClass( 've-mobile-fakeToolbar-container' )\n\t\t.append( $( '<div>' )\n\t\t\t.addClass( 've-mobile-fakeToolbar-header' )\n\t\t\t// Minerva has some complicated styling for this class, so we have to include it\n\t\t\t.addClass( 'overlay-header' )\n\t\t\t.append( $( '<div>' )\n\t\t\t\t.addClass( 've-mobile-fakeToolbar' )\n\t\t\t\t.append( $goBack, $loadingMessage )\n\t\t\t)\n\t\t);\n\n\treturn $fakeToolbar;\n};\n",
"const util = require( './util' );\n\nlet browser;\n\n/**\n * Memoize a class method. Caches the result of the method based on the\n * arguments. Instances do not share a cache.\n *\n * @private\n * @param {Function} method Method to be memoized\n * @return {Function}\n */\nfunction memoize( method ) {\n\t/**\n\t * Memoized version of the method\n\t *\n\t * @return {Function}\n\t */\n\tconst memoized = function () {\n\t\tconst cache = this[ '__cache' + memoized.cacheId ] ||\n\t\t\t( this[ '__cache' + memoized.cacheId ] = {} ),\n\t\t\tkey = [].join.call( arguments, '|' );\n\t\tif ( Object.prototype.hasOwnProperty.call( cache, key ) ) {\n\t\t\treturn cache[ key ];\n\t\t}\n\t\treturn ( cache[ key ] = method.apply( this, arguments ) );\n\t};\n\tmemoized.cacheId = Date.now().toString() + Math.random().toString();\n\treturn memoized;\n}\n\n/**\n * Representation of user's current browser\n *\n * @class Browser\n * @private\n * @param {string} ua the user agent of the current browser\n * @param {jQuery.Object} $container an element to associate with the Browser object\n */\nfunction Browser( ua, $container ) {\n\tthis.userAgent = ua;\n\tthis.$el = $container;\n}\n\nBrowser.prototype = {\n\t/**\n\t * Returns whether the current browser is an ios device.\n\t * FIXME: jquery.client does not support iPad detection so we cannot use it.\n\t *\n\t * @memberof Browser\n\t * @instance\n\t * @param {number} [version] integer describing a specific version you want to test against.\n\t * @return {boolean}\n\t */\n\tisIos: memoize( function ( version ) {\n\t\tconst ua = this.userAgent,\n\t\t\tios = /ipad|iphone|ipod/i.test( ua );\n\n\t\tif ( ios && version ) {\n\t\t\tswitch ( version ) {\n\t\t\t\tcase 8:\n\t\t\t\t\t// Test UA for iOS8. Or for simulator look for Version 8\n\t\t\t\t\t// In the iOS simulator the OS is the host machine OS version\n\t\t\t\t\t// This makes testing in iOS8 simulator work as expected\n\t\t\t\t\treturn /OS 8_/.test( ua ) || /Version\\/8/.test( ua );\n\t\t\t\tcase 4:\n\t\t\t\t\treturn /OS 4_/.test( ua );\n\t\t\t\tcase 5:\n\t\t\t\t\treturn /OS 5_/.test( ua );\n\t\t\t\tdefault:\n\t\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\treturn ios;\n\t\t}\n\t} ),\n\t/**\n\t * Determine if a device has a widescreen.\n\t *\n\t * @memberof Browser\n\t * @instance\n\t * @return {boolean}\n\t */\n\tisWideScreen: memoize( () => {\n\t\tconst val = parseInt( mw.config.get( 'wgMFDeviceWidthTablet' ), 10 );\n\t\t// Check viewport width to determine mobile vs tablet.\n\t\t// Note: Mobile devices held in landscape mode might receive tablet treatment.\n\t\treturn window.innerWidth >= val;\n\t} ),\n\t/**\n\t * Whether touchstart and other touch events are supported by the current browser.\n\t *\n\t * @memberof Browser\n\t * @instance\n\t * @return {boolean}\n\t */\n\tsupportsTouchEvents: memoize( () => 'ontouchstart' in window )\n};\n\n/**\n * @memberof Browser\n * @return {Browser}\n */\nBrowser.getSingleton = function () {\n\tlet $html;\n\tif ( !browser ) {\n\t\t$html = util.getDocument();\n\t\tbrowser = new Browser( window.navigator.userAgent, $html );\n\t}\n\treturn browser;\n};\n\nmodule.exports = Browser;\n",
"const\n\tmfExtend = require( './mfExtend' ),\n\tutil = require( './util' ),\n\tView = require( './View' ),\n\tIconButton = require( './IconButton' );\n\n/**\n * A wrapper for creating a button.\n * FIXME: T343036 This file should be combined with IconButton, all gadgets/extentions\n * using Button.js and IconButton.js will need to be updated to reflect this\n *\n * @class Button\n * @private\n * @extends module:mobile.startup/View\n *\n * @param {Object} options Configuration options\n */\nfunction Button( options ) {\n\tView.call( this, options );\n}\n\nmfExtend( Button, View, {\n\t/**\n\t * @inheritdoc\n\t * @memberof IconButton\n\t * @instance\n\t */\n\tpreRender() {\n\t\t// Mapping existing props to Codex props used in IconButton\n\t\tlet action = 'default';\n\t\tif ( this.options.progressive ) {\n\t\t\taction = 'progressive';\n\t\t} else if ( this.options.destructive ) {\n\t\t\taction = 'destructive';\n\t\t}\n\t\tlet weight = this.options.quiet ? 'quiet' : 'normal';\n\t\tif ( this.options.progressive || this.options.destructive ) {\n\t\t\tweight = 'primary';\n\t\t}\n\t\tif ( this.options.block ) {\n\t\t\tthis.options.additionalClassNames += ' mf-button-block';\n\t\t}\n\t\tconst options = util.extend( {\n\t\t\tweight,\n\t\t\taction,\n\t\t\tisIconOnly: false,\n\t\t\ticon: null\n\t\t}, this.options );\n\n\t\tthis._button = new IconButton( options );\n\t\tthis.options._buttonHTML = this._button.$el.get( 0 ).outerHTML;\n\t},\n\t/**\n\t * @inheritdoc\n\t * @memberof Button\n\t * @instance\n\t */\n\tisTemplateMode: true,\n\t/**\n\t * @memberof Button\n\t * @instance\n\t * @mixes module:mobile.startup/View#defaults\n\t * @property {Object} defaults Default options hash.\n\t * @property {string} defaults.tagName The name of the tag in which the button is wrapped.\n\t * @property {boolean} defaults.block is stacked button\n\t * @property {boolean} defaults.progressive is progressive action\n\t * @property {boolean} defaults.quiet is quiet button\n\t * @property {boolean} defaults.destructive is destructive action\n\t * @property {string} defaults.additionalClassNames Additional class name(s).\n\t * @property {string} defaults.href url\n\t * @property {string} defaults.label of button\n\t * @property {boolean} defaults.disabled should only be used with tagName button\n\t */\n\tdefaults: {\n\t\ttagName: 'a',\n\t\tdisabled: false,\n\t\tblock: undefined,\n\t\tprogressive: undefined,\n\t\tdestructive: undefined,\n\t\tquiet: undefined,\n\t\tadditionalClassNames: '',\n\t\thref: undefined,\n\t\tlabel: undefined,\n\t\tsize: 'medium'\n\t},\n\t/**\n\t * @memberof Button\n\t * @instance\n\t */\n\ttemplate: util.template( '{{{_buttonHTML}}}' )\n} );\n\nmodule.exports = Button;\n",
"const\n\tDrawer = require( './Drawer' ),\n\tutil = require( './util' ),\n\tButton = require( './Button' ),\n\tAnchor = require( './Anchor' );\n\n/**\n * Represents a query string value.\n *\n * @typedef {string|number|boolean|undefined|string[]|boolean[]} module:mobile.startup~QueryVal\n */\n/**\n * Describes a combination of query string parameters.\n *\n * @typedef {Object.<string, module:mobile.startup~QueryVal>} module:mobile.startup~QueryParams\n */\n\n/**\n * Describes a call to action drawer.\n *\n * @memberof module:mobile.startup\n * @typedef {Object} CtaOptions\n * @prop {string} [returnTo]\n * @prop {module:mobile.startup~QueryParams} [queryParams]\n * @prop {module:mobile.startup~QueryParams} [signupQueryParams]\n * @prop {Object} [progressiveButton] button options for Button element for signing in.\n * If omitted will create a login URL.\n * @prop {Object} [actionAnchor] anchor options for Anchor element for signing up. If omitted\n * will create a sign up URL\n * @prop {string} content text - what is the call to action?\n */\n\n/**\n * Internal for use inside Minerva only, creates the drawer at the bottom of the screen that appears when an anonymous\n * user tries to perform an action that requires being logged in. It presents the user\n * with options to log in or sign up for a new account.\n *\n * @function CtaDrawer\n * @memberof module:mobile.startup\n * @param {module:mobile.startup.CtaOptions} options Options for drawer.\n * @return {module:mobile.startup/Drawer}\n */\nfunction CtaDrawer( options = {} ) {\n\tconst params = redirectParams( options.queryParams, options.returnTo );\n\treturn new Drawer(\n\t\tutil.extend( {\n\t\t\tchildren: [\n\t\t\t\tutil.parseHTML( '<p>' ).text( options.content ),\n\t\t\t\tnew Button( util.extend( {\n\t\t\t\t\tprogressive: true,\n\t\t\t\t\thref: mw.util.getUrl( 'Special:UserLogin', params ),\n\t\t\t\t\tlabel: mw.msg( 'mobile-frontend-watchlist-cta-button-login' )\n\t\t\t\t}, options.progressiveButton ) ).$el,\n\t\t\t\tutil.parseHTML( '<div>' ).addClass( 'cta-drawer__anchors' ).append(\n\t\t\t\t\t// Update Minerva first to avoid needing to keep this closeAnchor\n\t\t\t\t\tnew Anchor( util.extend( {\n\t\t\t\t\t\thref: mw.util.getUrl(\n\t\t\t\t\t\t\t'Special:UserLogin', signUpParams( params, options.signupQueryParams )\n\t\t\t\t\t\t),\n\t\t\t\t\t\tprogressive: true,\n\t\t\t\t\t\tlabel: mw.msg( 'mobile-frontend-watchlist-cta-button-signup' )\n\t\t\t\t\t}, options.actionAnchor ) ).$el\n\t\t\t\t)\n\t\t\t]\n\t\t}, options )\n\t);\n}\n\n/**\n * Special:UserLogin post-request redirect query parameters.\n *\n * @ignore\n * @param {QueryParams} params\n * @param {string} [redirectURL]\n * @return {QueryParams}\n */\nfunction redirectParams( params, redirectURL ) {\n\treturn util.extend( {\n\t\t// use wgPageName as this includes the namespace if outside Main\n\t\treturnto: redirectURL || mw.config.get( 'wgPageName' )\n\t}, params );\n}\n\n/**\n * Special:UserLogin account creation query parameters.\n *\n * @ignore\n * @param {...QueryParams} params\n * @return {QueryParams}\n */\nfunction signUpParams() {\n\t[].push.call( arguments, { type: 'signup' } );\n\treturn util.extend.apply( util, arguments );\n}\n\nCtaDrawer.prototype.test = {\n\tredirectParams,\n\tsignUpParams\n};\n\nmodule.exports = CtaDrawer;\n",
"const\n\tmfExtend = require( './mfExtend' ),\n\tView = require( './View' ),\n\tutil = require( './util' ),\n\tIconButton = require( './IconButton' );\n\n/**\n * @classdesc A {@link View} that pops up from the bottom of the screen.\n * @class module:mobile.startup/Drawer\n * @extends module:mobile.startup/View\n * @final\n * @param {Object} props\n * @param {string} [props.className] Additional CSS classes to add\n * @param {jQuery.Element[]} [props.children] An array of elements to append to\n * @param {Function} [props.onShow] Callback called before showing the drawer.\n * It receives a promise given the show process is asynchronous.\n * @param {Function} [props.onBeforeHide] Callback called before hiding the drawer\n */\nfunction Drawer( props ) {\n\tthis.drawerClassName = props.className || '';\n\tthis.collapseIcon = new IconButton( {\n\t\ticon: 'expand',\n\t\tadditionalClassNames: 'cancel',\n\t\tlabel: mw.msg( 'mobile-frontend-drawer-arrow-label' )\n\t} );\n\tView.call( this,\n\t\tutil.extend(\n\t\t\t{\n\t\t\t\tonBeforeHide: () => {},\n\t\t\t\tshowCollapseIcon: true\n\t\t\t},\n\t\t\tprops,\n\t\t\t{\n\t\t\t\tclassName: 'drawer-container'\n\t\t\t},\n\t\t\t{ events: util.extend( {\n\t\t\t\t'click .drawer-container__mask': function () {\n\t\t\t\t\tthis.hide();\n\t\t\t\t}.bind( this ),\n\t\t\t\t'click .cancel': function ( ev ) {\n\t\t\t\t\tev.preventDefault();\n\t\t\t\t\tthis.hide();\n\t\t\t\t}.bind( this ),\n\t\t\t\tclick( ev ) {\n\t\t\t\t\tev.stopPropagation();\n\t\t\t\t}\n\t\t\t}, props.events ) }\n\t\t)\n\t);\n}\n\nmfExtend( Drawer, View, {\n\t$mask: null,\n\n\t/**\n\t * Shows panel after a slight delay\n\t *\n\t * @memberof module:mobile.startup/Drawer\n\t * @instance\n\t * @method\n\t * @return {jQuery.Promise}\n\t */\n\tshow() {\n\t\tconst d = util.Deferred();\n\t\tthis.$el.prepend( this.$mask );\n\t\t// Force redraw by asking the browser to measure the element's width\n\t\tthis.$el.width();\n\t\tconst $drawer = this.$el.find( '.drawer' );\n\t\tthis.$mask.addClass( 'drawer-container__mask--visible' );\n\t\tif ( !$drawer.hasClass( 'visible' ) ) {\n\t\t\t$drawer.addClass( 'visible' );\n\t\t\t// IntersectionObserver doesn't fire for content\n\t\t\t// in drawers, so trigger manually (T361212)\n\t\t\tmw.hook( 'mobileFrontend.loadLazyImages' ).fire( this.$el );\n\t\t\tif ( this.options.onShow ) {\n\t\t\t\tthis.options.onShow( d );\n\t\t\t}\n\t\t\trequestAnimationFrame( () => d.resolve() );\n\t\t} else {\n\t\t\td.resolve();\n\t\t}\n\t\treturn d.promise();\n\t},\n\n\t/**\n\t * Hides panel\n\t *\n\t * @memberof module:mobile.startup/Drawer\n\t * @instance\n\t */\n\thide() {\n\t\tconst $drawer = this.$el.find( '.drawer' );\n\t\t$drawer.removeClass( 'visible' );\n\t\tthis.$mask.removeClass( 'drawer-container__mask--visible' );\n\t\t// Should really use 'transitionend' event here, but as the\n\t\t// parent $drawer element is often detatched as well, this\n\t\t// might not fire until the next show animation.\n\t\tsetTimeout( () => {\n\t\t\tthis.$mask.detach();\n\t\t}, 100 );\n\t\trequestAnimationFrame( () => {\n\t\t\tthis.options.onBeforeHide( this );\n\t\t} );\n\t},\n\n\t/**\n\t * @inheritdoc\n\t * @memberof module:mobile.startup/Drawer\n\t * @instance\n\t */\n\tpostRender() {\n\t\tthis.$mask = util.parseHTML( '<div>' ).addClass( 'drawer-container__mask' );\n\t\tconst props = this.options,\n\t\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\t\t$drawer = util.parseHTML( '<div>' )\n\t\t\t\t.addClass( `drawer drawer-container__drawer position-fixed ${ this.drawerClassName }`.trim() );\n\n\t\tif ( props.showCollapseIcon ) {\n\t\t\t// append the collapse icon at the top of the drawer\n\t\t\t$drawer.prepend( this.collapseIcon.$el );\n\t\t}\n\n\t\tif ( props.children ) {\n\t\t\t// append children\n\t\t\t$drawer.append( props.children );\n\t\t}\n\t\tthis.$el.append( $drawer );\n\t}\n} );\n\nmodule.exports = Drawer;\n",
"const\n\tmfExtend = require( './mfExtend' ),\n\tutil = require( './util' ),\n\tView = require( './View' );\n\n/**\n * @class module:mobile.startup/Icon\n * @classdesc A wrapper for creating an icon.\n * @extends module:mobile.startup/View\n * @param {Object} options Configuration options\n */\nfunction Icon( options ) {\n\tView.call( this, options );\n}\n\nmfExtend( Icon, View, {\n\t/**\n\t * @inheritdoc\n\t * @memberof Icon\n\t * @instance\n\t */\n\tpreRender() {\n\t\tthis.options._iconClasses = this.getIconClasses();\n\t},\n\t/**\n\t * Internal method that sets the correct rotation class for the icon\n\t * based on the value of rotation\n\t *\n\t * @memberof Icon\n\t * @instance\n\t * @private\n\t */\n\tgetRotationClass() {\n\t\tlet rotationClass = '';\n\t\tif ( this.options.rotation ) {\n\t\t\tswitch ( this.options.rotation ) {\n\t\t\t\tcase -180:\n\t\t\t\tcase 180:\n\t\t\t\t\trotationClass = 'mf-icon-rotate-flip';\n\t\t\t\t\tbreak;\n\t\t\t\tcase -90:\n\t\t\t\t\trotationClass = 'mf-icon-rotate-anti-clockwise';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 90:\n\t\t\t\t\trotationClass = 'mf-icon-rotate-clockwise';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 0:\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow new Error( 'Bad value for rotation given. Must be \u00b190, 0 or \u00b1180.' );\n\t\t\t}\n\t\t}\n\t\treturn rotationClass;\n\t},\n\t/**\n\t * Set icon glyph class and icon type class\n\t *\n\t * @memberof Icon\n\t * @instance\n\t * @private\n\t */\n\tgetIconClasses() {\n\t\tconst base = this.options.base;\n\t\tconst icon = this.options.icon;\n\t\tconst isSmall = this.options.isSmall;\n\t\tconst rotationClasses = this.getRotationClass();\n\t\tconst additionalClassNames = this.options.additionalClassNames;\n\n\t\tlet classes = base + ' ';\n\t\tif ( icon ) {\n\t\t\tclasses += this.getGlyphClassName() + ' ';\n\t\t}\n\t\tif ( isSmall ) {\n\t\t\tclasses += 'mf-icon--small ';\n\t\t}\n\t\tif ( additionalClassNames ) {\n\t\t\tclasses += additionalClassNames + ' ';\n\t\t}\n\n\t\treturn classes + rotationClasses;\n\t},\n\t/**\n\t * @inheritdoc\n\t * @memberof Icon\n\t * @instance\n\t */\n\tisTemplateMode: true,\n\t/**\n\t * @memberof Icon\n\t * @instance\n\t * @mixes module:mobile.startup/View#defaults\n\t * @property {Object} defaults\n\t * @property {string} defaults.base Base icon class.\n\t * Defaults to 'mf-icon'.\n\t * @property {string} defaults.glyphPrefix Prefix for the icon class\n\t * Defaults to 'mf'.\n\t * @property {string} defaults.icon Name of the icon.\n\t * @property {boolean} defaults.rotation will rotate the icon by a certain number\n\t * of degrees. Must be \u00b190, 0 or \u00b1180 or will throw exception.\n\t * @property {boolean} defaults.isSmall If icon is small.\n\t * @property {string} defaults.addtionalClassNames Additional classes to be added to the icon.\n\t */\n\tdefaults: {\n\t\tbase: 'mf-icon',\n\t\tglyphPrefix: null,\n\t\ticon: '',\n\t\trotation: 0,\n\t\tisSmall: false,\n\t\tadditionalClassNames: null\n\t},\n\t/**\n\t * Return the full class name that is required for the icon to render\n\t *\n\t * @memberof Icon\n\t * @instance\n\t * @return {string}\n\t */\n\tgetClassName() {\n\t\treturn this.$el.attr( 'class' );\n\t},\n\t/**\n\t * Return the class that relates to the icon glyph\n\t *\n\t * @memberof Icon\n\t * @instance\n\t * @return {string}\n\t */\n\tgetGlyphClassName() {\n\t\tif ( this.options.glyphPrefix ) {\n\t\t\treturn 'mf-icon-' + this.options.glyphPrefix + '-' + this.options.icon;\n\t\t}\n\t\treturn 'mf-icon-' + this.options.icon;\n\t},\n\n\ttemplate: util.template(\n\t\t'<span class=\"{{_iconClasses}}\"> </span>'\n\t)\n} );\n\nmodule.exports = Icon;\n",
"const\n\tmfExtend = require( './mfExtend' ),\n\tutil = require( './util' ),\n\tView = require( './View' ),\n\tIcon = require( './Icon' );\n\n/**\n * A wrapper for creating an icon button.\n *\n * @class IconButton\n * @extends module:mobile.startup/View\n *\n * @param {Object} options Configuration options\n */\nfunction IconButton( options ) {\n\tif ( options.href ) {\n\t\toptions.tagName = 'a';\n\t}\n\tif ( options.tagName === 'button' ) {\n\t\toptions.isTypeButton = true;\n\t}\n\tView.call( this, options );\n}\n\nmfExtend( IconButton, View, {\n\t/**\n\t * @inheritdoc\n\t * @memberof IconButton\n\t * @instance\n\t */\n\tpreRender() {\n\t\tthis.options._buttonClasses = this.getButtonClasses();\n\t\tthis.options._iconHTML = '';\n\t\tif ( this.options.icon ) {\n\t\t\tthis._icon = new Icon( {\n\t\t\t\tbase: this.options.base,\n\t\t\t\tglyphPrefix: this.options.glyphPrefix,\n\t\t\t\ticon: this.options.icon,\n\t\t\t\trotation: this.options.rotation,\n\t\t\t\tisSmall: this.options.isSmall\n\t\t\t} );\n\t\t\tthis.options._iconHTML = this._icon.$el.get( 0 ).outerHTML;\n\t\t}\n\t},\n\tgetButtonClasses() {\n\t\tconst additionalClassNames = this.options.additionalClassNames;\n\t\tconst size = this.options.size;\n\t\tconst weight = this.options.weight;\n\t\tconst action = this.options.action;\n\t\tconst isIconOnly = this.options.isIconOnly;\n\t\tlet classes = 'cdx-button ';\n\n\t\tif ( this.options.tagName !== 'button' ) {\n\t\t\tclasses += 'cdx-button--fake-button cdx-button--fake-button--enabled ';\n\t\t}\n\t\tif ( size ) {\n\t\t\tclasses += `cdx-button--size-${ size } `;\n\t\t}\n\t\tif ( weight ) {\n\t\t\tclasses += `cdx-button--weight-${ weight } `;\n\t\t}\n\t\tif ( action ) {\n\t\t\tclasses += `cdx-button--action-${ action } `;\n\t\t}\n\t\tif ( isIconOnly ) {\n\t\t\tclasses += 'cdx-button--icon-only ';\n\t\t}\n\t\treturn classes + additionalClassNames;\n\t},\n\t/**\n\t * @inheritdoc\n\t * @memberof IconButton\n\t * @instance\n\t */\n\tisTemplateMode: true,\n\t/**\n\t * @memberof IconButton\n\t * @instance\n\t * @mixes module:mobile.startup/View#defaults\n\t * @property {Object} defaults Default options hash.\n\t * @property {string} defaults.tagName The name of the tag in which the button is wrapped.\n\t * Defaults to 'a' when href option present.\n\t * @property {string} [defaults.href] value of href attribute,\n\t * when set tagName will default to anchor tag\n\t * @property {string} defaults.additionalClassNames Additional classes to be added to the button\n\t * @property {string} defaults.title Tooltip text.\n\t * @property {string} defaults.size Button size.\n\t * Defaults to 'large'.\n\t * @property {boolean} defaults.weight Button weight.\n\t * Defaults to 'quiet'.\n\t * @property {boolean} defaults.action Button action.\n\t * @property {boolean} defaults.isIconOnly Whether button is an icon only button\n\t * Defaults to true.\n\t * @property {boolean} defaults.disabled should only be used with tagName button\n\t * @property {string} defaults.base String used as a base for generating class names.\n\t * Defaults to 'mf-icon'.\n\t * @property {string} defaults.glyphPrefix Prefix for the icon class\n\t * @property {string} defaults.icon Name of the icon.\n\t * @property {boolean} defaults.rotation will rotate the icon by a certain number\n\t * of degrees. Must be \u00b190, 0 or \u00b1180 or will throw exception.\n\t * @property {boolean} defaults.isSmall Whether the icon should be small.\n\t */\n\tdefaults: {\n\t\ttagName: 'button',\n\t\thref: undefined,\n\t\tadditionalClassNames: '',\n\t\ttitle: '',\n\t\tsize: 'large',\n\t\tweight: 'quiet',\n\t\taction: '',\n\t\tisIconOnly: true,\n\t\tdisabled: false,\n\t\tbase: 'mf-icon',\n\t\ticon: '',\n\t\trotation: 0,\n\t\tisSmall: false\n\t},\n\t/**\n\t * Return the full class name that is required for the icon to render\n\t *\n\t * @memberof IconButton\n\t * @instance\n\t * @return {string}\n\t */\n\tgetClassName() {\n\t\treturn this.$el.attr( 'class' );\n\t},\n\tgetIcon() {\n\t\treturn this._icon;\n\t},\n\ttemplate: util.template( `\n\t\t<{{tagName}}\n\t\t\ttype=\"button\"\n\t\t\t{{#isTypeButton}}{{#disabled}}disabled{{/disabled}}{{/isTypeButton}}\n\t\t\tclass=\"{{_buttonClasses}}\"\n\t\t\t{{#id}}id=\"{{id}}\"{{/id}}\n\t\t\t{{#href}}href=\"{{href}}\"{{/href}}\n\t\t\t{{#title}}title=\"{{title}}\"{{/title}}>\n\t\t\t\t{{{_iconHTML}}}\n\t\t\t\t<span>{{label}}</span>\n\t\t</{{tagName}}>\n\t` )\n} );\n\nmodule.exports = IconButton;\n",
"const\n\tView = require( './View' ),\n\theader = require( './headers' ).header,\n\tAnchor = require( './Anchor' ),\n\tutil = require( './util' ),\n\tbrowser = require( './Browser' ).getSingleton(),\n\tmfExtend = require( './mfExtend' );\n\n/**\n * @class module:mobile.startup/Overlay\n * @classdesc Mobile modal window\n * @extends module:mobile.startup/View\n * @uses Icon\n * @uses Button\n * @fires Overlay#hide\n * @param {Object} props\n * @param {Object} props.events - custom events to be bound to the overlay.\n * @param {boolean} [props.headerChrome] Whether the header has chrome.\n * @param {View[]} [props.headerActions] children (usually buttons or icons)\n * that should be placed in the header actions. Ignored when `headers` used.\n * @param {string} [props.heading] heading for the overlay header. Use `headers` where\n * overlays require more than one header. Ignored when `headers` used.\n * @param {boolean} props.noHeader renders an overlay without a header\n * @param {Element[]} [props.headers] allows overlays to have more than one\n * header. When used it is an array of jQuery Objects representing\n * headers created via the header util function. It is expected that only one of these\n * should be visible. If undefined, headerActions and heading is used.\n * @param {Object} [props.footerAnchor] options for an optional Anchor\n * that can appear in the footer\n * @param {Function} props.onBeforeExit allows a consumer to prevent exits in certain\n * situations. This callback gets the following parameters:\n * - 1) the exit function which should be run after the consumer has made their changes.\n * - 2) the cancel function which should be run if the consumer explicitly changes their mind\n */\nfunction Overlay( props ) {\n\tthis.isIos = browser.isIos();\n\n\tView.call(\n\t\tthis,\n\t\tutil.extend(\n\t\t\ttrue,\n\t\t\t{\n\t\t\t\theaderChrome: false,\n\t\t\t\tclassName: 'overlay'\n\t\t\t},\n\t\t\tprops,\n\t\t\t{\n\t\t\t\tevents: util.extend(\n\t\t\t\t\t{\n\t\t\t\t\t\t// FIXME: Remove .initial-header selector\n\t\t\t\t\t\t'click .cancel, .confirm, .initial-header .back': 'onExitClick',\n\t\t\t\t\t\tclick: ( ev ) => ev.stopPropagation()\n\t\t\t\t\t},\n\t\t\t\t\tprops.events\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t);\n}\n\nmfExtend( Overlay, View, {\n\ttemplate: util.template( `\n{{^noHeader}}\n<div class=\"overlay-header-container header-container{{#headerChrome}}\n\theader-chrome{{/headerChrome}} position-fixed\">\n</div>\n{{/noHeader}}\n<div class=\"overlay-content\">\n\t{{>content}}\n</div>\n<div class=\"overlay-footer-container position-fixed\"></div>\n\t` ),\n\n\thideTimeout: null,\n\n\t/**\n\t * Shows the spinner right to the input field.\n\t *\n\t * @memberof module:mobile.startup/Overlay\n\t * @instance\n\t * @method\n\t */\n\tshowSpinner() {\n\t\tthis.$el.find( '.spinner' ).removeClass( 'hidden' );\n\t},\n\n\t/**\n\t * Hide the spinner near to the input field.\n\t *\n\t * @memberof module:mobile.startup/Overlay\n\t * @instance\n\t * @method\n\t */\n\thideSpinner() {\n\t\tthis.$el.find( '.spinner' ).addClass( 'hidden' );\n\t},\n\n\t/**\n\t * @inheritdoc\n\t * @memberof module:mobile.startup/Overlay\n\t * @instance\n\t */\n\tpostRender() {\n\t\tconst footerAnchor = this.options.footerAnchor;\n\t\tthis.$overlayContent = this.$el.find( '.overlay-content' );\n\t\tif ( this.isIos ) {\n\t\t\tthis.$el.addClass( 'overlay-ios' );\n\t\t}\n\t\tif ( footerAnchor ) {\n\t\t\tthis.$el.find( '.overlay-footer-container' ).append( new Anchor( footerAnchor ).$el );\n\t\t}\n\t\tconst headers = this.options.headers || [\n\t\t\theader(\n\t\t\t\tthis.options.heading,\n\t\t\t\tthis.options.headerActions\n\t\t\t)\n\t\t];\n\t\tthis.$el.find( '.overlay-header-container' ).append( headers );\n\t},\n\n\t/**\n\t * ClickBack event handler\n\t *\n\t * @memberof module:mobile.startup/Overlay\n\t * @instance\n\t * @param {Object} ev event object\n\t */\n\tonExitClick( ev ) {\n\t\tconst exit = function () {\n\t\t\tthis.hide();\n\t\t}.bind( this );\n\t\tev.preventDefault();\n\t\tev.stopPropagation();\n\t\tif ( this.options.onBeforeExit ) {\n\t\t\tthis.options.onBeforeExit( exit, () => {\n\t\t\t} );\n\t\t} else {\n\t\t\texit();\n\t\t}\n\n\t},\n\t/**\n\t * Attach overlay to current view and show it.\n\t *\n\t * @memberof module:mobile.startup/Overlay\n\t * @instance\n\t */\n\tshow() {\n\t\tconst $html = util.getDocument();\n\n\t\tthis.scrollTop = window.pageYOffset;\n\n\t\t$html.addClass( 'overlay-enabled' );\n\t\t// skip the URL bar if possible\n\t\twindow.scrollTo( 0, 1 );\n\n\t\tthis.$el.addClass( 'visible' );\n\n\t\t// If .hide() was called earlier, and it scheduled an asynchronous detach\n\t\t// but it hasn't happened yet, cancel it\n\t\tif ( this.hideTimeout !== null ) {\n\t\t\tclearTimeout( this.hideTimeout );\n\t\t\tthis.hideTimeout = null;\n\t\t}\n\t},\n\t/**\n\t * Detach the overlay from the current view\n\t * Should not be overriden as soon to be deprecated.\n\t *\n\t * @memberof module:mobile.startup/Overlay\n\t * @instance\n\t * @final\n\t * @return {boolean} Whether the overlay was successfully hidden or not\n\t */\n\thide() {\n\t\tutil.getDocument().removeClass( 'overlay-enabled' );\n\t\t// return to last known scroll position\n\t\twindow.scrollTo( window.pageXOffset, this.scrollTop );\n\n\t\t// Since the hash change event caused by emitting hide will be detected later\n\t\t// and to avoid the article being shown during a transition from one overlay to\n\t\t// another, we regretfully detach the element asynchronously.\n\t\tthis.hideTimeout = setTimeout( () => {\n\t\t\tthis.$el.detach();\n\t\t\tthis.hideTimeout = null;\n\t\t}, 0 );\n\n\t\t/**\n\t\t * Fired when the overlay is closed.\n\t\t *\n\t\t * @event Overlay#hide\n\t\t */\n\t\tthis.emit( 'hide' );\n\n\t\treturn true;\n\t},\n\n\t/**\n\t * Show elements that are selected by the className.\n\t * Also hide .hideable elements\n\t * Can't use jQuery's hide() and show() because show() sets display: block.\n\t * And we want display: table for headers.\n\t *\n\t * @memberof module:mobile.startup/Overlay\n\t * @instance\n\t * @protected\n\t * @param {string} className CSS selector to show\n\t */\n\tshowHidden( className ) {\n\t\tthis.$el.find( '.hideable' ).addClass( 'hidden' );\n\t\tthis.$el.find( className ).removeClass( 'hidden' );\n\t}\n} );\n\n/**\n * Factory method for an overlay with a single child\n *\n * @memberof module:mobile.startup/Overlay\n * @instance\n * @protected\n * @param {Object} options\n * @param {module:mobile.startup/View} view\n * @return {module:mobile.startup/Overlay}\n */\nOverlay.make = function ( options, view ) {\n\tconst overlay = new Overlay( options );\n\toverlay.$el.find( '.overlay-content' ).append( view.$el );\n\treturn overlay;\n};\n\nmodule.exports = Overlay;\n",
"const util = require( './util' );\n\nlet overlayManager = null;\n\n// We pass this to history.pushState/replaceState to indicate that we're controlling the page URL.\n// Then we look for this marker on page load so that if the page is refreshed, we don't generate an\n// extra history entry (see #getSingleton below and T201852).\nconst MANAGED_STATE = 'MobileFrontend OverlayManager was here!';\n\n/**\n * Manages opening and closing overlays when the URL hash changes to one\n * of the registered values (see OverlayManager.add()).\n *\n * This allows overlays to function like real pages, with similar browser back/forward\n * and refresh behavior.\n *\n * @class module:mobile.startup/OverlayManager\n * @param {Router} router\n * @param {Element} container where overlays should be managed\n */\nfunction OverlayManager( router, container ) {\n\trouter.on( 'route', this._checkRoute.bind( this ) );\n\tthis.router = router;\n\t// use an object instead of an array for entries so that we don't\n\t// duplicate entries that already exist\n\tthis.entries = {};\n\t// stack of all the open overlays, stack[0] is the latest one\n\tthis.stack = [];\n\tthis.hideCurrent = true;\n\t// Set the element that overlays will be appended to\n\tthis.container = container;\n}\n\n/**\n * Attach an event to the overlays hide event\n *\n * @param {module:mobile.startup/Overlay} overlay\n * @private\n */\nfunction attachHideEvent( overlay ) {\n\toverlay.on( 'hide', () => overlay.emit( '_om_hide' ) );\n}\n\nOverlayManager.prototype = {\n\t/**\n\t * Don't try to hide the active overlay on a route change event triggered\n\t * by hiding another overlay.\n\t * Called when something other than OverlayManager calls Overlay.hide\n\t * on an overlay that it itself managed by the OverlayManager.\n\t * MUST be called when the stack is not empty.\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @private\n\t */\n\t_onHideOverlayOutsideOverlayManager() {\n\t\tif ( !this.stack.length ) {\n\t\t\treturn;\n\t\t}\n\t\tconst currentRoute = this.stack[0].route,\n\t\t\trouteIsString = typeof currentRoute === 'string',\n\t\t\tcurrentPath = this.router.getPath(),\n\t\t\t// Since routes can be strings or regexes, it's important to do an equality\n\t\t\t// check BEFORE a match check.\n\t\t\trouteIsSame = ( routeIsString && currentPath === currentRoute ) ||\n\t\t\t\tcurrentPath.match( currentRoute );\n\n\t\tthis.hideCurrent = false;\n\n\t\t// If the path hasn't changed then the user didn't close the overlay by\n\t\t// calling history.back() or triggering a route change. We must go back\n\t\t// to get out of the overlay. See T237677.\n\t\tif ( routeIsSame ) {\n\t\t\t// does the route need to change?\n\t\t\tthis.router.back();\n\t\t}\n\t},\n\n\t/**\n\t * Attach overlay to DOM\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @private\n\t * @param {module:mobile.startup/Overlay} overlay to attach\n\t */\n\t_attachOverlay( overlay ) {\n\t\tif ( !overlay.$el.parents().length ) {\n\t\t\tthis.container.appendChild( overlay.$el[0] );\n\t\t}\n\t},\n\t/**\n\t * Show the overlay and bind the '_om_hide' event to _onHideOverlay.\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @private\n\t * @param {module:mobile.startup/Overlay} overlay to show\n\t */\n\t_show( overlay ) {\n\t\t// Mark the state so that if the page is refreshed, we don't generate an extra history entry\n\t\t// (see #getSingleton below and T201852).\n\t\t// eslint-disable-next-line no-restricted-properties\n\t\twindow.history.replaceState( MANAGED_STATE, null, window.location.href );\n\n\t\t// the _om_hide event is added to an overlay that is displayed.\n\t\t// It will fire if an Overlay emits a hide event (See attachHideEvent)\n\t\t// in the case where a route change has not occurred (this event is disabled\n\t\t// inside _hideOverlay which is called inside _checkRoute)\n\t\toverlay.once( '_om_hide', this._onHideOverlayOutsideOverlayManager.bind( this ) );\n\n\t\tthis._attachOverlay( overlay );\n\t\toverlay.show();\n\t},\n\n\t/**\n\t * Hide overlay\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @private\n\t * @param {module:mobile.startup/Overlay} overlay to hide\n\t * @param {Function} onBeforeExitCancel to pass to onBeforeExit\n\t * @return {boolean} Whether the overlay has been hidden\n\t */\n\t_hideOverlay( overlay, onBeforeExitCancel ) {\n\t\tlet result;\n\n\t\tfunction exit() {\n\t\t\tresult = true;\n\t\t\toverlay.hide();\n\t\t}\n\n\t\t// remove the callback for updating state when overlay closed using\n\t\t// overlay close button\n\t\toverlay.off( '_om_hide' );\n\n\t\tif ( overlay.options && overlay.options.onBeforeExit ) {\n\t\t\toverlay.options.onBeforeExit( exit, onBeforeExitCancel );\n\t\t} else {\n\t\t\texit();\n\t\t}\n\n\t\t// if closing prevented, reattach the callback\n\t\tif ( !result ) {\n\t\t\toverlay.once( '_om_hide', this._onHideOverlayOutsideOverlayManager.bind( this ) );\n\t\t}\n\n\t\treturn result;\n\t},\n\n\t/**\n\t * Show match's overlay if match is not null.\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @private\n\t * @param {Object|null} match Object with factory function's result. null if no match.\n\t */\n\t_processMatch( match ) {\n\t\tconst self = this;\n\n\t\tif ( match ) {\n\t\t\tif ( match.overlay ) {\n\t\t\t\t// if the match is an overlay that was previously opened, reuse it\n\t\t\t\tself._show( match.overlay );\n\t\t\t} else {\n\t\t\t\t// else create an overlay using the factory function result\n\t\t\t\tconst factoryResult = match.factoryResult;\n\t\t\t\t// We were getting errors relating to no factoryResult.\n\t\t\t\t// This should never happen.\n\t\t\t\t// If it does an error should not be thrown.\n\t\t\t\tif ( factoryResult ) {\n\t\t\t\t\tmatch.overlay = factoryResult;\n\t\t\t\t\tattachHideEvent( match.overlay );\n\t\t\t\t\tself._show( factoryResult );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t/**\n\t * A callback for Router's `route` event.\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @private\n\t * @param {jQuery.Event} ev Event object.\n\t */\n\t_checkRoute( ev ) {\n\t\tconst current = this.stack[0];\n\n\t\t// When entering an overlay for the first time,\n\t\t// the manager should remember the user's scroll position\n\t\t// overlays always open at top of page\n\t\t// and we'll want to restore it later.\n\t\t// This should happen before the call to _matchRoute which will \"show\" the overlay.\n\t\t// The Overlay has similar logic for overlays that are not managed via the overlay.\n\t\tif ( !current ) {\n\t\t\tthis.scrollTop = window.pageYOffset;\n\t\t}\n\n\t\t// if there is an overlay in the stack and it's opened, try to close it\n\t\tif (\n\t\t\tcurrent &&\n\t\t\tcurrent.overlay !== undefined &&\n\t\t\tthis.hideCurrent &&\n\t\t\t!this._hideOverlay( current.overlay, () => {\n\t\t\t\t// if hide prevented, prevent route change event\n\t\t\t\tev.preventDefault();\n\t\t\t} )\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst match = Object.keys( this.entries ).reduce( ( m, id ) => m ||\n\t\t\tthis._matchRoute( ev.path, this.entries[id] ), null );\n\n\t\tif ( !match ) {\n\t\t\t// if hidden and no new matches, reset the stack\n\t\t\tthis.stack = [];\n\t\t\t// restore the scroll position.\n\t\t\twindow.scrollTo( window.pageXOffset, this.scrollTop );\n\t\t}\n\n\t\tthis.hideCurrent = true;\n\t\tthis._processMatch( match );\n\t},\n\n\t/**\n\t * Check if a given path matches one of the existing entries and\n\t * remove it from the stack.\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @private\n\t * @param {string} path Path (hash) to check.\n\t * @param {Object} entry Entry object created in OverlayManager#add.\n\t * @return {Object|null} Match object with factory function's result.\n\t * Returns null if no match.\n\t */\n\t_matchRoute( path, entry ) {\n\t\tlet\n\t\t\tdidMatch,\n\t\t\tcaptures,\n\t\t\tmatch;\n\n\t\tconst\n\t\t\tprevious = this.stack[1],\n\t\t\tself = this;\n\n\t\tif ( typeof entry.route === 'string' ) {\n\t\t\tdidMatch = entry.route === path;\n\t\t\tcaptures = [];\n\t\t} else {\n\t\t\tmatch = path.match( entry.route );\n\t\t\tdidMatch = !!match;\n\t\t\tcaptures = didMatch ? match.slice( 1 ) : [];\n\t\t}\n\n\t\t/**\n\t\t * Returns object to add to stack\n\t\t *\n\t\t * @method\n\t\t * @ignore\n\t\t * @return {Object}\n\t\t */\n\t\tfunction getNext() {\n\t\t\treturn {\n\t\t\t\tpath,\n\t\t\t\t// Important for managing states of things such as the image overlay which change\n\t\t\t\t// overlay routing parameters during usage.\n\t\t\t\troute: entry.route,\n\t\t\t\tfactoryResult: entry.factory.apply( self, captures )\n\t\t\t};\n\t\t}\n\n\t\tif ( didMatch ) {\n\t\t\t// if previous stacked overlay's path matches, assume we're going back\n\t\t\t// and reuse a previously opened overlay\n\t\t\tif ( previous && previous.path === path ) {\n\t\t\t\tself.stack.shift();\n\t\t\t\treturn previous;\n\t\t\t} else {\n\t\t\t\tconst next = getNext();\n\t\t\t\tif ( this.stack[0] && next.path === this.stack[0].path ) {\n\t\t\t\t\t// current overlay path is same as path to check which means overlay\n\t\t\t\t\t// is attempting to refresh so just replace current overlay with new\n\t\t\t\t\t// overlay\n\t\t\t\t\tself.stack[0] = next;\n\t\t\t\t} else {\n\t\t\t\t\tself.stack.unshift( next );\n\t\t\t\t}\n\t\t\t\treturn next;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t},\n\n\t/**\n\t * Add an overlay that should be shown for a specific fragment identifier.\n\t *\n\t * The following code will display an overlay whenever a user visits a URL that\n\t * ends with '#/hi/name'. The value of `name` will be passed to the overlay.\n\t * Note the factory must return an Overlay.\n\t * If the overlay needs to load code asynchronously that should be done inside\n\t * the overlay.\n\t *\n\t * @example\n\t * overlayManager.add( /\\/hi\\/(.*)/, function ( name ) {\n\t * var HiOverlay = M.require( 'HiOverlay' );\n\t * return new HiOverlay( { name: name } ) );\n\t * } );\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @param {RegExp|string} route definition that can be a regular\n\t * expression (optionally with parameters) or a string literal.\n\t *\n\t * T238364: Routes should only contain characters allowed by RFC3986 to ensure\n\t * compatibility across browsers. Encode the route with `encodeURIComponent()`\n\t * prior to registering it with OverlayManager if necessary (should probably\n\t * be done with all routes containing user generated characters) to avoid\n\t * inconsistencies with how different browsers encode illegal URI characters:\n\t *\n\t * ```\n\t * var encodedRoute = encodeURIComponent('ugc < \" ` >');\n\t *\n\t * overlayManager.add(\n\t * encodedRoute,\n\t * function () { return new Overlay(); }\n\t * );\n\t *\n\t * window.location.hash = '#' + encodedRoute;\n\t * ```\n\t * The above example shows how to register a string literal route with illegal\n\t * URI characters. Routes registered as a regex will likely NOT have to\n\t * perform any encoding (unless they explicitly contain illegal URI\n\t * characters) as their user generated content portion will likely just be a\n\t * capturing group (e.g. `/\\/hi\\/(.*)/`).\n\t * @param {Function} factory a function returning an overlay\n\t */\n\tadd( route, factory ) {\n\t\tconst self = this,\n\t\t\tentry = {\n\t\t\t\troute,\n\t\t\t\tfactory\n\t\t\t};\n\n\t\tthis.entries[route] = entry;\n\t\t// Check if overlay should be shown for the current path.\n\t\t// The DOM must fully load before we can show the overlay because Overlay relies on it.\n\t\tutil.docReady( () => self._processMatch( self._matchRoute( self.router.getPath(),\n\t\t\tentry ) ) );\n\t},\n\n\t/**\n\t * Replace the currently displayed overlay with a new overlay without changing the\n\t * URL. This is useful for when you want to switch overlays, but don't want to\n\t * change the back button or close box behavior.\n\t *\n\t * @memberof module:mobile.startup/OverlayManager\n\t * @instance\n\t * @param {Object} overlay The overlay to display\n\t */\n\treplaceCurrent( overlay ) {\n\t\tif ( this.stack.length === 0 ) {\n\t\t\tthrow new Error( \"Trying to replace OverlayManager's current overlay, but stack is empty\" );\n\t\t}\n\t\tconst stackOverlay = this.stack[0].overlay;\n\t\tif ( stackOverlay ) {\n\t\t\tthis._hideOverlay( stackOverlay );\n\t\t}\n\t\tthis.stack[0].overlay = overlay;\n\t\tattachHideEvent( overlay );\n\t\tthis._show( overlay );\n\t}\n};\n\n/**\n * Retrieve a singleton instance using 'mediawiki.router'.\n *\n * @memberof module:mobile.startup/OverlayManager\n * @return {module:mobile.startup/OverlayManager}\n */\nOverlayManager.getSingleton = function () {\n\tif ( !overlayManager ) {\n\t\tconst\n\t\t\trouter = __non_webpack_require__( 'mediawiki.router' ),\n\t\t\tcontainer = document.createElement( 'div' ),\n\t\t\t// Note getPath returns hash minus the '#' character:\n\t\t\thash = router.getPath(),\n\t\t\t// eslint-disable-next-line no-restricted-properties\n\t\t\tstate = window.history.state;\n\t\tcontainer.className = 'mw-overlays-container';\n\t\tdocument.body.appendChild( container );\n\t\t// If an overlay was loaded by directly navigating to an URL with a hash (e.g. linked from\n\t\t// another page or browser bookmark), generate an extra history entry to allow closing the\n\t\t// overlay without leaving the page (see T201852). Put our marker into the entry state so\n\t\t// that we can detect it if the page is refreshed and do not generate another entry.\n\t\tif ( hash && state !== MANAGED_STATE ) {\n\t\t\t// eslint-disable-next-line no-restricted-properties\n\t\t\twindow.history.replaceState( null, null, '#' );\n\t\t\t// eslint-disable-next-line no-restricted-properties\n\t\t\twindow.history.pushState( MANAGED_STATE, null, `#${ hash }` );\n\t\t}\n\t\toverlayManager = new OverlayManager( router, container );\n\t}\n\treturn overlayManager;\n};\n\nOverlayManager.test = {\n\tMANAGED_STATE,\n\t__clearCache: () => {\n\t\toverlayManager = null;\n\t}\n};\nmodule.exports = OverlayManager;\n",
"const\n\tHTML = mw.html,\n\tutil = require( './util' );\n\nclass Page {\n\t/**\n\t * @class module:mobile.startup/Page\n\t * @classdesc Mobile page view object\n\t * @param {Object} options Configuration options\n\t * @param {number} options.id Page ID. The default value of 0 represents a\n\t * new or missing page. Be sure to override it to avoid side effects.\n\t * @param {string} options.title Title of the page. It includes prefix where needed and\n\t * is human readable, e.g. Talk:The man who lived.\n\t * @param {Object} options.titleObj\n\t * @param {string} options.displayTitle HTML title of the page for display. Falls back\n\t * to defaults.title (escaped) if no value is provided. Must be safe HTML!\n\t * @param {number} options.namespaceNumber the number of the\n\t * namespace the page belongs to\n\t * @param {Object} options.protection List of permissions as returned by API,\n\t * e.g. [{ edit: ['*'] }]\n\t * @param {string} options.url\n\t * @param {string} options.wikidataDescription\n\t * @param {boolean} options.isMainPage Whether the page is the Main Page.\n\t * @param {boolean} options.isMissing Whether the page exists in the wiki.\n\t * @param {string} options.anchor\n\t * @param {string} [options.relevantTitle] associated with page.\n\t * For example Special:WhatLinksHere/Foo would be associated with the page `Foo`.\n\t * @param {number} options.revId Revision ID. See `wgRevisionId`.\n\t * @param {boolean} options.isWatched Whether the page is being watched\n\t * @param {Object} options.thumbnail thumbnail definition corresponding to page image\n\t * @param {boolean} options.thumbnail.isLandscape whether the image is in\n\t * landscape format\n\t * @param {number} options.thumbnail.width of image in pixels.\n\t * @param {number} options.thumbnail.height of image in pixels.\n\t * @param {string} options.thumbnail.source url for image\n\t */\n\tconstructor( options ) {\n\t\tconst title = options.title || '';\n\t\tutil.extend( this, {\n\t\t\tid: options.id || 0,\n\t\t\t// FIXME: Deprecate title property as it can be derived from titleObj\n\t\t\t// using getPrefixedText\n\t\t\ttitle,\n\t\t\trelevantTitle: options.relevantTitle || title,\n\t\t\ttitleObj: options.titleObj,\n\t\t\tdisplayTitle: options.displayTitle || HTML.escape( title ),\n\t\t\tnamespaceNumber: options.namespaceNumber || 0,\n\t\t\tprotection: options.protection,\n\t\t\turl: options.url || mw.util.getUrl( title ),\n\t\t\twikidataDescription: options.wikidataDescription,\n\t\t\t_isMainPage: options.isMainPage || false,\n\t\t\tisMissing: ( options.isMissing !== undefined ) ?\n\t\t\t\toptions.isMissing : options.id === 0,\n\t\t\tanchor: options.anchor,\n\t\t\trevId: options.revId,\n\t\t\t_isWatched: options.isWatched,\n\t\t\tthumbnail: ( Object.prototype.hasOwnProperty.call( options, 'thumbnail' ) ) ?\n\t\t\t\toptions.thumbnail : false\n\t\t} );\n\n\t\tif ( this.thumbnail && this.thumbnail.width ) {\n\t\t\tthis.thumbnail.isLandscape = this.thumbnail.width > this.thumbnail.height;\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve the title that should be displayed to the user\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {string} HTML\n\t */\n\tgetDisplayTitle() {\n\t\treturn this.displayTitle;\n\t}\n\n\t/**\n\t * Determine if current page is in a specified namespace\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @param {string} namespace Name of namespace\n\t * @return {boolean}\n\t */\n\tinNamespace( namespace ) {\n\t\treturn this.namespaceNumber === mw.config.get( 'wgNamespaceIds' )[namespace];\n\t}\n\n\t/**\n\t * Determines if content model is wikitext\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {boolean}\n\t */\n\tisWikiText() {\n\t\treturn mw.config.get( 'wgPageContentModel' ) === 'wikitext';\n\t}\n\n\t/**\n\t * Check if the visual editor is available on this page\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {boolean}\n\t */\n\tisVEAvailable() {\n\t\treturn !!mw.config.get( 'wgVisualEditorConfig' ) &&\n\t\t\t!mw.config.get( 'wgVisualEditorDisabledByHook' ) &&\n\t\t\tthis.isWikiText();\n\t}\n\n\t/**\n\t * Check if the visual editor in visual mode is available on this page\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {boolean}\n\t */\n\tisVEVisualAvailable() {\n\t\tif ( !this.isVEAvailable() ) {\n\t\t\treturn false;\n\t\t}\n\t\tconst config = mw.config.get( 'wgVisualEditorConfig' );\n\t\tconst visualEditorNamespaces = config.namespaces || [];\n\n\t\treturn visualEditorNamespaces.indexOf( mw.config.get( 'wgNamespaceNumber' ) ) !== -1;\n\t}\n\n\t/**\n\t * Check if the visual editor in source mode is available on this page\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {boolean}\n\t */\n\tisVESourceAvailable() {\n\t\treturn this.isVEAvailable() &&\n\t\t\tmw.config.get( 'wgMFEnableVEWikitextEditor' );\n\t}\n\n\t/**\n\t * Checks whether the current page is the main page\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {boolean}\n\t */\n\tisMainPage() {\n\t\treturn this._isMainPage;\n\t}\n\n\t/**\n\t * Checks whether the current page is watched\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {boolean}\n\t */\n\tisWatched() {\n\t\treturn this._isWatched;\n\t}\n\n\t/**\n\t * Return the latest revision id for this page\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {number}\n\t */\n\tgetRevisionId() {\n\t\treturn this.revId;\n\t}\n\n\t/**\n\t * Return prefixed page title\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {string}\n\t */\n\tgetTitle() {\n\t\treturn this.title;\n\t}\n\n\t/**\n\t * return namespace id\n\t *\n\t * @memberof module:mobile.startup/Page\n\t * @return {number} namespace Number\n\t */\n\tgetNamespaceId() {\n\t\tlet nsId;\n\t\tconst args = this.title.split( ':' );\n\n\t\tif ( args[1] ) {\n\t\t\tnsId = mw.config.get( 'wgNamespaceIds' )[ args[0].toLowerCase().replace( ' ', '_' ) ] || 0;\n\t\t} else {\n\t\t\tnsId = 0;\n\t\t}\n\t\treturn nsId;\n\t}\n}\n\nmodule.exports = Page;\n",
"const\n\tThumbnail = require( './Thumbnail' ),\n\tHEADING_SELECTOR = mw.config.get( 'wgMFMobileFormatterHeadings', [ 'h1', 'h2', 'h3', 'h4', 'h5' ] ).join( ',' ),\n\tEXCLUDE_THUMBNAIL_CLASS_SELECTORS = [ 'noviewer', 'metadata' ],\n\tNOT_SELECTORS = EXCLUDE_THUMBNAIL_CLASS_SELECTORS.map( ( excludeSelector ) => `:not(.${ excludeSelector })` ).join( '' ),\n\tTHUMB_SELECTOR = [ 'a.image', 'a.thumbimage, a.mw-file-description' ].map(\n\t\t( selector ) => `${ selector }${ NOT_SELECTORS }`\n\t).join( ',' );\n\nclass PageHTMLParser {\n\t/**\n\t * @constructor\n\t * @class module:mobile.startup/PageHTMLParser\n * @classdesc Parses an article and converts it into a queriable object.\n\t * @param {jQuery.Object} $container Used when parsing to find children within\n\t * this container\n\t */\n\tconstructor( $container ) {\n\t\tthis.$el = $container;\n\n\t\t// T220751: Cache headings as $el.find is a very expensive call.\n\t\t/** @private */\n\t\tthis.$headings = this.$el.find( HEADING_SELECTOR );\n\t}\n\n\t/**\n\t * Find the heading in the page.\n\t * This has the benefit of excluding any additional h2s and h3s that may\n\t * have been added programatically.\n\t *\n\t * @memberof module:mobile.startup/PageHTMLParser\n\t * @param {number} sectionIndex as defined by the PHP parser.\n\t * It should correspond to the section id\n\t * used in the edit link for the section.\n\t * Note, confusingly, this is different from section \"ID\" which is\n\t * used in methods\n\t * @return {jQuery.Object}\n\t */\n\tfindSectionHeadingByIndex( sectionIndex ) {\n\t\tif ( sectionIndex < 1 ) {\n\t\t\t// negative indexes will search from the end, which is behaviour we do not want.\n\t\t\t// return an empty set when this happens.\n\t\t\t// eslint-disable-next-line no-undef\n\t\t\treturn $( [] );\n\t\t} else {\n\t\t\treturn this.$headings\n\t\t\t\t// Headings must strictly be a child element of a section element\n\t\t\t\t// or the parser-output.\n\t\t\t\t// Not an ancestor!\n\t\t\t\t.filter( '.mw-parser-output > *, [class^=\"mf-section-\"] > *' ).eq( sectionIndex - 1 );\n\t\t}\n\t}\n\n\t/**\n\t * Finds all child elements that match the selector in a given section or subsection.\n\t * Returns any direct child elements that match the selector,\n\t * (i.e. searches only one level deep)\n\t * as well as any elements that match the selector within those children.\n\t * If the Page has no headings (e.g. a stub),\n\t * then the search will target all nodes within the page.\n\t *\n\t * This code should work on desktop (PHP parser HTML)\n\t * as well as mobile formatted HTML (PHP parser + MobileFormatter)\n\t *\n\t * @memberof module:mobile.startup/PageHTMLParser\n\t * @param {number} sectionIndex as defined by the PHP parser. It should correspond to\n\t * the section id used in the edit link for the section.\n\t * Note, confusingly, this is different from section \"ID\" which is\n\t * used in methods\n\t * @param {string} selector to match\n\t * @return {jQuery.Object}\n\t */\n\tfindChildInSectionLead( sectionIndex, selector ) {\n\t\tlet $heading, $nextHeading;\n\n\t\tconst headingSelector = HEADING_SELECTOR;\n\n\t\tfunction withNestedChildren( $matchingNodes ) {\n\t\t\treturn $matchingNodes.find( selector ).addBack();\n\t\t}\n\n\t\tif ( sectionIndex === 0 ) {\n\t\t\t// lead is easy\n\t\t\tconst $lead = this.getLeadSectionElement();\n\t\t\tif ( $lead && $lead.length ) {\n\n\t\t\t\t// Handle nested sections in Parsoid wikitext parset opt-in scenario.\n\t\t\t\tconst $nestedSection = $lead.find( 'section[data-mw-section-id=\"0\"]' );\n\t\t\t\tif ( $nestedSection.length ) {\n\t\t\t\t\treturn withNestedChildren( $nestedSection.children( selector ) );\n\t\t\t\t}\n\n\t\t\t\treturn withNestedChildren( $lead.children( selector ) );\n\t\t\t} else {\n\t\t\t\t$heading = this.findSectionHeadingByIndex( 1 );\n\t\t\t\treturn $heading.length ? withNestedChildren( $heading.prevAll( selector ) ) :\n\t\t\t\t\t// this page is a stub so search entire page\n\t\t\t\t\tthis.$el.find( selector );\n\t\t\t}\n\t\t}\n\n\t\t// find heading associated with the section by looking at its\n\t\t// index position in the article\n\t\t// section ids relate to the element position in the page and the first heading\n\t\t// lead has been dealt with above, so first heading corresponds to section 1,\n\t\t// the first heading in the article.\n\t\t$heading = this.findSectionHeadingByIndex( sectionIndex );\n\n\t\t// If section-heading is present on the heading,\n\t\t// then we know the page has been MobileFormatted\n\t\t// and that this is a wrapped section\n\t\tif ( $heading.hasClass( 'section-heading' ) ) {\n\t\t\t// get content of section\n\t\t\tconst $el = $heading.next();\n\t\t\t// inside section find the first heading\n\t\t\t$nextHeading = $el.find( headingSelector ).eq( 0 );\n\t\t\treturn $nextHeading.length ?\n\t\t\t\t// find all amboxes before the next heading\n\t\t\t\twithNestedChildren( $nextHeading.prevAll( selector ) ) :\n\t\t\t\t// There is no subheadings inside\n\t\t\t\t// Grab all issues in section\n\t\t\t\twithNestedChildren( $el.children( selector ) );\n\t\t} else {\n\t\t\t// the heading relates to a subsection (or unwrapped desktop section),\n\t\t\t// so grab elements between this and the next one\n\t\t\t$nextHeading = $heading.eq( 0 ).nextAll( headingSelector ).eq( 0 );\n\t\t\treturn $heading.nextUntil( $nextHeading, selector );\n\t\t}\n\t}\n\n\t/**\n\t * Get the lead section of the page view.\n\t *\n\t * @memberof module:mobile.startup/PageHTMLParser\n\t * @return {jQuery.Object|null}\n\t */\n\tgetLeadSectionElement() {\n\t\t/*\n\t\t * The page is formatted as follows:\n\t\t * <div id=\"bodyContent\">\n\t\t * <!-- content of the page.. -->\n\t\t * <div id=\"mw-content-text\">\n\t\t * <div class=\"mf-section-0\">lead section</div>\n\t\t * <h2></h2>\n\t\t * <div class=\"mf-section-1\">second section</div>\n\t\t * </div>\n\t\t * </div>\n\t\t */\n\t\tconst $leadSection = this.$el.find( '.mf-section-0' );\n\n\t\tif ( $leadSection.length ) {\n\t\t\treturn $leadSection;\n\t\t}\n\t\t// no lead section found\n\t\treturn null;\n\t}\n\n\t/**\n\t * Returns a Thumbnail object from an anchor element containing an image or\n\t * null if not valid.\n\t *\n\t * @memberof module:mobile.startup/PageHTMLParser\n\t * @param {jQuery} $a Anchor element that contains the image.\n\t * @return {Thumbnail|null}\n\t */\n\tgetThumbnail( $a ) {\n\t\tconst notSelector = '.' + EXCLUDE_THUMBNAIL_CLASS_SELECTORS.join( ',.' ),\n\t\t\t$lazyImage = $a.find( '.lazy-image-placeholder' ),\n\t\t\thref = $a.attr( 'href' ),\n\t\t\turl = href && new URL( href, location.href ),\n\t\t\tlegacyTitle = url && url.searchParams.get( 'title' ),\n\t\t\tmatch = url && url.pathname.match( /[^/]+$/ );\n\n\t\t// Parents need to be checked as well.\n\t\tlet valid = $a.parents( notSelector ).length === 0 &&\n\t\t\t$a.find( notSelector ).length === 0;\n\n\t\t// filter out invalid lazy loaded images if so far image is valid\n\t\tif ( $lazyImage.length && valid ) {\n\t\t\t// if the regex matches it means the image has one of the classes\n\t\t\t// thus we must invert the result\n\t\t\tvalid = !new RegExp( '\\\\b(' + EXCLUDE_THUMBNAIL_CLASS_SELECTORS.join( '|' ) + ')\\\\b' )\n\t\t\t\t.test( $lazyImage.data( 'class' ) );\n\t\t}\n\n\t\tif ( valid && ( legacyTitle !== null || match ) ) {\n\t\t\treturn new Thumbnail( {\n\t\t\t\tel: $a,\n\t\t\t\tfilename: mw.util.percentDecodeFragment(\n\t\t\t\t\tlegacyTitle !== null ? legacyTitle : match[0]\n\t\t\t\t)\n\t\t\t} );\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * Return all the thumbnails in the article.\n\t * Images which have a class or link container (.image|.thumbimage)\n\t * that matches one of the items of the constant EXCLUDE_THUMBNAIL_CLASS_SELECTORS\n\t * will be excluded.\n\t * A thumbnail nested inside one of these classes will still be returned.\n\t * e.g. `<div class=\"noviewer\"><a class=\"image\"><img></a></div>` is not a valid thumbnail\n\t * `<a class=\"image noviewer\"><img></a>` is not a valid thumbnail\n\t * `<a class=\"image\"><img class=\"noviewer\"></a>` is not a valid thumbnail\n\t *\n\t * @memberof module:mobile.startup/PageHTMLParser\n\t * @param {jQuery} [$el] Container to search, defaults to this.$el.\n\t * @return {Thumbnail[]}\n\t */\n\tgetThumbnails( $el ) {\n\t\tconst\n\t\t\tself = this,\n\t\t\tthumbs = [];\n\n\t\t$el = $el || this.$el;\n\n\t\tconst $thumbs = $el.find( THUMB_SELECTOR );\n\n\t\t$thumbs.each( function () {\n\t\t\tconst $a = $el.find( this );\n\t\t\tconst thumb = self.getThumbnail( $a );\n\n\t\t\tif ( thumb ) {\n\t\t\t\tthumbs.push( thumb );\n\t\t\t}\n\t\t} );\n\t\treturn thumbs;\n\t}\n\n\t/**\n\t * Returns a jQuery object representing all redlinks on the page.\n\t *\n\t * @memberof module:mobile.startup/PageHTMLParser\n\t * @return {jQuery.Object}\n\t */\n\tgetRedLinks() {\n\t\treturn this.$el.find( '.new' );\n\t}\n\n\t/**\n\t * Returns an object consistent with MediaWiki API representing languages\n\t * associated with the page in the user's current language.\n\t *\n\t * @memberof module:mobile.startup/PageHTMLParser\n\t * @param {string} pageTitle to fallback to if none found\n\t * @return {Object} containing langlinks\n\t * and variant links as defined @ https://en.m.wikipedia.org/w/api.php?action=help&modules=query%2Blanglinks\n\t */\n\tgetLanguages( pageTitle ) {\n\t\tconst mapLinkToLanguageObj = ( node ) => {\n\t\t\tconst DELIMITER = ' \u2013 ';\n\t\t\t// Name of language (e.g. \u05e2\u05d1\u05e8\u05d9\u05ea for Hebrew)\n\t\t\tconst autonym = node.textContent;\n\t\t\t// The name of the language in the current language\n\t\t\t// e.g. for english this would be Hebrew\n\t\t\tlet langname;\n\t\t\tlet title = node.getAttribute( 'title' ) || pageTitle;\n\t\t\tif ( title.indexOf( DELIMITER ) > -1 ) {\n\t\t\t\ttitle = title.split( DELIMITER );\n\t\t\t\tlangname = title.pop();\n\t\t\t\ttitle = title.join( DELIMITER );\n\t\t\t}\n\t\t\tif ( !langname ) {\n\t\t\t\tlangname = autonym;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tlang: node.getAttribute( 'hreflang' ),\n\t\t\t\tautonym,\n\t\t\t\tlangname,\n\t\t\t\ttitle,\n\t\t\t\turl: node.getAttribute( 'href' )\n\t\t\t};\n\t\t};\n\t\treturn {\n\t\t\tlanguages: Array.prototype.map.call(\n\t\t\t\tdocument.querySelectorAll( '#p-lang .interlanguage-link a' ),\n\t\t\t\tmapLinkToLanguageObj\n\t\t\t),\n\t\t\tvariants: Array.prototype.map.call(\n\t\t\t\tdocument.querySelectorAll( '#p-variants li a' ),\n\t\t\t\tmapLinkToLanguageObj\n\t\t\t)\n\t\t};\n\t}\n}\n\n/**\n * Selector for matching headings\n *\n * @memberof PageHTMLParser\n */\nPageHTMLParser.HEADING_SELECTOR = HEADING_SELECTOR;\n\n/**\n * Selector for thumbnails.\n *\n * @memberof PageHTMLParser\n */\nPageHTMLParser.THUMB_SELECTOR = THUMB_SELECTOR;\n\nmodule.exports = PageHTMLParser;\n",
"const util = require( './util.js' ),\n\tmfExtend = require( './mfExtend' ),\n\tView = require( './View' ),\n\tbrowser = require( './Browser' ).getSingleton();\n\n/**\n * List of items page view\n *\n * @class PageList\n * @extends module:mobile.startup/View\n */\nfunction PageList() {\n\tView.apply( this, arguments );\n}\n\nmfExtend( PageList, View, {\n\t/**\n\t * @memberof PageList\n\t * @instance\n\t * @mixes module:mobile.startup/View#defaults\n\t * @property {Object} defaults Default options hash.\n\t * @property {Page[]} defaults.pages Array of Page objects. These should match\n\t * the Page model and not necessarily the\n\t * underlying API format used.\n\t * E.g. [\n\t * {\n\t * heading: \"<strong>C</strong>laude Monet\",\n\t * id: undefined,\n\t * title: \"Claude Monet\",\n\t * displayTitle: \"<i>Claude Monet</i>\",\n\t * url: \"/wiki/Claude_Monet\",\n\t * thumbnail: {\n\t * height: 62,\n\t * source: \"http://127.0.0.1:8080/images/thumb/thumb.jpg\",\n\t * width: 80,\n\t * isLandscape: true\n\t * }\n\t * }\n\t * ]\n\t */\n\tdefaults: {\n\t\tpages: []\n\t},\n\t/**\n\t * Render page images for the existing page list. Assumes no page images have been loaded.\n\t *\n\t * @memberof PageList\n\t * @instance\n\t */\n\trenderPageImages() {\n\t\tconst self = this;\n\n\t\tsetTimeout( () => {\n\t\t\tself.$el.find( '.list-thumb' ).each( function () {\n\t\t\t\tconst style = self.$el.find( this ).data( 'style' );\n\t\t\t\tself.$el.find( this ).attr( 'style', style );\n\t\t\t} );\n\t\t\t// Delay an unnecessary load of images on mobile (slower?) connections\n\t\t\t// In particular on search results which can be regenerated quickly.\n\t\t}, browser.isWideScreen() ? 0 : 1000 );\n\t},\n\t/**\n\t * @inheritdoc\n\t * @memberof PageList\n\t * @instance\n\t */\n\tpostRender() {\n\t\tthis.renderPageImages();\n\t},\n\ttemplate: util.template( `\n<ul class=\"mw-mf-page-list thumbs actionable\">\n\t{{#pages}}\n\t\t{{>item}}\n\t{{/pages}}\n</ul>\n\t` ),\n\t/**\n\t * @memberof PageList\n\t * @instance\n\t */\n\ttemplatePartials: {\n\t\t// The server uses a very different structure in\n\t\t// SpecialMobileEditWatchlist.getLineHtml(). Be aware of these differences\n\t\t// when updating server rendered items.\n\t\titem: util.template( `\n<li title=\"{{title}}\" data-id=\"{{id}}\" class=\"page-summary\">\n <a href=\"{{url}}\" class=\"title {{#isMissing}}new{{/isMissing}}\"\n {{#anchor}}name=\"{{anchor}}\"{{/anchor}}\n {{#latitude}}data-latlng=\"{{latitude}},{{longitude}}\"{{/latitude}}\n data-title=\"{{title}}\">\n <div class=\"list-thumb\n {{#thumbnail.isLandscape}}list-thumb-y{{/thumbnail.isLandscape}}\n {{^thumbnail.isLandscape}}list-thumb-x{{/thumbnail.isLandscape}}\"\n {{#thumbnail}}data-style=\"background-image: url( {{thumbnail.source}} )\"{{/thumbnail}}>\n {{^thumbnail}}<span class=\"mf-icon-image\"></span>{{/thumbnail}}\n\t</div>\n <h3>{{{displayTitle}}}</h3>\n {{#wikidataDescription}}\n <div class=\"wikidata-description\">{{wikidataDescription}}</div>\n {{/wikidataDescription}}\n {{#proximity}}\n <div class=\"info proximity\">{{proximity}}</div>\n {{/proximity}}\n </a>\n</li>\n\t` )\n\t}\n} );\n\nmodule.exports = PageList;\n",
"const browser = require( './Browser' ).getSingleton(),\n\tView = require( './View' ),\n\tutil = require( './util' ),\n\tcurrentPage = require( './currentPage' ),\n\teventBus = require( './eventBusSingleton' ),\n\tmfExtend = require( './mfExtend' );\n\nlet skin;\n\n/**\n * Representation of the current skin being rendered.\n *\n * @class Skin\n * @extends module:mobile.startup/View\n * @uses Browser\n * @uses Page\n * @fires Skin#click\n * @param {Object} params Configuration options\n * @param {OO.EventEmitter} params.eventBus Object used to listen for\n * @param {Page} params.page\n * scroll:throttled, resize:throttled, and section-toggled events\n */\nfunction Skin( params ) {\n\tconst options = util.extend( {}, params );\n\n\tthis.page = options.page;\n\tthis.name = options.name;\n\tthis.eventBus = options.eventBus;\n\toptions.isBorderBox = false;\n\tView.call( this, options );\n}\n\nmfExtend( Skin, View, {\n\t/**\n\t * @memberof Skin\n\t * @instance\n\t * @mixes module:mobile.startup/View#defaults\n\t * @property {Object} defaults Default options hash.\n\t * @property {Page} defaults.page page the skin is currently rendering\n\t */\n\tdefaults: {\n\t\tpage: undefined\n\t},\n\n\t/**\n\t * @inheritdoc\n\t * @memberof Skin\n\t * @instance\n\t */\n\tpostRender() {\n\t\tconst $el = this.$el;\n\n\t\tif ( browser.supportsTouchEvents() ) {\n\t\t\t$el.addClass( 'touch-events' );\n\t\t}\n\n\t\t/**\n\t\t * Fired when the skin is clicked.\n\t\t *\n\t\t * @event Skin#click\n\t\t */\n\t\tthis.$el.find( '#mw-mf-page-center' ).on( 'click', ( ev ) => {\n\t\t\tthis.emit( 'click', ev );\n\t\t} );\n\t},\n\n\t/**\n\t * @memberof Skin\n\t * @throws {Error} if mediawiki message is in unexpected format.\n\t * @return {jQuery.Object} a list of links\n\t */\n\tgetLicenseLinks() {\n\t\tconst mobileLicense = mw.message( 'mobile-frontend-license-links' );\n\t\tconst mobileMsgExists = mobileLicense.exists() && mobileLicense.text();\n\t\tconst userLanguage = mw.config.get( 'wgUserLanguage' );\n\t\tif ( userLanguage === 'qqx' ) {\n\t\t\t// Special handling for qqx code so we can easily debug what's going on here.\n\t\t\treturn mobileLicense.parseDom();\n\t\t} else {\n\t\t\treturn mobileMsgExists ? mobileLicense.parseDom() : this.$el.find( '#footer-info-copyright a' ).clone();\n\t\t}\n\t},\n\t/**\n\t * Returns the appropriate license message including links/name to\n\t * terms of use (if any) and license page\n\t *\n\t * @memberof Skin\n\t * @instance\n\t * @return {string|undefined}\n\t */\n\tgetLicenseMsg() {\n\t\tconst $licenseLinks = this.getLicenseLinks();\n\n\t\tlet licenseMsg;\n\t\tif ( $licenseLinks.length ) {\n\t\t\tconst licensePlural = mw.language.convertNumber(\n\t\t\t\t$licenseLinks.filter( 'a' ).length\n\t\t\t);\n\n\t\t\tif ( this.$el.find( '#footer-places-terms-use' ).length > 0 ) {\n\n\t\t\t\tconst $termsLink = mw.message(\n\t\t\t\t\t'mobile-frontend-editor-terms-link',\n\t\t\t\t\tthis.$el.find( '#footer-places-terms-use a' ).attr( 'href' )\n\t\t\t\t).parseDom();\n\t\t\t\tlicenseMsg = mw.message(\n\t\t\t\t\t'mobile-frontend-editor-licensing-with-terms',\n\t\t\t\t\t$termsLink,\n\t\t\t\t\t$licenseLinks,\n\t\t\t\t\tlicensePlural\n\t\t\t\t).parse();\n\t\t\t} else {\n\t\t\t\tlicenseMsg = mw.message(\n\t\t\t\t\t'mobile-frontend-editor-licensing',\n\t\t\t\t\t$licenseLinks,\n\t\t\t\t\tlicensePlural\n\t\t\t\t).parse();\n\t\t\t}\n\t\t}\n\n\t\treturn licenseMsg;\n\t}\n} );\n\n/**\n * Get a skin singleton\n *\n * @return {Skin}\n */\nSkin.getSingleton = function () {\n\tif ( !skin ) {\n\t\tskin = new Skin( {\n\t\t\tel: 'body',\n\t\t\tpage: currentPage(),\n\t\t\teventBus\n\t\t} );\n\t}\n\treturn skin;\n};\nmodule.exports = Skin;\n",
"/* global $ */\nconst util = require( './util' ),\n\tmfExtend = require( './mfExtend' ),\n\t// Cached regex to split keys for `delegate`.\n\tdelegateEventSplitter = /^(\\S+)\\s*(.*)$/;\n\nlet idCounter = 0;\n\n/**\n * Generate a unique integer id (unique within the entire client session).\n * Useful for temporary DOM ids.\n *\n * @private\n * @param {string} prefix Prefix to be used when generating the id.\n * @return {string}\n */\nfunction uniqueId( prefix ) {\n\tconst id = ( ++idCounter ).toString();\n\treturn prefix ? prefix + id : id;\n}\n\n/**\n * Should be extended using extend().\n *\n * When options contains el property, this.$el in the constructed object\n * will be set to the corresponding jQuery object. Otherwise, this.$el\n * will be an empty div.\n *\n * When extended using extend(), if the extended prototype contains\n * template property, this.$el will be filled with rendered template (with\n * options parameter used as template data).\n *\n * template property can be a string which will be passed to mw.template.compile()\n * or an object that has a render() function which accepts an object with\n * template data as its argument (similarly to an object created by\n * mw.template.compile()).\n *\n * You can also define a defaults property which should be an object\n * containing default values for the template (if they're not present in\n * the options parameter).\n *\n * If this.$el is not a jQuery object bound to existing DOM element, the\n * view can be attached to an element using appendTo(), prependTo(),\n * insertBefore(), insertAfter() proxy functions.\n *\n * append(), prepend(), before(), after() can be used to modify $el. on()\n * can be used to bind events.\n *\n * You can also use declarative DOM events binding by specifying an `events`\n * map on the class. The keys will be 'event selector' and the value can be\n * either the name of a method to call, or a function. All methods and\n * functions will be executed on the context of the View.\n *\n * Inspired from Backbone.js\n * https://github.com/jashkenas/backbone/blob/master/backbone.js#L1128\n *\n * Example:\n * ```js\n * var\n * MyComponent = View.extend( {\n * edit: function ( ev ) {\n * //...\n * },\n * save: function ( ev ) {\n * //...\n * }\n * } ),\n * instance = new MyComponent({\n * events: {\n * 'mousedown .title': 'edit',\n * 'click .button': 'save',\n * 'click .open': function(e) { ... }\n * }\n * });\n * ```\n *\n * Example:\n * ```js\n * var View, section;\n * function Section( options ) {\n * var defaultOptions = {\n * events: {\n * // ...\n * }\n * }\n * View.call( this, util.extends( {}, defaultOptions, options ) );\n * }\n * View = require( './View' );\n * require( './mfExtend' )( Section, View, {\n * template: mw.template.compile( \"<h2>{{title}}</h2>\" ),\n * } );\n * section = new Section( { title: 'Test', text: 'Test section body' } );\n * section.appendTo( 'body' );\n * ```\n *\n * @class module:mobile.startup/View\n * @memberof module:mobile.startup/View\n * @classdesc Describes a component for rendering.\n * @mixes OO.EventEmitter\n */\n\nfunction View() {\n\tthis.initialize.apply( this, arguments );\n}\nOO.mixinClass( View, OO.EventEmitter );\nmfExtend( View, {\n\t/**\n\t * Name of tag that contains the rendered template\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @property {string} tagName\n\t */\n\ttagName: 'div',\n\t/**\n\t * Tells the View to ignore tagName and className when constructing the element\n\t * and to rely solely on the template\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @property {boolean} isTemplateMode\n\t */\n\tisTemplateMode: false,\n\t/**\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @property {any}\n\t * Specifies the template used in render(). Object|string\n\t */\n\ttemplate: undefined,\n\n\t/**\n\t * Specifies partials (sub-templates) for the main template. Example:\n\t *\n\t * @example\n\t * // example content for the \"some\" template (sub-template will be\n\t * // inserted where {{>content}} is):\n\t * // <h1>Heading</h1>\n\t * // {{>content}}\n\t *\n\t * oo.mfExtend( SomeView, View, {\n\t * template: util.template( '<source-code>' ),\n\t * templatePartials: { content: util.template( '<source-code>' ) }\n\t * }\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @property {Object}\n\t */\n\ttemplatePartials: {},\n\n\t/**\n\t * A set of default options that are merged with options passed into the initialize\n\t * function.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @property {Object} defaults Default options hash.\n\t * @property {jQuery.Object|string} [defaults.el] jQuery selector to use for rendering.\n\t * @property {boolean} [defaults.skipTemplateRender] Whether to enhance views already in\n\t * DOM. When enabled, the template is disabled so that it is not rendered in the DOM.\n\t * Use in conjunction with View::defaults.$el to associate the View with an existing\n\t * already rendered element in the DOM.\n\t */\n\tdefaults: {},\n\n\t/**\n\t * Run once during construction to set up the View\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @param {Object} options Object passed to the constructor.\n\t * @param {Object.<string, string>} [options.events]\n\t */\n\tinitialize( options ) {\n\t\tconst self = this;\n\n\t\tOO.EventEmitter.call( this );\n\t\toptions = util.extend( {}, this.defaults, options );\n\t\tthis.options = options;\n\t\t// Assign a unique id for dom events binding/unbinding\n\t\tthis.cid = uniqueId( 'view' );\n\n\t\t// TODO: if template compilation is too slow, don't compile them on a\n\t\t// per object basis, but don't worry about it now (maybe add cache to\n\t\t// M.template.compile())\n\t\tif ( typeof this.template === 'string' ) {\n\t\t\tthis.template = mw.template.compile( this.template );\n\t\t}\n\n\t\tif ( options.el ) {\n\t\t\t// Note the element may not be in the document so must use global jQuery here\n\t\t\tthis.$el = $( options.el );\n\t\t} else {\n\t\t\tthis.$el = this.parseHTML( '<' + this.tagName + '>' );\n\t\t}\n\n\t\t// Make sure the element is ready to be manipulated\n\t\tif ( this.$el.length ) {\n\t\t\tthis._postInitialize( options );\n\t\t} else {\n\t\t\tutil.docReady( () => {\n\t\t\t\t// Note the element may not be in the document so must use global jQuery here\n\t\t\t\tself.$el = $( options.el );\n\t\t\t\tself._postInitialize( options );\n\t\t\t} );\n\t\t}\n\t},\n\n\t/**\n\t * Called when this.$el is ready.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @private\n\t * @param {Object} props\n\t */\n\t_postInitialize( props ) {\n\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\tthis.$el.addClass( props.className );\n\t\t// border-box will be added provided this flag is not set\n\t\tif ( props.isBorderBox !== false ) {\n\t\t\tthis.$el.addClass( 'view-border-box' );\n\t\t}\n\n\t\tthis.render( {} );\n\t},\n\n\t/**\n\t * Function called before the view is rendered. Can be redefined in\n\t * objects that extend View.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t */\n\tpreRender() {\n\t},\n\n\t/**\n\t * Function called after the view is rendered. Can be redefined in\n\t * objects that extend View.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t */\n\tpostRender() {\n\t},\n\n\t/**\n\t * Fill this.$el with template rendered using data if template is set.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @param {Object} data Template data. Will be merged into the view's\n\t * options\n\t * @chainable\n\t */\n\trender( data ) {\n\t\tlet $el, html;\n\t\tutil.extend( this.options, data );\n\t\tthis.preRender();\n\t\tthis.undelegateEvents();\n\t\tif ( this.template && !this.options.skipTemplateRender ) {\n\t\t\thtml = this.template.render( this.options, this.templatePartials );\n\t\t\tif ( this.isTemplateMode ) {\n\t\t\t\t$el = $( html );\n\t\t\t\tthis.$el.replaceWith( $el );\n\t\t\t\tthis.$el = $el;\n\t\t\t} else {\n\t\t\t\tthis.$el.html( html );\n\t\t\t}\n\t\t}\n\t\tthis.postRender();\n\t\tthis.delegateEvents();\n\t\treturn this;\n\t},\n\n\t/**\n\t * Set callbacks for events.\n\t *\n\t * `this.options.events` is a hash of pairs:\n\t *\n\t * ```\n\t * { 'event selector': 'callback' }\n\t *\n\t * {\n\t * 'mousedown .title': 'edit',\n\t * 'click .button': 'save',\n\t * 'click .open': function(e) { ... }\n\t * }\n\t * ```\n\t *\n\t * Callbacks will be bound to the view, with `this` set properly.\n\t * Uses event delegation for efficiency.\n\t * Omitting the selector binds the event to `this.el`.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @param {Object} events Optionally set this events instead of the ones on this.\n\t */\n\tdelegateEvents( events ) {\n\t\tlet match, key, method;\n\t\tevents = events || this.options.events;\n\t\tif ( events ) {\n\t\t\t// Remove current events before re-binding them\n\t\t\tthis.undelegateEvents();\n\t\t\tfor ( key in events ) {\n\t\t\t\tmethod = events[key];\n\t\t\t\t// If the method is a string name of this.method, get it\n\t\t\t\tif ( typeof method !== 'function' ) {\n\t\t\t\t\tmethod = this[events[key]];\n\t\t\t\t}\n\t\t\t\tif ( method ) {\n\t\t\t\t\t// Extract event and selector from the key\n\t\t\t\t\tmatch = key.match( delegateEventSplitter );\n\t\t\t\t\tthis.delegate( match[1], match[2], method.bind( this ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t/**\n\t * Add a single event listener to the view's element (or a child element\n\t * using `selector`). This only works for delegate-able events: not `focus`\n\t * or `blur`.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @param {string} eventName\n\t * @param {string} selector\n\t * @param {Function} listener\n\t */\n\tdelegate( eventName, selector, listener ) {\n\t\tthis.$el.on( eventName + '.delegateEvents' + this.cid, selector,\n\t\t\tlistener );\n\t},\n\n\t/**\n\t * Clears all callbacks previously bound to the view by `delegateEvents`.\n\t * You usually don't need to use this, but may wish to if you have multiple\n\t * views attached to the same DOM element.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t */\n\tundelegateEvents() {\n\t\tif ( this.$el ) {\n\t\t\tthis.$el.off( '.delegateEvents' + this.cid );\n\t\t}\n\t},\n\n\t/**\n\t * A finer-grained `undelegateEvents` for removing a single delegated event.\n\t * `selector` and `listener` are both optional.\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @param {string} eventName\n\t * @param {string} selector\n\t * @param {Function} listener\n\t */\n\tundelegate( eventName, selector, listener ) {\n\t\tthis.$el.off( eventName + '.delegateEvents' + this.cid, selector,\n\t\t\tlistener );\n\t},\n\n\t/**\n\t * See parseHTML method of util singleton\n\t *\n\t * @memberof module:mobile.startup/View\n\t * @instance\n\t * @param {string} html to turn into a jQuery object.\n\t * @return {jQuery.Object}\n\t */\n\tparseHTML( html ) {\n\t\t// document is explicitly passed due to a bug we found in Safari 11.1.2 where failure\n\t\t// to use document resulted in an element without access to the documentElement\n\t\t// this should be redundant, but no problem in being explicit (T214451).\n\t\treturn util.parseHTML( html, document );\n\t}\n} );\n\n/**\n * @memberof View\n * @instance\n * @function append\n * @param {...(string|Node|Node[]|jQuery)} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function append\n * @param {function(number, string): string|Node|Node[]|jQuery} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function prepend\n * @param {...(string|Node|Node[]|jQuery)} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function prepend\n * @param {function(number, string): string|Node|Node[]|jQuery} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function appendTo\n * @param {string|Node|Node[]|jQuery} target\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function prependTo\n * @param {string|Node|Node[]|jQuery} target\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function after\n * @param {...(string|Node|Node[]|jQuery)} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function after\n * @param {function(number, string): string|Node|Node[]|jQuery} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @functiontion before\n * @param {...(string|Node|Node[]|jQuery)} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function before\n * @param {function(number, string): string|Node|Node[]|jQuery} contents\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @property {jQuery.Object} $el\n */\n\n/**\n * @memberof View\n * @instance\n * @function insertAfter\n * @param {string|Node|Node[]|jQuery} target\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function insertBefore\n * @param {string|Node|Node[]|jQuery} target\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function remove\n * @param {string} [selector]\n * @return {View}\n */\n\n/**\n * @memberof View\n * @instance\n * @function detach\n * @param {string} [selector]\n * @return {View}\n */\n\n[\n\t'append',\n\t'prepend',\n\t'appendTo',\n\t'prependTo',\n\t'after',\n\t'before',\n\t'insertAfter',\n\t'insertBefore',\n\t'remove',\n\t'detach'\n].forEach( ( prop ) => {\n\tView.prototype[prop] = function () {\n\t\tthis.$el[prop].apply( this.$el, arguments );\n\t\treturn this;\n\t};\n} );\n\n/**\n * Generates a view with children\n *\n * @param {Object} options\n * @param {jQuery.Element[]} children\n * @return {module:mobile.startup/View}\n */\nView.make = function ( options = {}, children = [] ) {\n\tconst view = new View( options );\n\tchildren.forEach( ( $child ) => view.append( $child ) );\n\treturn view;\n};\n\nmodule.exports = View;\n",
"const\n\tButton = require( '../Button' ),\n\tutil = require( '../util' ),\n\tView = require( '../View' );\n\n/**\n * @typedef {Object} FormField\n * @private\n * @property {string} name\n * @property {string} value\n */\n\n/**\n * A form for enabling the advanced mobile editor mode.\n *\n * @param {Object} options\n * @param {string} options.postUrl Form will POST to this endpoint\n * @param {FormField[]} options.fields An array of hidden form fields\n * @param {string} options.buttonLabel Label for submit button\n * submitted\n * @extends module:mobile.startup/View\n * @ignore\n */\nclass AmcEnableForm extends View {\n\t/** @inheritdoc */\n\tget isTemplateMode() {\n\t\treturn true;\n\t}\n\n\t/** @inheritdoc */\n\tget template() {\n\t\treturn util.template( `\n<form class=\"amc-enable-form\" action=\"{{postUrl}}\" method=\"POST\">\n\t{{#fields}}\n\t\t<input type=\"hidden\" name=\"{{name}}\" value=\"{{value}}\">\n\t{{/fields}}\n</form>\n\t\t` );\n\t}\n\n\t/** @inheritdoc */\n\tpostRender() {\n\t\tthis.$el.append(\n\t\t\tnew Button( {\n\t\t\t\ttagName: 'button',\n\t\t\t\tprogressive: true,\n\t\t\t\tlabel: this.options.buttonLabel\n\t\t\t} ).$el\n\t\t);\n\t}\n}\n\nmodule.exports = AmcEnableForm;\n",
"/**\n * Describes a promotional campaign for advanced mobile contributions.\n *\n * @typedef {Object} module:mobile.startup/AmcOutreach~PromoCampaign\n * @property {Function} showIfEligible\n * @property {Function} makeActionIneligible\n * @property {Function} makeAllActionsIneligible\n * @property {Function} isCampaignActive\n */\n\nconst\n\ttoast = require( '../showOnPageReload' ),\n\tcreatePromoCampaign = require( '../promoCampaign/promoCampaign' ),\n\tamcOutreachDrawer = require( './amcOutreachDrawer' ),\n\t// MW constants should be kept in sync with onMakeGlobalVariableScript() from\n\t// MobileFrontendHooks.php\n\tMW_CONFIG_CAMPAIGN_ACTIVE_NAME = 'wgMFAmcOutreachActive',\n\tMW_CONFIG_USER_ELIGIBLE_NAME = 'wgMFAmcOutreachUserEligible',\n\t// This object contains arbitrary actions that are meant to only be shown one\n\t// time at most. Each action is either 'eligible' or 'ineligible' at any given\n\t// time.\n\t//\n\t// When a new action is desired, it should be added to this object.\n\t//\n\t// In other languages, this would be an enum. However, because JS doesn't\n\t// support enums, the keys/value names are identical in an attempt to mimic\n\t// some of their functionality. The names in this object are used by\n\t// promoCampaign to mark when an action has become 'ineligible'.\n\tACTIONS = {\n\t\tonDesktopLink: 'onDesktopLink',\n\t\tonHistoryLink: 'onHistoryLink',\n\t\tonTalkLink: 'onTalkLink'\n\t},\n\tCAMPAIGN_NAME = 'amc-outreach';\n\n// singleton;\nlet campaign;\n\n/**\n * Internal for use inside Minerva only. See {@link module:mobile.startup} for access.\n *\n * @exports mobile.startup/AmcOutreach\n */\nmodule.exports = {\n\t/**\n\t * Loads a promotion campaign for advanced mobile contributions if the user is eligible.\n\t *\n\t * @return {module:mobile.startup/AmcOutreach~PromoCampaign}\n\t */\n\tloadCampaign: () => {\n\t\tif ( campaign ) {\n\t\t\treturn campaign;\n\t\t}\n\n\t\tcampaign = createPromoCampaign(\n\t\t\t/**\n\t\t\t * This callback is executed by promoCampaign's `showIfEligible` method.\n\t\t\t * promoCampaign will only execute it when an action is 'eligible'.\n\t\t\t *\n\t\t\t * @param {string} action Name of one of the actions in the ACTIONS\n\t\t\t * object. This is used by the drawer to notify promoCampaign when the\n\t\t\t * action has become 'ineligible' (e.g. after enabling or dismissing the\n\t\t\t * drawer).\n\t\t\t * @param {onBeforeHide} onBeforeHide Callback exected after user\n\t\t\t * dismisses drawer.\n\t\t\t * @param {string} returnToTitle Title of page to redirect to after user enables\n\t\t\t * AMC\n\t\t\t * @param {string} [returnToQuery] Optional query params to add to redirected\n\t\t\t * URL after user enables AMC. Can also include anchor (e.g.\n\t\t\t * `foo=bar#/Talk`\n\t\t\t * @return {module:mobile.startup/Drawer|null}\n\t\t\t */\n\t\t\t( action, onBeforeHide, returnToTitle, returnToQuery ) => {\n\t\t\t\treturn amcOutreachDrawer(\n\t\t\t\t\taction,\n\t\t\t\t\tcampaign,\n\t\t\t\t\tmw.message,\n\t\t\t\t\tmw.util,\n\t\t\t\t\ttoast,\n\t\t\t\t\tmw.user.tokens.get( 'csrfToken' ),\n\t\t\t\t\tonBeforeHide,\n\t\t\t\t\treturnToTitle,\n\t\t\t\t\treturnToQuery\n\t\t\t\t);\n\t\t\t},\n\t\t\tACTIONS,\n\t\t\tCAMPAIGN_NAME,\n\t\t\t// in minerva desktop, this config will not be set\n\t\t\t!!mw.config.get( MW_CONFIG_CAMPAIGN_ACTIVE_NAME ),\n\t\t\t!!mw.config.get( MW_CONFIG_USER_ELIGIBLE_NAME ),\n\t\t\tmw.storage\n\t\t);\n\n\t\treturn campaign;\n\t},\n\t/**\n\t * @private\n\t */\n\tACTIONS\n};\n",
"const\n\tDrawer = require( '../Drawer' ),\n\tAnchor = require( '../Anchor' ),\n\tutil = require( '../util' ),\n\tAmcEnableForm = require( './AmcEnableForm' ),\n\t// These constants should be kept in sync with SpecialMobileOptions.php\n\tAMC_ENABLE_FIELD_NAME = 'enableAMC',\n\tAMC_ENABLE_FIELD_VALUE = '1';\n\n/**\n * Callback intended to allow the client run extra logic (e.g. show a modal)\n * after the drawer is dismissed.\n *\n * @callback module:mobile.startup~onBeforeHide\n */\n\n/**\n * @ignore\n * @memberof module:mobile.startup\n * @param {string} action Used by the drawer to notify promoCampaign when the\n * action has become 'ineligible' (e.g. after enabling or dismissing the drawer).\n * @param {module:mobile.startup/AmcOutreach~PromoCampaign} promoCampaign\n * @param {mw.message} mwMessage Used for i18n\n * @param {mw.util} mwUtil Used to determine POST url for the enable form\n * @param {Toast} toast Displays success message after user submits enable form\n * @param {string} csrfToken\n * @param {onBeforeHide} onBeforeHide\n * @param {string} returnToTitle Title to redirect to after user enables\n * AMC\n * @param {string} [returnToQuery] Optional query params to add to redirected\n * URL after user enables AMC\n * @return {module:mobile.startup/Drawer} Returns the drawer that is shown\n */\nmodule.exports = function amcOutreachDrawer(\n\taction,\n\tpromoCampaign,\n\tmwMessage,\n\tmwUtil,\n\ttoast,\n\tcsrfToken,\n\tonBeforeHide,\n\treturnToTitle,\n\treturnToQuery\n) {\n\treturn new Drawer( {\n\t\tclassName: 'amc-outreach-drawer',\n\t\tchildren: [\n\t\t\tutil.parseHTML( '<div>' ).addClass( 'amc-outreach-image' ),\n\t\t\tutil.parseHTML( '<p>' ).append(\n\t\t\t\tutil.parseHTML( '<strong>' ).text(\n\t\t\t\t\tmwMessage( 'mobile-frontend-amc-outreach-intro' ).text()\n\t\t\t\t)\n\t\t\t),\n\t\t\tutil.parseHTML( '<p>' ).text(\n\t\t\t\tmwMessage( 'mobile-frontend-amc-outreach-description' ).text()\n\t\t\t),\n\t\t\tnew AmcEnableForm( {\n\t\t\t\tpostUrl: mwUtil.getUrl( 'Special:MobileOptions', {\n\t\t\t\t\treturnto: returnToTitle,\n\t\t\t\t\treturntoquery: returnToQuery || ''\n\t\t\t\t} ),\n\t\t\t\tfields: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'updateSingleOption',\n\t\t\t\t\t\tvalue: AMC_ENABLE_FIELD_NAME\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'enableAMC',\n\t\t\t\t\t\tvalue: AMC_ENABLE_FIELD_VALUE\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'token',\n\t\t\t\t\t\tvalue: csrfToken\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\tbuttonLabel: mwMessage( 'mobile-frontend-amc-outreach-enable' ).text(),\n\t\t\t\tevents: {\n\t\t\t\t\tsubmit: () => {\n\t\t\t\t\t\tpromoCampaign.makeActionIneligible( action );\n\t\t\t\t\t\ttoast.showOnPageReload( mwMessage( 'mobile-frontend-amc-outreach-enabled-message' ).text() );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ).$el,\n\t\t\tnew Anchor( {\n\t\t\t\thref: '#',\n\t\t\t\tadditionalClassNames: 'cancel',\n\t\t\t\tprogressive: true,\n\t\t\t\tlabel: mwMessage( 'mobile-frontend-amc-outreach-no-thanks' ).text()\n\t\t\t} ).$el\n\t\t],\n\t\tonBeforeHide: () => {\n\t\t\tpromoCampaign.makeActionIneligible( action );\n\t\t\tonBeforeHide();\n\t\t}\n\t} );\n};\n",
"/* global $ */\nconst\n\tPage = require( './Page' );\n\nlet page;\n\n/**\n * Constructs an incomplete Page model singleton representing the currently loaded page.\n *\n * Because this depends on the presence of certain DOM elements, it\n * should only be called after the DOMContentLoaded event.\n *\n * @ignore\n * @return {Page}\n */\nfunction loadCurrentPage() {\n\tif ( page ) {\n\t\treturn page;\n\t}\n\n\tconst permissions = [].concat( mw.config.get( 'wgRestrictionEdit', [] ) ),\n\t\trelevantTitle = mw.Title.newFromText( mw.config.get( 'wgRelevantPageName' ) ),\n\t\ttitle = mw.Title.newFromText( mw.config.get( 'wgPageName' ) );\n\n\tif ( permissions.length === 0 ) {\n\t\tpermissions.push( '*' );\n\t}\n\n\tpage = new Page( {\n\t\ttitle: title.getPrefixedText(),\n\t\ttitleObj: title,\n\t\trelevantTitle: relevantTitle.getPrefixedText(),\n\t\tprotection: {\n\t\t\tedit: permissions\n\t\t},\n\t\trevId: mw.config.get( 'wgRevisionId' ),\n\t\tisMainPage: mw.config.get( 'wgIsMainPage' ),\n\t\tisWatched: $( '#ca-watch' ).hasClass( 'watched' ),\n\t\tisMissing: mw.config.get( 'wgArticleId' ) === 0,\n\t\tid: mw.config.get( 'wgArticleId' ),\n\t\tnamespaceNumber: mw.config.get( 'wgNamespaceNumber' )\n\t} );\n\n\treturn page;\n}\n\nmodule.exports = loadCurrentPage;\n",
"/* global $ */\nconst\n\tPageHTMLParser = require( './PageHTMLParser' );\n\nlet pageHTMLParser;\n\n/**\n * Constructs a page parser singleton specific to the current page to find common child elements\n * more easily.\n *\n * Because this depends on the presence of certain DOM elements, it\n * should only be called after the DOMContentLoaded event.\n *\n * @ignore\n * @return {module:mobile.startup/PageHTMLParser}\n */\nfunction loadCurrentPageHTMLParser() {\n\tif ( pageHTMLParser ) {\n\t\treturn pageHTMLParser;\n\t}\n\n\tpageHTMLParser = new PageHTMLParser( $( '#content #bodyContent' ) );\n\n\treturn pageHTMLParser;\n}\n\nmodule.exports = loadCurrentPageHTMLParser;\n",
"/**\n * T156186: Please make judicious use of this singleton whose purpose is to\n * allow disparate components the ability to subscribe to a set of events and\n * react to those events.\n *\n * Prefer to use a more localized event bus when possible.\n *\n * Only import this in files at the edges. For example:\n *\n * Good: initialization scripts responsible for initializing classes such as\n * mobile.init/init.js\n * Bad: ImageOverlay.js or in any other component.\n *\n * By doing this, and using dependency injection of the event bus in the\n * components themselves, it will make it easier to switch this event bus out\n * for something more localized later on in our refactoring efforts.\n */\nmodule.exports = new OO.EventEmitter();\n",
"const util = require( './util' ),\n\tactionParams = require( './actionParams.js' );\n\n/**\n * Extends the API query parameters to include those parameters required to also fetch Wikibase\n * descriptions and appropriately sized thumbnail images as well as those required to make a query.\n *\n * This function wraps `util.extend` with some Wikibase-specific configuration\n * variable management\n * but, like `util.extend`, is variadic and so can be used as a replacement for it in search\n * gateways, e.g.\n *\n * ```\n * var params = extendSearchParams(\n * 'search',\n * baseParams,\n * specializedParams,\n * moreSpecializedParams\n * );\n * ```\n *\n * @param {string} feature The name of the feature\n * @throws {Error} If `feature` isn't one that shows Wikidata descriptions. See the\n * `wgMFDisplayWikibaseDescriptions` configuration variable for detail\n * @return {Object}\n */\nmodule.exports = function extendSearchParams( feature ) {\n\tconst displayWikibaseDescriptions = mw.config.get( 'wgMFDisplayWikibaseDescriptions' ) || {\n\t\t\t// These must be defined, as these are all the features that this can be used on.\n\t\t\t// If not defined, all these features will see their API calls broken\n\t\t\tsearch: true,\n\t\t\twatchlist: true,\n\t\t\ttagline: false\n\t\t},\n\t\tscriptPath = mw.config.get( 'wgMFScriptPath' );\n\n\tif ( !Object.prototype.hasOwnProperty.call( displayWikibaseDescriptions, feature ) ) {\n\t\tthrow new Error( '\"' + feature + '\" isn\\'t a feature that shows Wikibase descriptions.' );\n\t}\n\n\t// Construct the arguments for a call to `util.extend`\n\t// such that if it were hand-written, then it\n\t// would look like the following:\n\t//\n\t// ```\n\t// var result = util.extend( {\n\t// prop: []\n\t// }, params, /* ..., */ mw.config.get( 'wgMFSearchAPIParams' ) );\n\t// ```\n\tconst args = Array.prototype.slice.call( arguments, 1 );\n\targs.unshift( {\n\t\tprop: []\n\t} );\n\targs.push( mw.config.get( 'wgMFSearchAPIParams' ) );\n\n\tconst result = util.extend.apply( {}, args );\n\tresult.prop = result.prop.concat( mw.config.get( 'wgMFQueryPropModules' ) );\n\n\tif ( displayWikibaseDescriptions[feature] ) {\n\t\tif ( result.prop.indexOf( 'description' ) === -1 ) {\n\t\t\tresult.prop.push( 'description' );\n\t\t}\n\t}\n\n\tif ( scriptPath ) {\n\t\t// A foreign api is being accessed! Enable anonymous CORS queries!\n\t\tresult.origin = '*';\n\t}\n\treturn actionParams( result );\n};\n",
"const util = require( './util' ),\n\tButton = require( './Button' ),\n\ticons = require( './icons' );\n\n/**\n * Creates a header\n *\n * @ignore\n * @param {string|View} headingOrView (HTML allowed)\n * @param {View[]} headerActions\n * @param {module:mobile.startup/View} [headerCancel] defaults to cancel button\n * @param {string} [additionalClassNames] (should be escaped)\n * @return {Element}\n */\nfunction makeHeader( headingOrView, headerActions, headerCancel, additionalClassNames ) {\n\tconst heading = typeof headingOrView === 'string' ? headingOrView : undefined,\n\t\ttemplateData = {\n\t\t\thasActions: headerActions && headerActions.length,\n\t\t\tisHidden: false,\n\t\t\theading\n\t\t},\n\t\thtml = util.template( `\n<div class=\"overlay-header header ${ additionalClassNames || '' } hideable\">\n\t<ul class=\"header-cancel\">\n\t\t<li></li>\n\t</ul>\n\t{{{heading}}}\n\t{{#hasActions}}\n\t<div class=\"header-action\"></div>\n\t{{/hasActions}}\n</div>\n\t\t` ).render( templateData );\n\theaderCancel = headerCancel || icons.cancel();\n\tconst $el = util.parseHTML( html );\n\t// Truncate any text inside in the overlay header.\n\t$el.find( 'h2 span' ).addClass( 'truncated-text' );\n\t$el.find( '.header-cancel li' ).append(\n\t\theaderCancel.$el\n\t);\n\tif ( heading === undefined ) {\n\t\theadingOrView.$el.insertAfter( $el.find( '.header-cancel' ) );\n\t}\n\tif ( headerActions && headerActions.length ) {\n\t\t$el.find( '.header-action' ).append(\n\t\t\theaderActions.map( ( component ) => component.$el )\n\t\t);\n\t}\n\treturn $el[0];\n}\n\n/**\n * Creates a header with a h2 heading, internal for use inside GrowthExperiments only.\n *\n * @function\n * @memberof module:mobile.startup\n * @param {string} heading (HTML allowed)\n * @param {module:mobile.startup/View[]} headerActions\n * @param {module:mobile.startup/View} [headerCancel] defaults to cancel button\n * @param {string} [additionalClassNames] (should be escaped)\n * @return {Element}\n */\nfunction header( heading, headerActions, headerCancel, additionalClassNames ) {\n\theading = `<div class=\"overlay-title\"><h2>${ heading }</h2></div>`;\n\treturn makeHeader( heading, headerActions, headerCancel, additionalClassNames );\n}\n\n/**\n * Creates a header with a form\n *\n * @ignore\n * @param {string|View} formHTMLOrView of the header\n * @param {View[]} headerActions\n * @param {module:mobile.startup/View} [headerCancel] defaults to cancel button\n * @param {string} [additionalClassNames] (should be escaped)\n * @return {Element}\n */\nfunction formHeader( formHTMLOrView, headerActions, headerCancel, additionalClassNames ) {\n\treturn makeHeader( formHTMLOrView, headerActions, headerCancel, additionalClassNames );\n}\n\n/**\n * Creates a header with a form\n *\n * @ignore\n * @param {string} heading of the header\n * @param {string} additionalClassNames of the header\n * @return {Element}\n */\nfunction saveHeader( heading, additionalClassNames ) {\n\treturn header(\n\t\theading,\n\t\t[\n\t\t\tnew Button( {\n\t\t\t\ttagName: 'button',\n\t\t\t\tadditionalClassNames: 'save submit',\n\t\t\t\tdisabled: true,\n\t\t\t\tlabel: util.saveButtonMessage(),\n\t\t\t\tsize: 'large'\n\t\t\t} )\n\t\t],\n\t\ticons.back(),\n\t\tadditionalClassNames\n\t);\n}\n/**\n * Creates a header with a form\n *\n * @ignore\n * @param {string} heading of the header\n * @param {string} additionalClassNames of the header\n * @return {Element}\n */\nfunction savingHeader( heading ) {\n\treturn header(\n\t\theading,\n\t\t[\n\t\t\ticons.spinner( {\n\t\t\t\tadditionalClassNames: 'savespinner loading'\n\t\t\t} )\n\t\t],\n\t\ticons.cancel(),\n\t\t'saving-header hidden'\n\t);\n}\n\nmodule.exports = {\n\tsavingHeader,\n\tsaveHeader,\n\tformHeader,\n\theader\n};\n",
"const\n\tCANCEL_GLYPH = 'close',\n\tIcon = require( './Icon' ),\n\tIconButton = require( './IconButton' );\n\n/**\n * A set of shared icons.\n *\n * Factory methods are used to keep separate features that use the same icons\n * from accidentally manipulating one another's DOM when calling methods like\n * `remove`.\n *\n * @class icons\n * @singleton\n * @uses Icon\n */\nconst icons = {\n\tCANCEL_GLYPH,\n\t// Exported to support testing and stubbing\n\tIcon,\n\tIconButton,\n\t/**\n\t * Gets a back icon\n\t *\n\t * The icon should be used to inform the user that the front-end is\n\t * communicating with the back-end.\n\t *\n\t * @memberof icons\n\t * @instance\n\t * @return {IconButton}\n\t */\n\tback() {\n\t\treturn new icons.IconButton( {\n\t\t\ttagName: 'button',\n\t\t\ticon: 'previous',\n\t\t\tadditionalClassNames: 'back',\n\t\t\tlabel: mw.msg( 'mobile-frontend-overlay-close' )\n\t\t} );\n\t},\n\t/**\n\t * Gets a cancel icon\n\t *\n\t * The icon should be used to inform the user that the front-end is\n\t * communicating with the back-end.\n\t *\n\t * @memberof icons\n\t * @instance\n\t * @param {string} [variant]\n\t * @param {Object} [props] to extend\n\t * @return {IconButton}\n\t */\n\tcancel( variant, props = {} ) {\n\t\tconst glyph = variant ? `${ CANCEL_GLYPH }-${ variant }` : `${ CANCEL_GLYPH }`;\n\t\tprops.additionalClassNames = props.additionalClassNames || '';\n\t\tprops.additionalClassNames += ' cancel';\n\n\t\treturn new icons.IconButton( Object.assign( {\n\t\t\ttagName: 'button',\n\t\t\ticon: glyph,\n\t\t\tlabel: mw.msg( 'mobile-frontend-overlay-close' )\n\t\t}, props ) );\n\t},\n\t/**\n\t * Gets a spinner icon. This uses IconButton but should never actually\n\t * be a button or have full button styles, as its purely presentational\n\t *\n\t * The icon should be used to inform the user that the front-end is\n\t * communicating with the back-end.\n\t *\n\t * @memberof icons\n\t * @instance\n\t * @param {Object} [props] See `Icon` for more details\n\t * @return {IconButton}\n\t */\n\tspinner( props = {} ) {\n\t\tif ( props.additionalClassNames === undefined ) {\n\t\t\tprops.additionalClassNames = 'spinner loading';\n\t\t}\n\n\t\tconst spinner = new icons.IconButton( Object.assign( {\n\t\t\ttagName: 'span',\n\t\t\ticon: 'spinner',\n\t\t\tlabel: mw.msg( 'mobile-frontend-loading-message' )\n\t\t}, props ) );\n\n\t\t// Update the element to not use button classes or attributes\n\t\tspinner.$el.removeClass();\n\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\tspinner.$el.addClass( props.additionalClassNames );\n\t\tspinner.$el.attr( 'type', '' );\n\t\tif ( spinner.options.isIconOnly ) {\n\t\t\tspinner.$el.addClass( 'mf-spinner-icon-element' );\n\t\t}\n\t\treturn spinner;\n\t},\n\t/**\n\t * Gets a failure (error) icon\n\t *\n\t * @memberof icons\n\t * @instance\n\t * @return {IconButton}\n\t */\n\terror() {\n\t\treturn new icons.IconButton( {\n\t\t\ticon: 'alert-invert',\n\t\t\tadditionalClassNames: 'load-fail-msg-icon'\n\t\t} );\n\t},\n\t/**\n\t * Gets a non-filled watch star icon.\n\t *\n\t * @memberof icons\n\t * @instance\n\t * @param {Object} props\n\t * @return {IconButton}\n\t */\n\twatch( props = {} ) {\n\t\tprops.additionalClassNames = props.additionalClassNames || '';\n\t\tprops.additionalClassNames += ' watch-this-article';\n\n\t\treturn new icons.IconButton( Object.assign( {\n\t\t\ticon: 'star-subtle'\n\t\t}, props ) );\n\t},\n\t/**\n\t * Gets a filled watch star icon.\n\t *\n\t * @memberof icons\n\t * @instance\n\t * @param {Object} props\n\t * @return {IconButton}\n\t */\n\twatched( props = {} ) {\n\t\tprops.additionalClassNames = props.additionalClassNames || '';\n\t\tprops.additionalClassNames += ' watch-this-article watched';\n\n\t\treturn new icons.IconButton( Object.assign( {\n\t\t\ticon: 'unStar-progressive'\n\t\t}, props ) );\n\t}\n};\n\nmodule.exports = icons;\n",
"const\n\tutil = require( '../util' ),\n\tplaceholderClass = 'lazy-image-placeholder';\n\n/**\n * @ignore\n * @param {HTMLElement} root\n * @return {HTMLElement[]}\n */\nfunction queryPlaceholders( root ) {\n\treturn Array.prototype.slice.call(\n\t\troot.getElementsByClassName( placeholderClass )\n\t);\n}\n\n/**\n * Load an image on demand\n *\n * @ignore\n * @param {HTMLElement[]} placeholders a list of images that have not been loaded.\n * @return {jQuery.Deferred}\n */\nfunction loadImages( placeholders ) {\n\treturn util.Promise.all(\n\t\tplaceholders.map( ( placeholder ) => module.exports.loadImage( placeholder ).promise )\n\t);\n}\n\n/**\n * Load an image on demand\n *\n * @ignore\n * @param {HTMLElement} placeholder\n * @return {{promise: jQuery.Deferred<'load'|'error'>, image: HTMLImageElement}}\n */\nfunction loadImage( placeholder ) {\n\tconst\n\t\tdeferred = util.Deferred(),\n\t\t// data-width and height are attributes and do not specify dimension.\n\t\twidth = placeholder.dataset.width,\n\t\theight = placeholder.dataset.height,\n\t\timage = new Image();\n\n\tif ( width ) {\n\t\timage.setAttribute( 'width', parseInt( width, 10 ) );\n\t}\n\tif ( height ) {\n\t\timage.setAttribute( 'height', parseInt( height, 10 ) );\n\t}\n\n\t// eslint-disable-next-line mediawiki/class-doc\n\timage.className = placeholder.dataset.class || '';\n\timage.alt = placeholder.dataset.alt || '';\n\timage.useMap = placeholder.dataset.usemap;\n\timage.style.cssText = placeholder.style.cssText || '';\n\n\t// When the image has loaded\n\timage.addEventListener( 'load', () => {\n\t\t// Swap the HTML inside the placeholder (to keep the layout and\n\t\t// dimensions the same and not trigger layouts\n\t\timage.classList.add( 'image-lazy-loaded' );\n\t\tif ( placeholder.parentNode ) {\n\t\t\tplaceholder.parentNode.replaceChild( image, placeholder );\n\t\t}\n\t\tdeferred.resolve( 'load' );\n\t}, { once: true } );\n\timage.addEventListener( 'error', () => {\n\t\t// Swap the HTML and let the browser decide what to do with the broken image.\n\t\tif ( placeholder.parentNode ) {\n\t\t\tplaceholder.parentNode.replaceChild( image, placeholder );\n\t\t}\n\t\t// Never reject. Quietly resolve so that Promise.all() awaits for all Deferreds to complete.\n\t\t// Reevaluate using Deferred.reject in T136693.\n\t\tdeferred.resolve( 'error' );\n\t}, { once: true } );\n\n\t// Trigger image download after binding the load handler\n\tconst src = placeholder.dataset.mwSrc || '';\n\tif ( src ) {\n\t\timage.src = src;\n\t\timage.srcset = placeholder.dataset.mwSrcset || '';\n\t}\n\n\treturn {\n\t\tpromise: deferred,\n\t\timage\n\t};\n}\n\nmodule.exports = {\n\tplaceholderClass,\n\tqueryPlaceholders,\n\tloadImages,\n\tloadImage,\n\ttest: {\n\t\tplaceholderClass\n\t}\n};\n",
"/**\n * Extends a class with new methods and member properties.\n *\n * @ignore\n * @param {Function} Child function\n * @param {Object|Function} ParentOrPrototype class to inherit from\n * OR if no inheriting class a prototype to extend the class with\n * @param {Object} [prototype]\n */\nmodule.exports = function mfExtend( Child, ParentOrPrototype, prototype ) {\n\tlet key;\n\tif ( prototype ) {\n\t\tOO.inheritClass( Child, ParentOrPrototype );\n\t} else {\n\t\tOO.initClass( Child );\n\t\tprototype = ParentOrPrototype;\n\t}\n\tfor ( key in prototype ) {\n\t\tChild.prototype[key] = prototype[key];\n\t}\n};\n",
"/**\n * Class for managing modules\n *\n * A module in this context is essentially a Javascript class (not to be confused with\n * ResourceLoader modules).\n *\n * @class ModuleLoader\n */\nfunction ModuleLoader() {\n\t/**\n\t * @property {Object} register of defined modules\n\t * @private\n\t */\n\tthis._register = {};\n}\n\nModuleLoader.prototype = {\n\t/**\n\t * Require (import) a module previously defined using define().\n\t * Searches core module registry using ResourceLoader require before consulting\n\t * its own local registry. This method is deprecated, please do not use.\n\t *\n\t * @memberof ModuleLoader\n\t * @instance\n\t * @param {string} id Required module id.\n\t * @return {Object} Required module, can be any JavaScript object.\n\t */\n\trequire( id ) {\n\t\tconst registry = this._register;\n\n\t\t/**\n\t\t * @return {Object} Module\n\t\t */\n\t\tfunction localRequire() {\n\t\t\tif ( !Object.hasOwnProperty.call( registry, id ) ) {\n\t\t\t\tthrow new Error( 'MobileFrontend Module not found: ' + id );\n\t\t\t}\n\t\t\treturn registry[id];\n\t\t}\n\n\t\tconst args = id.split( '/' );\n\t\ttry {\n\t\t\tconst module = __non_webpack_require__( args[0] );\n\t\t\tif ( module[args[1]] ) {\n\t\t\t\treturn module[args[1]];\n\t\t\t} else {\n\t\t\t\treturn localRequire();\n\t\t\t}\n\t\t} catch ( e ) {\n\t\t\treturn localRequire();\n\t\t}\n\t},\n\n\t/**\n\t * Define a module which can be later required (imported) using require().\n\t *\n\t * @memberof ModuleLoader\n\t * @instance\n\t * @param {string} id Defined module id.\n\t * @param {Object} obj Defined module body, can be any JavaScript object.\n\t * @return {Object}\n\t */\n\tdefine( id, obj ) {\n\t\tconst self = this;\n\n\t\tif ( Object.hasOwnProperty.call( this._register, id ) ) {\n\t\t\tthrow new Error( 'Module already exists: ' + id );\n\t\t}\n\t\tthis._register[id] = obj;\n\t\t// return an object of additionally functions to do with the registered module\n\t\treturn {\n\t\t\t/**\n\t\t\t * @see ModuleLoader#deprecate\n\t\t\t * @param {string} deprecatedId Defined module id, which is deprecated.\n\t\t\t * @ignore\n\t\t\t */\n\t\t\tdeprecate( deprecatedId ) {\n\t\t\t\tself.deprecate( deprecatedId, obj, id );\n\t\t\t}\n\t\t};\n\t},\n\n\t/**\n\t * Deprecate a module and give an replacement (if there is any).\n\t *\n\t * @memberof ModuleLoader\n\t * @instance\n\t * @param {string} id Defined module id, which is deprecated.\n\t * @param {Object} obj Defined module body, can be any JavaScript object.\n\t * @param {string} [replacement] Give an optional replacement for this module (which\n\t * needs to be already defined!)\n\t */\n\tdeprecate( id, obj, replacement ) {\n\t\tlet msg;\n\t\tif ( replacement ) {\n\t\t\t// add an alternative for this module, if any given\n\t\t\tmsg = 'Use ' + replacement + ' instead.';\n\t\t}\n\t\t// register it as a deprecated one\n\t\tmw.log.deprecate( this._register, id, obj, msg );\n\t}\n};\n\nmodule.exports = ModuleLoader;\n",
"const ModuleLoader = require( './moduleLoader' );\n\nmodule.exports = new ModuleLoader();\n",
"const Page = require( '../Page' );\nconst util = require( '../util' );\n\n/**\n * Create a Page object from an API response.\n *\n * @memberof Page\n * @param {Object} resp as representing a page in the API\n * @return {Page}\n */\nfunction parse( resp ) {\n\tlet displayTitle;\n\tconst thumb = resp.thumbnail,\n\t\tpageprops = resp.pageprops || {\n\t\t\tdisplaytitle: mw.html.escape( resp.title )\n\t\t},\n\t\tterms = resp.terms || resp.entityterms;\n\n\tif ( pageprops || terms ) {\n\t\t// The label is either the display title or the label pageprop\n\t\t// (the latter used by Wikidata)\n\t\t// Long term we want to consolidate these.\n\t\t// Note that pageprops.displaytitle is HTML, while\n\t\t// terms.label[0] is plain text.\n\t\tdisplayTitle = terms && terms.label ?\n\t\t\tmw.html.escape( terms.label[0] ) : pageprops.displaytitle;\n\t}\n\t// Add Wikidata descriptions if available (T101719)\n\tresp.wikidataDescription = resp.description || undefined;\n\n\tif ( thumb ) {\n\t\tresp.thumbnail.isLandscape = thumb.width > thumb.height;\n\t}\n\n\treturn new Page(\n\t\tutil.extend( resp, {\n\t\t\tid: resp.pageid,\n\t\t\tisMissing: !!resp.missing,\n\t\t\turl: mw.util.getUrl( resp.title ),\n\t\t\tdisplayTitle // this is HTML!\n\t\t} )\n\t);\n}\n\nmodule.exports = { parse };\n",
"/**\n * Creates a campaign that makes showing promo drawers, modals, etc that should\n * only be shown once (using localStorage) per action (when page loads, when\n * user clicks on a link, etc) easier. The campaign executes a given callback\n * (e.g. showing a drawer or modal) for a specific action only when it is\n * eligible. A campaign can have multiple arbitrary actions via the supplied\n * `actions` object. An action is either 'eligible' or 'ineligible' at any given\n * time. If `ineligible`, the `showIfEligible` will not execute the `onShow`\n * callback.\n *\n * @ignore\n * @param {Function} onShow A callback intended to show something related to the\n * campaign (drawer, modal, etc) when executed. The callback will only execute\n * after the client calls `showIfEligible` and only if the passed in action is\n * 'eligible'.\n * @param {Object} actions Object of arbitrary actions that are intended to be\n * either \"eligible\" or \"ineligible\" at any given time (onPageLoad,\n * onHistoryLinkClick). The `onShow` callback will only be executed when an\n * action is 'eligible'. For each action, the key and value should be the same\n * (e.g. \"onLoad\":\"onLoad\") to mimic an enum. The client is responsible for\n * notifying when each action becomes \"ineligible\" by calling the\n * `makeActionIneligible` method. All actions can be marked as ineligible by\n * calling the `makeAllActionsIneligible` method.\n * @param {string} campaignName Name of campaign. This is only used to form part\n * of the localStorage key for each action.\n * @param {boolean} campaignActive Is campaign active\n * @param {boolean} userEligible Is current user eligible\n * @param {mw.storage} mwStorage Used to mark actions as ineligible\n * into localStorage\n * @return {mobile.startup/AmcOutreach~PromoCampaign}\n */\nfunction createPromoCampaign(\n\tonShow,\n\tactions,\n\tcampaignName,\n\tcampaignActive,\n\tuserEligible,\n\tmwStorage\n) {\n\t// This object maps actions to localStorage keys\n\tconst ACTIONS_TO_STORAGE_KEYS = {};\n\tfor ( const key in actions ) {\n\t\tconst a = actions[ key ];\n\t\tACTIONS_TO_STORAGE_KEYS[a] = `mobile-frontend-${ campaignName }-ineligible-${ a }`;\n\t}\n\n\t/**\n\t * @return {boolean}\n\t */\n\tfunction isCampaignActive() {\n\t\treturn campaignActive;\n\t}\n\n\t/**\n\t * @param {string} action\n\t * @throws {Error} Throws an error if action is not valid.\n\t */\n\tfunction validateAction( action ) {\n\t\tif ( !( action in actions ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t`Action '${ action }' not found in 'actions' object. Please add this to\n\t\t\t\tthe object when creating a campaign with promoCampaign.js if you believe\n\t\t\t\tthis is a valid action.`\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * @param {string} action Will check the eligibility of this action. This\n\t * should be a value in the actions object.\n\t * @throws {Error} Throws an error if action is not valid.\n\t * @return {boolean}\n\t */\n\tfunction isActionEligible( action ) {\n\t\tvalidateAction( action );\n\n\t\treturn isCampaignActive() &&\n\t\t\tuserEligible &&\n\t\t\tmwStorage.get( ACTIONS_TO_STORAGE_KEYS[action] ) === null;\n\t}\n\n\treturn {\n\t\t/**\n\t\t * @ignore\n\t\t * @param {string} action Should be one of the values in the\n\t\t * actions param\n\t\t * @param {...*} [args] Args to pass to the onShow callback\n\t\t * @throws {Error} Throws an error if action is not valid.\n\t\t * @return {module:mobile.startup/Drawer|null} Returns Drawer if drawer is eligible to be shown and\n\t\t * null if not.\n\t\t */\n\t\tshowIfEligible( action, ...args ) {\n\t\t\tif ( !isActionEligible( action ) ) {\n\t\t\t\t// If not eligible, there is no sense in continuing.\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn onShow( action, ...args );\n\t\t},\n\t\t/**\n\t\t * @ignore\n\t\t * @param {string} action\n\t\t * @throws {Error} Throws an error if action is not valid.\n\t\t * @return {boolean} Whether the save operation was successful\n\t\t */\n\t\tmakeActionIneligible( action ) {\n\t\t\tvalidateAction( action );\n\n\t\t\t// The value here actually doesn't matter. The only thing that matters is\n\t\t\t// if this key exists in localStorage.\n\t\t\treturn mwStorage.set( ACTIONS_TO_STORAGE_KEYS[action], '~' );\n\t\t},\n\t\tmakeAllActionsIneligible() {\n\t\t\tlet key, action;\n\t\t\tfor ( key in actions ) {\n\t\t\t\taction = actions[key];\n\t\t\t\tthis.makeActionIneligible( action );\n\t\t\t}\n\t\t},\n\t\tisCampaignActive\n\t};\n}\n\nmodule.exports = createPromoCampaign;\n",
"const\n\tstorageKey = 'mobileFrontend/toast';\n\n/**\n * Show the previously saved toast data and delete it from storage\n *\n * @memberof Toast\n * @instance\n * @private\n */\nfunction showPending() {\n\tlet data = mw.storage.get( storageKey );\n\tif ( data ) {\n\t\tdata = JSON.parse( data );\n\t\tmw.notify( data.content, data.options );\n\t\tmw.storage.remove( storageKey );\n\t}\n}\n\nmw.requestIdleCallback( showPending );\n\n/**\n * Save the toast data in storage so that we can show it on page reload.\n * Also check whether there is a pending message that's not shown yet.\n * If yes, output a warning message and discard this message.\n * This is to ensure that the page needs to be reloaded before adding\n * a new message for showing later.\n *\n * @memberof Toast\n * @instance\n * @param {string} content Content to be placed in element\n * @param {Object|string} [options]\n * If a string (deprecated) CSS class to add to the element\n * If an object, more options for the notification see mw.notification.show.\n * For backwards compatibility reasons if a string is given it will be\n * treated as options.type\n */\nfunction showOnPageReload( content, options ) {\n\tif ( mw.storage.get( storageKey ) ) {\n\t\tmw.log.warn(\n\t\t\t'A pending toast message already exits. ' +\n\t\t\t\t'The page should have been reloaded by now.'\n\t\t);\n\t\treturn;\n\t}\n\tmw.storage.set( storageKey, JSON.stringify( {\n\t\tcontent,\n\t\toptions\n\t} ) );\n}\n\nmodule.exports = { showOnPageReload };\n",
"/* global $ */\n\n/**\n * Utility library\n *\n * @class util\n * @singleton\n */\nmodule.exports = {\n\t/**\n\t * Obtains the correct label for the save button which is project specific. It's either\n\t * \"save\" or \"publish\"\n\t *\n\t * @return {string}\n\t */\n\tsaveButtonMessage() {\n\t\treturn mw.config.get( 'wgEditSubmitButtonLabelPublish' ) ?\n\t\t\tmw.msg( 'mobile-frontend-editor-publish' ) : mw.msg( 'mobile-frontend-editor-save' );\n\t},\n\t/**\n\t * Wrapper class for Promises\n\t *\n\t * @memberof util\n\t * @instance\n\t */\n\tPromise: {\n\t\t/**\n\t\t * Wrapper class for the $.when that is compatible with Promise.all\n\t\t *\n\t\t * @memberof util\n\t\t * @param {jQuery.Promise[]} promises\n\t\t * @instance\n\t\t * @return {jQuery.Promise}\n\t\t */\n\t\tall( promises ) {\n\t\t\treturn $.when.apply( $, promises );\n\t\t}\n\t},\n\t/**\n\t * Escape a string for use as a css selector\n\t *\n\t * @memberof util\n\t * @instance\n\t * @param {string} selector\n\t * @return {string}\n\t */\n\tescapeSelector( selector ) {\n\t\treturn $.escapeSelector( selector );\n\t},\n\t/**\n\t * Run method when document is ready.\n\t *\n\t * @memberof util\n\t * @instance\n\t * @param {Function} fn\n\t * @return {jQuery.Object}\n\t */\n\tdocReady( fn ) {\n\t\treturn $( fn );\n\t},\n\t/**\n\t * Wrapper class for the Deferred method\n\t *\n\t * @memberof util\n\t * @instance\n\t * @return {jQuery.Deferred}\n\t */\n\tDeferred() {\n\t\treturn $.Deferred();\n\t},\n\t/**\n\t * Adds a class to the document\n\t *\n\t * @memberof util\n\t * @instance\n\t * @return {jQuery.Object} element representing the documentElement\n\t */\n\tgetDocument() {\n\t\treturn $( document.documentElement );\n\t},\n\t/**\n\t * Get the window object\n\t *\n\t * @memberof util\n\t * @instance\n\t * @return {jQuery.Object}\n\t */\n\tgetWindow() {\n\t\treturn $( window );\n\t},\n\t/**\n\t * Given some html, create new element(s).\n\t * Unlike jQuery.parseHTML this will return a jQuery object\n\t * not an array.\n\t *\n\t * @memberof util\n\t * @instance\n\t * @param {string} html\n\t * @param {Element} [ctx] Document element to serve as the context\n\t * in which the HTML fragment will be created\n\t * @return {jQuery.Object}\n\t */\n\tparseHTML( html, ctx ) {\n\t\tctx = ctx || document;\n\t\treturn $( $.parseHTML( html, ctx ) );\n\t},\n\t/**\n\t * Wrapper for jQuery.extend method. In future this can be bound to Object.assign\n\t * when support allows.\n\t *\n\t * Warning: if only one argument is supplied to util.extend(), this means the target argument\n\t * was omitted. In this case, the jQuery object itself is assumed to be the target.\n\t *\n\t * @memberof util\n\t * @instance\n\t * @return {Object}\n\t */\n\textend() {\n\t\treturn $.extend.apply( $, arguments );\n\t},\n\n\ttemplate( source ) {\n\t\treturn {\n\t\t\t/**\n\t\t\t * @ignore\n\t\t\t * @return {string} The raw source code of the template\n\t\t\t */\n\t\t\tgetSource() {\n\t\t\t\treturn source;\n\t\t\t},\n\t\t\t/**\n\t\t\t * @ignore\n\t\t\t * @param {Object} data Data to render\n\t\t\t * @param {Object} partials Map partial names to Mustache template objects\n\t\t\t * @return {string} Rendered HTML\n\t\t\t */\n\t\t\trender( data, partials ) {\n\t\t\t\tconst partialSource = {};\n\t\t\t\t// Map MobileFrontend templates to partial strings\n\t\t\t\tObject.keys( partials || {} ).forEach( ( key ) => {\n\t\t\t\t\tpartialSource[key] = partials[key].getSource();\n\t\t\t\t} );\n\n\t\t\t\t// Use global Mustache which is loaded by mediawiki.template.mustache (a\n\t\t\t\t// dependency of the mobile.startup module)\n\t\t\t\t// eslint-disable-next-line no-undef\n\t\t\t\treturn Mustache.render(\n\t\t\t\t\tsource.trim(),\n\t\t\t\t\tdata,\n\t\t\t\t\tpartialSource\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\t}\n};\n",
"const util = require( '../util' ),\n\tactionParams = require( '../actionParams' );\n\n/**\n * @ignore\n * @typedef {string|number} PageID Page ID. 0 / \"0\" is a special no-ID value.\n * {@link https://www.mediawiki.org/wiki/Manual:Page_table#page_id Page ID}\n *\n * @ignore\n * @typedef {string} PageTitle Canonical page title.\n * {@link https://www.mediawiki.org/wiki/Manual:Title.php#Canonical_forms Canonical forms}\n *\n * @ignore\n * @typedef {boolean} WatchStatus Page watch status; true if watched, false if\n * unwatched.\n * {@link https://www.mediawiki.org/wiki/API:Info API:Info} (see inprop.watched)\n * {@link https://www.mediawiki.org/wiki/API:Watch API:Watch} (see unwatch)\n *\n * @ignore\n * @typedef {Object.<PageTitle, WatchStatus>} WatchStatusMap\n */\n\n/**\n * API for retrieving and modifying page watch statuses. This module interacts\n * with two endpoints, API:Info for GETs and API:Watch and for POSTs.\n *\n * @class WatchstarGateway\n * @param {mw.Api} api\n * @ignore\n */\nfunction WatchstarGateway( api ) {\n\tthis.api = api;\n}\n\nWatchstarGateway.prototype = {\n\t/**\n\t * Issues zero to two asynchronous HTTP requests for the watch status of\n\t * each page ID and title passed.\n\t *\n\t * Every watch entry has a title but not necessarily a page ID. Entries\n\t * without IDs are missing pages, i.e., pages that do not exist. These\n\t * entries are used to observe when a page with a given title is created.\n\t * Although it is convenient to use titles because they're always present,\n\t * IDs are preferred since they're far less likely to exceed the URL length\n\t * limit.\n\t *\n\t * No request is issued when no IDs and no titles are passed. Given that the\n\t * server state does not change between the two requests, overlapping title\n\t * and ID members will behave as expected but there is no reason to issue\n\t * such a request.\n\t *\n\t * @memberof WatchstarGateway\n\t * @instance\n\t * @param {PageID[]} ids\n\t * @param {PageTitle[]} titles\n\t * @return {jQuery.Deferred<WatchStatusMap>}\n\t */\n\tgetStatuses( ids, titles ) {\n\t\t// Issue two requests and coalesce the results.\n\t\treturn util.Promise.all( [\n\t\t\tthis.getStatusesByID( ids ),\n\t\t\tthis.getStatusesByTitle( titles )\n\t\t] ).then( function () {\n\t\t\treturn util.extend.apply( util, arguments );\n\t\t} );\n\t},\n\n\t/**\n\t * @memberof WatchstarGateway\n\t * @instance\n\t * @param {PageID[]} ids\n\t * @return {jQuery.Deferred<WatchStatusMap>}\n\t */\n\tgetStatusesByID( ids ) {\n\t\tconst self = this;\n\t\tif ( !ids.length ) {\n\t\t\treturn util.Deferred().resolve( {} );\n\t\t}\n\n\t\treturn this.api.get( {\n\t\t\tformatversion: 2,\n\t\t\taction: 'query',\n\t\t\tprop: 'info',\n\t\t\tinprop: 'watched',\n\t\t\tpageids: ids\n\t\t} ).then( ( rsp ) => self._unmarshalGetResponse( rsp ) );\n\t},\n\n\t/**\n\t * @memberof WatchstarGateway\n\t * @instance\n\t * @param {PageTitle[]} titles\n\t * @return {jQuery.Deferred<WatchStatusMap>}\n\t */\n\tgetStatusesByTitle( titles ) {\n\t\tconst self = this;\n\t\tif ( !titles.length ) {\n\t\t\treturn util.Deferred().resolve( {} );\n\t\t}\n\n\t\treturn this.api.get( actionParams( {\n\t\t\tprop: 'info',\n\t\t\tinprop: 'watched',\n\t\t\ttitles\n\t\t} ) ).then( ( rsp ) => self._unmarshalGetResponse( rsp ) );\n\t},\n\n\t/**\n\t * @memberof WatchstarGateway\n\t * @instance\n\t * @param {PageTitle[]} titles\n\t * @param {WatchStatus} watched\n\t * @return {jQuery.Deferred}\n\t */\n\tpostStatusesByTitle( titles, watched ) {\n\t\tconst params = {\n\t\t\taction: 'watch',\n\t\t\ttitles\n\t\t};\n\t\tif ( !watched ) {\n\t\t\tparams.unwatch = !watched;\n\t\t}\n\t\treturn this.api.postWithToken( 'watch', params );\n\t},\n\n\t/**\n\t * @memberof WatchstarGateway\n\t * @instance\n\t * @param {Object} rsp The API:Info response.\n\t * @return {jQuery.Deferred<WatchStatusMap>}\n\t * @see getStatusesByID\n\t * @see getStatusesByTitle\n\t * @ignore\n\t */\n\t_unmarshalGetResponse( rsp ) {\n\t\tconst pages = rsp && rsp.query && rsp.query.pages || [];\n\t\treturn pages.reduce( ( statuses, page ) => {\n\t\t\tstatuses[page.title] = page.watched;\n\t\t\treturn statuses;\n\t\t}, {} );\n\t}\n};\n\nmodule.exports = WatchstarGateway;\n",
"const PageList = require( '../PageList' ),\n\twatchstar = require( './watchstar' ),\n\tuser = mw.user,\n\tutil = require( '../util' ),\n\tPage = require( '../Page' ),\n\tmfExtend = require( '../mfExtend' ),\n\tWatchstarGateway = require( './WatchstarGateway' );\n\n/**\n * @typedef {Object.<PageTitle, PageID>} PageTitleToPageIDMap\n * @ignore\n */\n\n/**\n * List of items page view\n *\n * @class WatchstarPageList\n * @uses Page\n * @uses WatchstarGateway\n * @uses Watchstar\n * @extends PageList\n * @ignore\n *\n * @fires WatchstarPageList#unwatch\n * @fires WatchstarPageList#watch\n * @param {Object} options Configuration options\n */\nfunction WatchstarPageList( options ) {\n\tthis.wsGateway = new WatchstarGateway( options.api );\n\tPageList.apply( this, arguments );\n}\n\nmfExtend( WatchstarPageList, PageList, {\n\t/**\n\t * @memberof WatchstarPageList\n\t * @instance\n\t * @mixes PageList#defaults\n\t * @property {Object} defaults Default options hash.\n\t * @property {mw.Api} defaults.api\n\t */\n\tpostRender() {\n\t\tconst\n\t\t\tself = this,\n\t\t\tids = [],\n\t\t\ttitles = [];\n\n\t\tPageList.prototype.postRender.apply( this );\n\n\t\tconst $items = this.queryUnitializedItems();\n\t\tconst pages = this.parsePagesFromItems( $items );\n\n\t\tObject.keys( pages ).forEach( ( title ) => {\n\t\t\tconst id = pages[title];\n\t\t\t// Favor IDs since they're short and unlikely to exceed URL length\n\t\t\t// limits when batched.\n\t\t\tif ( id && id !== '0' ) {\n\t\t\t\t// ID is present and valid.\n\t\t\t\tids.push( id );\n\t\t\t} else {\n\t\t\t\t// Only titles are available for missing pages.\n\t\t\t\ttitles.push( title );\n\t\t\t}\n\t\t} );\n\n\t\treturn this.getPages( ids, titles )\n\t\t\t.then( ( statuses ) => self.renderItems( $items, statuses ) );\n\t},\n\n\t/**\n\t * @memberof WatchstarPageList\n\t * @param {jQuery.Element} $items\n\t * @param {WatchStatusMap} statuses\n\t * @ignore\n\t */\n\tqueryUnitializedItems() {\n\t\treturn this.$el.find( 'li:not(.with-watchstar)' );\n\t},\n\n\t/**\n\t * Retrieve pages\n\t *\n\t * @memberof WatchstarPageList\n\t * @instance\n\t * @param {PageID[]} ids\n\t * @param {PageTitle[]} titles\n\t * @return {jQuery.Deferred<WatchStatusMap>}\n\t * @ignore\n\t */\n\tgetPages( ids, titles ) {\n\t\t// Rendering Watchstars for anonymous users is not useful. Short-circuit\n\t\t// the request.\n\t\tif ( user.isAnon() ) {\n\t\t\treturn util.Deferred().resolve( {} );\n\t\t}\n\n\t\treturn this.wsGateway.getStatuses( ids, titles );\n\t},\n\n\t/**\n\t * @param {jQuery.Element} $items\n\t * @return {PageTitleToPageIDMap}\n\t * @memberof WatchstarPageList\n\t * @ignore\n\t */\n\tparsePagesFromItems( $items ) {\n\t\tconst\n\t\t\tself = this,\n\t\t\tpages = {};\n\t\t$items.each( ( _, item ) => {\n\t\t\tconst $item = self.$el.find( item );\n\t\t\tpages[$item.attr( 'title' )] = $item.data( 'id' );\n\t\t} );\n\t\treturn pages;\n\t},\n\n\t/**\n\t * @param {jQuery.Element} $items\n\t * @param {WatchStatusMap} statuses\n\t * @memberof WatchstarPageList\n\t * @ignore\n\t */\n\trenderItems( $items, statuses ) {\n\t\tconst self = this;\n\n\t\t// Rendering Watchstars for anonymous users is not useful. Nothing to do.\n\t\tif ( user.isAnon() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Create watch stars for each entry in list\n\t\t$items.each( ( _, item ) => {\n\t\t\tconst\n\t\t\t\t$item = self.$el.find( item ),\n\t\t\t\tpage = new Page( {\n\t\t\t\t\t// FIXME: Set sections so we don't hit the api (hacky)\n\t\t\t\t\tsections: [],\n\t\t\t\t\ttitle: $item.attr( 'title' ),\n\t\t\t\t\tid: $item.data( 'id' )\n\t\t\t\t} ),\n\t\t\t\twatched = statuses[page.getTitle()];\n\n\t\t\tself._appendWatchstar( $item, page, watched );\n\t\t\t$item.addClass( 'with-watchstar' );\n\t\t} );\n\t},\n\n\t/**\n\t * @param {jQuery.Object} $item\n\t * @param {Page} page\n\t * @param {WatchStatus} watched\n\t * @private\n\t */\n\t_appendWatchstar( $item, page, watched ) {\n\t\twatchstar( {\n\t\t\t// WatchstarPageList.getPages() already retrieved the status of\n\t\t\t// each page. Explicitly set the watch state so another request\n\t\t\t// will not be issued by the Watchstar.\n\t\t\tisWatched: watched,\n\t\t\tpage\n\t\t} ).appendTo( $item );\n\t}\n} );\n\nmodule.exports = WatchstarPageList;\n",