/*! * Monocle - A silky, tactile browser-based ebook JavaScript library. * * Copyright 2012, Joseph Pearson * Licensed under the MIT license. */ Monocle = { VERSION: "3.2.0" }; Monocle.Dimensions = {}; Monocle.Controls = {}; Monocle.Flippers = {}; Monocle.Panels = {}; // A class that tests the browser environment for required capabilities and // known bugs (for which we have workarounds). // Monocle.Env = function () { var API = { constructor: Monocle.Env } var k = API.constants = API.constructor; var p = API.properties = { // Assign to a function before running survey in order to get // results as they come in. The function should take two arguments: // testName and value. resultCallback: null } // These are private variables so they don't clutter up properties. var css = Monocle.Browser.css; var activeTestName = null; var frameLoadCallback = null; var testFrame = null; var testFrameCntr = null; var testFrameSize = 100; var surveyCallback = null; function survey(cb) { surveyCallback = cb; runNextTest(); } function runNextTest() { var test = envTests.shift(); if (!test) { return completed(); } activeTestName = test[0]; try { test[1](); } catch (e) { result(e); } } // Each test should call this to say "I'm finished, run the next test." // function result(val) { API[activeTestName] = val; if (p.resultCallback) { p.resultCallback(activeTestName, val); } runNextTest(); return val; } // Invoked after all tests have run. // function completed() { // Remove the test frame after a slight delay (otherwise Gecko spins). Monocle.defer(removeTestFrame); if (typeof surveyCallback == "function") { fn = surveyCallback; surveyCallback = null; fn(API); } } // A bit of sugar for simplifying a detection pattern: does this // function exist? // // Pass a string snippet of JavaScript to be evaluated. // function testForFunction(str) { return function () { result(typeof eval(str) == "function"); } } // A bit of sugar to indicate that the detection function for this test // hasn't been written yet... // // Pass the value you want assigned for the test until it is implemented. // function testNotYetImplemented(rslt) { return function () { result(rslt); } } // Loads (or reloads) a hidden iframe so that we can test browser features. // // cb is the callback that is fired when the test frame's content is loaded. // // src is optional, in which case it defaults to 4. If provided, it can be // a number (specifying the number of pages of default content), or a string, // which will be loaded as a URL. // function loadTestFrame(cb, src) { if (!testFrame) { testFrame = createTestFrame(); } frameLoadCallback = cb; src = src || 4; if (typeof src == "number") { var pgs = []; for (var i = 1, ii = src; i <= ii; ++i) { pgs.push("
Page "+i+"
"); } var divStyle = [ "display:inline-block", "line-height:"+testFrameSize+"px", "width:"+testFrameSize+"px" ].join(";"); src = "javascript:'"+ ''+ ''+ ''+pgs.join("")+''+ "'"; } testFrame.src = src; } // Creates the hidden test frame and returns it. // function createTestFrame() { testFrameCntr = document.createElement('div'); testFrameCntr.style.cssText = [ "width:"+testFrameSize+"px", "height:"+testFrameSize+"px", "overflow:hidden", "position:absolute", "visibility:hidden" ].join(";"); document.body.appendChild(testFrameCntr); var fr = document.createElement('iframe'); testFrameCntr.appendChild(fr); fr.setAttribute("scrolling", "no"); fr.style.cssText = [ "width:100%", "height:100%", "border:none", "background:#900" ].join(";"); fr.addEventListener( "load", function () { if (!fr.contentDocument || !fr.contentDocument.body) { return; } var bd = fr.contentDocument.body; bd.style.cssText = ([ "margin:0", "padding:0", "position:absolute", "height:100%", "width:100%", "-webkit-column-width:"+testFrameSize+"px", "-webkit-column-gap:0", "-webkit-column-fill:auto", "-moz-column-width:"+testFrameSize+"px", "-moz-column-gap:0", "-moz-column-fill:auto", "-o-column-width:"+testFrameSize+"px", "-o-column-gap:0", "-o-column-fill:auto", "column-width:"+testFrameSize+"px", "column-gap:0", "column-fill:auto" ].join(";")); if (bd.scrollHeight > testFrameSize) { bd.style.cssText += ["min-width:200%", "overflow:hidden"].join(";"); if (bd.scrollHeight <= testFrameSize) { bd.className = "column-force"; } else { bd.className = "column-failed "+bd.scrollHeight; } } frameLoadCallback(fr); }, false ); return fr; } function removeTestFrame() { if (testFrameCntr && testFrameCntr.parentNode) { testFrameCntr.parentNode.removeChild(testFrameCntr); } } function columnedWidth(fr) { var bd = fr.contentDocument.body; var de = fr.contentDocument.documentElement; return Math.max(bd.scrollWidth, de.scrollWidth); } var envTests = [ // TEST FOR REQUIRED CAPABILITIES ["supportsW3CEvents", testForFunction("window.addEventListener")], ["supportsCustomEvents", testForFunction("document.createEvent")], ["supportsColumns", function () { result(css.supportsPropertyWithAnyPrefix('column-width')); }], ["supportsTransform", function () { result(css.supportsProperty([ 'transformProperty', 'WebkitTransform', 'MozTransform', 'OTransform', 'msTransform' ])); }], // TEST FOR OPTIONAL CAPABILITIES // Does it do CSS transitions? ["supportsTransition", function () { result(css.supportsPropertyWithAnyPrefix('transition')) }], // Can we find nodes in a document with an XPath? // ["supportsXPath", testForFunction("document.evaluate")], // Can we find nodes in a document natively with a CSS selector? // ["supportsQuerySelector", testForFunction("document.querySelector")], // Can we do 3d transforms? // ["supportsTransform3d", function () { result( css.supportsMediaQueryProperty('transform-3d') && css.supportsProperty([ 'perspectiveProperty', 'WebkitPerspective', 'MozPerspective', 'OPerspective', 'msPerspective' ]) && !Monocle.Browser.renders.slow // Some older browsers can't be trusted. ); }], // Commonly-used browser functionality ["supportsOfflineCache", function () { result(typeof window.applicationCache != 'undefined'); }], ["supportsLocalStorage", function () { // NB: Some duplicitous early Android browsers claim to have // localStorage, but calls to getItem() fail. result( typeof window.localStorage != "undefined" && typeof window.localStorage.getItem == "function" ) }], // CHECK OUT OUR CONTEXT // Does the device have a MobileSafari-style touch interface? // ["touch", function () { result( ('ontouchstart' in window) || css.supportsMediaQueryProperty('touch-enabled') ); }], // Is the Reader embedded, or in the top-level window? // ["embedded", function () { result(top != self) }], // TEST FOR CERTAIN RENDERING OR INTERACTION BUGS // iOS (at least up to version 4.1) makes a complete hash of touch events // when an iframe is overlapped by other elements. It's a dog's breakfast. // See test/bugs/ios-frame-touch-bug for details. // ["brokenIframeTouchModel", function () { result(Monocle.Browser.iOSVersionBelow("4.2")); }], // Webkit-based browsers put floated elements in the wrong spot when // columns are used -- they appear way down where they would be if there // were no columns. Presumably the float positions are calculated before // the columns. A bug has been lodged, and it's fixed in recent WebKits. // ["floatsIgnoreColumns", function () { if (!Monocle.Browser.is.WebKit) { return result(false); } match = navigator.userAgent.match(/AppleWebKit\/([\d\.]+)/); if (!match) { return result(false); } return result(match[1] < "534.30"); }], // The latest engines all agree that if a body is translated leftwards, // its scrollWidth is shortened. But some older WebKits (notably iOS4) // do not subtract translateX values from scrollWidth. In this case, // we should not add the translate back when calculating the width. // ["widthsIgnoreTranslate", function () { loadTestFrame(function (fr) { var firstWidth = columnedWidth(fr); var s = fr.contentDocument.body.style; var props = css.toDOMProps("transform"); for (var i = 0, ii = props.length; i < ii; ++i) { s[props[i]] = "translateX(-600px)"; } var secondWidth = columnedWidth(fr); for (i = 0, ii = props.length; i < ii; ++i) { s[props[i]] = "none"; } result(secondWidth == firstWidth); }); }], // On Android browsers, if the component iframe has a relative width (ie, // 100%), the width of the entire browser will keep expanding and expanding // to fit the width of the body of the iframe (which, with columns, is // massive). So, 100% is treated as "of the body content" rather than "of // the parent dimensions". In this scenario, we need to give the component // iframe a fixed width in pixels. // // In iOS, the frame is clipped by overflow:hidden, so this doesn't seem to // be a problem. // ["relativeIframeExpands", function () { result(navigator.userAgent.indexOf("Android 2") >= 0); }], // iOS3 will pause JavaScript execution if it gets a style-change + a // scroll change on a component body. Weirdly, this seems to break GBCR // in iOS4. // ["scrollToApplyStyle", function () { result(Monocle.Browser.iOSVersionBelow("4")); }], // TEST FOR OTHER QUIRKY BROWSER BEHAVIOUR // Older versions of WebKit (iOS3, Kindle3) need a min-width set on the // body of the iframe at 200%. This forces columns. But when this // min-width is set, it's more difficult to recognise 1 page components, // so we generally don't want to force it unless we have to. // ["forceColumns", function () { loadTestFrame(function (fr) { var bd = fr.contentDocument.body; result(bd.className ? true : false); }); }], // A component iframe's body is absolutely positioned. This means that // the documentElement should have a height of 0, since it contains nothing // other than an absolutely positioned element. // // But for some browsers (Gecko and Opera), the documentElement is as // wide as the full columned content, and the body is only as wide as // the iframe element (ie, the first column). // // It gets weirder. Gecko sometimes behaves like WebKit (not clipping the // body) IF the component has been loaded via HTML/JS/Nodes, not URL. Still // can't reproduce outside Monocle. // // FIXME: If we can figure out a reliable behaviour for Gecko, we should // use it to precalculate the workaround. At the moment, this test isn't // used, but it belongs in src/dimensions/columns.js#columnedDimensions(). // // ["iframeBodyWidthClipped", function () { // loadTestFrame(function (fr) { // var doc = fr.contentDocument; // result( // doc.body.scrollWidth <= testFrameSize && // doc.documentElement.scrollWidth > testFrameSize // ); // }) // }], // Finding the page that a given HTML node is on is typically done by // calculating the offset of its rectange from the body's rectangle. // // But if this information isn't provided by the browser, we need to use // node.scrollIntoView and check the scrollOffset. Basically iOS3 is the // only target platform that doesn't give us the rectangle info. // ["findNodesByScrolling", function () { result(typeof document.body.getBoundingClientRect !== "function"); }], // In MobileSafari browsers, iframes are rendered at the width and height // of their content, rather than having scrollbars. So in that case, it's // the containing element (the "sheaf") that must be scrolled, not the // iframe body. // ["sheafIsScroller", function () { loadTestFrame(function (fr) { result(fr.parentNode.scrollWidth > testFrameSize); }); }], // For some reason, iOS MobileSafari sometimes loses track of a page after // slideOut -- it thinks it has an x-translation of 0, rather than -768 or // whatever. So the page gets "stuck" there, until it is given a non-zero // x-translation. The workaround is to set a non-zero duration on the jumpIn, // which seems to force WebKit to recalculate the x of the page. Weird, yeah. // ["stickySlideOut", function () { result(Monocle.Browser.is.MobileSafari); }], // Chrome and Firefox incorrectly clip text when the dimensions of // a page are not an integer. IE10 clips text when the page dimensions // are rounded. // ['roundPageDimensions', function () { result(!Monocle.Browser.is.IE); }], // In IE10, the element of the iframe's document has scrollbars, // unless you set its style.overflow to 'hidden'. // ['documentElementHasScrollbars', function () { result(Monocle.Browser.is.IE); }], // Older versions of iOS (<6) would render blank pages if they were // off the screen when their layout/position was updated. // ['offscreenRenderingClipped', function () { result(Monocle.Browser.iOSVersionBelow('6')); }], // Gecko is better at loading content with document.write than with // javascript: URLs. With the latter, it tends to put cruft in history, // and gets confused by . ['loadHTMLWithDocWrite', function () { result(Monocle.Browser.is.Gecko || Monocle.Browser.is.Opera); }] ]; function isCompatible() { return ( API.supportsW3CEvents && API.supportsCustomEvents && API.supportsColumns && API.supportsTransform && !API.brokenIframeTouchModel ); } API.survey = survey; API.isCompatible = isCompatible; return API; } ; // A class for manipulating CSS properties in a browser-engine-aware way. // Monocle.CSS = function () { var API = { constructor: Monocle.CSS } var k = API.constants = API.constructor; var p = API.properties = { guineapig: document.createElement('div') } // Returns engine-specific properties, // // eg: // // toCSSProps('transform') // // ... in WebKit, this will return: // // ['transform', '-webkit-transform'] // function toCSSProps(prop) { var props = [prop]; var eng = k.engines.indexOf(Monocle.Browser.engine); if (eng) { var pf = k.prefixes[eng]; if (pf) { props.push(pf+prop); } } return props; } // Returns an engine-specific CSS string. // // eg: // // toCSSDeclaration('column-width', '300px') // // ... in Mozilla, this will return: // // "column-width: 300px; -moz-column-width: 300px;" // function toCSSDeclaration(prop, val) { var props = toCSSProps(prop); for (var i = 0, ii = props.length; i < ii; ++i) { props[i] += ": "+val+";"; } return props.join(""); } // Returns an array of DOM properties specific to this engine. // // eg: // // toDOMProps('column-width') // // ... in Opera, this will return: // // [columnWidth, OColumnWidth] // function toDOMProps(prop) { var parts = prop.split('-'); for (var i = parts.length; i > 0; --i) { parts[i] = capStr(parts[i]); } var props = [parts.join('')]; var eng = k.engines.indexOf(Monocle.Browser.engine); if (eng) { var pf = k.domprefixes[eng]; if (pf) { parts[0] = capStr(parts[0]); props.push(pf+parts.join('')); } } return props; } // Is this exact property (or any in this array of properties) supported // by this engine? // function supportsProperty(props) { for (var i in props) { if (p.guineapig.style[props[i]] !== undefined) { return true; } } return false; } // Thanks modernizr! // Is this property (or a prefixed variant) supported by this engine? // function supportsPropertyWithAnyPrefix(prop) { return supportsProperty(toDOMProps(prop)); } function supportsMediaQuery(query) { var gpid = "monocle_guineapig"; p.guineapig.id = gpid; var st = document.createElement('style'); st.textContent = query+'{#'+gpid+'{height:3px}}'; (document.head || document.getElementsByTagName('head')[0]).appendChild(st); document.documentElement.appendChild(p.guineapig); var result = p.guineapig.offsetHeight === 3; st.parentNode.removeChild(st); p.guineapig.parentNode.removeChild(p.guineapig); return result; } // Thanks modernizr! function supportsMediaQueryProperty(prop) { return supportsMediaQuery( '@media (' + k.prefixes.join(prop+'),(') + 'monocle__)' ); } function capStr(wd) { return wd ? wd.charAt(0).toUpperCase() + wd.substr(1) : ""; } API.toCSSProps = toCSSProps; API.toCSSDeclaration = toCSSDeclaration; API.toDOMProps = toDOMProps; API.supportsProperty = supportsProperty; API.supportsPropertyWithAnyPrefix = supportsPropertyWithAnyPrefix; API.supportsMediaQuery = supportsMediaQuery; API.supportsMediaQueryProperty = supportsMediaQueryProperty; return API; } Monocle.CSS.engines = ["W3C", "WebKit", "Gecko", "Opera", "IE", "Konqueror"]; Monocle.CSS.prefixes = ["", "-webkit-", "-moz-", "-o-", "-ms-", "-khtml-"]; Monocle.CSS.domprefixes = ["", "Webkit", "Moz", "O", "ms", "Khtml"]; // STUBS - simple debug functions and polyfills to normalise client // execution environments. // A little console stub if not initialized in a console-equipped browser. // if (typeof window.console == "undefined") { window.console = { messages: [] } window.console.log = function (msg) { this.messages.push(msg); } window.console.warn = window.console.log; } // A simple version of console.dir that works on iOS. // window.console.compatDir = function (obj) { var stringify = function (o) { var parts = []; for (x in o) { parts.push(x + ": " + o[x]); } return parts.join(";\n"); } var out = stringify(obj); window.console.log(out); return out; } // This is called by Monocle methods and practices that are no longer // recommended and will soon be removed. // window.console.deprecation = function (msg) { console.warn("[DEPRECATION]: "+msg); if (typeof console.trace == "function") { console.trace(); } } // A convenient alias for setTimeout that assumes 0 if no timeout is specified. // Monocle.defer = function (fn, time) { if (typeof fn == "function") { return setTimeout(fn, time || 0); } } ; Monocle.Browser = {}; // Compare the user-agent string to a string or regex pattern. // Monocle.Browser.uaMatch = function (test) { var ua = navigator.userAgent; if (typeof test == "string") { return ua.indexOf(test) >= 0; } return !!ua.match(test); } // Detect the browser engine and set boolean flags for reference. // Monocle.Browser.is = { IE: !!(window.attachEvent && !Monocle.Browser.uaMatch('Opera')), Opera: Monocle.Browser.uaMatch('Opera'), WebKit: Monocle.Browser.uaMatch(/Apple\s?WebKit/), Gecko: Monocle.Browser.uaMatch('Gecko') && !Monocle.Browser.uaMatch('KHTML'), MobileSafari: Monocle.Browser.uaMatch(/OS \d_.*AppleWebKit.*Mobile/) } // Set the browser engine string. // if (Monocle.Browser.is.IE) { Monocle.Browser.engine = "IE"; } else if (Monocle.Browser.is.Opera) { Monocle.Browser.engine = "Opera"; } else if (Monocle.Browser.is.WebKit) { Monocle.Browser.engine = "WebKit"; } else if (Monocle.Browser.is.Gecko) { Monocle.Browser.engine = "Gecko"; } else { console.warn("Unknown engine; assuming W3C compliant."); Monocle.Browser.engine = "W3C"; } // Detect the client platform (typically device/operating system). // Monocle.Browser.on = { iPhone: Monocle.Browser.is.MobileSafari && screen.width == 320, iPad: Monocle.Browser.is.MobileSafari && screen.width == 768, UIWebView: ( Monocle.Browser.is.MobileSafari && !Monocle.Browser.uaMatch('Safari') && !navigator.standalone ), BlackBerry: Monocle.Browser.uaMatch('BlackBerry'), Android: ( Monocle.Browser.uaMatch('Android') || Monocle.Browser.uaMatch(/Linux;.*EBRD/) // Sony Readers ), MacOSX: ( Monocle.Browser.uaMatch('Mac OS X') && !Monocle.Browser.is.MobileSafari ), Kindle3: Monocle.Browser.uaMatch(/Kindle\/3/) } // It is only because MobileSafari is responsible for so much anguish that // we special-case it here. Not a badge of honour. // if (Monocle.Browser.is.MobileSafari) { (function () { var ver = navigator.userAgent.match(/ OS ([\d_]+)/); if (ver) { Monocle.Browser.iOSVersion = ver[1].replace(/_/g, '.'); } else { console.warn("Unknown MobileSafari user agent: "+navigator.userAgent); } })(); } Monocle.Browser.iOSVersionBelow = function (strOrNum) { return !!Monocle.Browser.iOSVersion && Monocle.Browser.iOSVersion < strOrNum; } // Some browser environments are too slow or too problematic for // special animation effects. // // FIXME: These tests are too opinionated. Replace with more targeted tests. // Monocle.Browser.renders = (function () { var ua = navigator.userAgent; var caps = {}; caps.eInk = Monocle.Browser.on.Kindle3; caps.slow = ( caps.eInk || (Monocle.Browser.on.Android && !ua.match(/Chrome/)) || Monocle.Browser.on.Blackberry || ua.match(/NintendoBrowser/) ); return caps; })(); // A helper class for sniffing CSS features and creating CSS rules // appropriate to the current rendering engine. // Monocle.Browser.css = new Monocle.CSS(); // During Reader initialization, this method is called to create the // "environment", which tests for the existence of various browser // features and bugs, then invokes the callback to continue initialization. // // If the survey has already been conducted and the env exists, calls // callback immediately. // Monocle.Browser.survey = function (callback) { if (!Monocle.Browser.env) { Monocle.Browser.env = new Monocle.Env(); Monocle.Browser.env.survey(callback); } else if (typeof callback == "function") { callback(); } } ; Gala = {} // Register an event listener. // Gala.listen = function (elem, evtType, fn, useCapture) { elem = Gala.$(elem); if (elem.addEventListener) { elem.addEventListener(evtType, fn, useCapture || false); } else if (elem.attachEvent) { if (evtType.indexOf(':') < 1) { elem.attachEvent('on'+evtType, fn); } else { var h = (Gala.IE_REGISTRATIONS[elem] = Gala.IE_REGISTRATIONS[elem] || {}); var a = (h[evtType] = h[evtType] || []); a.push(fn); } } } // Remove an event listener. // Gala.deafen = function (elem, evtType, fn, useCapture) { elem = Gala.$(elem); if (elem.removeEventListener) { elem.removeEventListener(evtType, fn, useCapture || false); } else if (elem.detachEvent) { if (evtType.indexOf(':') < 1) { elem.detachEvent('on'+evtType, fn); } else { var h = (Gala.IE_REGISTRATIONS[elem] = Gala.IE_REGISTRATIONS[elem] || {}); var a = (h[evtType] = h[evtType] || []); for (var i = 0, ii = a.length; i < ii; ++i) { if (a[i] == fn) { a.splice(i, 1); } } } } } // Fire an event on the element. // // The data supplied to this function will be available in the event object in // the 'm' property -- eg, alert(evt.m) --> 'foo' // Gala.dispatch = function (elem, evtType, data, cancelable) { elem = Gala.$(elem); if (elem.dispatchEvent) { var evt = document.createEvent('Events'); evt.initEvent(evtType, false, cancelable || false); evt.m = data; return elem.dispatchEvent(evt); } else if (elem.attachEvent && evtType.indexOf(':') >= 0) { if (!Gala.IE_REGISTRATIONS[elem]) { return true; } var evtHandlers = Gala.IE_REGISTRATIONS[elem][evtType]; if (!evtHandlers || evtHandlers.length < 1) { return true; } var evt = { type: evtType, currentTarget: elem, target: elem, m: data, defaultPrevented: false, preventDefault: function () { evt.defaultPrevented = true; } } var q, processQueue = Gala.IE_INVOCATION_QUEUE.length == 0; for (var i = 0, ii = evtHandlers.length; i < ii; ++i) { q = { elem: elem, evtType: evtType, handler: evtHandlers[i], evt: evt } Gala.IE_INVOCATION_QUEUE.push(q); } if (processQueue) { while (q = Gala.IE_INVOCATION_QUEUE.shift()) { //console.log("IE EVT on %s: '%s' with data: %s", q.elem, q.evtType, q.evt.m); q.handler(q.evt); } } return !(cancelable && evt.defaultPrevented); } else { console.warn('[GALA] Cannot dispatch non-namespaced events: '+evtType); return true; } } // Prevents the browser-default action on an event and stops it from // propagating up the DOM tree. // Gala.stop = function (evt) { evt = evt || window.event; if (evt.preventDefault) { evt.preventDefault(); } if (evt.stopPropagation) { evt.stopPropagation(); } evt.returnValue = false; evt.cancelBubble = true; return false; } // Add a group of listeners, which is just a hash of { evtType: callback, ... } // Gala.listenGroup = function (elem, listeners, useCapture) { for (evtType in listeners) { Gala.listen(elem, evtType, listeners[evtType], useCapture || false); } } // Remove a group of listeners. // Gala.deafenGroup = function (elem, listeners, useCapture) { for (evtType in listeners) { Gala.deafen(elem, evtType, listeners[evtType], useCapture || false); } } // Replace a group of listeners with another group, re-using the same // 'listeners' object -- a common pattern. // Gala.replaceGroup = function (elem, listeners, newListeners, useCapture) { Gala.deafenGroup(elem, listeners, useCapture || false); for (evtType in listeners) { delete listeners[evtType]; } for (evtType in newListeners) { listeners[evtType] = newListeners[evtType]; } Gala.listenGroup(elem, listeners, useCapture || false); return listeners; } // Listen for a tap or a click event. // // Returns a 'listener' object that can be passed to Gala.deafenGroup(). // // If 'tapClass' is undefined, it defaults to 'tapping'. If it is a blank // string, no class is assigned. // Gala.onTap = function (elem, fn, tapClass) { elem = Gala.$(elem); if (typeof tapClass == 'undefined') { tapClass = Gala.TAPPING_CLASS; } var tapping = false; var fns = { start: function (evt) { tapping = true; if (tapClass) { elem.classList.add(tapClass); } }, move: function (evt) { if (!tapping) { return; } tapping = false; if (tapClass) { elem.classList.remove(tapClass); } }, end: function (evt) { if (!tapping) { return; } fns.move(evt); evt.currentTarget = evt.currentTarget || evt.srcElement; fn(evt); }, noop: function (evt) {} } var noopOnClick = function (listeners) { Gala.listen(elem, 'click', listeners.click = fns.noop); } Gala.listen(window, 'gala:contact:cancel', fns.move); return Gala.onContact(elem, fns, false, noopOnClick); } // Register a series of functions to listen for the start, move, end // events of a mouse or touch interaction. // // 'fns' argument is an object like: // // { // 'start': function () { ... }, // 'move': function () { ... }, // 'end': function () { ... }, // 'cancel': function () { ... } // } // // All of the functions in this object are optional. // // Returns an object that can later be passed to Gala.deafenGroup. // Gala.onContact = function (elem, fns, useCapture, initCallback) { elem = Gala.$(elem); var listeners = null; var inited = false; // If we see a touchstart event, register all these listeners. var touchListeners = function () { var l = {} if (fns.start) { l.touchstart = function (evt) { if (evt.touches.length <= 1) { fns.start(evt); } } } if (fns.move) { l.touchmove = function (evt) { if (evt.touches.length <= 1) { fns.move(evt); } } } if (fns.end) { l.touchend = function (evt) { if (evt.touches.length <= 1) { fns.end(evt); } } } if (fns.cancel) { l.touchcancel = fns.cancel; } return l; } // Whereas if we see a mousedown event, register all these listeners. var mouseListeners = function () { var l = {}; if (fns.start) { l.mousedown = function (evt) { if (evt.button < 2) { fns.start(evt); } } } if (fns.move) { l.mousemove = fns.move; } if (fns.end) { l.mouseup = function (evt) { if (evt.button < 2) { fns.end(evt); } } } // if (fns.cancel) { // l.mouseout = function (evt) { // obj = evt.relatedTarget || evt.fromElement; // while (obj && (obj = obj.parentNode)) { if (obj == elem) { return; } } // fns.cancel(evt); // } // } return l; } if (typeof Gala.CONTACT_MODE == 'undefined') { var contactInit = function (evt, newListeners, mode) { if (inited) { return; } Gala.CONTACT_MODE = Gala.CONTACT_MODE || mode; if (Gala.CONTACT_MODE != mode) { return; } Gala.replaceGroup(elem, listeners, newListeners, useCapture); if (typeof initCallback == 'function') { initCallback(listeners); } if (listeners[evt.type]) { listeners[evt.type](evt); } inited = true; } var touchInit = function (evt) { contactInit(evt, touchListeners(), 'touch'); } var mouseInit = function (evt) { contactInit(evt, mouseListeners(), 'mouse'); } listeners = { 'touchstart': touchInit, 'touchmove': touchInit, 'touchend': touchInit, 'touchcancel': touchInit, 'mousedown': mouseInit, 'mousemove': mouseInit, 'mouseup': mouseInit, 'mouseout': mouseInit } } else if (Gala.CONTACT_MODE == 'touch') { listeners = touchListeners(); } else if (Gala.CONTACT_MODE == 'mouse') { listeners = mouseListeners(); } Gala.listenGroup(elem, listeners); if (typeof initCallback == 'function') { initCallback(listeners); } return listeners; } // The Gala.Cursor object provides more detail coordinates for the contact // event, and normalizes differences between touch and mouse coordinates. // // If you have a contact event listener, you can get the coordinates for it // with: // // var cursor = new Gala.Cursor(evt); // Gala.Cursor = function (evt) { var API = { constructor: Gala.Cursor } function initialize() { var ci = evt.type.indexOf('mouse') == 0 ? evt : ['touchstart', 'touchmove'].indexOf(evt.type) >= 0 ? evt.targetTouches[0] : ['touchend', 'touchcancel'].indexOf(evt.type) >= 0 ? evt.changedTouches[0] : null; // Basic coordinates (provided by the event). API.pageX = ci.pageX; API.pageY = ci.pageY; API.clientX = ci.clientX; API.clientY = ci.clientY; API.screenX = ci.screenX; API.screenY = ci.screenY; // Coordinates relative to the target element for the event. var tgt = API.target = evt.target || evt.srcElement; while (tgt.nodeType != 1 && tgt.parentNode) { tgt = tgt.parentNode; } assignOffsetFor(tgt, 'offset'); // Coordinates relative to the element that the event was registered on. var registrant = evt.currentTarget; if (registrant && typeof registrant.offsetLeft != 'undefined') { assignOffsetFor(registrant, 'registrant'); } } function assignOffsetFor(elem, attr) { var r; if (elem.getBoundingClientRect) { var er = elem.getBoundingClientRect(); var dr = document.documentElement.getBoundingClientRect(); r = { left: er.left - dr.left, top: er.top - dr.top } } else { r = { left: elem.offsetLeft, top: elem.offsetTop } while (elem = elem.offsetParent) { if (elem.offsetLeft || elem.offsetTop) { r.left += elem.offsetLeft; r.top += elem.offsetTop; } } } API[attr+'X'] = API.pageX - r.left; API[attr+'Y'] = API.pageY - r.top; } initialize(); return API; } // A little utility to dereference ids into elements. You've seen this before. // Gala.$ = function (elem) { if (typeof elem == 'string') { elem = document.getElementById(elem); } return elem; } // CONSTANTS // Gala.TAPPING_CLASS = 'tapping'; Gala.IE_REGISTRATIONS = {} Gala.IE_INVOCATION_QUEUE = [] ; // A shortcut for creating a bookdata object from a 'data' hash. // // eg: // // Monocle.bookData({ components: ['intro.html', 'ch1.html', 'ch2.html'] }); // // All keys in the 'data' hash are optional: // // components: must be an array of component urls // chapters: must be an array of nested chapters (the usual bookdata structure) // metadata: must be a hash of key/values // getComponentFn: override the default way to fetch components via id // Monocle.bookData = function (data) { return { getComponents: function () { return data.components || ['anonymous']; }, getContents: function () { return data.chapters || []; }, getComponent: data.getComponent || function (id) { return { url: id } }, getMetaData: function (key) { return (data.metadata || {})[key]; }, data: data } } // A shortcut for creating a bookdata object from an array of element ids. // // eg: // // Monocle.bookDataFromIds(['part1', 'part2']); // Monocle.bookDataFromIds = function (elementIds) { return Monocle.bookData({ components: elementIds, getComponent: function (cmptId) { return { nodes: [document.getElementById(cmptId)] } } }); } // A shortcut for creating a bookdata object from an array of nodes. // // eg: // // Monocle.bookDataFromNodes([document.getElementById('content')]); // Monocle.bookDataFromNodes = function (nodes) { return Monocle.bookData({ getComponent: function (n) { return { 'nodes': nodes }; } }); } ; Monocle.Factory = function (element, label, index, reader) { var API = { constructor: Monocle.Factory }; var k = API.constants = API.constructor; var p = API.properties = { element: element, label: label, index: index, reader: reader, prefix: reader.properties.classPrefix || '' } // If index is null, uses the first available slot. If index is not null and // the slot is not free, throws an error. // function initialize() { if (!p.label) { return; } // Append the element to the reader's graph of DOM elements. var node = p.reader.properties.graph; node[p.label] = node[p.label] || []; if (typeof p.index == 'undefined' && node[p.label][p.index]) { throw('Element already exists in graph: '+p.label+'['+p.index+']'); } else { p.index = p.index || node[p.label].length; } node[p.label][p.index] = p.element; // Add the label as a class name. addClass(p.label); } // Finds an element that has been created in the context of the current // reader, with the given label. If oIndex is not provided, returns first. // If oIndex is provided (eg, n), returns the nth element with the label. // function find(oLabel, oIndex) { if (!p.reader.properties.graph[oLabel]) { return null; } return p.reader.properties.graph[oLabel][oIndex || 0]; } // Takes an elements and assimilates it into the reader -- essentially // giving it a "dom" object of it's own. It will then be accessible via find. // // Note that (as per comments for initialize), if oIndex is provided and // there is no free slot in the array for this label at that index, an // error will be thrown. // function claim(oElement, oLabel, oIndex) { return oElement.dom = new Monocle.Factory( oElement, oLabel, oIndex, p.reader ); } // Create an element with the given label. // // The last argument (whether third or fourth) is the options hash. Your // options are: // // class - the classname for the element. Must only be one name. // html - the innerHTML for the element. // text - the innerText for the element (an alternative to html, simpler). // // Returns the created element. // function make(tagName, oLabel, index_or_options, or_options) { var oIndex, options; if (arguments.length == 1) { oLabel = null, oIndex = 0; options = {}; } else if (arguments.length == 2) { oIndex = 0; options = {}; } else if (arguments.length == 4) { oIndex = arguments[2]; options = arguments[3]; } else if (arguments.length == 3) { var lastArg = arguments[arguments.length - 1]; if (typeof lastArg == "number") { oIndex = lastArg; options = {}; } else { oIndex = 0; options = lastArg; } } var oElement = document.createElement(tagName); claim(oElement, oLabel, oIndex); if (options['class']) { oElement.className += " "+p.prefix+options['class']; } if (options['html']) { oElement.innerHTML = options['html']; } if (options['text']) { oElement.appendChild(document.createTextNode(options['text'])); } return oElement; } // Creates an element by passing all the given arguments to make. Then // appends the element as a child of the current element. // function append(tagName, oLabel, index_or_options, or_options) { var oElement = make.apply(this, arguments); p.element.appendChild(oElement); return oElement; } // Returns an array of [label, index, reader] for the given element. // A simple way to introspect the arguments required for #find, for eg. // function address() { return [p.label, p.index, p.reader]; } // Apply a set of style rules (hash or string) to the current element. // See Monocle.Styles.applyRules for more info. // function setStyles(rules) { return Monocle.Styles.applyRules(p.element, rules); } function setBetaStyle(property, value) { return Monocle.Styles.affix(p.element, property, value); } // ClassName manipulation functions - with thanks to prototype.js! // Returns true if one of the current element's classnames matches name -- // classPrefix aware (so don't concate the prefix onto it). // function hasClass(name) { name = p.prefix + name; var klass = p.element.className; if (!klass) { return false; } if (klass == name) { return true; } return new RegExp("(^|\\s)"+name+"(\\s|$)").test(klass); } // Adds name to the classnames of the current element (prepending the // reader's classPrefix first). // function addClass(name) { if (hasClass(name)) { return; } var gap = p.element.className ? ' ' : ''; return p.element.className += gap+p.prefix+name; } // Removes (classPrefix+)name from the classnames of the current element. // function removeClass(name) { var reName = new RegExp("(^|\\s+)"+p.prefix+name+"(\\s+|$)"); var reTrim = /^\s+|\s+$/g; var klass = p.element.className; p.element.className = klass.replace(reName, ' ').replace(reTrim, ''); return p.element.className; } API.find = find; API.claim = claim; API.make = make; API.append = append; API.address = address; API.setStyles = setStyles; API.setBetaStyle = setBetaStyle; API.hasClass = hasClass; API.addClass = addClass; API.removeClass = removeClass; initialize(); return API; } ; Monocle.Events = {}; Monocle.Events.wrapper = function (fn) { return function (evt) { evt.m = new Gala.Cursor(evt); fn(evt); } } Monocle.Events.listen = Gala.listen; Monocle.Events.deafen = Gala.deafen; Monocle.Events.dispatch = Gala.dispatch; Monocle.Events.listenForTap = function (elem, fn, tapClass) { return Gala.onTap(elem, Monocle.Events.wrapper(fn), tapClass); } Monocle.Events.deafenForTap = Gala.deafenGroup; Monocle.Events.listenForContact = function (elem, fns, options) { options = options || { useCapture: false }; var wrappers = {}; for (evtType in fns) { wrappers[evtType] = Monocle.Events.wrapper(fns[evtType]); } return Gala.onContact(elem, wrappers, options.useCapture); } Monocle.Events.deafenForContact = Gala.deafenGroup; // Listen for the next transition-end event on the given element, call // the function, then deafen. // // Returns a function that can be used to cancel the listen early. // Monocle.Events.afterTransition = function (elem, fn) { var evtName = "transitionend"; if (Monocle.Browser.is.WebKit) { evtName = 'webkitTransitionEnd'; } else if (Monocle.Browser.is.Opera) { evtName = 'oTransitionEnd'; } var l = null, cancel = null; l = function () { fn(); cancel(); } cancel = function () { Monocle.Events.deafen(elem, evtName, l); } Monocle.Events.listen(elem, evtName, l); return cancel; } ; Monocle.Styles = { // Takes a hash and returns a string. rulesToString: function (rules) { if (typeof rules != 'string') { var parts = []; for (var declaration in rules) { parts.push(declaration+": "+rules[declaration]+";") } rules = parts.join(" "); } return rules; }, // Takes a hash or string of CSS property assignments and applies them // to the element. // applyRules: function (elem, rules) { rules = Monocle.Styles.rulesToString(rules); elem.style.cssText += ';'+rules; return elem.style.cssText; }, // Generates cross-browser properties for a given property. // ie, affix(, 'transition', 'linear 100ms') would apply that value // to webkitTransition for WebKit browsers, and to MozTransition for Gecko. // affix: function (elem, property, value) { var target = elem.style ? elem.style : elem; var props = Monocle.Browser.css.toDOMProps(property); while (props.length) { target[props.shift()] = value; } }, setX: function (elem, x) { var s = elem.style; if (typeof x == "number") { x += "px"; } var val = Monocle.Browser.env.supportsTransform3d ? 'translate3d('+x+', 0, 0)' : 'translateX('+x+')'; val = (x == '0px') ? 'none' : val; s.webkitTransform = s.MozTransform = s.OTransform = s.transform = val; return x; }, setY: function (elem, y) { var s = elem.style; if (typeof y == "number") { y += "px"; } var val = Monocle.Browser.env.supportsTransform3d ? 'translate3d(0, '+y+', 0)' : 'translateY('+y+')'; val = (y == '0px') ? 'none' : val; s.webkitTransform = s.MozTransform = s.OTransform = s.transform = val; return y; }, getX: function (elem) { var currStyle = document.defaultView.getComputedStyle(elem, null); var re = /matrix\([^,]+,[^,]+,[^,]+,[^,]+,\s*([^,]+),[^\)]+\)/; var props = Monocle.Browser.css.toDOMProps('transform'); var matrix = null; while (props.length && !matrix) { matrix = currStyle[props.shift()]; } return parseInt(matrix.match(re)[1]); }, transitionFor: function (elem, prop, duration, timing, delay) { var tProps = Monocle.Browser.css.toDOMProps('transition'); var pProps = Monocle.Browser.css.toCSSProps(prop); timing = timing || "linear"; delay = (delay || 0)+"ms"; for (var i = 0, ii = tProps.length; i < ii; ++i) { var t = "none"; if (duration) { t = [pProps[i], duration+"ms", timing, delay].join(" "); } elem.style[tProps[i]] = t; } } } // These rule definitions are more or less compulsory for Monocle to behave // as expected. Which is why they appear here and not in the stylesheet. // Adjust them if you know what you're doing. // Monocle.Styles.container = { "position": "absolute", "overflow": "hidden", "top": "0", "left": "0", "bottom": "0", "right": "0" } Monocle.Styles.page = { "position": "absolute", "z-index": "1", "-webkit-user-select": "none", "-moz-user-select": "none", "-ms-user-select": "none", "user-select": "none", "-webkit-transform": "translate3d(0,0,0)", "visibility": "visible" /* "background": "white", "top": "0", "left": "0", "bottom": "0", "right": "0" */ } Monocle.Styles.sheaf = { "position": "absolute", "overflow": "hidden" /* "top": "0", "left": "0", "bottom": "0", "right": "0" */ } Monocle.Styles.component = { "width": "100%", "height": "100%", "border": "none", "-webkit-user-select": "none", "-moz-user-select": "none", "-ms-user-select": "none", "user-select": "none" } Monocle.Styles.control = { "z-index": "100", "cursor": "pointer" } Monocle.Styles.overlay = { "position": "absolute", "display": "none", "width": "100%", "height": "100%", "z-index": "1000" } ; Monocle.Formatting = function (reader, optStyles, optScale) { var API = { constructor: Monocle.Formatting }; var k = API.constants = API.constructor; var p = API.properties = { reader: reader, // An array of style rules that are automatically applied to every page. stylesheets: [], // A multiplier on the default font-size of each element in every // component. If null, the multiplier is not applied (or it is removed). fontScale: null } function initialize() { p.fontScale = optScale; clampStylesheets(optStyles); p.reader.listen('monocle:componentmodify', persistOnComponentChange); } // Clamp page frames to a set of styles that reduce Monocle breakage. // function clampStylesheets(implStyles) { var defCSS = k.DEFAULT_STYLE_RULES; if (Monocle.Browser.env.floatsIgnoreColumns) { defCSS.push("html#RS\\:monocle * { float: none !important; }"); } p.defaultStyles = addPageStyles(defCSS, false); if (implStyles) { p.initialStyles = addPageStyles(implStyles, false); } } function persistOnComponentChange(evt) { var doc = evt.m['document']; doc.documentElement.id = p.reader.properties.systemId; adjustFontScaleForDoc(doc, p.fontScale); for (var i = 0; i < p.stylesheets.length; ++i) { if (p.stylesheets[i]) { addPageStylesheet(doc, i); } } } /* PAGE STYLESHEETS */ // API for adding a new stylesheet to all components. styleRules should be // a string of CSS rules. restorePlace defaults to true. // // Returns a sheet index value that can be used with updatePageStyles // and removePageStyles. // function addPageStyles(styleRules, restorePlace) { return changingStylesheet(function () { p.stylesheets.push(styleRules); var sheetIndex = p.stylesheets.length - 1; var i = 0, cmpt = null; while (cmpt = p.reader.dom.find('component', i++)) { addPageStylesheet(cmpt.contentDocument, sheetIndex); } return sheetIndex; }, restorePlace); } // API for updating the styleRules in an existing page stylesheet across // all components. Takes a sheet index value obtained via addPageStyles. // function updatePageStyles(sheetIndex, styleRules, restorePlace) { return changingStylesheet(function () { p.stylesheets[sheetIndex] = styleRules; if (typeof styleRules.join == "function") { styleRules = styleRules.join("\n"); } var i = 0, cmpt = null; while (cmpt = p.reader.dom.find('component', i++)) { var doc = cmpt.contentDocument; var styleTag = doc.getElementById('monStylesheet'+sheetIndex); if (!styleTag) { console.warn('No such stylesheet: ' + sheetIndex); return; } if (styleTag.styleSheet) { styleTag.styleSheet.cssText = styleRules; } else { styleTag.replaceChild( doc.createTextNode(styleRules), styleTag.firstChild ); } } }, restorePlace); } // API for removing a page stylesheet from all components. Takes a sheet // index value obtained via addPageStyles. // function removePageStyles(sheetIndex, restorePlace) { return changingStylesheet(function () { p.stylesheets[sheetIndex] = null; var i = 0, cmpt = null; while (cmpt = p.reader.dom.find('component', i++)) { var doc = cmpt.contentDocument; var styleTag = doc.getElementById('monStylesheet'+sheetIndex); styleTag.parentNode.removeChild(styleTag); } }, restorePlace); } // Wraps all API-based stylesheet changes (add, update, remove) in a // brace of custom events (stylesheetchanging/stylesheetchange), and // recalculates component dimensions if specified (default to true). // function changingStylesheet(callback, restorePlace) { restorePlace = (restorePlace === false) ? false : true; if (restorePlace) { dispatchChanging(); } var result = callback(); if (restorePlace) { p.reader.recalculateDimensions(true); Monocle.defer(dispatchChange); } else { p.reader.recalculateDimensions(false); } return result; } function dispatchChanging() { p.reader.dispatchEvent("monocle:stylesheetchanging", {}); } function dispatchChange() { p.reader.dispatchEvent("monocle:stylesheetchange", {}); } // Private method for adding a stylesheet to a component. Used by // addPageStyles. // function addPageStylesheet(doc, sheetIndex) { var styleRules = p.stylesheets[sheetIndex]; if (!styleRules) { return; } if (!doc || !doc.documentElement) { return; } var head = doc.getElementsByTagName('head')[0]; if (!head) { head = doc.createElement('head'); doc.documentElement.appendChild(head); } if (typeof styleRules.join == "function") { styleRules = styleRules.join("\n"); } var styleTag = doc.createElement('style'); styleTag.type = 'text/css'; styleTag.id = "monStylesheet"+sheetIndex; if (styleTag.styleSheet) { styleTag.styleSheet.cssText = styleRules; } else { styleTag.appendChild(doc.createTextNode(styleRules)); } head.appendChild(styleTag); return styleTag; } /* FONT SCALING */ function setFontScale(scale, restorePlace) { p.fontScale = scale; if (restorePlace) { dispatchChanging(); } var i = 0, cmpt = null; while (cmpt = p.reader.dom.find('component', i++)) { adjustFontScaleForDoc(cmpt.contentDocument, scale); } if (restorePlace) { p.reader.recalculateDimensions(true); dispatchChange(); } else { p.reader.recalculateDimensions(false); } } function adjustFontScaleForDoc(doc, scale) { var elems = doc.getElementsByTagName('*'); if (scale) { scale = parseFloat(scale); if (!doc.body.pfsSwept) { sweepElements(doc, elems); } // Iterate over each element, applying scale to the original // font-size. If a proportional font sizing is already applied to // the element, update existing cssText, otherwise append new cssText. // for (var j = 0, jj = elems.length; j < jj; ++j) { var newFs = fsProperty(elems[j].pfsOriginal, scale); if (elems[j].pfsApplied) { replaceFontSizeInStyle(elems[j], newFs); } else { elems[j].style.cssText += newFs; } elems[j].pfsApplied = scale; } } else if (doc.body.pfsSwept) { // Iterate over each element, removing proportional font-sizing flag // and property from cssText. for (var j = 0, jj = elems.length; j < jj; ++j) { if (elems[j].pfsApplied) { var oprop = elems[j].pfsOriginalProp; var opropDec = oprop ? 'font-size: '+oprop+' ! important;' : ''; replaceFontSizeInStyle(elems[j], opropDec); elems[j].pfsApplied = null; } } // Establish new baselines in case classes have changed. sweepElements(doc, elems); } } function sweepElements(doc, elems) { // Iterate over each element, looking at its font size and storing // the original value against the element. for (var i = 0, ii = elems.length; i < ii; ++i) { var currStyle = doc.defaultView.getComputedStyle(elems[i], null); var fs = parseFloat(currStyle.getPropertyValue('font-size')); elems[i].pfsOriginal = fs; elems[i].pfsOriginalProp = elems[i].style.fontSize; } doc.body.pfsSwept = true; } function fsProperty(orig, scale) { return 'font-size: '+(orig*scale)+'px ! important;'; } function replaceFontSizeInStyle(elem, newProp) { var lastFs = /font-size:[^;]/ elem.style.cssText = elem.style.cssText.replace(lastFs, newProp); } API.addPageStyles = addPageStyles; API.updatePageStyles = updatePageStyles; API.removePageStyles = removePageStyles; API.setFontScale = setFontScale; initialize(); return API; } Monocle.Formatting.DEFAULT_STYLE_RULES = [ "html#RS\\:monocle * {" + "-webkit-font-smoothing: subpixel-antialiased;" + "text-rendering: auto !important;" + "word-wrap: break-word !important;" + "overflow: visible !important;" + "}", "html#RS\\:monocle body {" + "margin: 0 !important;"+ "border: none !important;" + "padding: 0 !important;" + "width: 100% !important;" + "position: absolute !important;" + "-webkit-text-size-adjust: none;" + "}", "html#RS\\:monocle body * {" + "max-width: 100% !important;" + "}", "html#RS\\:monocle img, html#RS\\:monocle video, html#RS\\:monocle object, html#RS\\:monocle svg {" + "max-height: 95% !important;" + "height: auto !important;" + "}" ] ; // READER // // // The full DOM hierarchy created by Reader is: // // box // -> container // -> pages (the number of page elements is determined by the flipper) // -> sheaf (basically just sets the margins) // -> component (an iframe created by the current component) // -> body (the document.body of the iframe) // -> page controls // -> standard controls // -> overlay // -> modal/popover controls // // // Options: // // flipper: The class of page flipper to use. // // panels: The class of panels to use // // stylesheet: A string of CSS rules to apply to the contents of each // component loaded into the reader. // // fontScale: a float to multiply against the default font-size of each // element in each component. // // place: A book locus for the page to open to when the reader is // initialized. (See comments at Book#pageNumberAt for more about // the locus option). // // systemId: the id for root elements of components, defaults to "RS:monocle" // Monocle.Reader = function (node, bookData, options, onLoadCallback) { var API = { constructor: Monocle.Reader } var k = API.constants = API.constructor; var p = API.properties = { // Initialization-completed flag. initialized: false, // The active book. book: null, // DOM graph of factory-generated objects. graph: {}, // Id applied to the HTML element of each component, can be used to scope // CSS rules. systemId: (options ? options.systemId : null) || k.DEFAULT_SYSTEM_ID, // Prefix for classnames for any created element. classPrefix: k.DEFAULT_CLASS_PREFIX, // Registered control objects (see addControl). Hashes of the form: // { // control: , // elements: , // controlType: // } controls: [], // After the reader has been resized, this resettable timer must expire // the place is restored. resizeTimer: null } var dom; // Inspects the browser environment and kicks off preparing the container. // function initialize() { options = options || {} Monocle.Browser.survey(prepareBox); } // Sets up the container and internal elements. // function prepareBox() { var box = node; if (typeof box == "string") { box = document.getElementById(box); } dom = API.dom = box.dom = new Monocle.Factory(box, 'box', 0, API); API.billboard = new Monocle.Billboard(API); if (!Monocle.Browser.env.isCompatible()) { if (dispatchEvent("monocle:incompatible", {}, true)) { fatalSystemMessage(k.COMPATIBILITY_INFO); } return; } dispatchEvent("monocle:initializing", API); bookData = bookData || Monocle.bookDataFromNodes([box.cloneNode(true)]); var bk = new Monocle.Book(bookData, options.preloadWindow || 1); box.innerHTML = ""; // Make sure the box div is absolutely or relatively positioned. positionBox(); // Attach the page-flipping gadget. attachFlipper(options.flipper); // Create the essential DOM elements. createReaderElements(); // Create the selection object. API.selection = new Monocle.Selection(API); // Create the formatting object. API.formatting = new Monocle.Formatting( API, options.stylesheet, options.fontScale ); primeFrames(options.primeURL, function () { // Make the reader elements look pretty. applyStyles(); p.flipper.listenForInteraction(options.panels); setBook(bk, options.place, function () { if (onLoadCallback) { onLoadCallback(API); } dispatchEvent("monocle:loaded", API); }); }); } function positionBox() { var currPosVal; var box = dom.find('box'); if (document.defaultView) { var currStyle = document.defaultView.getComputedStyle(box, null); currPosVal = currStyle.getPropertyValue('position'); } else if (box.currentStyle) { currPosVal = box.currentStyle.position } if (["absolute", "relative"].indexOf(currPosVal) == -1) { box.style.position = "relative"; } } function attachFlipper(flipperClass) { if (!flipperClass) { if (Monocle.Browser.renders.slow) { flipperClass = Monocle.Flippers.Instant; } else { flipperClass = Monocle.Flippers.Slider; } } p.flipper = new flipperClass(API, null, p.readerOptions); } function createReaderElements() { var cntr = dom.append('div', 'container'); for (var i = 0; i < p.flipper.pageCount; ++i) { var page = cntr.dom.append('div', 'page', i); page.style.visibility = "hidden"; page.m = { reader: API, pageIndex: i, place: null } page.m.sheafDiv = page.dom.append('div', 'sheaf', i); page.m.activeFrame = page.m.sheafDiv.dom.append('iframe', 'component', i); page.m.activeFrame.m = { 'pageDiv': page }; page.m.activeFrame.setAttribute('frameBorder', 0); page.m.activeFrame.setAttribute('scrolling', 'no'); p.flipper.addPage(page); } dom.append('div', 'overlay'); dispatchEvent("monocle:loading", API); } // Opens the frame to a particular URL (usually 'about:blank'). // function primeFrames(url, callback) { url = url || (Monocle.Browser.on.UIWebView ? "blank.html" : "about:blank"); var pageCount = 0; var cb = function (evt) { var frame = evt.target || evt.srcElement; Monocle.Events.deafen(frame, 'load', cb); dispatchEvent( 'monocle:frameprimed', { frame: frame, pageIndex: pageCount } ); if ((pageCount += 1) == p.flipper.pageCount) { Monocle.defer(callback); } } forEachPage(function (page) { Monocle.Events.listen(page.m.activeFrame, 'load', cb); page.m.activeFrame.src = url; }); } function applyStyles() { dom.find('container').dom.setStyles(Monocle.Styles.container); forEachPage(function (page, i) { page.dom.setStyles(Monocle.Styles.page); dom.find('sheaf', i).dom.setStyles(Monocle.Styles.sheaf); var cmpt = dom.find('component', i) cmpt.dom.setStyles(Monocle.Styles.component); }); lockFrameWidths(); dom.find('overlay').dom.setStyles(Monocle.Styles.overlay); dispatchEvent('monocle:styles'); } function lockingFrameWidths() { if (!Monocle.Browser.env.relativeIframeExpands) { return; } for (var i = 0, cmpt; cmpt = dom.find('component', i); ++i) { cmpt.style.display = "none"; } } function lockFrameWidths() { if (!Monocle.Browser.env.relativeIframeExpands) { return; } for (var i = 0, cmpt; cmpt = dom.find('component', i); ++i) { cmpt.style.width = cmpt.parentNode.offsetWidth+"px"; cmpt.style.display = "block"; } } // Apply the book, move to a particular place or just the first page, wait // for everything to complete, then fire the callback. // function setBook(bk, place, callback) { p.book = bk; var pageCount = 0; if (typeof callback == 'function') { var watchers = { 'monocle:componentchange': function (evt) { dispatchEvent('monocle:firstcomponentchange', evt.m); return (pageCount += 1) == p.flipper.pageCount; }, 'monocle:componentfailed': function (evt) { fatalSystemMessage(k.LOAD_FAILURE_INFO); return true; }, 'monocle:turn': function (evt) { deafen('monocle:componentfailed', listener); callback(); return true; } } var listener = function (evt) { if (watchers[evt.type](evt)) { deafen(evt.type, listener); } } for (evtType in watchers) { listen(evtType, listener) } } p.flipper.moveTo(place || { page: 1 }, initialized); } function getBook() { return p.book; } function initialized() { p.initialized = true; } // Attempts to restore the place we were up to in the book before the // reader was resized. // // The delay ensures that if we get multiple calls to this function in // a short period, we don't do lots of expensive recalculations. // function resized() { if (!p.initialized) { console.warn('Attempt to resize book before initialization.'); } lockingFrameWidths(); if (!dispatchEvent("monocle:resizing", {}, true)) { return; } clearTimeout(p.resizeTimer); p.resizeTimer = Monocle.defer(performResize, k.RESIZE_DELAY); } function performResize() { lockFrameWidths(); recalculateDimensions(true, afterResized); } function afterResized() { dispatchEvent('monocle:resize'); } function recalculateDimensions(andRestorePlace, callback) { if (!p.book) { return; } dispatchEvent("monocle:recalculating"); var place, locus; if (andRestorePlace !== false) { var place = getPlace(); var locus = { percent: place ? place.percentageThrough() : 0 }; } forEachPage(function (pageDiv) { pageDiv.m.activeFrame.m.component.updateDimensions(pageDiv); }); var cb = function () { dispatchEvent("monocle:recalculated"); Monocle.defer(callback); } Monocle.defer(function () { locus ? p.flipper.moveTo(locus, cb) : cb; }); } // Returns the current page number in the book. // // The pageDiv argument is optional - typically defaults to whatever the // flipper thinks is the "active" page. // function pageNumber(pageDiv) { var place = getPlace(pageDiv); return place ? (place.pageNumber() || 1) : 1; } // Returns the current "place" in the book -- ie, the page number, chapter // title, etc. // // The pageDiv argument is optional - typically defaults to whatever the // flipper thinks is the "active" page. // function getPlace(pageDiv) { if (!p.initialized) { console.warn('Attempt to access place before initialization.'); } return p.flipper.getPlace(pageDiv); } // Moves the current page as specified by the locus. See // Monocle.Book#pageNumberAt for documentation on the locus argument. // // The callback argument is optional. // function moveTo(locus, callback) { if (!p.initialized) { console.warn('Attempt to move place before initialization.'); } if (!p.book.isValidLocus(locus)) { dispatchEvent( "monocle:notfound", { href: locus ? locus.componentId : "anonymous" } ); return false; } var fn = callback; if (!locus.direction) { dispatchEvent('monocle:jumping', { locus: locus }); fn = function () { dispatchEvent('monocle:jump', { locus: locus }); if (callback) { callback(); } } } p.flipper.moveTo(locus, fn); return true; } // Moves to the relevant element in the relevant component. // function skipToChapter(src) { var locus = p.book.locusOfChapter(src); return moveTo(locus); } // Valid types: // - standard (an overlay above the pages) // - page (within the page) // - modal (overlay where click-away does nothing, for a single control) // - hud (overlay that multiple controls can share) // - popover (overlay where click-away removes the ctrl elements) // - invisible // // Options: // - hidden -- creates and hides the ctrl elements; // use showControl to show them // - container -- specify an existing DOM element to contain the control. // function addControl(ctrl, cType, options) { for (var i = 0; i < p.controls.length; ++i) { if (p.controls[i].control == ctrl) { console.warn("Already added control: %o", ctrl); return; } } options = options || {}; var ctrlData = { control: ctrl, elements: [], controlType: cType } p.controls.push(ctrlData); var addControlTo = function (cntr) { if (cntr == 'container') { cntr = options.container || dom.find('container'); if (typeof cntr == 'string') { cntr = document.getElementById(cntr); } if (!cntr.dom) { dom.claim(cntr, 'controlContainer'); } } else if (cntr == 'overlay') { cntr = dom.find('overlay'); } if (typeof ctrl.createControlElements != 'function') { return; } var ctrlElem = ctrl.createControlElements(cntr); if (!ctrlElem) { return; } cntr.appendChild(ctrlElem); ctrlData.elements.push(ctrlElem); Monocle.Styles.applyRules(ctrlElem, Monocle.Styles.control); return ctrlElem; } if (!cType || cType == 'standard' || cType == 'invisible') { addControlTo('container'); } else if (cType == 'page') { forEachPage(addControlTo); } else if (cType == 'modal' || cType == 'popover' || cType == 'hud') { addControlTo('overlay'); ctrlData.usesOverlay = true; } else if (cType == 'invisible') { addControlTo('container'); } else { console.warn('Unknown control type: ' + cType); } if (options.hidden) { hideControl(ctrl); } else { showControl(ctrl); } if (typeof ctrl.assignToReader == 'function') { ctrl.assignToReader(API); } return ctrl; } function dataForControl(ctrl) { for (var i = 0; i < p.controls.length; ++i) { if (p.controls[i].control == ctrl) { return p.controls[i]; } } } function hideControl(ctrl) { var controlData = dataForControl(ctrl); if (!controlData) { console.warn("No data for control: " + ctrl); return; } if (controlData.hidden) { return; } for (var i = 0; i < controlData.elements.length; ++i) { controlData.elements[i].style.display = "none"; } if (controlData.usesOverlay) { var overlay = dom.find('overlay'); overlay.style.display = "none"; Monocle.Events.deafenForContact(overlay, overlay.listeners); if (controlData.controlType != 'hud') { dispatchEvent('monocle:modal:off'); } } controlData.hidden = true; if (ctrl.properties) { ctrl.properties.hidden = true; } dispatchEvent('monocle:controlhide', { control: ctrl }, false); } function showControl(ctrl) { var controlData = dataForControl(ctrl); if (!controlData) { console.warn("No data for control: " + ctrl); return false; } if (showingControl(ctrl)) { return false; } var overlay = dom.find('overlay'); if (controlData.usesOverlay && controlData.controlType != "hud") { for (var i = 0, ii = p.controls.length; i < ii; ++i) { if (p.controls[i].usesOverlay && !p.controls[i].hidden) { return false; } } overlay.style.display = "block"; dispatchEvent('monocle:modal:on'); } for (var i = 0; i < controlData.elements.length; ++i) { controlData.elements[i].style.display = "block"; } if (controlData.controlType == "popover") { var onControl = function (evt) { var obj = evt.target; do { if (obj == controlData.elements[0]) { return true; } } while (obj && (obj = obj.parentNode)); return false; } overlay.listeners = Monocle.Events.listenForContact( overlay, { start: function (evt) { if (!onControl(evt)) { hideControl(ctrl); } }, move: function (evt) { if (!onControl(evt)) { evt.preventDefault(); } } } ); } controlData.hidden = false; if (ctrl.properties) { ctrl.properties.hidden = false; } dispatchEvent('monocle:controlshow', { control: ctrl }, false); return true; } function showingControl(ctrl) { var controlData = dataForControl(ctrl); return controlData.hidden == false; } function dispatchEvent(evtType, data, cancelable) { return Monocle.Events.dispatch(dom.find('box'), evtType, data, cancelable); } function listen(evtType, fn, useCapture) { Monocle.Events.listen(dom.find('box'), evtType, fn, useCapture); } function deafen(evtType, fn) { Monocle.Events.deafen(dom.find('box'), evtType, fn); } function visiblePages() { return p.flipper.visiblePages ? p.flipper.visiblePages() : [dom.find('page')]; } function forEachPage(callback) { for (var i = 0, ii = p.flipper.pageCount; i < ii; ++i) { var page = dom.find('page', i); callback(page, i); } } /* The Reader PageStyles API is deprecated - it has moved to Formatting */ function addPageStyles(styleRules, restorePlace) { console.deprecation("Use reader.formatting.addPageStyles instead."); return API.formatting.addPageStyles(styleRules, restorePlace); } function updatePageStyles(sheetIndex, styleRules, restorePlace) { console.deprecation("Use reader.formatting.updatePageStyles instead."); return API.formatting.updatePageStyles(sheetIndex, styleRules, restorePlace); } function removePageStyles(sheetIndex, restorePlace) { console.deprecation("Use reader.formatting.removePageStyles instead."); return API.formatting.removePageStyles(sheetIndex, restorePlace); } function fatalSystemMessage(msg) { var info = dom.make('div', 'book_fatality', { html: msg }); var box = dom.find('box'); var bbOrigin = [box.offsetWidth / 2, box.offsetHeight / 2]; API.billboard.show(info, { closeButton: false, from: bbOrigin }); } API.getBook = getBook; API.getPlace = getPlace; API.moveTo = moveTo; API.skipToChapter = skipToChapter; API.resized = resized; API.recalculateDimensions = recalculateDimensions; API.addControl = addControl; API.hideControl = hideControl; API.showControl = showControl; API.showingControl = showingControl; API.dispatchEvent = dispatchEvent; API.listen = listen; API.deafen = deafen; API.visiblePages = visiblePages; // Deprecated! API.addPageStyles = addPageStyles; API.updatePageStyles = updatePageStyles; API.removePageStyles = removePageStyles; initialize(); return API; } Monocle.Reader.RESIZE_DELAY = Monocle.Browser.renders.slow ? 500 : 100; Monocle.Reader.DEFAULT_SYSTEM_ID = 'RS:monocle' Monocle.Reader.DEFAULT_CLASS_PREFIX = 'monelem_' Monocle.Reader.DEFAULT_STYLE_RULES = Monocle.Formatting.DEFAULT_STYLE_RULES; Monocle.Reader.COMPATIBILITY_INFO = "

