I'm trying to migrate some jQuery code to pure JavaScript. The current jQuery code toggles a class named is-page-scrolled in the body element when the user scrolls down to a specific offset set in a data attribute in the body element named data-page-scrolled-offset. If the user scrolls up again to the top of the page, the class is toggled off.
My working jQuery code:
var scrolled_page_offset = parseInt( $( 'body' ).data( 'pageScrolledOffset' ) );
var scrolled_page = function() {
var scroll_top = window.pageYOffset;
$( 'body' ).toggleClass( 'is-page-scrolled', scroll_top > scrolled_page_offset );
};
$( window ).on( 'scroll', function() {
scrolled_page();
} );
scrolled_page();
My non-working pure JavaScript code:
var scrolled_page_offset = parseInt( document.body.dataset.pageScrolledOffset );
var scrolled_page = function() {
var scroll_top = window.pageYOffset;
if ( scroll_top > scrolled_page_offset ) {
document.body.classList.toggle( 'is-page-scrolled' );
}
};
window.addEventListener( 'scroll', function() {
scrolled_page();
} );
scrolled_page();
My proposed pure Javascript code is not working as expected because it toggles the class on and off intermittently when the user scrolls down in the page.
What am I doing wrong?
Explanation of the current problem, copied from my comment:
jQuery's toggleClass() is setting the class on/off based on the
scroll_top > scrolled_page_offset. Your if + toggle() is
inverting the on/off state of the class every time scroll_top > scrolled_page_offset is true.
If we compare "does the class exist" to "do we want the class to exist", we avoid accidentally inverting the state of the class.
If this example I've added:
const isEnabled = document.body.classList.contains(className)
const shouldBeEnabled = scroll_top > scrolled_page_offset
if (shouldBeEnabled !== isEnabled) { ... }
Which will only run the if when we expect the class to change.
(In this demo, the background turns green to show when the class is added)
// Set up the demo, ignore this
document.body.dataset.pageScrolledOffset = 200;
//
const className = 'is-page-scrolled'
const scrolled_page_offset = parseInt(document.body.dataset.pageScrolledOffset);
const scrolled_page = function() {
const scroll_top = window.pageYOffset;
const isEnabled = document.body.classList.contains(className)
const shouldBeEnabled = scroll_top > scrolled_page_offset
if (shouldBeEnabled !== isEnabled) {
document.body.classList.toggle(className);
}
};
window.addEventListener('scroll', function() {
scrolled_page();
});
scrolled_page();
#scrollable {
height: 10000px;
}
.is-page-scrolled {
background: #060;
}
<div id="scrollable">
Scroll down
</div>
Related
I Used this JQuery code to have a sticky "Go to top" button:
//sticky back-to-top button
(function (t) {
t(window).bind("scroll", function () {
500 < t(this).scrollTop()
? t(".back-to-top").fadeIn(400)
: t(".back-to-top").fadeOut(400);
}),
t(".back-to-top").click(function () {
t("html, body").animate({ scrollTop: "0px" }, 500);
});
})(jQuery);
this code works correctly.
but i want when this sticky button reaches a specific class called "go-top", disappears. sorry for my bad english.
You can use interception observer so when the target element with said class is in view, it triggers the callback to hide the button.
Example: The code might not be exact but it is meant to point in the direction to follow.
let options = {
root: document.querySelector('.go-top'),
rootMargin: '0px',
threshold: 1.0
}
let prevRatio = 0.0;
let observer = new IntersectionObserver(callback, options);
let target = document.querySelector('#back-to-top-button');
observer.observe(target);
function handleIntersect(entries, observer) {
entries.forEach((entry) => {
if (entry.intersectionRatio > prevRatio) {
entry.target.style.display = "none";
} else {
entry.target.style.backgroundColor = "block";
}
prevRatio = entry.intersectionRatio;
});
}
Let me know how it goes...
You can switch the root to null but this makes the button disappear as soon as the element with the class is in the viewport
There were some problems, led to scrolling "contents_container" instead of scrolling "body".
From then on, click "history.back" to forget the scroll position.
I found jquery cookie but it doesn't works.
// When document is ready...
$(document).ready(function() {
// If cookie is set, scroll to the position saved in the cookie.
if ( $.cookie("scroll") !== null ) {
$(document).scrollTop( $.cookie("scroll") );
}
// When a button is clicked...
$('#submit').on("click", function() {
// Set a cookie that holds the scroll position.
$.cookie("scroll", $(document).scrollTop() );
});
});
and this too.
if (history.scrollRestoration === 'manual') {
history.scrollRestoration = 'auto';
}
and this too.
I really want to remember the page's scroll position.
Here is my css code and could you fix the problem?
.contents_container {width: 100%; height: 100%; overflow-y: scroll; position: relative; float:left;}
Thank you.
I made it on popstate, you can achieve this on 'beforeunload' even
on this event page still exist but is not visible. (if is server side rendered)
window.addEventListener('popstate', onLocationChange);
function onLocationChange(e){
const scrollY = window.scrollY || window.pageYOffset;
localStorage.setItem("scrollY", scrollY)
}
window.addEventListener("load", onPageLoad);
function onPageLoad(){
const scrollY = parseInt(localStorage.getItem("scrollY"));
if(scrollY && !isNaN(scrollY)) {
window.scrollTo(0, scrollY)
}
}
for overflowded container
window.addEventListener('popstate', onLocationChange);
function onLocationChange(e){
//if(document.location.test() or startsWith() ....
// to fire only on desired pages
const container document.querySelector('.contents_containe');
const scrollTop = container.scrollTop;
localStorage.setItem("container-scrollTop", scrollTop)
}
window.addEventListener("load", onPageLoad);
function onPageLoad(){
const scrollTop = parseInt(localStorage.getItem("container-scrollTop"));
if(scrollTop && !isNaN(scrollTop)) {
const container document.querySelector('.contents_containe');
container.scrollTop = scrollTop;
}
}
I'm experiencing some strange behavior with a jQuery plugin that I wrote. Basically, the plugin makes a sidebar element stick to the top of the browser window when scrolling through the blog post that the sidebar belongs to. This should only happen when the window reaches a certain size (768px) or above (the plugin detects this by checking the float style of the sidebar).
Everything works as expected...until you resize the browser from large -- sidebar is sticky -- to small -- sidebar should not be sticky. My onResize function supposedly removes the scroll event handler and only adds it back if the startQuery is true (so, if the sidebar float value is something other than none). I've double and triple checked through the console: everything is working correctly as far as I can tell. I even added console.log('scroll') to the onScroll function and it doesn't show up when the event handler is supposed to be removed, but my sidebar is still turning sticky when I scroll through the blog posts.
You can see the problem in action here. To recreate the steps:
Resize your browser window to less than 768px wide and visit the page. See that the .share-bar element inside each blog post does not move as you scroll. 'scroll' is not logged in the console.
Resize your browser window to 768px or larger. See that the .share-bar element becomes sticky as you scroll through each blog post, then sticks to the bottom of the post as you scroll past it. 'scroll' is logged in the console.
Resize your browser window to less than 768px. See that the .share-bar element becomes sticky as you scroll through each blog post. 'scroll' is not logged in the console.
It's almost as if the event handler is removed, but the elements aren't updating or something. I'm sure I'm missing something fundamental, but I've researched and tried all sorts of fixes for $(window).scroll event problems and none of them are working here.
My call to plugin:
$('.share-bar').stickySides({
'wrapper': '.post-wrapper',
'content': '.entry-content' });
Plugin code:
;( function ( $, window, document, undefined ) {
var settings;
var throttled;
$.fn.stickySides = function( options ) {
settings = $.extend( {}, $.fn.stickySides.defaults, options );
// Store sidebars
settings.selector = this.selector;
settings.startQuery = '$("' + settings.selector + '").css(\'float\') != \'none\'';
// Create debounced resize function
var debounced = _.debounce( $.fn.stickySides.onResize, settings.wait );
$(window).resize( debounced );
// Create throttled scroll function
throttled = _.throttle( $.fn.stickySides.onScroll, settings.wait );
// Only continue if the start query is true
if ( eval(settings.startQuery) == true ) {
$(window).on( 'scroll.stickySides', throttled );
}
return this;
};
// Define default settings
$.fn.stickySides.defaults = {
wrapper: 'body',
content: '.content',
wait: 100,
startQuery: ''
};
$.fn.stickySides.onResize = function ( ) {
$(window).off( 'scroll.stickySides' );
// Only continue if the start query is true
if ( eval(settings.startQuery) == true ) {
$(window).on( 'scroll.stickySides', throttled );
} else {
var sides = $(settings.selector);
sides.each ( function () {
var elem = $(this);
var content = elem.siblings( settings.content );
if ( elem.css('position') == 'fixed' || elem.css('position') == 'absolute' ) {
elem.css( 'position', 'static' );
}
if ( content.css('margin-left') != '0px' ) {
content.css( 'margin-left', 0 );
}
});
}
};
$.fn.stickySides.onScroll = function ( ) {
console.log('scroll');
var sides = $(settings.selector);
// Check each sidebar
sides.each ( function () {
var elem = $(this);
var content = elem.siblings( settings.content );
var wrapper = elem.closest( settings.wrapper );
var elemHeight = elem.height();
var wrapperHeight = wrapper.height();
// Only continue if the wrapper is taller than the sidebar
if ( elemHeight >= wrapperHeight ) {
return;
} else {
var wrapperFracs = wrapper.fracs(function (fracs) {
// Only continue if the wrapper is in view
if ( fracs.visible == 0 ) {
return;
} else {
// Check if the wrapper extends beyond the top of
// the viewport
var wrapperSpaceTop = fracs.rects.element.top;
// If it does, change sidebar position as appropriate
if ( wrapperSpaceTop > 0 ) {
var visibleWrapper = fracs.rects.document.height;
// If the visible portion of the wrapper is smaller
// than the height of the sidebar...
if ( visibleWrapper <= elemHeight ) {
// ...position the sidebar at the bottom
// of the wrapper
if ( wrapperSpaceTop != 0 ) {
elem.css('position', 'absolute').css( 'top', (wrapperHeight - elemHeight) + content.position().top + 'px' );
}
// Otherwise, move sidebar to appropriate position
} else {
elem.css('position', 'fixed').css('top', 0);
}
content.css('margin-left', elem.outerWidth());
} else {
elem.css('position', 'static');
content.css('margin-left', 0);
}
}
});
}
});
};
} )( jQuery, window, document );
PS: I would use an existing plugin, but I didn't see one that got really close to the functionality I need here; feel free to point one out if you know of one. I haven't tested outside of Mac yet. And yes, I know some of the page elements don't flow very well on mobile -- ex: site header & nav -- and there are some other missing items unrelated to this problem. I'm waiting for some feedback from my client before I can address that.
Happens every time. Shortly after I ask for help, I figure it out on my own. ;)
The problem was in the fracs function inside the onScroll function. I didn't realize it called its own resize/scroll handlers, so those weren't getting unbound when I removed my scroll handler. I just reworked the plugin to take advantage of the fracs library's handlers instead of calling my own scroll handler:
;( function ( $, window, document, undefined ) {
var settings;
var throttled;
$.fn.stickySides = function( options ) {
settings = $.extend( {}, $.fn.stickySides.defaults, options );
// Store sidebars
settings.selector = this.selector;
settings.startQuery = '$("' + settings.selector + '").css(\'float\') != \'none\'';
if ( eval( settings.startQuery ) == true ) {
$.fn.stickySides.doFracs();
}
// Create debounced resize function
var debounced = _.debounce( $.fn.stickySides.onResize, settings.wait );
$(window).resize( debounced );
return this;
};
// Define default settings
$.fn.stickySides.defaults = {
wrapper: 'body',
content: '.content',
wait: 100,
startQuery: ''
};
$.fn.stickySides.doFracs = function ( ) {
var sides = $(settings.selector);
// Check each sidebar
sides.each ( function () {
var elem = $(this);
var content = elem.siblings( settings.content );
var wrapper = elem.closest( settings.wrapper );
var elemHeight = elem.height();
var wrapperHeight = wrapper.height();
// Only continue if the wrapper is taller than the sidebar
if ( elemHeight >= wrapperHeight ) {
return;
} else {
var wrapperFracs = wrapper.fracs( $.fn.stickySides.fracsCallback );
}
});
}
$.fn.stickySides.unbindFracs = function ( ) {
var sides = $(settings.selector);
// Check each sidebar
sides.each ( function () {
var elem = $(this);
var content = elem.siblings( settings.content );
var wrapper = elem.closest( settings.wrapper );
if ( elem.css('position') == 'fixed' || elem.css('position') == 'absolute' ) {
elem.css( 'position', 'static' );
}
if ( content.css('margin-left') != '0px' ) {
content.css( 'margin-left', 0 );
}
wrapper.fracs('unbind');
});
}
$.fn.stickySides.fracsCallback = function ( fracs ) {
// Only continue if the wrapper is in view
if ( fracs.visible == 0 ) {
return;
} else {
var wrapper = $(this);
var elem = wrapper.find(settings.selector);
var content = elem.siblings( settings.content );
var elemHeight = elem.height();
var wrapperHeight = wrapper.height();
// Check if the wrapper extends beyond the top of
// the viewport
var wrapperSpaceTop = fracs.rects.element.top;
// If it does, change sidebar position as appropriate
if ( wrapperSpaceTop > 0 ) {
var visibleWrapper = fracs.rects.document.height;
// If the visible portion of the wrapper is smaller
// than the height of the sidebar...
if ( visibleWrapper <= elemHeight ) {
// ...position the sidebar at the bottom
// of the wrapper
if ( wrapperSpaceTop != 0 ) {
elem.css('position', 'absolute').css( 'top', (wrapperHeight - elemHeight) + content.position().top + 'px' );
}
// Otherwise, move sidebar to appropriate position
} else {
elem.css('position', 'fixed').css('top', 0);
}
content.css('margin-left', elem.outerWidth());
} else {
elem.css('position', 'static');
content.css('margin-left', 0);
}
}
}
$.fn.stickySides.onResize = function ( ) {
// Only continue if the start query is true
if ( eval(settings.startQuery) == true ) {
$.fn.stickySides.doFracs();
} else {
$.fn.stickySides.unbindFracs();
}
};
} )( jQuery, window, document );
Voila! Problem solved.
Am trying to change the background color of the timeline on scroll like on this site.My replication of the sample is development site. Take a look at the codepen I tried using. The closest I have come to replicating it is with the code below which makes the change in color on each timeline circle flicker on/off on scroll.
jQuery(document).ready(function($) {
function onScroll() {
$('.cd-timeline-block').each( function() {
if( $(this).offset().top <= $(window).scrollTop()+$(window).height()*0.05 && $(this).find('.cd-timeline-img').hasClass('cd-inactive') ) {
$(this).find('.cd-timeline-img').removeClass('cd-inactive').addClass('cd-active');
$(this).find('.cd-date').addClass('cd-ssf-color');
} else {
$(this).find('.cd-timeline-img').addClass('cd-inactive').removeClass('cd-active');
$(this).find('.cd-date').removeClass('cd-ssf-color');
}
});
};
$(window).scroll(onScroll);
});
I have made some modification to the above code.
CodePen link:
http://codepen.io/anon/pen/KzqWVm
Javascript:
jQuery(document).ready(function($) {
var $timeline_block = $('.cd-timeline-block');
var firstelm = $timeline_block.first();
//on scolling, show/animate timeline blocks when enter the viewport
$(window).on('scroll', function() {
var _win = $(window), iselm = null;
$timeline_block.each(function(index) {
var _this = $(this);
if (((_this.offset().top - _win.height())) <= (_win.scrollTop())) {
iselm = _this;
}
});
if (_win.scrollTop() < $(firstelm).offset().top) {
iselm = $(firstelm);
}
if (iselm) {
$('.cd-active').removeClass('cd-active').addClass('cd-inactive');
iselm.find('.cd-timeline-img').removeClass('cd-inactive').addClass('cd-active');
if ((iselm.offset().top - _win.height()) > (_win.scrollTop() * 0.75)) {
$('.cd-date').removeClass('cd-ssf-color');
}
iselm.find('.cd-date').addClass('cd-ssf-color');
}
});
});
Continuing each loop on scroll might not work properly.
In drupal i have generated a list where each item is a fieldset with collapsible, that can contain extra information.
Because of the rather large list i want to avoid loading the extra information until a user clicks on the fieldset.
Best case scenario:
User clicks on collapsed fieldset.
Fieldset loads extra information.
Fieldset uncollapses.
I've copied and loaded the copy of collapse.js into my form, but I'm very new to js and jQuery, so I'm a little lost. If someone can show me how to call a function the first time the fieldset is expanded, I'm sure i can figure out the rest.
I've included the code from collapse.js:
(function ($) {
//Toggle the visibility of a fieldset using smooth animations.
Drupal.toggleFieldset = function (fieldset) {
var $fieldset = $(fieldset);
if ($fieldset.is('.collapsed')) {
var $content = $('> .fieldset-wrapper', fieldset).hide();
$fieldset
.removeClass('collapsed')
.trigger({ type: 'collapsed', value: false })
.find('> legend span.fieldset-legend-prefix').html(Drupal.t('Hide'));
$content.slideDown({
duration: 'fast',
easing: 'linear',
complete: function () {
Drupal.collapseScrollIntoView(fieldset);
fieldset.animating = false;
},
step: function () {
// Scroll the fieldset into view.
Drupal.collapseScrollIntoView(fieldset);
}
});
}
else {
$fieldset.trigger({ type: 'collapsed', value: true });
$('> .fieldset-wrapper', fieldset).slideUp('fast', function () {
$fieldset
.addClass('collapsed')
.find('> legend span.fieldset-legend-prefix').html(Drupal.t('Show'));
fieldset.animating = false;
});
}
};
//Scroll a given fieldset into view as much as possible.
Drupal.collapseScrollIntoView = function (node) {
var h = document.documentElement.clientHeight || document.body.clientHeight || 0;
var offset = document.documentElement.scrollTop || document.body.scrollTop || 0;
var posY = $(node).offset().top;
var fudge = 55;
if (posY + node.offsetHeight + fudge > h + offset) {
if (node.offsetHeight > h) {
window.scrollTo(0, posY);
}
else {
window.scrollTo(0, posY + node.offsetHeight - h + fudge);
}
}
};
Drupal.behaviors.collapse = {
attach: function (context, settings) {
$('fieldset.collapsible', context).once('collapse', function () {
var $fieldset = $(this);
// Expand fieldset if there are errors inside, or if it contains an
// element that is targeted by the uri fragment identifier.
var anchor = location.hash && location.hash != '#' ? ', ' + location.hash : '';
if ($('.error' + anchor, $fieldset).length) {
$fieldset.removeClass('collapsed');
}
var summary = $('<span class="summary"></span>');
$fieldset.
bind('summaryUpdated', function () {
var text = $.trim($fieldset.drupalGetSummary());
summary.html(text ? ' (' + text + ')' : '');
})
.trigger('summaryUpdated');
// Turn the legend into a clickable link, but retain span.fieldset-legend
// for CSS positioning.
var $legend = $('> legend .fieldset-legend', this);
$('<span class="fieldset-legend-prefix element-invisible"></span>')
.append($fieldset.hasClass('collapsed') ? Drupal.t('Show') : Drupal.t('Hide'))
.prependTo($legend)
.after(' ');
// .wrapInner() does not retain bound events.
var $link = $('<a class="fieldset-title" href="#"></a>')
.prepend($legend.contents())
.appendTo($legend)
.click(function () {
var fieldset = $fieldset.get(0);
// Don't animate multiple times.
if (!fieldset.animating) {
fieldset.animating = true;
Drupal.toggleFieldset(fieldset);
}
return false;
});
$legend.append(summary);
});
}
};
})(jQuery);
It looks to me like you'd have to override the whole Drupal.toggleFieldset function (just like when you are overriding a Drupal theme function.
You could perhaps add a class to the fieldset in FormAPI then catch it in the complete function of the $content.slideDown params and fire a custom function of yours, to add a 'loading' graphic and make your ajax request.
I'm assuming from your question that you are familiar enough with FormAPI/jQuery.ajax() to have a go. But let me know if not and i'll include some snippets
EDIT
Here is some example code, it'd take a quite a while to setup a test environment for this, so it'just a pointer (cant create a JS fiddle for this ;))
You might add your fieldset like this in PHP
$form['my_fieldset'] = array(
'#type' = 'fieldset',
'#title' = t('My fieldset'),
'#collapsible' = true,
'#collapsed' = true,
'#attributes' => array(
'class' => array('ajax-fieldset'),
'rel' => 'callback/url/path' // random attribute to store the link to a menu path that will return your HTML
)
);
$form['my_fieldset'] = array(
'#markup' => '<div class="response">loading...</div>'
);
You'll also obviously have setup a menu hook returning your themed data # callback/url/path. IMO it's better to return JSON data and theme them in with JS templating, but the Drupal way (for the moment at least) seems to be to render HTML in the menu hook callback function.
Then here is the JS. I've only included the altered complete function, rather than reproduce what you pasted. Add the complete function in to a copy of the code the re-specify the core Drupal function in your own JS file
$content.slideDown({
complete: function () {
Drupal.collapseScrollIntoView(fieldset);
fieldset.animating = false;
if($fieldset.hasClass('ajax-fieldset')) {
$.get(
Drupal.settings.basePath + $fielset.attr('rel'),
function(data) {
$fieldset.find('.response').html(data);
}
)
}
}
});
Or, rather than messing around with the collapsible function. just create your own fieldset without the collapsible/collapsed classes and implement from scratch yourself
....so.. something like that :)