/* * jbooklet jquery plugin * copyright (c) 2014 eugene zlobin (http://zlobin.pro/zlobin_eng.html) * * licensed under the mit license (http://www.opensource.org/licenses/mit-license.php) * * version : 2.1.0 * * originally based on the work of: * 1) charles mangin (http://clickheredammit.com/pageflip/) * 2) william grauvogel (http://builtbywill.com/) */ (function (window, $, undefined) { 'use strict'; $.fn.booklet = function(options) { var $el = $(this), result = [], method, output, config, args = array.prototype.slice.call(arguments, 1); // option type string - api call if (typeof options === 'string') { $el.each(function() { var obj = $el.data('jbooklet'); if (obj) { method = options; if (obj[method]) { output = obj[method].apply(obj, args); if (output !== undefined || output) { result.push(obj[method].apply(obj, args)); } } else { $.error('method "' + method + '" does not exist on jquery.booklet.'); } } else { $.error('jquery.booklet has not been initialized. method "' + options + '" cannot be called.'); } }); if (result.length === 1) { return result[0]; } else if (result.length > 0) { return result; } else { return $el; } } else if (typeof method === 'object' || !method) { // else build new booklet return $el.each(function() { var $element = $(this); var obj = $el.data('jbooklet'); config = $.extend({}, $.fn.booklet.defaults, options); // destroy old booklet before creating new one if (obj) { obj.destroy(); } // instantiate the booklet obj = new booklet($element, config); obj.init(); return this; }); } }; function booklet(target, inoptions) { var $wrapper = $('
', { class: 'b-page' }); var $underwrapper = $('
', { class: 'b-wrap' }); $underwrapper.appendto($wrapper); var options = inoptions, isinit = false, isbusy = false, isplaying = false, ishoveringright = false, ishoveringleft = false, templates = { //book page with no content empty: '
', //transparent item used with closed books blank: '
' }, css = {}, anim = {}, hovershadowwidth, hoverfullwidth, hovercurlwidth, pages = [], diff, originalpagetotal, startingpagenumber, // page content vars pn, p0, p1, p2, p3, p4, pnwrap, p0wrap, p1wrap, p2wrap, p3wrap, p4wrap, wraps, // control vars p3drag, p0drag, wpercent, worig, hpercent, horig, pwidth, pwidthn, pwidthh, pheight, speedh, page = function ($contentnode, index) { var $el = $wrapper.clone(); var $wrap = $el.find('.b-wrap'); $el.addclass('b-page-' + index); if (!$contentnode.hasclass('b-page-empty')) { if (index % 2 !== 0) { $wrap.addclass('b-wrap-right'); } else { $wrap.addclass('b-wrap-left'); } } $contentnode.appendto($wrap); return { index: index, contentnode: $contentnode[0], pagenode: $el[0] }; }, init = function () { target.addclass('booklet'); // store data for api calls target.data('jbooklet', this); // save original number of pages originalpagetotal = target.children().length; options.currentindex = 0; if (originalpagetotal > 500) { options.manual = false; } // generate page markup initpages(); // initialize options updateoptions(); // update after initialized updatepages(); isinit = true; }, destroy = function () { // destroy all booklet items destroycontrols(); destroypages(); target.removeclass('booklet').removedata('booklet'); isinit = false; }, initpages = function () { var nodes = []; var children = target.children(); var length = target.children().length; var newpage; var i; pages = []; // fix for odd number of pages if ((length % 2) !== 0) { children.last().after(templates.blank); } // set total page count options.pagetotal = length; startingpagenumber = 0; if (!isinit) { options.currentindex = 0; if (!isnan(options.startingpage) && options.startingpage <= options.pagetotal && options.startingpage > 0) { if ((options.startingpage % 2) !== 0) { options.startingpage--; } options.currentindex = options.startingpage; } } // load pages for (i = 0; i < length; i++) { newpage = new page($(children[i]), i); nodes.push(newpage.pagenode); pages.push(newpage); } target.append(nodes); }, updatepages = function () { updatepagestructure(); updatepagecss(); updatemanualcontrols(); }, updatepagestructure = function () { var currindex = options.currentindex; // reset all content target.find('.b-page').removeclass('b-pn b-p0 b-p1 b-p2 b-p3 b-p4').hide(); // add page classes if (currindex - 2 >= 0) { target.find('.b-page-' + (currindex - 2)).addclass('b-pn').show(); target.find('.b-page-' + (currindex - 1)).addclass('b-p0').show(); } target.find('.b-page-' + (currindex)).addclass('b-p1').show(); target.find('.b-page-' + (currindex + 1)).addclass('b-p2').show(); if (currindex + 3 <= options.pagetotal) { target.find('.b-page-' + (currindex + 2)).addclass('b-p3').show(); target.find('.b-page-' + (currindex + 3)).addclass('b-p4').show(); } // save structure elems to vars pn = target.find('.b-pn'); p0 = target.find('.b-p0'); p1 = target.find('.b-p1'); p2 = target.find('.b-p2'); p3 = target.find('.b-p3'); p4 = target.find('.b-p4'); pnwrap = pn.find('.b-wrap'); p0wrap = p0.find('.b-wrap'); p1wrap = p1.find('.b-wrap'); p2wrap = p2.find('.b-wrap'); p3wrap = p3.find('.b-wrap'); p4wrap = p4.find('.b-wrap'); wraps = target.find('.b-wrap'); }, updatepagecss = function () { wraps.css(css.wrap); p0wrap.css(css.p0wrap); p1.css(css.p1); p2.css(css.p2); pn.css(css.pn); p0.css(css.p0); p3.stop().css(css.p3); p4.css(css.p4); target.width(options.width); }, destroypages = function () { var bwrap = target.find('.b-wrap'); // remove booklet markup bwrap.unwrap(); bwrap.children().unwrap(); target.find('.b-counter, .b-page-blank, .b-page-empty').remove(); }, /////////////////////////////////////////////////////////////////////// // option / control functions /////////////////////////////////////////////////////////////////////// updateoptions = function (newoptions) { var didupdate = false; var parent = target.parent(); var opwidth = options.width; var opheight = options.height; // update options if newoptions have been passed in if (newoptions !== null && newoptions !== undefined) { // remove page structure, revert to original order destroypages(); destroycontrols(); options = $.extend({}, options, newoptions); didupdate = true; initpages(); } // set width. if (opwidth && typeof opwidth === 'string') { if (opwidth.indexof('px') !== -1) { options.width = opwidth.replace('px', ''); } else if (opwidth.indexof('%') !== -1) { wpercent = true; worig = opwidth; options.width = parsefloat((opwidth.replace('%', '') / 100) * parent.width()); } } // set height. if (opheight && typeof opheight === 'string') { if (opheight.indexof('px') !== -1) { options.height = opheight.replace('px', ''); } else if (opheight.indexof('%') !== -1) { hpercent = true; horig = opheight; options.height = parsefloat((opheight.replace('%', '') / 100) * parent.height()); } } //target.width(options.width); //target.height(options.height); // save page sizes and other vars pwidth = options.width / 2; pwidthn = '-' + pwidth + 'px'; pwidthh = pwidth / 2; pheight = options.height; speedh = options.speed / 2; // set total page count options.pagetotal = target.children('.b-page').length; // update all css, as sizes may have changed updatecssandanimations(); if (isinit) { updatepages(); } // percentage resizing $(window).on('resize.booklet', function () { if ((wpercent || hpercent)) { updatepercentagesize(); } }); isplaying = false; // if options were updated force pages, controls and menu to update if (didupdate) { updatepages(); } }, updatecssandanimations = function () { // init base css css = { wrap: { left: 0, width: pwidth - (options.pagepadding * 2), height: pheight - (options.pagepadding * 2), padding: options.pagepadding }, p0wrap: { right: 0, left: 'auto' }, p1: { left: 0, width: pwidth, height: pheight }, p2: { left: pwidth, width: pwidth, opacity: 1, height: pheight }, pn: { left: 0, width: pwidth, height: pheight }, p0: { left: 0, width: 0, height: pheight }, p3: { left: pwidth * 2, width: 0, height: pheight, paddingleft: 0 }, p4: { left: pwidth, width: pwidth, height: pheight } }; hovershadowwidth = 10; hoverfullwidth = options.hoverwidth + hovershadowwidth; hovercurlwidth = (options.hoverwidth / 2) + hovershadowwidth; // init animation params anim = { hover: { speed: options.hoverspeed, size: options.hoverwidth, p2: { width: pwidth - hovercurlwidth }, p3: { left: options.width - hoverfullwidth, width: hovercurlwidth }, p3closed: { left: pwidth - options.hoverwidth, width: hovercurlwidth }, p3wrap: { left: hovershadowwidth }, p2end: { width: pwidth }, p2closedend: { width: pwidth, left: 0 }, p3end: { left: options.width, width: 0 }, p3closedend: { left: pwidth, width: 0 }, p3wrapend: { left: 10 }, p1: { left: hovercurlwidth, width: pwidth - hovercurlwidth }, p1wrap: { left: '-' + hovercurlwidth + 'px' }, p0: { left: hovercurlwidth, width: hovercurlwidth }, p0wrap: { right: hovershadowwidth }, p1end: { left: 0, width: pwidth }, p1wrapend: { left: 0 }, p0end: { left: 0, width: 0 }, p0wrapend: { right: 0 } }, // forward p2: { width: 0 }, p2closed: { width: 0, left: pwidth }, p4closed: { left: pwidth }, p3in: { left: pwidthh, width: pwidthh, paddingleft: options.shadowbtmwidth }, p3indrag: { left: pwidth / 4, width: pwidth * 0.75, paddingleft: options.shadowbtmwidth }, p3out: { left: 0, width: pwidth, paddingleft: 0 }, p3wrapin: { left: options.shadowbtmwidth }, p3wrapout: { left: 0 }, // backwards p1: { left: pwidth, width: 0 }, p1wrap: { left: pwidthn }, p0: { left: pwidth, width: pwidth }, p0in: { left: pwidthh, width: pwidthh }, p0out: { left: pwidth, width: pwidth }, p0outclosed: { left: 0, width: pwidth }, p2back: { left: 0 }, p0wrapdrag: { right: 0 }, p0wrapin: { right: options.shadowbtmwidth }, p0wrapout: { right: 0 } }; }, updatepercentagesize = function () { var parent = target.parent(); // recalculate size for percentage values, called with window is resized if (wpercent) { options.width = parsefloat((worig.replace('%', '') / 100) * parent.width()); target.width(options.width); pwidth = options.width / 2; pwidthn = '-' + pwidth + 'px'; pwidthh = pwidth / 2; } if (hpercent) { options.height = parsefloat((horig.replace('%', '') / 100) * parent.height()); target.height(options.height); pheight = options.height; } updatecssandanimations(); updatepagecss(); }, updatemanualcontrols = function () { var origx, newx, diff, fullpercent, shadowpercent, shadoww, curlw, underw, curlleft, p1wrapleft, bpage = target.find('.b-page'); ishoveringright = ishoveringleft = p3drag = p0drag = false; if ($.ui && options.manual) { // manual page turning, check if jquery ui is loaded if (bpage.draggable()) { bpage.draggable('destroy').removeclass('b-grab b-grabbing'); } // implement draggable forward p3.draggable({ axis: 'x', containment: [ target.offset().left, 0, p2.offset().left + pwidth - hoverfullwidth, pheight ], drag: function (event, ui) { p3drag = true; p3.removeclass('b-grab').addclass('b-grabbing'); // calculate positions origx = ui.originalposition.left; newx = ui.position.left; diff = origx - newx; fullpercent = diff / origx; shadowpercent = fullpercent < 0.5 ? fullpercent : (1 - fullpercent); shadoww = (shadowpercent * options.shadowbtmwidth * 2) + hovershadowwidth; shadoww = diff / origx >= 0.5 ? shadoww -= hovershadowwidth : shadoww; // set top page curl width curlw = hovercurlwidth + diff / 2; curlw = curlw > pwidth ? pwidth : curlw; // constrain max width // set bottom page width, hide underw = pwidth - curlw; // set values p3.width(curlw); p3wrap.css({left: shadoww}); p2.width(underw); }, stop: function () { endhoveranimation(false); if (fullpercent > options.hoverthreshold) { next(); p3.removeclass('b-grab b-grabbing'); } else { p3drag = false; p3.removeclass('b-grabbing').addclass('b-grab'); } } }); // implement draggable backwards p0.draggable({ axis: 'x', //containment: 'parent', containment: [ target.offset().left + hovercurlwidth, 0, target.offset().left + options.width, pheight ], drag: function (event, ui) { p0drag = true; p0.removeclass('b-grab').addclass('b-grabbing'); // calculate positions origx = ui.originalposition.left; newx = ui.position.left; diff = newx - origx; fullpercent = diff / (options.width - origx); if (fullpercent > 1) { fullpercent = 1; } shadowpercent = fullpercent < 0.5 ? fullpercent : (1 - fullpercent); shadoww = (shadowpercent * options.shadowbtmwidth * 2) + hovershadowwidth; shadoww = diff / origx >= 0.5 ? shadoww -= hovershadowwidth : shadoww; curlw = fullpercent * (pwidth - hovercurlwidth) + hovercurlwidth + shadoww; curlleft = curlw - shadoww; p1wrapleft = -curlleft; // set values ui.position.left = curlleft; p0.css({width: curlw}); p0wrap.css({right: shadoww}); p1.css({left: curlleft, width: pwidth - curlleft}); p1wrap.css({left: p1wrapleft}); }, stop: function () { endhoveranimation(true); if (fullpercent > options.hoverthreshold) { prev(); p0.removeclass('b-grab b-grabbing'); } else { p0drag = false; p0 .removeclass('b-grabbing') .addclass('b-grab'); } } }); bpage.off('click.booklet'); if (options.hoverclick) { target.find('.b-pn, .b-p0').on('click.booklet', prev).css({cursor: 'pointer'}); target.find('.b-p3, .b-p4').on('click.booklet', next).css({cursor: 'pointer'}); } // mouse tracking for page movement target.off('mousemove.booklet').on('mousemove.booklet', function (e) { diff = e.pagex - target.offset().left; if (diff < anim.hover.size) { starthoveranimation(false); } else if (diff > anim.hover.size && diff <= options.width - anim.hover.size) { endhoveranimation(false); endhoveranimation(true); } else if (diff > options.width - anim.hover.size) { starthoveranimation(true); } }).off('mouseleave.booklet').on('mouseleave.booklet', function () { endhoveranimation(false); endhoveranimation(true); }); } }, destroycontrols = function () { destroymanualcontrols(); }, destroymanualcontrols = function () { if ($.ui) { // remove old draggables if (target.find('.b-page').draggable()) { target.find('.b-page').draggable('destroy').removeclass('b-grab b-grabbing'); } } // remove mouse tracking for page movement target.off('.booklet'); }, /* -------------------- pages -------------------- */ addpage = function (index, html) { // validate inputs if (index === 'first') { index = 0; } else if (index === 'last') { index = originalpagetotal; } else if (typeof index === 'number') { if (index < 0 || index > originalpagetotal) { return; } } else if (index === undefined) { return; } if (html === undefined || html === '') { return; } // remove page structure, revert to original order destroypages(); destroycontrols(); // add new page if (index === originalpagetotal) { //end of book target.children(':eq(' + (index - 1) + ')').after(html); } else { target.children(':eq(' + index + ')').before(html); } originalpagetotal = target.children().length; // recall initialize functions initpages(); updateoptions(); updatepages(); }, removepage = function (index) { var removedpage; // validate inputs if (index === 'start') { index = 0; } else if (index === 'end') { index = originalpagetotal; } else if (typeof index === 'number') { if (index < 0 || index > originalpagetotal) { return; } } else if (index === undefined) { return; } // stop if removing last remaining page if (target.children('.b-page').length === 2 && target.find('.b-page-blank').length > 0) { return; } // remove page structure, revert to original order destroypages(); destroycontrols(); if (index >= options.currentindex) { if (index > 0 && (index % 2) !== 0) { options.currentindex -= 2; } if (options.currentindex < 0) { options.currentindex = 0; } } // remove page if (index === originalpagetotal) { // end of book removedpage = target.children(':eq(' + (index - 1) + ')').remove(); } else { removedpage = target.children(':eq(' + index + ')').remove(); } originalpagetotal = target.children().length; removedpage = null; // recall initialize functions initpages(); updatepages(); updateoptions(); }, /* -------------------- navigation -------------------- */ next = function () { if (!isbusy) { if (isplaying && options.currentindex + 2 >= options.pagetotal) { gotopage(0); } else { gotopage(options.currentindex + 2); } } }, prev = function () { if (!isbusy) { if (isplaying && options.currentindex - 2 < 0) { gotopage(options.pagetotal - 2); } else { gotopage(options.currentindex - 2); } } }, gotopage = function (newindex) { var speed; if (newindex < options.pagetotal && newindex >= 0 && !isbusy) { // moving forward (increasing number) if (newindex > options.currentindex) { isbusy = true; diff = newindex - options.currentindex; options.currentindex = newindex; options.movingforward = true; // set animation speed, depending if user dragged any distance or not speed = p3drag === true ? options.speed * (p3.width() / pwidth) : speedh; startpageanimation(diff, true, speed); // hide p2 as p3 moves across it p2.stop().animate(anim.p2, speed, p3drag === true ? options.easeout : options.easein); // if animating after a manual drag, calculate new speed and animate out if (p3drag) { p3.animate(anim.p3out, speed, options.easeout); p3wrap.animate(anim.p3wrapout, speed, options.easeout, function () { updateafter(); }); } else { p3.stop() .animate(anim.p3in, speed, options.easein) .animate(anim.p3out, speed, options.easeout); p3wrap .animate(anim.p3wrapin, speed, options.easein) .animate(anim.p3wrapout, speed, options.easeout, function () { updateafter(); }); } } else if (newindex < options.currentindex) { // moving backward (decreasing number) isbusy = true; diff = options.currentindex - newindex; options.currentindex = newindex; options.movingforward = false; // set animation speed, depending if user dragged any distance or not speed = p0drag === true ? options.speed * (p0.width() / pwidth) : speedh; startpageanimation(diff, false, speed); if (p0drag) { // hide p1 as p0 moves across it p1.animate(anim.p1, speed, options.easeout); p1wrap.animate(anim.p1wrap, speed, options.easeout); p0.animate(anim.p0, speed, options.easeout); p0wrap.animate(anim.p0wrapdrag, speed, options.easeout, function () { updateafter(); }); } else { // hide p1 as p0 moves across it p1.animate(anim.p1, speed * 2, options.easing); p1wrap.animate(anim.p1wrap, speed * 2, options.easing); p0 .animate(anim.p0in, speed, options.easein) .animate(anim.p0out, speed, options.easeout); p0wrap .animate(anim.p0wrapin, speed, options.easein) .animate(anim.p0wrapout, speed, options.easeout, function () { updateafter(); }); } } } }, starthoveranimation = function (inc) { if (options.hovers || options.manual) { if (inc) { if (!isbusy && !ishoveringright && !ishoveringleft && !p3drag && options.currentindex + 2 <= options.pagetotal - 2) { p2.stop().animate(anim.hover.p2, anim.hover.speed, options.easing); p3.addclass('b-grab'); p3.stop().animate(anim.hover.p3, anim.hover.speed, options.easing); p3wrap.stop().animate(anim.hover.p3wrap, anim.hover.speed, options.easing); ishoveringright = true; } } else { if (!isbusy && !ishoveringleft && !ishoveringright && !p0drag && options.currentindex - 2 >= 0) { p1.stop().animate(anim.hover.p1, anim.hover.speed, options.easing); p0.addclass('b-grab'); p1wrap.stop().animate(anim.hover.p1wrap, anim.hover.speed, options.easing); p0.stop().animate(anim.hover.p0, anim.hover.speed, options.easing); p0wrap.stop().animate(anim.hover.p0wrap, anim.hover.speed, options.easing); ishoveringleft = true; } } } }, endhoveranimation = function (inc) { if (options.hovers || options.manual) { if (inc) { if (!isbusy && ishoveringright && !p3drag && options.currentindex + 2 <= options.pagetotal - 2) { p2.stop().animate(anim.hover.p2end, anim.hover.speed, options.easing); p3.stop().animate(anim.hover.p3end, anim.hover.speed, options.easing); p3wrap.stop().animate(anim.hover.p3wrapend, anim.hover.speed, options.easing); ishoveringright = false; } } else { if (!isbusy && ishoveringleft && !p0drag && options.currentindex - 2 >= 0) { p1.stop().animate(anim.hover.p1end, anim.hover.speed, options.easing); p1wrap.stop().animate(anim.hover.p1wrapend, anim.hover.speed, options.easing); p0.stop().animate(anim.hover.p0end, anim.hover.speed, options.easing); p0wrap.stop().animate(anim.hover.p0wrapend, anim.hover.speed, options.easing); ishoveringleft = false; } } } }, startpageanimation = function (diff, inc) { var currindex = options.currentindex; // setup content if (inc && diff > 2) { // initialize next 2 pages, if jumping forward in the book target.find('.b-p3, .b-p4').removeclass('b-p3 b-p4').hide(); target.find('.b-page-' + currindex).addclass('b-p3').show().stop().css(css.p3); target.find('.b-page-' + (currindex + 1)).addclass('b-p4').show().css(css.p4); target.find('.b-page-' + currindex + ' .b-wrap').show().css(css.wrap); target.find('.b-page-' + (currindex + 1) + ' .b-wrap').show().css(css.wrap); p3 = target.find('.b-p3'); p4 = target.find('.b-p4'); p3wrap = p3.find('.b-wrap'); p4wrap = p4.find('.b-wrap'); if (ishoveringright) { p3.css({ 'left': options.width - 40, 'width': 20, 'padding-left': 10 }); } } else if (!inc && diff > 2) { // initialize previous 2 pages, if jumping backwards in the book target.find('.b-pn, .b-p0').removeclass('b-pn b-p0').hide(); target.find('.b-page-' + currindex).addclass('b-pn').show().css(css.pn); target.find('.b-page-' + (currindex + 1)).addclass('b-p0').show().css(css.p0); target.find('.b-page-' + currindex + ' .b-wrap').show().css(css.wrap); target.find('.b-page-' + (currindex + 1) + ' .b-wrap').show().css(css.wrap); pn = target.find('.b-pn'); p0 = target.find('.b-p0'); pnwrap = pn.find('.b-wrap'); p0wrap = p0.find('.b-wrap'); p0wrap.css(css.p0wrap); if (ishoveringleft) { p0.css({ left: 10, width: 40 }); p0wrap.css({ right: 10 }); } } }, updateafter = function () { updatepages(); isbusy = false; }; /* -------------------------- api -------------------------- */ return { init: init, destroy: destroy, next: next, prev: prev, gotopage: function (index) { // validate inputs if (typeof index === 'string') { if (index === 'first') { index = 0; } else if (index === 'last') { index = options.pagetotal - 2; } else { this.gotopage(parseint(index)); } } else if (typeof index === 'number') { if (index < 0 || index >= options.pagetotal) { return; } } else if (index === undefined) { return; } // adjust for odd page if (index % 2 !== 0) { index -= 1; } gotopage(index); }, add: addpage, remove: removepage, option: function (name, value) { if (typeof name === 'string') { // if option exists if (options[name] !== undefined) { if (value !== undefined) { // if value is sent in, set the option value and update options options[name] = value; updateoptions(); } else { // if no value sent in, get the current option value return options[name]; } } else { $.error('option "' + name + '" does not exist on jquery.booklet.'); } } else if (typeof name === 'object') { // if sending in an object, update options updateoptions(name); } else if (name === undefined || !name) { // return a copy of the options object, to avoid changes return $.extend({}, options); } } }; } // define default options $.fn.booklet.defaults = { width: 1200, // container width, px height: 820, // container height, px speed: 1000, // speed of the transition between pages startingpage: 0, // index of the first page to be displayed easing: 'easeinoutquad', // easing method for complete transition easein: 'easeinquad', // easing method for first half of transition easeout: 'easeoutquad', // easing method for second half of transition pagepadding: 10, // padding for each page wrapper manual: true, // enables manual page turning, requires jquery ui to function hovers: true, // enables preview page-turn hover animation, shows a small preview of previous or next page on hover hoverwidth: 50, // default width for page-turn hover preview hoverspeed: 500, // default speed for page-turn hover preview hoverthreshold: 0.25, // default percentage used for manual page dragging, sets the percentage amount a drag must be before moving next or prev hoverclick: true, // enables hovered arreas to be clicked when using manual page turning shadowbtmwidth: 30 // shadow width for bottom shadow }; })(this, jquery);