/********************************************************************** * * XXX add copyright and licence info... * **********************************************************************/ //var DEBUG = DEBUG != null ? DEBUG : true // number of pages to display in ribbon... // NOTE: it is best to keep this odd-ish, so as to give the user the // impession of pages continuing off-screen... var PAGES_IN_RIBBON = 4.0 // if true, expand a page to fit the whole view in single page mode... // NOTE: if one of the NO_RESIZE_CLASSES is set on a page then this // will not have effect on the that page... var FIT_PAGE_TO_VIEW = true // if false, this will force all pages to be fit to screen size in full // page view... var USE_REAL_PAGE_SIZES = true // default page alignment in full view... // supported values: // - 'center' // - 'left' // - 'right' // NOTE: page local align values take priority over this. // NOTE: this has no effect on thumbnail view. // NOTE: this has no effect if FIT_PAGE_TO_VIEW is true and a page has // one of the NO_RESIZE_CLASSES classes set. // also, USE_REAL_PAGE_SIZES if set to false will make this have // no effect. var FULL_PAGE_ALIGN = 'center' // if true will make page resizes after window resize animated... var ANIMATE_WINDOW_RESIZE = true // if false will disable page dragging in single page mode... var DRAG_FULL_PAGE = true // if true this will make each page flip update the hash url... // if false, only direct linking will update the url. // NOTE: this can slow down navigation... var UPDATE_HASH_URL_POSITION = false // if true this will enable history for local page navigation regardless // of weather UPDATE_HASH_URL_POSITION state. // NOTE: UPDATE_HASH_URL_POSITION implicitly enables full browser history // based navigation. // NOTE: this, if enabled, can slow down navigation... // NOTE: partial history navigation over links will still work. var FULL_HISTORY_ENABLED = false // if true, use CSS3 transforms to scroll, of false, use left. var USE_TRANSFORM = true var SCROLL_TIME = 400 var BOUNCE_LENGTH = 10 var BOUNCE_TIME_DIVIDER = 5 // list of css classes of pages that will not allow page fitting. var NO_RESIZE_CLASSES = [ 'no-resize', 'page-align-left', 'page-align-center', 'page-align-right', 'caption-top-arrow', 'caption-bottom-arrow' ] // NOTE: to alter the following, just update the NO_RESIZE_CLASSES above... var RESIZABLE_PAGES = '.page' + ($(NO_RESIZE_CLASSES) .map(function(_, e){ return ':not(.' + e + ')' }) .toArray() .join('')) var NON_RESIZABLE_PAGES = $(NO_RESIZE_CLASSES) .map(function(_, e){ return '.page.' + e + '' }) .toArray() .join(', ') var SCREEN_SIZED_PAGE_CLASSES = [ 'screen-size', 'image-fit', ] var SCREEN_SIZED_PAGES = $(SCREEN_SIZED_PAGE_CLASSES) .map(function(_, e){ return '.page.' + e + '' }) .toArray() .join(', ') /*********************************************************** modes ***/ // toggles .dragging CSS class on the viewer while dragging is in // progress. // NOTE: this is used mostly for styling and drag assisting... var togglePageDragging = createCSSClassToggler('.viewer', 'dragging') // toggle global editor mode... var toggleEditorMode = createCSSClassToggler('.viewer', 'editor-mode') // toggles the editor mode, used for inline magazine editor... var toggleInlineEditorMode = createCSSClassToggler('.viewer', 'inline-editor-mode noSwipe') // toggle between viewer themes... var toggleThemes = createCSSClassToggler('.viewer', [ 'light-viewer', // this is the default (no class set)... 'none', 'dark-viewer' ]) // toggle box-shadows, this is here mostly for performance reasons... var toggleShadows = createCSSClassToggler('.viewer', ['none', 'shadowless']) // this is here only for speed, helps with dragging... // DO NOT USE DIRECTLY! var _PAGE_VIEW // toggle between the two main modes: // - single page mode (.page-view-mode) // - thumbnail/ribbon mode var togglePageView = createCSSClassToggler( '.chrome', 'page-view-mode', // post-change callback... function(action){ if(action == 'on'){ fitNPages(1, !FIT_PAGE_TO_VIEW) MagazineScroller.options.momentum = false _PAGE_VIEW = true } else { fitNPages(PAGES_IN_RIBBON) MagazineScroller.options.momentum = true _PAGE_VIEW = false } }) // this will simply update the current view... function updateView(){ return togglePageView(togglePageView('?')) } /********************************************************* helpers ***/ function centeredPageNumber(){ var scale = getMagazineScale() var o = -$($('.magazine')[0]).offset().left - $('.viewer').offset().left var W = $('.viewer').width() var cur = -1 var res = $('.page').map(function(i, e){ e = $(e) var l = e.position().left var w = e.width()*scale return Math.abs((l+(w/2)) - (o+(W/2))) }).toArray() cur = res.indexOf(Math.min.apply(Math, res)) return cur } function centeredPage(){ return $('.page').eq(centeredPageNumber()) } function visiblePages(partial){ var W = $('.viewer').width() var scale = getMagazineScale() return $('.page').filter(function(_, page){ page = $(page) // XXX this calculates the offset from the document rather than the magazine... var o = page.offset().left // page out of view (right)... if(o >= W){ return false } var w = page.width() * scale if(o < 0){ // partial left... if(partial && o + w > 0){ return true } // page out of view (left)... return false } // partial right... if(partial && W - o < w){ return true } // page compleately in view... if(W - o >= w){ return true } // NOTE: we should not get here but just in case... return false }) } function isPageResizable(page){ if(page == null){ page = $('.current.page') } else { page = $(page) } if(!USE_REAL_PAGE_SIZES){ return false } var mag = $('.magazine') var group = page.parents('.group').first() var article = page.parents('.article').first() // first check the page... // NOTE: check for all classes that make the page unresizable... return ($(NO_RESIZE_CLASSES) .filter(function(){ return page.hasClass(this) }) .length > 0 ? false // then the group... : group.hasClass('no-resize') ? false // then the article... : article.hasClass('no-resize') ? false // then the magazine... : mag.hasClass('no-resize') ? false // now for default... : true) } // this will get the current active alignment... // NOTE: align can be set for: // - page // - article // - magazine // - app (via. FULL_PAGE_ALIGN) // NOTE: the more local setting takes priority over the more general. function getPageAlign(page){ if(page == null){ page = $('.current.page') } else { page = $(page) } var mag = $('.magazine') var group = page.parents('.group').first() var article = page.parents('.article').first() // first check the page... return (page.hasClass('page-align-center') ? 'center' : page.hasClass('page-align-left') ? 'left' : page.hasClass('page-align-right') ? 'right' // page captions... : page.hasClass('caption-top-arrow') ? 'right' : page.hasClass('caption-bottom-arrow') ? 'right' // then the group... : group.hasClass('page-align-center') ? 'center' : group.hasClass('page-align-left') ? 'left' : group.hasClass('page-align-right') ? 'right' // then the article... : article.hasClass('page-align-center') ? 'center' : article.hasClass('page-align-left') ? 'left' : article.hasClass('page-align-right') ? 'right' // then the magazine... : mag.hasClass('page-align-center') ? 'center' : mag.hasClass('page-align-left') ? 'left' : mag.hasClass('page-align-right') ? 'right' // now for the app default... : FULL_PAGE_ALIGN) } // NOTE: this will only produce a title if a magazine is loaded and it // has a title, otherwise undefined is returned.... function getMagazineTitle(){ return ($('.magazine').attr('title') || $('.magazine').attr('name')) } function getMagazineScale(){ return getElementScale($('.magazine')) } function setMagazineScale(scale){ var mag = $('.magazine') var cur = $('.current.page') if(cur.length == 0){ cur = $('.page').first() } // center-align ribbon view pages... var align = togglePageView('?') == 'off' ? 'center' : null //var left = getMagazineOffset(cur, scale, align) //setElementTransform(mag, left, scale) MagazineScroller.zoom(scale) return mag } // NOTE: if page is not given get the current page number... function getPageNumber(page){ // a page/element is given explicitly... if(page != null){ page = $(page) if(!page.hasClass('page')){ page = page.parents('.page') } return $('.page').index(page) } // current page index... if(togglePageView('?') == 'on'){ return $('.page').index($('.current.page')) // get the closest page to view center... // NOTE: this ignores page aligns and only gets the page who's center // is closer to view's center } else { return centeredPageNumber() } } // NOTE: negative values will yield results from the tail... function getPageAt(n){ var page = $('.page') if(n < 0){ n = page.length + n } return $(page).eq(n) } function shiftMagazineTo(offset){ MagazineScroller.scrollTo(offset, 0, 200) //setElementTransform($('.magazine'), offset) } // XXX this is almost the same as getElementScale... function getMagazineShift(){ return getElementShift($('.magazine')).left } function getPageTargetScale(n, fit_to_content){ var view = $('.viewer') var content = $('.content') var W = view.width() var H = view.height() var cW = content.width() var cH = content.height() var scale = { value: getMagazineScale(), width: null, height: null, result_width: cW, valueOf: function(){return this.value}, } if(fit_to_content){ if(USE_REAL_PAGE_SIZES){ scale.width = 'auto' scale.height = 'auto' } else { scale.width = cW scale.height = cH } if(W/H > (cW*n)/cH){ scale.value = H/cH } else { scale.value = W/(cW*n) } // resulting page width... scale.result_width = cW } else { // need to calc width only... if(W/H > (cW*n)/cH){ scale.value = H/cH scale.width = W/scale scale.height = cH // need to calc height only... } else if(W/H > (cW*n)/cH){ scale.value = W/(cW*n) scale.height = H/scale scale.width = cW // set both width and height to defaults (content and page ratios match)... } else { scale.value = W/(cW*n) scale.height = cH scale.width = cW } // resulting page width... scale.result_width = W/scale } return scale } /************************************************** event handlers ***/ // #URL handler... var RELATIVE_URLS = [ 'back', 'forward', 'next', 'prev', 'nextArticle', 'prevArticle', 'nextBookmark', 'prevBookmark', 'bookmark', 'hideLayers' ] // NOTE: most of the handling actually happens in loadURLState... function hashChangeHandler(e){ e.preventDefault() var anchor = window.location.hash.split('#')[1] // skip empty #URL... if(anchor == ''){ return false } var r = loadURLState() var n = getPageNumber() // for relative #URLs remove them from hash... if(RELATIVE_URLS.indexOf(anchor) >= 0 && !UPDATE_HASH_URL_POSITION){ window.location.hash = '' } // if we are dealing with history actions the browser will // do the work for us... if(r == 'back'){ // we shift by 2 to compensate for the back/forward URL itself... window.history.go(-2) } else if(r == 'forward'){ window.history.go(2) } else if(r != n){ /* XXX this will put this into an endless loop... * ...mainly because it changes the #URL from within the handler if(!UPDATE_HASH_URL_POSITION){ // push current position... // NOTE: this will enable partial history navigation, but only // on actions envolving actual links... window.history.pushState(null, null, '#' + getPageNumber()) } */ setCurrentPage(r) } } // window resize event handler... function viewResizeHandler(){ if(ANIMATE_WINDOW_RESIZE){ updateView() } else { unanimated($('.scaler'), updateView, 30)() } } /********************************************************** layout ***/ // mode can be: // - viewer // - content // // XXX should this calculate offset???? function fitPagesTo(mode, cur, time, scale){ mode = mode == null ? 'content' : mode cur = cur == null ? centeredPageNumber() : cur time = time == null ? 0 : time scale = scale == null ? getMagazineScale() : scale // fit to content... if(mode == 'content'){ var target_width = 'auto' var target_height = 'auto' // fit to viewer... } else if(mode == 'viewer'){ var viewer = $('.viewer') var W = viewer.width() var H = viewer.height() // XXX is this a good way to sample content size??? var content = $('.content') var w = content.width() var h = content.height() // need to calc width only... if(W/H > w/h){ s = H/h w = W/s h = h // need to calc height only... } else if(W/H > w/h){ s = W/w h = H/s w = w // set both width and height to defaults (content and page ratios match)... } else { s = W/h h = h w = w } var target_width = w var target_height = h // bad mode... } else { return } var pages = $('.page') var offset = 0 $(RESIZABLE_PAGES) .each(function(_, e){ var E = $(e) var w = target_width == 'auto' ? E.find('.content').width() : target_width var h = target_height == 'auto' ? E.find('.content').height() : target_height // offset... if(pages.index(e) < cur){ offset += (E.width() - w) } // offset half the current page... if(pages.index(e) == cur){ // XXX to be more accurate we can use distance from page // center rather than 1/2... offset += ((E.width() - w)/2) } if(time <= 0){ e.style.width = w e.style.height = h } else { E.animate({ width: w, height: h, }, time, 'linear') } }) //$(NON_RESIZABLE_PAGES).width('auto') $(NON_RESIZABLE_PAGES) .each(function(_, e){ e.style.width = 'auto' e.style.height = 'auto' }) MagazineScroller.scrollBy(offset*scale, 0, time) setTimeout(function(){ MagazineScroller.refresh() }, 0) } // NOTE: if n is not given then it defaults to 1 // NOTE: if n > 1 and fit_to_content is not given it defaults to true // NOTE: if n is 1 then fit_to_content bool argument controls wether: // - the page will be stretched to viewer (false) // - or to content (true) // XXX need to align/focus the corrent page... // case: // when current page is pushed off center by a resized page... function fitNPages(n, fit_to_content){ n = n == null ? 1 : n var scale = getPageTargetScale(n, fit_to_content) if(n == 1){ fitPagesTo('viewer') } else { fitPagesTo('content') } MagazineScroller.zoom(scale) MagazineScroller.refresh() } /********************************************************* actions ***/ // Set the .current class... // // page can be: // - null - centered page in view // - number - page number/index // - page - a page element // - elem - resolves to a containing page element function setCurrent(page){ var pages = $('.page') page = page == null ? pages.eq(centeredPageNumber()) : typeof(page) == typeof(123) ? pages.eq(Math.max(0, Math.min(page, pages.length-1))) : !$(page).eq(0).hasClass('page') ? $(page).eq(0).parents('.page').eq(0) : $(page).eq(0) if(page.hasClass('current')){ return page } // set the class... $('.current.page').removeClass('current') return page.addClass('current') } // Focus a page... // // NOTE: n is a setCurrent(..) compatible value... // NOTE: if n is out of bounds (n < 0 | n >= length) this will focus the // first/last page and bounce... function focusPage(n, align, time){ // XXX the default needs to depend on the scale... align = align == null ? 'auto' : align time = time == null ? SCROLL_TIME : time var page = setCurrent(n) var pages = $('.page') align = align == 'auto' ? getPageAlign(page) : align // magazine offset... var m = page.position().left // base value for 'left' align... var o = 0 var w = page.width() * getMagazineScale() var W = $('.viewer').width() if(align != 'left'){ // right... if(align == 'right'){ o = W - w // center / default... } else { o = W/2 - w/2 } } // compensate for first/last page align to screen (optional???)... var offset = page.offset().left var f = pages.first().offset().left if(f + o - offset >= 0){ o = 0 m = 0 } var last = $('.page').last() var l = last.offset().left if(l + o - offset <= W - w){ o = 0 m = last.position().left + last.width()*getMagazineScale() - W } // do the bounce... if(time > 0){ var i = pages.index(page) var l = pages.length if(n < 0){ o += BOUNCE_LENGTH*getMagazineScale() time /= BOUNCE_TIME_DIVIDER } if(n >= l){ o -= BOUNCE_LENGTH*getMagazineScale() time /= BOUNCE_TIME_DIVIDER } } // NOTE: this does not care about the zoom... MagazineScroller.scrollTo(-m + o, 0, time) return page } // Focus first/last page... // // NOTE: if we are already at the first/last page, do a bounce... function first(align){ // visually show that we are at the start... if($('.magazine').offset().left >= 0){ return focusPage(-1, align) } return focusPage(0, align) } function last(align){ var mag = $('.magazine') var l = mag.offset().left var end = mag.offset().left + mag.width()*getMagazineScale() var i = $('.page').length-1 if(end <= $('.viewer').width()+1){ return focusPage(i+1, align) } return focusPage(i, align) } // Focus a page of class cls adjacent to current in direction... // // direction can be: // - 'next' - next page // - 'prev' - previous page // // If cls is not given, then all pages (.page) are considered. // // NOTE: if we are already at the first/last page and direction is // prev/next resp. then do a bounce... function step(direction, cls, align){ cls = cls == null ? '' : cls var page = visiblePages(true).filter('.current').eq(0) var pages = $('.page') if(page.length == 0){ page = setCurrent() } var i = pages.index(page) var l = pages.length // if we are at the first/last page do the bounce dance... // bounce first... if(i == 0 && direction == 'prev'){ return focusPage(-1, align) } // bounce last... if(i == l-1 && direction == 'next'){ return focusPage(l, align) } var to = page[direction+'All']('.page'+cls) // if we have no pages on the same level, to a deeper search... if(to.length == 0){ if(direction == 'next'){ to = pages.slice(i+1).filter('.page'+cls).first() } else { to = pages.slice(0, i).filter('.page'+cls).last() } } // still no candidates, then we can't do a thing... if(to.length == 0){ to = page } return focusPage(to.eq(0), align) } // Focus next/prev page shorthands... // function nextPage(cls, align){ return step('next', cls, align) } function prevPage(cls, align){ return step('prev', cls, align) } // Focus next/prev cover page shorthands... // function nextCover(cls, align){ cls = cls == null ? '' : cls return step('next', '.cover'+cls, align) } function prevCover(cls, align){ cls = cls == null ? '' : cls return step('prev', '.cover'+cls, align) } // Move the view a screen width (.viewer) left/right... // // NOTE: if we are at magazine start/end and try to move left/right resp. // this will do a bounce... function nextScreen(time){ time = time == null ? SCROLL_TIME : time var W = $('.viewer').width() var mag = $('.magazine') var o = mag.position().left var w = mag.width()*getMagazineScale() // we reached the end... if(w + o < 2*W){ // NOTE: we use focusPage(..) to handle stuff like bounces... return focusPage($('.page').length) } MagazineScroller.scrollTo(o-W, 0, time) return setCurrent() } function prevScreen(time){ time = time == null ? SCROLL_TIME : time var W = $('.viewer').width() var o = $('.magazine').position().left // we reached the start... if(-o < W){ // NOTE: we use focusPage(..) to handle stuff like bounces... return focusPage(-1) } MagazineScroller.scrollTo(o+W, 0, time) return setCurrent() } // Mode-aware next/prev high-level actions... // // Supported modes: // - page view - focus next/prev page // - magazine view - view next/prev screen // function next(){ if(togglePageView('?') == 'on'){ return nextPage() } else { return nextScreen() } } function prev(){ if(togglePageView('?') == 'on'){ return prevPage() } else { return prevScreen() } } /******************************************************* bookmarks ***/ // setup bookmarking active zones in page... function setupBookmarkTouchZones(){ $('.bookmark-toggler').remove() var page = $('.page') page.each(function(i, e){ $('
') .prependTo($(e)) .addClass('bookmark-toggler') .attr({ title: 'Toggle bookmark (B)' }) .on('click', function(){ toggleBookmark($(e)) }) /* .swipe({ click: function(){ toggleBookmark($(e)) } }) */ }) } // load bookmarks from list... function loadBookmarks(lst){ clearBookmarks() // setup/set bookmarks... $(lst).each(function(i, e){toggleBookmark(e)}) } // build bookmark list... function buildBookmarkList(){ var res = [] $('.magazine .page .bookmark').each(function(_, e){ res.push(getPageNumber($(e).parents('.page'))) }) return res } // NOTE: will trigger 'bookmarksCleared' event on the viewer function clearBookmarks(){ $('.magazine .page .bookmark').remove() $('.viewer').trigger('bookmarksCleared') } // NOTE: this will trigger the folowing events on the viewer: // - bookmarkAdded(n) // - bookmarkRemoved(n) // XXX rewrite... function toggleBookmark(n){ if(n == null){ n = getPageNumber() } else if(typeof(n) != typeof(1)){ n = getPageNumber(n) } var res var cur = getPageAt(n) if(cur.children('.bookmark').length == 0){ var res = $('