I have been tasked with creating one long page that will have various small articles for the user to scroll through. Each of the article sections have a category and I need for the URL to reflect that category being viewed. I am aware that history.pushState()
can be used to change a URL when triggered. But it is the logistics that I am unclear about.
I have already figured out a way to have the main menu links jump down to the appropriate category using the following code and it seems to work fine.
$('a.js-link').on('click',function (e) {
e.preventDefault; // dont actually follow the link
var category = $(this).data('category-id'); // get the id we should anchor to
location.hash = '#' + category; // jump to the location hash id
history.pushState("", "", category); // make the url pretty again
return false; // dont append #category to the pretty url
});
However, when a user scrolls down and a new category starts appearing the URL should change to example.com/category2 (as category 1 is the first and will appear on initial page load), user keeps scrolling then URL is changed to example.com/category3. Now if the user scrolls back up it should change back to example.com/category2. Am I simply listening for the scroll event and then checking to see if an element is in the viewport? How do I handle a situation where the upper half of the page is showing a previous category and the lower half the next?
My other roadblock is how to handle linking directly to a specific category on the page. If a user is linked directly to example.com/category5 the page should be loaded and anchored down to the category5 section.
I should mention that I plan on using jquery.lazy.js to load in the main container div's for categories in order to decrease initial page load time. Due to this, I cant use jquery to scroll down to an element since it calculates the size of the element/page and scrolls down by that amount, but then once the content is loaded in it is no longer the appropriate category in view. Although I am open to changing this approach if it majorly affects the ability to change the URL.
Sorry for the wall of text! I am not looking for someone to code this for me, but to push me in the right direction!
The short answer is I typically use history.replaceState()
to accomplish this but there is a lot more going on. MDN describes this method:
Updates the most recent entry on the history stack to have the specified data, title, and, if provided, URL. The data is treated as opaque by the DOM; you may specify any JavaScript object that can be serialized.
In practice, how this works out is that I create a state object with the different states being my page sections. I often use this in conjunction with the excellent Waypoints plugin, triggering a state change based on a waypoint. This code is pretty easy to understand, read my comments which will walk you through it.
// 1- Sets up state object that will store each page section 'state'
var stateObj = { state0: "group" };
// 2- Setups up waypoint to indicate a new section
var yourNameWaypoint = new Waypoint({
// 3- Target an ID for waypoints to watch, corresponds to a page section
element: document.getElementById('page-id-target'),
handler: function(direction) {
// 4- Create down direction handler
if (direction === 'down') {
// 5- Assign stateObj a page state that corresponds to your page section
stateObj = { state1: "page-section-1" };
history.replaceState(stateObj, "Section Title", "#new-page-section-slug-here");
}
// 6- Do the same thing in the other direction now, reseting the URL to what it was before the waypoint was hit
if (direction === 'up') {
stateObj = { state0: "original-state" };
history.replaceState(stateObj, "Original Page Section Title", "original-page-slug-here");
}
}
});
To get the hashes to correspond to scroll positions is a bit more difficult. I modified a pretty good script to get this to work (http://jsfiddle.net/ianclark001/rkocah23/), but I'll post the original here.
The idea is pretty simple. Basically, you are reading each URL hash you created in the function above within init
and then feeding that into scrollToCurrent
part of the function when they match, using history.pushState()
. I've found I like to put a little delay in the scroll animation to make the behavior feel more normal (set at 500ms below but you could adjust that).
(function(document, history, location) {
var HISTORY_SUPPORT = !!(history && history.pushState);
var anchorScrolls = {
ANCHOR_REGEX: /^#[^ ]+$/,
OFFSET_HEIGHT_PX: 50,
/**
* Establish events, and fix initial scroll position if a hash is provided.
*/
init: function() {
this.scrollToCurrent();
$(window).on('hashchange', $.proxy(this, 'scrollToCurrent'));
$('body').on('click', 'a', $.proxy(this, 'delegateAnchors'));
},
/**
* Return the offset amount to deduct from the normal scroll position.
* Modify as appropriate to allow for dynamic calculations
*/
getFixedOffset: function() {
return this.OFFSET_HEIGHT_PX;
},
/**
* If the provided href is an anchor which resolves to an element on the
* page, scroll to it.
* @param {String} href
* @return {Boolean} - Was the href an anchor.
*/
scrollIfAnchor: function(href, pushToHistory) {
var match, anchorOffset;
if(!this.ANCHOR_REGEX.test(href)) {
return false;
}
match = document.getElementById(href.slice(1));
if(match) {
anchorOffset = $(match).offset().top - this.getFixedOffset();
$('html, body').delay(500).animate({ scrollTop: anchorOffset});
// Add the state to history as-per normal anchor links
if(HISTORY_SUPPORT && pushToHistory) {
history.pushState({}, document.title, location.pathname + href);
}
}
return !!match;
},
/**
* Attempt to scroll to the current location's hash.
*/
scrollToCurrent: function(e) {
if(this.scrollIfAnchor(window.location.hash) && e) {
e.preventDefault();
}
},
/**
* If the click event's target was an anchor, fix the scroll position.
*/
delegateAnchors: function(e) {
var elem = e.target;
if(this.scrollIfAnchor(elem.getAttribute('href'), true)) {
e.preventDefault();
}
}
};
$(document).ready($.proxy(anchorScrolls, 'init'));
})(window.document, window.history, window.location);