All files / mobile.startup Overlay.js

89.79% Statements 44/49
70% Branches 7/10
84.61% Functions 11/13
89.79% Lines 44/49

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  1x 1x 1x 1x 1x 1x                                                           23x   23x                                             1x                                             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                             6x 6x                           1x 2x 2x 2x     1x  
var
	View = require( './View' ),
	header = require( './headers' ).header,
	Anchor = require( './Anchor' ),
	util = require( './util' ),
	browser = require( './Browser' ).getSingleton(),
	mfExtend = require( './mfExtend' );
 
/**
 * Mobile modal window
 *
 * @class Overlay
 * @extends View
 * @uses Icon
 * @uses Button
 * @fires Overlay#hide
 * @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
 */
function Overlay( props ) {
	this.isIos = browser.isIos();
 
	View.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
				)
			}
		)
	);
}
 
mfExtend( Overlay, View, {
	template: 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>
	` ),
 
	hideTimeout: null,
 
	/**
	 * Shows the spinner right to the input field.
	 *
	 * @memberof Overlay
	 * @instance
	 * @method
	 */
	showSpinner() {
		this.$el.find( '.spinner' ).removeClass( 'hidden' );
	},
 
	/**
	 * Hide the spinner near to the input field.
	 *
	 * @memberof Overlay
	 * @instance
	 * @method
	 */
	hideSpinner() {
		this.$el.find( '.spinner' ).addClass( 'hidden' );
	},
 
	/**
	 * @inheritdoc
	 * @memberof 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 Overlay
	 * @instance
	 * @param {Object} ev event object
	 */
	onExitClick( ev ) {
		const exit = function () {
			this.hide();
		}.bind( this );
		ev.preventDefault();
		ev.stopPropagation();
		if ( this.options.onBeforeExit ) {
			this.options.onBeforeExit( exit, () => {
			} );
		} else {
			exit();
		}
 
	},
	/**
	 * Attach overlay to current view and show it.
	 *
	 * @memberof Overlay
	 * @instance
	 */
	show() {
		var $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
		Iif ( 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 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 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 Overlay
 * @instance
 * @protected
 * @param {Object} options
 * @param {View} view
 * @return {Overlay}
 */
Overlay.make = function ( options, view ) {
	var overlay = new Overlay( options );
	overlay.$el.find( '.overlay-content' ).append( view.$el );
	return overlay;
};
 
module.exports = Overlay;