8a33a55fee
--HG-- extra : rebase_source : 9503ba50aa6dbba68dce8dbb4898cefcba1293f8
5642 lines
153 KiB
JavaScript
5642 lines
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|