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

Change subject: Implemented jquery.sticknode plugin
......................................................................


Implemented jquery.sticknode plugin

Applied to the sitelinkgroupview edit toolbar container and the 
sitelinklistview table header,
both will be sticked when scrolling while their corresponding sitelinklistview 
is still visible.

Change-Id: I7c5c70e506b9d4a52bad2261486df9e0313c933f
---
M lib/resources/Resources.php
M lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
M lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
M lib/resources/jquery.wikibase/resources.php
M 
lib/resources/jquery.wikibase/themes/default/jquery.wikibase.sitelinkgroupview.css
A lib/resources/jquery/jquery.sticknode.js
M lib/tests/qunit/jquery/jquery.removeClassByRegex.tests.js
A lib/tests/qunit/jquery/jquery.sticknode.tests.js
M lib/tests/qunit/resources.php
9 files changed, 380 insertions(+), 1 deletion(-)

Approvals:
  Adrian Lang: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/lib/resources/Resources.php b/lib/resources/Resources.php
index 770ec7d..b27b25f 100644
--- a/lib/resources/Resources.php
+++ b/lib/resources/Resources.php
@@ -128,6 +128,15 @@
                        ),
                ),
 
+               'jquery.sticknode' => $moduleTemplate + array(
+                       'scripts' => array(
+                               'jquery/jquery.sticknode.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery.throttle-debounce',
+                       ),
+               ),
+
                'jquery.ui.tagadata' => $moduleTemplate + array(
                        'scripts' => array(
                                'jquery.ui/jquery.ui.tagadata.js',
diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
index 6a92c82..c891ec5 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
@@ -382,6 +382,10 @@
                                        sitelinkgroupview.stopEditing( false );
                                }
                        } );
+
+                       $container.sticknode( {
+                               $container: 
sitelinkgroupview.$sitelinklistview.data( 'sitelinklistview' ).$thead
+                       } );
                },
                'sitelinkgroupviewchange sitelinkgroupviewafterstartediting': 
function( event ) {
                        var $sitelinkgroupview = $( event.target ),
diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
index 9cac24a..29a83d1 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
@@ -111,12 +111,19 @@
                                self.enterNewItem();
                        } );
                }
+
+               this.$thead.sticknode( {
+                       $container: this.element
+               } );
+
+               this._applyStickiness();
        },
 
        /**
         * @see jQuery.ui.TemplatedWidget.destroy
         */
        destroy: function() {
+               this.$thead.data( 'sticknode' ).destroy();
                this.$listview.data( 'listview' ).destroy();
                this.$listview.off( '.' + this.widgetName );
                this.element.removeData( 'tablesorter' );
@@ -220,6 +227,50 @@
                );
        },
 
