%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/OverlayManager.js |
var
util = require( './util' ),
mfExtend = require( './mfExtend' );
/**
* Manages opening and closing overlays when the URL hash changes to one
* of the registered values (see OverlayManager.add()).
*
* This allows overlays to function like real pages, with similar browser back/forward
* and refresh behavior.
*
* @class OverlayManager
* @param {Router} router
*/
function OverlayManager( router ) {
router.on( 'route', this._checkRoute.bind( this ) );
this.router = router;
// use an object instead of an array for entries so that we don't
// duplicate entries that already exist
this.entries = {};
// stack of all the open overlays, stack[0] is the latest one
this.stack = [];
this.hideCurrent = true;
}
mfExtend( OverlayManager, {
/**
* Don't try to hide the active overlay on a route change event triggered
* by hiding another overlay.
* Called when hiding an overlay.
* @memberof OverlayManager
* @instance
* @private
*/
_onHideOverlay: function () {
this.hideCurrent = false;
this.router.back();
},
/**
* Show the overlay and bind the '_om_hide' event to _onHideOverlay.
* @memberof OverlayManager
* @instance
* @private
* @param {Overlay} overlay to show
*/
_showOverlay: function ( overlay ) {
// if hidden using overlay (not hardware) button, update the state
overlay.once( '_om_hide', this._onHideOverlay.bind( this ) );
overlay.show();
},
/**
* Hide overlay
* @memberof OverlayManager
* @instance
* @private
* @param {Overlay} overlay to hide
* @return {boolean} Whether the overlay has been hidden
*/
_hideOverlay: function ( overlay ) {
var result;
// remove the callback for updating state when overlay closed using
// overlay close button
overlay.off( '_om_hide' );
result = overlay.hide( this.stack.length > 1 );
// if closing prevented, reattach the callback
if ( !result ) {
overlay.once( '_om_hide', this._onHideOverlay.bind( this ) );
}
return result;
},
/**
* Show match's overlay if match is not null.
* @memberof OverlayManager
* @instance
* @private
* @param {Object|null} match Object with factory function's result. null if no match.
*/
_processMatch: function ( match ) {
var factoryResult,
self = this;
/**
* Attach an event to the overlays hide event
* @param {Overlay} overlay
*/
function attachHideEvent( overlay ) {
overlay.on( 'hide', function () {
overlay.emit( '_om_hide' );
} );
}
if ( match ) {
if ( match.overlay ) {
// if the match is an overlay that was previously opened, reuse it
self._showOverlay( match.overlay );
} else {
// else create an overlay using the factory function result (either
// a promise or an overlay)
factoryResult = match.factoryResult;
// http://stackoverflow.com/a/13075985/365238
if ( typeof factoryResult.promise === 'function' ) {
factoryResult.then( function ( overlay ) {
match.overlay = overlay;
attachHideEvent( overlay );
self._showOverlay( overlay );
} );
} else {
match.overlay = factoryResult;
attachHideEvent( match.overlay );
self._showOverlay( factoryResult );
}
}
}
},
/**
* A callback for Router's `route` event.
* @memberof OverlayManager
* @instance
* @private
* @param {jQuery.Event} ev Event object.
*/
_checkRoute: function ( ev ) {
var
current = this.stack[0],
match;
// When entering an overlay for the first time,
// the manager should remember the user's scroll position
// overlays always open at top of page
// and we'll want to restore it later.
// This should happen before the call to _matchRoute which will "show" the overlay.
// The Overlay has similar logic for overlays that are not managed via the overlay.
if ( !current ) {
this.scrollTop = window.pageYOffset;
}
match = Object.keys( this.entries ).reduce( function ( m, id ) {
return m || this._matchRoute( ev.path, this.entries[ id ] );
}.bind( this ), null );
// if there is an overlay in the stack and it's opened, try to close it
if (
current &&
current.overlay !== undefined &&
this.hideCurrent &&
!this._hideOverlay( current.overlay )
) {
// if hide prevented, prevent route change event
ev.preventDefault();
} else if ( !match ) {
// if hidden and no new matches, reset the stack
this.stack = [];
// restore the scroll position.
window.scrollTo( window.pageXOffset, this.scrollTop );
}
this.hideCurrent = true;
this._processMatch( match );
},
/**
* Check if a given path matches one of the entries.
* @memberof OverlayManager
* @instance
* @private
* @param {string} path Path (hash) to check.
* @param {Object} entry Entry object created in OverlayManager#add.
* @return {Object|null} Match object with factory function's result.
* Returns null if no match.
*/
_matchRoute: function ( path, entry ) {
var
next,
match = path.match( entry.route ),
previous = this.stack[1],
self = this;
/**
* Returns object to add to stack
* @method
* @ignore
* @return {Object}
*/
function getNext() {
return {
path: path,
factoryResult: entry.factory.apply( self, match.slice( 1 ) )
};
}
if ( match ) {
// if previous stacked overlay's path matches, assume we're going back
// and reuse a previously opened overlay
if ( previous && previous.path === path ) {
if ( previous.overlay && previous.overlay.hasLoadError ) {
self.stack.shift();
// Loading of overlay failed so we want to replace it with a new
// overlay (which will try to load successfully)
self.stack[0] = getNext();
return self.stack[0];
}
self.stack.shift();
return previous;
} else {
next = getNext();
if ( this.stack[0] && next.path === this.stack[0].path ) {
// current overlay path is same as path to check which means overlay
// is attempting to refresh so just replace current overlay with new
// overlay
self.stack[0] = next;
} else {
self.stack.unshift( next );
}
return next;
}
}
return null;
},
/**
* Add an overlay that should be shown for a specific fragment identifier.
*
* The following code will display an overlay whenever a user visits a URL that
* end with '#/hi/name'. The value of `name` will be passed to the overlay.
*
* @example
* overlayManager.add( /\/hi\/(.*)/, function ( name ) {
* var factoryResult = $.Deferred();
*
* mw.using( 'mobile.HiOverlay', function () {
* var HiOverlay = M.require( 'HiOverlay' );
* factoryResult.resolve( new HiOverlay( { name: name } ) );
* } );
*
* return factoryResult;
* } );
*
* @memberof OverlayManager
* @instance
* @param {RegExp} route route regular expression, optionally with parameters.
* @param {Function} factory a function returning an overlay or a $.Deferred
* which resolves to an overlay.
*/
add: function ( route, factory ) {
var self = this,
entry = {
route: route,
factory: factory
};
this.entries[route] = entry;
// Check if overlay should be shown for the current path.
// The DOM must fully load before we can show the overlay because Overlay relies on it.
util.docReady( function () {
self._processMatch( self._matchRoute( self.router.getPath(), entry ) );
} );
},
/**
* Replace the currently displayed overlay with a new overlay without changing the
* URL. This is useful for when you want to switch overlays, but don't want to
* change the back button or close box behavior.
*
* @memberof OverlayManager
* @instance
* @param {Object} overlay The overlay to display
*/
replaceCurrent: function ( overlay ) {
if ( this.stack.length === 0 ) {
throw new Error( 'Trying to replace OverlayManager\'s current overlay, but stack is empty' );
}
this._hideOverlay( this.stack[0].overlay );
this.stack[0].overlay = overlay;
this._showOverlay( overlay );
}
} );
module.exports = OverlayManager;