Merge lp:~serenityvalley/calibre/calibre_reader into lp:calibre

Proposed by Caleb06
Status: Needs review
Proposed branch: lp:~serenityvalley/calibre/calibre_reader
Merge into: lp:calibre
Diff against target: 7371 lines (+6928/-61)
11 files modified
resources/content_server/browse/details.html (+1/-0)
resources/content_server/browse/read.html (+22/-0)
resources/content_server/browse/summary.html (+1/-0)
resources/content_server/read/README.md (+139/-0)
resources/content_server/read/monocore.css (+126/-0)
resources/content_server/read/monocore.js (+5203/-0)
resources/content_server/read/monoctrl.css (+165/-0)
resources/content_server/read/monoctrl.js (+969/-0)
src/calibre/library/server/browse.py (+152/-61)
src/calibre/library/server/reader.py (+8/-0)
src/calibre/library/server/unzip.py (+142/-0)
To merge this branch: bzr merge lp:~serenityvalley/calibre/calibre_reader
Reviewer Review Type Date Requested Status
Kovid Goyal Pending
Review via email: mp+101061@code.launchpad.net

Description of the change

This branch allows users to read HTMLZ format books through the content server.

To post a comment you must log in.
11848. By Caleb06

Change read link so that it will open the book in a new tab

11849. By Caleb06

Added monocle source files for multi format reading.
Still working on integrating them.

Unmerged revisions

11849. By Caleb06

Added monocle source files for multi format reading.
Still working on integrating them.

11848. By Caleb06

Change read link so that it will open the book in a new tab

11847. By Caleb06