+       _applyStickiness: function() {
+               var self = this,
+                       stickyNode = this.$thead.data( 'sticknode' );
+
+               this.$thead.on( 'sticknodeupdate', function() {
+                       if( !stickyNode.isFixed() ) {
+                               return;
+                       }
+
+                       var $firstBodyTrTds = self.$listview.find( 'tr:first 
td' );
+
+                       if( !$firstBodyTrTds.length ) {
+                               return;
+                       }
+
+                       self.$thead.find( 'th' ).each( function( i ) {
+                               var $th = $( this );
+
+                               if( !self._isInEditMode ) {
+                                       $th.removeAttr( 'style' );
+                               }
+
+                               if( i === 2 && !self._isInEditMode ) {
+                                       return;
+                               }
+
+                               var width = $firstBodyTrTds.eq( i ).width();
+
+                               // Translate border width and padding added by 
tablesorter:
+                               if( i === 0 ) {
+                                       width -= 10;
+                               } else if( i === 3 ) {
+                                       width += 1;
+                               } else {
+                                       width -= 11;
+                               }
+
+                               $th.width( width );
+                       } );
+
+                       self.$thead.width( self.element.width() );
+               } );
+       },
+
        /**
         * @param {jQuery.wikibase.sitelinkview} sitelinkview
         */
diff --git a/lib/resources/jquery.wikibase/resources.php 
b/lib/resources/jquery.wikibase/resources.php
index 2f13aed..f8811d3 100644
--- a/lib/resources/jquery.wikibase/resources.php
+++ b/lib/resources/jquery.wikibase/resources.php
@@ -319,6 +319,7 @@
                                
'themes/default/jquery.wikibase.sitelinkgroupview.css',
                        ),
                        'dependencies' => array(
+                               'jquery.sticknode',
                                'jquery.ui.TemplatedWidget',
                                'jquery.wikibase.sitelinklistview',
                                'mediawiki.jqueryMsg', // for {{plural}} and 
{{gender}} support in messages
@@ -335,6 +336,7 @@
                        ),
                        'dependencies' => array(
                                'jquery.event.special.eachchange',
+                               'jquery.sticknode',
                                'jquery.tablesorter',
                                'jquery.ui.TemplatedWidget',
                                'jquery.wikibase.addtoolbar',
diff --git 
a/lib/resources/jquery.wikibase/themes/default/jquery.wikibase.sitelinkgroupview.css
 
b/lib/resources/jquery.wikibase/themes/default/jquery.wikibase.sitelinkgroupview.css
index 8212dd9..589cb5f 100644
--- 
a/lib/resources/jquery.wikibase/themes/default/jquery.wikibase.sitelinkgroupview.css
+++ 
b/lib/resources/jquery.wikibase/themes/default/jquery.wikibase.sitelinkgroupview.css
@@ -21,4 +21,5 @@
 
 .wikibase-sitelinkgroupview .wikibase-sitelinkgroupview-heading-container > 
.wikibase-toolbar-container {
        margin-top: 1.9em;
+       z-index: 1;
 }
diff --git a/lib/resources/jquery/jquery.sticknode.js 
b/lib/resources/jquery/jquery.sticknode.js
new file mode 100644
index 0000000..92aa88a
--- /dev/null
+++ b/lib/resources/jquery/jquery.sticknode.js
@@ -0,0 +1,277 @@
+/**
+ * @licence GNU GPL v2+
+ * @author H. Snater < mediaw...@snater.com >
+ */
+( function( $ ) {
+       'use strict';
+
+var $window = $( window ),
+       stickyInstances = [],
+       PLUGIN_NAME = 'sticknode';
+
+/**
+ * jQuery sticknode plugin.
+ * Sticks a node with "position: fixed" when vertically scrolling it out of 
the viewport.
+ *
+ * @param {Object} [options]
+ *        - {jQuery} $container
+ *          Node specifying the bottom boundary for the node the plugin is 
initialized on. If the
+ *          node the plugin is initialized on clips out of the container, it 
is reset to static
+ *          position.
+ * @return {jQuery}
+ *
+ * @event sticknodeupdate
+ *        Triggered when the node the widget is initialized on updates its 
positioning behaviour.
+ *        - {jQuery.Event}
+ */
+$.fn.sticknode = function( options ) {
+       options = options || {};
+
+       this.each( function() {
+               var $node = $( this );
+
+               if( $node.data( PLUGIN_NAME ) ) {
+                       return;
+               }
+
+               register( new StickyNode( $( this ), options ) );
+       } );
+
+       return this;
+};
+
+/**
+ * @param {boolean} [force]
+ */
+function update( force ) {
+       force = force === true;
+
+       for( var i = 0; i < stickyInstances.length; i++ ) {
+               if( stickyInstances[i].update( $window.scrollTop(), force ) ) {
+                       stickyInstances[i].$node.triggerHandler( PLUGIN_NAME + 
'update' );
+               }
+       }
+}
+
+function updateAndForceTriggerEvent() {
+       update( true );
+}
+
+/**
+ * @param {Function} fn
+ * @return {Function}
+ */
+function throttle( fn ) {
+       return $.throttle ? $.throttle( 150, fn ) : fn;
+}
+
+/**
+ * @param {StickyNode} sticky
+ */
+function register( sticky ) {
+       if( !stickyInstances.length ) {
+               $window
+               .on( 'scroll.' + PLUGIN_NAME + ' ' + 'touchmove.' + 
PLUGIN_NAME, ( function() {
+                       return throttle( update );
+               }() ) )
+               .on( 'resize.' + PLUGIN_NAME, ( function() {
+                       return throttle( updateAndForceTriggerEvent );
+               }() ) );
+       }
+       stickyInstances.push( sticky );
+}
+
+/**
+ * @param {StickyNode} sticky
+ */
+function deregister( sticky ) {
+       var index = $.inArray( sticky );
+       if( index ) {
+               stickyInstances.splice( index, 1 );
+       }
+       if( !stickyInstances.length ) {
+               $window.off( '.' + PLUGIN_NAME );
+       }
+}
+
+/**
+ * @constructor
+ *
+ * @param {jQuery} $node
+ * @param {Object} options
+ */
+var StickyNode = function( $node, options ) {
+       this.$node = $node;
+       this.$node.data( PLUGIN_NAME, this );
+
+       this._options = $.extend( {
+               $container: null
+       }, options );
+
+       this._initialAttributes = {};
+};
+
+$.extend( StickyNode.prototype, {
+       /**
+        * @type {jQuery}
+        */
+       $node: null,
+
+       /**
+        * @type {Object}
+        */
+       _options: null,
+
+       /**
+        * @type {Object}
+        */
+       _initialAttributes: null,
+
+       /**
+        * @type {boolean}
+        */
+       _changesDocumentHeight: false,
+
+       /**
+        * Destroys and deregisters the plugin.
+        */
+       destroy: function() {
+               deregister( this );
+               this.$node.removeData( PLUGIN_NAME );
+       },
+
+       /**
+        * @return {boolean}
+        */
+       _clipsContainer: function() {
+               if( !this._options.$container || !this.isFixed() ) {
+                       return false;
+               }
+
+               var nodeBottom = this.$node.offset().top + 
this.$node.outerHeight();
+
+               var containerBottom = this._options.$container.offset().top
+                       + this._options.$container.outerHeight();
+
+               return nodeBottom > containerBottom;
+       },
+
+       /**
+        * @return {boolean}
+        */
+       _isScrolledAfterContainer: function() {
+               if( !this._options.$container ) {
+                       return false;
+               }
+
+               var containerBottom = this._options.$container.offset().top
+                       + this._options.$container.outerHeight();
+
+               return $window.scrollTop() + this.$node.outerHeight() > 
containerBottom;
+       },
+
+       /**
+        * @param {number} scrollTop
+        * @return {boolean}
+        */
+       _isScrolledBeforeContainer: function( scrollTop ) {
+               if( !this._initialAttributes.offset ) {
+                       return false;
+               }
+
+               var initTopOffset = this._initialAttributes.offset.top;
+
+               return !this._changesDocumentHeight && scrollTop < initTopOffset
+                       || this._changesDocumentHeight && scrollTop < 
initTopOffset - this.$node.outerHeight();
+       },
+
+       _fix: function() {
+               if( this.isFixed() ) {
+                       return;
+               }
+
+               this._initialAttributes = {
+                       offset: this.$node.offset(),
+                       position: this.$node.css( 'position' ),
+                       top: this.$node.css( 'top' ),
+                       left: this.$node.css( 'left' )
+               };
+
+               this.$node
+               .css( 'left', this._initialAttributes.offset.left + 'px' )
+               .css( 'top', this.$node.outerHeight() - this.$node.outerHeight( 
true ) )
+               .css( 'position', 'fixed' );
+       },
+
+       _unfix: function() {
+               this.$node
+               .css( 'left', this._initialAttributes.left )
+               .css( 'top', this._initialAttributes.top )
+               .css( 'position', this._initialAttributes.position );
+
+               this._initialAttributes.offset = null;
+       },
+
+       /**
+        * Returns whether the node the plugin is initialized on is in "fixed" 
position.
+        *
+        * @return {boolean}
+        */
+       isFixed: function() {
+               return this.$node.css( 'position' ) === 'fixed';
+       },
+
+       /**
+        * Updates the node's positioning behaviour according to a specific 
scroll offset.
+        *
+        * @param {number} scrollTop
+        * @param {boolean} force
+        * @return {boolean}
+        */
+       update: function( scrollTop, force ) {
+               var changedState = false,
+                       $document = $( document ),
+                       initialDocumentHeight = $document.height(),
+                       newDocumentHeight;
+
+               if( force && this.isFixed() ) {
+                       this._unfix();
+               }
+
+               if(
+                       !this.isFixed()
+                       && scrollTop > this.$node.offset().top
+                       && !this._isScrolledAfterContainer()
+               ) {
+                       this._fix();
+
+                       newDocumentHeight = $document.height();
+                       if( newDocumentHeight < initialDocumentHeight ) {
+                               $window.scrollTop( scrollTop - ( 
initialDocumentHeight - newDocumentHeight ) );
+                               initialDocumentHeight = newDocumentHeight;
+                               this._changesDocumentHeight = true;
+                       }
+
+                       changedState = true;
+               }
+
+               if(
+                       this.isFixed() && this._isScrolledBeforeContainer( 
scrollTop )
+                       || this._clipsContainer()
+               ) {
+                       this._unfix();
+
+                       newDocumentHeight = $document.height();
+                       if( newDocumentHeight > initialDocumentHeight ) {
+                               $window.scrollTop( scrollTop + ( 
newDocumentHeight - initialDocumentHeight ) );
+                               this._changesDocumentHeight = true;
+                       }
+
+                       changedState = !changedState;
+               }
+
+               return changedState;
+       }
+} );
+
+}( jQuery ) );
\ No newline at end of file
diff --git a/lib/tests/qunit/jquery/jquery.removeClassByRegex.tests.js 
b/lib/tests/qunit/jquery/jquery.removeClassByRegex.tests.js
index 4914748..2580dce 100644
--- a/lib/tests/qunit/jquery/jquery.removeClassByRegex.tests.js
+++ b/lib/tests/qunit/jquery/jquery.removeClassByRegex.tests.js
@@ -5,7 +5,7 @@
 ( function( $, QUnit ) {
        'use strict';
 
-QUnit.module( 'jquery.removeClassByRegex', QUnit.newMwEnvironment() );
+QUnit.module( 'jquery.removeClassByRegex' );
 
 QUnit.test( 'Basics', function( assert ) {
        var classes = [ 'a11a', 'bbb', 'c333', 'dddd', 'e', '6', '7' ];
diff --git a/lib/tests/qunit/jquery/jquery.sticknode.tests.js 
b/lib/tests/qunit/jquery/jquery.sticknode.tests.js
new file mode 100644
index 0000000..736e452
--- /dev/null
+++ b/lib/tests/qunit/jquery/jquery.sticknode.tests.js
@@ -0,0 +1,26 @@
+/**
+ * @licence GNU GPL v2+
+ * @author H. Snater < mediaw...@snater.com >
+ */
+( function( $, QUnit ) {
+       'use strict';
+
+QUnit.module( 'jquery.sticknode' );
+
+QUnit.test( 'Create & destroy', function( assert ) {
+       var $node = $( '<div/>' ).sticknode();
+
+       assert.ok(
+               $node.data( 'sticknode' ) !== undefined,
+               'Attached plugin.'
+       );
+
+       $node.data( 'sticknode' ).destroy();
+
+       assert.ok(
+               $node.data( 'sticknode' ) === undefined,
+               'Detached plugin.'
+       );
+} );
+
+}( jQuery, QUnit ) );
diff --git a/lib/tests/qunit/resources.php b/lib/tests/qunit/resources.php
index cd4b163..3812c2d 100644
--- a/lib/tests/qunit/resources.php
+++ b/lib/tests/qunit/resources.php
@@ -44,6 +44,15 @@
                        ),
                ),
 
+               'jquery.sticknode.tests' => $moduleBase + array(
+                       'scripts' => array(
+                               'jquery/jquery.sticknode.tests.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery.sticknode',
+                       ),
+               ),
+
                'jquery.ui.tagadata.tests' => $moduleBase + array(
                        'scripts' => array(
                                'jquery.ui/jquery.ui.tagadata.tests.js',

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I7c5c70e506b9d4a52bad2261486df9e0313c933f
Gerrit-PatchSet: 10
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Henning Snater <henning.sna...@wikimedia.de>
Gerrit-Reviewer: Adrian Lang <adrian.l...@wikimedia.de>
Gerrit-Reviewer: Henning Snater <henning.sna...@wikimedia.de>
Gerrit-Reviewer: Thiemo Mättig (WMDE) <thiemo.maet...@wikimedia.de>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to