diff --git a/resources/monocle/scripts/monocore.js b/resources/monocle/scripts/monocore.js
new file mode 100644
index 0000000..ae8b645
--- /dev/null
+++ b/resources/monocle/scripts/monocore.js
@@ -0,0 +1,5641 @@
+/*!
+ * 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;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/monocle/scripts/monoctrl.js b/resources/monocle/scripts/monoctrl.js
new file mode 100644
index 0000000..5e71552
--- /dev/null
+++ b/resources/monocle/scripts/monoctrl.js
@@ -0,0 +1,985 @@
+Monocle.Controls.Contents = function (reader) {
+
+ var API = { constructor: Monocle.Controls.Contents }
+ var k = API.constants = API.constructor;
+ var p = API.properties = {
+ reader: reader
+ }
+
+
+ function createControlElements() {
+ var div = reader.dom.make('div', 'controls_contents_container');
+ contentsForBook(div, reader.getBook());
+ return div;
+ }
+
+
+ function contentsForBook(div, book) {
+ while (div.hasChildNodes()) {
+ div.removeChild(div.firstChild);
+ }
+ var list = div.dom.append('ol', 'controls_contents_list');
+
+ var contents = book.properties.contents;
+ for (var i = 0; i < contents.length; ++i) {
+ chapterBuilder(list, contents[i], 0);
+ }
+ }
+
+
+ function chapterBuilder(list, chp, padLvl) {
+ var index = list.childNodes.length;
+ var li = list.dom.append('li', 'controls_contents_chapter', index);
+ var span = li.dom.append(
+ 'span',
+ 'controls_contents_chapterTitle',
+ index,
+ { html: chp.title }
+ );
+ span.style.paddingLeft = padLvl + "em";
+
+ var invoked = function () {
+ p.reader.skipToChapter(chp.src);
+ p.reader.hideControl(API);
+ }
+
+ Monocle.Events.listenForTap(li, invoked, 'controls_contents_chapter_active');
+
+ if (chp.children) {
+ for (var i = 0; i < chp.children.length; ++i) {
+ chapterBuilder(list, chp.children[i], padLvl + 1);
+ }
+ }
+ }
+
+
+ API.createControlElements = createControlElements;
+
+ return API;
+}
+;
+Monocle.Controls.Magnifier = function (reader) {
+
+ var API = { constructor: Monocle.Controls.Magnifier }
+ var k = API.constants = API.constructor;
+ var p = API.properties = {
+ buttons: [],
+ magnified: false
+ }
+
+
+ function initialize() {
+ p.reader = reader;
+ }
+
+
+ function createControlElements(holder) {
+ var btn = holder.dom.make('div', 'controls_magnifier_button');
+ btn.smallA = btn.dom.append('span', 'controls_magnifier_a', { text: 'A' });
+ btn.largeA = btn.dom.append('span', 'controls_magnifier_A', { text: 'A' });
+ p.buttons.push(btn);
+ Monocle.Events.listenForTap(btn, toggleMagnification);
+ return btn;
+ }
+
+
+ function toggleMagnification(evt) {
+ var opacities;
+ p.magnified = !p.magnified;
+ if (p.magnified) {
+ opacities = [0.3, 1];
+ p.reader.formatting.setFontScale(k.MAGNIFICATION, true);
+ } else {
+ opacities = [1, 0.3];
+ p.reader.formatting.setFontScale(null, true);
+ }
+
+ for (var i = 0; i < p.buttons.length; i++) {
+ p.buttons[i].smallA.style.opacity = opacities[0];
+ p.buttons[i].largeA.style.opacity = opacities[1];
+ }
+ }
+
+ API.createControlElements = createControlElements;
+
+ initialize();
+
+ return API;
+}
+
+
+Monocle.Controls.Magnifier.MAGNIFICATION = 1.2;
+// 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%'
+}
+;
+Monocle.Controls.PlaceSaver = function (bookId) {
+
+ var API = { constructor: Monocle.Controls.PlaceSaver }
+ var k = API.constants = API.constructor;
+ var p = API.properties = {}
+
+
+ function initialize() {
+ applyToBook(bookId);
+ }
+
+
+ function assignToReader(reader) {
+ p.reader = reader;
+ p.reader.listen('monocle:turn', savePlaceToCookie);
+ }
+
+
+ function applyToBook(bookId) {
+ p.bkTitle = bookId.toLowerCase().replace(/[^a-z0-9]/g, '');
+ p.prefix = k.COOKIE_NAMESPACE + p.bkTitle + ".";
+ }
+
+
+ function setCookie(key, value, days) {
+ var expires = "";
+ if (days) {
+ var d = new Date();
+ d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
+ expires = "; expires="+d.toGMTString();
+ }
+ var path = "; path=/";
+ document.cookie = p.prefix + key + "=" + value + expires + path;
+ return value;
+ }
+
+
+ function getCookie(key) {
+ if (!document.cookie) {
+ return null;
+ }
+ var regex = new RegExp(p.prefix + key + "=(.+?)(;|$)");
+ var matches = document.cookie.match(regex);
+ if (matches) {
+ return matches[1];
+ } else {
+ return null;
+ }
+ }
+
+
+ function savePlaceToCookie() {
+ var place = p.reader.getPlace();
+ setCookie(
+ "component",
+ encodeURIComponent(place.componentId()),
+ k.COOKIE_EXPIRES_IN_DAYS
+ );
+ setCookie(
+ "percent",
+ place.percentageThrough(),
+ k.COOKIE_EXPIRES_IN_DAYS
+ );
+ }
+
+
+ function savedPlace() {
+ var locus = {
+ componentId: getCookie('component'),
+ percent: getCookie('percent')
+ }
+ if (locus.componentId && locus.percent) {
+ locus.componentId = decodeURIComponent(locus.componentId);
+ locus.percent = parseFloat(locus.percent);
+ return locus;
+ } else {
+ return null;
+ }
+ }
+
+
+ function restorePlace() {
+ var locus = savedPlace();
+ if (locus) {
+ p.reader.moveTo(locus);
+ }
+ }
+
+
+ API.assignToReader = assignToReader;
+ API.savedPlace = savedPlace;
+ API.restorePlace = restorePlace;
+
+ initialize();
+
+ return API;
+}
+
+Monocle.Controls.PlaceSaver.COOKIE_NAMESPACE = "monocle.controls.placesaver.";
+Monocle.Controls.PlaceSaver.COOKIE_EXPIRES_IN_DAYS = 7; // Set to 0 for session-based expiry.
+;
+Monocle.Controls.Scrubber = function (reader) {
+
+ var API = { constructor: Monocle.Controls.Scrubber }
+ var k = API.constants = API.constructor;
+ var p = API.properties = {}
+
+
+ function initialize() {
+ p.reader = reader;
+ p.reader.listen('monocle:turn', updateNeedles);
+ updateNeedles();
+ }
+
+
+ function pixelToPlace(x, cntr) {
+ if (!p.componentIds) {
+ p.componentIds = p.reader.getBook().properties.componentIds;
+ p.componentWidth = 100 / p.componentIds.length;
+ }
+ var pc = (x / cntr.offsetWidth) * 100;
+ var cmpt = p.componentIds[Math.floor(pc / p.componentWidth)];
+ var cmptPc = ((pc % p.componentWidth) / p.componentWidth);
+ return { componentId: cmpt, percentageThrough: cmptPc };
+ }
+
+
+ function placeToPixel(place, cntr) {
+ if (!p.componentIds) {
+ p.componentIds = p.reader.getBook().properties.componentIds;
+ p.componentWidth = 100 / p.componentIds.length;
+ }
+ var componentIndex = p.componentIds.indexOf(place.componentId());
+ var pc = p.componentWidth * componentIndex;
+ pc += place.percentageThrough() * p.componentWidth;
+ return Math.round((pc / 100) * cntr.offsetWidth);
+ }
+
+
+ function updateNeedles() {
+ if (p.hidden || !p.reader.dom.find(k.CLS.container)) {
+ return;
+ }
+ var place = p.reader.getPlace();
+ var x = placeToPixel(place, p.reader.dom.find(k.CLS.container));
+ var needle, i = 0;
+ for (var i = 0, needle; needle = p.reader.dom.find(k.CLS.needle, i); ++i) {
+ setX(needle, x - needle.offsetWidth / 2);
+ p.reader.dom.find(k.CLS.trail, i).style.width = x + "px";
+ }
+ }
+
+
+ function setX(node, x) {
+ var cntr = p.reader.dom.find(k.CLS.container);
+ x = Math.min(cntr.offsetWidth - node.offsetWidth, x);
+ x = Math.max(x, 0);
+ Monocle.Styles.setX(node, x);
+ }
+
+
+ function createControlElements(holder) {
+ var cntr = holder.dom.make('div', k.CLS.container);
+ var track = cntr.dom.append('div', k.CLS.track);
+ var needleTrail = cntr.dom.append('div', k.CLS.trail);
+ var needle = cntr.dom.append('div', k.CLS.needle);
+ var bubble = cntr.dom.append('div', k.CLS.bubble);
+
+ var cntrListeners, bodyListeners;
+
+ var moveEvt = function (evt, x) {
+ evt.preventDefault();
+ x = (typeof x == "number") ? x : evt.m.registrantX;
+ var place = pixelToPlace(x, cntr);
+ setX(needle, x - needle.offsetWidth / 2);
+ var book = p.reader.getBook();
+ var chps = book.chaptersForComponent(place.componentId);
+ var cmptIndex = p.componentIds.indexOf(place.componentId);
+ var chp = chps[Math.floor(chps.length * place.percentageThrough)];
+ if (cmptIndex > -1 && book.properties.components[cmptIndex]) {
+ var actualPlace = Monocle.Place.FromPercentageThrough(
+ book.properties.components[cmptIndex],
+ place.percentageThrough
+ );
+ chp = actualPlace.chapterInfo() || chp;
+ }
+
+ if (chp) {
+ bubble.innerHTML = chp.title;
+ }
+ setX(bubble, x - bubble.offsetWidth / 2);
+
+ p.lastX = x;
+ return place;
+ }
+
+ var endEvt = function (evt) {
+ var place = moveEvt(evt, p.lastX);
+ p.reader.moveTo({
+ percent: place.percentageThrough,
+ componentId: place.componentId
+ });
+ Monocle.Events.deafenForContact(cntr, cntrListeners);
+ Monocle.Events.deafenForContact(document.body, bodyListeners);
+ bubble.style.display = "none";
+ }
+
+ var startFn = function (evt) {
+ bubble.style.display = "block";
+ moveEvt(evt);
+ cntrListeners = Monocle.Events.listenForContact(
+ cntr,
+ { move: moveEvt }
+ );
+ bodyListeners = Monocle.Events.listenForContact(
+ document.body,
+ { end: endEvt }
+ );
+ }
+
+ Monocle.Events.listenForContact(cntr, { start: startFn });
+
+ return cntr;
+ }
+
+
+ API.createControlElements = createControlElements;
+ API.updateNeedles = updateNeedles;
+
+ initialize();
+
+ return API;
+}
+
+Monocle.Controls.Scrubber.CLS = {
+ container: 'controls_scrubber_container',
+ track: 'controls_scrubber_track',
+ needle: 'controls_scrubber_needle',
+ trail: 'controls_scrubber_trail',
+ bubble: 'controls_scrubber_bubble'
+}
+;
+Monocle.Controls.Spinner = function (reader) {
+
+ var API = { constructor: Monocle.Controls.Spinner }
+ var k = API.constants = API.constructor;
+ var p = API.properties = {
+ reader: reader,
+ divs: [],
+ repeaters: {},
+ showForPages: []
+ }
+
+
+ function createControlElements(cntr) {
+ var anim = cntr.dom.make('div', 'controls_spinner_anim');
+ anim.dom.append('div', 'controls_spinner_inner');
+ p.divs.push(anim);
+ return anim;
+ }
+
+
+ function registerSpinEvent(startEvtType, stopEvtType) {
+ var label = startEvtType;
+ p.reader.listen(startEvtType, function (evt) { spin(label, evt) });
+ p.reader.listen(stopEvtType, function (evt) { spun(label, evt) });
+ }
+
+
+ // Registers spin/spun event handlers for certain time-consuming events.
+ //
+ function listenForUsualDelays() {
+ registerSpinEvent('monocle:componentloading', 'monocle:componentloaded');
+ registerSpinEvent('monocle:componentchanging', 'monocle:componentchange');
+ registerSpinEvent('monocle:resizing', 'monocle:resize');
+ registerSpinEvent('monocle:jumping', 'monocle:jump');
+ registerSpinEvent('monocle:recalculating', 'monocle:recalculated');
+ p.reader.listen('monocle:notfound', forceSpun);
+ p.reader.listen('monocle:componentfailed', forceSpun);
+ }
+
+
+ // Displays the spinner. Both arguments are optional.
+ //
+ function spin(label, evt) {
+ label = label || k.GENERIC_LABEL;
+ p.repeaters[label] = true;
+ p.reader.showControl(API);
+
+ // If the delay is on a page other than the page we've been assigned to,
+ // don't show the animation. p.global ensures that if an event affects
+ // all pages, the animation is always shown, even if other events in this
+ // spin cycle are page-specific.
+ var page = (evt && evt.m && evt.m.page) ? evt.m.page : null;
+ if (page && p.divs.length > 1) {
+ p.showForPages[page.m.pageIndex] = true;
+ } else {
+ p.global = true;
+ p.reader.dispatchEvent('monocle:modal:on');
+ }
+ for (var i = 0; i < p.divs.length; ++i) {
+ var show = (p.global || p.showForPages[i]) ? true : false;
+ p.divs[i].dom[show ? 'removeClass' : 'addClass']('dormant');
+ }
+ }
+
+
+ // Stops displaying the spinner. Both arguments are optional.
+ //
+ function spun(label, evt) {
+ label = label || k.GENERIC_LABEL;
+ p.repeaters[label] = false;
+ for (var l in p.repeaters) {
+ if (p.repeaters[l]) { return; }
+ }
+ forceSpun();
+ }
+
+
+ function forceSpun() {
+ if (p.global) { p.reader.dispatchEvent('monocle:modal:off'); }
+ p.global = false;
+ p.repeaters = {};
+ p.showForPages = [];
+ for (var i = 0; i < p.divs.length; ++i) {
+ p.divs[i].dom.addClass('dormant');
+ }
+ }
+
+
+ API.createControlElements = createControlElements;
+ API.registerSpinEvent = registerSpinEvent;
+ API.listenForUsualDelays = listenForUsualDelays;
+ API.spin = spin;
+ API.spun = spun;
+ API.forceSpun = forceSpun;
+
+ return API;
+}
+
+Monocle.Controls.Spinner.GENERIC_LABEL = "generic";
+Monocle.Controls.Stencil = function (reader, behaviorClasses) {
+
+ var API = { constructor: Monocle.Controls.Stencil }
+ var k = API.constants = API.constructor;
+ var p = API.properties = {
+ reader: reader,
+ behaviors: [],
+ components: {},
+ masks: []
+ }
+
+
+ // Create the stencil container and listen for draw/update events.
+ //
+ function createControlElements(holder) {
+ behaviorClasses = behaviorClasses || k.DEFAULT_BEHAVIORS;
+ for (var i = 0, ii = behaviorClasses.length; i < ii; ++i) {
+ addBehavior(behaviorClasses[i]);
+ }
+ p.container = holder.dom.make('div', k.CLS.container);
+ p.reader.listen('monocle:turning', hide);
+ p.reader.listen('monocle:turn:cancel', show);
+ p.reader.listen('monocle:turn', update);
+ p.reader.listen('monocle:stylesheetchange', update);
+ p.reader.listen('monocle:resize', update);
+ update();
+ return p.container;
+ }
+
+
+ // Pass this method an object that responds to 'findElements(doc)' with
+ // an array of DOM elements for that document, and to 'fitMask(elem, mask)'.
+ //
+ // After you have added all your behaviors this way, you would typically
+ // call update() to make them take effect immediately.
+ //
+ function addBehavior(bhvrClass) {
+ var bhvr = new bhvrClass(API);
+ if (typeof bhvr.findElements != 'function') {
+ console.warn('Missing "findElements" method for behavior: %o', bhvr);
+ }
+ if (typeof bhvr.fitMask != 'function') {
+ console.warn('Missing "fitMask" method for behavior: %o', bhvr);
+ }
+ p.behaviors.push(bhvr);
+ }
+
+
+ // Resets any pre-calculated rectangles for the active component,
+ // recalculates them, and forces masks to be "drawn" (moved into the new
+ // rectangular locations).
+ //
+ function update() {
+ var visPages = p.reader.visiblePages();
+ if (!visPages || !visPages.length) { return; }
+ var pageDiv = visPages[0];
+ var cmptId = pageComponentId(pageDiv);
+ if (!cmptId) { return; }
+ p.components[cmptId] = null;
+ calculateRectangles(pageDiv);
+ draw();
+ }
+
+
+ function hide() {
+ p.container.style.display = 'none';
+ }
+
+
+ function show() {
+ p.container.style.display = 'block';
+ }
+
+
+ // Removes any existing masks.
+ function clear() {
+ while (p.container.childNodes.length) {
+ p.container.removeChild(p.container.lastChild);
+ }
+ }
+
+
+ // Aligns the stencil container to the shape of the page, then moves the
+ // masks to sit above any currently visible rectangles.
+ //
+ function draw() {
+ var pageDiv = p.reader.visiblePages()[0];
+ var cmptId = pageComponentId(pageDiv);
+ if (!p.components[cmptId]) {
+ return;
+ }
+
+ // Position the container.
+ alignToComponent(pageDiv);
+
+ // Clear old masks.
+ clear();
+
+ // Layout the masks.
+ if (!p.disabled) {
+ show();
+ var rects = p.components[cmptId];
+ if (rects && rects.length) {
+ layoutRectangles(pageDiv, rects);
+ }
+ }
+ }
+
+
+ // Iterate over all the elements in the active component, and
+ // create an array of rectangular points corresponding to their positions.
+ //
+ function calculateRectangles(pageDiv) {
+ var cmptId = pageComponentId(pageDiv);
+ if (!p.components[cmptId]) {
+ p.components[cmptId] = [];
+ } else {
+ return;
+ }
+
+ var doc = pageDiv.m.activeFrame.contentDocument;
+ var offset = getOffset(pageDiv);
+
+ for (var b = 0, bb = p.behaviors.length; b < bb; ++b) {
+ var bhvr = p.behaviors[b];
+ var elems = bhvr.findElements(doc);
+ for (var i = 0; i < elems.length; ++i) {
+ var elem = elems[i];
+ if (elem.getClientRects) {
+ var r = elem.getClientRects();
+ for (var j = 0; j < r.length; j++) {
+ p.components[cmptId].push({
+ element: elem,
+ behavior: bhvr,
+ left: Math.ceil(r[j].left + offset.l),
+ top: Math.ceil(r[j].top),
+ width: Math.floor(r[j].width),
+ height: Math.floor(r[j].height)
+ });
+ }
+ }
+ }
+ }
+
+ return p.components[cmptId];
+ }
+
+
+ // Update location of visible rectangles - creating as required.
+ //
+ function layoutRectangles(pageDiv, rects) {
+ var offset = getOffset(pageDiv);
+ var visRects = [];
+ for (var i = 0; i < rects.length; ++i) {
+ if (rectVisible(rects[i], offset.l, offset.l + offset.w)) {
+ visRects.push(rects[i]);
+ }
+ }
+
+ for (i = 0; i < visRects.length; ++i) {
+ var r = visRects[i];
+ var cr = {
+ left: r.left - offset.l,
+ top: r.top,
+ width: r.width,
+ height: r.height
+ };
+ var mask = createMask(r.element, r.behavior);
+ mask.dom.setStyles({
+ display: 'block',
+ left: cr.left+"px",
+ top: cr.top+"px",
+ width: cr.width+"px",
+ height: cr.height+"px",
+ position: 'absolute'
+ });
+ mask.stencilRect = cr;
+ }
+ }
+
+
+ // Find the offset position in pixels from the left of the current page.
+ //
+ function getOffset(pageDiv) {
+ return {
+ l: pageDiv.m.offset || 0,
+ w: pageDiv.m.dimensions.properties.width
+ };
+ }
+
+
+ // Is this area presently on the screen?
+ //
+ function rectVisible(rect, l, r) {
+ return rect.left >= l && rect.left < r;
+ }
+
+
+ // Returns the active component id for the given page, or the current
+ // page if no argument passed in.
+ //
+ function pageComponentId(pageDiv) {
+ pageDiv = pageDiv || p.reader.visiblePages()[0];
+ if (!pageDiv.m.activeFrame.m.component) { return; }
+ return pageDiv.m.activeFrame.m.component.properties.id;
+ }
+
+
+ // Positions the stencil container over the active frame.
+ //
+ function alignToComponent(pageDiv) {
+ cmpt = pageDiv.m.activeFrame.parentNode;
+ p.container.dom.setStyles({
+ left: cmpt.offsetLeft+"px",
+ top: cmpt.offsetTop+"px"
+ });
+ }
+
+
+ function createMask(element, bhvr) {
+ var mask = p.container.dom.append(bhvr.maskTagName || 'div', k.CLS.mask);
+ Monocle.Events.listenForContact(mask, {
+ start: function () { p.reader.dispatchEvent('monocle:magic:halt'); },
+ move: function (evt) { evt.preventDefault(); },
+ end: function () { p.reader.dispatchEvent('monocle:magic:init'); }
+ });
+ bhvr.fitMask(element, mask);
+ return mask;
+ }
+
+
+ // Make the active masks visible (by giving them a class -- override style
+ // in monoctrl.css).
+ //
+ function toggleHighlights() {
+ var cls = k.CLS.highlights;
+ if (p.container.dom.hasClass(cls)) {
+ p.container.dom.removeClass(cls);
+ } else {
+ p.container.dom.addClass(cls);
+ }
+ }
+
+
+ function disable() {
+ p.disabled = true;
+ draw();
+ }
+
+
+ function enable() {
+ p.disabled = false;
+ draw();
+ }
+
+
+ function filterElement(elem, behavior) {
+ if (typeof behavior.filterElement == 'function') {
+ return behavior.filterElement(elem);
+ }
+ return elem;
+ }
+
+
+ function maskAssigned(elem, mask, behavior) {
+ if (typeof behavior.maskAssigned == 'function') {
+ return behavior.maskAssigned(elem, mask);
+ }
+ return false;
+ }
+
+
+ API.createControlElements = createControlElements;
+ API.addBehavior = addBehavior;
+ API.draw = draw;
+ API.update = update;
+ API.toggleHighlights = toggleHighlights;
+
+ return API;
+}
+
+
+Monocle.Controls.Stencil.CLS = {
+ container: 'controls_stencil_container',
+ mask: 'controls_stencil_mask',
+ highlights: 'controls_stencil_highlighted'
+}
+
+
+
+Monocle.Controls.Stencil.Links = function (stencil) {
+ var API = { constructor: Monocle.Controls.Stencil.Links }
+
+ // Optionally specify the HTML tagname of the mask.
+ API.maskTagName = 'a';
+
+ // Returns an array of all the elements in the given doc that should
+ // be covered with a stencil mask for interactivity.
+ //
+ // (Hint: doc.querySelectorAll() is your friend.)
+ //
+ API.findElements = function (doc) {
+ return doc.querySelectorAll('a[href]');
+ }
+
+
+ // Return an element. It should usually be a child of the container element,
+ // with a className of the given maskClass. You set up the interactivity of
+ // the mask element here.
+ //
+ API.fitMask = function (link, mask) {
+ var hrefObject = deconstructHref(link);
+
+ if (hrefObject.internal) {
+ mask.setAttribute('href', 'javascript:"Skip to chapter"');
+ mask.onclick = function (evt) {
+ stencil.properties.reader.skipToChapter(hrefObject.internal);
+ evt.preventDefault();
+ return false;
+ }
+ } else {
+ mask.setAttribute('href', hrefObject.external);
+ mask.setAttribute('target', '_blank');
+ mask.onclick = function (evt) { return true; }
+ }
+
+ link.onclick = function (evt) {
+ evt.preventDefault();
+ return false;
+ }
+ }
+
+
+ // Returns an object with either:
+ //
+ // - an 'external' property -- an absolute URL with a protocol,
+ // host & etc, which should be treated as an external resource (eg,
+ // open in new window)
+ //
+ // OR
+ //
+ // - an 'internal' property -- a relative URL (with optional hash anchor),
+ // that is treated as a link to component in the book
+ //
+ // A weird but useful property of tags is that while
+ // link.getAttribute('href') will return the actual string value of the
+ // attribute (eg, 'foo.html'), link.href will return the absolute URL (eg,
+ // 'http://example.com/monocles/foo.html').
+ //
+ function deconstructHref(elem) {
+ var loc = document.location;
+ var origin = loc.protocol+'//'+loc.host;
+ var href = elem.href;
+ var path = href.substring(origin.length);
+ var ext = { external: href };
+
+ // Anchor tags with 'target' attributes are always external URLs.
+ if (elem.getAttribute('target')) {
+ return ext;
+ }
+ // URLs with a different protocol or domain are always external.
+ //console.log("Domain test: %s <=> %s", origin, href);
+ if (href.indexOf(origin) != 0) {
+ return ext;
+ }
+
+ // If it is in a sub-path of the current path, it's internal.
+ var topPath = loc.pathname.replace(/[^\/]*\.[^\/]+$/,'');
+ if (topPath[topPath.length - 1] != '/') {
+ topPath += '/';
+ }
+ //console.log("Sub-path test: %s <=> %s", topPath, path);
+ if (path.indexOf(topPath) == 0) {
+ return { internal: path.substring(topPath.length) }
+ }
+
+ // If it's a root-relative URL and it's in our list of component ids,
+ // it's internal.
+ var cmptIds = stencil.properties.reader.getBook().properties.componentIds;
+ for (var i = 0, ii = cmptIds.length; i < ii; ++i) {
+ //console.log("Component test: %s <=> %s", cmptIds[i], path);
+ if (path.indexOf(cmptIds[i]) == 0) {
+ return { internal: path }
+ }
+ }
+
+ // Otherwise it's external.
+ return ext;
+ }
+
+
+ return API;
+}
+
+
+Monocle.Controls.Stencil.DEFAULT_BEHAVIORS = [Monocle.Controls.Stencil.Links];
diff --git a/resources/monocle/styles/monocore.css b/resources/monocle/styles/monocore.css
new file mode 100644
index 0000000..02b4951
--- /dev/null
+++ b/resources/monocle/styles/monocore.css
@@ -0,0 +1,195 @@
+/*===========================================================================
+
+This is a base-level Monocle stylesheet. It assumes no class-prefix has been
+given to the Reader during initialisation - if one has, you can copy and
+modify this stylesheet accordingly.
+
+---------------------------------------------------------------------------*/
+
+/* The reader object that holds pretty much everything.
+ * (A direct child of the element passed to reader initialisation). */
+
+div.monelem_container {
+ background-color: black;
+}
+
+
+/* The div that mimics a leaf of paper in a book. */
+div.monelem_page {
+ background: white;
+ top: 0;
+ left: 0;
+ bottom: 3px;
+ right: 5px;
+ border-right: 1px solid #999;
+}
+
+
+/* The div within the page that determines page margins. */
+div.monelem_sheaf {
+ top: 1em;
+ left: 1em;
+ bottom: 1em;
+ right: 1em;
+}
+
+
+/* The iframe within the page that loads the content of the book. */
+div.monelem_component {
+}
+
+
+/* A panel that sits above the entire reader object, holding controls. */
+div.monelem_overlay {
+}
+
+
+/* A full-size panel to display an announcement (iframe or div) */
+div.monelem_billboard_container {
+ background: #FFF;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ z-index: 2000;
+ -webkit-transform: scale(0);
+ -moz-transform: scale(0);
+ transform: scale(0);
+ -webkit-transform-origin: -0 -0;
+ -moz-transform-origin: -0 -0;
+ transform-origin: -0 -0;
+}
+
+.monelem_billboard_inner {
+ height: 100%;
+ width: 100%;
+ border: none;
+ overflow: auto;
+ /*-webkit-overflow-scrolling: touch;*/ /* This is sexy, but crashy. */
+}
+
+div.monelem_billboard_inner {
+ min-width: 100%;
+ min-height: 100%;
+ text-align: center;
+ vertical-align: middle;
+ display: -webkit-box;
+ -webkit-box-pack: center;
+ -webkit-box-align: center;
+}
+
+
+div.monelem_billboard_close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 50px;
+ height: 30px;
+ color: white;
+ background: #C00;
+ cursor: pointer;
+ border-bottom-left-radius: 4px;
+ text-shadow: 1px 1px 1px #900;
+ font: 9pt Helvetica Neue, Helvetica, sans-serif;
+}
+
+div.monelem_billboard_close:after {
+ display: block;
+ content: 'Close';
+ width: 100%;
+ line-height: 30px;
+ text-align: center;
+}
+
+div.monelem_book_fatality {
+ font-family: Helvetica Neue, Helvetica, sans-serif;
+ margin: 0 auto;
+ max-width: 75%;
+}
+
+div.monelem_book_fatality p {
+ line-height: 1.4;
+}
+
+
+/*===========================================================================
+ PANELS
+---------------------------------------------------------------------------*/
+
+
+.monelem_panels_imode_panel {
+ background: rgba(255,255,255,0.7);
+ opacity: 0;
+}
+
+.monelem_panels_imode_backwardsPanel {
+ -webkit-box-shadow: 1px 1px 3px #777;
+ -moz-box-shadow: 1px 1px 3px #777;
+ box-shadow: 1px 1px 3px #777;
+}
+
+.monelem_panels_imode_forwardsPanel {
+ -webkit-box-shadow: -1px 1px 3px #777;
+ -moz-box-shadow: -1px 1px 3px #777;
+ box-shadow: -1px 1px 3px #777;
+}
+
+.monelem_panels_imode_centralPanel {
+}
+
+.monelem_panels_imode_toggleIcon {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ width: 50px;
+ height: 50px;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+
+/* If you modify this you could significantly change the way panels work. */
+div.monelem_controls_panel_expanded {
+ left: 0 !important;
+ width: 100% !important;
+ z-index: 1001 !important;
+}
+
+/*===========================================================================
+ Flippers
+---------------------------------------------------------------------------*/
+
+div.monelem_flippers_slider_wait {
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ width: 92px;
+ height: 112px;
+ background-repeat: no-repeat;
+ -webkit-background-size: 100%;
+ -moz-background-size: 100%;
+ background-size: 100%;
+}
+
+@media screen and (max-width: 640px) {
+ div.monelem_flippers_slider_wait {
+ width: 61px;
+ height: 75px;
+ }
+}
+
+
+/*===========================================================================
+ DATA URIs
+
+ These are data-uri packed images, inlined for loading speed and simplicity.
+ Placed at the end of this file because they're visually noisy...
+---------------------------------------------------------------------------*/
+
+div.monelem_panels_imode_toggleIcon {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAaCAYAAABPY4eKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1%2B%2FAAAABV0RVh0Q3JlYXRpb24gVGltZQAzMC82LzEwBMfmVwAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAANYSURBVEiJtdZbiNVVFMfxj8cx85JkIGlqSESgOGA9WIQgGmTRUyRaYFJDnUWYGV2eyiCpkIbEKJI1UqYvUkmFDxFBgpghonajSDCM7hcxLSnt4ulh%2F2c4HufMTOH8Xs75%2F%2Ffa67v3%2Bu%2B91hphGJWZNUzCXJyKiHd6xxqNhhGDTB6NOViAyzARY3EaP%2BNL7MCBiPi9Ze4leBlTsR9jcCnuiYgDbeGZeV4F7EINe7EP3%2BJ49W4GrsZ8NPAGXouIk5k5F93YFhHPVT5H4kbcjaX1ev3kWfDMPB9P4ko8ERE7BopONWcOVmMc1uBRrG8Oc5Ptq1hdr9cPdrQMTMUWfBQRCweD9ioiPsQtmbkeu7G8P3ClsZSI98EzcxqeUsLXM1RwZs7ErRiJKXgQN2Tmzoj4qsV2Hn7BYcq369UaHIqI5yPizyGCx2MPfsRVOBoR6%2FA%2BNmXmqCbbm%2FAiMiJO9cEzcwEuwLODwMZk5oXVLYA6PouIF%2FC6cvBgI37D0mreStyJroh4r9df785XYGtEHG8Hfnjb1w08Xu2qq3regtOZuaka2whV5NZieWY%2BhkV4ICJ2N%2FusZeYMJQm8NdCuuxdPH4HENGzsXjx9REQcqRxvR2dEfNBrHxF7lHywGPXW7085cEvwZkScHAheaRz%2BwngcqyAnlEPan%2Fbh5oj4rr%2FBDlyOXUMA%2Fx%2F9oFytM5SZs3t6epbWlOtxeJjg%2BzEmMye3vF%2BCYx2YhdFnTTs3OoQT2JqZ3TiC2zETyzrwrnIwhkMTqwVsxW24GLsiYmWj0dCBo2gNy7nSRfgpIjZjM6WU1ut1lHt%2BGLOHCd6J79sN1pSkMSUzJwwD%2FBoD5I9aRHyiFIVFQ3D2j1KR%2Fh7MMDPnY1JE7GwLr3434N5BnI3GFRiFzuai0Ub34aWBDGr0pcKPM%2FPpqovpT11KoVinNAvXt1lkLTNXKFesXU1HUz3HI0plWqW0QGcoIjYoERpMy7AS17b2da06o43KzLF4RanRzwwx3%2FfOHYW7lL5ubUR83p9do9Ho%2B99fDzcZDynfdxPejog%2FBoCOxHW4AxOwKiK%2BaGc%2FILzJ6ULcXznciwM4qFSzCUob3Km0UCeU3W5v5%2B8%2FwZsWMQvzlN1Nq8C%2F4ht8qkRm72B%2B%2BoP%2FC0sEOftJmUbfAAAAAElFTkSuQmCC);
+}
+
+div.monelem_flippers_slider_wait {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFwAAABwCAMAAACkX%2BcnAAAB0VBMVEUAAACDg4OEhISFhYWGhoaHh4eIiIiJiIiJiYmKioqLi4uMjIyNjY2Ojo6Pj4%2BQkJCRkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqbm5ucnJycnJ2dnZ2dnZ6dnZ%2Benp6enp%2Bfn5%2Bfn6CgoKCgoKGhoKChoaGioqKjo6OkpKSlpaWmpaWmpqaoqKiqqqqrq6usrKytra2urq6wsLCxsbGzs7O0tLS0tLW1tbW1tba1tbe2tri4uLi4uLm4uLq6ury7u7y8vLy8vL28vL%2B9vb2%2Bvr6%2Bvr%2B%2Fv7%2B%2Fv8HAwMDAwMLAwMPBwcPCwsPExMTExMXFxcXGxsbHx8fIyMjJycrOztDOztHPz9DPz9HR0dTS0tTT09TT09XU1NbU1NfV1dfW1tjW1tnX19fX19rY2Nra2tva2tzd3eDe3t7f39%2Fh4eHi4uLl5enn5%2Bnp6ezp6e3q6u3q6u7r6%2B7r6%2B%2Fs7O%2Fs7PDt7fDt7fHu7vHu7vLv7%2B%2Fv7%2FLv7%2FPw8PDw8PPw8PTx8fTx8fXy8vXy8vbz8%2Fbz8%2Ff09Pf09Pj19fj19fn29vn39%2Fn39%2Fr4%2BPr4%2BPv5%2Bfv6%2Bvv6%2Bvz7%2B%2Fz7%2B%2F38%2FP39%2Ff39%2Ff7%2B%2Fv7%2B%2Fv%2F%2F%2F%2F%2BHSJEZAAAAAXRSTlMAQObYZgAAA5dJREFUaN61lk1uE0EQhd%2BrsQlREAgkFkQKLJByteQU3IIdd2OBYIFASFmAFLurWPT0uOfXme6aWUXy6PNL9XPXR3z6DSI93wQ0GkHjzweapM%2B%2Btn8SMAERPzKQQKN7IDRhD2APgkbumucvXp24T3s%2BH47H7%2F9U1AxmpvaDzV5IUMBfD0CbQXYPly93K%2BEiwneqphpMVc3e7p492zciQhGKNN2bX%2F42shJOEQFIQgAKgfgdpvFz7d58%2FPO4Fn5PiggBAUkAYhoUMJipwU5vhsfjWjhESMTsBChQVVMDYICadfjD4VAAFyGYZVcN7Vzar4iP6frkd5RuLjG7WlCFwdSy4ICtPlBAKJLNhYBq6HKf8IHrx4J7IQX5maqFLHeC3yrWwyEiFACSzlTVVFNuzQZTAG%2BrLoQwVT1kubvGF4wlVj2vi2isuvWrbiXJIUISYKwL5qpuWgbvXQHxSCeqbiXwvOrpClC1QdXViuAQUnpXgE1U%2FSb%2BUwVVF7JfdTWN2G4uFyiaeZz6oOpB1drzTF0sSw6ySdc5Y%2FZe1SPeCpPfS6p6yq4arK16V5eyAwWEp6oTEKpqewXEygBW9iMabzsAZjqoOkuTL227tjJvSg8UaG%2FGhW33obSK8d4dVj1eAV3VrXQsuBtXvd12XdWteCxg2nbobbuU2xQsHst42zHe6lllypOnbcdUeZ62HUzNoOXJz4vdpZXDz4rde5TDz4rdsQ6%2BLHZNxVjOip3VJD8ndjVtOSt2rEp%2BRuxCHXxZ7G6tCr4sdhUX1xPETmvhC2KndWNZFjtUjmVR7KRyLItiF2qTL4ndtdXCF8Tuqhq%2BIHaonfmi2Ek1fEHsQjV8YdtVt2VR7DzgM2J36QCfFbsbB%2Fi82MEBPit2HvBZsfMYy6zYuSSfq7oLfE7sLpzgk2J37QKfETt1gc%2BJnQ98Rux84NNiJ07wSbELTvBpsXOCT4rdRz%2F4WOzMCz4pdl7wKbGDG3xC7NzGMiV2jvCx2PnNfELsbvzgY7FrHOFjsXOEj7YdHeFjsfOF96sePOFjsXOED8XutSt8sO2uXOFDsfOFD6ruCx9U3Rc%2BEDt3eC52zvC%2B2DnD%2B2LnDe9V3RveEzt3eC527vBc7NzhudhtAe%2BuAH94VnV%2FeCZ2G8BzscMmUxdgi5lnYrcF%2FCR2wCZHSvftP9x2m8DTttsEnsRuK7hs8%2FPPxG4beCt2G8HbbbcNPG67reAUEfwHRePBMkvuZ4wAAAAASUVORK5CYII%3D);
+}
diff --git a/resources/monocle/styles/monoctrl.css b/resources/monocle/styles/monoctrl.css
new file mode 100644
index 0000000..89e8ab6
--- /dev/null
+++ b/resources/monocle/styles/monoctrl.css
@@ -0,0 +1,169 @@
+/*===========================================================================
+ CONTROLS
+
+ The standard Monocle stylesheet for the optional Monocle controls. See
+ comments for monocore.css, which apply here too.
+---------------------------------------------------------------------------*/
+
+/* Contents */
+
+div.monelem_controls_contents_container {
+ position: absolute;
+ width: 75%;
+ height: 75%;
+ left: 12.5%;
+ top: 12.5%;
+ background: #EEE;
+ border: 2px solid #F7F7F7;
+ border-radius: 9px;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ -moz-border-radius: 9px;
+ -webkit-border-radius: 9px;
+ box-shadow: 1px 2px 6px rgba(0,0,0,0.5);
+ -moz-box-shadow: 1px 2px 6px rgba(0,0,0,0.5);
+ -webkit-box-shadow: 1px 2px 6px rgba(0,0,0,0.5);
+}
+
+ol.monelem_controls_contents_list {
+ margin: 6px;
+ padding: 0;
+}
+
+li.monelem_controls_contents_chapter {
+ list-style: none;
+ line-height: 220%;
+ padding-left: 1em;
+ padding-right: 2em;
+ border-bottom: 2px groove #FEFEFE;
+ cursor: pointer;
+}
+
+li.monelem_controls_contents_chapter_active {
+ background: #999;
+ color: white;
+}
+
+/* Magnifier */
+
+.monelem_controls_magnifier_button {
+ cursor: pointer;
+ color: #555;
+ position: absolute;
+ top: 2px;
+ right: 10px;
+ padding: 0 2px;
+}
+
+.monelem_controls_magnifier_a {
+ font-size: 11px;
+}
+
+.monelem_controls_magnifier_A {
+ font-size: 18px;
+ opacity: 0.3;
+}
+
+
+/* Spinner */
+
+.monelem_controls_spinner_anim {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: white;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+.monelem_controls_spinner_anim.monelem_dormant {
+ width: 0;
+ height: 0;
+}
+
+
+/* Scrubber */
+
+div.monelem_controls_scrubber_container {
+ position: absolute;
+ left: 1em;
+ right: 1em;
+ bottom: 4px;
+ height: 30px;
+ background: rgba(255,255,255,0.8);
+}
+
+div.monelem_controls_scrubber_track {
+ margin-top: 10px;
+ height: 5px;
+ border: 1px solid #999;
+ cursor: pointer;
+}
+
+div.monelem_controls_scrubber_needle {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ top: 5px;
+ background: #CCC;
+ border: 1px solid #999;
+ border-radius: 8px;
+ -moz-border-radius: 8px;
+ -webkit-border-radius: 8px;
+}
+
+div.monelem_controls_scrubber_trail {
+ position: absolute;
+ background: #DDD;
+ top: 11px;
+ left: 1px;
+ height: 5px;
+}
+
+div.monelem_controls_scrubber_bubble {
+ display: none;
+ position: absolute;
+ padding: 1em;
+ min-width: 20%;
+ max-width: 30%;
+ bottom: 2.5em;
+ background: rgba(0, 0, 0, 0.9);
+ color: #CCC;
+ font: bold 12px Lucida Grande, Tahoma, Helvetica, Arial, sans-serif;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+
+/* Stencil */
+div.monelem_controls_stencil_container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0;
+ height: 0;
+}
+
+.monelem_controls_stencil_mask {
+ display: block;
+ position: absolute;
+}
+
+div.monelem_controls_stencil_highlighted .monelem_controls_stencil_mask {
+ background: rgba(0,0,255,0.15);
+}
+
+
+/*===========================================================================
+ DATA URIs
+
+ These are data-uri packed images, inlined for loading speed and simplicity.
+ Placed at the end of this file because they're visually noisy...
+---------------------------------------------------------------------------*/
+
+div.monelem_controls_spinner_anim {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA0CAMAAAANBM47AAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M1cbXjNgAAABV0RVh0Q3JlYXRpb24gVGltZQAxNy81LzEwnOhoKAAAAE5QTFRFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxKKmWQAAABp0Uk5TAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBl0wLilAAAC8klEQVQYGZXBB2LjOAADQFCimtVFEoD//9HLbrJxipzoZoBToYptUwV8V/Xrsc8RP6i7aduPXHI69mWIAR9UY6Is5rnCuTBsWXeLkijbTFOLf7okW6R8zxEnwphskfIrifJdW4u/RtlpbGLsdjoHfDNkSZTSNg192w3jchSJEtcawCRzDvgjLPINX1SbSSvNXcC7eNuLXpQuTFbp8CZkH/isyS68H0PAF+0iUzxoNni33HPAR51UxDHgRLObslLEw3TPFT7oKPqIeOImURs+WJ0CHlqKXgLOxL4NgyRqxbuqeMNDXURPOBNWSokquRRP+GeVOzwcLlpwJmx3WVJuY2ZRi1ezfOBhdNGGU52ZhrloBzqSucKLerdLxLtIKlc4Nd9LA6wuNTC5aAbQZzs3eFhE9Tg3mw2wqkQgHCZrTJK3iIcoasMTvXX0E30EAK2k+Wbrho8mky2eCLslSz3+2ERKucVHIZsbnqp2WWXEX60ossMnrakeP+jGocabg9SGzyaXHHDRpOIO/zRjDWCTNlzVsLjFm4bODapE33BZoke8mVy8oqXY4rLNXvFmEnXDKJYaly3SjlchkSOwiCngstFMeDXLE4CVygGX3e6FawUgzFIKANbiHHDZ7U4qL7c5SWzxYqFywGXjvVD3F3Zu8ccs5gqXzeYx7CTTWOOvnmTEZZu0ItSxrvAmZrrHZYme8dkhLbiqLkUDPlvMA1cNIiM+613Y4KJNSviiprTgmrrQM75arVzhkllUxFetqBlXVEXa8d0hMeKCxVSH73rRG37XidpxZlXRiN9UhYUtztRFVI+fhUPFE851KlSHn4TNxTueGU2yx3PVbipVeGpxIaeAJ2IynRv8YHEp3iNOjRRdGvxotGjONb7pD7M4RfyiK6ZclhYf1bdDprRW+FW9SZSUlqGtq1BVTTftRaKce1zS7bIpWyW/oK0i38tU4apupWyRsijKVhoj/o+6W45cJEoqaR+bgP8txH5a1nUZ2gq/+Q/51T5MhuG3fQAAAABJRU5ErkJggg==);
+}