Upstream `isElementInViewport` from MobileFrontend
authorjhobs <jhobson@wikimedia.org>
Thu, 28 Jan 2016 17:49:38 +0000 (12:49 -0500)
committerjdlrobson <jdlrobson@gmail.com>
Wed, 2 Mar 2016 22:22:14 +0000 (14:22 -0800)
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

maintenance/jsduck/categories.json
resources/Resources.php
resources/src/mediawiki/mediawiki.viewport.js [new file with mode: 0644]
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js [new file with mode: 0644]

index 41b56f6..d9e2c50 100644 (file)
@@ -32,7 +32,8 @@
                                        "mw.util",
                                        "mw.plugin.*",
                                        "mw.cookie",
-                                       "mw.experiments"
+                                       "mw.experiments",
+                                       "mw.viewport"
                                ]
                        },
                        {
index 1179a9a..8b6b559 100644 (file)
@@ -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 (file)
index 0000000..aa9dd05
--- /dev/null
@@ -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 ) );
index a2dead6..310268f 100644 (file)
@@ -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 (file)
index 0000000..61391d8
--- /dev/null
@@ -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 = $( '<div />' )
+                               .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 = $( '<div />' )
+                               .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 ) );