1/* 2 * By Elementor Team 3 */ 4( function( $ ) { 5 var Sticky = function( element, userSettings ) { 6 var $element, 7 isSticky = false, 8 isFollowingParent = false, 9 isReachedEffectsPoint = false, 10 elements = {}, 11 settings, 12 elementOffsetValue, 13 elementWidth; 14 15 var defaultSettings = { 16 to: 'top', 17 offset: 0, 18 effectsOffset: 0, 19 parent: false, 20 classes: { 21 sticky: 'sticky', 22 stickyActive: 'sticky-active', 23 stickyEffects: 'sticky-effects', 24 spacer: 'sticky-spacer', 25 }, 26 isRTL: false, 27 isScrollSnapActive: false, 28 handleScrollbarWidth: false, 29 }; 30 31 var initElements = function() { 32 $element = $( element ).addClass( settings.classes.sticky ); 33 34 elements.$window = $( window ); 35 elements.$body = $( document ).find( 'body' ); 36 37 if ( settings.parent ) { 38 elements.$parent = $element.parent(); 39 40 if ( 'parent' !== settings.parent ) { 41 elements.$parent = elements.$parent.closest( settings.parent ); 42 } 43 } 44 }; 45 46 var initSettings = function() { 47 settings = jQuery.extend( true, defaultSettings, userSettings ); 48 }; 49 50 var bindEvents = function() { 51 elements.$window.on( 'resize', onWindowResize ); 52 53 if ( settings.isScrollSnapActive ) { 54 elements.$body.on( 'scroll', onWindowScroll ); 55 } else { 56 elements.$window.on( 'scroll', onWindowScroll ); 57 } 58 }; 59 60 var unbindEvents = function() { 61 elements.$window 62 .off( 'scroll', onWindowScroll ) 63 .off( 'resize', onWindowResize ); 64 65 elements.$body.off( 'scroll', onWindowScroll ); 66 }; 67 68 var init = function() { 69 initSettings(); 70 71 initElements(); 72 73 bindEvents(); 74 75 checkPosition(); 76 }; 77 78 var backupCSS = function( $elementBackupCSS, backupState, properties ) { 79 var css = {}, 80 elementStyle = $elementBackupCSS[ 0 ].style; 81 82 properties.forEach( function( property ) { 83 css[ property ] = undefined !== elementStyle[ property ] ? elementStyle[ property ] : ''; 84 } ); 85 86 $elementBackupCSS.data( 'css-backup-' + backupState, css ); 87 }; 88 89 var getCSSBackup = function( $elementCSSBackup, backupState ) { 90 return $elementCSSBackup.data( 'css-backup-' + backupState ); 91 }; 92 93 const updateElementSizesData = () => { 94 elementWidth = getElementOuterSize( $element, 'width' ); 95 elementOffsetValue = $element.offset().left; 96 97 if ( settings.isRTL ) { 98 // `window.innerWidth` includes the scrollbar while `document.body.offsetWidth` doesn't. 99 const documentWidth = settings.handleScrollbarWidth ? window.innerWidth : document.body.offsetWidth; 100 101 elementOffsetValue = Math.max( documentWidth - elementWidth - elementOffsetValue, 0 ); 102 } 103 } 104 105 var addSpacer = function() { 106 elements.$spacer = $element.clone() 107 .addClass( settings.classes.spacer ) 108 .css( { 109 visibility: 'hidden', 110 transition: 'none', 111 animation: 'none', 112 } ); 113 114 $element.after( elements.$spacer ); 115 }; 116 117 var removeSpacer = function() { 118 elements.$spacer.remove(); 119 }; 120 121 var stickElement = function() { 122 backupCSS( $element, 'unsticky', [ 'position', 'width', 'margin-top', 'margin-bottom', 'top', 'bottom', 'inset-inline-start' ] ); 123 124 const css = { 125 position: 'fixed', 126 width: elementWidth, 127 marginTop: 0, 128 marginBottom: 0, 129 }; 130 131 css[ settings.to ] = settings.offset; 132 css[ 'top' === settings.to ? 'bottom' : 'top' ] = ''; 133 134 if ( elementOffsetValue ) { 135 css[ 'inset-inline-start' ] = elementOffsetValue + 'px'; 136 } 137 138 $element 139 .css( css ) 140 .addClass( settings.classes.stickyActive ); 141 }; 142 143 var unstickElement = function() { 144 $element 145 .css( getCSSBackup( $element, 'unsticky' ) ) 146 .removeClass( settings.classes.stickyActive ); 147 }; 148 149 var followParent = function() { 150 backupCSS( elements.$parent, 'childNotFollowing', [ 'position' ] ); 151 152 elements.$parent.css( 'position', 'relative' ); 153 154 backupCSS( $element, 'notFollowing', [ 'position', 'inset-inline-start', 'top', 'bottom' ] ); 155 156 const css = { 157 position: 'absolute', 158 }; 159 160 elementOffsetValue = elements.$spacer.position().left; 161 162 if ( settings.isRTL ) { 163 const parentWidth = $element.parent().outerWidth(), 164 elementOffsetValueLeft = elements.$spacer.position().left; 165 166 elementWidth = elements.$spacer.outerWidth(); 167 elementOffsetValue = Math.max( parentWidth - elementWidth - elementOffsetValueLeft, 0 ); 168 } 169 170 css[ 'inset-inline-start' ] = elementOffsetValue + 'px'; 171 172 css[ settings.to ] = ''; 173 174 css[ 'top' === settings.to ? 'bottom' : 'top' ] = 0; 175 176 $element.css( css ); 177 178 isFollowingParent = true; 179 }; 180 181 var unfollowParent = function() { 182 elements.$parent.css( getCSSBackup( elements.$parent, 'childNotFollowing' ) ); 183
184 $element.css( getCSSBackup( $element, 'notFollowing' ) ); 185 186 isFollowingParent = false; 187 }; 188 189 var getElementOuterSize = function( $elementOuterSize, dimension, includeMargins ) { 190 var computedStyle = getComputedStyle( $elementOuterSize[ 0 ] ), 191 elementSize = parseFloat( computedStyle[ dimension ] ), 192 sides = 'height' === dimension ? [ 'top', 'bottom' ] : [ 'left', 'right' ], 193 propertiesToAdd = []; 194 195 if ( 'border-box' !== computedStyle.boxSizing ) { 196 propertiesToAdd.push( 'border', 'padding' ); 197 } 198 199 if ( includeMargins ) { 200 propertiesToAdd.push( 'margin' ); 201 } 202 203 propertiesToAdd.forEach( function( property ) { 204 sides.forEach( function( side ) { 205 elementSize += parseFloat( computedStyle[ property + '-' + side ] ); 206 } ); 207 } ); 208 209 return elementSize; 210 }; 211 212 var getElementViewportOffset = function( $elementViewportOffset ) { 213 var windowScrollTop = elements.$window.scrollTop(), 214 elementHeight = getElementOuterSize( $elementViewportOffset, 'height' ), 215 viewportHeight = innerHeight, 216 elementOffsetFromTop = $elementViewportOffset.offset().top, 217 distanceFromTop = elementOffsetFromTop - windowScrollTop, 218 topFromBottom = distanceFromTop - viewportHeight; 219 220 return { 221 top: { 222 fromTop: distanceFromTop, 223 fromBottom: topFromBottom, 224 }, 225 bottom: { 226 fromTop: distanceFromTop + elementHeight, 227 fromBottom: topFromBottom + elementHeight, 228 }, 229 }; 230 }; 231 232 var stick = function() { 233 updateElementSizesData(); 234 235 addSpacer(); 236 237 stickElement(); 238 239 isSticky = true; 240 241 $element.trigger( 'sticky:stick' ); 242 }; 243 244 var unstick = function() { 245 unstickElement(); 246 247 removeSpacer(); 248 249 isSticky = false; 250 251 $element.trigger( 'sticky:unstick' ); 252 }; 253 254 var checkParent = function() { 255 var elementOffset = getElementViewportOffset( $element ), 256 isTop = 'top' === settings.to; 257 258 if ( isFollowingParent ) { 259 var isNeedUnfollowing = isTop ? elementOffset.top.fromTop > settings.offset : elementOffset.bottom.fromBottom < -settings.offset; 260 261 if ( isNeedUnfollowing ) { 262 unfollowParent(); 263 } 264 } else { 265 var parentOffset = getElementViewportOffset( elements.$parent ), 266 parentStyle = getComputedStyle( elements.$parent[ 0 ] ), 267 borderWidthToDecrease = parseFloat( parentStyle[ isTop ? 'borderBottomWidth' : 'borderTopWidth' ] ), 268 parentViewportDistance = isTop ? parentOffset.bottom.fromTop - borderWidthToDecrease : parentOffset.top.fromBottom + borderWidthToDecrease, 269 isNeedFollowing = isTop ? parentViewportDistance <= elementOffset.bottom.fromTop : parentViewportDistance >= elementOffset.top.fromBottom; 270 271 if ( isNeedFollowing ) { 272 followParent(); 273 } 274 } 275 }; 276 277 var checkEffectsPoint = function( distanceFromTriggerPoint ) { 278 if ( isReachedEffectsPoint && -distanceFromTriggerPoint < settings.effectsOffset ) { 279 $element.removeClass( settings.classes.stickyEffects ); 280 281 isReachedEffectsPoint = false; 282 } else if ( ! isReachedEffectsPoint && -distanceFromTriggerPoint >= settings.effectsOffset ) { 283 $element.addClass( settings.classes.stickyEffects ); 284 285 isReachedEffectsPoint = true; 286 } 287 }; 288 289 var checkPosition = function() { 290 var offset = settings.offset, 291 distanceFromTriggerPoint; 292 293 if ( isSticky ) { 294 var spacerViewportOffset = getElementViewportOffset( elements.$spacer ); 295 296 distanceFromTriggerPoint = 'top' === settings.to ? spacerViewportOffset.top.fromTop - offset : -spacerViewportOffset.bottom.fromBottom - offset; 297 298 if ( settings.parent ) { 299 checkParent(); 300 } 301 302 if ( distanceFromTriggerPoint > 0 ) { 303 unstick(); 304 } 305 } else { 306 var elementViewportOffset = getElementViewportOffset( $element ); 307 308 distanceFromTriggerPoint = 'top' === settings.to ? elementViewportOffset.top.fromTop - offset : -elementViewportOffset.bottom.fromBottom - offset; 309 310 if ( distanceFromTriggerPoint <= 0 ) { 311 stick(); 312 313 if ( settings.parent ) { 314 checkParent(); 315 } 316 } 317 } 318 319 checkEffectsPoint( distanceFromTriggerPoint ); 320 }; 321 322 var onWindowScroll = function() { 323 checkPosition(); 324 }; 325 326 var onWindowResize = function() { 327 if ( ! isSticky ) { 328 return; 329 } 330 331 unstickElement(); 332 333 removeSpacer(); 334 335 updateElementSizesData(); 336 337 addSpacer(); 338 339 stickElement(); 340 341 if ( settings.parent ) { 342 // Force recalculation of the relation between the element and its parent. 343 isFollowingParent = false;
344 345 checkParent(); 346 } 347 }; 348 349 this.destroy = function() { 350 if ( isSticky ) { 351 unstick(); 352 } 353 354 unbindEvents(); 355 356 $element.removeClass( settings.classes.sticky ); 357 }; 358 359 init(); 360 }; 361 362 $.fn.sticky = function( settings ) { 363 var isCommand = 'string' === typeof settings; 364 365 this.each( function() { 366 var $this = $( this ); 367 368 if ( ! isCommand ) { 369 $this.data( 'sticky', new Sticky( this, settings ) ); 370 371 return; 372 } 373 374 var instance = $this.data( 'sticky' ); 375 376 if ( ! instance ) { 377 throw Error( 'Trying to perform the `' + settings + '` method prior to initialization' ); 378 } 379 380 if ( ! instance[ settings ] ) { 381 throw ReferenceError( 'Method `' + settings + '` not found in sticky instance' ); 382 } 383 384 instance[ settings ].apply( instance, Array.prototype.slice.call( arguments, 1 ) ); 385 386 if ( 'destroy' === settings ) { 387 $this.removeData( 'sticky' ); 388 } 389 } ); 390 391 return this; 392 }; 393 394 window.Sticky = Sticky; 395} )( jQuery );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.