| Current File : /home/jvzmxxx/wiki1/extensions/MobileFrontend/resources/mobile.startup/Skin.js |
( function ( M, $ ) {
var browser = M.require( 'mobile.startup/Browser' ).getSingleton(),
View = M.require( 'mobile.startup/View' ),
icons = M.require( 'mobile.startup/icons' );
/**
* Get the id of the section $el belongs to.
* @param {jQuery.Object} $el
* @return {string|null} either the anchor (id attribute of the section heading
* or null if none found)
* @ignore
*/
function getSectionId( $el ) {
var id,
hSelector = 'h1,h2,h3,h4,h5,h6',
$parent = $el.parent(),
// e.g. matches Subheading in
// <h2>H</h2><div><h3 id="subheading">Subh</h3><a class="element"></a></div>
$heading = $el.prevAll( hSelector ).eq( 0 );
if ( $heading.length ) {
id = $heading.find( '.mw-headline' ).attr( 'id' );
if ( id ) {
return id;
}
}
if ( $parent.length ) {
// if we couldnt find a sibling heading, check the sibling of the parents
// consider <div><h2 /><div><$el/></div></div>
return getSectionId( $parent );
} else {
return null;
}
}
/**
* Representation of the current skin being rendered.
*
* @class Skin
* @extends View
* @uses Browser
* @uses Page
*
* @constructor
* @param {Object} options Configuration options
*/
function Skin( options ) {
var self = this;
this.page = options.page;
this.name = options.name;
this.mainMenu = options.mainMenu;
View.call( this, options );
// Must be run after merging with defaults as must be defined.
this.tabletModules = options.tabletModules;
this.referencesGateway = options.referencesGateway;
/**
* Tests current window size and if suitable loads styles and scripts specific for larger devices
*
* @method
* @ignore
*/
function loadWideScreenModules() {
if ( browser.isWideScreen() ) {
// Adjust screen for tablets
if ( self.page.inNamespace( '' ) ) {
mw.loader.using( self.tabletModules ).always( function () {
self.off( '_resize' );
self.emit.call( self, 'changed' );
} );
}
}
}
M.on( 'resize', $.proxy( this, 'emit', '_resize' ) );
this.on( '_resize', loadWideScreenModules );
this.emit( '_resize' );
if (
!mw.config.get( 'wgImagesDisabled' ) &&
mw.config.get( 'wgMFLazyLoadImages' )
) {
$( function () {
self.loadImages();
} );
}
if ( mw.config.get( 'wgMFLazyLoadReferences' ) ) {
M.on( 'before-section-toggled', $.proxy( this.lazyLoadReferences, this ) );
}
}
OO.mfExtend( Skin, View, {
/**
* @inheritdoc
* Skin contains components that we do not control
*/
isBorderBox: false,
/**
* @inheritdoc
* @cfg {Object} defaults Default options hash.
* @cfg {Page} defaults.page page the skin is currently rendering
* @cfg {Array} defaults.tabletModules modules to load when in tablet
* @cfg {MainMenu} defaults.mainMenu instance of the mainMenu
* @cfg {ReferencesGateway} defaults.referencesGateway instance of references gateway
*/
defaults: {
page: undefined,
tabletModules: [],
mainMenu: undefined
},
/**
* @inheritdoc
*/
events: {},
/**
* Close navigation if content tapped
* @param {jQuery.Event} ev
* @private
*/
_onPageCenterClick: function ( ev ) {
var $target = $( ev.target );
// Make sure the menu is open and we are not clicking on the menu button
if (
this.mainMenu.isOpen() &&
!$target.hasClass( 'main-menu-button' )
) {
this.mainMenu.closeNavigationDrawers();
ev.preventDefault();
}
},
/**
* @inheritdoc
*/
postRender: function () {
var $el = this.$el;
if ( browser.supportsAnimations() ) {
$el.addClass( 'animations' );
}
if ( browser.supportsTouchEvents() ) {
$el.addClass( 'touch-events' );
}
$( '<div class="transparent-shield cloaked-element">' ).appendTo( '#mw-mf-page-center' );
/**
* @event changed
* Fired when appearance of skin changes.
*/
this.emit( 'changed' );
// FIXME: Move back into events when T98200 resolved
this.$( '#mw-mf-page-center' ).on( 'click',
$.proxy( this, '_onPageCenterClick' ) );
},
/**
* Return the instance of MainMenu
* @return {MainMenu}
*/
getMainMenu: function () {
return this.mainMenu;
},
/**
* Load images on demand
* @param {jQuery.Object} [$container] The container that should be
* searched for image placeholders. Defaults to "#content".
*/
loadImages: function ( $container ) {
var self = this,
offset = $( window ).height() * 1.5,
imagePlaceholders;
$container = $container || this.$( '#content' );
imagePlaceholders = $container.find( '.lazy-image-placeholder' ).toArray();
/**
* Load remaining images in viewport
*/
function _loadImages() {
imagePlaceholders = $.grep( imagePlaceholders, function ( placeholder ) {
var $placeholder = $( placeholder );
if (
mw.viewport.isElementCloseToViewport( placeholder, offset ) &&
// If a placeholder is an inline element without a height attribute set it will record as hidden
// to circumvent this we also need to test the height (see T143768).
( $placeholder.is( ':visible' ) || $placeholder.height() === 0 )
) {
self.loadImage( $placeholder );
return false;
}
return true;
} );
if ( !imagePlaceholders.length ) {
M.off( 'scroll:throttled', _loadImages );
M.off( 'resize:throttled', _loadImages );
M.off( 'section-toggled', _loadImages );
self.off( 'changed', _loadImages );
}
}
M.on( 'scroll:throttled', _loadImages );
M.on( 'resize:throttled', _loadImages );
M.on( 'section-toggled', _loadImages );
this.on( 'changed', _loadImages );
_loadImages();
},
/**
* Load an image on demand
* @param {jQuery.Object} $placeholder
*/
loadImage: function ( $placeholder ) {
var
width = $placeholder.attr( 'data-width' ),
height = $placeholder.attr( 'data-height' ),
// Image will start downloading
$downloadingImage = $( '<img/>' );
// When the image has loaded
$downloadingImage.on( 'load', function () {
// Swap the HTML inside the placeholder (to keep the layout and
// dimensions the same and not trigger layouts
$placeholder.empty().append( $downloadingImage );
// Set the loaded class after insertion of the HTML to trigger the
// animations.
$placeholder.addClass( 'loaded' );
} );
// Trigger image download after binding the load handler
$downloadingImage.attr( {
'class': $placeholder.attr( 'data-class' ),
width: width,
height: height,
src: $placeholder.attr( 'data-src' ),
alt: $placeholder.attr( 'data-alt' ),
srcset: $placeholder.attr( 'data-srcset' )
} );
},
/**
* Load the references section content from API if it's not already loaded.
*
* All references tags content will be loaded per section.
*
* @param {Object} data Information about the section. It's in the following form:
* {
* @property {string} page,
* @property {boolean} wasExpanded,
* @property {jQuery.Object} $heading,
* @property {boolean} isReferenceSection
* }
* @return {jQuery.Deferred} rejected when not a reference section.
*/
lazyLoadReferences: function ( data ) {
var $content, $spinner,
gateway = this.referencesGateway,
self = this;
// If the section was expanded before toggling, do not load anything as
// section is being collapsed now.
// Also return early if lazy loading is not required or the section is
// not a reference section
if (
data.wasExpanded ||
!data.isReferenceSection
) {
return;
}
$content = data.$heading.next();
if ( !$content.data( 'are-references-loaded' ) ) {
$content.children().addClass( 'hidden' );
$spinner = $( icons.spinner().toHtmlString() ).prependTo( $content );
// First ensure we retrieve all of the possible lists
return gateway.getReferencesLists( data.page )
.done( function () {
var lastId;
$content.find( '.mf-lazy-references-placeholder' ).each( function () {
var refListIndex = 0,
$placeholder = $( this ),
// search for id of the collapsible heading
id = getSectionId( $placeholder );
if ( lastId !== id ) {
// If the placeholder belongs to a new section reset index
refListIndex = 0;
lastId = id;
} else {
// otherwise increment it
refListIndex++;
}
if ( id ) {
gateway.getReferencesList( data.page, id ).done( function ( refListElements ) {
// Note if no section html is provided no substitution will happen so user is
// forced to rely on placeholder link.
if ( refListElements && refListElements[refListIndex] ) {
$placeholder.replaceWith( refListElements[refListIndex] );
}
} );
}
} );
// Show the section now the references lists have been placed.
$spinner.remove();
$content.children().removeClass( 'hidden' );
/**
* @event references-loaded
* Fired when references list is loaded into the HTML
*/
self.emit( 'references-loaded', self.page );
} )
.fail( function () {
$spinner.remove();
// unhide on a failure
$content.children().removeClass( 'hidden' );
} )
.always( function () {
// lazy load images if any
self.loadImages( $content );
// Do not attempt further loading even if we're unable to load this time.
$content.data( 'are-references-loaded', 1 );
} );
} else {
return $.Deferred().reject();
}
},
/**
* Returns the appropriate license message including links/name to
* terms of use (if any) and license page
* @return {string}
*/
getLicenseMsg: function () {
var licenseMsg,
mfLicense = mw.config.get( 'wgMFLicense' ),
licensePlural = mw.language.convertNumber( mfLicense.plural );
if ( mfLicense.link ) {
if ( $( '#footer-places-terms-use' ).length > 0 ) {
licenseMsg = mw.msg(
'mobile-frontend-editor-licensing-with-terms',
mw.message(
'mobile-frontend-editor-terms-link',
$( '#footer-places-terms-use a' ).attr( 'href' )
).parse(),
mfLicense.link,
licensePlural
);
} else {
licenseMsg = mw.msg(
'mobile-frontend-editor-licensing',
mfLicense.link,
licensePlural
);
}
}
return licenseMsg;
}
} );
Skin.getSectionId = getSectionId;
M.define( 'mobile.startup/Skin', Skin );
}( mw.mobileFrontend, jQuery ) );