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