jenkins-bot has submitted this change and it was merged.

Change subject: (bug 51460) Introduction of $.wikibase.claimgrouplabelscroll
......................................................................


(bug 51460) Introduction of $.wikibase.claimgrouplabelscroll

Widget which will reposition labels of Claim groups while scrolling through the 
page. This
ensures that the labels are always displayed on the same line with the first 
Main Snak
visible within the viewport. When the label gets moved, the movement is 
animated for a smooth
transition.

Change-Id: I0639c32633342884921d7f639947ae8f419dd91d
---
M lib/WikibaseLib.hooks.php
M lib/resources/Resources.php
A lib/resources/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.js
A lib/tests/qunit/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.tests.js
M repo/resources/Resources.php
M repo/resources/wikibase.ui.entityViewInit.js
6 files changed, 416 insertions(+), 7 deletions(-)

Approvals:
  Henning Snater: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/lib/WikibaseLib.hooks.php b/lib/WikibaseLib.hooks.php
index 8370393..6d0e46c 100644
--- a/lib/WikibaseLib.hooks.php
+++ b/lib/WikibaseLib.hooks.php
@@ -50,6 +50,8 @@
         * Add new javascript testing modules. This is called after the 
addition of MediaWiki core test suites.
         * @see 
https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderTestModules
         *
+        * TODO: Move into a file with only this definition.
+        *
         * @since 0.2 (in repo as RepoHooks::onResourceLoaderTestModules in 0.1)
         *
         * @param array &$testModules
@@ -58,7 +60,13 @@
         * @return boolean
         */
        public static function registerQUnitTests( array &$testModules, 
\ResourceLoader &$resourceLoader ) {
-               $testModules['qunit']['wikibase.tests'] = array(
+               $moduleBase = array(
+                       'localBasePath' => __DIR__,
+                       'remoteExtPath' => 'Wikibase/lib',
+               );
+
+               // TODO: Split into test modules per QUnit module.
+               $testModules['qunit']['wikibase.tests'] = $moduleBase + array(
                        'scripts' => array(
                                'tests/qunit/templates.tests.js',
                                'tests/qunit/wikibase.tests.js',
@@ -136,9 +144,16 @@
                                'jquery.nativeEventHandler',
                                'jquery.client',
                                'jquery.eachchange',
+                       )
+               );
+
+               
$testModules['qunit']['jquery.wikibase.claimgrouplabelscroll.tests'] = 
$moduleBase + array(
+                       'scripts' => array(
+                               
'tests/qunit/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.tests.js',
                        ),
-                       'localBasePath' => __DIR__,
-                       'remoteExtPath' => 'Wikibase/lib',
+                       'dependencies' => array(
+                               'jquery.wikibase.claimgrouplabelscroll'
+                       ),
                );
 
                return true;
diff --git a/lib/resources/Resources.php b/lib/resources/Resources.php
index 4396a4d..e0a7f8c 100644
--- a/lib/resources/Resources.php
+++ b/lib/resources/Resources.php
@@ -689,6 +689,15 @@
                        )
                ),
 
+               'jquery.wikibase.claimgrouplabelscroll' => $moduleTemplate + 
array(
+                       'scripts' => array(
+                               
'jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.js'
+                       ),
+                       'dependencies' => array(
+                               'jquery.ui.widget',
+                       ),
+               ),
+
                // jQuery.valueview views for Wikibase specific 