Incompatible browser

"+ "

Unfortunately, your browser isn't able to display this book. "+ "If possible, try again in another browser or on another device.

"; Monocle.Reader.LOAD_FAILURE_INFO = "

Book could not be loaded

"+ "

Sorry, parts of the book could not be retrieved.
"+ "Please check your connection and refresh to try again.

"; /* BOOK */ /* The Book handles movement through the content by the reader page elements. * * It's responsible for instantiating components as they are required, * and for calculating which component and page number to move to (based on * requests from the Reader). * */ Monocle.Book = function (dataSource, preloadWindow) { var API = { constructor: Monocle.Book } var k = API.constants = API.constructor; var p = API.properties = { dataSource: dataSource, preloadWindow: preloadWindow, cmptLoadQueue: {}, components: [], chapters: {} // flat arrays of chapters per component } function initialize() { p.componentIds = dataSource.getComponents(); p.contents = dataSource.getContents(); p.lastCIndex = p.componentIds.length - 1; } // Adjusts the given locus object to provide the page number within the // current component. // // If the locus implies movement to another component, the locus // 'componentId' property will be updated to point to this component, and // the 'load' property will be set to true, which should be taken as a // sign to call loadPageAt with a callback. // // The locus argument is an object that has one of the following properties: // // - page: positive integer. Counting up from the start of component. // - pagesBack: negative integer. Counting back from the end of component. // - percent: float indicating percentage through the component // - direction: integer relative to the current page number for this pageDiv // - position: string, one of "start" or "end", moves to corresponding point // in the given component // - anchor: an element id within the component // - xpath: the node at this XPath within the component // - selector: the first node at this CSS selector within the component // // The locus object can also specify a componentId. If it is not provided // we default to the currently active component, and if that doesn't exist, // we default to the very first component. // // The locus result will be an object with the following properties: // // - load: boolean, true if loading component required, false otherwise // - componentId: component to load (current componentId if load is false) // - if load is false: // - page // - if load is true: // - one of page / pagesBack / percent / direction / position / anchor // function pageNumberAt(pageDiv, locus) { locus.load = false; var currComponent = pageDiv.m.activeFrame ? pageDiv.m.activeFrame.m.component : null; var component = null; var cIndex = p.componentIds.indexOf(locus.componentId); if (cIndex < 0 && !currComponent) { // No specified component, no current component. Load first component. locus.load = true; locus.componentId = p.componentIds[0]; return locus; } else if ( cIndex < 0 && locus.componentId && currComponent.properties.id != locus.componentId ) { // Invalid component, say not found. pageDiv.m.reader.dispatchEvent( "monocle:notfound", { href: locus.componentId } ); return null; } else if (cIndex < 0) { // No specified (or invalid) component, use current component. component = currComponent; locus.componentId = pageDiv.m.activeFrame.m.component.properties.id; cIndex = p.componentIds.indexOf(locus.componentId); } else if (!p.components[cIndex] || p.components[cIndex] != currComponent) { // Specified component differs from current component. Load specified. locus.load = true; return locus; } else { component = currComponent; } // If we're here, then the locus is based on the current component. var result = { load: false, componentId: locus.componentId, page: 1 } // Get the current last page. lastPageNum = component.lastPageNumber(); // Deduce the page number for the given locus. if (typeof(locus.page) == "number") { result.page = locus.page; } else if (typeof(locus.pagesBack) == "number") { result.page = lastPageNum + locus.pagesBack; } else if (typeof(locus.percent) == "number") { var place = new Monocle.Place(); place.setPlace(component, 1); result.page = place.pageAtPercentageThrough(locus.percent); } else if (typeof(locus.direction) == "number") { if (!pageDiv.m.place) { console.warn("Can't move in a direction if pageDiv has no place."); } result.page = pageDiv.m.place.pageNumber(); result.page += locus.direction; } else if (typeof(locus.anchor) == "string") { result.page = component.pageForChapter(locus.anchor, pageDiv); } else if (typeof(locus.xpath) == "string") { result.page = component.pageForXPath(locus.xpath, pageDiv); } else if (typeof(locus.selector) == "string") { result.page = component.pageForSelector(locus.selector, pageDiv); } else if (typeof(locus.position) == "string") { if (locus.position == "start") { result.page = 1; } else if (locus.position == "end") { result.page = lastPageNum['new']; } } else { console.warn("Unrecognised locus: " + locus); } if (result.page < 1) { if (cIndex == 0) { // On first page of book. result.page = 1; result.boundarystart = true; } else { // Moving backwards from current component. result.load = true; result.componentId = p.componentIds[cIndex - 1]; result.pagesBack = result.page; result.page = null; } } else if (result.page > lastPageNum) { if (cIndex == p.lastCIndex) { // On last page of book. result.page = lastPageNum; result.boundaryend = true; } else { // Moving forwards from current component. result.load = true; result.componentId = p.componentIds[cIndex + 1]; result.page -= lastPageNum; } } return result; } // Same as pageNumberAt, but if a load is not flagged, this will // automatically update the pageDiv's place to the given pageNumber. // // If you call this (ie, from a flipper), you are effectively entering into // a contract to move the frame offset to the given page returned in the // locus if load is false. // function setPageAt(pageDiv, locus) { locus = pageNumberAt(pageDiv, locus); if (locus && !locus.load) { var evtData = { locus: locus, page: pageDiv } if (locus.boundarystart) { pageDiv.m.reader.dispatchEvent('monocle:boundarystart', evtData); } else if (locus.boundaryend) { pageDiv.m.reader.dispatchEvent('monocle:boundaryend', evtData); } else { var component = p.components[p.componentIds.indexOf(locus.componentId)]; pageDiv.m.place = pageDiv.m.place || new Monocle.Place(); pageDiv.m.place.setPlace(component, locus.page); var evtData = { page: pageDiv, locus: locus, pageNumber: pageDiv.m.place.pageNumber(), componentId: locus.componentId } pageDiv.m.reader.dispatchEvent("monocle:pagechange", evtData); } } return locus; } // Will load the given component into the pageDiv's frame, then invoke the // callback with resulting locus (provided by pageNumberAt). // // If the resulting page number is outside the bounds of the new component, // (ie, pageNumberAt again requests a load), this will recurse into further // components until non-loading locus is returned by pageNumberAt. Then the // callback will fire with that locus. // // As with setPageAt, if you call this you're obliged to move the frame // offset to the given page in the locus passed to the callback. // function loadPageAt(pageDiv, locus, onLoad, onFail) { var cIndex = p.componentIds.indexOf(locus.componentId); if (!locus.load || cIndex < 0) { locus = pageNumberAt(pageDiv, locus); } if (!locus) { return onFail ? onFail() : null; } if (!locus.load) { return onLoad(locus); } var findPageNumber = function () { locus = setPageAt(pageDiv, locus); if (!locus) { return onFail ? onFail() : null; } else if (locus.load) { loadPageAt(pageDiv, locus, onLoad, onFail) } else { onLoad(locus); } } var applyComponent = function (component) { component.applyTo(pageDiv, findPageNumber); for (var l = 1; l <= p.preloadWindow; ++l) { deferredPreloadComponent(cIndex+l, l*k.PRELOAD_INTERVAL); } } loadComponent(cIndex, applyComponent, onFail, pageDiv); } // If your flipper doesn't care whether a component needs to be // loaded before the page can be set, you can use this shortcut. // function setOrLoadPageAt(pageDiv, locus, onLoad, onFail) { locus = setPageAt(pageDiv, locus); if (!locus) { if (onFail) { onFail(); } } else if (locus.load) { loadPageAt(pageDiv, locus, onLoad, onFail); } else { onLoad(locus); } } // Fetches the component source from the dataSource. // // 'index' is the index of the component in the // dataSource.getComponents array. // // 'onLoad' is invoked when the source is received. // // 'onFail' is optional, and is invoked if the source could not be fetched. // // 'pageDiv' is optional, and simply allows firing events on // the reader object that has requested this component, ONLY if // the source has not already been received. // function loadComponent(index, onLoad, onFail, pageDiv) { if (p.components[index]) { return onLoad(p.components[index]); } var cmptId = p.components[index]; var evtData = { 'page': pageDiv, 'component': cmptId, 'index': index }; pageDiv.m.reader.dispatchEvent('monocle:componentloading', evtData); var onCmptLoad = function (cmpt) { evtData['component'] = cmpt; pageDiv.m.reader.dispatchEvent('monocle:componentloaded', evtData); onLoad(cmpt); } var onCmptFail = function (cmptId) { console.warn("Failed to load component: "+cmptId); pageDiv.m.reader.dispatchEvent('monocle:componentfailed', evtData); if (onFail) { onFail(); } } _loadComponent(index, onCmptLoad, onCmptFail); } function preloadComponent(index) { if (p.components[index]) { return; } var cmptId = p.componentIds[index]; if (!cmptId) { return; } if (p.cmptLoadQueue[cmptId]) { return; } _loadComponent(index); } function deferredPreloadComponent(index, delay) { Monocle.defer(function () { preloadComponent(index); }, delay); } function _loadComponent(index, successCallback, failureCallback) { var cmptId = p.componentIds[index]; var queueItem = { success: successCallback, failure: failureCallback }; if (p.cmptLoadQueue[cmptId]) { return p.cmptLoadQueue[cmptId] = queueItem; } else { p.cmptLoadQueue[cmptId] = queueItem; } var onCmptFail = function () { fireLoadQueue(cmptId, 'failure', cmptId); } var onCmptLoad = function (cmptSource) { if (cmptSource === false) { return onCmptFail(); } p.components[index] = new Monocle.Component( API, cmptId, index, chaptersForComponent(cmptId), cmptSource ); fireLoadQueue(cmptId, 'success', p.components[index]); } var cmptSource = p.dataSource.getComponent(cmptId, onCmptLoad); if (cmptSource && !p.components[index]) { onCmptLoad(cmptSource); } else if (cmptSource === false) { onCmptFail(); } } function fireLoadQueue(cmptId, cbName, args) { if (typeof p.cmptLoadQueue[cmptId][cbName] == 'function') { p.cmptLoadQueue[cmptId][cbName](args); } p.cmptLoadQueue[cmptId] = null; } // Returns an array of chapter objects that are found in the given component. // // A chapter object has this format: // // { // title: "Chapter 1", // fragment: null // } // // The fragment property of a chapter object is either null (the chapter // starts at the head of the component) or the fragment part of the URL // (eg, "foo" in "index.html#foo"). // function chaptersForComponent(cmptId) { if (p.chapters[cmptId]) { return p.chapters[cmptId]; } p.chapters[cmptId] = []; var matcher = new RegExp('^'+decodeURIComponent(cmptId)+"(\#(.+)|$)"); var matches; var recurser = function (chp) { if (matches = decodeURIComponent(chp.src).match(matcher)) { p.chapters[cmptId].push({ title: chp.title, fragment: matches[2] || null }); } if (chp.children) { for (var i = 0; i < chp.children.length; ++i) { recurser(chp.children[i]); } } } for (var i = 0; i < p.contents.length; ++i) { recurser(p.contents[i]); } return p.chapters[cmptId]; } // Returns a locus for the chapter that has the URL given in the // 'src' argument. // // See the comments at pageNumberAt for an explanation of locus objects. // function locusOfChapter(src) { var matcher = new RegExp('^(.+?)(#(.*))?$'); var matches = src.match(matcher); if (!matches) { return null; } var cmptId = componentIdMatching(matches[1]); if (!cmptId) { return null; } var locus = { componentId: cmptId } matches[3] ? locus.anchor = matches[3] : locus.position = "start"; return locus; } function isValidLocus(locus) { if (!locus) { return false; } if (locus.componentId && !componentIdMatching(locus.componentId)) { return false; } return true; } function componentIdMatching(str) { str = decodeURIComponent(str); for (var i = 0, ii = p.componentIds.length; i < ii; ++i) { if (decodeURIComponent(p.componentIds[i]) == str) { return str; } } return null; } function componentWeights() { if (!p.weights) { p.weights = dataSource.getMetaData('componentWeights') || []; if (!p.weights.length) { var cmptSize = 1.0 / p.componentIds.length; for (var i = 0, ii = p.componentIds.length; i < ii; ++i) { p.weights.push(cmptSize); } } } return p.weights; } API.getMetaData = dataSource.getMetaData; API.pageNumberAt = pageNumberAt; API.setPageAt = setPageAt; API.loadPageAt = loadPageAt; API.setOrLoadPageAt = setOrLoadPageAt; API.chaptersForComponent = chaptersForComponent; API.locusOfChapter = locusOfChapter; API.isValidLocus = isValidLocus; API.componentWeights = componentWeights; initialize(); return API; } // Legacy function. Deprecated. // Monocle.Book.fromNodes = function (nodes) { console.deprecation("Book.fromNodes() will soon be removed."); return new Monocle.Book(Monocle.bookDataFromNodes(nodes)); } Monocle.Book.PRELOAD_INTERVAL = 1000; // PLACE Monocle.Place = function () { var API = { constructor: Monocle.Place } var k = API.constants = API.constructor; var p = API.properties = { component: null, percent: null } function setPlace(cmpt, pageN) { p.component = cmpt; p.percent = pageN / cmpt.lastPageNumber(); p.chapter = null; } function setPercentageThrough(cmpt, percent) { p.component = cmpt; p.percent = percent; p.chapter = null; } function componentId() { return p.component.properties.id; } // How far we are through the component at the "top of the page". // // 0 - start of book. 1.0 - end of book. // function percentAtTopOfPage() { if (k.PAGE_ANCHOR == 'bottom') { return p.percent - 1.0 / p.component.lastPageNumber(); } else { return p.percent; } } function percentOnPage() { return percentAtTopOfPage() + k.PAGE_ANCHOR_OFFSET / pagesInComponent(); } // How far we are through the component at the "bottom of the page". // function percentAtBottomOfPage() { if (k.PAGE_ANCHOR == 'bottom') { return p.percent; } else { return p.percent + 1.0 / p.component.lastPageNumber(); } } // The page number at a given point (0: start, 1: end) within the component. // function pageAtPercentageThrough(percent) { var pages = pagesInComponent(); if (typeof percent != 'number') { percent = 0; } return Math.max(Math.round(pages * percent), 1); } // The page number of this point within the component. // function pageNumber() { return pageAtPercentageThrough(p.percent); } function pagesInComponent() { return p.component.lastPageNumber(); } function chapterInfo() { if (p.chapter) { return p.chapter; } return p.chapter = p.component.chapterForPage(pageNumber()+1); } function chapterTitle() { var chp = chapterInfo(); return chp ? chp.title : null; } function chapterSrc() { var src = componentId(); var cinfo = chapterInfo(); if (cinfo && cinfo.fragment) { src += "#" + cinfo.fragment; } return src; } function getLocus(options) { options = options || {}; var locus = { page: pageNumber(), componentId: componentId() } if (options.direction) { locus.page += options.direction; } else { locus.percent = percentAtBottomOfPage(); } return locus; } // Returns how far this place is in the entire book (0 - start, 1.0 - end). // function percentageOfBook() { var book = p.component.properties.book; var componentIds = book.properties.componentIds; var weights = book.componentWeights(); var cmptIndex = p.component.properties.index; var pc = weights[cmptIndex] * p.percent; for (var i = 0, ii = cmptIndex; i < ii; ++i) { pc += weights[i]; } // Note: This is a decent estimation of current page number and total // number of pages, but it's very approximate. Could be improved by storing // the page counts of all components accessed (since the dimensions of the // reader last changed), and averaging the result across them. (You // probably want to ignore calcs for components < 2 or 3 pages long, too. // The bigger the component, the more accurate the calculation.) // // var bkPages = p.component.lastPageNumber() / weights[cmptIndex]; // console.log('Page: '+ Math.floor(pc*bkPages)+ ' of '+ Math.floor(bkPages)); return pc; } function onFirstPageOfBook() { return p.component.properties.index == 0 && pageNumber() == 1; } function onLastPageOfBook() { return ( p.component.properties.index == p.component.properties.book.properties.lastCIndex && pageNumber() == p.component.lastPageNumber() ); } API.setPlace = setPlace; API.setPercentageThrough = setPercentageThrough; API.componentId = componentId; API.percentAtTopOfPage = percentAtTopOfPage; API.percentOnPage = percentOnPage; API.percentAtBottomOfPage = percentAtBottomOfPage; API.pageAtPercentageThrough = pageAtPercentageThrough; API.pageNumber = pageNumber; API.pagesInComponent = pagesInComponent; API.chapterInfo = chapterInfo; API.chapterTitle = chapterTitle; API.chapterSrc = chapterSrc; API.getLocus = getLocus; API.percentageOfBook = percentageOfBook; API.onFirstPageOfBook = onFirstPageOfBook; API.onLastPageOfBook = onLastPageOfBook; API.percentageThrough = k.PAGE_ANCHOR == 'bottom' ? percentAtBottomOfPage : k.PAGE_ANCHOR == 'offset' ? percentOnPage : percentAtTopOfPage; return API; } // Can set this to 'top', 'offset' or 'bottom'. Old Monocle behaviour is 'bottom'. // Monocle.Place.PAGE_ANCHOR = 'offset'; Monocle.Place.PAGE_ANCHOR_OFFSET = 0.1; Monocle.Place.FromPageNumber = function (component, pageNumber) { var place = new Monocle.Place(); place.setPlace(component, pageNumber); return place; } Monocle.Place.FromPercentageThrough = function (component, percent) { var place = new Monocle.Place(); place.setPercentageThrough(component, percent); return place; } // We can't create a place from a percentage of the book, because the // component may not have been loaded yet. But we can get a locus. // Monocle.Place.percentOfBookToLocus = function (reader, percent) { var book = reader.getBook(); var componentIds = book.properties.componentIds; var weights = book.componentWeights(); var cmptIndex = 0, cmptWeight = 0; percent = Math.min(percent, 0.99999); while (percent >= 0) { cmptWeight = weights[cmptIndex]; percent -= weights[cmptIndex]; if (percent >= 0) { cmptIndex += 1; if (cmptIndex >= weights.length) { console.error('Unable to calculate locus from percentage: '+percent); return; } } } var cmptPercent = (percent + cmptWeight) / cmptWeight; return { componentId: componentIds[cmptIndex], percent: cmptPercent } } ; /* COMPONENT */ // See the properties declaration for details of constructor arguments. // Monocle.Component = function (book, id, index, chapters, source) { var API = { constructor: Monocle.Component } var k = API.constants = API.constructor; var p = API.properties = { // a back-reference to the public API of the book that owns this component book: book, // the string that represents this component in the book's component array id: id, // the position in the book's components array of this component index: index, // The chapters argument is an array of objects that list the chapters that // can be found in this component. A chapter object is defined as: // // { // title: str, // fragment: str, // optional anchor id // percent: n // how far into the component the chapter begins // } // // NOTE: the percent property is calculated by the component - you only need // to pass in the title and the optional id string. // chapters: chapters, // the frame provided by dataSource.getComponent() for this component source: source } // Makes this component the active component for the pageDiv. There are // several strategies for this (see loadFrame). // // When the component has been loaded into the pageDiv's frame, the callback // will be invoked with the pageDiv and this component as arguments. // function applyTo(pageDiv, callback) { prepareSource(pageDiv.m.reader); var evtData = { 'page': pageDiv, 'source': p.source }; pageDiv.m.reader.dispatchEvent('monocle:componentchanging', evtData); var onLoaded = function () { setupFrame( pageDiv, pageDiv.m.activeFrame, function () { callback(pageDiv, API) } ); } Monocle.defer(function () { loadFrame(pageDiv, onLoaded); }); } // Loads this component into the given frame, using one of the following // strategies: // // * HTML - a HTML string // * URL - a URL string // * Nodes - an array of DOM body nodes (NB: no way to populate head) // * Document - a DOM DocumentElement object // function loadFrame(pageDiv, callback) { var frame = pageDiv.m.activeFrame; // We own this frame now. frame.m.component = API; // Hide the frame while we're changing it. frame.style.visibility = "hidden"; frame.whenDocumentReady = function () { var doc = frame.contentDocument; var evtData = { 'page': pageDiv, 'document': doc, 'component': API }; pageDiv.m.reader.dispatchEvent('monocle:componentmodify', evtData); frame.whenDocumentReady = null; } if (p.source.html) { return loadFrameFromHTML(p.source.html || p.source, frame, callback); } else if (p.source.url) { return loadFrameFromURL(p.source.url, frame, callback); } else if (p.source.doc) { return loadFrameFromDocument(p.source.doc, frame, callback); } } // LOAD STRATEGY: HTML // Loads a HTML string into the given frame, invokes the callback once loaded. // function loadFrameFromHTML(src, frame, callback) { var fn = function () { Monocle.Events.deafen(frame, 'load', fn); frame.whenDocumentReady(); Monocle.defer(callback); } Monocle.Events.listen(frame, 'load', fn); if (Monocle.Browser.env.loadHTMLWithDocWrite) { frame.contentDocument.open('text/html', 'replace'); frame.contentDocument.write(src); frame.contentDocument.close(); } else { frame.contentWindow['monCmptData'] = src; frame.src = "javascript:window['monCmptData'];" } } // LOAD STRATEGY: URL // Loads the URL into the given frame, invokes callback once loaded. // function loadFrameFromURL(url, frame, callback) { // If it's a relative path, we need to make it absolute. if (!url.match(/^\//)) { url = absoluteURL(url); } var onDocumentReady = function () { Monocle.Events.deafen(frame, 'load', onDocumentReady); frame.whenDocumentReady(); } var onDocumentLoad = function () { Monocle.Events.deafen(frame, 'load', onDocumentLoad); Monocle.defer(callback); } Monocle.Events.listen(frame, 'load', onDocumentReady); Monocle.Events.listen(frame, 'load', onDocumentLoad); frame.contentWindow.location.replace(url); } // LOAD STRATEGY: DOCUMENT // Replaces the DocumentElement of the given frame with the given srcDoc. // Invokes the callback when loaded. // function loadFrameFromDocument(srcDoc, frame, callback) { var doc = frame.contentDocument; // WebKit has an interesting quirk. The tag must exist in the // document being replaced, not the new document. if (Monocle.Browser.is.WebKit) { var srcBase = srcDoc.querySelector('base'); if (srcBase) { var head = doc.querySelector('head'); if (!head) { try { head = doc.createElement('head'); prependChild(doc.documentElement, head); } catch (e) { head = doc.body; } } var base = doc.createElement('base'); base.setAttribute('href', srcBase.href); head.appendChild(base); } } doc.replaceChild( doc.importNode(srcDoc.documentElement, true), doc.documentElement ); // NB: It's a significant problem with this load strategy that there's // no indication when it is complete. Monocle.defer(callback); } // Once a frame is loaded with this component, call this method to style // and measure its contents. // function setupFrame(pageDiv, frame, callback) { updateDimensions(pageDiv, function () { frame.style.visibility = "visible"; // Find the place of any chapters in the component. locateChapters(pageDiv); // Nothing can prevent iframe scrolling on Android, so we have to undo it. if (Monocle.Browser.on.Android) { Monocle.Events.listen(frame.contentWindow, 'scroll', function () { frame.contentWindow.scrollTo(0,0); }); } // Announce that the component has changed. var doc = frame.contentDocument; var evtData = { 'page': pageDiv, 'document': doc, 'component': API }; pageDiv.m.reader.dispatchEvent('monocle:componentchange', evtData); callback(); }); } // Checks whether the pageDiv dimensions have changed. If they have, // remeasures dimensions and returns true. Otherwise returns false. // function updateDimensions(pageDiv, callback) { pageDiv.m.dimensions.update(function (pageLength) { p.pageLength = pageLength; if (typeof callback == "function") { callback() }; }); } // Iterates over all the chapters that are within this component // (according to the array we were provided on initialization) and finds // their location (in percentage terms) within the text. // // Stores this percentage with the chapter object in the chapters array. // function locateChapters(pageDiv) { if (p.chapters[0] && typeof p.chapters[0].percent == "number") { return; } var doc = pageDiv.m.activeFrame.contentDocument; for (var i = 0; i < p.chapters.length; ++i) { var chp = p.chapters[i]; chp.percent = 0; if (chp.fragment) { var node = doc.getElementById(chp.fragment); chp.percent = pageDiv.m.dimensions.percentageThroughOfNode(node); } } return p.chapters; } // For a given page number within the component, return the chapter that // starts on or most-recently-before this page. // // Useful, for example, in displaying the current chapter title as a // running head on the page. // function chapterForPage(pageN) { var cand = null; var percent = (pageN - 1) / p.pageLength; for (var i = 0; i < p.chapters.length; ++i) { if (percent >= p.chapters[i].percent) { cand = p.chapters[i]; } else { return cand; } } return cand; } // For a given chapter fragment (the bit after the hash // in eg, "index.html#foo"), return the page number on which // the chapter starts. If the fragment is null or blank, will // return the first page of the component. // function pageForChapter(fragment, pageDiv) { if (!fragment) { return 1; } for (var i = 0; i < p.chapters.length; ++i) { if (p.chapters[i].fragment == fragment) { return percentToPageNumber(p.chapters[i].percent); } } var doc = pageDiv.m.activeFrame.contentDocument; var node = doc.getElementById(fragment); var percent = pageDiv.m.dimensions.percentageThroughOfNode(node); return percentToPageNumber(percent); } function pageForXPath(xpath, pageDiv) { var doc = pageDiv.m.activeFrame.contentDocument; var percent = 0; if (Monocle.Browser.env.supportsXPath) { var node = doc.evaluate(xpath, doc, null, 9, null).singleNodeValue; if (node) { percent = pageDiv.m.dimensions.percentageThroughOfNode(node); } } else { console.warn("XPath not supported in this client."); } return percentToPageNumber(percent); } function pageForSelector(selector, pageDiv) { var doc = pageDiv.m.activeFrame.contentDocument; var percent = 0; if (Monocle.Browser.env.supportsQuerySelector) { var node = doc.querySelector(selector); if (node) { percent = pageDiv.m.dimensions.percentageThroughOfNode(node); } } else { console.warn("querySelector not supported in this client."); } return percentToPageNumber(percent); } function percentToPageNumber(pc) { return Math.floor(pc * p.pageLength) + 1; } // A public getter for p.pageLength. // function lastPageNumber() { return p.pageLength; } function prepareSource(reader) { if (p.sourcePrepared) { return; } p.sourcePrepared = true; if (typeof p.source == "string") { p.source = { html: p.source }; } // If supplied as escaped javascript, unescape it to HTML by evalling it. if (p.source.javascript) { console.deprecation( "Loading a component by 'javascript' is deprecated. " + "Use { 'html': src } -- no need to escape or clean the string." ); var src = p.source.javascript; src = src.replace(/\\n/g, "\n"); src = src.replace(/\\r/g, "\r"); src = src.replace(/\\'/g, "'"); p.source = { html: src }; } // If supplied as DOM nodes, convert to HTML by concatenating outerHTMLs. if (p.source.nodes) { var srcs = []; for (var i = 0, ii = p.source.nodes.length; i < ii; ++i) { var node = p.source.nodes[i]; if (node.outerHTML) { srcs.push(node.outerHTML); } else { var div = document.createElement('div'); div.appendChild(node.cloneNode(true)); srcs.push(div.innerHTML); delete(div); } } p.source = { html: srcs.join('') }; } if (p.source.html && !p.source.html.match(new RegExp("", "im"))) { var baseURI = computeBaseURI(reader); if (baseURI) { p.source.html = p.source.html.replace( new RegExp("(]*>)", "im"), '$1' ); } } if (p.source.doc && !p.source.doc.querySelector('base')) { var srcHead = p.source.doc.querySelector('head') || p.source.doc.body; var baseURI = computeBaseURI(reader); if (srcHead && baseURI) { var srcBase = p.source.doc.createElement('base'); srcBase.setAttribute('href', baseURI); prependChild(srcHead, srcBase); } } } function computeBaseURI(reader) { var evtData = { cmptId: p.id, cmptURI: absoluteURL(p.id) } if (reader.dispatchEvent('monocle:component:baseuri', evtData, true)) { return evtData.cmptURI; } } function absoluteURL(url) { var link = document.createElement('a'); link.setAttribute('href', url); result = link.href; delete(link); return result; } function prependChild(pr, el) { pr.firstChild ? pr.insertBefore(el, pr.firstChild) : pr.appendChild(el); } API.applyTo = applyTo; API.updateDimensions = updateDimensions; API.chapterForPage = chapterForPage; API.pageForChapter = pageForChapter; API.pageForXPath = pageForXPath; API.pageForSelector = pageForSelector; API.lastPageNumber = lastPageNumber; return API; } ; Monocle.Selection = function (reader) { var API = { constructor: Monocle.Selection }; var k = API.constants = API.constructor; var p = API.properties = { reader: reader, lastSelection: [] }; function initialize() { if (k.SELECTION_POLLING_INTERVAL) { setInterval(pollSelection, k.SELECTION_POLLING_INTERVAL); } } function pollSelection() { var index = 0, frame = null; while (frame = reader.dom.find('component', index++)) { if (frame.contentWindow) { pollSelectionOnWindow(frame.contentWindow, index); } } } function pollSelectionOnWindow(win, index) { var sel = win.getSelection(); var lm = p.lastSelection[index] || {}; var nm = p.lastSelection[index] = { selected: anythingSelected(win), range: sel.rangeCount ? sel.getRangeAt(0) : null, string: sel.toString() }; if (nm.selected) { nm.rangeStartContainer = nm.range.startContainer; nm.rangeEndContainer = nm.range.endContainer; nm.rangeStartOffset = nm.range.startOffset; nm.rangeEndOffset = nm.range.endOffset; if (!sameRange(nm, lm)) { p.reader.dispatchEvent('monocle:selection', nm); } } else if (lm.selected) { p.reader.dispatchEvent('monocle:deselection', lm); } } function sameRange(m1, m2) { return ( m1.rangeStartContainer == m2.rangeStartContainer && m1.rangeEndContainer == m2.rangeEndContainer && m1.rangeStartOffset == m2.rangeStartOffset && m1.rangeEndOffset == m2.rangeEndOffset ); } // Given a window object, remove any user selections within. Trivial in // most browsers, but involving major mojo on iOS. // function deselect() { var index = 0, frame = null; while (frame = reader.dom.find('component', index++)) { deselectOnWindow(frame.contentWindow); } } function deselectOnWindow(win) { win = win || window; if (!anythingSelected(win)) { return; } if (Monocle.Browser.iOSVersion && !Monocle.Browser.iOSVersionBelow(5)) { preservingScale(function () { preservingScrollPosition(function () { var inp = document.createElement('input'); inp.style.cssText = [ 'position: absolute', 'top: 0', 'left: 0', 'width: 0', 'height: 0' ].join(';'); document.body.appendChild(inp); inp.focus(); document.body.removeChild(inp); }) }); } var sel = win.getSelection(); sel.removeAllRanges(); win.document.body.scrollLeft = 0; win.document.body.scrollTop = 0; } function preservingScrollPosition(fn) { var sx = window.scrollX, sy = window.scrollY; fn(); window.scrollTo(sx, sy); } function preservingScale(fn) { var head = document.querySelector('head'); var ovp = head.querySelector('meta[name=viewport]'); var createViewportMeta = function (content) { var elem = document.createElement('meta'); elem.setAttribute('name', 'viewport'); elem.setAttribute('content', content); head.appendChild(elem); return elem; } if (ovp) { var ovpcontent = ovp.getAttribute('content'); var re = /user-scalable\s*=\s*([^,$\s])*/; var result = ovpcontent.match(re); if (result && ['no', '0'].indexOf(result[1]) >= 0) { fn(); } else { var nvpcontent = ovpcontent.replace(re, ''); nvpcontent += nvpcontent ? ', ' : ''; nvpcontent += 'user-scalable=no'; head.removeChild(ovp); var nvp = createViewportMeta(nvpcontent); fn(); head.removeChild(nvp); head.appendChild(ovp); } } else { var nvp = createViewportMeta('user-scalable=no'); fn(); nvp.setAttribute('content', 'user-scalable=yes'); } } function anythingSelected(win) { return !win.getSelection().isCollapsed; } API.deselect = deselect; initialize(); return API; } Monocle.Selection.SELECTION_POLLING_INTERVAL = 250; Monocle.Billboard = function (reader) { var API = { constructor: Monocle.Billboard }; var k = API.constants = API.constructor; var p = API.properties = { reader: reader, cntr: null }; function show(urlOrElement, options) { p.reader.dispatchEvent('monocle:modal:on'); if (p.cntr) { return console.warn("Modal billboard already showing."); } var options = options || {}; var elem = urlOrElement; p.cntr = reader.dom.append('div', k.CLS.cntr); if (typeof urlOrElement == 'string') { var url = urlOrElement; p.inner = elem = p.cntr.dom.append('iframe', k.CLS.inner); elem.setAttribute('src', url); } else { p.inner = p.cntr.dom.append('div', k.CLS.inner); p.inner.appendChild(elem); } p.dims = [ elem.naturalWidth || elem.offsetWidth, elem.naturalHeight || elem.offsetHeight ]; if (options.closeButton != false) { var cBtn = p.cntr.dom.append('div', k.CLS.closeButton); Monocle.Events.listenForTap(cBtn, hide); } align(options.align || 'left top'); p.reader.listen('monocle:resize', align); shrink(options.from); Monocle.defer(grow); } function hide(evt) { shrink(); Monocle.Events.afterTransition(p.cntr, remove); } function grow() { Monocle.Styles.transitionFor(p.cntr, 'transform', k.ANIM_MS, 'ease-in-out'); Monocle.Styles.affix(p.cntr, 'transform', 'translate(0, 0) scale(1)'); } function shrink(from) { p.from = from || p.from || [0,0]; var translate = 'translate('+p.from[0]+'px, '+p.from[1]+'px)'; var scale = 'scale(0)'; if (typeof p.from[2] === 'number') { scale = 'scaleX('+(p.from[2] / p.cntr.offsetWidth)+') '; scale += 'scaleY('+(p.from[3] / p.cntr.offsetHeight)+')'; } Monocle.Styles.affix(p.cntr, 'transform', translate+' '+scale); } function remove () { p.cntr.parentNode.removeChild(p.cntr); p.cntr = p.inner = null; p.reader.deafen('monocle:resize', align); p.reader.dispatchEvent('monocle:modal:off'); } function align(loc) { p.alignment = (typeof loc == 'string') ? loc : p.alignment; if (!p.alignment) { return; } if (p.dims[0] > p.inner.offsetWidth || p.dims[1] > p.inner.offsetHeight) { p.cntr.dom.addClass(k.CLS.oversized); } else { p.cntr.dom.removeClass(k.CLS.oversized); } var s = p.alignment.split(/\s+/); var l = 0, t = 0; var w = (p.inner.scrollWidth - p.inner.offsetWidth); var h = (p.inner.scrollHeight - p.inner.offsetHeight); if (s[0].match(/^\d+$/)) { l = Math.max(0, parseInt(s[0]) - (p.inner.offsetWidth / 2)); } else if (s[0] == 'center') { l = w / 2; } else if (s[0] == 'right') { l = w; } if (s[1] && s[1].match(/^\d+$/)) { t = Math.max(0, parseInt(s[1]) - (p.inner.offsetHeight / 2)); } else if (!s[1] || s[1] == 'center') { t = h / 2; } else if (s[1] == 'bottom') { t = h; } p.inner.scrollLeft = l; p.inner.scrollTop = t; } API.show = show; API.hide = hide; API.align= align; return API; } Monocle.Billboard.CLS = { cntr: 'billboard_container', inner: 'billboard_inner', closeButton: 'billboard_close', oversized: 'billboard_oversized' } Monocle.Billboard.ANIM_MS = 400; // A panel is an invisible column of interactivity. When contact occurs // (mousedown, touchstart), the panel expands to the full width of its // container, to catch all interaction events and prevent them from hitting // other things. // // Panels are used primarily to provide hit zones for page flipping // interactions, but you can do whatever you like with them. // // After instantiating a panel and adding it to the reader as a control, // you can call listenTo() with a hash of methods for any of 'start', 'move' // 'end' and 'cancel'. // Monocle.Controls.Panel = function () { var API = { constructor: Monocle.Controls.Panel } var k = API.constants = API.constructor; var p = API.properties = { evtCallbacks: {} } function createControlElements(cntr) { p.div = cntr.dom.make('div', k.CLS.panel); p.div.dom.setStyles(k.DEFAULT_STYLES); Monocle.Events.listenForContact( p.div, { 'start': start, 'move': move, 'end': end, 'cancel': cancel }, { useCapture: false } ); return p.div; } function setDirection(dir) { p.direction = dir; } function listenTo(evtCallbacks) { p.evtCallbacks = evtCallbacks; } function deafen() { p.evtCallbacks = {} } function start(evt) { p.contact = true; evt.m.offsetX += p.div.offsetLeft; evt.m.offsetY += p.div.offsetTop; expand(); invoke('start', evt); } function move(evt) { if (!p.contact) { return; } invoke('move', evt); } function end(evt) { if (!p.contact) { return; } Monocle.Events.deafenForContact(p.div, p.listeners); contract(); p.contact = false; invoke('end', evt); } function cancel(evt) { if (!p.contact) { return; } Monocle.Events.deafenForContact(p.div, p.listeners); contract(); p.contact = false; invoke('cancel', evt); } function invoke(evtType, evt) { if (p.evtCallbacks[evtType]) { p.evtCallbacks[evtType](p.direction, evt.m.offsetX, evt.m.offsetY, API); } evt.preventDefault(); } function expand() { if (p.expanded) { return; } p.div.dom.addClass(k.CLS.expanded); p.expanded = true; } function contract(evt) { if (!p.expanded) { return; } p.div.dom.removeClass(k.CLS.expanded); p.expanded = false; } API.createControlElements = createControlElements; API.listenTo = listenTo; API.deafen = deafen; API.expand = expand; API.contract = contract; API.setDirection = setDirection; return API; } Monocle.Controls.Panel.CLS = { panel: 'panel', expanded: 'controls_panel_expanded' } Monocle.Controls.Panel.DEFAULT_STYLES = { position: 'absolute', height: '100%' } ; // The simplest page-flipping interaction system: contact to the left half of // the reader turns back one page, contact to the right half turns forward // one page. // Monocle.Panels.TwoPane = function (flipper, evtCallbacks) { var API = { constructor: Monocle.Panels.TwoPane } var k = API.constants = API.constructor; var p = API.properties = {} function initialize() { p.panels = { forwards: new Monocle.Controls.Panel(), backwards: new Monocle.Controls.Panel() } for (dir in p.panels) { flipper.properties.reader.addControl(p.panels[dir]); p.panels[dir].listenTo(evtCallbacks); p.panels[dir].setDirection(flipper.constants[dir.toUpperCase()]); var style = { "width": k.WIDTH }; style[(dir == "forwards" ? "right" : "left")] = 0; p.panels[dir].properties.div.dom.setStyles(style); } } initialize(); return API; } Monocle.Panels.TwoPane.WIDTH = "50%"; // A three-pane system of page interaction. The left 33% turns backwards, the // right 33% turns forwards, and contact on the middle third causes the // system to go into "interactive mode". In this mode, the page-flipping panels // are only active in the margins, and all of the actual text content of the // book is selectable. The user can exit "interactive mode" by hitting the little // IMode icon in the lower right corner of the reader. // Monocle.Panels.IMode = function (flipper, evtCallbacks) { var API = { constructor: Monocle.Panels.IMode } var k = API.constants = API.constructor; var p = API.properties = {} function initialize() { p.flipper = flipper; p.reader = flipper.properties.reader; p.panels = { forwards: new Monocle.Controls.Panel(), backwards: new Monocle.Controls.Panel() } p.divs = {} for (dir in p.panels) { p.reader.addControl(p.panels[dir]); p.divs[dir] = p.panels[dir].properties.div; p.panels[dir].listenTo(evtCallbacks); p.panels[dir].setDirection(flipper.constants[dir.toUpperCase()]); p.divs[dir].style.width = "33%"; p.divs[dir].style[dir == "forwards" ? "right" : "left"] = 0; } p.panels.central = new Monocle.Controls.Panel(); p.reader.addControl(p.panels.central); p.divs.central = p.panels.central.properties.div; p.divs.central.dom.setStyles({ left: "33%", width: "34%" }); menuCallbacks({ end: modeOn }); for (dir in p.panels) { p.divs[dir].dom.addClass('panels_imode_panel'); p.divs[dir].dom.addClass('panels_imode_'+dir+'Panel'); } p.toggleIcon = { createControlElements: function (cntr) { var div = cntr.dom.make('div', 'panels_imode_toggleIcon'); Monocle.Events.listenForTap(div, modeOff); return div; } } p.reader.addControl(p.toggleIcon, null, { hidden: true }); } function menuCallbacks(callbacks) { p.menuCallbacks = callbacks; p.panels.central.listenTo(p.menuCallbacks); } function toggle() { p.interactive ? modeOff() : modeOn(); } function modeOn() { if (p.interactive) { return; } p.panels.central.contract(); var page = p.reader.visiblePages()[0]; var sheaf = page.m.sheafDiv; var bw = sheaf.offsetLeft; var fw = page.offsetWidth - (sheaf.offsetLeft + sheaf.offsetWidth); bw = Math.floor(((bw - 2) / page.offsetWidth) * 10000 / 100 ) + "%"; fw = Math.floor(((fw - 2) / page.offsetWidth) * 10000 / 100 ) + "%"; startCameo(function () { p.divs.forwards.style.width = fw; p.divs.backwards.style.width = bw; Monocle.Styles.affix(p.divs.central, 'transform', 'translateY(-100%)'); }); p.reader.showControl(p.toggleIcon); p.interactive = true; } function modeOff() { if (!p.interactive) { return; } p.panels.central.contract(); p.reader.selection.deselect(); startCameo(function () { p.divs.forwards.style.width = "33%"; p.divs.backwards.style.width = "33%"; Monocle.Styles.affix(p.divs.central, 'transform', 'translateY(0)'); }); p.reader.hideControl(p.toggleIcon); p.interactive = false; } function startCameo(fn) { // Set transitions on the panels. var trn = Monocle.Panels.IMode.CAMEO_DURATION+"ms ease-in"; Monocle.Styles.affix(p.divs.forwards, 'transition', "width "+trn); Monocle.Styles.affix(p.divs.backwards, 'transition', "width "+trn); Monocle.Styles.affix(p.divs.central, 'transition', "-webkit-transform "+trn); // Temporarily disable listeners. for (var pan in p.panels) { p.panels[pan].deafen(); } // Set the panels to opaque. for (var div in p.divs) { p.divs[div].style.opacity = 1; } if (typeof WebkitTransitionEvent != "undefined") { p.cameoListener = Monocle.Events.listen( p.divs.central, 'webkitTransitionEnd', endCameo ); } else { setTimeout(endCameo, k.CAMEO_DURATION); } fn(); } function endCameo() { setTimeout(function () { // Remove panel transitions. var trn = "opacity linear " + Monocle.Panels.IMode.LINGER_DURATION + "ms"; Monocle.Styles.affix(p.divs.forwards, 'transition', trn); Monocle.Styles.affix(p.divs.backwards, 'transition', trn); Monocle.Styles.affix(p.divs.central, 'transition', trn); // Set the panels to transparent. for (var div in p.divs) { p.divs[div].style.opacity = 0; } // Re-enable listeners. p.panels.forwards.listenTo(evtCallbacks); p.panels.backwards.listenTo(evtCallbacks); p.panels.central.listenTo(p.menuCallbacks); }, Monocle.Panels.IMode.LINGER_DURATION); if (p.cameoListener) { Monocle.Events.deafen(p.divs.central, 'webkitTransitionEnd', endCameo); } } API.toggle = toggle; API.modeOn = modeOn; API.modeOff = modeOff; API.menuCallbacks = menuCallbacks; initialize(); return API; } Monocle.Panels.IMode.CAMEO_DURATION = 250; Monocle.Panels.IMode.LINGER_DURATION = 250; Monocle.Panels.eInk = function (flipper, evtCallbacks) { var API = { constructor: Monocle.Panels.eInk } var k = API.constants = API.constructor; var p = API.properties = { flipper: flipper } function initialize() { p.panel = new Monocle.Controls.Panel(); p.reader = p.flipper.properties.reader; p.reader.addControl(p.panel); p.panel.listenTo({ end: function (panel, x) { if (x < p.panel.properties.div.offsetWidth / 2) { p.panel.setDirection(flipper.constants.BACKWARDS); } else { p.panel.setDirection(flipper.constants.FORWARDS); } evtCallbacks.end(panel, x); } }); var s = p.panel.properties.div.style; p.reader.listen("monocle:componentchanging", function () { s.opacity = 1; Monocle.defer(function () { s.opacity = 0 }, 40); }); s.width = "100%"; s.background = "#000"; s.opacity = 0; if (k.LISTEN_FOR_KEYS) { Monocle.Events.listen(window.top.document, 'keyup', handleKeyEvent); } } function handleKeyEvent(evt) { var eventCharCode = evt.charCode || evt.keyCode; var dir = null; if (eventCharCode == k.KEYS["PAGEUP"]) { dir = flipper.constants.BACKWARDS; } else if (eventCharCode == k.KEYS["PAGEDOWN"]) { dir = flipper.constants.FORWARDS; } if (dir) { flipper.moveTo({ direction: dir }); evt.preventDefault(); } } initialize(); return API; } Monocle.Panels.eInk.LISTEN_FOR_KEYS = true; Monocle.Panels.eInk.KEYS = { "PAGEUP": 33, "PAGEDOWN": 34 }; // Provides page-flipping panels only in the margins of the book. This is not // entirely suited to small screens with razor-thin margins, but is an // appropriate panel class for larger screens (like, say, an iPad). // // Since the flipper hit zones are only in the margins, the actual text content // of the book is always selectable. // Monocle.Panels.Marginal = function (flipper, evtCallbacks) { var API = { constructor: Monocle.Panels.Marginal } var k = API.constants = API.constructor; var p = API.properties = {} function initialize() { p.panels = { forwards: new Monocle.Controls.Panel(), backwards: new Monocle.Controls.Panel() } for (dir in p.panels) { flipper.properties.reader.addControl(p.panels[dir]); p.panels[dir].listenTo(evtCallbacks); p.panels[dir].setDirection(flipper.constants[dir.toUpperCase()]); with (p.panels[dir].properties.div.style) { dir == "forwards" ? right = 0 : left = 0; } } setWidths(); } function setWidths() { var page = flipper.properties.reader.dom.find('page'); var sheaf = page.m.sheafDiv; var bw = sheaf.offsetLeft; var fw = page.offsetWidth - (sheaf.offsetLeft + sheaf.offsetWidth); bw = Math.floor(((bw - 2) / page.offsetWidth) * 10000 / 100) + "%"; fw = Math.floor(((fw - 2) / page.offsetWidth) * 10000 / 100) + "%"; p.panels.forwards.properties.div.style.width = fw; p.panels.backwards.properties.div.style.width = bw; } API.setWidths = setWidths; initialize(); return API; } ; Monocle.Panels.Magic = function (flipper, evtCallbacks) { var API = { constructor: Monocle.Panels.Magic } var k = API.constants = API.constructor; var p = API.properties = { flipper: flipper, evtCallbacks: evtCallbacks, parts: {}, action: {}, contacts: [], startListeners: [], disabled: false } function initialize() { p.reader = flipper.properties.reader; p.parts = { reader: p.reader.dom.find('box'), cmpts: [] } for (var i = 0; i < p.flipper.pageCount; ++i) { p.parts.cmpts.push(p.reader.dom.find('component', i)); } initListeners(); p.reader.listen('monocle:componentmodify', initListeners); p.reader.listen('monocle:magic:init', initListeners); p.reader.listen('monocle:magic:halt', haltListeners); p.reader.listen('monocle:modal:on', disable); p.reader.listen('monocle:modal:off', enable); Monocle.Events.listen(window, 'gala:contact:cancel', resetAction); } function initListeners(evt) { //console.log('magic:init'); stopListening(); startListening(); } function haltListeners(evt) { //console.log('magic:halt'); stopListening(); } function disable(evt) { //console.log('modal:on - halting magic'); stopListening(); p.disabled = true; } function enable(evt) { //console.log('modal:off - initing magic'); p.disabled = false; startListening(); } function startListening() { if (p.disabled || p.startListeners.length) { return; } p.startListeners.push([ p.parts.reader, Monocle.Events.listenForContact( p.parts.reader, { 'start': translatorFunction(p.parts.reader, readerContactStart) } ) ]); for (var i = 0, ii = p.parts.cmpts.length; i < ii; ++i) { p.startListeners.push([ p.parts.cmpts[i].contentDocument.defaultView, Monocle.Events.listenForContact( p.parts.cmpts[i].contentDocument.defaultView, { 'start': translatorFunction(p.parts.cmpts[i], cmptContactStart) } ) ]); } } function stopListening() { if (p.disabled || !p.startListeners.length) { return; } for (var j = 0, jj = p.startListeners.length; j < jj; ++j) { Monocle.Events.deafenForContact( p.startListeners[j][0], p.startListeners[j][1] ); } p.startListeners = []; } function listenForMoveAndEnd(fnMove, fnEnd) { listenOnElem( document.defaultView, translatorFunction(document.documentElement, fnMove), translatorFunction(document.documentElement, fnEnd) ); for (var i = 0, ii = p.parts.cmpts.length; i < ii; ++i) { listenOnElem( p.parts.cmpts[i].contentDocument.defaultView, translatorFunction(p.parts.cmpts[i], fnMove), translatorFunction(p.parts.cmpts[i], fnEnd) ); } } function listenOnElem(elem, fnMove, fnEnd) { var contactListeners = Monocle.Events.listenForContact( elem, { 'move': fnMove, 'end': function (evt) { deafenContactListeners(); fnEnd(evt); } } ); p.contacts.push([elem, contactListeners]); } function deafenContactListeners() { for (var i = 0, ii = p.contacts.length; i < ii; ++i) { Monocle.Events.deafenForContact(p.contacts[i][0], p.contacts[i][1]); } p.contacts = []; } function readerContactStart(evt) { listenForMoveAndEnd(readerContactMove, readerContactEnd); p.action.startX = evt.m.readerX; p.action.startY = evt.m.readerY; p.action.screenX = evt.m.screenX; p.action.screenY = evt.m.screenY; p.action.dir = evt.m.readerX > halfway() ? k.FORWARDS : k.BACKWARDS; p.action.handled = !dispatch('monocle:magic:contact:start', evt); if (!p.action.handled) { invoke('start', evt); } } function readerContactMove(evt) { if (p.action.handled) { dispatch('monocle:magic:contact:move', evt); } else { invoke('move', evt); } // Can't prevent mousemove, so has no effect there. Preventing default // for touchmove will override scrolling, while still allowing selection. evt.preventDefault(); } function readerContactEnd(evt) { p.action.endX = evt.m.readerX; p.action.endY = evt.m.readerY; if (dispatch('monocle:magic:contact', evt)) { invoke('end', evt); } p.action = {}; } function cmptContactStart(evt) { if (actionIsCancelled(evt)) { return resetAction(); } p.action.startX = evt.m.readerX; p.action.startY = evt.m.readerY; p.action.screenX = evt.m.screenX; p.action.screenY = evt.m.screenY; listenForMoveAndEnd(cmptContactMove, cmptContactEnd); } function cmptContactMove(evt) { if (actionIsEmpty()) { return; } if (actionIsCancelled(evt)) { return resetAction(); } // Can't prevent mousemove, so has no effect there. Preventing default // for touchmove will override scrolling, while still allowing selection. evt.preventDefault(); } function cmptContactEnd(evt) { if (actionIsEmpty()) { return; } if (actionIsCancelled(evt)) { return resetAction(); } p.action.endX = evt.m.readerX; p.action.endY = evt.m.readerY; if (Math.abs(p.action.endX - p.action.startX) < k.LEEWAY) { p.action.dir = p.action.startX > halfway() ? k.FORWARDS : k.BACKWARDS; } else { p.action.dir = p.action.startX > p.action.endX ? k.FORWARDS : k.BACKWARDS; } if (dispatch('monocle:magic:contact', evt)) { invoke('start', evt); invoke('end', evt); } p.action = {}; } // Adds two new properties to evt.m: // - readerX // - readerY // // Calculated as the offset of the click from the top left of reader element. // // Then calls the passed function. // function translatorFunction(registrant, callback) { return function (evt) { translatingReaderOffset(registrant, evt, callback); } } function translatingReaderOffset(registrant, evt, callback) { if (typeof p.action.screenX != 'undefined') { evt.m.readerX = p.action.startX + (evt.m.screenX - p.action.screenX); evt.m.readerY = p.action.startY + (evt.m.screenY - p.action.screenY); } else { var dr = document.documentElement.getBoundingClientRect(); var rr = p.parts.reader.getBoundingClientRect(); rr = { left: rr.left - dr.left, top: rr.top - dr.top } if (evt.view == window) { evt.m.readerX = Math.round(evt.m.pageX - rr.left); evt.m.readerY = Math.round(evt.m.pageY - rr.top); } else { var er = registrant.getBoundingClientRect(); er = { left: er.left - dr.left, top: er.top - dr.top } evt.m.readerX = Math.round((er.left - rr.left) + evt.m.clientX); evt.m.readerY = Math.round((er.top - rr.top) + evt.m.clientY); } } callback(evt); } function halfway() { return p.parts.reader.offsetWidth / 2; } function resetAction() { p.action = {}; deafenContactListeners(); } function actionIsCancelled(evt) { var win = evt.target.ownerDocument.defaultView; return (evt.defaultPrevented || !win.getSelection().isCollapsed); } function actionIsEmpty() { return typeof p.action.startX == 'undefined'; } // Returns true if the event WAS NOT cancelled. function dispatch(evtName, trigger) { var rr = p.parts.reader.getBoundingClientRect(); var evtData = { trigger: trigger, start: { x: p.action.startX, y: p.action.startY }, end: { x: p.action.endX, y: p.action.endY }, max: { x: rr.right - rr.left, y: rr.bottom - rr.top } } return p.reader.dispatchEvent(evtName, evtData, true); } function invoke(evtType, evt) { if (p.evtCallbacks[evtType]) { p.evtCallbacks[evtType](p.action.dir, evt.m.readerX, evt.m.readerY, API); } } API.enable = enable; API.disable = disable; initialize(); return API; } Monocle.Panels.Magic.LEEWAY = 3; Monocle.Panels.Magic.FORWARDS = 1; Monocle.Panels.Magic.BACKWARDS = -1; Monocle.Dimensions.Columns = function (pageDiv) { var API = { constructor: Monocle.Dimensions.Columns } var k = API.constants = API.constructor; var p = API.properties = { page: pageDiv, reader: pageDiv.m.reader, length: 0, width: 0 } // Logically, forceColumn browsers can't have a gap, because that would // make the minWidth > 200%. But how much greater? Not worth the effort. k.GAP = Monocle.Browser.env.forceColumns ? 0 : 20; function update(callback) { setColumnWidth(); Monocle.defer(function () { p.length = columnCount(); if (Monocle.DEBUG) { console.log( 'page['+p.page.m.pageIndex+'] -> '+p.length+ ' ('+p.page.m.activeFrame.m.component.properties.id+')' ); } callback(p.length); }); } function setColumnWidth() { var pdims = pageDimensions(); var ce = columnedElement(); p.width = pdims.width; var rules = Monocle.Styles.rulesToString(k.STYLE["columned"]); rules += Monocle.Browser.css.toCSSDeclaration('column-width', pdims.col+'px'); rules += Monocle.Browser.css.toCSSDeclaration('column-gap', k.GAP+'px'); rules += Monocle.Browser.css.toCSSDeclaration('column-fill', 'auto'); rules += Monocle.Browser.css.toCSSDeclaration('transform', 'none'); if (Monocle.Browser.env.forceColumns && ce.scrollHeight > pdims.height) { rules += Monocle.Styles.rulesToString(k.STYLE['column-force']); if (Monocle.DEBUG) { console.warn("Force columns ("+ce.scrollHeight+" > "+pdims.height+")"); } } if (ce.style.cssText != rules) { // Update offset because we're translating to zero. p.page.m.offset = 0; // IE10 hack. if (Monocle.Browser.env.documentElementHasScrollbars) { ce.ownerDocument.documentElement.style.overflow = 'hidden'; } // Apply body style changes. ce.style.cssText = rules; if (Monocle.Browser.env.scrollToApplyStyle) { ce.scrollLeft = 0; } } } // Returns the element to which columns CSS should be applied. // function columnedElement() { return p.page.m.activeFrame.contentDocument.body; } // Returns the width of the offsettable area of the columned element. By // definition, the number of pages is always this divided by the // width of a single page (eg, the client area of the columned element). // function columnedWidth() { var bd = columnedElement(); var de = p.page.m.activeFrame.contentDocument.documentElement; var w = Math.max(bd.scrollWidth, de.scrollWidth); // Add one because the final column doesn't have right gutter. // w += k.GAP; if (!Monocle.Browser.env.widthsIgnoreTranslate && p.page.m.offset) { w += p.page.m.offset; } return w; } function pageDimensions() { var elem = p.page.m.sheafDiv; var w = elem.clientWidth; if (elem.getBoundingClientRect) { w = elem.getBoundingClientRect().width; } if (Monocle.Browser.env.roundPageDimensions) { w = Math.round(w); } return { col: w, width: w + k.GAP, height: elem.clientHeight } } function columnCount() { return Math.ceil(columnedWidth() / pageDimensions().width) } function locusToOffset(locus) { return pageDimensions().width * (locus.page - 1); } // Moves the columned element to the offset implied by the locus. // // The 'transition' argument is optional, allowing the translation to be // animated. If not given, no change is made to the columned element's // transition property. // function translateToLocus(locus, transition) { var offset = locusToOffset(locus); p.page.m.offset = offset; translateToOffset(offset, transition); return offset; } function translateToOffset(offset, transition) { var ce = columnedElement(); if (transition) { Monocle.Styles.affix(ce, "transition", transition); } // NB: can't use setX as it causes a flicker on iOS. Monocle.Styles.affix(ce, "transform", "translateX(-"+offset+"px)"); } function percentageThroughOfNode(target) { if (!target) { return 0; } var doc = p.page.m.activeFrame.contentDocument; var offset = 0; if (Monocle.Browser.env.findNodesByScrolling) { // First, remove translation... translateToOffset(0); // Store scroll offsets for all windows. var win = s = p.page.m.activeFrame.contentWindow; var scrollers = [ [win, win.scrollX, win.scrollY], [window, window.scrollX, window.scrollY] ]; //while (s != s.parent) { scrollers.push([s, s.scrollX]); s = s.parent; } if (Monocle.Browser.env.sheafIsScroller) { var scroller = p.page.m.sheafDiv; var x = scroller.scrollLeft; target.scrollIntoView(); offset = scroller.scrollLeft; } else { var scroller = win; var x = scroller.scrollX; target.scrollIntoView(); offset = scroller.scrollX; } // Restore scroll offsets for all windows. while (s = scrollers.shift()) { s[0].scrollTo(s[1], s[2]); } // ... finally, replace translation. translateToOffset(p.page.m.offset); } else { offset = target.getBoundingClientRect().left; offset -= doc.body.getBoundingClientRect().left; } // We know at least 1px will be visible, and offset should not be 0. offset += 1; // Percent is the offset divided by the total width of the component. var percent = offset / (p.length * p.width); // Page number would be offset divided by the width of a single page. // var pageNum = Math.ceil(offset / pageDimensions().width); return percent; } API.update = update; API.percentageThroughOfNode = percentageThroughOfNode; API.locusToOffset = locusToOffset; API.translateToLocus = translateToLocus; return API; } Monocle.Dimensions.Columns.STYLE = { // Most of these are already applied to body, but they're repeated here // in case columnedElement() is ever anything other than body. "columned": { "margin": "0", "padding": "0", "height": "100%", "width": "100%", "position": "absolute" }, "column-force": { "min-width": "200%", "overflow": "hidden" } } ; Monocle.Flippers.Slider = function (reader) { var API = { constructor: Monocle.Flippers.Slider } var k = API.constants = API.constructor; var p = API.properties = { reader: reader, pageCount: 2, activeIndex: 1, turnData: {}, nextPageReady: true } function initialize() { p.reader.listen("monocle:componentchanging", showWaitControl); } function addPage(pageDiv) { pageDiv.m.dimensions = new Monocle.Dimensions.Columns(pageDiv); // BROWSERHACK: Firefox 4 is prone to beachballing on the first page turn // unless a zeroed translateX has been applied to the page div. Monocle.Styles.setX(pageDiv, 0); } function visiblePages() { return [upperPage()]; } function listenForInteraction(panelClass) { if (typeof panelClass != "function") { panelClass = k.DEFAULT_PANELS_CLASS; if (!panelClass) { console.warn("Invalid panel class.") } } p.panels = new panelClass( API, { 'start': lift, 'move': turning, 'end': release, 'cancel': release } ); } function getPlace(pageDiv) { pageDiv = pageDiv || upperPage(); return pageDiv.m ? pageDiv.m.place : null; } function moveTo(locus, callback) { var cb = function () { if (typeof callback == "function") { callback(); } announceTurn(); } setPage(upperPage(), locus, function () { prepareNextPage(cb) }); } function setPage(pageDiv, locus, onLoad, onFail) { p.reader.getBook().setOrLoadPageAt( pageDiv, locus, function (locus) { pageDiv.m.dimensions.translateToLocus(locus); Monocle.defer(onLoad); }, onFail ); } function upperPage() { return p.reader.dom.find('page', p.activeIndex); } function lowerPage() { return p.reader.dom.find('page', (p.activeIndex + 1) % 2); } function flipPages() { upperPage().style.zIndex = 1; lowerPage().style.zIndex = 2; return p.activeIndex = (p.activeIndex + 1) % 2; } function lift(dir, boxPointX) { if (p.turnData.lifting || p.turnData.releasing) { return; } p.reader.selection.deselect(); p.turnData.points = { start: boxPointX, min: boxPointX, max: boxPointX } p.turnData.lifting = true; var place = getPlace(); if (dir == k.FORWARDS) { if (place.onLastPageOfBook()) { p.reader.dispatchEvent( 'monocle:boundaryend', { locus: getPlace().getLocus({ direction : dir }), page: upperPage() } ); resetTurnData(); return; } onGoingForward(boxPointX); } else if (dir == k.BACKWARDS) { if (place.onFirstPageOfBook()) { p.reader.dispatchEvent( 'monocle:boundarystart', { locus: getPlace().getLocus({ direction : dir }), page: upperPage() } ); resetTurnData(); return; } onGoingBackward(boxPointX); } else { console.warn("Invalid direction: " + dir); } } function turning(dir, boxPointX) { if (!p.turnData.points) { return; } if (p.turnData.lifting || p.turnData.releasing) { return; } checkPoint(boxPointX); slideToCursor(boxPointX, null, "0"); } function release(dir, boxPointX) { if (!p.turnData.points) { return; } if (p.turnData.lifting) { p.turnData.releaseArgs = [dir, boxPointX]; return; } if (p.turnData.releasing) { return; } checkPoint(boxPointX); p.turnData.releasing = true; if (dir == k.FORWARDS) { if ( p.turnData.points.tap || p.turnData.points.start - boxPointX > 60 || p.turnData.points.min >= boxPointX ) { // Completing forward turn slideOut(afterGoingForward); } else { // Cancelling forward turn slideIn(afterCancellingForward); } } else if (dir == k.BACKWARDS) { if ( p.turnData.points.tap || boxPointX - p.turnData.points.start > 60 || p.turnData.points.max <= boxPointX ) { // Completing backward turn slideIn(afterGoingBackward); } else { // Cancelling backward turn slideOut(afterCancellingBackward); } } else { console.warn("Invalid direction: " + dir); } } function checkPoint(boxPointX) { p.turnData.points.min = Math.min(p.turnData.points.min, boxPointX); p.turnData.points.max = Math.max(p.turnData.points.max, boxPointX); p.turnData.points.tap = p.turnData.points.max - p.turnData.points.min < 10; } function onGoingForward(x) { if (p.nextPageReady == false) { prepareNextPage(function () { lifted(x); }, resetTurnData); } else { lifted(x); } } function onGoingBackward(x) { var lp = lowerPage(), up = upperPage(); var onFail = function () { slideOut(afterCancellingBackward); } if (Monocle.Browser.env.offscreenRenderingClipped) { // set lower to "the page before upper" setPage( lp, getPlace(up).getLocus({ direction: k.BACKWARDS }), function () { // flip lower to upper, ready to slide in from left flipPages(); // move lower off the screen to the left jumpOut(lp, function () { lifted(x); }); }, onFail ); } else { jumpOut(lp, function () { flipPages(); setPage( lp, getPlace(up).getLocus({ direction: k.BACKWARDS }), function () { lifted(x); }, onFail ); }); } } function afterGoingForward() { var up = upperPage(), lp = lowerPage(); flipPages(); jumpIn(up, function () { prepareNextPage(announceTurn); }); } function afterGoingBackward() { announceTurn(); } function afterCancellingForward() { announceCancel(); } function afterCancellingBackward() { flipPages(); // flip upper to lower jumpIn(lowerPage(), function () { prepareNextPage(announceCancel); }); } // Prepares the lower page to show the next page after the current page, // and calls onLoad when done. // // Note that if the next page is a new component, and it fails to load, // onFail will be called. If onFail is not supplied, onLoad will be called. // function prepareNextPage(onLoad, onFail) { setPage( lowerPage(), getPlace().getLocus({ direction: k.FORWARDS }), onLoad, function () { onFail ? onFail() : onLoad(); p.nextPageReady = false; } ); } function lifted(x) { p.turnData.lifting = false; p.reader.dispatchEvent('monocle:turning'); var releaseArgs = p.turnData.releaseArgs; if (releaseArgs) { p.turnData.releaseArgs = null; release(releaseArgs[0], releaseArgs[1]); } else if (x) { slideToCursor(x); } } function announceTurn() { p.nextPageReady = true; p.reader.dispatchEvent('monocle:turn'); resetTurnData(); } function announceCancel() { p.reader.dispatchEvent('monocle:turn:cancel'); resetTurnData(); } function resetTurnData() { hideWaitControl(); p.turnData = {}; } function setX(elem, x, options, callback) { var duration, transition; if (!options.duration) { duration = 0; } else { duration = parseInt(options.duration); } if (Monocle.Browser.env.supportsTransition) { Monocle.Styles.transitionFor( elem, 'transform', duration, options.timing, options.delay ); if (Monocle.Browser.env.supportsTransform3d) { Monocle.Styles.affix(elem, 'transform', 'translate3d('+x+'px,0,0)'); } else { Monocle.Styles.affix(elem, 'transform', 'translateX('+x+'px)'); } if (typeof callback == "function") { if (duration && Monocle.Styles.getX(elem) != x) { Monocle.Events.afterTransition(elem, callback); } else { Monocle.defer(callback); } } } else { // Old-school JS animation. elem.currX = elem.currX || 0; var completeTransition = function () { elem.currX = x; Monocle.Styles.setX(elem, x); if (typeof callback == "function") { callback(); } } if (!duration) { completeTransition(); } else { var stamp = (new Date()).getTime(); var frameRate = 40; var step = (x - elem.currX) * (frameRate / duration); var stepFn = function () { var destX = elem.currX + step; var timeElapsed = ((new Date()).getTime() - stamp) >= duration; var pastDest = (destX > x && elem.currX < x) || (destX < x && elem.currX > x); if (timeElapsed || pastDest) { completeTransition(); } else { Monocle.Styles.setX(elem, destX); elem.currX = destX; setTimeout(stepFn, frameRate); } } stepFn(); } } } function jumpIn(pageDiv, callback) { opts = { duration: (Monocle.Browser.env.stickySlideOut ? 1 : 0) } setX(pageDiv, 0, opts, callback); } function jumpOut(pageDiv, callback) { setX(pageDiv, 0 - pageDiv.offsetWidth, { duration: 0 }, callback); } // NB: Slides are always done by the visible upper page. function slideIn(callback) { setX(upperPage(), 0, slideOpts(), callback); } function slideOut(callback) { setX(upperPage(), 0 - upperPage().offsetWidth, slideOpts(), callback); } function slideToCursor(cursorX, callback, duration) { setX( upperPage(), Math.min(0, cursorX - upperPage().offsetWidth), { duration: duration || k.FOLLOW_DURATION }, callback ); } function slideOpts() { var opts = { timing: 'ease-in', duration: 320 } var now = (new Date()).getTime(); if (p.lastSlide && now - p.lastSlide < 1500) { opts.duration *= 0.5; } p.lastSlide = now; return opts; } function ensureWaitControl() { if (p.waitControl) { return; } p.waitControl = { createControlElements: function (holder) { return holder.dom.make('div', 'flippers_slider_wait'); } } p.reader.addControl(p.waitControl, 'page'); } function showWaitControl() { ensureWaitControl(); p.reader.dom.find('flippers_slider_wait', 0).style.opacity = 1; p.reader.dom.find('flippers_slider_wait', 1).style.opacity = 1; } function hideWaitControl() { ensureWaitControl(); p.reader.dom.find('flippers_slider_wait', 0).style.opacity = 0; p.reader.dom.find('flippers_slider_wait', 1).style.opacity = 0; } // THIS IS THE CORE API THAT ALL FLIPPERS MUST PROVIDE. API.pageCount = p.pageCount; API.addPage = addPage; API.getPlace = getPlace; API.moveTo = moveTo; API.listenForInteraction = listenForInteraction; // OPTIONAL API - WILL BE INVOKED (WHERE RELEVANT) IF PROVIDED. API.visiblePages = visiblePages; initialize(); return API; } // Constants Monocle.Flippers.Slider.DEFAULT_PANELS_CLASS = Monocle.Panels.TwoPane; Monocle.Flippers.Slider.FORWARDS = 1; Monocle.Flippers.Slider.BACKWARDS = -1; Monocle.Flippers.Slider.FOLLOW_DURATION = 100; Monocle.Flippers.Scroller = function (reader, setPageFn) { var API = { constructor: Monocle.Flippers.Scroller } var k = API.constants = API.constructor; var p = API.properties = { pageCount: 1, duration: 300 } function initialize() { p.reader = reader; p.setPageFn = setPageFn; } function addPage(pageDiv) { pageDiv.m.dimensions = new Monocle.Dimensions.Columns(pageDiv); } function page() { return p.reader.dom.find('page'); } function listenForInteraction(panelClass) { if (typeof panelClass != "function") { panelClass = k.DEFAULT_PANELS_CLASS; } p.panels = new panelClass( API, { 'end': turn } ); } function turn(dir) { if (p.turning) { return; } p.reader.selection.deselect(); moveTo({ page: getPlace().pageNumber() + dir}); p.reader.dispatchEvent('monocle:turning'); } function getPlace() { return page().m.place; } function moveTo(locus, callback) { var fn = frameToLocus; if (typeof callback == "function") { fn = function (locus) { frameToLocus(locus); callback(locus); } } p.reader.getBook().setOrLoadPageAt(page(), locus, fn); } function frameToLocus(locus) { if (locus.boundarystart || locus.boundaryend) { return; } p.turning = true; var dims = page().m.dimensions; var fr = page().m.activeFrame; var bdy = fr.contentDocument.body; var anim = true; if (p.activeComponent != fr.m.component) { // No animation. p.activeComponent = fr.m.component; dims.translateToLocus(locus, "none"); Monocle.defer(turned); } else if (Monocle.Browser.env.supportsTransition) { // Native animation. dims.translateToLocus(locus, p.duration+"ms ease-in 0ms"); Monocle.Events.afterTransition(bdy, turned); } else { // Old-school JS animation. var x = dims.locusToOffset(locus); var finalX = 0 - x; var stamp = (new Date()).getTime(); var frameRate = 40; var currX = p.currX || 0; var step = (finalX - currX) * (frameRate / p.duration); var stepFn = function () { var destX = currX + step; if ( (new Date()).getTime() - stamp > p.duration || Math.abs(currX - finalX) <= Math.abs((currX + step) - finalX) ) { Monocle.Styles.setX(bdy, finalX); turned(); } else { Monocle.Styles.setX(bdy, destX); currX = destX; setTimeout(stepFn, frameRate); } p.currX = destX; } stepFn(); } } function turned() { p.turning = false; p.reader.dispatchEvent('monocle:turn'); } // THIS IS THE CORE API THAT ALL FLIPPERS MUST PROVIDE. API.pageCount = p.pageCount; API.addPage = addPage; API.getPlace = getPlace; API.moveTo = moveTo; API.listenForInteraction = listenForInteraction; initialize(); return API; } Monocle.Flippers.Scroller.speed = 200; // How long the animation takes Monocle.Flippers.Scroller.rate = 20; // frame-rate of the animation Monocle.Flippers.Scroller.FORWARDS = 1; Monocle.Flippers.Scroller.BACKWARDS = -1; Monocle.Flippers.Scroller.DEFAULT_PANELS_CLASS = Monocle.Panels.TwoPane; Monocle.Flippers.Instant = function (reader) { var API = { constructor: Monocle.Flippers.Instant } var k = API.constants = API.constructor; var p = API.properties = { pageCount: 1 } function initialize() { p.reader = reader; } function addPage(pageDiv) { pageDiv.m.dimensions = new Monocle.Dimensions.Columns(pageDiv); } function getPlace() { return page().m.place; } function moveTo(locus, callback) { var fn = frameToLocus; if (typeof callback == "function") { fn = function (locus) { frameToLocus(locus); callback(locus); } } p.reader.getBook().setOrLoadPageAt(page(), locus, fn); } function listenForInteraction(panelClass) { if (typeof panelClass != "function") { if (Monocle.Browser.on.Kindle3) { panelClass = Monocle.Panels.eInk; } panelClass = panelClass || k.DEFAULT_PANELS_CLASS; } if (!panelClass) { throw("Panels not found."); } p.panels = new panelClass(API, { 'end': turn }); } function page() { return p.reader.dom.find('page'); } function turn(dir) { p.reader.selection.deselect(); moveTo({ page: getPlace().pageNumber() + dir}); p.reader.dispatchEvent('monocle:turning'); } function frameToLocus(locus) { page().m.dimensions.translateToLocus(locus); Monocle.defer(function () { p.reader.dispatchEvent('monocle:turn'); }); } // THIS IS THE CORE API THAT ALL FLIPPERS MUST PROVIDE. API.pageCount = p.pageCount; API.addPage = addPage; API.getPlace = getPlace; API.moveTo = moveTo; API.listenForInteraction = listenForInteraction; initialize(); return API; } Monocle.Flippers.Instant.FORWARDS = 1; Monocle.Flippers.Instant.BACKWARDS = -1; Monocle.Flippers.Instant.DEFAULT_PANELS_CLASS = Monocle.Panels.TwoPane;