var $ = require('jquery');
var url = require('url');
var loading = require('./loading');
var platform = require('./platform');
var gitbook = window.gitbook;
var usePushState = (typeof history.pushState !== 'undefined');
/*
Get current scroller element
*/
function getScroller() {
if (platform.isSmallScreen()) {
return $('.book-body');
} else {
return $('.body-inner');
}
}
/*
Scroll to a specific hash tag in the content
*/
function scrollToHash(hash) {
var $scroller = getScroller(),
dest = 0;
if (hash) {
dest = getElementTopPosition(hash);
}
// Unbind scroll detection
$scroller.unbind('scroll');
$scroller.animate({
scrollTop: dest
}, 800, 'swing', function() {
// Reset scroll binding when finished
$scroller.scroll(handleScrolling);
});
// Directly set chapter as active
setChapterActive(null, hash);
}
/*
Return the top position of an element
*/
function getElementTopPosition(id) {
// Get actual position of element if nested
var $scroller = getScroller(),
$container = $scroller.find('.page-inner'),
$el = $scroller.find(id),
$parent = $el.offsetParent(),
dest = 0;
dest = $el.position().top;
while (!$parent.is($container)) {
$el = $parent;
dest += $el.position().top;
$parent = $el.offsetParent();
}
// Return rounded value since
// jQuery scrollTop() returns an integer
return Math.floor(dest);
}
/*
Handle updating summary at scrolling
*/
var $chapters,
$activeChapter;
// Set a chapter as active in summary and update state
function setChapterActive($chapter, hash) {
// No chapter and no hash means first chapter
if (!$chapter && !hash) {
$chapter = $chapters.first();
}
// If hash is provided, set as active chapter
if (!!hash) {
$chapter = $chapters.filter(function() {
var titleId = getChapterHash($(this));
return titleId == hash;
}).first();
}
// Don't update current chapter
if ($chapter.is($activeChapter)) {
return;
}
// Update current active chapter
$activeChapter = $chapter;
// Add class to selected chapter
$chapters.removeClass('active');
$chapter.addClass('active');
// Update history state if needed
hash = getChapterHash($chapter);
var oldUri = window.location.pathname + window.location.hash,
uri = window.location.pathname + hash;
if (uri != oldUri) {
history.replaceState({ path: uri }, null, uri);
}
}
// Return the hash of link for a chapter
function getChapterHash($chapter) {
var $link = $chapter.children('a'),
hash = $link.attr('href').split('#')[1];
if (hash) hash = '#'+hash;
return (!!hash)? hash : '';
}
// Handle user scrolling
function handleScrolling() {
// Get current page scroll
var $scroller = getScroller(),
scrollTop = $scroller.scrollTop(),
scrollHeight = $scroller.prop('scrollHeight'),
clientHeight = $scroller.prop('clientHeight'),
nbChapters = $chapters.length,
$chapter = null;
// Find each title position in reverse order
$($chapters.get().reverse()).each(function(index) {
var titleId = getChapterHash($(this)),
titleTop;
if (!!titleId && !$chapter) {
titleTop = getElementTopPosition(titleId);
// Set current chapter as active if scroller passed it
if (scrollTop >= titleTop) {
$chapter = $(this);
}
}
// If no active chapter when reaching first chapter, set it as active
if (index == (nbChapters - 1) && !$chapter) {
$chapter = $(this);
}
});
// ScrollTop is at 0, set first chapter anyway
if (!$chapter && !scrollTop) {
$chapter = $chapters.first();
}
// Set last chapter as active if scrolled to bottom of page
if (!!scrollTop && (scrollHeight - scrollTop == clientHeight)) {
$chapter = $chapters.last();
}
setChapterActive($chapter);
}
/*
Handle a change of url withotu refresh the whole page
*/
var prevUri = location.href;
function handleNavigation(relativeUrl, push) {
var prevUriParsed = url.parse(prevUri);
var uri = url.resolve(window.location.pathname, relativeUrl);
var uriParsed = url.parse(uri);
var hash = uriParsed.hash;
// Is it the same url (just hash changed?)
var pathHasChanged = (uriParsed.pathname !== prevUriParsed.pathname);
// Is it an absolute url
var isAbsolute = Boolean(uriParsed.hostname);
if (!usePushState || isAbsolute) {
// Refresh the page to the new URL if pushState not supported
location.href = relativeUrl;
return;
}
// Don't fetch same page
if (!pathHasChanged) {
if (push) history.pushState({ path: uri }, null, uri);
return scrollToHash(hash);
}
prevUri = uri;
var promise = $.Deferred(function(deferred) {
$.ajax({
type: 'GET',
url: uri,
cache: true,
headers:{
'Access-Control-Expose-Headers': 'X-Current-Location'
},
success: function(html, status, xhr) {
// For GitBook.com, we handle redirection signaled by the server
var responseURL = xhr.getResponseHeader('X-Current-Location') || uri;
// Replace html content
html = html.replace( /<(\/?)(html|head|body)([^>]*)>/ig, function(a,b,c,d){
return '<' + b + 'div' + ( b ? '' : ' data-element="' + c + '"' ) + d + '>';
});
var $page = $(html),
$pageBody = $page.find('.book'),
$pageHead;
// We only use history.pushState for pages generated with GitBook
if ($pageBody.length === 0) {
var err = new Error('Invalid gitbook page, redirecting...');
return deferred.reject(err);
}
// Push url to history
if (push) {
history.pushState({
path: responseURL
}, null, responseURL);
}
// Force reparsing HTML to prevent wrong URLs in Safari
$page = $(html);
$pageHead = $page.find('[data-element=head]');
$pageBody = $page.find('.book');
// Merge heads
// !! Warning !!: we only update necessary portions to avoid strange behavior (page flickering etc ...)
// Update title
document.title = $pageHead.find('title').text();
// Reference to $('head');
var $head = $('head');
// Update next & prev tags
// Remove old
$head.find('link[rel=prev]').remove();
$head.find('link[rel=next]').remove();
// Add new next * prev tags
$head.append($pageHead.find('link[rel=prev]'));
$head.append($pageHead.find('link[rel=next]'));
// Merge body
var bodyClass = $('.book').attr('class');
var scrollPosition = $('.book-summary').scrollTop();
$pageBody.toggleClass('with-summary', $('.book').hasClass('with-summary'));
$('.book').replaceWith($pageBody);
$('.book').attr('class', bodyClass);
$('.book-summary').scrollTop(scrollPosition);
// Update state
gitbook.state.$book = $('.book');
preparePage(!hash);
// Scroll to hashtag position
if (hash) {
scrollToHash(hash);
}
deferred.resolve();
}
});
}).promise();
return loading.show(
promise
.fail(function (e) {
console.log(e); // eslint-disable-line no-console
// location.href = relativeUrl;
})
);
}
function updateNavigationPosition() {
var bodyInnerWidth, pageWrapperWidth;
bodyInnerWidth = parseInt($('.body-inner').css('width'), 10);
pageWrapperWidth = parseInt($('.page-wrapper').css('width'), 10);
$('.navigation-next').css('margin-right', (bodyInnerWidth - pageWrapperWidth) + 'px');
// Reset scroll to get current scroller
var $scroller = getScroller();
// Unbind existing scroll event
$scroller.unbind('scroll');
$scroller.scroll(handleScrolling);
}
function preparePage(resetScroll) {
var $bookBody = $('.book-body');
var $bookInner = $bookBody.find('.body-inner');
var $pageWrapper = $bookInner.find('.page-wrapper');
// Update navigation position
updateNavigationPosition();
// Focus on content
$pageWrapper.focus();
// Reset scroll
if (resetScroll !== false) $bookInner.scrollTop(0);
$bookBody.scrollTop(0);
// Get current page summary chapters
$chapters = $('.book-summary .summary .chapter')
.filter(function() {
var $link = $(this).children('a'),
href = null;
// Chapter doesn't have a link
if (!$link.length) {
return false;
}
else {
href = $link.attr('href').split('#')[0];
}
var resolvedRef = url.resolve(window.location.pathname, href);
return window.location.pathname == resolvedRef;
});
// Bind scrolling if summary contains more than one link to this page
var $scroller = getScroller();
if ($chapters.length > 1) {
$scroller.scroll(handleScrolling);
}
}
function isLeftClickEvent(e) {
return e.button === 0;
}
function isModifiedEvent(e) {
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
}
/*
Handle click on a link
*/
function handleLinkClick(e) {
var $this = $(this);
var target = $this.attr('target');
if (isModifiedEvent(e) || !isLeftClickEvent(e) || target) {
return;
}
e.stopPropagation();
e.preventDefault();
var url = $this.attr('href');
if (url) handleNavigation(url, true);
}
function goNext() {
var url = $('.navigation-next').attr('href');
if (url) handleNavigation(url, true);
}
function goPrev() {
var url = $('.navigation-prev').attr('href');
if (url) handleNavigation(url, true);
}
function init() {
// Prevent cache so that using the back button works
// See: http://stackoverflow.com/a/15805399/983070
$.ajaxSetup({
cache: false
});
// Recreate first page when the page loads.
history.replaceState({ path: window.location.href }, '');
// Back Button Hijacking :(
window.onpopstate = function (event) {
if (event.state === null) {
return;
}
return handleNavigation(event.state.path, false);
};
$(document).on('click', '.navigation-prev', handleLinkClick);
$(document).on('click', '.navigation-next', handleLinkClick);
$(document).on('click', '.summary [data-path] a', handleLinkClick);
$(document).on('click', '.page-inner a', handleLinkClick);
$(window).resize(updateNavigationPosition);
// Prepare current page
preparePage();
}
module.exports = {
init: init,
goNext: goNext,
goPrev: goPrev
};