cops/resources/monocle/scripts/monocore.js

5642 righe
153 KiB
JavaScript

/*!
* 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("<div>Page "+i+"</div>");
}
var divStyle = [
"display:inline-block",
"line-height:"+testFrameSize+"px",
"width:"+testFrameSize+"px"
].join(";");
src = "javascript:'<!DOCTYPE html><html>"+
'<head><meta name="time" content="'+(new Date()).getTime()+'" />'+
'<style>div{'+divStyle+'}</style></head>'+
'<body>'+pgs.join("")+'</body>'+
"</html>'";
}
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 <html> 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 <base>.
['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(<elem>, '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: <control instance>,
// elements: <array of topmost elements created by control>,
// controlType: <standard, page, modal, popover, invisible, etc>
// }
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 =
"<h1>Incompatible browser</h1>"+
"<p>Unfortunately, your browser isn't able to display this book. "+
"If possible, try again in another browser or on another device.</p>";
Monocle.Reader.LOAD_FAILURE_INFO =
"<h1>Book could not be loaded</h1>"+
"<p>Sorry, parts of the book could not be retrieved.<br />"+
"Please check your connection and refresh to try again.</p>";
/* 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 <base> 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("<base\s.+>", "im"))) {
var baseURI = computeBaseURI(reader);
if (baseURI) {
p.source.html = p.source.html.replace(
new RegExp("(<head[^>]*>)", "im"),
'$1<base href="'+baseURI+'" />'
);
}
}
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;