elsewhere
Version:
A node project that aims to replicate the functionality of the Google Social Graph API
573 lines (437 loc) • 16 kB
JavaScript
/***********************************************
Main page controller
Scrolling
Subnav
Floating navigation
See demo.js for Code Examples Controller
***********************************************/
var satya = satya || {};
satya.jQuery = satya.noConflict ? jQuery.noConflict(satya.noConflict) : jQuery;
satya.page = (function ($, $qS) { // jQuery and document.querySelector
"use strict";
// --------------------
// LIBRARIES
// jQuery Tiny Pub/Sub - v0.7 - 10/27/2011 http://benalman.com/
// Copyright 2011 'Cowboy' Ben Alman; Licensed MIT, GPL
var o = $({});
$.subscribe = function() {
o.on.apply(o, arguments);
};
$.unsubscribe = function() {
o.off.apply(o, arguments);
};
$.publish = function() {
o.trigger.apply(o, arguments);
};
// UTILITIES
function throttle(fn, delay) {
var timer = null;
return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
function setClass(el, className, state){
var toggleClass = state ? 'add' : 'remove';
el.classList[toggleClass](className);
}
function getOffsetY(target){
var top = 0;
while (target && target !== document.body){
if (target.offsetTop){
top += target.offsetTop;
}
target = target.offsetParent;
}
return top;
}
// Find the width of the biggest link in the subnav
// to see how wide it is visually
function getLinkListWidth(el){
var links = el.querySelectorAll('a'),
visualWidth = 0,
currentWidth;
for (var i = 0; i < links.length; ++i) {
currentWidth = links[i].getBoundingClientRect().width;
visualWidth = currentWidth > visualWidth ? currentWidth : visualWidth;
}
return visualWidth;
}
function getTargetId(hashId){
return hashId ? hashId.substring(1, hashId.length) : null;
}
// Animate a scroll to the provided offset.
function animateScrollTo(offset, callback) {
var total = Math.abs(window.pageYOffset - offset),
start = document.documentElement.scrollHeight < window.innerHeight * 8 ?
window.Math.ceil(500 * total /
document.documentElement.scrollHeight)
: window.Math.ceil(1000 * total /
(window.innerHeight * 8)) ,
last,
timer;
clearTimeout(timer);
(function doScroll() {
if (last && window.pageYOffset !== last) {
// Manually moved by the user so stop scrolling.
return clearTimeout(timer);
}
var difference = window.pageYOffset - offset,
direction = difference < 1 ? 1 : -1,
modifier = Math.abs(difference) / total,
increment = Math.ceil(start * modifier);
if (difference !== last && (direction < 0 ||
window.innerHeight + window.pageYOffset !== document.documentElement.scrollHeight)) {
if (difference < increment && difference > increment * -1) {
increment = Math.abs(difference);
}
last = window.pageYOffset + (increment * direction);
if(Math.abs(difference) < 2){
if(callback){
callback();
}
return clearTimeout(timer);
}
window.scrollTo(0, last);
timer = setTimeout(doScroll, 500 / 60);
}
})();
}
function setLogoPosition(){
var header = $qS('h1.title'),
svg_width = parseInt(getComputedStyle(header, ':after').width, 10);
if(($qS('h1.title a').clientWidth + svg_width) > header.clientWidth){
header.classList.add('long-title');
}
}
function setPermalinkTopOffset(){
var permalinks = document.querySelectorAll('.permalink'),
navHeight = navEl.getBoundingClientRect().height,
margin = 20,
topOffset;
topOffset = narrowScreen ?
navHeight + navOffsetTop
: navHeight + margin;
for (var i = 0; i < permalinks.length; ++i) {
var permalink = permalinks[i];
permalink.style.paddingTop = topOffset + 'px';
permalink.style.top = '-' + topOffset + 'px';
}
}
// ---------------------
// satya
var narrowScreen = satya.narrowScreen,
// Why sniff for ipad?
// It's to prevent iOS5 position fixed bugs,
// rather than anything to do with width
isIPad = satya.isIPad,
navEl = $qS('#navigation'),
header = $qS('header'),
navOffsetTop = navEl.offsetTop,
subnavId = 'subnav',
subnavEl = $qS('#subnav'),
content = $qS('section.content'),
contentWidth = content.clientWidth,
navigation,
subnav;
// --------------------
// SCROLLING
function moveToAnchor(anchor){
// set the position to scroll to - include hidden padding
// in anchor to set the position below fixed navigation
var scrollYPos = getOffsetY(anchor),
maxScrollDist = window.innerHeight * 2,
distance = Math.abs(scrollYPos - window.pageYOffset);
// the distance between link and anchor > x screen heights
if((distance > maxScrollDist)){
// jump to anchor
window.scrollTo(0, scrollYPos);
setLocationHash();
}else{
animateScrollTo(scrollYPos, setLocationHash);
}
function setLocationHash(){
window.location.hash = anchor.id;
}
}
// --------------------
// NAV STATE CONTROLLER
// See wiki/Navigation-State
// HELPERS
// Scroll position > height of the header
function isScrollGtHeader(){
return window.pageYOffset > navOffsetTop;
}
// width of subnav plus negative shift offscreen < visible width of subnav
// or (space on left of content area < visible width of subnav (+ margin))
function isSubnavSqueezed(){
var subnavSqueezed = $(subnav.el).width() + parseInt(subnav.getLeftOffset(), 10) < subnav.width;
//var subnavSqueezed = $(content).offset().left < subnav.width + subnav.margin;
return subnavSqueezed;
}
// COMPONENTS
// Navigation (navigation)
function Navigation(el) {
this.el = el;
this.isScrollGtHeader = false;
this.height = el.getBoundingClientRect().height;
this.subscribeEvents();
}
Navigation.prototype.subscribeEvents = function() {
var nav = this;
// page scrolls beyond header height, set navigation to fixed position
$.subscribe('scrollGtHeader', function(e, state){
if(state !== nav.isScrollGtHeader){
nav.isScrollGtHeader = state;
setClass(nav.el, 'float', state);
// add placeholder for height of nav to not alter document height
header.style.marginBottom = nav.isScrollGtHeader ?
nav.height + "px" : null;
nav.toggleTitle(state);
}
});
};
Navigation.prototype.setSubnavButtonState = function(state) {
setClass(this.el, 'show-subnav-button', state);
};
Navigation.prototype.toggleTitle = function(add) {
var navItems = this.el.querySelector("ul");
if(add){
navItems.insertBefore(
$qS('h1.title').cloneNode(true),
this.el.querySelector('.show-subnav').nextSibling
);
}else{
navItems.removeChild(this.el.querySelector('h1'));
}
};
// Left hand nav area
function Subnav(el) {
this.el = el;
this.isScrollGtHeader = false;
this.isSubnavSqueezed = false;
this.isOpen = null;
this.timeout = null;
this.width = null;
this.height = this.el.getBoundingClientRect().height;
this.clone = this.el.cloneNode(true);
// TO DO
this.margin = 20;
this.fixedLeftPos = 0;
this.addClone();
this.subscribeEvents();
}
// Subnav (subnav)
Subnav.prototype.addClone = function addClone() {
var subnav = this;
subnav.clone.id = 'subnavClone';
subnav.clone.style.visibility = 'hidden';
subnav.clone.style.top = '0px';
content.appendChild(subnav.clone);
};
Subnav.prototype.subscribeEvents = function() {
var subnav = this;
subnav.fixedLeftPos = subnav.getLeftOffset();
// page scrolls beyond header height, set subnav to fixed,
// set left subnav to former left position (and vice versa)
$.subscribe('scrollGtHeader', function(e, state){
if(state !== subnav.isScrollGtHeader){ // there's a boundary change
subnav.isScrollGtHeader = state; // set model
setClass(subnav.el, 'fixed', state);
subnav.el.style.left = subnav.isScrollGtHeader ?
subnav.isOpen ?
subnav.openPos :
subnav.fixedLeftPos
: null;
// Set a fixed height on the subnav at the point
// the subnav is taller than the available space on-screen
subnav.setSubnavHeight();
}
});
$.subscribe('windowResized', function() {
// update model for vertical scrolling state changes
subnav.fixedLeftPos = subnav.getLeftOffset();
subnav.openPos = subnav.fixedLeftPos;
// set the left pos if position fixed
// otherwise it will sit in the same place when the browser resizes
if(subnav.isScrollGtHeader){
subnav.el.style.left = subnav.fixedLeftPos;
subnav.setSubnavHeight();
}
});
// show and hide the subnav and its button depending on the
// avaiable space to the left of the content area
$.subscribe('subnavSqueezed', function(e, state){
subnav.el.style.visibility = 'visible';
if(state !== subnav.isSubnavSqueezed){
subnav.isSubnavSqueezed = state;
subnav.updateSubnavView(state);
// if there's enough room for the subnav and the subnav is open
// ISSUE: if the subnav is opened, then there's enough room for the subnav
// Required: if the subnav is open and there's a enough room for the subnav if the subnav was closed.
if(subnav.isOpen && subnav.isSubnavSqueezed === false){
subnav.close();
}
}
});
};
Subnav.prototype.updateSubnavView = function updateSubnavView(state) {
navigation.setSubnavButtonState(state);
setClass(this.el, 'off-left', state);
};
// Set a fixed height on the subnav at the point
// the subnav is taller than the available space on-screen
Subnav.prototype.setSubnavHeight = function setSubnavHeight(setHeight) {
var availHeight = window.innerHeight - this.el.offsetTop - 10;
if((this.isScrollGtHeader || setHeight) && (this.height > availHeight)){
this.el.style.height = (availHeight - 20) + 'px';
}else{
this.el.style.height = null;
}
};
Subnav.prototype.getLeftOffset = function getLeftOffset() {
return $(this.clone).offset().left + 'px';
};
Subnav.prototype.toggle = function() {
if(this.isOpen){
this.close();
}
else {
this.open();
}
};
Subnav.prototype.open = function() {
this.isOpen = true;
this.el.classList.add("show-nav");
var subnav = this,
gutter = (window.innerWidth - contentWidth)/2;
if(narrowScreen){
this.el.classList.add("show-nav-small");
}
else{
content.style.left = (this.width - gutter) + (this.margin * 2) + "px";
this.el.style.opacity = 0;
// set open left position in model after the
// animation to open is complete
this.timeout = window.setTimeout(function(){
subnav.openPos = subnav.getLeftOffset();
if(subnav.isScrollGtHeader){
// set the left pos if position fixed
subnav.el.style.left = subnav.openPos;
}
subnav.el.style.opacity = 1;
}, 309);
}
};
Subnav.prototype.close = function() {
this.isOpen = false;
this.el.classList.remove("show-nav");
if(narrowScreen){
this.el.classList.remove("show-nav-small");
} else {
content.style.left = null;
this.el.style.opacity = null;
clearTimeout(this.timeout);
}
};
Subnav.prototype.setSelectSubnav = function() {
var options = $(this.el).find('a').map(function(){
var link = this,
$link = $(link),
linktext = $.trim($link.text()),
option = $('<option></option>')
.attr('value',link.hash)
.text(linktext);
return option[0];
});
var $select = $('<select id="subnav-menu">').append(options);
$select.on('change', function(){
window.location.hash = $(this).val();
});
$(navEl).find('ul').append($select[0]);
};
Subnav.prototype.isAncestor = function(child){
var parent = child.parentNode,
isAncestor = false;
while(parent){
if (parent.id === this.el.id){
isAncestor = true;
break;
}
parent = parent.parentNode;
}
return isAncestor;
};
// --------------------
// PAGE CONTROLLER
function init(){
navigation = new Navigation(navEl);
subnav = new Subnav(subnavEl);
// don't do transition of navigation on narrow screens
if(narrowScreen){
content.classList.add('content-small');
document.body.classList.add('narrowScreen');
// replaced by a non-visible select box (setSelectSubnav)
//subnav.setSubnavHeight(true);
subnav.setSelectSubnav();
}
else{
if(isIPad){
// always use a select as a dropdown on the ipad
subnav.setSelectSubnav();
subnav.updateSubnavView(true);
}else{
// publish resize events for setting subnav visibility
window.addEventListener('resize', throttle(function(){
$.publish('subnavSqueezed', isSubnavSqueezed());
$.publish('windowResized');
} , 1), false);
}
window.addEventListener('scroll', throttle(function(){
$.publish('scrollGtHeader', isScrollGtHeader());
} , 1), false);
}
window.addEventListener('load', function(){
subnav.width = getLinkListWidth(subnav.el);
$.publish('subnavSqueezed', isSubnavSqueezed());
setLogoPosition();
document.body.classList.remove('loading');
});
/* document.body.addEventListener('orientationchange', function(){
subnav.setSubnavHeight(true);
}, false);*/
// Handle scroll between inter-document links.
document.body.addEventListener('click', function (event) {
var hashId = event.target.hash,
anchor = hashId && $qS(hashId);
// replaced by a non-visible select box (setSelectSubnav)
var selectNav = narrowScreen || isIPad;
// open close subnav was clicked
if(getTargetId(hashId) === subnavId) {
event.preventDefault();
if(!selectNav){
subnav.toggle();
}
}
// link within page was clicked
else if (anchor && !selectNav) {
event.preventDefault();
if(subnav.isAncestor(event.target)){
subnav.close();
}
moveToAnchor(anchor);
}
});
// -------
setPermalinkTopOffset();
}
// Initialise after feature detection
if (document.querySelectorAll && document.body.classList) {
init();
}else{
document.body.className = document.body.className.replace( /(?:^|\s)loading(?!\S)/g , '' );
}
})(satya.jQuery, function () { "use strict"; return document.querySelector.apply(document, arguments); });