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