I have added the ability to read books that are in HTMLZ format online through the content server.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'resources/content_server/Thumbs.db'
2Binary files resources/content_server/Thumbs.db 1970-01-01 00:00:00 +0000 and resources/content_server/Thumbs.db 2012-04-12 16:42:25 +0000 differ
3=== modified file 'resources/content_server/browse/details.html'
4--- resources/content_server/browse/details.html 2010-10-28 05:10:17 +0000
5+++ resources/content_server/browse/details.html 2012-04-12 16:42:25 +0000
6@@ -3,6 +3,7 @@
7 <img alt="Cover of {title}" src="{prefix}/get/cover/{id}" />
8 </div>
9 <div class="right">
10+ <div class="read">{read_button}</div>
11 <div class="field formats">{formats}</div>
12 {fields}
13 {comments}
14
15=== added file 'resources/content_server/browse/read.html'
16--- resources/content_server/browse/read.html 1970-01-01 00:00:00 +0000
17+++ resources/content_server/browse/read.html 2012-04-12 16:42:25 +0000
18@@ -0,0 +1,22 @@
19+<head>
20+<!-- Include the Monocle library and styles -->
21+<script src="./read/monocore.js"></script>
22+<link rel="stylesheet" type="text/css" href="styles/monocore.css" />
23+<style>
24+#reader {
25+ width: 300px;
26+ height: 400px;
27+ border: 1px solid #000;
28+}
29+</style>
30+</head>
31+
32+<body onload="Monocle.Reader('reader');">
33+ <!-- The reader element, with all content to paginate inside it -->
34+ <div id="reader">
35+ <h1>Hello world.</h1>
36+ </div>
37+
38+ <!-- Instantiate the reader when the containing element has loaded -->
39+ <script></script>
40+</body>
41\ No newline at end of file
42
43=== modified file 'resources/content_server/browse/summary.html'
44--- resources/content_server/browse/summary.html 2010-10-28 05:10:17 +0000
45+++ resources/content_server/browse/summary.html 2012-04-12 16:42:25 +0000
46@@ -2,6 +2,7 @@
47 <div class="left">
48 <img alt="Cover of {title}" src="{prefix}/get/thumb_90_120/{id}" />
49 {get_button}
50+ {read_button}
51 </div>
52 <div class="right">
53 <div class="stars">
54
55=== added file 'resources/content_server/read/README.md'
56--- resources/content_server/read/README.md 1970-01-01 00:00:00 +0000
57+++ resources/content_server/read/README.md 2012-04-12 16:42:25 +0000
58@@ -0,0 +1,139 @@
59+# Monocle
60+
61+A silky, tactile browser-based ebook reader.
62+
63+Invented by [Inventive Labs](http://inventivelabs.com.au). Released under the
64+MIT license.
65+
66+More information (including demos): http://monocle.inventivelabs.com.au
67+
68+Contributions welcome - fork the repository on
69+[GitHub](http://github.com/joseph/monocle).
70+
71+
72+## Getting Monocle
73+
74+You can download a unified, redistributable version of Monocle
75+[from Github](https://github.com/joseph/Monocle/downloads).
76+
77+The scripts and stylesheets are separated into:
78+
79+* `monocore` - the essential Monocle functionality
80+* `monoctrl` - the optional basic controls for page numbers, font-sizing, etc
81+
82+It's recommended that you develop against the unminified files, to make
83+debugging easier. In production, use the minified files.
84+
85+
86+## Integrating Monocle
87+
88+Here's the simplest thing that could possibly work.
89+
90+ <head>
91+ <!-- Include the Monocle library and styles -->
92+ <script src="scripts/monocore.js"></script>
93+ <link rel="stylesheet" type="text/css" href="styles/monocore.css" />
94+ <style>
95+ #reader { width: 300px; height: 400px; border: 1px solid #000; }
96+ </style>
97+ </head>
98+
99+ <body>
100+ <!-- The reader element, with all content to paginate inside it -->
101+ <div id="reader">
102+ <h1>Hello world.</h1>
103+ </div>
104+
105+ <!-- Instantiate the reader when the containing element has loaded -->
106+ <script>Monocle.Reader('reader');</script>
107+ </body>
108+
109+
110+In this example, we initialise the reader with the contents of the div
111+itself. In theory there's no limit on the size of the contents of that div.
112+
113+A more advanced scenario involves feeding Monocle a "book data object", from
114+which it can lazily load the contents of the book as the user requests it.
115+
116+
117+## Exploring Monocle
118+
119+If you want to explore all of Monocle's features, clone this repository and
120+open `test/index.html` in your browser. This will guide you through Monocle's
121+tests, which incidentally demonstrate all the major features. View source or
122+browse the test directory in your text editor for implementation details.
123+
124+
125+## Connecting Monocle to your book content
126+
127+For a non-trivial Monocle implementation, your task is to connect the
128+Monocle Reader to your book's HTML content and structure. You create
129+something called "the book data object" to do this.
130+
131+The book data object is really pretty simple. You'll find the specification
132+and some examples in the [Monocle Wiki page on the book data object](https://github.com/joseph/Monocle/wiki/Book-data-object).
133+
134+For more advanced uses and customisations of Monocle, you should definitely
135+read the [Monocle Wiki](https://github.com/joseph/Monocle/wiki).
136+
137+
138+## Browser support
139+
140+At this time, Monocle aims for full support of all browsers with a
141+W3C-compliant CSS column module implementation. That is Gecko, WebKit and
142+Opera at this point. Legacy support is provided for some other browsers,
143+including recent versions of Internet Explorer. Please encourage your
144+browser-maker to work on implementing these standards in particular:
145+
146+* CSS Multi-Column Layout
147+* W3C DOM Level 2 Event Model
148+* CSS 2D Transforms (better: 3D Transforms, even better: hardware acceleration)
149+
150+Monocle has a particular focus on mobile devices. Monocle either supports or
151+is trying to support:
152+
153+* iOS 3.1+
154+* Android 2.0+
155+* Blackberry 6
156+* Kindle 3
157+
158+All these mobile platforms implement columned iframes differently, so support
159+may be patchy in places, but we're working on it. Patches that improve or
160+broaden Monocle's browser support are very welcome (but please provide tests).
161+
162+Inventive Labs would like to thank Ebooq for providing a device to assist with
163+Android testing.
164+
165+
166+## Future directions
167+
168+Monocle has a small set of big goals:
169+
170+* Faster, more responsive page flipping
171+* Wider browser support (and better tests, automated as far as possible)
172+* Tracking spec developments in EPUB and Zhook, supporting where appropriate
173+
174+We'd also like to provide more implementation showcases in the tests, and
175+offer more developer documentation in the wiki.
176+
177+If you can help out with any of these things, fork away (or contact 'joseph'
178+on GitHub).
179+
180+
181+## History
182+
183+2.3.1 - Fix for serious Firefox 12 bug in paginating content.
184+
185+2.3.0 - Smoother transitions and animations in more browsers.
186+
187+2.2.1 - Slider fixes for better iOS performance.
188+
189+2.2.0 - Speed, compatibility improvements (esp iOS5, Android, Kindle3).
190+
191+2.1.0 - Source file reorganisation, Sprockets 2, distributables, wiki.
192+
193+2.0.0 - Complete rewrite to sandbox content in iframes (the Componentry branch).
194+
195+1.0.1 - Scrolling flipper, more tests, work on sandboxing in iframe (Framer).
196+
197+1.0.0 - Initial release.
198
199=== added directory 'resources/content_server/read/formats'
200=== added file 'resources/content_server/read/monocore.css'
201--- resources/content_server/read/monocore.css 1970-01-01 00:00:00 +0000
202+++ resources/content_server/read/monocore.css 2012-04-12 16:42:25 +0000
203@@ -0,0 +1,126 @@
204+/*===========================================================================
205+
206+This is a base-level Monocle stylesheet. It assumes no class-prefix has been
207+given to the Reader during initialisation - if one has, you can copy and
208+modify this stylesheet accordingly.
209+
210+---------------------------------------------------------------------------*/
211+
212+/* The reader object that holds pretty much everything.
213+ * (A direct child of the element passed to reader initialisation). */
214+
215+div.monelem_container {
216+ background-color: black;
217+}
218+
219+
220+/* The div that mimics a leaf of paper in a book. */
221+div.monelem_page {
222+ background: white;
223+ top: 0;
224+ left: 0;
225+ bottom: 3px;
226+ right: 5px;
227+ border-right: 1px solid #999;
228+}
229+
230+
231+/* The div within the page that determines page margins. */
232+div.monelem_sheaf {
233+ top: 1em;
234+ left: 1em;
235+ bottom: 1em;
236+ right: 1em;
237+}
238+
239+
240+/* The iframe within the page that loads the content of the book. */
241+div.monelem_component {
242+}
243+
244+
245+/* A panel that sits above the entire reader object, holding controls. */
246+div.monelem_overlay {
247+}
248+
249+
250+/*===========================================================================
251+ PANELS
252+---------------------------------------------------------------------------*/
253+
254+
255+.monelem_panels_imode_panel {
256+ background: rgba(255,255,255,0.7);
257+ opacity: 0;
258+}
259+
260+.monelem_panels_imode_backwardsPanel {
261+ -webkit-box-shadow: 1px 1px 3px #777;
262+ -moz-box-shadow: 1px 1px 3px #777;
263+ box-shadow: 1px 1px 3px #777;
264+}
265+
266+.monelem_panels_imode_forwardsPanel {
267+ -webkit-box-shadow: -1px 1px 3px #777;
268+ -moz-box-shadow: -1px 1px 3px #777;
269+ box-shadow: -1px 1px 3px #777;
270+}
271+
272+.monelem_panels_imode_centralPanel {
273+}
274+
275+.monelem_panels_imode_toggleIcon {
276+ position: absolute;
277+ right: 0;
278+ bottom: 0;
279+ width: 50px;
280+ height: 50px;
281+ background-repeat: no-repeat;
282+ background-position: center center;
283+}
284+
285+/* If you modify this you could significantly change the way panels work. */
286+div.monelem_controls_panel_expanded {
287+ left: 0 !important;
288+ width: 100% !important;
289+ z-index: 1001 !important;
290+}
291+
292+/*===========================================================================
293+ Flippers
294+---------------------------------------------------------------------------*/
295+
296+div.monelem_flippers_slider_wait {
297+ position: absolute;
298+ right: 0px;
299+ top: 0px;
300+ width: 92px;
301+ height: 112px;
302+ background-repeat: no-repeat;
303+ -webkit-background-size: 100%;
304+ -moz-background-size: 100%;
305+ background-size: 100%;
306+}
307+
308+@media screen and (max-width: 640px) {
309+ div.monelem_flippers_slider_wait {
310+ width: 61px;
311+ height: 75px;
312+ }
313+}
314+
315+
316+/*===========================================================================
317+ DATA URIs
318+
319+ These are data-uri packed images, inlined for loading speed and simplicity.
320+ Placed at the end of this file because they're visually noisy...
321+---------------------------------------------------------------------------*/
322+
323+div.monelem_panels_imode_toggleIcon {
324+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAaCAYAAABPY4eKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1%2B%2FAAAABV0RVh0Q3JlYXRpb24gVGltZQAzMC82LzEwBMfmVwAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNXG14zYAAANYSURBVEiJtdZbiNVVFMfxj8cx85JkIGlqSESgOGA9WIQgGmTRUyRaYFJDnUWYGV2eyiCpkIbEKJI1UqYvUkmFDxFBgpghonajSDCM7hcxLSnt4ulh%2F2c4HufMTOH8Xs75%2F%2Ffa67v3%2Bu%2B91hphGJWZNUzCXJyKiHd6xxqNhhGDTB6NOViAyzARY3EaP%2BNL7MCBiPi9Ze4leBlTsR9jcCnuiYgDbeGZeV4F7EINe7EP3%2BJ49W4GrsZ8NPAGXouIk5k5F93YFhHPVT5H4kbcjaX1ev3kWfDMPB9P4ko8ERE7BopONWcOVmMc1uBRrG8Oc5Ptq1hdr9cPdrQMTMUWfBQRCweD9ioiPsQtmbkeu7G8P3ClsZSI98EzcxqeUsLXM1RwZs7ErRiJKXgQN2Tmzoj4qsV2Hn7BYcq369UaHIqI5yPizyGCx2MPfsRVOBoR6%2FA%2BNmXmqCbbm%2FAiMiJO9cEzcwEuwLODwMZk5oXVLYA6PouIF%2FC6cvBgI37D0mreStyJroh4r9df785XYGtEHG8Hfnjb1w08Xu2qq3regtOZuaka2whV5NZieWY%2BhkV4ICJ2N%2FusZeYMJQm8NdCuuxdPH4HENGzsXjx9REQcqRxvR2dEfNBrHxF7lHywGPXW7085cEvwZkScHAheaRz%2BwngcqyAnlEPan%2Fbh5oj4rr%2FBDlyOXUMA%2Fx%2F9oFytM5SZs3t6epbWlOtxeJjg%2BzEmMye3vF%2BCYx2YhdFnTTs3OoQT2JqZ3TiC2zETyzrwrnIwhkMTqwVsxW24GLsiYmWj0dCBo2gNy7nSRfgpIjZjM6WU1ut1lHt%2BGLOHCd6J79sN1pSkMSUzJwwD%2FBoD5I9aRHyiFIVFQ3D2j1KR%2Fh7MMDPnY1JE7GwLr3434N5BnI3GFRiFzuai0Ub34aWBDGr0pcKPM%2FPpqovpT11KoVinNAvXt1lkLTNXKFesXU1HUz3HI0plWqW0QGcoIjYoERpMy7AS17b2da06o43KzLF4RanRzwwx3%2FfOHYW7lL5ubUR83p9do9Ho%2B99fDzcZDynfdxPejog%2FBoCOxHW4AxOwKiK%2BaGc%2FILzJ6ULcXznciwM4qFSzCUob3Km0UCeU3W5v5%2B8%2FwZsWMQvzlN1Nq8C%2F4ht8qkRm72B%2B%2BoP%2FC0sEOftJmUbfAAAAAElFTkSuQmCC);
325+}
326+
327+div.monelem_flippers_slider_wait {
328+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFwAAABwCAMAAACkX%2BcnAAAB0VBMVEUAAACDg4OEhISFhYWGhoaHh4eIiIiJiIiJiYmKioqLi4uMjIyNjY2Ojo6Pj4%2BQkJCRkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqbm5ucnJycnJ2dnZ2dnZ6dnZ%2Benp6enp%2Bfn5%2Bfn6CgoKCgoKGhoKChoaGioqKjo6OkpKSlpaWmpaWmpqaoqKiqqqqrq6usrKytra2urq6wsLCxsbGzs7O0tLS0tLW1tbW1tba1tbe2tri4uLi4uLm4uLq6ury7u7y8vLy8vL28vL%2B9vb2%2Bvr6%2Bvr%2B%2Fv7%2B%2Fv8HAwMDAwMLAwMPBwcPCwsPExMTExMXFxcXGxsbHx8fIyMjJycrOztDOztHPz9DPz9HR0dTS0tTT09TT09XU1NbU1NfV1dfW1tjW1tnX19fX19rY2Nra2tva2tzd3eDe3t7f39%2Fh4eHi4uLl5enn5%2Bnp6ezp6e3q6u3q6u7r6%2B7r6%2B%2Fs7O%2Fs7PDt7fDt7fHu7vHu7vLv7%2B%2Fv7%2FLv7%2FPw8PDw8PPw8PTx8fTx8fXy8vXy8vbz8%2Fbz8%2Ff09Pf09Pj19fj19fn29vn39%2Fn39%2Fr4%2BPr4%2BPv5%2Bfv6%2Bvv6%2Bvz7%2B%2Fz7%2B%2F38%2FP39%2Ff39%2Ff7%2B%2Fv7%2B%2Fv%2F%2F%2F%2F%2BHSJEZAAAAAXRSTlMAQObYZgAAA5dJREFUaN61lk1uE0EQhd%2BrsQlREAgkFkQKLJByteQU3IIdd2OBYIFASFmAFLurWPT0uOfXme6aWUXy6PNL9XPXR3z6DSI93wQ0GkHjzweapM%2B%2Btn8SMAERPzKQQKN7IDRhD2APgkbumucvXp24T3s%2BH47H7%2F9U1AxmpvaDzV5IUMBfD0CbQXYPly93K%2BEiwneqphpMVc3e7p492zciQhGKNN2bX%2F42shJOEQFIQgAKgfgdpvFz7d58%2FPO4Fn5PiggBAUkAYhoUMJipwU5vhsfjWjhESMTsBChQVVMDYICadfjD4VAAFyGYZVcN7Vzar4iP6frkd5RuLjG7WlCFwdSy4ICtPlBAKJLNhYBq6HKf8IHrx4J7IQX5maqFLHeC3yrWwyEiFACSzlTVVFNuzQZTAG%2BrLoQwVT1kubvGF4wlVj2vi2isuvWrbiXJIUISYKwL5qpuWgbvXQHxSCeqbiXwvOrpClC1QdXViuAQUnpXgE1U%2FSb%2BUwVVF7JfdTWN2G4uFyiaeZz6oOpB1drzTF0sSw6ySdc5Y%2FZe1SPeCpPfS6p6yq4arK16V5eyAwWEp6oTEKpqewXEygBW9iMabzsAZjqoOkuTL227tjJvSg8UaG%2FGhW33obSK8d4dVj1eAV3VrXQsuBtXvd12XdWteCxg2nbobbuU2xQsHst42zHe6lllypOnbcdUeZ62HUzNoOXJz4vdpZXDz4rde5TDz4rdsQ6%2BLHZNxVjOip3VJD8ndjVtOSt2rEp%2BRuxCHXxZ7G6tCr4sdhUX1xPETmvhC2KndWNZFjtUjmVR7KRyLItiF2qTL4ndtdXCF8Tuqhq%2BIHaonfmi2Ek1fEHsQjV8YdtVt2VR7DzgM2J36QCfFbsbB%2Fi82MEBPit2HvBZsfMYy6zYuSSfq7oLfE7sLpzgk2J37QKfETt1gc%2BJnQ98Rux84NNiJ07wSbELTvBpsXOCT4rdRz%2F4WOzMCz4pdl7wKbGDG3xC7NzGMiV2jvCx2PnNfELsbvzgY7FrHOFjsXOEj7YdHeFjsfOF96sePOFjsXOED8XutSt8sO2uXOFDsfOFD6ruCx9U3Rc%2BEDt3eC52zvC%2B2DnD%2B2LnDe9V3RveEzt3eC527vBc7NzhudhtAe%2BuAH94VnV%2FeCZ2G8BzscMmUxdgi5lnYrcF%2FCR2wCZHSvftP9x2m8DTttsEnsRuK7hs8%2FPPxG4beCt2G8HbbbcNPG67reAUEfwHRePBMkvuZ4wAAAAASUVORK5CYII%3D);
329+}
330
331=== added file 'resources/content_server/read/monocore.js'
332--- resources/content_server/read/monocore.js 1970-01-01 00:00:00 +0000
333+++ resources/content_server/read/monocore.js 2012-04-12 16:42:25 +0000
334@@ -0,0 +1,5203 @@
335+Monocle = {
336+ VERSION: "2.3.1"
337+};
338+
339+
340+Monocle.pieceLoaded = function (piece) {
341+ if (typeof onMonoclePiece == 'function') {
342+ onMonoclePiece(piece);
343+ }
344+}
345+
346+
347+Monocle.defer = function (fn, time) {
348+ if (fn && typeof fn == "function") {
349+ return setTimeout(fn, time || 0);
350+ }
351+}
352+
353+Monocle.Dimensions = {}
354+Monocle.Controls = {};
355+Monocle.Flippers = {};
356+Monocle.Panels = {};
357+
358+
359+Monocle.pieceLoaded("core/monocle");
360+Monocle.Env = function () {
361+
362+ var API = { constructor: Monocle.Env }
363+ var k = API.constants = API.constructor;
364+ var p = API.properties = {
365+ // Assign to a function before running survey in order to get
366+ // results as they come in. The function should take two arguments:
367+ // testName and value.
368+ resultCallback: null
369+ }
370+
371+ // These are private variables so they don't clutter up properties.
372+ var css = Monocle.Browser.css;
373+ var activeTestName = null;
374+ var frameLoadCallback = null;
375+ var testFrame = null;
376+ var testFrameCntr = null;
377+ var testFrameSize = 100;
378+ var surveyCallback = null;
379+
380+
381+ function survey(cb) {
382+ surveyCallback = cb;
383+ runNextTest();
384+ }
385+
386+
387+ function runNextTest() {
388+ var test = envTests.shift();
389+ if (!test) { return completed(); }
390+ activeTestName = test[0];
391+ try { test[1](); } catch (e) { result(e); }
392+ }
393+
394+
395+ // Each test should call this to say "I'm finished, run the next test."
396+ //
397+ function result(val) {
398+ //console.log("["+activeTestName+"] "+val);
399+ API[activeTestName] = val;
400+ if (p.resultCallback) { p.resultCallback(activeTestName, val); }
401+ runNextTest();
402+ return val;
403+ }
404+
405+
406+ // Invoked after all tests have run.
407+ //
408+ function completed() {
409+ // Remove the test frame after a slight delay (otherwise Gecko spins).
410+ Monocle.defer(removeTestFrame);
411+
412+ if (typeof surveyCallback == "function") {
413+ surveyCallback(API);
414+ }
415+ }
416+
417+
418+ // A bit of sugar for simplifying a detection pattern: does this
419+ // function exist?
420+ //
421+ // Pass a string snippet of JavaScript to be evaluated.
422+ //
423+ function testForFunction(str) {
424+ return function () { result(typeof eval(str) == "function"); }
425+ }
426+
427+
428+ // A bit of sugar to indicate that the detection function for this test
429+ // hasn't been written yet...
430+ //
431+ // Pass the value you want assigned for the test until it is implemented.
432+ //
433+ function testNotYetImplemented(rslt) {
434+ return function () { result(rslt); }
435+ }
436+
437+
438+
439+ // Loads (or reloads) a hidden iframe so that we can test browser features.
440+ //
441+ // cb is the callback that is fired when the test frame's content is loaded.
442+ //
443+ // src is optional, in which case it defaults to 4. If provided, it can be
444+ // a number (specifying the number of pages of default content), or a string,
445+ // which will be loaded as a URL.
446+ //
447+ function loadTestFrame(cb, src) {
448+ if (!testFrame) { testFrame = createTestFrame(); }
449+ frameLoadCallback = cb;
450+
451+ src = src || 4;
452+
453+ if (typeof src == "number") {
454+ var pgs = [];
455+ for (var i = 1, ii = src; i <= ii; ++i) {
456+ pgs.push("<div>Page "+i+"</div>");
457+ }
458+ var divStyle = [
459+ "display:inline-block",
460+ "line-height:"+testFrameSize+"px",
461+ "width:"+testFrameSize+"px"
462+ ].join(";");
463+ src = "javascript:'<!DOCTYPE html><html>"+
464+ '<head><meta name="time" content="'+(new Date()).getTime()+'" />'+
465+ '<style>div{'+divStyle+'}</style></head>'+
466+ '<body>'+pgs.join("")+'</body>'+
467+ "</html>'";
468+ }
469+
470+ testFrame.src = src;
471+ }
472+
473+
474+ // Creates the hidden test frame and returns it.
475+ //
476+ function createTestFrame() {
477+ testFrameCntr = document.createElement('div');
478+ testFrameCntr.style.cssText = [
479+ "width:"+testFrameSize+"px",
480+ "height:"+testFrameSize+"px",
481+ "overflow:hidden",
482+ "position:absolute",
483+ "visibility:hidden"
484+ ].join(";");
485+ document.body.appendChild(testFrameCntr);
486+
487+ var fr = document.createElement('iframe');
488+ testFrameCntr.appendChild(fr);
489+ fr.setAttribute("scrolling", "no");
490+ fr.style.cssText = [
491+ "width:100%",
492+ "height:100%",
493+ "border:none",
494+ "background:#900"
495+ ].join(";");
496+ fr.addEventListener(
497+ "load",
498+ function () {
499+ if (!fr.contentDocument || !fr.contentDocument.body) { return; }
500+ var bd = fr.contentDocument.body;
501+ bd.style.cssText = ([
502+ "margin:0",
503+ "padding:0",
504+ "position:absolute",
505+ "height:100%",
506+ "width:100%",
507+ "-webkit-column-width:"+testFrameSize+"px",
508+ "-webkit-column-gap:0",
509+ "-webkit-column-fill:auto",
510+ "-moz-column-width:"+testFrameSize+"px",
511+ "-moz-column-gap:0",
512+ "-moz-column-fill:auto",
513+ "-o-column-width:"+testFrameSize+"px",
514+ "-o-column-gap:0",
515+ "-o-column-fill:auto",
516+ "column-width:"+testFrameSize+"px",
517+ "column-gap:0",
518+ "column-fill:auto"
519+ ].join(";"));
520+ if (bd.scrollHeight > testFrameSize) {
521+ bd.style.cssText += ["min-width:200%", "overflow:hidden"].join(";");
522+ if (bd.scrollHeight <= testFrameSize) {
523+ bd.className = "column-force";
524+ } else {
525+ bd.className = "column-failed "+bd.scrollHeight;
526+ }
527+ }
528+ frameLoadCallback(fr);
529+ },
530+ false
531+ );
532+ return fr;
533+ }
534+
535+
536+ function removeTestFrame() {
537+ if (testFrameCntr && testFrameCntr.parentNode) {
538+ testFrameCntr.parentNode.removeChild(testFrameCntr);
539+ }
540+ }
541+
542+
543+ function columnedWidth(fr) {
544+ var bd = fr.contentDocument.body;
545+ var de = fr.contentDocument.documentElement;
546+ return Math.max(bd.scrollWidth, de.scrollWidth);
547+ }
548+
549+
550+ var envTests = [
551+
552+ // TEST FOR REQUIRED CAPABILITIES
553+
554+ ["supportsW3CEvents", testForFunction("window.addEventListener")],
555+ ["supportsCustomEvents", testForFunction("document.createEvent")],
556+ ["supportsColumns", function () {
557+ result(css.supportsPropertyWithAnyPrefix('column-width'));
558+ }],
559+ ["supportsTransform", function () {
560+ result(css.supportsProperty([
561+ 'transformProperty',
562+ 'WebkitTransform',
563+ 'MozTransform',
564+ 'OTransform',
565+ 'msTransform'
566+ ]));
567+ }],
568+
569+
570+ // TEST FOR OPTIONAL CAPABILITIES
571+
572+ // Does it do CSS transitions?
573+ ["supportsTransition", function () {
574+ result(css.supportsPropertyWithAnyPrefix('transition'))
575+ }],
576+
577+ // Can we find nodes in a document with an XPath?
578+ //
579+ ["supportsXPath", testForFunction("document.evaluate")],
580+
581+ // Can we find nodes in a document natively with a CSS selector?
582+ //
583+ ["supportsQuerySelector", testForFunction("document.querySelector")],
584+
585+ // Can we do 3d transforms?
586+ //
587+ ["supportsTransform3d", function () {
588+ result(
589+ css.supportsMediaQueryProperty('transform-3d') &&
590+ css.supportsProperty([
591+ 'perspectiveProperty',
592+ 'WebkitPerspective',
593+ 'MozPerspective',
594+ 'OPerspective',
595+ 'msPerspective'
596+ ])
597+ );
598+ }],
599+
600+
601+ // CHECK OUT OUR CONTEXT
602+
603+ // Does the device have a MobileSafari-style touch interface?
604+ //
605+ ["touch", function () {
606+ result(
607+ ('ontouchstart' in window) ||
608+ css.supportsMediaQueryProperty('touch-enabled')
609+ );
610+ }],
611+
612+ // Is the Reader embedded, or in the top-level window?
613+ //
614+ ["embedded", function () { result(top != self) }],
615+
616+
617+ // TEST FOR CERTAIN RENDERING OR INTERACTION BUGS
618+
619+ // iOS (at least up to version 4.1) makes a complete hash of touch events
620+ // when an iframe is overlapped by other elements. It's a dog's breakfast.
621+ // See test/bugs/ios-frame-touch-bug for details.
622+ //
623+ ["brokenIframeTouchModel", function () {
624+ result(Monocle.Browser.iOSVersionBelow("4.2"));
625+ }],
626+
627+ // In early versions of iOS (up to 4.1), MobileSafari would send text-select
628+ // activity to the first iframe, even if that iframe is overlapped by a
629+ // "higher" iframe.
630+ //
631+ ["selectIgnoresZOrder", function () {
632+ result(Monocle.Browser.iOSVersionBelow("4.2"));
633+ }],
634+
635+ // Webkit-based browsers put floated elements in the wrong spot when
636+ // columns are used -- they appear way down where they would be if there
637+ // were no columns. Presumably the float positions are calculated before
638+ // the columns. A bug has been lodged, and it's fixed in recent WebKits.
639+ //
640+ ["floatsIgnoreColumns", function () {
641+ if (!Monocle.Browser.is.WebKit) { return result(false); }
642+ match = navigator.userAgent.match(/AppleWebKit\/([\d\.]+)/);
643+ if (!match) { return result(false); }
644+ return result(match[1] < "534.30");
645+ }],
646+
647+ // The latest engines all agree that if a body is translated leftwards,
648+ // its scrollWidth is shortened. But some older WebKits (notably iOS4)
649+ // do not subtract translateX values from scrollWidth. In this case,
650+ // we should not add the translate back when calculating the width.
651+ //
652+ ["widthsIgnoreTranslate", function () {
653+ loadTestFrame(function (fr) {
654+ var firstWidth = columnedWidth(fr);
655+ var s = fr.contentDocument.body.style;
656+ var props = css.toDOMProps("transform");
657+ for (var i = 0, ii = props.length; i < ii; ++i) {
658+ s[props[i]] = "translateX(-600px)";
659+ }
660+ var secondWidth = columnedWidth(fr);
661+ for (i = 0, ii = props.length; i < ii; ++i) {
662+ s[props[i]] = "none";
663+ }
664+ result(secondWidth == firstWidth);
665+ });
666+ }],
667+
668+ // On Android browsers, if the component iframe has a relative width (ie,
669+ // 100%), the width of the entire browser will keep expanding and expanding
670+ // to fit the width of the body of the iframe (which, with columns, is
671+ // massive). So, 100% is treated as "of the body content" rather than "of
672+ // the parent dimensions". In this scenario, we need to give the component
673+ // iframe a fixed width in pixels.
674+ //
675+ // In iOS, the frame is clipped by overflow:hidden, so this doesn't seem to
676+ // be a problem.
677+ //
678+ ["relativeIframeExpands", function () {
679+ result(navigator.userAgent.indexOf("Android 2") >= 0);
680+ }],
681+
682+ // iOS3 will pause JavaScript execution if it gets a style-change + a
683+ // scroll change on a component body. Weirdly, this seems to break GBCR
684+ // in iOS4.
685+ //
686+ ["scrollToApplyStyle", function () {
687+ result(Monocle.Browser.iOSVersionBelow("4"));
688+ }],
689+
690+
691+ // TEST FOR OTHER QUIRKY BROWSER BEHAVIOUR
692+
693+ // Older versions of WebKit (iOS3, Kindle3) need a min-width set on the
694+ // body of the iframe at 200%. This forces columns. But when this
695+ // min-width is set, it's more difficult to recognise 1 page components,
696+ // so we generally don't want to force it unless we have to.
697+ //
698+ ["forceColumns", function () {
699+ loadTestFrame(function (fr) {
700+ var bd = fr.contentDocument.body;
701+ result(bd.className ? true : false);
702+ });
703+ }],
704+
705+ // A component iframe's body is absolutely positioned. This means that
706+ // the documentElement should have a height of 0, since it contains nothing
707+ // other than an absolutely positioned element.
708+ //
709+ // But for some browsers (Gecko and Opera), the documentElement is as
710+ // wide as the full columned content, and the body is only as wide as
711+ // the iframe element (ie, the first column).
712+ //
713+ // It gets weirder. Gecko sometimes behaves like WebKit (not clipping the
714+ // body) IF the component has been loaded via HTML/JS/Nodes, not URL. Still
715+ // can't reproduce outside Monocle.
716+ //
717+ // FIXME: If we can figure out a reliable behaviour for Gecko, we should
718+ // use it to precalculate the workaround. At the moment, this test isn't
719+ // used, but it belongs in src/dimensions/columns.js#columnedDimensions().
720+ //
721+ // ["iframeBodyWidthClipped", function () {
722+ // loadTestFrame(function (fr) {
723+ // var doc = fr.contentDocument;
724+ // result(
725+ // doc.body.scrollWidth <= testFrameSize &&
726+ // doc.documentElement.scrollWidth > testFrameSize
727+ // );
728+ // })
729+ // }],
730+
731+ // Finding the page that a given HTML node is on is typically done by
732+ // calculating the offset of its rectange from the body's rectangle.
733+ //
734+ // But if this information isn't provided by the browser, we need to use
735+ // node.scrollIntoView and check the scrollOffset. Basically iOS3 is the
736+ // only target platform that doesn't give us the rectangle info.
737+ //
738+ ["findNodesByScrolling", function () {
739+ result(typeof document.body.getBoundingClientRect !== "function");
740+ }],
741+
742+ // In MobileSafari browsers, iframes are rendered at the width and height
743+ // of their content, rather than having scrollbars. So in that case, it's
744+ // the containing element (the "sheaf") that must be scrolled, not the
745+ // iframe body.
746+ //
747+ ["sheafIsScroller", function () {
748+ loadTestFrame(function (fr) {
749+ result(fr.parentNode.scrollWidth > testFrameSize);
750+ });
751+ }],
752+
753+ // For some reason, iOS MobileSafari sometimes loses track of a page after
754+ // slideOut -- it thinks it has an x-translation of 0, rather than -768 or
755+ // whatever. So the page gets "stuck" there, until it is given a non-zero
756+ // x-translation. The workaround is to set a non-zero duration on the jumpIn,
757+ // which seems to force WebKit to recalculate the x of the page. Weird, yeah.
758+ //
759+ ["stickySlideOut", function () {
760+ result(Monocle.Browser.is.MobileSafari);
761+ }]
762+
763+ ];
764+
765+
766+ function isCompatible() {
767+ return (
768+ API.supportsW3CEvents &&
769+ API.supportsCustomEvents &&
770+ // API.supportsColumns && // This is coming in 3.0!
771+ API.supportsTransform
772+ );
773+ }
774+
775+
776+ API.survey = survey;
777+ API.isCompatible = isCompatible;
778+
779+ return API;
780+}
781+
782+
783+
784+Monocle.pieceLoaded('compat/env');
785+Monocle.CSS = function () {
786+
787+ var API = { constructor: Monocle.CSS }
788+ var k = API.constants = API.constructor;
789+ var p = API.properties = {
790+ guineapig: document.createElement('div')
791+ }
792+
793+
794+ // Returns engine-specific properties,
795+ //
796+ // eg:
797+ //
798+ // toCSSProps('transform')
799+ //
800+ // ... in WebKit, this will return:
801+ //
802+ // ['transform', '-webkit-transform']
803+ //
804+ function toCSSProps(prop) {
805+ var props = [prop];
806+ var eng = k.engines.indexOf(Monocle.Browser.engine);
807+ if (eng) {
808+ var pf = k.prefixes[eng];
809+ if (pf) {
810+ props.push(pf+prop);
811+ }
812+ }
813+ return props;
814+ }
815+
816+
817+ // Returns an engine-specific CSS string.
818+ //
819+ // eg:
820+ //
821+ // toCSSDeclaration('column-width', '300px')
822+ //
823+ // ... in Mozilla, this will return:
824+ //
825+ // "column-width: 300px; -moz-column-width: 300px;"
826+ //
827+ function toCSSDeclaration(prop, val) {
828+ var props = toCSSProps(prop);
829+ for (var i = 0, ii = props.length; i < ii; ++i) {
830+ props[i] += ": "+val+";";
831+ }
832+ return props.join("");
833+ }
834+
835+
836+ // Returns an array of DOM properties specific to this engine.
837+ //
838+ // eg:
839+ //
840+ // toDOMProps('column-width')
841+ //
842+ // ... in Opera, this will return:
843+ //
844+ // [columnWidth, OColumnWidth]
845+ //
846+ function toDOMProps(prop) {
847+ var parts = prop.split('-');
848+ for (var i = parts.length; i > 0; --i) {
849+ parts[i] = capStr(parts[i]);
850+ }
851+
852+ var props = [parts.join('')];
853+ var eng = k.engines.indexOf(Monocle.Browser.engine);
854+ if (eng) {
855+ var pf = k.domprefixes[eng];
856+ if (pf) {
857+ parts[0] = capStr(parts[0]);
858+ props.push(pf+parts.join(''));
859+ }
860+ }
861+ return props;
862+ }
863+
864+
865+ // Is this exact property (or any in this array of properties) supported
866+ // by this engine?
867+ //
868+ function supportsProperty(props) {
869+ for (var i in props) {
870+ if (p.guineapig.style[props[i]] !== undefined) { return true; }
871+ }
872+ return false;
873+ } // Thanks modernizr!
874+
875+
876+
877+ // Is this property (or a prefixed variant) supported by this engine?
878+ //
879+ function supportsPropertyWithAnyPrefix(prop) {
880+ return supportsProperty(toDOMProps(prop));
881+ }
882+
883+
884+ function supportsMediaQuery(query) {
885+ var gpid = "monocle_guineapig";
886+ p.guineapig.id = gpid;
887+ var st = document.createElement('style');
888+ st.textContent = query+'{#'+gpid+'{height:3px}}';
889+ (document.head || document.getElementsByTagName('head')[0]).appendChild(st);
890+ document.documentElement.appendChild(p.guineapig);
891+
892+ var result = p.guineapig.offsetHeight === 3;
893+
894+ st.parentNode.removeChild(st);
895+ p.guineapig.parentNode.removeChild(p.guineapig);
896+
897+ return result;
898+ } // Thanks modernizr!
899+
900+
901+ function supportsMediaQueryProperty(prop) {
902+ return supportsMediaQuery(
903+ '@media (' + k.prefixes.join(prop+'),(') + 'monocle__)'
904+ );
905+ }
906+
907+
908+ function capStr(wd) {
909+ return wd ? wd.charAt(0).toUpperCase() + wd.substr(1) : "";
910+ }
911+
912+
913+ API.toCSSProps = toCSSProps;
914+ API.toCSSDeclaration = toCSSDeclaration;
915+ API.toDOMProps = toDOMProps;
916+ API.supportsProperty = supportsProperty;
917+ API.supportsPropertyWithAnyPrefix = supportsPropertyWithAnyPrefix;
918+ API.supportsMediaQuery = supportsMediaQuery;
919+ API.supportsMediaQueryProperty = supportsMediaQueryProperty;
920+
921+ return API;
922+}
923+
924+
925+Monocle.CSS.engines = ["W3C", "WebKit", "Gecko", "Opera", "IE", "Konqueror"];
926+Monocle.CSS.prefixes = ["", "-webkit-", "-moz-", "-o-", "-ms-", "-khtml-"];
927+Monocle.CSS.domprefixes = ["", "Webkit", "Moz", "O", "ms", "Khtml"];
928+
929+
930+Monocle.pieceLoaded('compat/css');
931+// STUBS - simple debug functions and polyfills to normalise client
932+// execution environments.
933+
934+
935+// A little console stub if not initialized in a console-equipped browser.
936+//
937+if (typeof window.console == "undefined") {
938+ window.console = {
939+ messages: [],
940+ log: function (msg) {
941+ this.messages.push(msg);
942+ }
943+ }
944+}
945+
946+
947+// A simple version of console.dir that works on iOS.
948+//
949+window.console.compatDir = function (obj) {
950+ var stringify = function (o) {
951+ var parts = [];
952+ for (x in o) {
953+ parts.push(x + ": " + o[x]);
954+ }
955+ return parts.join("; ");
956+ }
957+
958+ window.console.log(stringify(obj));
959+}
960+
961+
962+Monocle.pieceLoaded('compat/stubs');
963+Monocle.Browser = {}
964+
965+// Detect the browser engine and set boolean flags for reference.
966+//
967+Monocle.Browser.is = {
968+ IE: !!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
969+ Opera: navigator.userAgent.indexOf('Opera') > -1,
970+ WebKit: navigator.userAgent.indexOf('AppleWebKit') > -1,
971+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 &&
972+ navigator.userAgent.indexOf('KHTML') === -1,
973+ MobileSafari: !!navigator.userAgent.match(/AppleWebKit.*Mobile/)
974+} // ... with thanks to PrototypeJS.
975+
976+
977+if (Monocle.Browser.is.IE) {
978+ Monocle.Browser.engine = "IE";
979+} else if (Monocle.Browser.is.Opera) {
980+ Monocle.Browser.engine = "Opera";
981+} else if (Monocle.Browser.is.WebKit) {
982+ Monocle.Browser.engine = "WebKit";
983+} else if (Monocle.Browser.is.Gecko) {
984+ Monocle.Browser.engine = "Gecko";
985+} else {
986+ Monocle.Browser.engine = "W3C";
987+}
988+
989+
990+// Detect the client platform (typically device/operating system).
991+//
992+Monocle.Browser.on = {
993+ iPhone: Monocle.Browser.is.MobileSafari && screen.width == 320,
994+ iPad: Monocle.Browser.is.MobileSafari && screen.width == 768,
995+ UIWebView: Monocle.Browser.is.MobileSafari &&
996+ navigator.userAgent.indexOf("Safari") < 0 &&
997+ !navigator.standalone,
998+ BlackBerry: navigator.userAgent.indexOf("BlackBerry") != -1,
999+ Android: navigator.userAgent.indexOf('Android') != -1,
1000+ MacOSX: navigator.userAgent.indexOf('Mac OS X') != -1 &&
1001+ !Monocle.Browser.is.MobileSafari,
1002+ Kindle3: !!navigator.userAgent.match(/Kindle\/3/)
1003+ // TODO: Mac, Windows, etc
1004+}
1005+
1006+
1007+// It is only because MobileSafari is responsible for so much anguish that
1008+// we special-case it here. Not a badge of honour.
1009+//
1010+if (Monocle.Browser.is.MobileSafari) {
1011+ (function () {
1012+ var ver = navigator.userAgent.match(/ OS ([\d_]+)/);
1013+ if (ver) {
1014+ Monocle.Browser.iOSVersion = ver[1].replace(/_/g, '.');
1015+ } else {
1016+ console.warn("Unknown MobileSafari user agent: "+navigator.userAgent);
1017+ }
1018+ })();
1019+}
1020+Monocle.Browser.iOSVersionBelow = function (strOrNum) {
1021+ return !!Monocle.Browser.iOSVersion && Monocle.Browser.iOSVersion < strOrNum;
1022+}
1023+
1024+
1025+// A helper class for sniffing CSS features and creating CSS rules
1026+// appropriate to the current rendering engine.
1027+//
1028+Monocle.Browser.css = new Monocle.CSS();
1029+
1030+
1031+// During Reader initialization, this method is called to create the
1032+// "environment", which tests for the existence of various browser
1033+// features and bugs, then invokes the callback to continue initialization.
1034+//
1035+// If the survey has already been conducted and the env exists, calls
1036+// callback immediately.
1037+//
1038+Monocle.Browser.survey = function (callback) {
1039+ if (!Monocle.Browser.env) {
1040+ Monocle.Browser.env = new Monocle.Env();
1041+ Monocle.Browser.env.survey(callback);
1042+ } else if (typeof callback == "function") {
1043+ callback();
1044+ }
1045+}
1046+
1047+Monocle.pieceLoaded('compat/browser');
1048+Monocle.Factory = function (element, label, index, reader) {
1049+
1050+ var API = { constructor: Monocle.Factory };
1051+ var k = API.constants = API.constructor;
1052+ var p = API.properties = {
1053+ element: element,
1054+ label: label,
1055+ index: index,
1056+ reader: reader,
1057+ prefix: reader.properties.classPrefix || ''
1058+ }
1059+
1060+
1061+ // If index is null, uses the first available slot. If index is not null and
1062+ // the slot is not free, throws an error.
1063+ //
1064+ function initialize() {
1065+ if (!p.label) { return; }
1066+ // Append the element to the reader's graph of DOM elements.
1067+ var node = p.reader.properties.graph;
1068+ node[p.label] = node[p.label] || [];
1069+ if (typeof p.index == 'undefined' && node[p.label][p.index]) {
1070+ throw('Element already exists in graph: '+p.label+'['+p.index+']');
1071+ } else {
1072+ p.index = p.index || node[p.label].length;
1073+ }
1074+ node[p.label][p.index] = p.element;
1075+
1076+ // Add the label as a class name.
1077+ addClass(p.label);
1078+ }
1079+
1080+
1081+ // Finds an element that has been created in the context of the current
1082+ // reader, with the given label. If oIndex is not provided, returns first.
1083+ // If oIndex is provided (eg, n), returns the nth element with the label.
1084+ //
1085+ function find(oLabel, oIndex) {
1086+ if (!p.reader.properties.graph[oLabel]) {
1087+ return null;
1088+ }
1089+ return p.reader.properties.graph[oLabel][oIndex || 0];
1090+ }
1091+
1092+
1093+ // Takes an elements and assimilates it into the reader -- essentially
1094+ // giving it a "dom" object of it's own. It will then be accessible via find.
1095+ //
1096+ // Note that (as per comments for initialize), if oIndex is provided and
1097+ // there is no free slot in the array for this label at that index, an
1098+ // error will be thrown.
1099+ //
1100+ function claim(oElement, oLabel, oIndex) {
1101+ return oElement.dom = new Monocle.Factory(
1102+ oElement,
1103+ oLabel,
1104+ oIndex,
1105+ p.reader
1106+ );
1107+ }
1108+
1109+
1110+ // Create an element with the given label.
1111+ //
1112+ // The last argument (whether third or fourth) is the options hash. Your
1113+ // options are:
1114+ //
1115+ // class - the classname for the element. Must only be one name.
1116+ // html - the innerHTML for the element.
1117+ // text - the innerText for the element (an alternative to html, simpler).
1118+ //
1119+ // Returns the created element.
1120+ //
1121+ function make(tagName, oLabel, index_or_options, or_options) {
1122+ var oIndex, options;
1123+ if (arguments.length == 1) {
1124+ oLabel = null,
1125+ oIndex = 0;
1126+ options = {};
1127+ } else if (arguments.length == 2) {
1128+ oIndex = 0;
1129+ options = {};
1130+ } else if (arguments.length == 4) {
1131+ oIndex = arguments[2];
1132+ options = arguments[3];
1133+ } else if (arguments.length == 3) {
1134+ var lastArg = arguments[arguments.length - 1];
1135+ if (typeof lastArg == "number") {
1136+ oIndex = lastArg;
1137+ options = {};
1138+ } else {
1139+ oIndex = 0;
1140+ options = lastArg;
1141+ }
1142+ }
1143+
1144+ var oElement = document.createElement(tagName);
1145+ claim(oElement, oLabel, oIndex);
1146+ if (options['class']) {
1147+ oElement.className += " "+p.prefix+options['class'];
1148+ }
1149+ if (options['html']) {
1150+ oElement.innerHTML = options['html'];
1151+ }
1152+ if (options['text']) {
1153+ oElement.appendChild(document.createTextNode(options['text']));
1154+ }
1155+
1156+ return oElement;
1157+ }
1158+
1159+
1160+ // Creates an element by passing all the given arguments to make. Then
1161+ // appends the element as a child of the current element.
1162+ //
1163+ function append(tagName, oLabel, index_or_options, or_options) {
1164+ var oElement = make.apply(this, arguments);
1165+ p.element.appendChild(oElement);
1166+ return oElement;
1167+ }
1168+
1169+
1170+ // Returns an array of [label, index, reader] for the given element.
1171+ // A simple way to introspect the arguments required for #find, for eg.
1172+ //
1173+ function address() {
1174+ return [p.label, p.index, p.reader];
1175+ }
1176+
1177+
1178+ // Apply a set of style rules (hash or string) to the current element.
1179+ // See Monocle.Styles.applyRules for more info.
1180+ //
1181+ function setStyles(rules) {
1182+ return Monocle.Styles.applyRules(p.element, rules);
1183+ }
1184+
1185+
1186+ function setBetaStyle(property, value) {
1187+ return Monocle.Styles.affix(p.element, property, value);
1188+ }
1189+
1190+
1191+ // ClassName manipulation functions - with thanks to prototype.js!
1192+
1193+ // Returns true if one of the current element's classnames matches name --
1194+ // classPrefix aware (so don't concate the prefix onto it).
1195+ //
1196+ function hasClass(name) {
1197+ name = p.prefix + name;
1198+ var klass = p.element.className;
1199+ if (!klass) { return false; }
1200+ if (klass == name) { return true; }
1201+ return new RegExp("(^|\\s)"+name+"(\\s|$)").test(klass);
1202+ }
1203+
1204+
1205+ // Adds name to the classnames of the current element (prepending the
1206+ // reader's classPrefix first).
1207+ //
1208+ function addClass(name) {
1209+ if (hasClass(name)) { return; }
1210+ var gap = p.element.className ? ' ' : '';
1211+ return p.element.className += gap+p.prefix+name;
1212+ }
1213+
1214+
1215+ // Removes (classPrefix+)name from the classnames of the current element.
1216+ //
1217+ function removeClass(name) {
1218+ var reName = new RegExp("(^|\\s+)"+p.prefix+name+"(\\s+|$)");
1219+ var reTrim = /^\s+|\s+$/g;
1220+ var klass = p.element.className;
1221+ p.element.className = klass.replace(reName, ' ').replace(reTrim, '');
1222+ return p.element.className;
1223+ }
1224+
1225+
1226+ API.find = find;
1227+ API.claim = claim;
1228+ API.make = make;
1229+ API.append = append;
1230+ API.address = address;
1231+
1232+ API.setStyles = setStyles;
1233+ API.setBetaStyle = setBetaStyle;
1234+ API.hasClass = hasClass;
1235+ API.addClass = addClass;
1236+ API.removeClass = removeClass;
1237+
1238+ initialize();
1239+
1240+ return API;
1241+}
1242+
1243+Monocle.pieceLoaded('core/factory');
1244+Monocle.Events = {}
1245+
1246+
1247+// Fire a custom event on a given target element. The attached data object will
1248+// be available to all listeners at evt.m.
1249+//
1250+// Internet Explorer does not permit custom events; we'll wait for a
1251+// version of IE that supports the W3C model.
1252+//
1253+Monocle.Events.dispatch = function (elem, evtType, data, cancelable) {
1254+ if (!document.createEvent) {
1255+ return true;
1256+ }
1257+ var evt = document.createEvent("Events");
1258+ evt.initEvent(evtType, false, cancelable || false);
1259+ evt.m = data;
1260+ try {
1261+ return elem.dispatchEvent(evt);
1262+ } catch(e) {
1263+ console.warn("Failed to dispatch event: "+evtType);
1264+ return false;
1265+ }
1266+}
1267+
1268+
1269+// Register a function to be invoked when an event fires.
1270+//
1271+Monocle.Events.listen = function (elem, evtType, fn, useCapture) {
1272+ if (typeof elem == "string") { elem = document.getElementById(elem); }
1273+ return elem.addEventListener(evtType, fn, useCapture || false);
1274+}
1275+
1276+
1277+// De-register a function from an event.
1278+//
1279+Monocle.Events.deafen = function (elem, evtType, fn, useCapture) {
1280+ if (elem.removeEventListener) {
1281+ return elem.removeEventListener(evtType, fn, useCapture || false);
1282+ } else if (elem.detachEvent) {
1283+ try {
1284+ return elem.detachEvent('on'+evtType, fn);
1285+ } catch(e) {}
1286+ }
1287+}
1288+
1289+
1290+// Register a series of functions to listen for the start, move, end
1291+// events of a mouse or touch interaction.
1292+//
1293+// 'fns' argument is an object like:
1294+//
1295+// {
1296+// 'start': function () { ... },
1297+// 'move': function () { ... },
1298+// 'end': function () { ... },
1299+// 'cancel': function () { ... }
1300+// }
1301+//
1302+// All of the functions in this object are optional.
1303+//
1304+// Each function is passed the event, with additional generic info about the
1305+// cursor/touch position:
1306+//
1307+// event.m.offsetX (& offsetY) -- relative to top-left of document
1308+// event.m.registrantX (& registrantY) -- relative to top-left of elem
1309+//
1310+// 'options' argument:
1311+//
1312+// {
1313+// 'useCapture': true/false
1314+// }
1315+//
1316+// Returns an object that can later be passed to Monocle.Events.deafenForContact
1317+//
1318+Monocle.Events.listenForContact = function (elem, fns, options) {
1319+ var listeners = {};
1320+
1321+ var cursorInfo = function (evt, ci) {
1322+ evt.m = {
1323+ pageX: ci.pageX,
1324+ pageY: ci.pageY
1325+ };
1326+
1327+ var target = evt.target || evt.srcElement;
1328+ while (target.nodeType != 1 && target.parentNode) {
1329+ target = target.parentNode;
1330+ }
1331+
1332+ // The position of contact from the top left of the element
1333+ // on which the event fired.
1334+ var offset = offsetFor(evt, target);
1335+ evt.m.offsetX = offset[0];
1336+ evt.m.offsetY = offset[1];
1337+
1338+ // The position of contact from the top left of the element
1339+ // on which the event is registered.
1340+ if (evt.currentTarget) {
1341+ offset = offsetFor(evt, evt.currentTarget);
1342+ evt.m.registrantX = offset[0];
1343+ evt.m.registrantY = offset[1];
1344+ }
1345+
1346+ return evt;
1347+ }
1348+
1349+
1350+ var offsetFor = function (evt, elem) {
1351+ var r;
1352+ if (elem.getBoundingClientRect) {
1353+ var er = elem.getBoundingClientRect();
1354+ var dr = document.documentElement.getBoundingClientRect();
1355+ r = { left: er.left - dr.left, top: er.top - dr.top };
1356+ } else {
1357+ r = { left: elem.offsetLeft, top: elem.offsetTop }
1358+ while (elem = elem.offsetParent) {
1359+ if (elem.offsetLeft || elem.offsetTop) {
1360+ r.left += elem.offsetLeft;
1361+ r.top += elem.offsetTop;
1362+ }
1363+ }
1364+ }
1365+ return [evt.m.pageX - r.left, evt.m.pageY - r.top];
1366+ }
1367+
1368+
1369+ var capture = (options && options.useCapture) || false;
1370+
1371+ if (!Monocle.Browser.env.touch) {
1372+ if (fns.start) {
1373+ listeners.mousedown = function (evt) {
1374+ if (evt.button != 0) { return; }
1375+ fns.start(cursorInfo(evt, evt));
1376+ }
1377+ Monocle.Events.listen(elem, 'mousedown', listeners.mousedown, capture);
1378+ }
1379+ if (fns.move) {
1380+ listeners.mousemove = function (evt) {
1381+ fns.move(cursorInfo(evt, evt));
1382+ }
1383+ Monocle.Events.listen(elem, 'mousemove', listeners.mousemove, capture);
1384+ }
1385+ if (fns.end) {
1386+ listeners.mouseup = function (evt) {
1387+ fns.end(cursorInfo(evt, evt));
1388+ }
1389+ Monocle.Events.listen(elem, 'mouseup', listeners.mouseup, capture);
1390+ }
1391+ if (fns.cancel) {
1392+ listeners.mouseout = function (evt) {
1393+ obj = evt.relatedTarget || evt.fromElement;
1394+ while (obj && (obj = obj.parentNode)) {
1395+ if (obj == elem) { return; }
1396+ }
1397+ fns.cancel(cursorInfo(evt, evt));
1398+ }
1399+ Monocle.Events.listen(elem, 'mouseout', listeners.mouseout, capture);
1400+ }
1401+ } else {
1402+ if (fns.start) {
1403+ listeners.start = function (evt) {
1404+ if (evt.touches.length > 1) { return; }
1405+ fns.start(cursorInfo(evt, evt.targetTouches[0]));
1406+ }
1407+ }
1408+ if (fns.move) {
1409+ listeners.move = function (evt) {
1410+ if (evt.touches.length > 1) { return; }
1411+ fns.move(cursorInfo(evt, evt.targetTouches[0]));
1412+ }
1413+ }
1414+ if (fns.end) {
1415+ listeners.end = function (evt) {
1416+ fns.end(cursorInfo(evt, evt.changedTouches[0]));
1417+ // BROWSERHACK:
1418+ // If there is something listening for contact end events, we always
1419+ // prevent the default, because TouchMonitor can't do it (since it
1420+ // fires it on a delay: ugh). Would be nice to remove this line and
1421+ // standardise things.
1422+ evt.preventDefault();
1423+ }
1424+ }
1425+ if (fns.cancel) {
1426+ listeners.cancel = function (evt) {
1427+ fns.cancel(cursorInfo(evt, evt.changedTouches[0]));
1428+ }
1429+ }
1430+
1431+ if (Monocle.Browser.env.brokenIframeTouchModel) {
1432+ Monocle.Events.tMonitor = Monocle.Events.tMonitor ||
1433+ new Monocle.Events.TouchMonitor();
1434+ Monocle.Events.tMonitor.listen(elem, listeners, options);
1435+ } else {
1436+ for (etype in listeners) {
1437+ Monocle.Events.listen(elem, 'touch'+etype, listeners[etype], capture);
1438+ }
1439+ }
1440+ }
1441+
1442+ return listeners;
1443+}
1444+
1445+
1446+// The 'listeners' argument is a hash of event names and the functions that
1447+// are registered to them -- de-registers the functions from the events.
1448+//
1449+Monocle.Events.deafenForContact = function (elem, listeners) {
1450+ var prefix = "";
1451+ if (Monocle.Browser.env.touch) {
1452+ prefix = Monocle.Browser.env.brokenIframeTouchModel ? "contact" : "touch";
1453+ }
1454+
1455+ for (evtType in listeners) {
1456+ Monocle.Events.deafen(elem, prefix + evtType, listeners[evtType]);
1457+ }
1458+}
1459+
1460+
1461+// Looks for start/end events on an element without significant move events in
1462+// between. Fires on the end event.
1463+//
1464+// Also sets up a dummy click event on Kindle3, so that the elem becomes a
1465+// cursor target.
1466+//
1467+// If the optional activeClass string is provided, and if the element was
1468+// created by a Monocle.Factory, then the activeClass will be applied to the
1469+// element while it is being tapped.
1470+//
1471+// Returns a listeners object that you should pass to deafenForTap if you
1472+// need to.
1473+Monocle.Events.listenForTap = function (elem, fn, activeClass) {
1474+ var startPos;
1475+
1476+ // On Kindle, register a noop function with click to make the elem a
1477+ // cursor target.
1478+ if (Monocle.Browser.on.Kindle3) {
1479+ Monocle.Events.listen(elem, 'click', function () {});
1480+ }
1481+
1482+ var annul = function () {
1483+ startPos = null;
1484+ if (activeClass && elem.dom) { elem.dom.removeClass(activeClass); }
1485+ }
1486+
1487+ var annulIfOutOfBounds = function (evt) {
1488+ // We don't have to track this nonsense for mouse events.
1489+ if (evt.type.match(/^mouse/)) {
1490+ return;
1491+ }
1492+ // Doesn't work on iOS 3.1 for some reason, so ignore for that version.
1493+ if (Monocle.Browser.is.MobileSafari && Monocle.Browser.iOSVersion < "3.2") {
1494+ return;
1495+ }
1496+ if (
1497+ evt.m.registrantX < 0 || evt.m.registrantX > elem.offsetWidth ||
1498+ evt.m.registrantY < 0 || evt.m.registrantY > elem.offsetHeight
1499+ ) {
1500+ annul();
1501+ }
1502+ }
1503+
1504+ return Monocle.Events.listenForContact(
1505+ elem,
1506+ {
1507+ start: function (evt) {
1508+ startPos = [evt.m.pageX, evt.m.pageY];
1509+ if (activeClass && elem.dom) { elem.dom.addClass(activeClass); }
1510+ },
1511+ move: annulIfOutOfBounds,
1512+ end: function (evt) {
1513+ annulIfOutOfBounds(evt);
1514+ if (startPos) {
1515+ evt.m.startOffset = startPos;
1516+ fn(evt);
1517+ }
1518+ annul();
1519+ },
1520+ cancel: annul
1521+ },
1522+ {
1523+ useCapture: false
1524+ }
1525+ );
1526+}
1527+
1528+
1529+Monocle.Events.deafenForTap = Monocle.Events.deafenForContact;
1530+
1531+
1532+// Listen for the next transition-end event on the given element, call
1533+// the function, then deafen.
1534+//
1535+// Returns a function that can be used to cancel the listen early.
1536+//
1537+Monocle.Events.afterTransition = function (elem, fn) {
1538+ var evtName = "transitionend";
1539+ if (Monocle.Browser.is.WebKit) {
1540+ evtName = 'webkitTransitionEnd';
1541+ } else if (Monocle.Browser.is.Opera) {
1542+ evtName = 'oTransitionEnd';
1543+ }
1544+ var l = null, cancel = null;
1545+ l = function () { fn(); cancel(); }
1546+ cancel = function () { Monocle.Events.deafen(elem, evtName, l); }
1547+ Monocle.Events.listen(elem, evtName, l);
1548+ return cancel;
1549+}
1550+
1551+
1552+
1553+// BROWSERHACK: iOS touch events on iframes are busted. The TouchMonitor,
1554+// transposes touch events on underlying iframes onto the elements that
1555+// sit above them. It's a massive hack.
1556+Monocle.Events.TouchMonitor = function () {
1557+ if (Monocle.Events == this) {
1558+ return new Monocle.Events.TouchMonitor();
1559+ }
1560+
1561+ var API = { constructor: Monocle.Events.TouchMonitor }
1562+ var k = API.constants = API.constructor;
1563+ var p = API.properties = {
1564+ touching: null,
1565+ edataPrev: null,
1566+ originator: null,
1567+ brokenModel_4_1: navigator.userAgent.match(/ OS 4_1/)
1568+ }
1569+
1570+
1571+ function listenOnIframe(iframe) {
1572+ if (iframe.contentDocument) {
1573+ enableTouchProxy(iframe.contentDocument);
1574+ iframe.contentDocument.isTouchFrame = true;
1575+ }
1576+
1577+ // IN 4.1 ONLY, a touchstart/end/both fires on the *frame* itself when a
1578+ // touch completes on a part of the iframe that is not overlapped by
1579+ // anything. This should be translated to a touchend on the touching
1580+ // object.
1581+ if (p.brokenModel_4_1) {
1582+ enableTouchProxy(iframe);
1583+ }
1584+ }
1585+
1586+
1587+ function listen(element, fns, useCapture) {
1588+ for (etype in fns) {
1589+ Monocle.Events.listen(element, 'contact'+etype, fns[etype], useCapture);
1590+ }
1591+ enableTouchProxy(element, useCapture);
1592+ }
1593+
1594+
1595+ function enableTouchProxy(element, useCapture) {
1596+ if (element.monocleTouchProxy) {
1597+ return;
1598+ }
1599+ element.monocleTouchProxy = true;
1600+
1601+ var fn = function (evt) { touchProxyHandler(element, evt) }
1602+ Monocle.Events.listen(element, "touchstart", fn, useCapture);
1603+ Monocle.Events.listen(element, "touchmove", fn, useCapture);
1604+ Monocle.Events.listen(element, "touchend", fn, useCapture);
1605+ Monocle.Events.listen(element, "touchcancel", fn, useCapture);
1606+ }
1607+
1608+
1609+ function touchProxyHandler(element, evt) {
1610+ var edata = {
1611+ start: evt.type == "touchstart",
1612+ move: evt.type == "touchmove",
1613+ end: evt.type == "touchend" || evt.type == "touchcancel",
1614+ time: new Date().getTime(),
1615+ frame: element.isTouchFrame
1616+ }
1617+
1618+ if (!p.touching) {
1619+ p.originator = element;
1620+ }
1621+
1622+ var target = element;
1623+ var touch = evt.touches[0] || evt.changedTouches[0];
1624+ target = document.elementFromPoint(touch.screenX, touch.screenY);
1625+
1626+ if (target) {
1627+ translateTouchEvent(element, target, evt, edata);
1628+ }
1629+ }
1630+
1631+
1632+ function translateTouchEvent(element, target, evt, edata) {
1633+ // IN 4.1 ONLY, if we have a touch start on the layer, and it's been
1634+ // almost no time since we had a touch end on the layer, discard the start.
1635+ // (This is the most broken thing about 4.1.)
1636+ // FIXME: this seems to discard all "taps" on a naked iframe.
1637+ if (
1638+ p.brokenModel_4_1 &&
1639+ !edata.frame &&
1640+ !p.touching &&
1641+ edata.start &&
1642+ p.edataPrev &&
1643+ p.edataPrev.end &&
1644+ (edata.time - p.edataPrev.time) < 30
1645+ ) {
1646+ evt.preventDefault();
1647+ return;
1648+ }
1649+
1650+ // If we don't have a touch and we see a start or a move on anything, start
1651+ // a touch.
1652+ if (!p.touching && !edata.end) {
1653+ return fireStart(evt, target, edata);
1654+ }
1655+
1656+ // If this is a move event and we already have a touch, continue the move.
1657+ if (edata.move && p.touching) {
1658+ return fireMove(evt, edata);
1659+ }
1660+
1661+ if (p.brokenModel_4_1) {
1662+ // IN 4.1 ONLY, if we have a touch in progress, and we see a start, end
1663+ // or cancel event (moves are covered in previous rule), and the event
1664+ // is not on the iframe, end the touch.
1665+ // (This is because 4.1 bizarrely sends a random event to the layer
1666+ // above the iframe, rather than an end event to the iframe itself.)
1667+ if (p.touching && !edata.frame) {
1668+ // However, a touch start will fire on the layer when moving out of
1669+ // the overlap with the frame. This would trigger the end of the touch.
1670+ // And the immediately subsequent move starts a new touch.
1671+ //
1672+ // To get around this, we only provisionally end the touch - if we get
1673+ // a touchmove momentarily, we'll cancel this touchend.
1674+ return fireProvisionalEnd(evt, edata);
1675+ }
1676+ } else {
1677+ // In older versions of MobileSafari, if the touch ends when we're
1678+ // touching something, just fire it.
1679+ if (edata.end && p.touching) {
1680+ return fireProvisionalEnd(evt, edata);
1681+ }
1682+ }
1683+
1684+ // IN 4.1 ONLY, a touch that has started outside an iframe should not be
1685+ // endable by the iframe.
1686+ if (
1687+ p.brokenModel_4_1 &&
1688+ p.originator != element &&
1689+ edata.frame &&
1690+ edata.end
1691+ ) {
1692+ evt.preventDefault();
1693+ return;
1694+ }
1695+
1696+ // If we see a touch end on the frame, end the touch.
1697+ if (edata.frame && edata.end && p.touching) {
1698+ return fireProvisionalEnd(evt, edata);
1699+ }
1700+ }
1701+
1702+
1703+ function fireStart(evt, target, edata) {
1704+ p.touching = target;
1705+ p.edataPrev = edata;
1706+ return fireTouchEvent(p.touching, 'start', evt);
1707+ }
1708+
1709+
1710+ function fireMove(evt, edata) {
1711+ clearProvisionalEnd();
1712+ p.edataPrev = edata;
1713+ return fireTouchEvent(p.touching, 'move', evt);
1714+ }
1715+
1716+
1717+ function fireEnd(evt, edata) {
1718+ var result = fireTouchEvent(p.touching, 'end', evt);
1719+ p.edataPrev = edata;
1720+ p.touching = null;
1721+ return result;
1722+ }
1723+
1724+
1725+ function fireProvisionalEnd(evt, edata) {
1726+ clearProvisionalEnd();
1727+ var mimicEvt = mimicTouchEvent(p.touching, 'end', evt);
1728+ p.edataPrev = edata;
1729+
1730+ p.provisionalEnd = setTimeout(
1731+ function() {
1732+ if (p.touching) {
1733+ p.touching.dispatchEvent(mimicEvt);
1734+ p.touching = null;
1735+ }
1736+ },
1737+ 30
1738+ );
1739+ }
1740+
1741+
1742+ function clearProvisionalEnd() {
1743+ if (p.provisionalEnd) {
1744+ clearTimeout(p.provisionalEnd);
1745+ p.provisionalEnd = null;
1746+ }
1747+ }
1748+
1749+
1750+ function mimicTouchEvent(target, newtype, evt) {
1751+ var cloneTouch = function (t) {
1752+ return document.createTouch(
1753+ document.defaultView,
1754+ target,
1755+ t.identifier,
1756+ t.screenX,
1757+ t.screenY,
1758+ t.screenX,
1759+ t.screenY
1760+ );
1761+ }
1762+
1763+ var findTouch = function (id) {
1764+ for (var i = 0; i < touches.all.length; ++i) {
1765+ if (touches.all[i].identifier == id) {
1766+ return touches.all[i];
1767+ }
1768+ }
1769+ }
1770+
1771+ // Mimic the event data, dispatching it on the new target.
1772+ var touches = { all: [], target: [], changed: [] };
1773+ for (var i = 0; i < evt.touches.length; ++i) {
1774+ touches.all.push(cloneTouch(evt.touches[i]));
1775+ }
1776+ for (var i = 0; i < evt.targetTouches.length; ++i) {
1777+ touches.target.push(
1778+ findTouch(evt.targetTouches[i].identifier) ||
1779+ cloneTouch(evt.targetTouches[i])
1780+ );
1781+ }
1782+ for (var i = 0; i < evt.changedTouches.length; ++i) {
1783+ touches.changed.push(
1784+ findTouch(evt.changedTouches[i].identifier) ||
1785+ cloneTouch(evt.changedTouches[i])
1786+ );
1787+ }
1788+
1789+ var mimicEvt = document.createEvent('TouchEvent');
1790+ mimicEvt.initTouchEvent(
1791+ "contact"+newtype,
1792+ true,
1793+ true,
1794+ document.defaultView,
1795+ evt.detail,
1796+ evt.screenX,
1797+ evt.screenY,
1798+ evt.screenX,
1799+ evt.screenY,
1800+ evt.ctrlKey,
1801+ evt.altKey,
1802+ evt.shiftKey,
1803+ evt.metaKey,
1804+ document.createTouchList.apply(document, touches.all),
1805+ document.createTouchList.apply(document, touches.target),
1806+ document.createTouchList.apply(document, touches.changed),
1807+ evt.scale,
1808+ evt.rotation
1809+ );
1810+
1811+ return mimicEvt;
1812+ }
1813+
1814+
1815+ function fireTouchEvent(target, newtype, evt) {
1816+ var mimicEvt = mimicTouchEvent(target, newtype, evt);
1817+ var result = target.dispatchEvent(mimicEvt);
1818+ if (!result) {
1819+ evt.preventDefault();
1820+ }
1821+ return result;
1822+ }
1823+
1824+
1825+ API.listen = listen;
1826+ API.listenOnIframe = listenOnIframe;
1827+
1828+ return API;
1829+}
1830+
1831+
1832+Monocle.Events.listenOnIframe = function (frame) {
1833+ if (!Monocle.Browser.env.brokenIframeTouchModel) {
1834+ return;
1835+ }
1836+ Monocle.Events.tMonitor = Monocle.Events.tMonitor ||
1837+ new Monocle.Events.TouchMonitor();
1838+ Monocle.Events.tMonitor.listenOnIframe(frame);
1839+}
1840+
1841+Monocle.pieceLoaded('core/events');
1842+Monocle.Styles = {
1843+
1844+ // Takes a hash and returns a string.
1845+ rulesToString: function (rules) {
1846+ if (typeof rules != 'string') {
1847+ var parts = [];
1848+ for (var declaration in rules) {
1849+ parts.push(declaration+": "+rules[declaration]+";")
1850+ }
1851+ rules = parts.join(" ");
1852+ }
1853+ return rules;
1854+ },
1855+
1856+
1857+ // Takes a hash or string of CSS property assignments and applies them
1858+ // to the element.
1859+ //
1860+ applyRules: function (elem, rules) {
1861+ rules = Monocle.Styles.rulesToString(rules);
1862+ elem.style.cssText += ';'+rules;
1863+ return elem.style.cssText;
1864+ },
1865+
1866+
1867+ // Generates cross-browser properties for a given property.
1868+ // ie, affix(<elem>, 'transition', 'linear 100ms') would apply that value
1869+ // to webkitTransition for WebKit browsers, and to MozTransition for Gecko.
1870+ //
1871+ affix: function (elem, property, value) {
1872+ var target = elem.style ? elem.style : elem;
1873+ var props = Monocle.Browser.css.toDOMProps(property);
1874+ while (props.length) { target[props.shift()] = value; }
1875+ },
1876+
1877+
1878+ setX: function (elem, x) {
1879+ var s = elem.style;
1880+ if (typeof x == "number") { x += "px"; }
1881+ if (Monocle.Browser.env.supportsTransform3d) {
1882+ s.webkitTransform = "translate3d("+x+", 0, 0)";
1883+ } else {
1884+ s.webkitTransform = "translateX("+x+")";
1885+ }
1886+ s.MozTransform = s.OTransform = s.transform = "translateX("+x+")";
1887+ return x;
1888+ },
1889+
1890+
1891+ setY: function (elem, y) {
1892+ var s = elem.style;
1893+ if (typeof y == "number") { y += "px"; }
1894+ if (Monocle.Browser.env.supportsTransform3d) {
1895+ s.webkitTransform = "translate3d(0, "+y+", 0)";
1896+ } else {
1897+ s.webkitTransform = "translateY("+y+")";
1898+ }
1899+ s.MozTransform = s.OTransform = s.transform = "translateY("+y+")";
1900+ return y;
1901+ },
1902+
1903+
1904+ getX: function (elem) {
1905+ var currStyle = document.defaultView.getComputedStyle(elem, null);
1906+ var re = /matrix\([^,]+,[^,]+,[^,]+,[^,]+,\s*([^,]+),[^\)]+\)/;
1907+ var props = Monocle.Browser.css.toDOMProps('transform');
1908+ var matrix = null;
1909+ while (props.length && !matrix) {
1910+ matrix = currStyle[props.shift()];
1911+ }
1912+ return parseInt(matrix.match(re)[1]);
1913+ },
1914+
1915+
1916+ transitionFor: function (elem, prop, duration, timing, delay) {
1917+ var tProps = Monocle.Browser.css.toDOMProps('transition');
1918+ var pProps = Monocle.Browser.css.toCSSProps(prop);
1919+ timing = timing || "linear";
1920+ delay = (delay || 0)+"ms";
1921+ for (var i = 0, ii = tProps.length; i < ii; ++i) {
1922+ var t = "none";
1923+ if (duration) {
1924+ t = [pProps[i], duration+"ms", timing, delay].join(" ");
1925+ }
1926+ elem.style[tProps[i]] = t;
1927+ }
1928+ }
1929+
1930+}
1931+
1932+
1933+// These rule definitions are more or less compulsory for Monocle to behave
1934+// as expected. Which is why they appear here and not in the stylesheet.
1935+// Adjust them if you know what you're doing.
1936+//
1937+Monocle.Styles.container = {
1938+ "position": "absolute",
1939+ "top": "0",
1940+ "left": "0",
1941+ "bottom": "0",
1942+ "right": "0"
1943+}
1944+
1945+Monocle.Styles.page = {
1946+ "position": "absolute",
1947+ "z-index": "1",
1948+ "-webkit-user-select": "none",
1949+ "-moz-user-select": "none",
1950+ "user-select": "none",
1951+ "-webkit-transform": "translate3d(0,0,0)",
1952+ "visibility": "visible"
1953+
1954+ /*
1955+ "background": "white",
1956+ "top": "0",
1957+ "left": "0",
1958+ "bottom": "0",
1959+ "right": "0"
1960+ */
1961+}
1962+
1963+Monocle.Styles.sheaf = {
1964+ "position": "absolute",
1965+ "overflow": "hidden"
1966+
1967+ /*
1968+ "top": "0",
1969+ "left": "0",
1970+ "bottom": "0",
1971+ "right": "0"
1972+ */
1973+}
1974+
1975+Monocle.Styles.component = {
1976+ "width": "100%",
1977+ "height": "100%",
1978+ "border": "none",
1979+ "-webkit-user-select": "none",
1980+ "-moz-user-select": "none",
1981+ "user-select": "none"
1982+}
1983+
1984+Monocle.Styles.control = {
1985+ "z-index": "100",
1986+ "cursor": "pointer"
1987+}
1988+
1989+Monocle.Styles.overlay = {
1990+ "position": "absolute",
1991+ "display": "none",
1992+ "width": "100%",
1993+ "height": "100%",
1994+ "z-index": "1000"
1995+}
1996+
1997+
1998+
1999+Monocle.pieceLoaded('core/styles');
2000+// READER
2001+//
2002+//
2003+// The full DOM hierarchy created by Reader is:
2004+//
2005+// box
2006+// -> container
2007+// -> pages (the number of page elements is determined by the flipper)
2008+// -> sheaf (basically just sets the margins)
2009+// -> component (an iframe created by the current component)
2010+// -> body (the document.body of the iframe)
2011+// -> page controls
2012+// -> standard controls
2013+// -> overlay
2014+// -> modal/popover controls
2015+//
2016+//
2017+// Options:
2018+//
2019+// flipper: The class of page flipper to use.
2020+//
2021+// panels: The class of panels to use
2022+//
2023+// stylesheet: A string of CSS rules to apply to the contents of each
2024+// component loaded into the reader.
2025+//
2026+// place: A book locus for the page to open to when the reader is
2027+// initialized. (See comments at Book#pageNumberAt for more about
2028+// the locus option).
2029+//
2030+// systemId: the id for root elements of components, defaults to "RS:monocle"
2031+//
2032+Monocle.Reader = function (node, bookData, options, onLoadCallback) {
2033+ if (Monocle == this) {
2034+ return new Monocle.Reader(node, bookData, options, onLoadCallback);
2035+ }
2036+
2037+ var API = { constructor: Monocle.Reader }
2038+ var k = API.constants = API.constructor;
2039+ var p = API.properties = {
2040+ // Initialization-completed flag.
2041+ initialized: false,
2042+
2043+ // The active book.
2044+ book: null,
2045+
2046+ // DOM graph of factory-generated objects.
2047+ graph: {},
2048+
2049+ // An array of style rules that are automatically applied to every page.
2050+ pageStylesheets: [],
2051+
2052+ // Id applied to the HTML element of each component, can be used to scope
2053+ // CSS rules.
2054+ systemId: (options ? options.systemId : null) || k.DEFAULT_SYSTEM_ID,
2055+
2056+ // Prefix for classnames for any created element.
2057+ classPrefix: k.DEFAULT_CLASS_PREFIX,
2058+
2059+ // Registered control objects (see addControl). Hashes of the form:
2060+ // {
2061+ // control: <control instance>,
2062+ // elements: <array of topmost elements created by control>,
2063+ // controlType: <standard, page, modal, popover, invisible, etc>
2064+ // }
2065+ controls: [],
2066+
2067+ // After the reader has been resized, this resettable timer must expire
2068+ // the place is restored.
2069+ resizeTimer: null
2070+ }
2071+
2072+ var dom;
2073+
2074+
2075+ // Inspects the browser environment and kicks off preparing the container.
2076+ //
2077+ function initialize() {
2078+ options = options || {}
2079+
2080+ Monocle.Browser.survey(prepareBox);
2081+ }
2082+
2083+
2084+ // Sets up the container and internal elements.
2085+ //
2086+ function prepareBox() {
2087+ var box = node;
2088+ if (typeof box == "string") { box = document.getElementById(box); }
2089+ dom = API.dom = box.dom = new Monocle.Factory(box, 'box', 0, API);
2090+
2091+ if (!Monocle.Browser.env.isCompatible()) {
2092+ if (dispatchEvent("monocle:incompatible", {}, true)) {
2093+ // TODO: Something cheerier? More informative?
2094+ box.innerHTML = "Your browser is not compatible with Monocle.";
2095+ }
2096+ return;
2097+ }
2098+
2099+ dispatchEvent("monocle:initializing");
2100+
2101+ var bk;
2102+ if (bookData) {
2103+ bk = new Monocle.Book(bookData);
2104+ } else {
2105+ bk = Monocle.Book.fromNodes([box.cloneNode(true)]);
2106+ }
2107+ box.innerHTML = "";
2108+
2109+ // Make sure the box div is absolutely or relatively positioned.
2110+ positionBox();
2111+
2112+ // Attach the page-flipping gadget.
2113+ attachFlipper(options.flipper);
2114+
2115+ // Create the essential DOM elements.
2116+ createReaderElements();
2117+
2118+ // Clamp page frames to a set of styles that reduce Monocle breakage.
2119+ clampStylesheets(options.stylesheet);
2120+
2121+ primeFrames(options.primeURL, function () {
2122+ // Make the reader elements look pretty.
2123+ applyStyles();
2124+
2125+ listen('monocle:componentmodify', persistPageStylesOnComponentChange);
2126+
2127+ p.flipper.listenForInteraction(options.panels);
2128+
2129+ setBook(bk, options.place, function () {
2130+ p.initialized = true;
2131+ if (onLoadCallback) { onLoadCallback(API); }
2132+ dispatchEvent("monocle:loaded");
2133+ });
2134+ });
2135+ }
2136+
2137+
2138+ function positionBox() {
2139+ var currPosVal;
2140+ var box = dom.find('box');
2141+ if (document.defaultView) {
2142+ var currStyle = document.defaultView.getComputedStyle(box, null);
2143+ currPosVal = currStyle.getPropertyValue('position');
2144+ } else if (box.currentStyle) {
2145+ currPosVal = box.currentStyle.position
2146+ }
2147+ if (["absolute", "relative"].indexOf(currPosVal) == -1) {
2148+ box.style.position = "relative";
2149+ }
2150+ }
2151+
2152+
2153+ function attachFlipper(flipperClass) {
2154+ if (!flipperClass) {
2155+ if (!Monocle.Browser.env.supportsColumns) {
2156+ flipperClass = Monocle.Flippers.Legacy;
2157+ } else if (Monocle.Browser.on.Kindle3) {
2158+ flipperClass = Monocle.Flippers.Instant;
2159+ }
2160+
2161+ flipperClass = flipperClass || Monocle.Flippers.Slider;
2162+ }
2163+
2164+ if (!flipperClass) { throw("Flipper not found."); }
2165+
2166+ p.flipper = new flipperClass(API, null, p.readerOptions);
2167+ }
2168+
2169+
2170+ function createReaderElements() {
2171+ var cntr = dom.append('div', 'container');
2172+ for (var i = 0; i < p.flipper.pageCount; ++i) {
2173+ var page = cntr.dom.append('div', 'page', i);
2174+ page.style.visibility = "hidden";
2175+ page.m = { reader: API, pageIndex: i, place: null }
2176+ page.m.sheafDiv = page.dom.append('div', 'sheaf', i);
2177+ page.m.activeFrame = page.m.sheafDiv.dom.append('iframe', 'component', i);
2178+ page.m.activeFrame.m = { 'pageDiv': page };
2179+ page.m.activeFrame.setAttribute('frameBorder', 0);
2180+ page.m.activeFrame.setAttribute('scrolling', 'no');
2181+ p.flipper.addPage(page);
2182+ // BROWSERHACK: hook up the iframe to the touchmonitor if it exists.
2183+ Monocle.Events.listenOnIframe(page.m.activeFrame);
2184+ }
2185+ dom.append('div', 'overlay');
2186+ dispatchEvent("monocle:loading");
2187+ }
2188+
2189+
2190+ function clampStylesheets(customStylesheet) {
2191+ var defCSS = k.DEFAULT_STYLE_RULES;
2192+ if (Monocle.Browser.env.floatsIgnoreColumns) {
2193+ defCSS.push("html#RS\\:monocle * { float: none !important; }");
2194+ }
2195+ p.defaultStyles = addPageStyles(defCSS, false);
2196+ if (customStylesheet) {
2197+ p.initialStyles = addPageStyles(customStylesheet, false);
2198+ }
2199+ }
2200+
2201+
2202+ // Opens the frame to a particular URL (usually 'about:blank').
2203+ //
2204+ function primeFrames(url, callback) {
2205+ url = url || (Monocle.Browser.on.UIWebView ? "blank.html" : "about:blank");
2206+
2207+ var pageCount = 0;
2208+
2209+ var cb = function (evt) {
2210+ var frame = evt.target || evt.srcElement;
2211+ Monocle.Events.deafen(frame, 'load', cb);
2212+ dispatchEvent(
2213+ 'monocle:frameprimed',
2214+ { frame: frame, pageIndex: pageCount }
2215+ );
2216+ if ((pageCount += 1) == p.flipper.pageCount) {
2217+ Monocle.defer(callback);
2218+ }
2219+ }
2220+
2221+ forEachPage(function (page) {
2222+ Monocle.Events.listen(page.m.activeFrame, 'load', cb);
2223+ page.m.activeFrame.src = url;
2224+ });
2225+ }
2226+
2227+
2228+ function applyStyles() {
2229+ dom.find('container').dom.setStyles(Monocle.Styles.container);
2230+ forEachPage(function (page, i) {
2231+ page.dom.setStyles(Monocle.Styles.page);
2232+ dom.find('sheaf', i).dom.setStyles(Monocle.Styles.sheaf);
2233+ var cmpt = dom.find('component', i)
2234+ cmpt.dom.setStyles(Monocle.Styles.component);
2235+ });
2236+ lockFrameWidths();
2237+ dom.find('overlay').dom.setStyles(Monocle.Styles.overlay);
2238+ dispatchEvent('monocle:styles');
2239+ }
2240+
2241+
2242+ function lockingFrameWidths() {
2243+ if (!Monocle.Browser.env.relativeIframeExpands) { return; }
2244+ for (var i = 0, cmpt; cmpt = dom.find('component', i); ++i) {
2245+ cmpt.style.display = "none";
2246+ }
2247+ }
2248+
2249+
2250+ function lockFrameWidths() {
2251+ if (!Monocle.Browser.env.relativeIframeExpands) { return; }
2252+ for (var i = 0, cmpt; cmpt = dom.find('component', i); ++i) {
2253+ cmpt.style.width = cmpt.parentNode.offsetWidth+"px";
2254+ cmpt.style.display = "block";
2255+ }
2256+ }
2257+
2258+
2259+ // Apply the book, move to a particular place or just the first page, wait
2260+ // for everything to complete, then fire the callback.
2261+ //
2262+ function setBook(bk, place, callback) {
2263+ p.book = bk;
2264+ var pageCount = 0;
2265+ if (typeof callback == 'function') {
2266+ var watchers = {
2267+ 'monocle:componentchange': function (evt) {
2268+ dispatchEvent('monocle:firstcomponentchange', evt.m);
2269+ return (pageCount += 1) == p.flipper.pageCount;
2270+ },
2271+ 'monocle:turn': function (evt) {
2272+ callback();
2273+ return true;
2274+ }
2275+ }
2276+ var listener = function (evt) {
2277+ if (watchers[evt.type](evt)) { deafen(evt.type, listener); }
2278+ }
2279+ for (evtType in watchers) { listen(evtType, listener) }
2280+ }
2281+ p.flipper.moveTo(place || { page: 1 });
2282+ }
2283+
2284+
2285+ function getBook() {
2286+ return p.book;
2287+ }
2288+
2289+
2290+ // Attempts to restore the place we were up to in the book before the
2291+ // reader was resized.
2292+ //
2293+ function resized() {
2294+ if (!p.initialized) {
2295+ console.warn('Attempt to resize book before initialization.');
2296+ }
2297+ lockingFrameWidths();
2298+ if (!dispatchEvent("monocle:resizing", {}, true)) {
2299+ return;
2300+ }
2301+ clearTimeout(p.resizeTimer);
2302+ p.resizeTimer = setTimeout(
2303+ function () {
2304+ lockFrameWidths();
2305+ recalculateDimensions(true);
2306+ dispatchEvent("monocle:resize");
2307+ },
2308+ k.RESIZE_DELAY
2309+ );
2310+ }
2311+
2312+
2313+ function recalculateDimensions(andRestorePlace) {
2314+ if (!p.book) { return; }
2315+ dispatchEvent("monocle:recalculating");
2316+
2317+ var place, locus;
2318+ if (andRestorePlace !== false) {
2319+ var place = getPlace();
2320+ var locus = { percent: place ? place.percentageThrough() : 0 };
2321+ }
2322+
2323+ // Better to use an event? Or chaining consecutively?
2324+ forEachPage(function (pageDiv) {
2325+ pageDiv.m.activeFrame.m.component.updateDimensions(pageDiv);
2326+ });
2327+
2328+ Monocle.defer(function () {
2329+ if (locus) { p.flipper.moveTo(locus); }
2330+ dispatchEvent("monocle:recalculated");
2331+ });
2332+ }
2333+
2334+
2335+ // Returns the current page number in the book.
2336+ //
2337+ // The pageDiv argument is optional - typically defaults to whatever the
2338+ // flipper thinks is the "active" page.
2339+ //
2340+ function pageNumber(pageDiv) {
2341+ var place = getPlace(pageDiv);
2342+ return place ? (place.pageNumber() || 1) : 1;
2343+ }
2344+
2345+
2346+ // Returns the current "place" in the book -- ie, the page number, chapter
2347+ // title, etc.
2348+ //
2349+ // The pageDiv argument is optional - typically defaults to whatever the
2350+ // flipper thinks is the "active" page.
2351+ //
2352+ function getPlace(pageDiv) {
2353+ if (!p.initialized) {
2354+ console.warn('Attempt to access place before initialization.');
2355+ }
2356+ return p.flipper.getPlace(pageDiv);
2357+ }
2358+
2359+
2360+ // Moves the current page as specified by the locus. See
2361+ // Monocle.Book#pageNumberAt for documentation on the locus argument.
2362+ //
2363+ // The callback argument is optional.
2364+ //
2365+ function moveTo(locus, callback) {
2366+ if (!p.initialized) {
2367+ console.warn('Attempt to move place before initialization.');
2368+ }
2369+ if (!p.book.isValidLocus(locus)) {
2370+ dispatchEvent(
2371+ "monocle:notfound",
2372+ { href: locus ? locus.componentId : "anonymous" }
2373+ );
2374+ return false;
2375+ }
2376+ var fn = callback;
2377+ if (!locus.direction) {
2378+ dispatchEvent('monocle:jumping', { locus: locus });
2379+ fn = function () {
2380+ dispatchEvent('monocle:jump', { locus: locus });
2381+ if (callback) { callback(); }
2382+ }
2383+ }
2384+ p.flipper.moveTo(locus, fn);
2385+ return true;
2386+ }
2387+
2388+
2389+ // Moves to the relevant element in the relevant component.
2390+ //
2391+ function skipToChapter(src) {
2392+ var locus = p.book.locusOfChapter(src);
2393+ return moveTo(locus);
2394+ }
2395+
2396+
2397+ // Valid types:
2398+ // - standard (an overlay above the pages)
2399+ // - page (within the page)
2400+ // - modal (overlay where click-away does nothing, for a single control)
2401+ // - hud (overlay that multiple controls can share)
2402+ // - popover (overlay where click-away removes the ctrl elements)
2403+ // - invisible
2404+ //
2405+ // Options:
2406+ // - hidden -- creates and hides the ctrl elements;
2407+ // use showControl to show them
2408+ //
2409+ function addControl(ctrl, cType, options) {
2410+ for (var i = 0; i < p.controls.length; ++i) {
2411+ if (p.controls[i].control == ctrl) {
2412+ console.warn("Already added control: " + ctrl);
2413+ return;
2414+ }
2415+ }
2416+
2417+ options = options || {};
2418+
2419+ var ctrlData = {
2420+ control: ctrl,
2421+ elements: [],
2422+ controlType: cType
2423+ }
2424+ p.controls.push(ctrlData);
2425+
2426+ var ctrlElem;
2427+ var cntr = dom.find('container'), overlay = dom.find('overlay');
2428+ if (!cType || cType == "standard") {
2429+ ctrlElem = ctrl.createControlElements(cntr);
2430+ cntr.appendChild(ctrlElem);
2431+ ctrlData.elements.push(ctrlElem);
2432+ } else if (cType == "page") {
2433+ forEachPage(function (page, i) {
2434+ var runner = ctrl.createControlElements(page);
2435+ page.appendChild(runner);
2436+ ctrlData.elements.push(runner);
2437+ });
2438+ } else if (cType == "modal" || cType == "popover" || cType == "hud") {
2439+ ctrlElem = ctrl.createControlElements(overlay);
2440+ overlay.appendChild(ctrlElem);
2441+ ctrlData.elements.push(ctrlElem);
2442+ ctrlData.usesOverlay = true;
2443+ } else if (cType == "invisible") {
2444+ if (
2445+ typeof(ctrl.createControlElements) == "function" &&
2446+ (ctrlElem = ctrl.createControlElements(cntr))
2447+ ) {
2448+ cntr.appendChild(ctrlElem);
2449+ ctrlData.elements.push(ctrlElem);
2450+ }
2451+ } else {
2452+ console.warn("Unknown control type: " + cType);
2453+ }
2454+
2455+ for (var i = 0; i < ctrlData.elements.length; ++i) {
2456+ Monocle.Styles.applyRules(ctrlData.elements[i], Monocle.Styles.control);
2457+ }
2458+
2459+ if (options.hidden) {
2460+ hideControl(ctrl);
2461+ } else {
2462+ showControl(ctrl);
2463+ }
2464+
2465+ if (typeof ctrl.assignToReader == 'function') {
2466+ ctrl.assignToReader(API);
2467+ }
2468+
2469+ return ctrl;
2470+ }
2471+
2472+
2473+ function dataForControl(ctrl) {
2474+ for (var i = 0; i < p.controls.length; ++i) {
2475+ if (p.controls[i].control == ctrl) {
2476+ return p.controls[i];
2477+ }
2478+ }
2479+ }
2480+
2481+
2482+ function hideControl(ctrl) {
2483+ var controlData = dataForControl(ctrl);
2484+ if (!controlData) {
2485+ console.warn("No data for control: " + ctrl);
2486+ return;
2487+ }
2488+ if (controlData.hidden) {
2489+ return;
2490+ }
2491+ for (var i = 0; i < controlData.elements.length; ++i) {
2492+ controlData.elements[i].style.display = "none";
2493+ }
2494+ if (controlData.usesOverlay) {
2495+ var overlay = dom.find('overlay');
2496+ overlay.style.display = "none";
2497+ Monocle.Events.deafenForContact(overlay, overlay.listeners);
2498+ }
2499+ controlData.hidden = true;
2500+ if (ctrl.properties) {
2501+ ctrl.properties.hidden = true;
2502+ }
2503+ dispatchEvent('monocle:controlhide', { control: ctrl }, false);
2504+ }
2505+
2506+
2507+ function showControl(ctrl) {
2508+ var controlData = dataForControl(ctrl);
2509+ if (!controlData) {
2510+ console.warn("No data for control: " + ctrl);
2511+ return false;
2512+ }
2513+
2514+ if (showingControl(ctrl)) {
2515+ return false;
2516+ }
2517+
2518+ var overlay = dom.find('overlay');
2519+ if (controlData.usesOverlay && controlData.controlType != "hud") {
2520+ for (var i = 0, ii = p.controls.length; i < ii; ++i) {
2521+ if (p.controls[i].usesOverlay && !p.controls[i].hidden) {
2522+ return false;
2523+ }
2524+ }
2525+ overlay.style.display = "block";
2526+ }
2527+
2528+ for (var i = 0; i < controlData.elements.length; ++i) {
2529+ controlData.elements[i].style.display = "block";
2530+ }
2531+
2532+ if (controlData.controlType == "popover") {
2533+ var onControl = function (evt) {
2534+ var obj = evt.target;
2535+ do {
2536+ if (obj == controlData.elements[0]) { return true; }
2537+ } while (obj && (obj = obj.parentNode));
2538+ return false;
2539+ }
2540+ overlay.listeners = Monocle.Events.listenForContact(
2541+ overlay,
2542+ {
2543+ start: function (evt) { if (!onControl(evt)) { hideControl(ctrl); } },
2544+ move: function (evt) { if (!onControl(evt)) { evt.preventDefault(); } }
2545+ }
2546+ );
2547+ }
2548+ controlData.hidden = false;
2549+ if (ctrl.properties) {
2550+ ctrl.properties.hidden = false;
2551+ }
2552+ dispatchEvent('monocle:controlshow', { control: ctrl }, false);
2553+ return true;
2554+ }
2555+
2556+
2557+ function showingControl(ctrl) {
2558+ var controlData = dataForControl(ctrl);
2559+ return controlData.hidden == false;
2560+ }
2561+
2562+
2563+ function dispatchEvent(evtType, data, cancelable) {
2564+ return Monocle.Events.dispatch(dom.find('box'), evtType, data, cancelable);
2565+ }
2566+
2567+
2568+ function listen(evtType, fn, useCapture) {
2569+ Monocle.Events.listen(dom.find('box'), evtType, fn, useCapture);
2570+ }
2571+
2572+
2573+ function deafen(evtType, fn) {
2574+ Monocle.Events.deafen(dom.find('box'), evtType, fn);
2575+ }
2576+
2577+
2578+ /* PAGE STYLESHEETS */
2579+
2580+ // API for adding a new stylesheet to all components. styleRules should be
2581+ // a string of CSS rules. restorePlace defaults to true.
2582+ //
2583+ // Returns a sheet index value that can be used with updatePageStyles
2584+ // and removePageStyles.
2585+ //
2586+ function addPageStyles(styleRules, restorePlace) {
2587+ return changingStylesheet(function () {
2588+ p.pageStylesheets.push(styleRules);
2589+ var sheetIndex = p.pageStylesheets.length - 1;
2590+
2591+ for (var i = 0; i < p.flipper.pageCount; ++i) {
2592+ var doc = dom.find('component', i).contentDocument;
2593+ addPageStylesheet(doc, sheetIndex);
2594+ }
2595+ return sheetIndex;
2596+ }, restorePlace);
2597+ }
2598+
2599+
2600+ // API for updating the styleRules in an existing page stylesheet across
2601+ // all components. Takes a sheet index value obtained via addPageStyles.
2602+ //
2603+ function updatePageStyles(sheetIndex, styleRules, restorePlace) {
2604+ return changingStylesheet(function () {
2605+ p.pageStylesheets[sheetIndex] = styleRules;
2606+ if (typeof styleRules.join == "function") {
2607+ styleRules = styleRules.join("\n");
2608+ }
2609+ for (var i = 0; i < p.flipper.pageCount; ++i) {
2610+ var doc = dom.find('component', i).contentDocument;
2611+ var styleTag = doc.getElementById('monStylesheet'+sheetIndex);
2612+ if (!styleTag) {
2613+ console.warn('No such stylesheet: ' + sheetIndex);
2614+ return;
2615+ }
2616+ if (styleTag.styleSheet) {
2617+ styleTag.styleSheet.cssText = styleRules;
2618+ } else {
2619+ styleTag.replaceChild(
2620+ doc.createTextNode(styleRules),
2621+ styleTag.firstChild
2622+ );
2623+ }
2624+ }
2625+ }, restorePlace);
2626+ }
2627+
2628+
2629+ // API for removing a page stylesheet from all components. Takes a sheet
2630+ // index value obtained via addPageStyles.
2631+ //
2632+ function removePageStyles(sheetIndex, restorePlace) {
2633+ return changingStylesheet(function () {
2634+ p.pageStylesheets[sheetIndex] = null;
2635+ for (var i = 0; i < p.flipper.pageCount; ++i) {
2636+ var doc = dom.find('component', i).contentDocument;
2637+ var styleTag = doc.getElementById('monStylesheet'+sheetIndex);
2638+ styleTag.parentNode.removeChild(styleTag);
2639+ }
2640+ }, restorePlace);
2641+ }
2642+
2643+
2644+ // Called when a page changes its component. Injects our current page
2645+ // stylesheets into the new component.
2646+ //
2647+ function persistPageStylesOnComponentChange(evt) {
2648+ var doc = evt.m['document'];
2649+ doc.documentElement.id = p.systemId;
2650+ for (var i = 0; i < p.pageStylesheets.length; ++i) {
2651+ if (p.pageStylesheets[i]) {
2652+ addPageStylesheet(doc, i);
2653+ }
2654+ }
2655+ }
2656+
2657+
2658+ // Wraps all API-based stylesheet changes (add, update, remove) in a
2659+ // brace of custom events (stylesheetchanging/stylesheetchange), and
2660+ // recalculates component dimensions if specified (default to true).
2661+ //
2662+ function changingStylesheet(callback, restorePlace) {
2663+ restorePlace = (restorePlace === false) ? false : true;
2664+ if (restorePlace) {
2665+ dispatchEvent("monocle:stylesheetchanging", {});
2666+ }
2667+ var result = callback();
2668+ if (restorePlace) {
2669+ recalculateDimensions(true);
2670+ Monocle.defer(
2671+ function () { dispatchEvent("monocle:stylesheetchange", {}); }
2672+ );
2673+ } else {
2674+ recalculateDimensions(false);
2675+ }
2676+ return result;
2677+ }
2678+
2679+
2680+ // Private method for adding a stylesheet to a component. Used by
2681+ // addPageStyles.
2682+ //
2683+ function addPageStylesheet(doc, sheetIndex) {
2684+ var styleRules = p.pageStylesheets[sheetIndex];
2685+
2686+ if (!styleRules) {
2687+ return;
2688+ }
2689+
2690+ var head = doc.getElementsByTagName('head')[0];
2691+ if (!head) {
2692+ head = doc.createElement('head');
2693+ doc.documentElement.appendChild(head);
2694+ }
2695+
2696+ if (typeof styleRules.join == "function") {
2697+ styleRules = styleRules.join("\n");
2698+ }
2699+
2700+ var styleTag = doc.createElement('style');
2701+ styleTag.type = 'text/css';
2702+ styleTag.id = "monStylesheet"+sheetIndex;
2703+ if (styleTag.styleSheet) {
2704+ styleTag.styleSheet.cssText = styleRules;
2705+ } else {
2706+ styleTag.appendChild(doc.createTextNode(styleRules));
2707+ }
2708+
2709+ head.appendChild(styleTag);
2710+
2711+ return styleTag;
2712+ }
2713+
2714+
2715+ function visiblePages() {
2716+ return p.flipper.visiblePages ? p.flipper.visiblePages() : [dom.find('page')];
2717+ }
2718+
2719+
2720+ function forEachPage(callback) {
2721+ for (var i = 0, ii = p.flipper.pageCount; i < ii; ++i) {
2722+ var page = dom.find('page', i);
2723+ callback(page, i);
2724+ }
2725+ }
2726+
2727+
2728+ API.getBook = getBook;
2729+ API.getPlace = getPlace;
2730+ API.moveTo = moveTo;
2731+ API.skipToChapter = skipToChapter;
2732+ API.resized = resized;
2733+ API.addControl = addControl;
2734+ API.hideControl = hideControl;
2735+ API.showControl = showControl;
2736+ API.showingControl = showingControl;
2737+ API.dispatchEvent = dispatchEvent;
2738+ API.listen = listen;
2739+ API.deafen = deafen;
2740+ API.addPageStyles = addPageStyles;
2741+ API.updatePageStyles = updatePageStyles;
2742+ API.removePageStyles = removePageStyles;
2743+ API.visiblePages = visiblePages;
2744+
2745+ initialize();
2746+
2747+ return API;
2748+}
2749+
2750+
2751+Monocle.Reader.RESIZE_DELAY = 100;
2752+Monocle.Reader.DEFAULT_SYSTEM_ID = 'RS:monocle'
2753+Monocle.Reader.DEFAULT_CLASS_PREFIX = 'monelem_'
2754+Monocle.Reader.DEFAULT_STYLE_RULES = [
2755+ "html#RS\\:monocle * {" +
2756+ "-webkit-font-smoothing: subpixel-antialiased;" +
2757+ "text-rendering: auto !important;" +
2758+ "word-wrap: break-word !important;" +
2759+ "overflow: visible !important;" +
2760+ "}",
2761+ "html#RS\\:monocle body {" +
2762+ "margin: 0 !important;"+
2763+ "border: none !important;" +
2764+ "padding: 0 !important;" +
2765+ "width: 100% !important;" +
2766+ "position: absolute !important;" +
2767+ "-webkit-text-size-adjust: none;" +
2768+ "}",
2769+ "html#RS\\:monocle body * {" +
2770+ "max-width: 100% !important;" +
2771+ "}",
2772+ "html#RS\\:monocle img, html#RS\\:monocle video, html#RS\\:monocle object {" +
2773+ "max-height: 95% !important;" +
2774+ "height: auto !important;" +
2775+ "}"
2776+]
2777+
2778+Monocle.pieceLoaded('core/reader');
2779+/* BOOK */
2780+
2781+/* The Book handles movement through the content by the reader page elements.
2782+ *
2783+ * It's responsible for instantiating components as they are required,
2784+ * and for calculating which component and page number to move to (based on
2785+ * requests from the Reader).
2786+ *
2787+ * It should set and know the place of each page element too.
2788+ *
2789+ */
2790+
2791+Monocle.Book = function (dataSource) {
2792+ if (Monocle == this) { return new Monocle.Book(dataSource); }
2793+
2794+ var API = { constructor: Monocle.Book }
2795+ var k = API.constants = API.constructor;
2796+ var p = API.properties = {
2797+ dataSource: dataSource,
2798+ components: [],
2799+ chapters: {} // flat arrays of chapters per component
2800+ }
2801+
2802+
2803+ function initialize() {
2804+ p.componentIds = dataSource.getComponents();
2805+ p.contents = dataSource.getContents();
2806+ p.lastCIndex = p.componentIds.length - 1;
2807+ }
2808+
2809+
2810+ // Adjusts the given locus object to provide the page number within the
2811+ // current component.
2812+ //
2813+ // If the locus implies movement to another component, the locus
2814+ // 'componentId' property will be updated to point to this component, and
2815+ // the 'load' property will be set to true, which should be taken as a
2816+ // sign to call loadPageAt with a callback.
2817+ //
2818+ // The locus argument is an object that has one of the following properties:
2819+ //
2820+ // - page: positive integer. Counting up from the start of component.
2821+ // - pagesBack: negative integer. Counting back from the end of component.
2822+ // - percent: float
2823+ // - direction: integer relative to the current page number for this pageDiv
2824+ // - position: string, one of "start" or "end", moves to corresponding point
2825+ // in the given component
2826+ // - anchor: an element id within the component
2827+ // - xpath: the node at this XPath within the component
2828+ // - selector: the first node at this CSS selector within the component
2829+ //
2830+ // The locus object can also specify a componentId. If it is not provided
2831+ // we default to the currently active component, and if that doesn't exist,
2832+ // we default to the very first component.
2833+ //
2834+ // The locus result will be an object with the following properties:
2835+ //
2836+ // - load: boolean, true if loading component required, false otherwise
2837+ // - componentId: component to load (current componentId if load is false)
2838+ // - if load is false:
2839+ // - page
2840+ // - if load is true:
2841+ // - one of page / pagesBack / percent / direction / position / anchor
2842+ //
2843+ function pageNumberAt(pageDiv, locus) {
2844+ locus.load = false;
2845+ var currComponent = pageDiv.m.activeFrame ?
2846+ pageDiv.m.activeFrame.m.component :
2847+ null;
2848+ var component = null;
2849+ var cIndex = p.componentIds.indexOf(locus.componentId);
2850+ if (cIndex < 0 && !currComponent) {
2851+ // No specified component, no current component. Load first component.
2852+ locus.load = true;
2853+ locus.componentId = p.componentIds[0];
2854+ return locus;
2855+ } else if (
2856+ cIndex < 0 &&
2857+ locus.componentId &&
2858+ currComponent.properties.id != locus.componentId
2859+ ) {
2860+ // Invalid component, say not found.
2861+ pageDiv.m.reader.dispatchEvent(
2862+ "monocle:notfound",
2863+ { href: locus.componentId }
2864+ );
2865+ return null;
2866+ } else if (cIndex < 0) {
2867+ // No specified (or invalid) component, use current component.
2868+ component = currComponent;
2869+ locus.componentId = pageDiv.m.activeFrame.m.component.properties.id;
2870+ cIndex = p.componentIds.indexOf(locus.componentId);
2871+ } else if (!p.components[cIndex] || p.components[cIndex] != currComponent) {
2872+ // Specified component differs from current component. Load specified.
2873+ locus.load = true;
2874+ return locus;
2875+ } else {
2876+ component = currComponent;
2877+ }
2878+
2879+ // If we're here, then the locus is based on the current component.
2880+ var result = { load: false, componentId: locus.componentId, page: 1 }
2881+
2882+ // Get the current last page.
2883+ lastPageNum = component.lastPageNumber();
2884+
2885+ // Deduce the page number for the given locus.
2886+ if (typeof(locus.page) == "number") {
2887+ result.page = locus.page;
2888+ } else if (typeof(locus.pagesBack) == "number") {
2889+ result.page = lastPageNum + locus.pagesBack;
2890+ } else if (typeof(locus.percent) == "number") {
2891+ var place = new Monocle.Place();
2892+ place.setPlace(component, 1);
2893+ result.page = place.pageAtPercentageThrough(locus.percent);
2894+ } else if (typeof(locus.direction) == "number") {
2895+ if (!pageDiv.m.place) {
2896+ console.warn("Can't move in a direction if pageDiv has no place.");
2897+ }
2898+ result.page = pageDiv.m.place.pageNumber();
2899+ result.page += locus.direction;
2900+ } else if (typeof(locus.anchor) == "string") {
2901+ result.page = component.pageForChapter(locus.anchor, pageDiv);
2902+ } else if (typeof(locus.xpath) == "string") {
2903+ result.page = component.pageForXPath(locus.xpath, pageDiv);
2904+ } else if (typeof(locus.selector) == "string") {
2905+ result.page = component.pageForSelector(locus.selector, pageDiv);
2906+ } else if (typeof(locus.position) == "string") {
2907+ if (locus.position == "start") {
2908+ result.page = 1;
2909+ } else if (locus.position == "end") {
2910+ result.page = lastPageNum['new'];
2911+ }
2912+ } else {
2913+ console.warn("Unrecognised locus: " + locus);
2914+ }
2915+
2916+ if (result.page < 1) {
2917+ if (cIndex == 0) {
2918+ // On first page of book.
2919+ result.page = 1;
2920+ result.boundarystart = true;
2921+ } else {
2922+ // Moving backwards from current component.
2923+ result.load = true;
2924+ result.componentId = p.componentIds[cIndex - 1];
2925+ result.pagesBack = result.page;
2926+ result.page = null;
2927+ }
2928+ } else if (result.page > lastPageNum) {
2929+ if (cIndex == p.lastCIndex) {
2930+ // On last page of book.
2931+ result.page = lastPageNum;
2932+ result.boundaryend = true;
2933+ } else {
2934+ // Moving forwards from current component.
2935+ result.load = true;
2936+ result.componentId = p.componentIds[cIndex + 1];
2937+ result.page -= lastPageNum;
2938+ }
2939+ }
2940+
2941+ return result;
2942+ }
2943+
2944+
2945+ // Same as pageNumberAt, but if a load is not flagged, this will
2946+ // automatically update the pageDiv's place to the given pageNumber.
2947+ //
2948+ // If you call this (ie, from a flipper), you are effectively entering into
2949+ // a contract to move the frame offset to the given page returned in the
2950+ // locus if load is false.
2951+ //
2952+ function setPageAt(pageDiv, locus) {
2953+ locus = pageNumberAt(pageDiv, locus);
2954+ if (locus && !locus.load) {
2955+ var evtData = { locus: locus, page: pageDiv }
2956+ if (locus.boundarystart) {
2957+ pageDiv.m.reader.dispatchEvent('monocle:boundarystart', evtData);
2958+ } else if (locus.boundaryend) {
2959+ pageDiv.m.reader.dispatchEvent('monocle:boundaryend', evtData);
2960+ } else {
2961+ var component = p.components[p.componentIds.indexOf(locus.componentId)];
2962+ pageDiv.m.place = pageDiv.m.place || new Monocle.Place();
2963+ pageDiv.m.place.setPlace(component, locus.page);
2964+
2965+ var evtData = {
2966+ page: pageDiv,
2967+ locus: locus,
2968+ pageNumber: pageDiv.m.place.pageNumber(),
2969+ componentId: locus.componentId
2970+ }
2971+ pageDiv.m.reader.dispatchEvent("monocle:pagechange", evtData);
2972+ }
2973+ }
2974+ return locus;
2975+ }
2976+
2977+
2978+ // Will load the given component into the pageDiv's frame, then invoke the
2979+ // callback with resulting locus (provided by pageNumberAt).
2980+ //
2981+ // If the resulting page number is outside the bounds of the new component,
2982+ // (ie, pageNumberAt again requests a load), this will recurse into further
2983+ // components until non-loading locus is returned by pageNumberAt. Then the
2984+ // callback will fire with that locus.
2985+ //
2986+ // As with setPageAt, if you call this you're obliged to move the frame
2987+ // offset to the given page in the locus passed to the callback.
2988+ //
2989+ // If you pass a function as the progressCallback argument, the logic of this
2990+ // function will be in your control. The function will be invoked between:
2991+ //
2992+ // a) loading the component and
2993+ // b) applying the component to the frame and
2994+ // c) loading any further components if required
2995+ //
2996+ // with a function argument that performs the next step in the process. So
2997+ // if you need to do some special handling during the load process, you can.
2998+ //
2999+ function loadPageAt(pageDiv, locus, callback, progressCallback) {
3000+ var cIndex = p.componentIds.indexOf(locus.componentId);
3001+ if (!locus.load || cIndex < 0) {
3002+ locus = pageNumberAt(pageDiv, locus);
3003+ }
3004+
3005+ if (!locus) {
3006+ return;
3007+ }
3008+
3009+ if (!locus.load) {
3010+ callback(locus);
3011+ return;
3012+ }
3013+
3014+ var findPageNumber = function () {
3015+ locus = setPageAt(pageDiv, locus);
3016+ if (!locus) {
3017+ return;
3018+ } else if (locus.load) {
3019+ loadPageAt(pageDiv, locus, callback, progressCallback)
3020+ } else {
3021+ callback(locus);
3022+ }
3023+ }
3024+
3025+ var pgFindPageNumber = function () {
3026+ progressCallback ? progressCallback(findPageNumber) : findPageNumber();
3027+ }
3028+
3029+ var applyComponent = function (component) {
3030+ component.applyTo(pageDiv, pgFindPageNumber);
3031+ }
3032+
3033+ var pgApplyComponent = function (component) {
3034+ progressCallback ?
3035+ progressCallback(function () { applyComponent(component) }) :
3036+ applyComponent(component);
3037+ }
3038+
3039+ loadComponent(cIndex, pgApplyComponent, pageDiv);
3040+ }
3041+
3042+
3043+ // If your flipper doesn't care whether a component needs to be
3044+ // loaded before the page can be set, you can use this shortcut.
3045+ //
3046+ function setOrLoadPageAt(pageDiv, locus, callback, onProgress, onFail) {
3047+ locus = setPageAt(pageDiv, locus);
3048+ if (!locus) {
3049+ if (onFail) { onFail(); }
3050+ } else if (locus.load) {
3051+ loadPageAt(pageDiv, locus, callback, onProgress);
3052+ } else {
3053+ callback(locus);
3054+ }
3055+ }
3056+
3057+
3058+ // Fetches the component source from the dataSource.
3059+ //
3060+ // 'index' is the index of the component in the
3061+ // dataSource.getComponents array.
3062+ //
3063+ // 'callback' is invoked when the source is received.
3064+ //
3065+ // 'pageDiv' is optional, and simply allows firing events on
3066+ // the reader object that has requested this component, ONLY if
3067+ // the source has not already been received.
3068+ //
3069+ function loadComponent(index, callback, pageDiv) {
3070+ if (p.components[index]) {
3071+ return callback(p.components[index]);
3072+ }
3073+ var cmptId = p.componentIds[index];
3074+ if (pageDiv) {
3075+ var evtData = { 'page': pageDiv, 'component': cmptId, 'index': index };
3076+ pageDiv.m.reader.dispatchEvent('monocle:componentloading', evtData);
3077+ }
3078+ var failedToLoadComponent = function () {
3079+ console.warn("Failed to load component: "+cmptId);
3080+ pageDiv.m.reader.dispatchEvent('monocle:componentfailed', evtData);
3081+ try {
3082+ var currCmpt = pageDiv.m.activeFrame.m.component;
3083+ evtData.cmptId = currCmpt.properties.id;
3084+ callback(currCmpt);
3085+ } catch (e) {
3086+ console.warn("Failed to fall back to previous component.");
3087+ }
3088+ }
3089+
3090+ var fn = function (cmptSource) {
3091+ if (cmptSource === false) { return failedToLoadComponent(); }
3092+ if (pageDiv) {
3093+ evtData['source'] = cmptSource;
3094+ pageDiv.m.reader.dispatchEvent('monocle:componentloaded', evtData);
3095+ html = evtData['html'];
3096+ }
3097+ p.components[index] = new Monocle.Component(
3098+ API,
3099+ cmptId,
3100+ index,
3101+ chaptersForComponent(cmptId),
3102+ cmptSource
3103+ );
3104+ callback(p.components[index]);
3105+ }
3106+ var cmptSource = p.dataSource.getComponent(cmptId, fn);
3107+ if (cmptSource && !p.components[index]) {
3108+ fn(cmptSource);
3109+ } else if (cmptSource === false) {
3110+ return failedToLoadComponent();
3111+ }
3112+ }
3113+
3114+
3115+ // Returns an array of chapter objects that are found in the given component.
3116+ //
3117+ // A chapter object has this format:
3118+ //
3119+ // {
3120+ // title: "Chapter 1",
3121+ // fragment: null
3122+ // }
3123+ //
3124+ // The fragment property of a chapter object is either null (the chapter
3125+ // starts at the head of the component) or the fragment part of the URL
3126+ // (eg, "foo" in "index.html#foo").
3127+ //
3128+ function chaptersForComponent(cmptId) {
3129+ if (p.chapters[cmptId]) {
3130+ return p.chapters[cmptId];
3131+ }
3132+ p.chapters[cmptId] = [];
3133+ var matcher = new RegExp('^'+cmptId+"(\#(.+)|$)");
3134+ var matches;
3135+ var recurser = function (chp) {
3136+ if (matches = chp.src.match(matcher)) {
3137+ p.chapters[cmptId].push({
3138+ title: chp.title,
3139+ fragment: matches[2] || null
3140+ });
3141+ }
3142+ if (chp.children) {
3143+ for (var i = 0; i < chp.children.length; ++i) {
3144+ recurser(chp.children[i]);
3145+ }
3146+ }
3147+ }
3148+
3149+ for (var i = 0; i < p.contents.length; ++i) {
3150+ recurser(p.contents[i]);
3151+ }
3152+ return p.chapters[cmptId];
3153+ }
3154+
3155+
3156+ // Returns a locus for the chapter that has the URL given in the
3157+ // 'src' argument.
3158+ //
3159+ // See the comments at pageNumberAt for an explanation of locus objects.
3160+ //
3161+ function locusOfChapter(src) {
3162+ var matcher = new RegExp('^(.+?)(#(.*))?$');
3163+ var matches = src.match(matcher);
3164+ if (!matches) { return null; }
3165+ var cmptId = componentIdMatching(matches[1]);
3166+ if (!cmptId) { return null; }
3167+ var locus = { componentId: cmptId }
3168+ matches[3] ? locus.anchor = matches[3] : locus.position = "start";
3169+ return locus;
3170+ }
3171+
3172+
3173+ function isValidLocus(locus) {
3174+ if (!locus) { return false; }
3175+ if (locus.componentId && !componentIdMatching(locus.componentId)) {
3176+ return false;
3177+ }
3178+ return true;
3179+ }
3180+
3181+
3182+ function componentIdMatching(str) {
3183+ return p.componentIds.indexOf(str) >= 0 ? str : null;
3184+ }
3185+
3186+
3187+ API.getMetaData = dataSource.getMetaData;
3188+ API.pageNumberAt = pageNumberAt;
3189+ API.setPageAt = setPageAt;
3190+ API.loadPageAt = loadPageAt;
3191+ API.setOrLoadPageAt = setOrLoadPageAt;
3192+ API.chaptersForComponent = chaptersForComponent;
3193+ API.locusOfChapter = locusOfChapter;
3194+ API.isValidLocus = isValidLocus;
3195+
3196+ initialize();
3197+
3198+ return API;
3199+}
3200+
3201+
3202+// A shortcut for creating a book from an array of nodes.
3203+//
3204+// You could use this as follows, for eg:
3205+//
3206+// Monocle.Book.fromNodes([document.getElementById('content')]);
3207+//
3208+Monocle.Book.fromNodes = function (nodes) {
3209+ var bookData = {
3210+ getComponents: function () {
3211+ return ['anonymous'];
3212+ },
3213+ getContents: function () {
3214+ return [];
3215+ },
3216+ getComponent: function (n) {
3217+ return { 'nodes': nodes };
3218+ },
3219+ getMetaData: function (key) {
3220+ }
3221+ }
3222+
3223+ return new Monocle.Book(bookData);
3224+}
3225+
3226+Monocle.pieceLoaded('core/book');
3227+// PLACE
3228+
3229+Monocle.Place = function () {
3230+
3231+ var API = { constructor: Monocle.Place }
3232+ var k = API.constants = API.constructor;
3233+ var p = API.properties = {
3234+ component: null,
3235+ percent: null
3236+ }
3237+
3238+
3239+ function setPlace(cmpt, pageN) {
3240+ p.component = cmpt;
3241+ p.percent = pageN / cmpt.lastPageNumber();
3242+ p.chapter = null;
3243+ }
3244+
3245+
3246+ function setPercentageThrough(cmpt, percent) {
3247+ p.component = cmpt;
3248+ p.percent = percent;
3249+ p.chapter = null;
3250+ }
3251+
3252+
3253+ function componentId() {
3254+ return p.component.properties.id;
3255+ }
3256+
3257+
3258+ // How far we are through the component at the "top of the page".
3259+ //
3260+ // 0 - start of book. 1.0 - end of book.
3261+ //
3262+ function percentAtTopOfPage() {
3263+ return p.percent - 1.0 / p.component.lastPageNumber();
3264+ }
3265+
3266+
3267+ // How far we are through the component at the "bottom of the page".
3268+ //
3269+ // NB: API aliases this to percentageThrough().
3270+ //
3271+ function percentAtBottomOfPage() {
3272+ return p.percent;
3273+ }
3274+
3275+
3276+ // The page number at a given point (0: start, 1: end) within the component.
3277+ //
3278+ function pageAtPercentageThrough(percent) {
3279+ return Math.max(Math.round(p.component.lastPageNumber() * percent), 1);
3280+ }
3281+
3282+
3283+ // The page number of this point within the component.
3284+ //
3285+ function pageNumber() {
3286+ return pageAtPercentageThrough(p.percent);
3287+ }
3288+
3289+
3290+ function chapterInfo() {
3291+ if (p.chapter) {
3292+ return p.chapter;
3293+ }
3294+ return p.chapter = p.component.chapterForPage(pageNumber());
3295+ }
3296+
3297+
3298+ function chapterTitle() {
3299+ var chp = chapterInfo();
3300+ return chp ? chp.title : null;
3301+ }
3302+
3303+
3304+ function chapterSrc() {
3305+ var src = componentId();
3306+ var cinfo = chapterInfo();
3307+ if (cinfo && cinfo.fragment) {
3308+ src += "#" + cinfo.fragment;
3309+ }
3310+ return src;
3311+ }
3312+
3313+
3314+ function getLocus(options) {
3315+ options = options || {};
3316+ var locus = {
3317+ page: pageNumber(),
3318+ componentId: componentId()
3319+ }
3320+ if (options.direction) {
3321+ locus.page += options.direction;
3322+ } else {
3323+ locus.percent = percentAtBottomOfPage();
3324+ }
3325+ return locus;
3326+ }
3327+
3328+
3329+ // Returns how far this place is in the entire book (0 - start, 1.0 - end).
3330+ //
3331+ function percentageOfBook() {
3332+ componentIds = p.component.properties.book.properties.componentIds;
3333+ componentSize = 1.0 / componentIds.length;
3334+ var pc = componentIds.indexOf(componentId()) * componentSize;
3335+ pc += componentSize * p.percent;
3336+ return pc;
3337+ }
3338+
3339+
3340+ function onFirstPageOfBook() {
3341+ return p.component.properties.index == 0 && pageNumber() == 1;
3342+ }
3343+
3344+
3345+ function onLastPageOfBook() {
3346+ return (
3347+ p.component.properties.index ==
3348+ p.component.properties.book.properties.lastCIndex &&
3349+ pageNumber() == p.component.lastPageNumber()
3350+ );
3351+ }
3352+
3353+
3354+ API.setPlace = setPlace;
3355+ API.setPercentageThrough = setPercentageThrough;
3356+ API.componentId = componentId;
3357+ API.percentAtTopOfPage = percentAtTopOfPage;
3358+ API.percentAtBottomOfPage = percentAtBottomOfPage;
3359+ API.percentageThrough = percentAtBottomOfPage;
3360+ API.pageAtPercentageThrough = pageAtPercentageThrough;
3361+ API.pageNumber = pageNumber;
3362+ API.chapterInfo = chapterInfo;
3363+ API.chapterTitle = chapterTitle;
3364+ API.chapterSrc = chapterSrc;
3365+ API.getLocus = getLocus;
3366+ API.percentageOfBook = percentageOfBook;
3367+ API.onFirstPageOfBook = onFirstPageOfBook;
3368+ API.onLastPageOfBook = onLastPageOfBook;
3369+
3370+ return API;
3371+}
3372+
3373+
3374+Monocle.Place.FromPageNumber = function (component, pageNumber) {
3375+ var place = new Monocle.Place();
3376+ place.setPlace(component, pageNumber);
3377+ return place;
3378+}
3379+
3380+
3381+Monocle.Place.FromPercentageThrough = function (component, percent) {
3382+ var place = new Monocle.Place();
3383+ place.setPercentageThrough(component, percent);
3384+ return place;
3385+}
3386+
3387+
3388+// We can't create a place from a percentage of the book, because the
3389+// component may not have been loaded yet. But we can get a locus.
3390+//
3391+Monocle.Place.percentOfBookToLocus = function (reader, percent) {
3392+ var componentIds = reader.getBook().properties.componentIds;
3393+ var componentSize = 1.0 / componentIds.length;
3394+ return {
3395+ componentId: componentIds[Math.floor(percent / componentSize)],
3396+ percent: (percent % componentSize) / componentSize
3397+ }
3398+}
3399+
3400+Monocle.pieceLoaded('core/place');
3401+/* COMPONENT */
3402+
3403+// See the properties declaration for details of constructor arguments.
3404+//
3405+Monocle.Component = function (book, id, index, chapters, source) {
3406+
3407+ var API = { constructor: Monocle.Component }
3408+ var k = API.constants = API.constructor;
3409+ var p = API.properties = {
3410+ // a back-reference to the public API of the book that owns this component
3411+ book: book,
3412+
3413+ // the string that represents this component in the book's component array
3414+ id: id,
3415+
3416+ // the position in the book's components array of this component
3417+ index: index,
3418+
3419+ // The chapters argument is an array of objects that list the chapters that
3420+ // can be found in this component. A chapter object is defined as:
3421+ //
3422+ // {
3423+ // title: str,
3424+ // fragment: str, // optional anchor id
3425+ // percent: n // how far into the component the chapter begins
3426+ // }
3427+ //
3428+ // NOTE: the percent property is calculated by the component - you only need
3429+ // to pass in the title and the optional id string.
3430+ //
3431+ chapters: chapters,
3432+
3433+ // the frame provided by dataSource.getComponent() for this component
3434+ source: source
3435+ }
3436+
3437+
3438+ // Makes this component the active component for the pageDiv. There are
3439+ // several strategies for this (see loadFrame).
3440+ //
3441+ // When the component has been loaded into the pageDiv's frame, the callback
3442+ // will be invoked with the pageDiv and this component as arguments.
3443+ //
3444+ function applyTo(pageDiv, callback) {
3445+ var evtData = { 'page': pageDiv, 'source': p.source };
3446+ pageDiv.m.reader.dispatchEvent('monocle:componentchanging', evtData);
3447+
3448+ var onLoaded = function () {
3449+ setupFrame(
3450+ pageDiv,
3451+ pageDiv.m.activeFrame,
3452+ function () { callback(pageDiv, API) }
3453+ );
3454+ }
3455+
3456+ Monocle.defer(function () { loadFrame(pageDiv, onLoaded); });
3457+ }
3458+
3459+
3460+ // Loads this component into the given frame, using one of the following
3461+ // strategies:
3462+ //
3463+ // * HTML - a HTML string
3464+ // * URL - a URL string
3465+ // * Nodes - an array of DOM body nodes (NB: no way to populate head)
3466+ // * Document - a DOM DocumentElement object
3467+ //
3468+ function loadFrame(pageDiv, callback) {
3469+ var frame = pageDiv.m.activeFrame;
3470+
3471+ // We own this frame now.
3472+ frame.m.component = API;
3473+
3474+ // Hide the frame while we're changing it.
3475+ frame.style.visibility = "hidden";
3476+
3477+ // Prevent about:blank overriding imported nodes in Firefox.
3478+ // Disabled again because it seems to result in blank pages in Saf.
3479+ //frame.contentWindow.stop();
3480+
3481+ if (p.source.html || (typeof p.source == "string")) { // HTML
3482+ return loadFrameFromHTML(p.source.html || p.source, frame, callback);
3483+ } else if (p.source.javascript) { // JAVASCRIPT
3484+ //console.log("Loading as javascript: "+p.source.javascript);
3485+ return loadFrameFromJavaScript(p.source.javascript, frame, callback);
3486+ } else if (p.source.url) { // URL
3487+ return loadFrameFromURL(p.source.url, frame, callback);
3488+ } else if (p.source.nodes) { // NODES
3489+ return loadFrameFromNodes(p.source.nodes, frame, callback);
3490+ } else if (p.source.doc) { // DOCUMENT
3491+ return loadFrameFromDocument(p.source.doc, frame, callback);
3492+ }
3493+ }
3494+
3495+
3496+ // LOAD STRATEGY: HTML
3497+ // Loads a HTML string into the given frame, invokes the callback once loaded.
3498+ //
3499+ // Cleans the string so it can be used in a JavaScript statement. This is
3500+ // slow, so if the string is already clean, skip this and use
3501+ // loadFrameFromJavaScript directly.
3502+ //
3503+ function loadFrameFromHTML(src, frame, callback) {
3504+ // Compress whitespace.
3505+ src = src.replace(/\n/g, '\\n').replace(/\r/, '\\r');
3506+
3507+ // Escape single-quotes.
3508+ src = src.replace(/\'/g, '\\\'');
3509+
3510+ // Remove scripts. (DISABLED -- Monocle should leave this to implementers.)
3511+ //var scriptFragment = "<script[^>]*>([\\S\\s]*?)<\/script>";
3512+ //src = src.replace(new RegExp(scriptFragment, 'img'), '');
3513+
3514+ // BROWSERHACK: Gecko chokes on the DOCTYPE declaration.
3515+ if (Monocle.Browser.is.Gecko) {
3516+ var doctypeFragment = "<!DOCTYPE[^>]*>";
3517+ src = src.replace(new RegExp(doctypeFragment, 'm'), '');
3518+ }
3519+
3520+ loadFrameFromJavaScript(src, frame, callback);
3521+ }
3522+
3523+
3524+ // LOAD STRATEGY: JAVASCRIPT
3525+ // Like the HTML strategy, but assumes that the src string is already clean.
3526+ //
3527+ function loadFrameFromJavaScript(src, frame, callback) {
3528+ src = "javascript:'"+src+"';";
3529+ frame.onload = function () {
3530+ frame.onload = null;
3531+ Monocle.defer(callback);
3532+ }
3533+ frame.src = src;
3534+ }
3535+
3536+
3537+ // LOAD STRATEGY: URL
3538+ // Loads the URL into the given frame, invokes callback once loaded.
3539+ //
3540+ function loadFrameFromURL(url, frame, callback) {
3541+ // If it's a relative path, we need to make it absolute, using the
3542+ // reader's location (not the active component's location).
3543+ if (!url.match(/^\//)) {
3544+ var link = document.createElement('a');
3545+ link.setAttribute('href', url);
3546+ url = link.href;
3547+ delete(link);
3548+ }
3549+ frame.onload = function () {
3550+ frame.onload = null;
3551+ Monocle.defer(callback);
3552+ }
3553+ frame.contentWindow.location.replace(url);
3554+ }
3555+
3556+
3557+ // LOAD STRATEGY: NODES
3558+ // Loads the array of DOM nodes into the body of the frame (replacing all
3559+ // existing nodes), then invokes the callback.
3560+ //
3561+ function loadFrameFromNodes(nodes, frame, callback) {
3562+ var destDoc = frame.contentDocument;
3563+ destDoc.documentElement.innerHTML = "";
3564+ var destHd = destDoc.createElement("head");
3565+ var destBdy = destDoc.createElement("body");
3566+
3567+ for (var i = 0; i < nodes.length; ++i) {
3568+ var node = destDoc.importNode(nodes[i], true);
3569+ destBdy.appendChild(node);
3570+ }
3571+
3572+ var oldHead = destDoc.getElementsByTagName('head')[0];
3573+ if (oldHead) {
3574+ destDoc.documentElement.replaceChild(destHd, oldHead);
3575+ } else {
3576+ destDoc.documentElement.appendChild(destHd);
3577+ }
3578+ if (destDoc.body) {
3579+ destDoc.documentElement.replaceChild(destBdy, destDoc.body);
3580+ } else {
3581+ destDoc.documentElement.appendChild(destBdy);
3582+ }
3583+
3584+ if (callback) { callback(); }
3585+ }
3586+
3587+
3588+ // LOAD STRATEGY: DOCUMENT
3589+ // Replaces the DocumentElement of the given frame with the given srcDoc.
3590+ // Invokes the callback when loaded.
3591+ //
3592+ function loadFrameFromDocument(srcDoc, frame, callback) {
3593+ var destDoc = frame.contentDocument;
3594+
3595+ var srcBases = srcDoc.getElementsByTagName('base');
3596+ if (srcBases[0]) {
3597+ var head = destDoc.getElementsByTagName('head')[0];
3598+ if (!head) {
3599+ try {
3600+ head = destDoc.createElement('head');
3601+ if (destDoc.body) {
3602+ destDoc.insertBefore(head, destDoc.body);
3603+ } else {
3604+ destDoc.appendChild(head);
3605+ }
3606+ } catch (e) {
3607+ head = destDoc.body;
3608+ }
3609+ }
3610+ var bases = destDoc.getElementsByTagName('base');
3611+ var base = bases[0] ? bases[0] : destDoc.createElement('base');
3612+ base.setAttribute('href', srcBases[0].getAttribute('href'));
3613+ head.appendChild(base);
3614+ }
3615+
3616+ destDoc.replaceChild(
3617+ destDoc.importNode(srcDoc.documentElement, true),
3618+ destDoc.documentElement
3619+ );
3620+
3621+ // DISABLED: immediate readiness - webkit has some difficulty with this.
3622+ // if (callback) { callback(); }
3623+
3624+ Monocle.defer(callback);
3625+ }
3626+
3627+
3628+ // Once a frame is loaded with this component, call this method to style
3629+ // and measure its contents.
3630+ //
3631+ function setupFrame(pageDiv, frame, callback) {
3632+ // iOS touch events on iframes are busted. See comments in
3633+ // events.js for an explanation of this hack.
3634+ //
3635+ Monocle.Events.listenOnIframe(frame);
3636+
3637+ var doc = frame.contentDocument;
3638+ var evtData = { 'page': pageDiv, 'document': doc, 'component': API };
3639+
3640+ // Announce that the component is loaded.
3641+ pageDiv.m.reader.dispatchEvent('monocle:componentmodify', evtData);
3642+
3643+ updateDimensions(pageDiv, function () {
3644+ frame.style.visibility = "visible";
3645+
3646+ // Find the place of any chapters in the component.
3647+ locateChapters(pageDiv);
3648+
3649+ // Announce that the component has changed.
3650+ pageDiv.m.reader.dispatchEvent('monocle:componentchange', evtData);
3651+
3652+ callback();
3653+ });
3654+ }
3655+
3656+
3657+ // Checks whether the pageDiv dimensions have changed. If they have,
3658+ // remeasures dimensions and returns true. Otherwise returns false.
3659+ //
3660+ function updateDimensions(pageDiv, callback) {
3661+ pageDiv.m.dimensions.update(function (pageLength) {
3662+ p.pageLength = pageLength;
3663+ if (typeof callback == "function") { callback() };
3664+ });
3665+ }
3666+
3667+
3668+ // Iterates over all the chapters that are within this component
3669+ // (according to the array we were provided on initialization) and finds
3670+ // their location (in percentage terms) within the text.
3671+ //
3672+ // Stores this percentage with the chapter object in the chapters array.
3673+ //
3674+ function locateChapters(pageDiv) {
3675+ if (p.chapters[0] && typeof p.chapters[0].percent == "number") {
3676+ return;
3677+ }
3678+ var doc = pageDiv.m.activeFrame.contentDocument;
3679+ for (var i = 0; i < p.chapters.length; ++i) {
3680+ var chp = p.chapters[i];
3681+ chp.percent = 0;
3682+ if (chp.fragment) {
3683+ var node = doc.getElementById(chp.fragment);
3684+ chp.percent = pageDiv.m.dimensions.percentageThroughOfNode(node);
3685+ }
3686+ }
3687+ return p.chapters;
3688+ }
3689+
3690+
3691+ // For a given page number within the component, return the chapter that
3692+ // starts on or most-recently-before this page.
3693+ //
3694+ // Useful, for example, in displaying the current chapter title as a
3695+ // running head on the page.
3696+ //
3697+ function chapterForPage(pageN) {
3698+ var cand = null;
3699+ var percent = (pageN - 1) / p.pageLength;
3700+ for (var i = 0; i < p.chapters.length; ++i) {
3701+ if (percent >= p.chapters[i].percent) {
3702+ cand = p.chapters[i];
3703+ } else {
3704+ return cand;
3705+ }
3706+ }
3707+ return cand;
3708+ }
3709+
3710+
3711+ // For a given chapter fragment (the bit after the hash
3712+ // in eg, "index.html#foo"), return the page number on which
3713+ // the chapter starts. If the fragment is null or blank, will
3714+ // return the first page of the component.
3715+ //
3716+ function pageForChapter(fragment, pageDiv) {
3717+ if (!fragment) {
3718+ return 1;
3719+ }
3720+ for (var i = 0; i < p.chapters.length; ++i) {
3721+ if (p.chapters[i].fragment == fragment) {
3722+ return percentToPageNumber(p.chapters[i].percent);
3723+ }
3724+ }
3725+ var doc = pageDiv.m.activeFrame.contentDocument;
3726+ var node = doc.getElementById(fragment);
3727+ var percent = pageDiv.m.dimensions.percentageThroughOfNode(node);
3728+ return percentToPageNumber(percent);
3729+ }
3730+
3731+
3732+ function pageForXPath(xpath, pageDiv) {
3733+ var doc = pageDiv.m.activeFrame.contentDocument;
3734+ var percent = 0;
3735+ if (Monocle.Browser.env.supportsXPath) {
3736+ var node = doc.evaluate(xpath, doc, null, 9, null).singleNodeValue;
3737+ if (node) {
3738+ percent = pageDiv.m.dimensions.percentageThroughOfNode(node);
3739+ }
3740+ } else {
3741+ console.warn("XPath not supported in this client.");
3742+ }
3743+ return percentToPageNumber(percent);
3744+ }
3745+
3746+
3747+ function pageForSelector(selector, pageDiv) {
3748+ var doc = pageDiv.m.activeFrame.contentDocument;
3749+ var percent = 0;
3750+ if (Monocle.Browser.env.supportsQuerySelector) {
3751+ var node = doc.querySelector(selector);
3752+ if (node) {
3753+ percent = pageDiv.m.dimensions.percentageThroughOfNode(node);
3754+ }
3755+ } else {
3756+ console.warn("querySelector not supported in this client.");
3757+ }
3758+ return percentToPageNumber(percent);
3759+ }
3760+
3761+
3762+ function percentToPageNumber(pc) {
3763+ return Math.floor(pc * p.pageLength) + 1;
3764+ }
3765+
3766+
3767+ // A public getter for p.pageLength.
3768+ //
3769+ function lastPageNumber() {
3770+ return p.pageLength;
3771+ }
3772+
3773+
3774+ API.applyTo = applyTo;
3775+ API.updateDimensions = updateDimensions;
3776+ API.chapterForPage = chapterForPage;
3777+ API.pageForChapter = pageForChapter;
3778+ API.pageForXPath = pageForXPath;
3779+ API.pageForSelector = pageForSelector;
3780+ API.lastPageNumber = lastPageNumber;
3781+
3782+ return API;
3783+}
3784+
3785+Monocle.pieceLoaded('core/component');
3786+// A panel is an invisible column of interactivity. When contact occurs
3787+// (mousedown, touchstart), the panel expands to the full width of its
3788+// container, to catch all interaction events and prevent them from hitting
3789+// other things.
3790+//
3791+// Panels are used primarily to provide hit zones for page flipping
3792+// interactions, but you can do whatever you like with them.
3793+//
3794+// After instantiating a panel and adding it to the reader as a control,
3795+// you can call listenTo() with a hash of methods for any of 'start', 'move'
3796+// 'end' and 'cancel'.
3797+//
3798+Monocle.Controls.Panel = function () {
3799+
3800+ var API = { constructor: Monocle.Controls.Panel }
3801+ var k = API.constants = API.constructor;
3802+ var p = API.properties = {
3803+ evtCallbacks: {}
3804+ }
3805+
3806+ function createControlElements(cntr) {
3807+ p.div = cntr.dom.make('div', k.CLS.panel);
3808+ p.div.dom.setStyles(k.DEFAULT_STYLES);
3809+ Monocle.Events.listenForContact(
3810+ p.div,
3811+ {
3812+ 'start': start,
3813+ 'move': move,
3814+ 'end': end,
3815+ 'cancel': cancel
3816+ },
3817+ { useCapture: false }
3818+ );
3819+ return p.div;
3820+ }
3821+
3822+
3823+ function listenTo(evtCallbacks) {
3824+ p.evtCallbacks = evtCallbacks;
3825+ }
3826+
3827+
3828+ function deafen() {
3829+ p.evtCallbacks = {}
3830+ }
3831+
3832+
3833+ function start(evt) {
3834+ p.contact = true;
3835+ evt.m.offsetX += p.div.offsetLeft;
3836+ evt.m.offsetY += p.div.offsetTop;
3837+ expand();
3838+ invoke('start', evt);
3839+ }
3840+
3841+
3842+ function move(evt) {
3843+ if (!p.contact) {
3844+ return;
3845+ }
3846+ invoke('move', evt);
3847+ }
3848+
3849+
3850+ function end(evt) {
3851+ if (!p.contact) {
3852+ return;
3853+ }
3854+ Monocle.Events.deafenForContact(p.div, p.listeners);
3855+ contract();
3856+ p.contact = false;
3857+ invoke('end', evt);
3858+ }
3859+
3860+
3861+ function cancel(evt) {
3862+ if (!p.contact) {
3863+ return;
3864+ }
3865+ Monocle.Events.deafenForContact(p.div, p.listeners);
3866+ contract();
3867+ p.contact = false;
3868+ invoke('cancel', evt);
3869+ }
3870+
3871+
3872+ function invoke(evtType, evt) {
3873+ if (p.evtCallbacks[evtType]) {
3874+ p.evtCallbacks[evtType](API, evt.m.offsetX, evt.m.offsetY);
3875+ }
3876+ evt.preventDefault();
3877+ }
3878+
3879+
3880+ function expand() {
3881+ if (p.expanded) {
3882+ return;
3883+ }
3884+ p.div.dom.addClass(k.CLS.expanded);
3885+ p.expanded = true;
3886+ }
3887+
3888+
3889+ function contract(evt) {
3890+ if (!p.expanded) {
3891+ return;
3892+ }
3893+ p.div.dom.removeClass(k.CLS.expanded);
3894+ p.expanded = false;
3895+ }
3896+
3897+
3898+ API.createControlElements = createControlElements;
3899+ API.listenTo = listenTo;
3900+ API.deafen = deafen;
3901+ API.expand = expand;
3902+ API.contract = contract;
3903+
3904+ return API;
3905+}
3906+
3907+
3908+Monocle.Controls.Panel.CLS = {
3909+ panel: 'panel',
3910+ expanded: 'controls_panel_expanded'
3911+}
3912+Monocle.Controls.Panel.DEFAULT_STYLES = {
3913+ position: 'absolute',
3914+ height: '100%'
3915+}
3916+
3917+
3918+Monocle.pieceLoaded('controls/panel');
3919+// The simplest page-flipping interaction system: contact to the left half of
3920+// the reader turns back one page, contact to the right half turns forward
3921+// one page.
3922+//
3923+Monocle.Panels.TwoPane = function (flipper, evtCallbacks) {
3924+
3925+ var API = { constructor: Monocle.Panels.TwoPane }
3926+ var k = API.constants = API.constructor;
3927+ var p = API.properties = {}
3928+
3929+
3930+ function initialize() {
3931+ p.panels = {
3932+ forwards: new Monocle.Controls.Panel(),
3933+ backwards: new Monocle.Controls.Panel()
3934+ }
3935+
3936+ for (dir in p.panels) {
3937+ flipper.properties.reader.addControl(p.panels[dir]);
3938+ p.panels[dir].listenTo(evtCallbacks);
3939+ p.panels[dir].properties.direction = flipper.constants[dir.toUpperCase()];
3940+ var style = { "width": k.WIDTH };
3941+ style[(dir == "forwards" ? "right" : "left")] = 0;
3942+ p.panels[dir].properties.div.dom.setStyles(style);
3943+ }
3944+ }
3945+
3946+
3947+ initialize();
3948+
3949+ return API;
3950+}
3951+
3952+Monocle.Panels.TwoPane.WIDTH = "50%";
3953+
3954+Monocle.pieceLoaded('panels/twopane');
3955+// A three-pane system of page interaction. The left 33% turns backwards, the
3956+// right 33% turns forwards, and contact on the middle third causes the
3957+// system to go into "interactive mode". In this mode, the page-flipping panels
3958+// are only active in the margins, and all of the actual text content of the
3959+// book is selectable. The user can exit "interactive mode" by hitting the little
3960+// IMode icon in the lower right corner of the reader.
3961+//
3962+Monocle.Panels.IMode = function (flipper, evtCallbacks) {
3963+
3964+ var API = { constructor: Monocle.Panels.IMode }
3965+ var k = API.constants = API.constructor;
3966+ var p = API.properties = {}
3967+
3968+
3969+ function initialize() {
3970+ p.flipper = flipper;
3971+ p.reader = flipper.properties.reader;
3972+ p.panels = {
3973+ forwards: new Monocle.Controls.Panel(),
3974+ backwards: new Monocle.Controls.Panel()
3975+ }
3976+ p.divs = {}
3977+
3978+ for (dir in p.panels) {
3979+ p.reader.addControl(p.panels[dir]);
3980+ p.divs[dir] = p.panels[dir].properties.div;
3981+ p.panels[dir].listenTo(evtCallbacks);
3982+ p.panels[dir].properties.direction = flipper.constants[dir.toUpperCase()];
3983+ p.divs[dir].style.width = "33%";
3984+ p.divs[dir].style[dir == "forwards" ? "right" : "left"] = 0;
3985+ }
3986+
3987+ p.panels.central = new Monocle.Controls.Panel();
3988+ p.reader.addControl(p.panels.central);
3989+ p.divs.central = p.panels.central.properties.div;
3990+ p.divs.central.dom.setStyles({ left: "33%", width: "34%" });
3991+ menuCallbacks({ end: modeOn });
3992+
3993+ for (dir in p.panels) {
3994+ p.divs[dir].dom.addClass('panels_imode_panel');
3995+ p.divs[dir].dom.addClass('panels_imode_'+dir+'Panel');
3996+ }
3997+
3998+ p.toggleIcon = {
3999+ createControlElements: function (cntr) {
4000+ var div = cntr.dom.make('div', 'panels_imode_toggleIcon');
4001+ Monocle.Events.listenForTap(div, modeOff);
4002+ return div;
4003+ }
4004+ }
4005+ p.reader.addControl(p.toggleIcon, null, { hidden: true });
4006+ }
4007+
4008+
4009+ function menuCallbacks(callbacks) {
4010+ p.menuCallbacks = callbacks;
4011+ p.panels.central.listenTo(p.menuCallbacks);
4012+ }
4013+
4014+
4015+ function toggle() {
4016+ p.interactive ? modeOff() : modeOn();
4017+ }
4018+
4019+
4020+ function modeOn() {
4021+ if (p.interactive) {
4022+ return;
4023+ }
4024+
4025+ p.panels.central.contract();
4026+
4027+ var page = p.reader.visiblePages()[0];
4028+ var sheaf = page.m.sheafDiv;
4029+ var bw = sheaf.offsetLeft;
4030+ var fw = page.offsetWidth - (sheaf.offsetLeft + sheaf.offsetWidth);
4031+ bw = Math.floor(((bw - 2) / page.offsetWidth) * 10000 / 100 ) + "%";
4032+ fw = Math.floor(((fw - 2) / page.offsetWidth) * 10000 / 100 ) + "%";
4033+
4034+ startCameo(function () {
4035+ p.divs.forwards.style.width = fw;
4036+ p.divs.backwards.style.width = bw;
4037+ Monocle.Styles.affix(p.divs.central, 'transform', 'translateY(-100%)');
4038+ });
4039+
4040+ p.reader.showControl(p.toggleIcon);
4041+
4042+ p.interactive = true;
4043+ if (flipper.interactiveMode) {
4044+ flipper.interactiveMode(true);
4045+ }
4046+ }
4047+
4048+
4049+ function modeOff() {
4050+ if (!p.interactive) {
4051+ return;
4052+ }
4053+
4054+ p.panels.central.contract();
4055+
4056+ deselect();
4057+
4058+ startCameo(function () {
4059+ p.divs.forwards.style.width = "33%";
4060+ p.divs.backwards.style.width = "33%";
4061+ Monocle.Styles.affix(p.divs.central, 'transform', 'translateY(0)');
4062+ });
4063+
4064+ p.reader.hideControl(p.toggleIcon);
4065+
4066+ p.interactive = false;
4067+ if (flipper.interactiveMode) {
4068+ flipper.interactiveMode(false);
4069+ }
4070+ }
4071+
4072+
4073+ function startCameo(fn) {
4074+ // Set transitions on the panels.
4075+ var trn = Monocle.Panels.IMode.CAMEO_DURATION+"ms ease-in";
4076+ Monocle.Styles.affix(p.divs.forwards, 'transition', "width "+trn);
4077+ Monocle.Styles.affix(p.divs.backwards, 'transition', "width "+trn);
4078+ Monocle.Styles.affix(p.divs.central, 'transition', "-webkit-transform "+trn);
4079+
4080+ // Temporarily disable listeners.
4081+ for (var pan in p.panels) {
4082+ p.panels[pan].deafen();
4083+ }
4084+
4085+ // Set the panels to opaque.
4086+ for (var div in p.divs) {
4087+ p.divs[div].style.opacity = 1;
4088+ }
4089+
4090+ if (typeof WebkitTransitionEvent != "undefined") {
4091+ p.cameoListener = Monocle.Events.listen(
4092+ p.divs.central,
4093+ 'webkitTransitionEnd',
4094+ endCameo
4095+ );
4096+ } else {
4097+ setTimeout(endCameo, k.CAMEO_DURATION);
4098+ }
4099+ fn();
4100+ }
4101+
4102+
4103+ function endCameo() {
4104+ setTimeout(function () {
4105+ // Remove panel transitions.
4106+ var trn = "opacity linear " + Monocle.Panels.IMode.LINGER_DURATION + "ms";
4107+ Monocle.Styles.affix(p.divs.forwards, 'transition', trn);
4108+ Monocle.Styles.affix(p.divs.backwards, 'transition', trn);
4109+ Monocle.Styles.affix(p.divs.central, 'transition', trn);
4110+
4111+ // Set the panels to transparent.
4112+ for (var div in p.divs) {
4113+ p.divs[div].style.opacity = 0;
4114+ }
4115+
4116+ // Re-enable listeners.
4117+ p.panels.forwards.listenTo(evtCallbacks);
4118+ p.panels.backwards.listenTo(evtCallbacks);
4119+ p.panels.central.listenTo(p.menuCallbacks);
4120+ }, Monocle.Panels.IMode.LINGER_DURATION);
4121+
4122+
4123+ if (p.cameoListener) {
4124+ Monocle.Events.deafen(p.divs.central, 'webkitTransitionEnd', endCameo);
4125+ }
4126+ }
4127+
4128+
4129+ function deselect() {
4130+ for (var i = 0, cmpt; cmpt = p.reader.dom.find('component', i); ++i) {
4131+ var sel = cmpt.contentWindow.getSelection() || cmpt.contentDocument.selection;
4132+ //if (sel.collapse) { sel.collapse(true); }
4133+ if (sel.removeAllRanges) { sel.removeAllRanges(); }
4134+ if (sel.empty) { sel.empty(); }
4135+ cmpt.contentDocument.body.scrollLeft = 0;
4136+ cmpt.contentDocument.body.scrollTop = 0;
4137+ }
4138+ }
4139+
4140+
4141+ API.toggle = toggle;
4142+ API.modeOn = modeOn;
4143+ API.modeOff = modeOff;
4144+ API.menuCallbacks = menuCallbacks;
4145+
4146+ initialize();
4147+
4148+ return API;
4149+}
4150+
4151+Monocle.Panels.IMode.CAMEO_DURATION = 250;
4152+Monocle.Panels.IMode.LINGER_DURATION = 250;
4153+
4154+Monocle.pieceLoaded('panels/imode');
4155+Monocle.Panels.eInk = function (flipper, evtCallbacks) {
4156+
4157+ var API = { constructor: Monocle.Panels.eInk }
4158+ var k = API.constants = API.constructor;
4159+ var p = API.properties = {
4160+ flipper: flipper
4161+ }
4162+
4163+
4164+ function initialize() {
4165+ p.panel = new Monocle.Controls.Panel();
4166+ p.reader = p.flipper.properties.reader;
4167+ p.reader.addControl(p.panel);
4168+
4169+ p.panel.listenTo({ end: function (panel, x) {
4170+ if (x < p.panel.properties.div.offsetWidth / 2) {
4171+ p.panel.properties.direction = flipper.constants.BACKWARDS;
4172+ } else {
4173+ p.panel.properties.direction = flipper.constants.FORWARDS;
4174+ }
4175+ evtCallbacks.end(panel, x);
4176+ } });
4177+
4178+ var s = p.panel.properties.div.style;
4179+ p.reader.listen("monocle:componentchanging", function () {
4180+ s.opacity = 1;
4181+ Monocle.defer(function () { s.opacity = 0 }, 40);
4182+ });
4183+ s.width = "100%";
4184+ s.background = "#000";
4185+ s.opacity = 0;
4186+
4187+ if (k.LISTEN_FOR_KEYS) {
4188+ Monocle.Events.listen(window.top.document, 'keyup', handleKeyEvent);
4189+ }
4190+ }
4191+
4192+
4193+ function handleKeyEvent(evt) {
4194+ var eventCharCode = evt.charCode || evt.keyCode;
4195+ var dir = null;
4196+ if (eventCharCode == k.KEYS["PAGEUP"]) {
4197+ dir = flipper.constants.BACKWARDS;
4198+ } else if (eventCharCode == k.KEYS["PAGEDOWN"]) {
4199+ dir = flipper.constants.FORWARDS;
4200+ }
4201+ if (dir) {
4202+ flipper.moveTo({ direction: dir });
4203+ evt.preventDefault();
4204+ }
4205+ }
4206+
4207+
4208+ initialize();
4209+
4210+ return API;
4211+}
4212+
4213+
4214+Monocle.Panels.eInk.LISTEN_FOR_KEYS = true;
4215+Monocle.Panels.eInk.KEYS = { "PAGEUP": 33, "PAGEDOWN": 34 };
4216+// Provides page-flipping panels only in the margins of the book. This is not
4217+// entirely suited to small screens with razor-thin margins, but is an
4218+// appropriate panel class for larger screens (like, say, an iPad).
4219+//
4220+// Since the flipper hit zones are only in the margins, the actual text content
4221+// of the book is always selectable.
4222+//
4223+Monocle.Panels.Marginal = function (flipper, evtCallbacks) {
4224+
4225+ var API = { constructor: Monocle.Panels.Marginal }
4226+ var k = API.constants = API.constructor;
4227+ var p = API.properties = {}
4228+
4229+
4230+ function initialize() {
4231+ p.panels = {
4232+ forwards: new Monocle.Controls.Panel(),
4233+ backwards: new Monocle.Controls.Panel()
4234+ }
4235+
4236+ for (dir in p.panels) {
4237+ flipper.properties.reader.addControl(p.panels[dir]);
4238+ p.panels[dir].listenTo(evtCallbacks);
4239+ p.panels[dir].properties.direction = flipper.constants[dir.toUpperCase()];
4240+ with (p.panels[dir].properties.div.style) {
4241+ dir == "forwards" ? right = 0 : left = 0;
4242+ }
4243+ }
4244+ setWidths();
4245+
4246+ if (flipper.interactiveMode) {
4247+ flipper.interactiveMode(true);
4248+ }
4249+ }
4250+
4251+
4252+ function setWidths() {
4253+ var page = flipper.properties.reader.dom.find('page');
4254+ var sheaf = page.m.sheafDiv;
4255+ var bw = sheaf.offsetLeft;
4256+ var fw = page.offsetWidth - (sheaf.offsetLeft + sheaf.offsetWidth);
4257+ bw = Math.floor(((bw - 2) / page.offsetWidth) * 10000 / 100 ) + "%";
4258+ fw = Math.floor(((fw - 2) / page.offsetWidth) * 10000 / 100 ) + "%";
4259+ p.panels.forwards.properties.div.style.width = fw;
4260+ p.panels.backwards.properties.div.style.width = bw;
4261+ }
4262+
4263+
4264+ API.setWidths = setWidths;
4265+
4266+ initialize();
4267+
4268+ return API;
4269+}
4270+
4271+Monocle.pieceLoaded('panels/marginal');
4272+Monocle.Dimensions.Vert = function (pageDiv) {
4273+
4274+ var API = { constructor: Monocle.Dimensions.Vert }
4275+ var k = API.constants = API.constructor;
4276+ var p = API.properties = {
4277+ page: pageDiv,
4278+ reader: pageDiv.m.reader
4279+ }
4280+
4281+
4282+ function initialize() {
4283+ p.reader.listen('monocle:componentchange', componentChanged);
4284+ }
4285+
4286+
4287+ function update(callback) {
4288+ p.bodyHeight = getBodyHeight();
4289+ p.pageHeight = getPageHeight();
4290+ p.length = Math.ceil(p.bodyHeight / p.pageHeight);
4291+ callback(p.length);
4292+ }
4293+
4294+
4295+ function getBodyHeight() {
4296+ return p.page.m.activeFrame.contentDocument.body.scrollHeight;
4297+ }
4298+
4299+
4300+ function getPageHeight() {
4301+ return p.page.m.activeFrame.offsetHeight - k.GUTTER;
4302+ }
4303+
4304+
4305+ function percentageThroughOfNode(target) {
4306+ if (!target) {
4307+ return 0;
4308+ }
4309+ var doc = p.page.m.activeFrame.contentDocument;
4310+ var offset = 0;
4311+ if (target.getBoundingClientRect) {
4312+ offset = target.getBoundingClientRect().top;
4313+ offset -= doc.body.getBoundingClientRect().top;
4314+ } else {
4315+ var oldScrollTop = doc.body.scrollTop;
4316+ target.scrollIntoView();
4317+ offset = doc.body.scrollTop;
4318+ doc.body.scrollLeft = 0;
4319+ doc.body.scrollTop = oldScrollTop;
4320+ }
4321+
4322+ //console.log(id + ": " + offset + " of " + p.bodyHeight);
4323+ var percent = offset / p.bodyHeight;
4324+ return percent;
4325+ }
4326+
4327+
4328+ function componentChanged(evt) {
4329+ if (evt.m['page'] != p.page) { return; }
4330+ var sheaf = p.page.m.sheafDiv;
4331+ var cmpt = p.page.m.activeFrame;
4332+ sheaf.dom.setStyles(k.SHEAF_STYLES);
4333+ cmpt.dom.setStyles(k.COMPONENT_STYLES);
4334+ var doc = evt.m['document'];
4335+ doc.documentElement.style.overflow = 'hidden';
4336+ doc.body.style.marginRight = '10px !important';
4337+ cmpt.contentWindow.scrollTo(0,0);
4338+ }
4339+
4340+
4341+ function locusToOffset(locus) {
4342+ return p.pageHeight * (locus.page - 1);
4343+ }
4344+
4345+
4346+ API.update = update;
4347+ API.percentageThroughOfNode = percentageThroughOfNode;
4348+ API.locusToOffset = locusToOffset;
4349+
4350+ initialize();
4351+
4352+ return API;
4353+}
4354+
4355+Monocle.Dimensions.Vert.GUTTER = 10;
4356+
4357+Monocle.pieceLoaded("dimensions/vert");
4358+Monocle.Flippers.Legacy = function (reader) {
4359+
4360+ var API = { constructor: Monocle.Flippers.Legacy }
4361+ var k = API.constants = API.constructor;
4362+ var p = API.properties = {
4363+ pageCount: 1,
4364+ divs: {}
4365+ }
4366+
4367+
4368+ function initialize() {
4369+ p.reader = reader;
4370+ }
4371+
4372+
4373+ function addPage(pageDiv) {
4374+ pageDiv.m.dimensions = new Monocle.Dimensions.Vert(pageDiv);
4375+ }
4376+
4377+
4378+ function getPlace() {
4379+ return page().m.place;
4380+ }
4381+
4382+
4383+ function moveTo(locus, callback) {
4384+ var fn = frameToLocus;
4385+ if (typeof callback == "function") {
4386+ fn = function (locus) { frameToLocus(locus); callback(locus); }
4387+ }
4388+ p.reader.getBook().setOrLoadPageAt(page(), locus, fn);
4389+ }
4390+
4391+
4392+ function listenForInteraction(panelClass) {
4393+ if (typeof panelClass != "function") {
4394+ panelClass = k.DEFAULT_PANELS_CLASS;
4395+ if (!panelClass) {
4396+ console.warn("Invalid panel class.")
4397+ }
4398+ }
4399+ p.panels = new panelClass(API, { 'end': turn });
4400+ }
4401+
4402+
4403+ function page() {
4404+ return p.reader.dom.find('page');
4405+ }
4406+
4407+
4408+ function turn(panel) {
4409+ var dir = panel.properties.direction;
4410+ var place = getPlace();
4411+ if (
4412+ (dir < 0 && place.onFirstPageOfBook()) ||
4413+ (dir > 0 && place.onLastPageOfBook())
4414+ ) { return; }
4415+ moveTo({ page: getPlace().pageNumber() + dir });
4416+ }
4417+
4418+
4419+ function frameToLocus(locus) {
4420+ var cmpt = p.reader.dom.find('component');
4421+ var win = cmpt.contentWindow;
4422+ var srcY = scrollPos(win);
4423+ var dims = page().m.dimensions;
4424+ var pageHeight = dims.properties.pageHeight;
4425+ var destY = dims.locusToOffset(locus);
4426+
4427+ //console.log(srcY + " => " + destY);
4428+ if (Math.abs(destY - srcY) > pageHeight) {
4429+ return win.scrollTo(0, destY);
4430+ }
4431+
4432+ showIndicator(win, srcY < destY ? srcY + pageHeight : srcY);
4433+ Monocle.defer(
4434+ function () { smoothScroll(win, srcY, destY, 300, scrollingFinished); },
4435+ 150
4436+ );
4437+ }
4438+
4439+
4440+ function scrollPos(win) {
4441+ // Firefox, Chrome, Opera, Safari
4442+ if (win.pageYOffset) {
4443+ return win.pageYOffset;
4444+ }
4445+ // Internet Explorer 6 - standards mode
4446+ if (win.document.documentElement && win.document.documentElement.scrollTop) {
4447+ return win.document.documentElement.scrollTop;
4448+ }
4449+ // Internet Explorer 6, 7 and 8
4450+ if (win.document.body.scrollTop) {
4451+ return win.document.body.scrollTop;
4452+ }
4453+ return 0;
4454+ }
4455+
4456+
4457+ function smoothScroll(win, currY, finalY, duration, callback) {
4458+ clearTimeout(win.smoothScrollInterval);
4459+ var stamp = (new Date()).getTime();
4460+ var frameRate = 40;
4461+ var step = (finalY - currY) * (frameRate / duration);
4462+ var stepFn = function () {
4463+ var destY = currY + step;
4464+ if (
4465+ (new Date()).getTime() - stamp > duration ||
4466+ Math.abs(currY - finalY) < Math.abs((currY + step) - finalY)
4467+ ) {
4468+ clearTimeout(win.smoothScrollInterval);
4469+ win.scrollTo(0, finalY);
4470+ if (callback) { callback(); }
4471+ } else {
4472+ win.scrollTo(0, destY);
4473+ currY = destY;
4474+ }
4475+ }
4476+ win.smoothScrollInterval = setInterval(stepFn, frameRate);
4477+ }
4478+
4479+
4480+ function scrollingFinished() {
4481+ hideIndicator(page().m.activeFrame.contentWindow);
4482+ p.reader.dispatchEvent('monocle:turn');
4483+ }
4484+
4485+
4486+ function showIndicator(win, pos) {
4487+ if (p.hideTO) { clearTimeout(p.hideTO); }
4488+
4489+ var doc = win.document;
4490+ if (!doc.body.indicator) {
4491+ doc.body.indicator = createIndicator(doc);
4492+ doc.body.appendChild(doc.body.indicator);
4493+ }
4494+ doc.body.indicator.line.style.display = "block";
4495+ doc.body.indicator.style.opacity = 1;
4496+ positionIndicator(pos);
4497+ }
4498+
4499+
4500+ function hideIndicator(win) {
4501+ var doc = win.document;
4502+ p.hideTO = Monocle.defer(
4503+ function () {
4504+ if (!doc.body.indicator) {
4505+ doc.body.indicator = createIndicator(doc);
4506+ doc.body.appendChild(doc.body.indicator);
4507+ }
4508+ var dims = page().m.dimensions;
4509+ positionIndicator(
4510+ dims.locusToOffset(getPlace().getLocus()) + dims.properties.pageHeight
4511+ )
4512+ doc.body.indicator.line.style.display = "none";
4513+ doc.body.indicator.style.opacity = 0.5;
4514+ },
4515+ 600
4516+ );
4517+ }
4518+
4519+
4520+ function createIndicator(doc) {
4521+ var iBox = doc.createElement('div');
4522+ doc.body.appendChild(iBox);
4523+ Monocle.Styles.applyRules(iBox, k.STYLES.iBox);
4524+
4525+ iBox.arrow = doc.createElement('div');
4526+ iBox.appendChild(iBox.arrow);
4527+ Monocle.Styles.applyRules(iBox.arrow, k.STYLES.arrow);
4528+
4529+ iBox.line = doc.createElement('div');
4530+ iBox.appendChild(iBox.line);
4531+ Monocle.Styles.applyRules(iBox.line, k.STYLES.line);
4532+
4533+ return iBox;
4534+ }
4535+
4536+
4537+ function positionIndicator(y) {
4538+ var p = page();
4539+ var doc = p.m.activeFrame.contentDocument;
4540+ var maxHeight = p.m.dimensions.properties.bodyHeight;
4541+ maxHeight -= doc.body.indicator.offsetHeight;
4542+ if (y > maxHeight) {
4543+ y = maxHeight;
4544+ }
4545+ doc.body.indicator.style.top = y + "px";
4546+ }
4547+
4548+
4549+ // THIS IS THE CORE API THAT ALL FLIPPERS MUST PROVIDE.
4550+ API.pageCount = p.pageCount;
4551+ API.addPage = addPage;
4552+ API.getPlace = getPlace;
4553+ API.moveTo = moveTo;
4554+ API.listenForInteraction = listenForInteraction;
4555+
4556+ initialize();
4557+
4558+ return API;
4559+}
4560+
4561+Monocle.Flippers.Legacy.FORWARDS = 1;
4562+Monocle.Flippers.Legacy.BACKWARDS = -1;
4563+Monocle.Flippers.Legacy.DEFAULT_PANELS_CLASS = Monocle.Panels.TwoPane;
4564+
4565+Monocle.Flippers.Legacy.STYLES = {
4566+ iBox: {
4567+ 'position': 'absolute',
4568+ 'right': 0,
4569+ 'left': 0,
4570+ 'height': '10px'
4571+ },
4572+ arrow: {
4573+ 'position': 'absolute',
4574+ 'right': 0,
4575+ 'height': '10px',
4576+ 'width': '10px',
4577+ 'background': '#333',
4578+ 'border-radius': '6px'
4579+ },
4580+ line: {
4581+ 'width': '100%',
4582+ 'border-top': '2px dotted #333',
4583+ 'margin-top': '5px'
4584+ }
4585+}
4586+
4587+Monocle.pieceLoaded('flippers/legacy');
4588+Monocle.Dimensions.Columns = function (pageDiv) {
4589+
4590+ var API = { constructor: Monocle.Dimensions.Columns }
4591+ var k = API.constants = API.constructor;
4592+ var p = API.properties = {
4593+ page: pageDiv,
4594+ reader: pageDiv.m.reader,
4595+ length: 0,
4596+ width: 0
4597+ }
4598+
4599+ // Logically, forceColumn browsers can't have a gap, because that would
4600+ // make the minWidth > 200%. But how much greater? Not worth the effort.
4601+ k.GAP = Monocle.Browser.env.forceColumns ? 0 : 20;
4602+
4603+ function update(callback) {
4604+ setColumnWidth();
4605+ Monocle.defer(function () {
4606+ p.length = columnCount();
4607+ if (Monocle.DEBUG) {
4608+ console.log(
4609+ 'page['+p.page.m.pageIndex+'] -> '+p.length+
4610+ ' ('+p.page.m.activeFrame.m.component.properties.id+')'
4611+ );
4612+ }
4613+ callback(p.length);
4614+ });
4615+ }
4616+
4617+
4618+ function setColumnWidth() {
4619+ var pdims = pageDimensions();
4620+ var ce = columnedElement();
4621+
4622+ p.width = pdims.width;
4623+
4624+ var rules = Monocle.Styles.rulesToString(k.STYLE["columned"]);
4625+ rules += Monocle.Browser.css.toCSSDeclaration('column-width', pdims.col+'px');
4626+ rules += Monocle.Browser.css.toCSSDeclaration('column-gap', k.GAP+'px');
4627+ rules += Monocle.Browser.css.toCSSDeclaration('column-fill', 'auto');
4628+ rules += Monocle.Browser.css.toCSSDeclaration('transform', 'translateX(0)');
4629+
4630+ if (Monocle.Browser.env.forceColumns && ce.scrollHeight > pdims.height) {
4631+ rules += Monocle.Styles.rulesToString(k.STYLE['column-force']);
4632+ if (Monocle.DEBUG) {
4633+ console.warn("Force columns ("+ce.scrollHeight+" > "+pdims.height+")");
4634+ }
4635+ }
4636+
4637+ if (ce.style.cssText != rules) {
4638+ // Update offset because we're translating to zero.
4639+ p.page.m.offset = 0;
4640+
4641+ // Apply body style changes.
4642+ ce.style.cssText = rules;
4643+
4644+ if (Monocle.Browser.env.scrollToApplyStyle) {
4645+ ce.scrollLeft = 0;
4646+ }
4647+ }
4648+ }
4649+
4650+
4651+ // Returns the element to which columns CSS should be applied.
4652+ //
4653+ function columnedElement() {
4654+ return p.page.m.activeFrame.contentDocument.body;
4655+ }
4656+
4657+
4658+ // Returns the width of the offsettable area of the columned element. By
4659+ // definition, the number of pages is always this divided by the
4660+ // width of a single page (eg, the client area of the columned element).
4661+ //
4662+ function columnedWidth() {
4663+ var bd = columnedElement();
4664+ var de = p.page.m.activeFrame.contentDocument.documentElement;
4665+
4666+ var w = Math.max(bd.scrollWidth, de.scrollWidth);
4667+
4668+ // Add one because the final column doesn't have right gutter.
4669+ w += k.GAP;
4670+
4671+ if (!Monocle.Browser.env.widthsIgnoreTranslate && p.page.m.offset) {
4672+ w += p.page.m.offset;
4673+ }
4674+ return w;
4675+ }
4676+
4677+
4678+ function pageDimensions() {
4679+ var elem = p.page.m.sheafDiv;
4680+ return {
4681+ col: elem.clientWidth,
4682+ width: elem.clientWidth + k.GAP,
4683+ height: elem.clientHeight
4684+ }
4685+ }
4686+
4687+
4688+ function columnCount() {
4689+ return Math.ceil(columnedWidth() / pageDimensions().width)
4690+ }
4691+
4692+
4693+ function locusToOffset(locus) {
4694+ return pageDimensions().width * (locus.page - 1);
4695+ }
4696+
4697+
4698+ // Moves the columned element to the offset implied by the locus.
4699+ //
4700+ // The 'transition' argument is optional, allowing the translation to be
4701+ // animated. If not given, no change is made to the columned element's
4702+ // transition property.
4703+ //
4704+ function translateToLocus(locus, transition) {
4705+ var offset = locusToOffset(locus);
4706+ p.page.m.offset = offset;
4707+ translateToOffset(offset, transition);
4708+ return offset;
4709+ }
4710+
4711+
4712+ function translateToOffset(offset, transition) {
4713+ var ce = columnedElement();
4714+ if (transition) {
4715+ Monocle.Styles.affix(ce, "transition", transition);
4716+ }
4717+ Monocle.Styles.affix(ce, "transform", "translateX(-"+offset+"px)");
4718+ }
4719+
4720+
4721+ function percentageThroughOfNode(target) {
4722+ if (!target) { return 0; }
4723+ var doc = p.page.m.activeFrame.contentDocument;
4724+ var offset = 0;
4725+ if (Monocle.Browser.env.findNodesByScrolling) {
4726+ // First, remove translation...
4727+ translateToOffset(0);
4728+
4729+ // Store scroll offsets for all windows.
4730+ var win = s = p.page.m.activeFrame.contentWindow;
4731+ var scrollers = [
4732+ [win, win.scrollX, win.scrollY],
4733+ [window, window.scrollX, window.scrollY]
4734+ ];
4735+ //while (s != s.parent) { scrollers.push([s, s.scrollX]); s = s.parent; }
4736+
4737+ if (Monocle.Browser.env.sheafIsScroller) {
4738+ var scroller = p.page.m.sheafDiv;
4739+ var x = scroller.scrollLeft;
4740+ target.scrollIntoView();
4741+ offset = scroller.scrollLeft;
4742+ } else {
4743+ var scroller = win;
4744+ var x = scroller.scrollX;
4745+ target.scrollIntoView();
4746+ offset = scroller.scrollX;
4747+ }
4748+
4749+ // Restore scroll offsets for all windows.
4750+ while (s = scrollers.shift()) {
4751+ s[0].scrollTo(s[1], s[2]);
4752+ }
4753+
4754+ // ... finally, replace translation.
4755+ translateToOffset(p.page.m.offset);
4756+ } else {
4757+ offset = target.getBoundingClientRect().left;
4758+ offset -= doc.body.getBoundingClientRect().left;
4759+ }
4760+
4761+ // We know at least 1px will be visible, and offset should not be 0.
4762+ offset += 1;
4763+
4764+ // Percent is the offset divided by the total width of the component.
4765+ var percent = offset / (p.length * p.width);
4766+
4767+ // Page number would be offset divided by the width of a single page.
4768+ // var pageNum = Math.ceil(offset / pageDimensions().width);
4769+
4770+ return percent;
4771+ }
4772+
4773+
4774+ API.update = update;
4775+ API.percentageThroughOfNode = percentageThroughOfNode;
4776+
4777+ API.locusToOffset = locusToOffset;
4778+ API.translateToLocus = translateToLocus;
4779+
4780+ return API;
4781+}
4782+
4783+
4784+Monocle.Dimensions.Columns.STYLE = {
4785+ // Most of these are already applied to body, but they're repeated here
4786+ // in case columnedElement() is ever anything other than body.
4787+ "columned": {
4788+ "margin": "0",
4789+ "padding": "0",
4790+ "height": "100%",
4791+ "width": "100%",
4792+ "position": "absolute"
4793+ },
4794+ "column-force": {
4795+ "min-width": "200%",
4796+ "overflow": "hidden"
4797+ }
4798+}
4799+
4800+Monocle.pieceLoaded("dimensions/columns");
4801+Monocle.Flippers.Slider = function (reader) {
4802+ if (Monocle.Flippers == this) {
4803+ return new Monocle.Flippers.Slider(reader);
4804+ }
4805+
4806+ var API = { constructor: Monocle.Flippers.Slider }
4807+ var k = API.constants = API.constructor;
4808+ var p = API.properties = {
4809+ pageCount: 2,
4810+ activeIndex: 1,
4811+ turnData: {}
4812+ }
4813+
4814+
4815+ function initialize() {
4816+ p.reader = reader;
4817+ p.reader.listen("monocle:componentchanging", showWaitControl);
4818+ }
4819+
4820+
4821+ function addPage(pageDiv) {
4822+ pageDiv.m.dimensions = new Monocle.Dimensions.Columns(pageDiv);
4823+
4824+ // BROWSERHACK: Firefox 4 is prone to beachballing on the first page turn
4825+ // unless a zeroed translateX has been applied to the page div.
4826+ Monocle.Styles.setX(pageDiv, 0);
4827+ }
4828+
4829+
4830+ function visiblePages() {
4831+ return [upperPage()];
4832+ }
4833+
4834+
4835+ function listenForInteraction(panelClass) {
4836+ // BROWSERHACK: Firstly, prime interactiveMode for buggy iOS WebKit.
4837+ interactiveMode(true);
4838+ interactiveMode(false);
4839+
4840+ if (typeof panelClass != "function") {
4841+ panelClass = k.DEFAULT_PANELS_CLASS;
4842+ if (!panelClass) {
4843+ console.warn("Invalid panel class.")
4844+ }
4845+ }
4846+ var q = function (action, panel, x) {
4847+ var dir = panel.properties.direction;
4848+ if (action == "lift") {
4849+ lift(dir, x);
4850+ } else if (action == "release") {
4851+ release(dir, x);
4852+ }
4853+ }
4854+ p.panels = new panelClass(
4855+ API,
4856+ {
4857+ 'start': function (panel, x) { q('lift', panel, x); },
4858+ 'move': function (panel, x) { turning(panel.properties.direction, x); },
4859+ 'end': function (panel, x) { q('release', panel, x); },
4860+ 'cancel': function (panel, x) { q('release', panel, x); }
4861+ }
4862+ );
4863+ }
4864+
4865+
4866+ // A panel can call this with true/false to indicate that the user needs
4867+ // to be able to select or otherwise interact with text.
4868+ function interactiveMode(bState) {
4869+ p.reader.dispatchEvent('monocle:interactive:'+(bState ? 'on' : 'off'));
4870+ if (!Monocle.Browser.env.selectIgnoresZOrder) { return; }
4871+ if (p.interactive = bState) {
4872+ if (p.activeIndex != 0) {
4873+ var place = getPlace();
4874+ if (place) {
4875+ setPage(
4876+ p.reader.dom.find('page', 0),
4877+ place.getLocus(),
4878+ function () {
4879+ flipPages();
4880+ prepareNextPage();
4881+ }
4882+ );
4883+ } else {
4884+ flipPages();
4885+ }
4886+ }
4887+ }
4888+ }
4889+
4890+
4891+ function getPlace(pageDiv) {
4892+ pageDiv = pageDiv || upperPage();
4893+ return pageDiv.m ? pageDiv.m.place : null;
4894+ }
4895+
4896+
4897+ function moveTo(locus, callback) {
4898+ var fn = function () {
4899+ prepareNextPage(function () {
4900+ if (typeof callback == "function") { callback(); }
4901+ announceTurn();
4902+ });
4903+ }
4904+ setPage(upperPage(), locus, fn);
4905+ }
4906+
4907+
4908+ function setPage(pageDiv, locus, callback) {
4909+ p.reader.getBook().setOrLoadPageAt(
4910+ pageDiv,
4911+ locus,
4912+ function (locus) {
4913+ pageDiv.m.dimensions.translateToLocus(locus);
4914+ Monocle.defer(callback);
4915+ }
4916+ );
4917+ }
4918+
4919+
4920+ function upperPage() {
4921+ return p.reader.dom.find('page', p.activeIndex);
4922+ }
4923+
4924+
4925+ function lowerPage() {
4926+ return p.reader.dom.find('page', (p.activeIndex + 1) % 2);
4927+ }
4928+
4929+
4930+ function flipPages() {
4931+ upperPage().style.zIndex = 1;
4932+ lowerPage().style.zIndex = 2;
4933+ return p.activeIndex = (p.activeIndex + 1) % 2;
4934+ }
4935+
4936+
4937+ function lift(dir, boxPointX) {
4938+ if (p.turnData.lifting || p.turnData.releasing) { return; }
4939+
4940+ p.turnData.points = {
4941+ start: boxPointX,
4942+ min: boxPointX,
4943+ max: boxPointX
4944+ }
4945+ p.turnData.lifting = true;
4946+
4947+ var place = getPlace();
4948+
4949+ if (dir == k.FORWARDS) {
4950+ if (place.onLastPageOfBook()) {
4951+ p.reader.dispatchEvent(
4952+ 'monocle:boundaryend',
4953+ {
4954+ locus: getPlace().getLocus({ direction : dir }),
4955+ page: upperPage()
4956+ }
4957+ );
4958+ resetTurnData();
4959+ return;
4960+ }
4961+ onGoingForward(boxPointX);
4962+ } else if (dir == k.BACKWARDS) {
4963+ if (place.onFirstPageOfBook()) {
4964+ p.reader.dispatchEvent(
4965+ 'monocle:boundarystart',
4966+ {
4967+ locus: getPlace().getLocus({ direction : dir }),
4968+ page: upperPage()
4969+ }
4970+ );
4971+ resetTurnData();
4972+ return;
4973+ }
4974+ onGoingBackward(boxPointX);
4975+ } else {
4976+ console.warn("Invalid direction: " + dir);
4977+ }
4978+ }
4979+
4980+
4981+ function turning(dir, boxPointX) {
4982+ if (!p.turnData.points) { return; }
4983+ if (p.turnData.lifting || p.turnData.releasing) { return; }
4984+ checkPoint(boxPointX);
4985+ slideToCursor(boxPointX, null, "0");
4986+ }
4987+
4988+
4989+ function release(dir, boxPointX) {
4990+ if (!p.turnData.points) {
4991+ return;
4992+ }
4993+ if (p.turnData.lifting) {
4994+ p.turnData.releaseArgs = [dir, boxPointX];
4995+ return;
4996+ }
4997+ if (p.turnData.releasing) {
4998+ return;
4999+ }
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches