All files / mobile.startup Overlay.js

92.85% Statements 52/56
70% Branches 7/10
84.21% Functions 16/19
92.85% Lines 52/56

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275  1x 1x 1x 1x 1x                                                             18x                                             36x                           23x       15x       10x                     1x                     6x                 23x 23x 23x     23x     23x           23x                     5x 2x   5x 5x 5x 4x     1x                       5x   5x   5x   5x   5x       5x 5x 5x                           5x   5x         5x 5x 5x               5x   5x                             6x 6x                           1x 2x 2x 2x                     5x 5x                                     1x 1x   1x 1x  
const
	View = require( './View' ),
	header = require( './headers' ).header,
	Anchor = require( './Anchor' ),
	util = require( './util' ),
	browser = require( './Browser' ).getSingleton();
 
/**
 * Mobile modal window
 *
 * @uses Icon
 * @uses Button
 * @fires Overlay#hide
 */
class Overlay extends View {
	/**
	 * @param {Object} props
	 * @param {Object} props.events - custom events to be bound to the overlay.
	 * @param {boolean} [props.headerChrome] Whether the header has chrome.
	 * @param {View[]} [props.headerActions] children (usually buttons or icons)
	 *   that should be placed in the header actions. Ignored when `headers` used.
	 * @param {string} [props.heading] heading for the overlay header. Use `headers` where
	 *  overlays require more than one header. Ignored when `headers` used.
	 * @param {boolean} props.noHeader renders an overlay without a header
	 * @param {Element[]} [props.headers] allows overlays to have more than one
	 *  header. When used it is an array of jQuery Objects representing
	 *  headers created via the header util function. It is expected that only one of these
	 *  should be visible. If undefined, headerActions and heading is used.
	 * @param {Object} [props.footerAnchor] options for an optional Anchor
	 *  that can appear in the footer
	 * @param {Function} props.onBeforeExit allows a consumer to prevent exits in certain
	 *  situations. This callback gets the following parameters:
	 *  - 1) the exit function which should be run after the consumer has made their changes.
	 *  - 2) the cancel function which should be run if the consumer explicitly changes their mind
	 */
	constructor( props ) {
		super(
			util.extend(
				true,
				{
					headerChrome: false,
					className: 'overlay'
				},
				props,
				{
					events: util.extend(
						{
							// FIXME: Remove .initial-header selector
							'click .cancel, .confirm, .initial-header .back': 'onExitClick',
							click: ( ev ) => ev.stopPropagation()
						},
						props.events
					)
				}
			)
		);
	}
 
	get template() {
		return util.template( `
{{^noHeader}}
<div class="overlay-header-container header-container{{#headerChrome}}
	header-chrome{{/headerChrome}} position-fixed">
</div>
{{/noHeader}}
<div class="overlay-content">
	{{>content}}
</div>
<div class="overlay-footer-container position-fixed"></div>
	` );
	}
 
	get isIos() {
		return browser.isIos();
	}
 
	set hideTimeout( timeout ) {
		this._hideTimeout = timeout;
	}
 
	get hideTimeout() {
		return this._hideTimeout;
	}
 
	/**
	 * Shows the spinner right to the input field.
	 *
	 * @memberof module:mobile.startup/Overlay
	 * @instance
	 * @method
	 */
	showSpinner() {
		this.$el.find( '.spinner' ).removeClass( 'hidden' );
	}
 
	/**
	 * Hide the spinner near to the input field.
	 *
	 * @memberof module:mobile.startup/Overlay
	 * @instance
	 * @method
	 */
	hideSpinner() {
		this.$el.find( '.spinner' ).addClass( 'hidden' );
	}
 
	/**
	 * @inheritdoc
	 * @memberof module:mobile.startup/Overlay
	 * @instance
	 */
	postRender() {
		const footerAnchor = this.options.footerAnchor;
		this.$overlayContent = this.$el.find( '.overlay-content' );
		Iif ( this.isIos ) {
			this.$el.addClass( 'overlay-ios' );
		}
		Iif ( footerAnchor ) {
			this.$el.find( '.overlay-footer-container' ).append( new Anchor( footerAnchor ).$el );
		}
		const headers = this.options.headers || [
			header(
				this.options.heading,
				this.options.headerActions
			)
		];
		this.$el.find( '.overlay-header-container' ).append( headers );
	}
 
