%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/Skin.js |
var
browser = require( './Browser' ).getSingleton(),
View = require( './View' ),
util = require( './util' ),
Page = require( './Page' ),
Deferred = util.Deferred,
when = util.when,
icons = require( './icons' ),
viewport = mw.viewport,
spinner = icons.spinner(),
mfExtend = require( './mfExtend' ),
M = require( './moduleSingleton' );
/**
* 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)
*/
function getSectionId( $el ) {
var id,
hSelector = Page.HEADING_SELECTOR,
$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
* @fires Skin#click
* @fires Skin#references-loaded
* @fires Skin#changed
*
* @param {Object} options Configuration options
*/
function Skin( options ) {
var self = this;
this.page = options.page;
this.name = options.name;
if ( options.mainMenu ) {
this.mainMenu = options.mainMenu;
mw.log.warn( 'Skin: Use of mainMenu is deprecated.' );
}
View.call( this, options );
this.referencesGateway = options.referencesGateway;
if (
mw.config.get( 'wgMFLazyLoadImages' )
) {
util.docReady( function () {
self.setupImageLoading();
} );
}
if ( mw.config.get( 'wgMFLazyLoadReferences' ) ) {
M.on( 'before-section-toggled', this.lazyLoadReferences.bind( this ) );
}
}
mfExtend( Skin, View, {
/**
* Skin contains components that we do not control
* @inheritdoc
* @memberof Skin
* @instance
*/
isBorderBox: false,
/**
* @memberof Skin
* @instance
* @mixes View#defaults
* @property {Object} defaults Default options hash.
* @property {Page} defaults.page page the skin is currently rendering
* @property {ReferencesGateway} defaults.referencesGateway instance of references gateway
*/
defaults: {
page: undefined
},
/**
* @inheritdoc
* @memberof Skin
* @instance
*/
events: {},
/**
* @inheritdoc
* @memberof Skin
* @instance
*/
postRender: function () {
var $el = this.$el;
if ( browser.supportsAnimations() ) {
$el.addClass( 'animations' );
}
if ( browser.supportsTouchEvents() ) {
$el.addClass( 'touch-events' );
}
util.parseHTML( '<div class="transparent-shield cloaked-element">' )
.appendTo( $el.find( '#mw-mf-page-center' ) );
/**
* Fired when appearance of skin changes.
* @event Skin#changed
*/
this.emit( 'changed' );
/**
* Fired when the skin is clicked.
* @event Skin#click
*/
this.$( '#mw-mf-page-center' ).on( 'click', this.emit.bind( this, 'click' ) );
},
/**
* Get images that have not yet been loaded in the page
* @memberof Skin
* @instance
* @param {jQuery.Object} [$container] The container that should be
* searched for image placeholders. Defaults to "#content".
* @return {Array} of unloaded image placeholders in the page
*/
getUnloadedImages: function ( $container ) {
$container = $container || this.$( '#content' );
return $container.find( '.lazy-image-placeholder' ).toArray();
},
/**
* Setup listeners to watch unloaded images and load them into the page
* as and when they are needed.
* @memberof Skin
* @instance
* @param {jQuery.Object} [$container] The container that should be
* searched for image placeholders. Defaults to "#content".
* @return {jQuery.Deferred} which will be resolved when the attempts to
* load all images subject to loading have been completed.
*/
setupImageLoading: function ( $container ) {
var self = this,
offset = util.getWindow().height() * 1.5,
loadImagesList = this.loadImagesList.bind( this ),
imagePlaceholders = this.getUnloadedImages( $container );
/**
* Check whether an image should be loaded based on its proximity to the
* viewport; and whether it is displayed to the user.
* @param {jQuery.Object} $placeholder
* @return {boolean}
*/
function shouldLoadImage( $placeholder ) {
return viewport.isElementCloseToViewport( $placeholder[0], 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 );
}
/**
* Load remaining images in viewport
* @return {jQuery.Deferred}
*/
function _loadImages() {
var images = [];
// Filter unloaded images to only the images that still need to be loaded
imagePlaceholders = util.grep( imagePlaceholders, function ( placeholder ) {
var $placeholder = self.$( placeholder );
// Check length to ensure the image is still in the DOM.
if ( $placeholder.length && shouldLoadImage( $placeholder ) ) {
images.push( placeholder );
return false;
}
return true;
} );
// When no images are left unbind all events
if ( !imagePlaceholders.length ) {
M.off( 'scroll:throttled', _loadImages );
M.off( 'resize:throttled', _loadImages );
M.off( 'section-toggled', _loadImages );
self.off( 'changed', _loadImages );
}
// load any remaining images.
return loadImagesList( images );
}
M.on( 'scroll:throttled', _loadImages );
M.on( 'resize:throttled', _loadImages );
M.on( 'section-toggled', _loadImages );
this.on( 'changed', _loadImages );
return _loadImages();
},
/**
* Load an image on demand
* @memberof Skin
* @instance
* @param {Array} [images] a list of images that have not been loaded.
* If none given all will be loaded.
* @return {jQuery.Deferred}
*/
loadImagesList: function ( images ) {
var callbacks,
$ = this.$.bind( this ),
loadImage = this.loadImage.bind( this );
images = images || this.getUnloadedImages();
callbacks = images.map( function ( placeholder ) {
return loadImage( $( placeholder ) );
} );
return when.apply( null, callbacks );
},
/**
* Load an image on demand
* @memberof Skin
* @instance
* @param {jQuery.Object} $placeholder
* @return {jQuery.Deferred}
*/
loadImage: function ( $placeholder ) {
var
d = Deferred(),
width = $placeholder.attr( 'data-width' ),
height = $placeholder.attr( 'data-height' ),
// document must be passed to ensure image will start downloading
$downloadingImage = util.parseHTML( '<img>', this.$el[0].ownerDocument );
// 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
$downloadingImage.addClass( 'image-lazy-loaded' );
$placeholder.replaceWith( $downloadingImage );
d.resolve();
} );
$downloadingImage.on( 'error', function () {
d.reject();
} );
// 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' ),
style: $placeholder.attr( 'style' ),
srcset: $placeholder.attr( 'data-srcset' )
} );
return d;
},
/**
* Load the references section content from API if it's not already loaded.
*
* All references tags content will be loaded per section.
* @memberof Skin
* @instance
* @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.Promise} rejected when not a reference section.
*/
lazyLoadReferences: function ( data ) {
var $content, $spinner,
gateway = this.referencesGateway,
getUnloadedImages = this.getUnloadedImages.bind( this ),
loadImagesList = this.loadImagesList.bind( this ),
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();
function loadImagesAndSetData() {
// lazy load images if any
loadImagesList( getUnloadedImages( $content ) );
// Do not attempt further loading even if we're unable to load this time.
$content.data( 'are-references-loaded', 1 );
}
if ( !$content.data( 'are-references-loaded' ) ) {
$content.children().addClass( 'hidden' );
$spinner = spinner.$el.prependTo( $content );
// First ensure we retrieve all of the possible lists
return gateway.getReferencesLists( data.page )
.then( function () {
var lastId;
$content.find( '.mf-lazy-references-placeholder' ).each( function () {
var refListIndex = 0,
$placeholder = $content.find( 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 )
.then( 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' );
/**
* Fired when references list is loaded into the HTML
* @event references-loaded
*/
self.emit( 'references-loaded', self.page );
loadImagesAndSetData();
}, function () {
$spinner.remove();
// unhide on a failure
$content.children().removeClass( 'hidden' );
loadImagesAndSetData();
} );
} else {
return Deferred().reject().promise();
}
},
/**
* Returns the appropriate license message including links/name to
* terms of use (if any) and license page
* @memberof Skin
* @instance
* @return {string}
*/
getLicenseMsg: function () {
var licenseMsg,
mfLicense = mw.config.get( 'wgMFLicense' ),
licensePlural = mw.language.convertNumber( mfLicense.plural );
if ( mfLicense.link ) {
if ( this.$( '#footer-places-terms-use' ).length > 0 ) {
licenseMsg = mw.msg(
'mobile-frontend-editor-licensing-with-terms',
mw.message(
'mobile-frontend-editor-terms-link',
this.$( '#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;
module.exports = Skin;