+
+
\ No newline at end of file
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.