%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/src/mobile.startup/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/MobileFrontend/src/mobile.startup/Overlay.js |
var
View = require( './View' ),
Icon = require( './Icon' ),
Button = require( './Button' ),
Anchor = require( './Anchor' ),
icons = require( './icons' ),
util = require( './util' ),
browser = require( './Browser' ).getSingleton(),
mfExtend = require( './mfExtend' );
/**
* Mobile modal window
* @class Overlay
* @extends View
* @uses Icon
* @uses Button
* @fires Overlay#Overlay-exit
* @fires Overlay#hide
*/
function Overlay() {
this.isIos = browser.isIos();
this.useVirtualKeyboardHack = browser.isIos( 4 ) || browser.isIos( 5 );
// Set to true when overlay has failed to load
this.hasLoadError = false;
View.apply( this, arguments );
}
mfExtend( Overlay, View, {
/**
* Identify whether the element contains position fixed elements
* @memberof Overlay
* @instance
* @property {boolean}
*/
hasFixedHeader: true,
/**
* Is overlay fullscreen
* @memberof Overlay
* @instance
* @property {boolean}
*/
fullScreen: true,
/**
* True if this.hide() should be invoked before firing the Overlay-exit
* event
* @memberof Overlay
* @instance
* @property {boolean}
*/
hideOnExitClick: true,
/**
* use '#mw-mf-viewport' rather than 'body' - for some reasons this has
* odd consequences on Opera Mobile (see bug 52361)
* @memberof Overlay
* @instance
* @property {string|jQuery.Object}
*/
appendToElement: '#mw-mf-viewport',
/**
* Default class name
* @memberof Overlay
* @instance
* @property {string}
*/
className: 'overlay',
templatePartials: {
header: mw.template.get( 'mobile.startup', 'header.hogan' ),
anchor: Anchor.prototype.template,
button: Button.prototype.template
},
template: mw.template.get( 'mobile.startup', 'Overlay.hogan' ),
/**
* @memberof Overlay
* @instance
* @mixes View#defaults
* @property {Object} defaults Default options hash.
* @property {string} defaults.saveMessage Caption for save button on edit form.
* @property {string} defaults.cancelButton HTML of the cancel button.
* @property {string} defaults.backButton HTML of the back button.
* @property {string} defaults.headerButtonsListClassName A comma separated string of class
* names of the wrapper of the header buttons.
* @property {boolean} defaults.headerChrome Whether the header has chrome.
* @property {boolean} defaults.fixedHeader Whether the header is fixed.
* @property {string} defaults.spinner HTML of the spinner icon.
* @property {Object} [defaults.footerAnchor] options for an optional Anchor
* that can appear in the footer
*/
defaults: {
saveMsg: mw.msg( 'mobile-frontend-editor-save' ),
cancelButton: new Icon( {
tagName: 'button',
name: 'overlay-close',
additionalClassNames: 'cancel',
label: mw.msg( 'mobile-frontend-overlay-close' )
} ).toHtmlString(),
backButton: new Icon( {
tagName: 'button',
name: 'back',
additionalClassNames: 'back',
label: mw.msg( 'mobile-frontend-overlay-close' )
} ).toHtmlString(),
headerButtonsListClassName: '',
headerChrome: false,
fixedHeader: true,
spinner: icons.spinner().toHtmlString()
},
/**
* @memberof Overlay
* @instance
*/
events: {
// FIXME: Remove .initial-header selector when bug 71203 resolved.
'click .cancel, .confirm, .initial-header .back': 'onExitClick',
click: 'stopPropagation'
},
/**
* Flag overlay to close on content tap
* @memberof Overlay
* @instance
* @property {boolean}
*/
closeOnContentTap: false,
/**
* Shows the spinner right to the input field.
* @memberof Overlay
* @instance
* @method
*/
showSpinner: function () {
this.$spinner.removeClass( 'hidden' );
},
/**
* Hide the spinner near to the input field.
* @memberof Overlay
* @instance
* @method
*/
hideSpinner: function () {
this.$spinner.addClass( 'hidden' );
},
/**
* @inheritdoc
* @memberof Overlay
* @instance
*/
postRender: function () {
this.$overlayContent = this.$( '.overlay-content' );
this.$spinner = this.$( '.spinner' );
if ( this.isIos ) {
this.$el.addClass( 'overlay-ios' );
}
// Truncate any text inside in the overlay header.
this.$( '.overlay-header h2 span' ).addClass( 'truncated-text' );
this.setupEmulatedIosOverlayScrolling();
},
/**
* Setups an emulated scroll behaviour for overlays in ios.
* @memberof Overlay
* @instance
*/
setupEmulatedIosOverlayScrolling: function () {
var self = this;
if ( this.isIos && this.hasFixedHeader ) {
this.$( '.overlay-content' ).on( 'touchstart', this.onTouchStart.bind( this ) )
.on( 'touchmove', this.onTouchMove.bind( this ) );
// wait for things to render before doing any calculations
setTimeout( function () {
self._fixIosHeader( self.$( 'textarea, input' ) );
}, 0 );
}
},
/**
* ClickBack event handler
* @memberof Overlay
* @instance
* @param {Object} ev event object
*/
onExitClick: function ( ev ) {
ev.preventDefault();
ev.stopPropagation();
if ( this.hideOnExitClick ) {
this.hide();
}
this.emit( Overlay.EVENT_EXIT );
},
/**
* Event handler for touchstart, for IOS
* @memberof Overlay
* @instance
* @param {Object} ev Event Object
*/
onTouchStart: function ( ev ) {
this.startY = ev.originalEvent.touches[0].pageY;
},
/**
* Event handler for touch move, for IOS
* @memberof Overlay
* @instance
* @param {Object} ev Event Object
*/
onTouchMove: function ( ev ) {
var
y = ev.originalEvent.touches[0].pageY,
contentOuterHeight = this.$overlayContent.outerHeight(),
contentLength = this.$overlayContent.prop( 'scrollHeight' ) - contentOuterHeight;
ev.stopPropagation();
// prevent scrolling and bouncing outside of .overlay-content
if (
( this.$overlayContent.scrollTop() === 0 && this.startY < y ) ||
( this.$overlayContent.scrollTop() === contentLength && this.startY > y )
) {
ev.preventDefault();
}
},
/**
* Stop clicks in the overlay from propagating to the page
* (prevents non-fullscreen overlays from being closed when they're tapped)
* @memberof Overlay
* @instance
* @param {Object} ev Event Object
*/
stopPropagation: function ( ev ) {
ev.stopPropagation();
},
/**
* Attach overlay to current view and show it.
* @memberof Overlay
* @instance
*/
show: function () {
var self = this,
$html = util.getDocument(),
$window = util.getWindow();
this.$el.appendTo( this.appendToElement );
this.scrollTop = window.pageYOffset;
if ( this.fullScreen ) {
$html.addClass( 'overlay-enabled' );
// skip the URL bar if possible
window.scrollTo( 0, 1 );
}
if ( this.closeOnContentTap ) {
$html.find( '#mw-mf-page-center' ).one( 'click', this.hide.bind( this ) );
}
// prevent scrolling and bouncing outside of .overlay-content
if ( this.isIos && this.hasFixedHeader ) {
$window
.on( 'touchmove.ios', function ( ev ) {
ev.preventDefault();
} )
.on( 'resize.ios', function () {
self._resizeContent( $window.height() );
} );
}
this.$el.addClass( 'visible' );
},
/**
* Detach the overlay from the current view
* @memberof Overlay
* @instance
* @param {boolean} [force] Whether the overlay should be closed regardless of
* state (see PhotoUploadProgress)
* @return {boolean} Whether the overlay was successfully hidden or not
*/
hide: function () {
var $window = util.getWindow(),
$html = util.getDocument();
if ( this.fullScreen ) {
$html.removeClass( 'overlay-enabled' );
// return to last known scroll position
window.scrollTo( window.pageXOffset, this.scrollTop );
}
this.$el.detach();
if ( this.isIos ) {
$window.off( '.ios' );
}
/**
* Fired when the overlay is closed.
* @event Overlay#hide
*/
this.emit( 'hide' );
return true;
},
/**
* Fit the overlay content height to the window taking overlay header and footer heights
* into consideration.
* @memberof Overlay
* @instance
* @private
* @param {number} windowHeight The height of the window
*/
_resizeContent: function ( windowHeight ) {
this.$overlayContent.height(
windowHeight -
this.$( '.overlay-header-container' ).outerHeight() -
this.$( '.overlay-footer-container' ).outerHeight()
);
},
/**
* Resize .overlay-content to occupy 100% of screen space when virtual
* keyboard is shown/hidden on iOS.
*
* This function supplements the custom styles for Overlays on iOS.
* On iOS we scroll the content inside of .overlay-content div instead
* of scrolling the whole page to achieve a consistent sticky header
* effect (position: fixed doesn't work on iOS when the virtual keyboard
* is open).
*
* @memberof Overlay
* @instance
* @private
* @param {jQuery.Object} $el for elements that may trigger virtual
* keyboard (usually inputs, textareas, contenteditables).
*/
_fixIosHeader: function ( $el ) {
var self = this,
$window = util.getWindow();
if ( this.isIos ) {
this._resizeContent( $window.height() );
$el
.on( 'focus', function () {
setTimeout( function () {
var keyboardHeight = 0;
// detect virtual keyboard height
if ( self.useVirtualKeyboardHack ) {
// this method does not work in iOS 8.02
$window.scrollTop( 999 );
keyboardHeight = $window.scrollTop();
$window.scrollTop( 0 );
}
if ( $window.height() > keyboardHeight ) {
self._resizeContent( $window.height() - keyboardHeight );
}
}, 0 );
} )
.on( 'blur', function () {
self._resizeContent( $window.height() );
// restore the fixed header in view.
$window.scrollTop( 0 );
} );
}
},
/**
* 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: function ( className ) {
this.$( '.hideable' ).addClass( 'hidden' );
this.$( className ).removeClass( 'hidden' );
}
} );
/*
* Fires when close button is clicked. Not to be confused with hide event.
* @memberof Overlay
* @event Overlay#Overlay-exit
*/
Overlay.EVENT_EXIT = 'Overlay-exit';
module.exports = Overlay;