| Current File : /home/jvzmxxx/wiki1/extensions/Wikibase/view/resources/jquery/jquery.sticknode.js |
/**
* @license GPL-2.0+
* @author H. Snater < mediawiki@snater.com >
*/
( function( $ ) {
'use strict';
var $window = $( window ),
eventSingleton = new $.util.EventSingletonManager(),
PLUGIN_NAME = 'sticknode';
/**
* @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,
autoWidth: false,
zIndex: 1
}, options );
this._initialAttributes = {};
};
$.extend( StickyNode.prototype, {
/**
* @type {jQuery}
*/
$node: null,
/**
* @type {jQuery|null}
*/
_$clone: null,
/**
* @type {Object}
*/
_options: null,
/**
* @type {Object}
*/
_initialAttributes: null,
/**
* @type {boolean}
*/
_changesDocumentHeight: false,
/**
* Destroys and unregisters the plugin.
*/
destroy: function() {
eventSingleton.unregister(
this.$node.data( PLUGIN_NAME ),
window,
'.' + PLUGIN_NAME
);
this.$node.removeData( PLUGIN_NAME );
if ( this._$clone ) {
this._$clone.remove();
this._$clone = null;
}
},
/**
* @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' ),
width: this.$node.css( 'width' )
};
var width = this.$node.width();
// Cannot fix the clone instead of the original node since the clone does not feature event
// bindings.
this._$clone = this.$node.clone()
.css( 'visibility', 'hidden' )
.insertBefore( this.$node );
this.$node
.css( 'left', this._initialAttributes.offset.left + 'px' )
.css( 'top', this.$node.outerHeight() - this.$node.outerHeight( true ) )
.css( 'width', width )
.css( 'position', 'fixed' )
.css( 'z-index', this._options.zIndex );
if ( this._$clone.css( 'display' ) === 'table-header-group' ) {
var $original = this._$clone.find( '*' );
this.$node.find( '*' ).each( function( i ) {
var $node = $( this );
if ( $node.css( 'display' ) === 'table-cell' ) {
$node.width( $original.eq( i ).width() + 'px' );
}
} );
}
},
_unfix: function() {
if ( !this.isFixed() ) {
return;
}
if ( this._$clone ) {
this._$clone.remove();
this._$clone = null;
}
this.$node
.css( 'left', this._initialAttributes.left )
.css( 'top', this._initialAttributes.top )
.css( 'width', this._options.autoWidth ? 'auto' : this._initialAttributes.width )
.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;
if ( force && this.isFixed() ) {
this._unfix();
}
if ( !this.isFixed()
&& scrollTop > this.$node.offset().top
&& !this._isScrolledAfterContainer()
) {
this._fix();
changedState = true;
}
if ( this.isFixed() && this._isScrolledBeforeContainer( scrollTop )
|| this._clipsContainer()
) {
this._unfix();
changedState = !changedState;
}
return changedState;
},
/**
* Re-fixes the node if it is fixed, properly updating scroll position. Should be called
* whenever the node's content has been updated.
*/
refresh: function() {
if ( this.isFixed() ) {
this._unfix();
this._fix();
this.$node.triggerHandler( PLUGIN_NAME + 'update' );
}
}
} );
/**
* jQuery sticknode plugin.
* Sticks a node with "position: fixed" when vertically scrolling it out of the viewport.
* Be aware that plugin does not handle dynamic height changes (e.g. if the node contains
* interactive elements that wipe out additional content). The code applying the widget needs to be
* aware of dynamic height changes. Consequently, whenever the height of the node the plugin is
* initialized on changes, a call to the refresh() function should be made to avoid undesired
* clipping.
*
* @param {Object} [options]
* @param {jQuery} [options.$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.
* @param {boolean} [options.autoWidth=false]
* When not fixed, apply "auto" width attribute instead of width computed from the unfixed
* state.
* @param {number} [options.zIndex=1]
* Custom z-index attribute.
* @return {jQuery}
*
* @event sticknodeupdate
* Triggered when the node the widget is initialized and updates its positioning behaviour.
* - {jQuery.Event}
*/
$.fn.sticknode = function( options ) {
options = options || {};
this.each( function() {
var $node = $( this );
if ( $node.data( PLUGIN_NAME ) ) {
return;
}
var stickyNode = new StickyNode( $( this ), options );
eventSingleton.register(
stickyNode,
window,
'scroll.' + PLUGIN_NAME + ' ' + 'touchmove.' + PLUGIN_NAME,
function( event, stickyNode ) {
if ( stickyNode.update( $window.scrollTop() ) ) {
stickyNode.$node.triggerHandler( PLUGIN_NAME + 'update' );
}
},
{
throttle: 150
}
);
eventSingleton.register(
stickyNode,
window,
'resize.' + PLUGIN_NAME,
function( event, stickyNode ) {
if ( stickyNode.update( $window.scrollTop(), true ) ) {
stickyNode.$node.triggerHandler( PLUGIN_NAME + 'update' );
}
},
{
throttle: 150
}
);
} );
return this;
};
}( jQuery ) );