jenkins-bot has submitted this change and it was merged. Change subject: Lazy load images (only JS, when configured) ......................................................................
Lazy load images (only JS, when configured) * When configured with wgMFLazyLoadImages apply lazy loading * Both on client and server * Images are rewritten to have a HTML only fallback and a placeholder for JS enabled clients * Images are loaded with JS when they are in viewport * On scroll or resize triggers the recalculation. * Guard against width and height not existing in the original image markup * Take into account disabled images to not do any of this * Section collapsing now triggers a resize (it resizes the height of the page) to help trigger adjustments in content (like lazy loading) Notes: * setTimeout applied to 100 magically to always trigger CSS transition :( * Only apply lazy loading to images after lead section in followup patch * https://phabricator.wikimedia.org/T124770#2011918 * Why this is happening in MobileFormatter instead of in parser hooks, and upstreaming plans: https://phabricator.wikimedia.org/T124770#2007602 Bug: T124770 Change-Id: I8b6e68a47f6ee081776bee28d824d51a183d7607 --- M extension.json M includes/MobileFormatter.php M includes/MobileFrontend.hooks.php M resources/mobile.startup/Skin.js M resources/mobile.toggle/toggle.js A resources/skins.minerva.base.styles/images.less 6 files changed, 187 insertions(+), 6 deletions(-) Approvals: Jdlrobson: Looks good to me, approved jenkins-bot: Verified diff --git a/extension.json b/extension.json index df345f1..c45ea91 100644 --- a/extension.json +++ b/extension.json @@ -128,7 +128,8 @@ "resources/skins.minerva.base.styles/pageactions.less", "resources/skins.minerva.base.styles/footer.less", "resources/skins.minerva.base.styles/common.less", - "resources/skins.minerva.base.styles/icons.less" + "resources/skins.minerva.base.styles/icons.less", + "resources/skins.minerva.base.styles/images.less" ] }, "skins.minerva.content.styles": { @@ -599,6 +600,7 @@ "resources/mobile.startup/panel.less" ], "scripts": [ + "resources/mobile.startup/util.js", "resources/mobile.startup/Router.js", "resources/mobile.startup/OverlayManager.js", "resources/mobile.startup/PageGateway.js", @@ -611,8 +613,7 @@ "resources/mobile.startup/Thumbnail.js", "resources/mobile.startup/Page.js", "resources/mobile.startup/Skin.js", - "resources/mobile.startup/Schema.js", - "resources/mobile.startup/util.js" + "resources/mobile.startup/Schema.js" ], "position": "bottom" }, diff --git a/includes/MobileFormatter.php b/includes/MobileFormatter.php index 3193aed..714f47f 100644 --- a/includes/MobileFormatter.php +++ b/includes/MobileFormatter.php @@ -118,11 +118,59 @@ if ( $this->removeMedia ) { $this->doRemoveImages(); + } else { + $mfLazyLoadImages = $ctx->getMFConfig() + ->get( 'MFLazyLoadImages' ); + + if ( + $mfLazyLoadImages['base'] || + ( $ctx->isBetaGroupMember() && $mfLazyLoadImages['beta'] ) + ) { + $this->doRewriteImagesForLazyLoading(); + } } + return parent::filterContent(); } /** + * Enables images to be loaded asynchronously + */ + private function doRewriteImagesForLazyLoading() { + $doc = $this->getDoc(); + + foreach ( $doc->getElementsByTagName( 'img' ) as $img ) { + $parent = $img->parentNode; + $width = $img->getAttribute( 'width' ); + $height = $img->getAttribute( 'height' ); + $dimensionsStyle = ( $width ? "width: {$width}px;" : '' ) . + ( $height ? "height: {$height}px;" : '' ); + + // HTML only clients + $noscript = $doc->createElement( 'noscript' ); + + // Loading status of image placeholder + $spinner = $doc->createElement( 'span' ); + $spinner->setAttribute( 'class', MobileUI::iconClass( + 'spinner', 'element', 'loading spinner' + ) ); + + // To be loaded image placeholder + $imgPlaceholder = $doc->createElement( 'span' ); + $imgPlaceholder->setAttribute( 'class', 'lazy-image-placeholder' ); + $imgPlaceholder->setAttribute( 'style', $dimensionsStyle ); + $imgPlaceholder->appendChild( $spinner ); + + // Set the placeholder where the original image was + $parent->replaceChild( $imgPlaceholder, $img ); + // Add the original image to the HTML only markup + $noscript->appendChild( $img ); + // Insert the HTML only markup before the placeholder + $parent->insertBefore( $noscript, $imgPlaceholder ); + } + } + + /** * Replaces images with [annotations from alt] */ private function doRemoveImages() { diff --git a/includes/MobileFrontend.hooks.php b/includes/MobileFrontend.hooks.php index 2bf8a1a..a16f499 100644 --- a/includes/MobileFrontend.hooks.php +++ b/includes/MobileFrontend.hooks.php @@ -433,7 +433,8 @@ 'wgMFEditorOptions' => $config->get( 'MFEditorOptions' ), 'wgMFLicense' => MobileFrontendSkinHooks::getLicense( 'editor' ), 'wgMFUploadLicenseLink' => $wgMFUploadLicense['link'], - 'wgMFSchemaEditSampleRate' => $config->get( 'MFSchemaEditSampleRate' ) + 'wgMFSchemaEditSampleRate' => $config->get( 'MFSchemaEditSampleRate' ), + 'wgMFLazyLoadImages' => $config->get( 'MFLazyLoadImages' ), ); if ( $context->shouldDisplayMobileView() ) { diff --git a/resources/mobile.startup/Skin.js b/resources/mobile.startup/Skin.js index c2a06d1..5117638 100644 --- a/resources/mobile.startup/Skin.js +++ b/resources/mobile.startup/Skin.js @@ -1,7 +1,9 @@ ( function ( M, $ ) { var browser = M.require( 'mobile.browser/browser' ), - View = M.require( 'mobile.view/View' ); + View = M.require( 'mobile.view/View' ), + util = M.require( 'mobile.startup/util' ), + context = M.require( 'mobile.context/context' ); /** * Representation of the current skin being rendered. @@ -12,7 +14,9 @@ * @uses Page */ function Skin( options ) { - var self = this; + var self = this, + isBeta = context.isBetaGroupMember(), + wgMFLazyLoadImages = mw.config.get( 'wgMFLazyLoadImages' ); this.page = options.page; this.name = options.name; @@ -41,6 +45,17 @@ M.on( 'resize', $.proxy( this, 'emit', '_resize' ) ); this.on( '_resize', loadWideScreenModules ); this.emit( '_resize' ); + + if ( + !mw.config.get( 'wgImagesDisabled' ) && ( + wgMFLazyLoadImages.base || + ( isBeta && wgMFLazyLoadImages.beta ) + ) + ) { + $( function () { + self.loadImages(); + } ); + } } OO.mfExtend( Skin, View, { @@ -157,6 +172,78 @@ }, /** + * Load images on demand + */ + loadImages: function () { + var self = this, + $imageLinks = this.$( '#content' ).find( '.image' ); + + /** + * Load remaining images in viewport + */ + function _loadImages() { + + $imageLinks = $imageLinks.filter( function ( index, link ) { + var $imageLink = $( link ); + + if ( + util.isElementInViewport( $imageLink ) && + $imageLink.find( '.spinner' ).is( ':visible' ) + ) { + self.loadImage( $imageLink ); + return false; + } + + return true; + } ); + + if ( !$imageLinks.length ) { + M.off( 'scroll', _loadImages ); + M.off( 'resize', _loadImages ); + M.off( 'section-toggled', _loadImages ); + } + + } + + M.on( 'scroll', _loadImages ); + M.on( 'resize', _loadImages ); + M.on( 'section-toggled', _loadImages ); + + _loadImages(); + }, + + /** + * Load an image on demand + * @param {jQuery.Object} $imageLink + */ + loadImage: function ( $imageLink ) { + var $noscript = $imageLink.find( 'noscript' ), + $placeholder = $imageLink.find( '.lazy-image-placeholder' ), + // Grab the image markup from the HTML only fallback + // Image will start downloading + $image = $( $.parseHTML( $noscript.text() ) ), + $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( $image ); + // Set the loaded class after insertion of the HTML to trigger the + // animations. + setTimeout( function () { + $placeholder.addClass( 'loaded' ); + }, 100 ); + } ); + + // Trigger image download after binding the load handler + $downloadingImage.attr( { + src: $image.attr( 'src' ), + srcset: $image.attr( 'srcset' ) + } ); + }, + + /** * Returns the appropriate license message including links/name to * terms of use (if any) and license page */ diff --git a/resources/mobile.toggle/toggle.js b/resources/mobile.toggle/toggle.js index 603c5f3..32aecf7 100644 --- a/resources/mobile.toggle/toggle.js +++ b/resources/mobile.toggle/toggle.js @@ -153,6 +153,8 @@ 'aria-expanded': !wasExpanded } ); + M.emit( 'section-toggled', wasExpanded, sectionId ); + if ( !browser.isWideScreen() ) { storeSectionToggleState( $heading, page ); } diff --git a/resources/skins.minerva.base.styles/images.less b/resources/skins.minerva.base.styles/images.less new file mode 100644 index 0000000..8c4a0bd --- /dev/null +++ b/resources/skins.minerva.base.styles/images.less @@ -0,0 +1,42 @@ +@import "minerva.variables.less"; + +@transitionDuration: 0.3s; + +.lazy-image-placeholder { + display: inline-block; + // The image placeholder for shouldn't show for no-js devices + .client-nojs & { + display: none; + } + + background-color: @colorGray14; + border: 1px solid @colorGray11; + border-radius: 2px; + + // Hide anything around the dimensions of the placeholder (avoids spinner + // overflowing on smaller images + overflow: hidden; + + // Center spinner vertically and horizontally + display: flex; + justify-content: center; + align-items: center; + + // When inserted, don't show the image because we want to animate it + img { + opacity: 0; + } + + // When the image has loaded transition background color and image opacity + // for a fade-in effect + &.loaded { + transition: background-color @transitionDuration ease-in; + background-color: transparent; + border: none; + + img { + opacity: 1; + transition: opacity @transitionDuration ease-in; + } + } +} -- To view, visit https://gerrit.wikimedia.org/r/268185 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I8b6e68a47f6ee081776bee28d824d51a183d7607 Gerrit-PatchSet: 19 Gerrit-Project: mediawiki/extensions/MobileFrontend Gerrit-Branch: master Gerrit-Owner: Jhernandez <[email protected]> Gerrit-Reviewer: Bmansurov <[email protected]> Gerrit-Reviewer: Jdlrobson <[email protected]> Gerrit-Reviewer: Jhernandez <[email protected]> Gerrit-Reviewer: Krinkle <[email protected]> Gerrit-Reviewer: Phuedx <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