	/**
	 * ClickBack event handler
	 *
	 * @memberof module:mobile.startup/Overlay
	 * @instance
	 * @param {Object} ev event object
	 */
	onExitClick( ev ) {
		const exit = () => {
			this.hide();
		};
		ev.preventDefault();
		ev.stopPropagation();
		if ( this.options.onBeforeExit ) {
			this.options.onBeforeExit( exit, () => {
			} );
		} else {
			exit();
		}
 
	}
 
	/**
	 * Attach overlay to current view and show it.
	 *
	 * @memberof module:mobile.startup/Overlay
	 * @instance
	 */
	show() {
		const $html = util.getDocument();
 
		this.scrollTop = window.pageYOffset;
 
		$html.addClass( 'overlay-enabled' );
		// skip the URL bar if possible
		window.scrollTo( 0, 1 );
 
		this.$el.addClass( 'visible' );
 
		// If .hide() was called earlier, and it scheduled an asynchronous detach
		// but it hasn't happened yet, cancel it
		Eif ( this.hideTimeout !== null ) {
			clearTimeout( this.hideTimeout );
			this.hideTimeout = null;
		}
	}
 
	/**
	 * Detach the overlay from the current view
	 * Should not be overriden as soon to be deprecated.
	 *
	 * @memberof module:mobile.startup/Overlay
	 * @instance
	 * @final
	 * @return {boolean} Whether the overlay was successfully hidden or not
	 */
	hide() {
		util.getDocument().removeClass( 'overlay-enabled' );
		// return to last known scroll position
		window.scrollTo( window.pageXOffset, this.scrollTop );
 
		// Since the hash change event caused by emitting hide will be detected later
		// and to avoid the article being shown during a transition from one overlay to
		// another, we regretfully detach the element asynchronously.
		this.hideTimeout = setTimeout( () => {
			this.$el.detach();
			this.hideTimeout = null;
		}, 0 );
 
		/**
		 * Fired when the overlay is closed.
		 *
		 * @event Overlay#hide
		 */
		this.emit( 'hide' );
 
		return true;
	}
 
	/**
	 * Show elements that are selected by the className.
	 * Also hide .hideable elements
	 * Can't use jQuery's hide() and show() because show() sets display: block.
	 * And we want display: table for headers.
	 *
	 * @memberof module:mobile.startup/Overlay
	 * @instance
	 * @protected
	 * @param {string} className CSS selector to show
	 */
	showHidden( className ) {
		this.$el.find( '.hideable' ).addClass( 'hidden' );
		this.$el.find( className ).removeClass( 'hidden' );
	}
}
 
/**
 * Factory method for an overlay with a single child
 *
 * @memberof module:mobile.startup/Overlay
 * @instance
 * @protected
 * @param {Object} options
 * @param {module:mobile.startup/View} view
 * @return {module:mobile.startup/Overlay}
 */
Overlay.make = function ( options, view ) {
	const overlay = new Overlay( options );
	overlay.$el.find( '.overlay-content' ).append( view.$el );
	return overlay;
};
 
/**
 * ES5 compatible version of class for backwards compatibility
 *
 * @param {Object} props
 * @deprecated 1.44
 * @ignore
 */
function ClassES5( props ) {
	mw.log.warn( '[1.44] Extending Overlay class constructor is deprecated. Please use Overlay.make' );
	View.ClassES5.call( this, util.extend(
		true,
		{
			headerChrome: false,
			className: 'overlay'
		},
		props,
		{
			events: util.extend(
				{
					// FIXME: Remove .initial-header selector
					'click .cancel, .confirm, .initial-header .back': 'onExitClick',
					click: ( ev ) => ev.stopPropagation()
				},
				props.events
			)
		}
	) );
}
ClassES5.prototype = Overlay.prototype;
ClassES5.make = Overlay.make;
 
Overlay.ClassES5 = ClassES5;
module.exports = Overlay;