From: jhobs Date: Thu, 28 Jan 2016 17:49:38 +0000 (-0500) Subject: Upstream `isElementInViewport` from MobileFrontend X-Git-Tag: 1.31.0-rc.0~7742^2 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_aide%28?a=commitdiff_plain;h=3f4e016873da100a002d60e2de38ae5d167b4981;p=lhc%2Fweb%2Fwiklou.git Upstream `isElementInViewport` from MobileFrontend As a useful utility function, we've copied this method several times across multiple extensions, which is a pretty good sign it should actually live in core. Changes: * Add `mediawiki.viewport` module * Rewrite method to be more robust and accept any viewport * Add `mw.viewport` to jsduck categories file * Add method for checking if an element is close to the viewport * Add unit tests Bug: T124317 Change-Id: I38eec4f1e568f51e7e212b2b3f10b8da8d36f316 --- diff --git a/maintenance/jsduck/categories.json b/maintenance/jsduck/categories.json index 41b56f6ca5..d9e2c50e99 100644 --- a/maintenance/jsduck/categories.json +++ b/maintenance/jsduck/categories.json @@ -32,7 +32,8 @@ "mw.util", "mw.plugin.*", "mw.cookie", - "mw.experiments" + "mw.experiments", + "mw.viewport" ] }, { diff --git a/resources/Resources.php b/resources/Resources.php index 1179a9a78e..8b6b559e12 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1334,6 +1334,11 @@ return [ 'position' => 'top', // For $wgPreloadJavaScriptMwUtil 'targets' => [ 'desktop', 'mobile' ], ], + 'mediawiki.viewport' => [ + 'scripts' => 'resources/src/mediawiki/mediawiki.viewport.js', + 'position' => 'top', + 'targets' => [ 'desktop', 'mobile' ], + ], 'mediawiki.checkboxtoggle' => [ 'scripts' => 'resources/src/mediawiki/mediawiki.checkboxtoggle.js', 'position' => 'top', diff --git a/resources/src/mediawiki/mediawiki.viewport.js b/resources/src/mediawiki/mediawiki.viewport.js new file mode 100644 index 0000000000..aa9dd0581f --- /dev/null +++ b/resources/src/mediawiki/mediawiki.viewport.js @@ -0,0 +1,89 @@ +( function ( mw, $ ) { + 'use strict'; + + /** + * Utility library for viewport-related functions + * + * Notable references: + * - https://github.com/tuupola/jquery_lazyload + * - https://github.com/luis-almeida/unveil + * + * @class mw.viewport + * @singleton + */ + var viewport = { + + /** + * This is a private method pulled inside the module for testing purposes. + * + * @ignore + * @private + */ + makeViewportFromWindow: function () { + var $window = $( window ), + scrollTop = $window.scrollTop(), + scrollLeft = $window.scrollLeft(); + + return { + top: scrollTop, + left: scrollLeft, + right: scrollLeft + $window.width(), + bottom: ( window.innerHeight ? window.innerHeight : $window.height() ) + scrollTop + }; + }, + + /** + * Check if any part of a given element is in a given viewport + * + * @method + * @param {HTMLElement} el Element that's being tested + * @param {Object} [rectangle] Viewport to test against; structured as such: + * + * var rectangle = { + * top: topEdge, + * left: leftEdge, + * right: rightEdge, + * bottom: bottomEdge + * } + * Defaults to viewport made from `window`. + * + * @return {boolean} + */ + isElementInViewport: function ( el, rectangle ) { + var elRect = el.getBoundingClientRect(), + viewport = rectangle || this.makeViewportFromWindow(); + + return ( + ( viewport.bottom >= elRect.top ) && + ( viewport.right >= elRect.left ) && + ( viewport.top <= elRect.top + elRect.height ) && + ( viewport.left <= elRect.left + elRect.width ) + ); + }, + + /** + * Check if an element is a given threshold away in any direction from a given viewport + * + * @method + * @param {HTMLElement} el Element that's being tested + * @param {number} [threshold] Pixel distance considered "close". Must be a positive number. + * Defaults to 50. + * @param {Object} [rectangle] Viewport to test against. + * Defaults to viewport made from `window`. + * @return {boolean} + */ + isElementCloseToViewport: function ( el, threshold, rectangle ) { + var viewport = rectangle ? $.extend( {}, rectangle ) : this.makeViewportFromWindow(); + threshold = threshold || 50 ; + + viewport.top -= threshold; + viewport.left -= threshold; + viewport.right += threshold; + viewport.bottom += threshold; + return this.isElementInViewport( el, viewport ); + } + + }; + + mw.viewport = viewport; +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index a2dead66d7..310268f27b 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -80,6 +80,7 @@ return [ 'tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js', @@ -130,6 +131,7 @@ return [ 'mediawiki.template.mustache', 'mediawiki.template', 'mediawiki.util', + 'mediawiki.viewport', 'mediawiki.special.recentchanges', 'mediawiki.language', 'mediawiki.cldr', diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js new file mode 100644 index 0000000000..61391d8bc8 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js @@ -0,0 +1,89 @@ +( function ( mw, $ ) { + + // Simulate square element with 20px long edges placed at (20, 20) on the page + var + DEFAULT_VIEWPORT = { + top: 0, + left: 0, + right: 100, + bottom: 100 + }; + + QUnit.module( 'mediawiki.viewport', QUnit.newMwEnvironment( { + setup: function () { + this.el = $( '
' ) + .appendTo( '#qunit-fixture' ) + .width( 20 ) + .height( 20 ) + .offset( { + top: 20, + left: 20 + } ) + .get( 0 ); + this.sandbox.stub( mw.viewport, 'makeViewportFromWindow' ) + .returns( DEFAULT_VIEWPORT ); + } + } ) ); + + QUnit.test( 'isElementInViewport', 6, function ( assert ) { + var viewport = $.extend( {}, DEFAULT_VIEWPORT ); + assert.ok( mw.viewport.isElementInViewport( this.el, viewport ), + 'It should return true when the element is fully enclosed in the viewport' ); + + viewport.right = 20; + viewport.bottom = 20; + assert.ok( mw.viewport.isElementInViewport( this.el, viewport ), + 'It should return true when only the top-left of the element is within the viewport' ); + + viewport.top = 40; + viewport.left = 40; + viewport.right = 50; + viewport.bottom = 50; + assert.ok( mw.viewport.isElementInViewport( this.el, viewport ), + 'It should return true when only the bottom-right is within the viewport' ); + + viewport.top = 30; + viewport.left = 30; + viewport.right = 35; + viewport.bottom = 35; + assert.ok( mw.viewport.isElementInViewport( this.el, viewport ), + 'It should return true when the element encapsulates the viewport' ); + + viewport.top = 0; + viewport.left = 0; + viewport.right = 19; + viewport.bottom = 19; + assert.notOk( mw.viewport.isElementInViewport( this.el, viewport ), + 'It should return false when the element is not within the viewport' ); + + assert.ok( mw.viewport.isElementInViewport( this.el ), + 'It should default to the window object if no viewport is given' ); + } ); + + QUnit.test( 'isElementCloseToViewport', 3, function ( assert ) { + var + viewport = { + top: 90, + left: 90, + right: 100, + bottom: 100 + }, + distantElement = $( '
' ) + .appendTo( '#qunit-fixture' ) + .width( 20 ) + .height( 20 ) + .offset( { + top: 220, + left: 20 + } ) + .get( 0 ); + + assert.ok( mw.viewport.isElementCloseToViewport( this.el, 60, viewport ), + 'It should return true when the element is within the given threshold away' ); + assert.notOk( mw.viewport.isElementCloseToViewport( this.el, 20, viewport ), + 'It should return false when the element is further than the given threshold away' ); + assert.notOk( mw.viewport.isElementCloseToViewport( distantElement ), + 'It should default to a threshold of 50px and the window\'s viewport' ); + } ); + +}( mediaWiki, jQuery ) );