DataValues/DataTypes
                'jquery.valueview.experts.wikibase' => $moduleTemplate + array(
                        'scripts' => array(
diff --git 
a/lib/resources/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.js
new file mode 100644
index 0000000..a48953f
--- /dev/null
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.js
@@ -0,0 +1,303 @@
+/**
+ * @file
+ * @ingroup WikibaseLib
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ */
+( function( $ ) {
+       'use strict';
+
+       var WIDGET_NAME = 'claimgrouplabelscroll';
+
+       /**
+        * For keeping track of currently active claimgrouplabelscroll widgets 
which need updates on
+        * certain browser window events.
+        *
+        * NOTE: In this performance critical case this makes more sense than 
jQuery's widget selector.
+        *
+        * @type {jQuery.wikibase.claimgrouplabelscroll[]}
+        */
+       var activeInstances = [];
+
+       function updateActiveInstances() {
+               for( var i in activeInstances ) {
+                       activeInstances[i].update();
+               }
+       }
+
+       function registerWidgetInstance( instance ) {
+               if( activeInstances.length === 0 ) {
+                       $( window ).on(
+                               'scroll resize'.replace( /(\w+)/g, '$1.' + 
WIDGET_NAME ),
+                               updateActiveInstances
+                       );
+               }
+               activeInstances.push( instance );
+       }
+
+       function unregisterWidgetInstance( instance ) {
+               var index = $.inArray( instance );
+               if( index ) {
+                       activeInstances.splice( index, 1 );
+               }
+               if( activeInstances.length === 0 ) {
+                       $( window ).off( '.' + WIDGET_NAME );
+               }
+       }
+
+       /**
+        * Name of the animation queue used for animations moving the claim 
group labels around.
+        * @type {string}
+        */
+       var ANIMATION_QUEUE = 'wikibase-' + WIDGET_NAME;
+
+       /**
+        * Counter for expensive checks done in an update. Used for debugging 
output.
+        * @type {number}
+        */
+       var expensiveChecks = 0;
+
+       /**
+        * Widget which will reposition labels of Claim groups while scrolling 
through the page. This
+        * ensures that the labels are always displayed on the same line with 
the first Main Snak
+        * visible within the viewport. When the label gets moved, the movement 
is animated for a smooth
+        * transition.
+        *
+        * TODO: Consider the rare case where window.scrollTo() is used. In 
that case we should move all
+        *  labels below the top of the new viewport position to the first 
claim and all labels above
+        *  the viewport position to the last claim in their group.
+        *
+        * @since 0.4
+        *
+        * @widget jQuery.wikibase.claimgrouplabelscroll
+        * @extends jQuery.Widget
+        */
+       $.widget( 'wikibase.' + WIDGET_NAME, {
+               /**
+                * @see jQuery.widget.options
+                * @type {Object}
+                */
+               options: {
+                       /**
+                        * If set, this object will be used for logging certain 
debug messages. Requires a
+                        * member called "log" taking any value as parameter 1 
to n.
+                        *
+                        * @type {Object|null}
+                        */
+                       logger: null
+               },
+
+               /**
+                * @see jQuery.Widget._create
+                */
+               _create: function() {
+                       registerWidgetInstance( this );
+
+                       // Assume that all labels are in the proper place if no 
scrolling has happened yet.
+                       if( window.pageYOffset ) {
+                               this.update();
+                       }
+               },
+
+               /**
+                * @see jQuery.Widget.destroy
+                */
+               destroy: function() {
+                       unregisterWidgetInstance( this );
+               },
+
+               /**
+                * Will update the position of the claimgroup labels the widget 
is controlling.
+                *
+                * @since 0.4
+                */
+               update: function() {
+                       var startTime = new Date().getTime();
+
+                       expensiveChecks = 0;
+
+                       var $firstVisibleMainSnak =
+                                       
findFirstVisibleMainSnakElementWithinClaimList( this.element );
+
+                       if( !$firstVisibleMainSnak ) {
+                               return;
+                       }
+
+                       var $claimGroup = $firstVisibleMainSnak.closest( 
'.wb-claim-section' ),
+                               $claimGroupLabel =
+                                       $claimGroup.children( 
'.wb-claim-section-name' ).children( '.wb-claim-name' );
+
+                       this._log( 'positioning', $claimGroupLabel.get( 0 ), 
'on', $firstVisibleMainSnak.get( 0 ) );
+
+                       var newLabelPosition =
+                               positionElementInOneLineWithAnother( 
$claimGroupLabel, $firstVisibleMainSnak );
+
+                       this._log( newLabelPosition
+                               ? ( 'moving label to ' + newLabelPosition )
+                               : 'no position update required'
+                       );
+
+                       var endTime = new Date().getTime();
+                       this._log( expensiveChecks + ' expensive checks, 
execution time '
+                               + ( endTime - startTime ) + 'ms' );
+               },
+
+               /**
+                * If the "logger" option is set, then this method will forward 
any given arguments
+                * to its "log" function.
+                */
+               _log: function() {
+                       var logger = this.option( 'logger' );
+                       if( logger ) {
+                               logger.log.apply( logger, arguments );
+                       }
+               }
+       } );
+
+       /**
+        * Returns an array with the active instances of the widget. A widget 
instance is considered
+        * active after its first initialization and inactive after its 
"destroy" function got called.
+        *
+        * @return $.wikibase.claimgrouplabelscroll[]
+        */
+       $.wikibase[ WIDGET_NAME ].activeInstances = function() {
+               return activeInstances.slice();
+       };
+
+       /**
+        * Checks an Claim Group's element for Main Snak elements and returns 
the first one visible in
+        * the browser's viewport.
+        * This is an optimized version of "findFirstVisibleMainSnakElement" in 
case Claim groups
+        * are expected within the DOM that should be searched for Main Snaks.
+        *
+        * @param {jQuery} $searchRange
+        * @return {null|jQuery}
+        */
+       function findFirstVisibleMainSnakElementWithinClaimList( $searchRange ) 
{
+               var $claimGroups = $searchRange.find( '.wb-claim-section' ),
+                       result = null;
+
+               // TODO: Optimize! E.g.:
+               //  (1) don't walk them top to bottom, instead, take the one in 
the middle, check whether
+               //      it is within/above/below viewport and exclude 
following/preceding ones which are
+               //      obviously not within the viewport.
+               //  (2) remember last visible node, start checking there and 
depending on scroll movement
+               //      (up/down) on its neighbouring nodes.
+               $claimGroups.each( function( i, claimGroupNode ) {
+                       if( elementPartlyVerticallyInViewport( claimGroupNode ) 
) {
+                               result = findFirstVisibleMainSnakElement( $( 
claimGroupNode ) );
+                               if( result ) {
+                                       return false;
+                               }
+                       }
+               } );
+
+               return result;
+       }
+
+       /**
+        * Checks an element for Main Snak elements and returns the first one 
visible in the browser's
+        * viewport.
+        *
+        * @param {jQuery} $searchRange
+        * @return {null|jQuery}
+        */
+       function findFirstVisibleMainSnakElement( $searchRange ) {
+               var result = null;
+
+               // ".wb-snak-value-container" is better than using 
".wb-claim-mainsnak" since we don't
+               // care about whether the margin/padding around the value is 
within viewport or not.
+               var $mainSnaks =
+                               $searchRange.find( '.wb-claim-mainsnak' 
).children( '.wb-snak-value-container' );
+
+               $mainSnaks.each( function( i, mainSnakNode ) {
+                       // Take first Main Snak value in viewport. If value is 
not fully visible in viewport,
+                       // check whether the next one is fully visible, if so, 
take that one.
+                       if( elementPartlyVerticallyInViewport( mainSnakNode ) ) 
{
+                               result = $( mainSnakNode );
+
+                               if( !elementFullyVerticallyInViewport( 
mainSnakNode ) ) {
+                                       var nextMainSnakNode = $mainSnaks.get( 
i+1 );
+                                       if( nextMainSnakNode && 
elementFullyVerticallyInViewport( nextMainSnakNode ) ) {
+                                               result = $( nextMainSnakNode );
+                                       }
+                               }
+                               return false;
+                       }
+               } );
+
+               if( result ) {
+                       // Don't forget to get the actual Snak node rather than 
the value container.
+                       result = result.closest( '.wb-claim-mainsnak');
+               }
+               return result;
+       }
+
+       /**
+        * Takes an element and positions it to be vertically at the same 
position as another given
+        * element. Animates the element to move towards that position.
+        *
+        * @param {jQuery} $element
+        * @param {jQuery} $target
+        * @return {false|string} false if the position requires no update, 
otherwise the string of
+        *          the "top" css style after the animation will be complete.
+        */
+       function positionElementInOneLineWithAnother( $element, $target ) {
+               var elementNode = $element.get( 0 ),
+                       targetNode = $target.get( 0 );
+
+               var newElementOffset = absoluteOffsetFromTop( targetNode ) - 
absoluteOffsetFromTop( elementNode.offsetParent ),
+                       currentElementOffset = $element.css( 'top' );
+
+               // Have '0' without 'px' suffix, make it a string either way:
+               newElementOffset = newElementOffset ? newElementOffset + 'px' : 
'0';
+
+               if( currentElementOffset === 'auto' && newElementOffset === '0'
+                       || currentElementOffset === newElementOffset
+               ) {
+                       return false;
+               }
+
+               $element
+               .css( 'position', 'relative' )
+               .stop( ANIMATION_QUEUE, true, false ) // stop all queued 
animations, don't jump to end
+               .animate(
+                       {
+                               top: newElementOffset
+                       }, {
+                               queue: ANIMATION_QUEUE,
+                               easing: 'easeInOutCubic',
+                               duration: 'normal'
+                       }
+               ).dequeue( ANIMATION_QUEUE ); // run animations in queue
+
+               return newElementOffset;
+       }
+
+       function absoluteOffsetFromTop( elem ) {
+               ++expensiveChecks;
+               var top = 0;
+               while( elem ) {
+                       top += elem.offsetTop;
+                       elem = elem.offsetParent;
+               }
+               return top;
+       }
+
+       function elementFullyVerticallyInViewport( elem ) {
+               var top = absoluteOffsetFromTop( elem );
+               return (
+                       top >= window.pageYOffset
+                       && ( top + elem.offsetHeight ) <= ( window.pageYOffset 
+ window.innerHeight )
+               );
+       }
+
+       function elementPartlyVerticallyInViewport( elem ) {
+               var top = absoluteOffsetFromTop( elem );
+               return (
+                       top < ( window.pageYOffset + window.innerHeight )
+                       && ( top + elem.offsetHeight ) > window.pageYOffset
+               );
+       }
+
+}( jQuery ) );
diff --git 
a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.tests.js
 
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.tests.js
new file mode 100644
index 0000000..60cb67d
--- /dev/null
+++ 
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.claimgrouplabelscroll.tests.js
@@ -0,0 +1,80 @@
+/**
+ * QUnit tests for "wikibase.claimgrouplabelscroll" jQuery widget.
+ *
+ * @file
+ * @ingroup WikibaseLib
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Werner < [email protected] >
+ */
+( function( $, QUnit, ClaimGroupLabelScrollWidget ) {
+       'use strict';
+
+       /**
+        * Returns a DOM object within a HTML page suitable for testing the 
widget on.
+        * @return {jQuery}
+        * @throws {Error} If the test runs in a non-browser environment or on 
a unsuitable HTML page.
+        */
+       function newTestNode() {
+               var $body = $( 'body' );
+
+               if( !$body.length ) {
+                       throw new Error( 'Can only run this test on a HTML page 
with "body" tag in the browser.' );
+               }
+
+               return $( '<div/>' ).appendTo( $body );
+       }
+
+       QUnit.module( 'jquery.wikibase.claimgrouplabelscroll', {
+               teardown: function() {
+                       $.each( ClaimGroupLabelScrollWidget.activeInstances(), 
function( i, instance ) {
+                               instance.destroy();
+                               instance.element.remove();
+                       } );
+               }
+       } );
+
+       QUnit.test( 'widget definition', function( assert ) {
+               assert.ok(
+                       $.isFunction( ClaimGroupLabelScrollWidget ),
+                       '"jQuery.wikibase.claimgrouplabelscroll" (widget 
definition) is defined'
+               );
+
+               assert.ok(
+                       $.isFunction( $.fn.claimgrouplabelscroll ),
+                       '"jQuery.fn.claimgrouplabelscroll" (widget bridge) is 
defined'
+               );
+
+               assert.strictEqual(
+                       ClaimGroupLabelScrollWidget.activeInstances().length,
+                       0,
+                       'Zero active instance of the widget before first 
instantiation'
+               );
+       } );
+
+       QUnit.test( 'widget instantiation and destruction', function( assert ) {
+               var $testNode = newTestNode().claimgrouplabelscroll(),
+                       instance = $testNode.data( 'claimgrouplabelscroll' );
+
+               assert.ok(
+                       instance instanceof ClaimGroupLabelScrollWidget,
+                       'Widget successfully instantiated'
+               );
+
+               assert.strictEqual(
+                       ClaimGroupLabelScrollWidget.activeInstances()[0],
+                       instance,
+                       'Instantiated widget returned by 
$.wikibase.claimgrouplabelscroll.activeInstances()'
+               );
+
+               instance.destroy();
+
+               assert.strictEqual(
+                       ClaimGroupLabelScrollWidget.activeInstances().length,
+                       0,
+                       'Zero active instances of the widget after destruction 
of only active instance'
+               );
+
+       } );
+
+}( jQuery, QUnit, jQuery.wikibase.claimgrouplabelscroll ) );
diff --git a/repo/resources/Resources.php b/repo/resources/Resources.php
index 37623ba..8ee0b99 100644
--- a/repo/resources/Resources.php
+++ b/repo/resources/Resources.php
@@ -50,7 +50,8 @@
                                'jquery.json',
                                'jquery.cookie',
                                'wikibase.serialization.entities',
-                               'wikibase.serialization.fetchedcontent'
+                               'wikibase.serialization.fetchedcontent',
+                               'jquery.wikibase.claimgrouplabelscroll'
                        ),
                        'messages' => array(
                                'wikibase-statements',
diff --git a/repo/resources/wikibase.ui.entityViewInit.js 
b/repo/resources/wikibase.ui.entityViewInit.js
index d19fda3..564eb81 100644
--- a/repo/resources/wikibase.ui.entityViewInit.js
+++ b/repo/resources/wikibase.ui.entityViewInit.js
@@ -139,12 +139,13 @@
                                } );
                        } );
 
-                       // BUILD TOOLBARS
-                       $( '.wb-entity' ).toolbarcontroller( {
+                       $( '.wb-entity' )
+                       .toolbarcontroller( { // BUILD TOOLBARS
                                addtoolbar: ['claimlistview', 'claimsection', 
'claim-qualifiers-snak', 'references', 'referenceview-snakview'],
                                edittoolbar: ['statementview', 'referenceview'],
                                removetoolbar: ['claim-qualifiers-snak', 
'referenceview-snakview-remove']
-                       } );
+                       } )
+                       .claimgrouplabelscroll();
                }
 
                // handle edit restrictions

-- 
To view, visit https://gerrit.wikimedia.org/r/69632
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I0639c32633342884921d7f639947ae8f419dd91d
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Daniel Werner <[email protected]>
Gerrit-Reviewer: Daniel Werner <[email protected]>
Gerrit-Reviewer: Henning Snater <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to