bookreader 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +7 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +43 -0
  9. data/Rakefile +10 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/bookreader.gemspec +42 -0
  13. data/lib/bookreader/engine.rb +6 -0
  14. data/lib/bookreader/version.rb +3 -0
  15. data/lib/bookreader.rb +8 -0
  16. data/vendor/assets/images/BRicons.png +0 -0
  17. data/vendor/assets/images/BRicons.svg +94 -0
  18. data/vendor/assets/images/BRicons_ia.png +0 -0
  19. data/vendor/assets/images/back_pages.png +0 -0
  20. data/vendor/assets/images/book_bottom_icon.png +0 -0
  21. data/vendor/assets/images/book_down_icon.png +0 -0
  22. data/vendor/assets/images/book_left_icon.png +0 -0
  23. data/vendor/assets/images/book_leftmost_icon.png +0 -0
  24. data/vendor/assets/images/book_right_icon.png +0 -0
  25. data/vendor/assets/images/book_rightmost_icon.png +0 -0
  26. data/vendor/assets/images/book_top_icon.png +0 -0
  27. data/vendor/assets/images/book_up_icon.png +0 -0
  28. data/vendor/assets/images/books_graphic.svg +177 -0
  29. data/vendor/assets/images/booksplit.png +0 -0
  30. data/vendor/assets/images/control_pause_icon.png +0 -0
  31. data/vendor/assets/images/control_play_icon.png +0 -0
  32. data/vendor/assets/images/embed_icon.png +0 -0
  33. data/vendor/assets/images/icon-home-ia.png +0 -0
  34. data/vendor/assets/images/icon_OL-logo-xs.png +0 -0
  35. data/vendor/assets/images/icon_alert-xs.png +0 -0
  36. data/vendor/assets/images/icon_book.svg +12 -0
  37. data/vendor/assets/images/icon_bookmark.svg +12 -0
  38. data/vendor/assets/images/icon_close-pop.png +0 -0
  39. data/vendor/assets/images/icon_download.png +0 -0
  40. data/vendor/assets/images/icon_gear.svg +17 -0
  41. data/vendor/assets/images/icon_hamburger.svg +20 -0
  42. data/vendor/assets/images/icon_home.png +0 -0
  43. data/vendor/assets/images/icon_home.svg +21 -0
  44. data/vendor/assets/images/icon_home_ia.png +0 -0
  45. data/vendor/assets/images/icon_indicator.png +0 -0
  46. data/vendor/assets/images/icon_info.svg +12 -0
  47. data/vendor/assets/images/icon_one_page.svg +16 -0
  48. data/vendor/assets/images/icon_return.png +0 -0
  49. data/vendor/assets/images/icon_search_button.svg +14 -0
  50. data/vendor/assets/images/icon_search_button_blue.svg +18 -0
  51. data/vendor/assets/images/icon_share.svg +17 -0
  52. data/vendor/assets/images/icon_speaker.svg +18 -0
  53. data/vendor/assets/images/icon_speaker_open.svg +26 -0
  54. data/vendor/assets/images/icon_thumbnails.svg +19 -0
  55. data/vendor/assets/images/icon_two_pages.svg +17 -0
  56. data/vendor/assets/images/icon_zoomer.png +0 -0
  57. data/vendor/assets/images/left_edges.png +0 -0
  58. data/vendor/assets/images/loading.gif +0 -0
  59. data/vendor/assets/images/logo_icon.png +0 -0
  60. data/vendor/assets/images/marker_chap-off.png +0 -0
  61. data/vendor/assets/images/marker_chap-off.svg +11 -0
  62. data/vendor/assets/images/marker_chap-off_ia.png +0 -0
  63. data/vendor/assets/images/marker_chap-on.png +0 -0
  64. data/vendor/assets/images/marker_chap-on.svg +11 -0
  65. data/vendor/assets/images/marker_srch-off.png +0 -0
  66. data/vendor/assets/images/marker_srch-off.svg +11 -0
  67. data/vendor/assets/images/marker_srch-on.png +0 -0
  68. data/vendor/assets/images/marker_srch-on.svg +11 -0
  69. data/vendor/assets/images/marker_srchchap-off.png +0 -0
  70. data/vendor/assets/images/marker_srchchap-on.png +0 -0
  71. data/vendor/assets/images/nav_control-dn.png +0 -0
  72. data/vendor/assets/images/nav_control-dn_ia.png +0 -0
  73. data/vendor/assets/images/nav_control-up.png +0 -0
  74. data/vendor/assets/images/nav_control-up_ia.png +0 -0
  75. data/vendor/assets/images/nav_control.png +0 -0
  76. data/vendor/assets/images/one_page_mode_icon.png +0 -0
  77. data/vendor/assets/images/paper-badge.png +0 -0
  78. data/vendor/assets/images/print_icon.png +0 -0
  79. data/vendor/assets/images/progressbar.gif +0 -0
  80. data/vendor/assets/images/right_edges.png +0 -0
  81. data/vendor/assets/images/slider.png +0 -0
  82. data/vendor/assets/images/slider_ia.png +0 -0
  83. data/vendor/assets/images/thumbnail_mode_icon.png +0 -0
  84. data/vendor/assets/images/transparent.png +0 -0
  85. data/vendor/assets/images/two_page_mode_icon.png +0 -0
  86. data/vendor/assets/images/zoom_in_icon.png +0 -0
  87. data/vendor/assets/images/zoom_out_icon.png +0 -0
  88. data/vendor/assets/javascripts/BookReader.js +4849 -0
  89. data/vendor/assets/javascripts/BookReaderJSAdvanced.js +115 -0
  90. data/vendor/assets/javascripts/BookReaderJSSimple.js +56 -0
  91. data/vendor/assets/javascripts/IIIFBookReader.js +207 -0
  92. data/vendor/assets/javascripts/demo-iiif.js +26 -0
  93. data/vendor/assets/javascripts/dragscrollable-br.js +261 -0
  94. data/vendor/assets/javascripts/excanvas.compiled.js +35 -0
  95. data/vendor/assets/javascripts/jquery-1.10.1.js +9807 -0
  96. data/vendor/assets/javascripts/jquery-ui-1.12.0.min.js +18686 -0
  97. data/vendor/assets/javascripts/jquery.browser.min.js +14 -0
  98. data/vendor/assets/javascripts/jquery.bt.min.js +8 -0
  99. data/vendor/assets/javascripts/jquery.colorbox-min.js +6 -0
  100. data/vendor/assets/javascripts/jquery.ui.touch-punch.min.js +11 -0
  101. data/vendor/assets/javascripts/mmenu/dist/addons/navbars/jquery.mmenu.navbars.min.js +43 -0
  102. data/vendor/assets/javascripts/mmenu/dist/addons/offcanvas/jquery.mmenu.offcanvas.min.js +7 -0
  103. data/vendor/assets/javascripts/mmenu/dist/addons/screenreader/jquery.mmenu.screenreader.min.js +7 -0
  104. data/vendor/assets/javascripts/mmenu/dist/addons/searchfield/jquery.mmenu.searchfield.min.js +7 -0
  105. data/vendor/assets/javascripts/mmenu/dist/js/jquery.mmenu.all.min.js +151 -0
  106. data/vendor/assets/javascripts/mmenu/dist/js/jquery.mmenu.all.min.umd.js +162 -0
  107. data/vendor/assets/javascripts/mmenu/dist/js/jquery.mmenu.min.js +25 -0
  108. data/vendor/assets/javascripts/mmenu/dist/js/jquery.mmenu.min.umd.js +36 -0
  109. data/vendor/assets/javascripts/mmenu/dist/js/jquery.mmenu.oncanvas.min.js +13 -0
  110. data/vendor/assets/javascripts/mmenu/jquery.mmenu.searchfield.js +510 -0
  111. data/vendor/assets/javascripts/plugins/plugin.archive_analytics.js +67 -0
  112. data/vendor/assets/javascripts/plugins/plugin.chapters.js +197 -0
  113. data/vendor/assets/javascripts/plugins/plugin.iframe.js +73 -0
  114. data/vendor/assets/javascripts/plugins/plugin.mobile_nav.js +197 -0
  115. data/vendor/assets/javascripts/plugins/plugin.print.js +70 -0
  116. data/vendor/assets/javascripts/plugins/plugin.resume.js +73 -0
  117. data/vendor/assets/javascripts/plugins/plugin.search.js +523 -0
  118. data/vendor/assets/javascripts/plugins/plugin.themes.js +49 -0
  119. data/vendor/assets/javascripts/plugins/plugin.tts.js +523 -0
  120. data/vendor/assets/javascripts/plugins/plugin.url.js +168 -0
  121. data/vendor/assets/javascripts/soundmanager/license.txt +29 -0
  122. data/vendor/assets/javascripts/soundmanager/script/soundmanager2-jsmin.js +113 -0
  123. data/vendor/assets/javascripts/soundmanager/script/soundmanager2-nodebug-jsmin.js +83 -0
  124. data/vendor/assets/javascripts/soundmanager/script/soundmanager2-nodebug.js +2765 -0
  125. data/vendor/assets/javascripts/soundmanager/script/soundmanager2.js +6325 -0
  126. data/vendor/assets/javascripts/soundmanager/swf/soundmanager2.swf +0 -0
  127. data/vendor/assets/javascripts/soundmanager/swf/soundmanager2_debug.swf +0 -0
  128. data/vendor/assets/javascripts/soundmanager/swf/soundmanager2_flash9.swf +0 -0
  129. data/vendor/assets/javascripts/soundmanager/swf/soundmanager2_flash9_debug.swf +0 -0
  130. data/vendor/assets/javascripts/soundmanager/swf/soundmanager2_flash_xdomain.zip +0 -0
  131. data/vendor/assets/stylesheets/BookReader.css +1887 -0
  132. data/vendor/assets/stylesheets/BookReaderDemo.css +18 -0
  133. data/vendor/assets/stylesheets/BookReaderEmbed.css +0 -0
  134. data/vendor/assets/stylesheets/demo.css +0 -0
  135. data/vendor/assets/stylesheets/dist/addons/navbars/jquery.mmenu.navbars.css +29 -0
  136. data/vendor/assets/stylesheets/dist/addons/offcanvas/jquery.mmenu.offcanvas.css +14 -0
  137. data/vendor/assets/stylesheets/dist/addons/screenreader/jquery.mmenu.screenreader.css +1 -0
  138. data/vendor/assets/stylesheets/dist/addons/searchfield/jquery.mmenu.searchfield.css +16 -0
  139. data/vendor/assets/stylesheets/dist/css/jquery.mmenu.all.css +413 -0
  140. data/vendor/assets/stylesheets/dist/css/jquery.mmenu.css +80 -0
  141. data/vendor/assets/stylesheets/dist/css/jquery.mmenu.oncanvas.css +66 -0
  142. metadata +226 -0
@@ -0,0 +1,4849 @@
1
+ /*
2
+ Copyright(c)2008-2016 Internet Archive. Software license AGPL version 3.
3
+
4
+ This file is part of BookReader.
5
+
6
+ BookReader is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ BookReader is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with BookReader. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ The BookReader source is hosted at http://github.com/internetarchive/bookreader/
20
+
21
+ */
22
+
23
+ window.BookReader = (function ($) {
24
+
25
+ // BookReader()
26
+ //_________________________________________________________________________
27
+
28
+ /**
29
+ * BookReader
30
+ * @param {Object} options
31
+ * TODO document all options properties
32
+ * @constructor
33
+ */
34
+ function BookReader(options) {
35
+ options = options || {};
36
+ options = jQuery.extend({}, BookReader.defaultOptions, options, BookReader.optionOverrides);
37
+ this.setup(options);
38
+ }
39
+
40
+ BookReader.version = "4.0.0-beta";
41
+
42
+ // Mode constants
43
+ BookReader.constMode1up = 1;
44
+ BookReader.constMode2up = 2;
45
+ BookReader.constModeThumb = 3;
46
+
47
+ // Names of events that can be triggered via BookReader.prototype.trigger()
48
+ BookReader.eventNames = {
49
+ // Indicates that the fragment (a serialization of the reader state)
50
+ // has changed.
51
+ fragmentChange: 'fragmentChange',
52
+ PostInit: 'PostInit',
53
+ stop: 'stop',
54
+ };
55
+
56
+ BookReader.defaultOptions = {
57
+ // A string, such as "mode/1up"
58
+ defaults: null,
59
+
60
+ // Padding in 1up
61
+ padding: 10,
62
+ // UI mode
63
+ ui: 'full', // full, embed, responsive
64
+
65
+ // Controls whether nav/toolbar will autohide
66
+ uiAutoHide: false,
67
+
68
+ // thumbnail mode
69
+ // number of rows to pre-cache out a view
70
+ thumbRowBuffer: 2,
71
+ thumbColumns: 6,
72
+ // number of thumbnails to load at once
73
+ thumbMaxLoading: 4,
74
+ // spacing between thumbnails
75
+ thumbPadding: 10,
76
+ // speed for flip animation
77
+ flipSpeed: 'fast',
78
+
79
+ showToolbar: true,
80
+ showLogo: true,
81
+ // Where the logo links to
82
+ logoURL: 'https://archive.org',
83
+
84
+ // Base URL for UI images - should be overriden (before init) by
85
+ // custom implementations.
86
+ // $$$ This is the same directory as the images referenced by relative
87
+ // path in the CSS. Would be better to automagically find that path.
88
+ imagesBaseURL: '/BookReader/images/',
89
+
90
+ // Zoom levels
91
+ // $$$ provide finer grained zooming, {reduce: 8, autofit: null}, {reduce: 16, autofit: null}
92
+ /* The autofit code ensures that fit to width and fit to height will be available */
93
+ reductionFactors: [
94
+ {reduce: 0.5, autofit: null},
95
+ {reduce: 1, autofit: null},
96
+ {reduce: 2, autofit: null},
97
+ {reduce: 3, autofit: null},
98
+ {reduce: 4, autofit: null},
99
+ {reduce: 6, autofit: null}
100
+ ],
101
+
102
+ // Object to hold parameters related to 1up mode
103
+ onePage: {
104
+ autofit: 'auto', // valid values are height, width, auto, none
105
+ },
106
+
107
+ // Object to hold parameters related to 2up mode
108
+ twoPage: {
109
+ coverInternalPadding: 0, // Width of cover
110
+ coverExternalPadding: 0, // Padding outside of cover
111
+ bookSpineDivWidth: 64, // Width of book spine $$$ consider sizing based on book length
112
+ autofit: 'auto'
113
+ },
114
+
115
+ onePageMinBreakpoint: 800,
116
+
117
+ bookTitle: '',
118
+ bookUrl: null,
119
+ bookUrlText: null,
120
+ bookUrlTitle: null,
121
+ enableBookTitleLink: true,
122
+
123
+ // Fields used to populate the info window
124
+ metadata: [],
125
+ thumbnail: null,
126
+ bookUrlMoreInfo: null,
127
+
128
+ // Experimental Controls (eg b/w)
129
+ enableExperimentalControls: false,
130
+
131
+ // CSS selectors
132
+ // Where BookReader mounts to
133
+ el: '#BookReader',
134
+
135
+ // Page progression. Choices: 'lr', 'rl'
136
+ pageProgression: 'lr',
137
+
138
+ // Should image downloads be blocked
139
+ protected: false,
140
+
141
+ // Data is a simple way to populate the bookreader
142
+ // Example:
143
+ // [
144
+ // // Each child is a spread
145
+ // [
146
+ // {
147
+ // width: 123,
148
+ // height: 123,
149
+ // // Optional: If not provided, include a getPageURI
150
+ // uri: 'https://archive.org/image.jpg',
151
+ // // Optional: Shown instead of leaf number if present.
152
+ // pageNum: 1
153
+ // },
154
+ // {width: 123, height: 123, uri: 'https://archive.org/image2.jpg', pageNum: 2},
155
+ // ]
156
+ // ],
157
+ //
158
+ // Note if URI is omitted, a custom getPageURI can be provided. This allows the page
159
+ // URI to the result of a function, which allows for thigns such as dynamic
160
+ // page scaling.
161
+ data: [],
162
+
163
+ // Advanced methods for page rendering
164
+ getNumLeafs: null,
165
+ getPageWidth: null,
166
+ getPageHeight: null,
167
+ getPageURI: null,
168
+
169
+ // Return which side, left or right, that a given page should be displayed on
170
+ getPageSide: null,
171
+
172
+ // This function returns the left and right indices for the user-visible
173
+ // spread that contains the given index. The return values may be
174
+ // null if there is no facing page or the index is invalid.
175
+ getSpreadIndices: null,
176
+
177
+ getPageNum: null,
178
+ leafNumToIndex: null,
179
+
180
+ // Optional: if present, and embed code will be shown in the share dialog
181
+ getEmbedCode: null,
182
+ };
183
+
184
+ /**
185
+ * This is here, just in case you need to absolutely override an option.
186
+ */
187
+ BookReader.optionOverrides = {};
188
+
189
+ /**
190
+ * Setup
191
+ * It is separate from the constructor, so plugins can extend.
192
+ * @param {Object} options
193
+ */
194
+ BookReader.prototype.setup = function(options) {
195
+ // Store the options used to setup bookreader
196
+ this.options = options;
197
+
198
+ // @deprecated: Instance constants. Use Class constants instead
199
+ this.constMode1up = BookReader.constMode1up;
200
+ this.constMode2up = BookReader.constMode2up;
201
+ this.constModeThumb = BookReader.constModeThumb;
202
+
203
+ // Private properties below. Configuration should be done with options.
204
+ this.reduce = 4;
205
+ this.defaults = options.defaults;
206
+ this.padding = options.padding;
207
+ this.mode = this.constMode1up;
208
+ this.prevReadMode = null;
209
+ this.ui = options.ui;
210
+ this.uiAutoHide = options.uiAutoHide;
211
+
212
+ this.thumbWidth = 100; // will be overridden during prepareThumbnailView
213
+ this.thumbRowBuffer = options.thumbRowBuffer;
214
+ this.thumbColumns = options.thumbColumns;
215
+ this.thumbMaxLoading = options.thumbMaxLoading;
216
+ this.thumbPadding = options.thumbPadding;
217
+ this.displayedRows = [];
218
+
219
+ this.displayedIndices = [];
220
+ this.imgs = {};
221
+ this.prefetchedImgs = {}; //an object with numeric keys cooresponding to page index
222
+
223
+ this.animating = false;
224
+ this.auto = false;
225
+ this.autoTimer = null;
226
+ this.flipSpeed = options.flipSpeed;
227
+ this.twoPagePopUp = null;
228
+ this.leafEdgeTmp = null;
229
+ this.firstIndex = null;
230
+ this.lastDisplayableIndex2up = null;
231
+ this.isFullscreenActive = false;
232
+
233
+ this.showToolbar = options.showToolbar;
234
+ this.showLogo = options.showLogo;
235
+ this.logoURL = options.logoURL;
236
+ this.imagesBaseURL = options.imagesBaseURL;
237
+
238
+ this.reductionFactors = options.reductionFactors;
239
+ this.onePage = options.onePage;
240
+ this.twoPage = options.twoPage;
241
+ this.onePageMinBreakpoint = options.onePageMinBreakpoint;
242
+
243
+ this.bookTitle = options.bookTitle;
244
+ this.bookUrl = options.bookUrl;
245
+ this.bookUrlText = options.bookUrlText;
246
+ this.bookUrlTitle = options.bookUrlTitle;
247
+ this.titleLeaf = options.titleLeaf;
248
+
249
+ this.metadata = options.metadata;
250
+ this.thumbnail = options.thumbnail;
251
+ this.bookUrlMoreInfo = options.bookUrlMoreInfo;
252
+
253
+ this.enableExperimentalControls = options.enableExperimentalControls;
254
+ this.el = options.el;
255
+
256
+ this.pageProgression = options.pageProgression;
257
+ this.protected = options.protected;
258
+ this.getEmbedCode = options.getEmbedCode;
259
+
260
+ // Assign the data methods
261
+ this.data = options.data;
262
+ this.getNumLeafs = options.getNumLeafs || BookReader.prototype.getNumLeafs;
263
+ this.getPageWidth = options.getPageWidth || BookReader.prototype.getPageWidth;
264
+ this.getPageHeight = options.getPageHeight || BookReader.prototype.getPageHeight;
265
+ this.getPageURI = options.getPageURI || BookReader.prototype.getPageURI;
266
+ this.getPageSide = options.getPageSide || BookReader.prototype.getPageSide;
267
+ this.getPageNum = options.getPageNum || BookReader.prototype.getPageNum;
268
+ this.getSpreadIndices = options.getSpreadIndices || BookReader.prototype.getSpreadIndices;
269
+ this.leafNumToIndex = options.leafNumToIndex || BookReader.prototype.leafNumToIndex;
270
+ this.refs = {};
271
+ };
272
+
273
+ // Library functions
274
+ // At top of file so they can be used below
275
+ BookReader.util = {
276
+ disableSelect: function(jObject) {
277
+ // Bind mouse handlers
278
+ // Disable mouse click to avoid selected/highlighted page images - bug 354239
279
+ jObject.bind('mousedown', function(e) {
280
+ // $$$ check here for right-click and don't disable. Also use jQuery style
281
+ // for stopping propagation. See https://bugs.edge.launchpad.net/gnubook/+bug/362626
282
+ return false;
283
+ });
284
+ // Special hack for IE7
285
+ jObject[0].onselectstart = function(e) { return false; };
286
+ },
287
+
288
+ clamp: function(value, min, max) {
289
+ return Math.min(Math.max(value, min), max);
290
+ },
291
+
292
+ // Given value and maximum, calculate a percentage suitable for CSS
293
+ cssPercentage: function(value, max) {
294
+ return (((value + 0.0) / max) * 100) + '%';
295
+ },
296
+
297
+ notInArray: function(value, array) {
298
+ // inArray returns -1 or undefined if value not in array
299
+ return ! (jQuery.inArray(value, array) >= 0);
300
+ },
301
+
302
+ getIFrameDocument: function(iframe) {
303
+ // Adapted from http://xkr.us/articles/dom/iframe-document/
304
+ var outer = (iframe.contentWindow || iframe.contentDocument);
305
+ return (outer.document || outer);
306
+ },
307
+
308
+ escapeHTML: function (str) {
309
+ return(
310
+ str.replace(/&/g,'&amp;').
311
+ replace(/>/g,'&gt;').
312
+ replace(/</g,'&lt;').
313
+ replace(/"/g,'&quot;')
314
+ );
315
+ },
316
+
317
+ decodeURIComponentPlus: function(value) {
318
+ // Decodes a URI component and converts '+' to ' '
319
+ return decodeURIComponent(value).replace(/\+/g, ' ');
320
+ },
321
+
322
+ encodeURIComponentPlus: function(value) {
323
+ // Encodes a URI component and converts ' ' to '+'
324
+ return encodeURIComponent(value).replace(/%20/g, '+');
325
+ },
326
+
327
+ /**
328
+ * Returns a function, that, as long as it continues to be invoked, will not
329
+ * be triggered. The function will be called after it stops being called for
330
+ * N milliseconds. If `immediate` is passed, trigger the function on the
331
+ * leading edge, instead of the trailing.
332
+ * @see https://davidwalsh.name/javascript-debounce-function
333
+ */
334
+ debounce: function(func, wait, immediate) {
335
+ var timeout;
336
+ return function() {
337
+ var context = this, args = arguments;
338
+ var later = function() {
339
+ timeout = null;
340
+ if (!immediate) func.apply(context, args);
341
+ };
342
+ var callNow = immediate && !timeout;
343
+ clearTimeout(timeout);
344
+ timeout = setTimeout(later, wait);
345
+ if (callNow) func.apply(context, args);
346
+ };
347
+ },
348
+
349
+ /**
350
+ * Throttle function
351
+ * @see https://remysharp.com/2010/07/21/throttling-function-calls
352
+ */
353
+ throttle: function(fn, threshhold, delay) {
354
+ threshhold || (threshhold = 250);
355
+ var last,
356
+ deferTimer;
357
+ if (delay) last = +new Date;
358
+ return function () {
359
+ var context = this;
360
+ var now = +new Date,
361
+ args = arguments;
362
+ if (last && now < last + threshhold) {
363
+ // hold on to it
364
+ clearTimeout(deferTimer);
365
+ deferTimer = setTimeout(function () {
366
+ last = now;
367
+ fn.apply(context, args);
368
+ }, threshhold);
369
+ } else {
370
+ last = now;
371
+ fn.apply(context, args);
372
+ }
373
+ };
374
+ },
375
+ };
376
+
377
+ /**
378
+ * Helper to merge in params in to a params object.
379
+ * It normalizes "page" into the "index" field to disambiguate and prevent concflicts
380
+ * @private
381
+ */
382
+ BookReader.prototype.extendParams = function(params, newParams) {
383
+ var modifiedNewParams = $.extend({}, newParams);
384
+ if ('undefined' != typeof(modifiedNewParams.page)) {
385
+ var pageIndex = this.parsePageString(modifiedNewParams.page);
386
+ if (!isNaN(pageIndex))
387
+ modifiedNewParams.index = pageIndex;
388
+ delete modifiedNewParams.page;
389
+ }
390
+ $.extend(params, modifiedNewParams);
391
+ }
392
+
393
+ /**
394
+ * Parses params from from various initialization contexts (url, cookie, options)
395
+ * @private
396
+ * @return {object} the parased params
397
+ */
398
+ BookReader.prototype.initParams = function() {
399
+ var params = {};
400
+
401
+ // This is ordered from lowest to highest priority
402
+
403
+ // If we have a title leaf, use that as the default instead of index 0,
404
+ // but only use as default if book has a few pages
405
+ if ('undefined' != typeof(this.titleLeaf) && this.getNumLeafs() > 2) {
406
+ params.index = this.leafNumToIndex(this.titleLeaf);
407
+ } else {
408
+ params.index = 0;
409
+ }
410
+
411
+ // this.defaults is a string passed in the url format. eg "page/1/mode/1up"
412
+ if (this.defaults) {
413
+ this.extendParams(params, this.paramsFromFragment(this.defaults));
414
+ }
415
+
416
+ // Check for Resume plugin
417
+ if (this.options.enablePageResume) {
418
+ // Check cookies
419
+ var val = this.getResumeValue();
420
+ if (val !== null) {
421
+ params.index = val;
422
+ }
423
+ }
424
+
425
+ if (this.options.enableUrlPlugin) {
426
+ // params explicitly set in URL take precedence over all other methods
427
+ var urlParams = this.paramsFromFragment(this.urlReadFragment());
428
+ if (urlParams.mode) {
429
+ this.prevReadMode = urlParams.mode;
430
+ }
431
+ this.extendParams(params, urlParams);
432
+ }
433
+
434
+ return params;
435
+ }
436
+
437
+ // init()
438
+ //______________________________________________________________________________
439
+ BookReader.prototype.init = function() {
440
+ this.pageScale = this.reduce; // preserve current reduce
441
+
442
+ var params = this.initParams();
443
+
444
+ // params.index takes precedence over params.page
445
+ if (params.index) {
446
+ this.firstIndex = params.index;
447
+ } else {
448
+ this.firstIndex = 0;
449
+ }
450
+
451
+ // Use params or browser width to set view mode
452
+ var windowWidth = $(window).width();
453
+ var nextMode;
454
+ if ('undefined' != typeof(params.mode)) {
455
+ nextMode = params.mode;
456
+ } else if (this.ui == 'full'
457
+ && this.enableMobileNav
458
+ && this.isFullscreenActive
459
+ && windowWidth <= this.onePageMinBreakpoint
460
+ ) {
461
+ // In full mode, we set the default based on width
462
+ nextMode = this.constMode1up;
463
+ } else {
464
+ nextMode = this.constMode2up;
465
+ }
466
+
467
+ if (this.canSwitchToMode(nextMode)) {
468
+ this.mode = nextMode;
469
+ } else {
470
+ this.mode = this.constMode1up;
471
+ }
472
+
473
+ //-------------------------------------------------------------------------
474
+ // Setup Navbars and other UI
475
+
476
+ this.isTouchDevice = !!('ontouchstart' in window) || !!('msmaxtouchpoints' in window.navigator);
477
+
478
+ // Calculate Max page num (used for pagination display)
479
+ this.maxPageNum = 0;
480
+ var pageNumVal;
481
+ for (var i = 0; i < this.getNumLeafs(); i++) {
482
+ pageNumVal = this.getPageNum(i);
483
+ if (!isNaN(pageNumVal) && pageNumVal > this.maxPageNum) {
484
+ this.maxPageNum = pageNumVal;
485
+ }
486
+ }
487
+
488
+ this.refs.$br = $(this.el);
489
+ this.refs.$br.empty().removeClass().addClass("ui-" + this.ui).addClass('BookReader');
490
+
491
+ this.refs.$brContainer = $("<div class='BRcontainer' dir='ltr'></div>");
492
+ this.refs.$br.append(this.refs.$brContainer);
493
+
494
+ // We init the nav bar after the params processing so that the nav slider
495
+ // knows where it should start (doesn't jump after init)
496
+ if (this.ui == "embed") {
497
+ this.initEmbedNavbar();
498
+ } else {
499
+ if (this.showToolbar) {
500
+ this.initToolbar(this.mode, this.ui); // Build inside of toolbar div
501
+ }
502
+ this.initNavbar();
503
+ }
504
+ this.resizeBRcontainer();
505
+
506
+ // Set strings in the UI
507
+ this.initUIStrings();
508
+
509
+ // $$$ refactor this so it's enough to set the first index and call preparePageView
510
+ // (get rid of mode-specific logic at this point)
511
+ if (this.constMode1up == this.mode) {
512
+ this.prepareOnePageView();
513
+ } else if (this.constModeThumb == this.mode) {
514
+ this.prepareThumbnailView();
515
+ } else {
516
+ this.displayedIndices = [this.firstIndex];
517
+ this.prepareTwoPageView();
518
+ }
519
+
520
+ // Enact other parts of initial params
521
+ this.updateFromParams(params);
522
+
523
+ // Add a class if this is a touch enabled device
524
+ if (this.isTouchDevice) {
525
+ this.refs.$br.addClass("touch");
526
+ } else {
527
+ this.refs.$br.addClass("no-touch");
528
+ }
529
+
530
+ // Add class to body for mode. Responsiveness is disabled in embed.
531
+ this.refs.$br.addClass("br-ui-" + this.ui);
532
+
533
+ //-------------------------------------------------------------------------
534
+ // Bind to events
535
+
536
+ if (!this.isTouchDevice) this.setupTooltips();
537
+ this.bindNavigationHandlers();
538
+ this.setupKeyListeners();
539
+
540
+ this.lastScroll = (new Date().getTime());
541
+ this.refs.$brContainer.bind('scroll', this, function(e) {
542
+ // Note, this scroll event fires for both user, and js generated calls
543
+ // It is functioning in some cases as the primary triggerer for rendering
544
+ e.data.lastScroll = (new Date().getTime());
545
+ if (e.data.constMode2up != e.data.mode) {
546
+ e.data.drawLeafsThrottled();
547
+ }
548
+ });
549
+
550
+ $(window).bind('resize', this, function(e) {
551
+ e.data.resize();
552
+ });
553
+ $(window).bind("orientationchange", this, function(e) {
554
+ e.data.resize();
555
+ });
556
+
557
+ if (this.protected) {
558
+ $(document).on('contextmenu dragstart', '.BRpagediv1up', function(e) {
559
+ return false;
560
+ });
561
+ $(document).on('contextmenu dragstart', '.BRpageimage', function(e) {
562
+ return false;
563
+ });
564
+ $(document).on('contextmenu dragstart', '.BRpagedivthumb', function(e) {
565
+ return false;
566
+ });
567
+ this.$('.BRicon.share').hide();
568
+ }
569
+
570
+ this.$('.BRpagediv1up').bind('mousedown', this, function(e) {
571
+ // $$$ the purpose of this is to disable selection of the image (makes it turn blue)
572
+ // but this also interferes with right-click. See https://bugs.edge.launchpad.net/gnubook/+bug/362626
573
+ return false;
574
+ });
575
+
576
+ this.bind(BookReader.eventNames.stop, function(e, br) {
577
+ br.autoStop();
578
+ });
579
+
580
+ this.trigger(BookReader.eventNames.PostInit);
581
+
582
+ this.init.initComplete = true;
583
+ }
584
+
585
+ BookReader.prototype.trigger = function(name, props) {
586
+ $(document).trigger('BookReader:' + name, this, props);
587
+ };
588
+
589
+ BookReader.prototype.bind = function(name, callback) {
590
+ $(document).bind('BookReader:' + name, callback);
591
+ };
592
+
593
+ BookReader.prototype.unbind = function(name, callback) {
594
+ $(document).bind('BookReader:' + name, callback);
595
+ };
596
+
597
+ // resize
598
+ // Resizes the bookreader
599
+ //______________________________________________________________________________
600
+ BookReader.prototype.resize = function() {
601
+ if (!this.init.initComplete) return;
602
+
603
+ this.resizeBRcontainer();
604
+
605
+ if (this.constMode1up == this.mode) {
606
+ if (this.onePage.autofit != 'none') {
607
+ this.resizePageView();
608
+ this.centerPageView();
609
+ if (this.enableSearch) this.updateSearchHilites(); //deletes hilights but does not call remove()
610
+ } else {
611
+ this.centerPageView();
612
+ this.displayedIndices = [];
613
+ if (this.enableSearch) this.updateSearchHilites(); //deletes hilights but does not call remove()
614
+ this.drawLeafsThrottled();
615
+ }
616
+ } else if (this.constModeThumb == this.mode){
617
+ this.prepareThumbnailView();
618
+ } else {
619
+ // We only need to prepare again in autofit (size of spread changes)
620
+ if (this.twoPage.autofit) {
621
+ this.prepareTwoPageView();
622
+ } else {
623
+ // Re-center if the scrollbars have disappeared
624
+ var center = this.twoPageGetViewCenter();
625
+ var doRecenter = false;
626
+ if (this.twoPage.totalWidth < this.refs.$brContainer.prop('clientWidth')) {
627
+ center.percentageX = 0.5;
628
+ doRecenter = true;
629
+ }
630
+ if (this.twoPage.totalHeight < this.refs.$brContainer.prop('clientHeight')) {
631
+ center.percentageY = 0.5;
632
+ doRecenter = true;
633
+ }
634
+ if (doRecenter) {
635
+ this.twoPageCenterView(center.percentageX, center.percentageY);
636
+ }
637
+ }
638
+ }
639
+ };
640
+
641
+ BookReader.prototype.setupKeyListeners = function() {
642
+ var self = this;
643
+
644
+ var KEY_PGUP = 33;
645
+ var KEY_PGDOWN = 34;
646
+ var KEY_END = 35;
647
+ var KEY_HOME = 36;
648
+
649
+ var KEY_LEFT = 37;
650
+ var KEY_UP = 38;
651
+ var KEY_RIGHT = 39;
652
+ var KEY_DOWN = 40;
653
+
654
+ // We use document here instead of window to avoid a bug in jQuery on IE7
655
+ $(document).keydown(function(e) {
656
+
657
+ // Keyboard navigation
658
+ if (!self.keyboardNavigationIsDisabled(e)) {
659
+ switch(e.keyCode) {
660
+ case KEY_PGUP:
661
+ case KEY_UP:
662
+ // In 1up mode page scrolling is handled by browser
663
+ if (self.constMode2up == self.mode) {
664
+ e.preventDefault();
665
+ self.prev();
666
+ }
667
+ break;
668
+ case KEY_DOWN:
669
+ case KEY_PGDOWN:
670
+ if (self.constMode2up == self.mode) {
671
+ e.preventDefault();
672
+ self.next();
673
+ }
674
+ break;
675
+ case KEY_END:
676
+ e.preventDefault();
677
+ self.last();
678
+ break;
679
+ case KEY_HOME:
680
+ e.preventDefault();
681
+ self.first();
682
+ break;
683
+ case KEY_LEFT:
684
+ if (self.constModeThumb != self.mode) {
685
+ e.preventDefault();
686
+ self.left();
687
+ }
688
+ break;
689
+ case KEY_RIGHT:
690
+ if (self.constModeThumb != self.mode) {
691
+ e.preventDefault();
692
+ self.right();
693
+ }
694
+ break;
695
+ }
696
+ }
697
+ });
698
+ };
699
+
700
+ // setupTooltips()
701
+ //______________________________________________________________________________
702
+ BookReader.prototype.setupTooltips = function() {
703
+ this.$('.js-tooltip').each(function(idx, el) {
704
+ var options = {
705
+ positions: ['top', 'bottom'],
706
+ shrinkToFit: true,
707
+ spikeGirth: 0,
708
+ spikeLength: 8,
709
+ fill: 'transparent',
710
+ cornerRadius: 0,
711
+ strokeWidth: 0,
712
+ cssStyles: {},
713
+ };
714
+ var $el = $(el);
715
+ if ($el.parents('.BRtoolbar').length) {
716
+ options.positions = ['bottom'];
717
+ options.spikeLength = 12;
718
+ } else if ($el.parents('.BRnav').length) {
719
+ options.positions = ['top'];
720
+ }
721
+ $el.bt(options);
722
+ });
723
+ }
724
+
725
+
726
+ // drawLeafs()
727
+ //______________________________________________________________________________
728
+ BookReader.prototype.drawLeafs = function() {
729
+ if (this.constMode1up == this.mode) {
730
+ this.drawLeafsOnePage();
731
+ } else if (this.constModeThumb == this.mode) {
732
+ this.drawLeafsThumbnail();
733
+ } else {
734
+ this.drawLeafsTwoPage();
735
+ }
736
+ };
737
+
738
+ // bindGestures(jElement)
739
+ //______________________________________________________________________________
740
+ BookReader.prototype.bindGestures = function(jElement) {
741
+ // TODO support gesture change is only iOS. Support android.
742
+ // HACK(2017-01-20) - Momentum scrolling is causing the scroll position
743
+ // to jump after zooming in on mobile device. I am able to reproduce
744
+ // when you move the book with one finger and then add another
745
+ // finger to pinch. Gestures are aware of scroll state.
746
+
747
+ var self = this;
748
+ var numTouches = 1;
749
+
750
+ jElement.unbind('touchmove').bind('touchmove', function(e) {
751
+ if (e.originalEvent.cancelable) numTouches = e.originalEvent.touches.length;
752
+ e.stopPropagation();
753
+ });
754
+
755
+ jElement.unbind('gesturechange').bind('gesturechange', function(e) {
756
+ e.preventDefault();
757
+ // These are two very important fixes to adjust for the scroll position
758
+ // issues described below
759
+ if (!(numTouches !== 2 || (new Date().getTime()) - self.lastScroll < 500)) {
760
+ if (e.originalEvent.scale > 1.5) {
761
+ self.zoom(1);
762
+ } else if (e.originalEvent.scale < 0.6) {
763
+ self.zoom(-1);
764
+ }
765
+ }
766
+ });
767
+ };
768
+
769
+ BookReader.prototype.setClickHandler2UP = function( element, data, handler) {
770
+ $(element).unbind('click').bind('click', data, function(e) {
771
+ handler(e);
772
+ });
773
+ };
774
+
775
+ // drawLeafsOnePage()
776
+ //______________________________________________________________________________
777
+ BookReader.prototype.drawLeafsOnePage = function() {
778
+ var containerHeight = this.refs.$brContainer.height();
779
+ var scrollTop = this.refs.$brContainer.prop('scrollTop');
780
+ var scrollBottom = scrollTop + containerHeight;
781
+ var viewWidth = this.refs.$brContainer.prop('scrollWidth');
782
+
783
+ var indicesToDisplay = [];
784
+
785
+ var i;
786
+ var leafTop = 0;
787
+ var leafBottom = 0;
788
+ for (i=0; i<this.getNumLeafs(); i++) {
789
+ var height = parseInt(this._getPageHeight(i)/this.reduce);
790
+
791
+ leafBottom += height;
792
+ var topInView = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
793
+ var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
794
+ var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
795
+ if (topInView || bottomInView || middleInView) {
796
+ indicesToDisplay.push(i);
797
+ }
798
+ leafTop += height +10;
799
+ leafBottom += 10;
800
+ }
801
+
802
+ // Based of the pages displayed in the view we set the current index
803
+ // $$$ we should consider the page in the center of the view to be the current one
804
+ var firstIndexToDraw = indicesToDisplay[0];
805
+ this.firstIndex = firstIndexToDraw;
806
+
807
+ // Notify of new fragment, but only if we're currently displaying a leaf
808
+ // Hack that fixes #365790
809
+ if (this.displayedIndices.length > 0) {
810
+ this.trigger(BookReader.eventNames.fragmentChange);
811
+ }
812
+
813
+ if ((0 != firstIndexToDraw) && (1 < this.reduce)) {
814
+ firstIndexToDraw--;
815
+ indicesToDisplay.unshift(firstIndexToDraw);
816
+ }
817
+
818
+ var lastIndexToDraw = indicesToDisplay[indicesToDisplay.length-1];
819
+ if ( ((this.getNumLeafs()-1) != lastIndexToDraw) && (1 < this.reduce) ) {
820
+ indicesToDisplay.push(lastIndexToDraw+1);
821
+ }
822
+
823
+ var BRpageViewEl = this.refs.$brPageViewEl.get(0);
824
+
825
+ leafTop = 0;
826
+ var i;
827
+ for (i=0; i<firstIndexToDraw; i++) {
828
+ leafTop += parseInt(this._getPageHeight(i)/this.reduce) +10;
829
+ }
830
+
831
+ for (i=0; i<indicesToDisplay.length; i++) {
832
+ var index = indicesToDisplay[i];
833
+ var height = parseInt(this._getPageHeight(index)/this.reduce);
834
+
835
+ if (BookReader.util.notInArray(indicesToDisplay[i], this.displayedIndices)) {
836
+ var width = parseInt(this._getPageWidth(index)/this.reduce);
837
+ var div = document.createElement('div');
838
+ div.className = 'BRpagediv1up pagediv' + index;
839
+ div.style.position = "absolute";
840
+ div.style.top = leafTop + 'px';
841
+ var left = (viewWidth-width)>>1;
842
+ if (left<0) left = 0;
843
+ div.style.left = left + 'px';
844
+ div.style.width = width + 'px';
845
+ div.style.height = height + 'px';
846
+
847
+ BRpageViewEl.appendChild(div);
848
+
849
+ var img = document.createElement('img');
850
+ img.src = this._getPageURI(index, this.reduce, 0);
851
+ img.className = 'BRnoselect BRonePageImage';
852
+ img.style.width = width + 'px';
853
+ img.style.height = height + 'px';
854
+ div.appendChild(img);
855
+ }
856
+
857
+ leafTop += height +10;
858
+
859
+ }
860
+
861
+ for (i=0; i<this.displayedIndices.length; i++) {
862
+ if (BookReader.util.notInArray(this.displayedIndices[i], indicesToDisplay)) {
863
+ var index = this.displayedIndices[i];
864
+ this.$('.pagediv'+index).remove();
865
+ }
866
+ }
867
+
868
+ this.displayedIndices = indicesToDisplay.slice();
869
+ if (this.enableSearch) this.updateSearchHilites();
870
+
871
+ this.updateToolbarZoom(this.reduce);
872
+
873
+ // Update the slider
874
+ this.updateNavIndexThrottled();
875
+ };
876
+
877
+ // drawLeafsThumbnail()
878
+ //______________________________________________________________________________
879
+ // If seekIndex is defined, the view will be drawn with that page visible (without any
880
+ // animated scrolling)
881
+ BookReader.prototype.drawLeafsThumbnail = function( seekIndex ) {
882
+ var viewWidth = this.refs.$brContainer.prop('scrollWidth') - 20; // width minus buffer
883
+
884
+ var i;
885
+ var leafWidth;
886
+ var leafHeight;
887
+ var rightPos = 0;
888
+ var bottomPos = 0;
889
+ var maxRight = 0;
890
+ var currentRow = 0;
891
+ var leafIndex = 0;
892
+ var leafMap = [];
893
+
894
+ var self = this;
895
+
896
+ // Will be set to top of requested seek index, if set
897
+ var seekTop;
898
+
899
+ // Calculate the position of every thumbnail. $$$ cache instead of calculating on every draw
900
+ for (i=0; i<this.getNumLeafs(); i++) {
901
+ leafWidth = this.thumbWidth;
902
+ if (rightPos + (leafWidth + this.thumbPadding) > viewWidth){
903
+ currentRow++;
904
+ rightPos = 0;
905
+ leafIndex = 0;
906
+ }
907
+
908
+ if (leafMap[currentRow]===undefined) { leafMap[currentRow] = {}; }
909
+ if (leafMap[currentRow].leafs===undefined) {
910
+ leafMap[currentRow].leafs = [];
911
+ leafMap[currentRow].height = 0;
912
+ leafMap[currentRow].top = 0;
913
+ }
914
+ leafMap[currentRow].leafs[leafIndex] = {};
915
+ leafMap[currentRow].leafs[leafIndex].num = i;
916
+ leafMap[currentRow].leafs[leafIndex].left = rightPos;
917
+
918
+ leafHeight = parseInt((this.getPageHeight(leafMap[currentRow].leafs[leafIndex].num)*this.thumbWidth)/this.getPageWidth(leafMap[currentRow].leafs[leafIndex].num), 10);
919
+ if (leafHeight > leafMap[currentRow].height) {
920
+ leafMap[currentRow].height = leafHeight;
921
+ }
922
+ if (leafIndex===0) { bottomPos += this.thumbPadding + leafMap[currentRow].height; }
923
+ rightPos += leafWidth + this.thumbPadding;
924
+ if (rightPos > maxRight) { maxRight = rightPos; }
925
+ leafIndex++;
926
+
927
+ if (i == seekIndex) {
928
+ seekTop = bottomPos - this.thumbPadding - leafMap[currentRow].height;
929
+ }
930
+ }
931
+
932
+ // reset the bottom position based on thumbnails
933
+ this.refs.$brPageViewEl.height(bottomPos);
934
+
935
+ var pageViewBuffer = Math.floor((this.refs.$brContainer.prop('scrollWidth') - maxRight) / 2) - 14;
936
+
937
+ // If seekTop is defined, seeking was requested and target found
938
+ if (typeof(seekTop) != 'undefined') {
939
+ this.refs.$brContainer.scrollTop( seekTop );
940
+ }
941
+
942
+ var scrollTop = this.refs.$brContainer.prop('scrollTop');
943
+ var scrollBottom = scrollTop + this.refs.$brContainer.height();
944
+
945
+ var leafTop = 0;
946
+ var leafBottom = 0;
947
+ var rowsToDisplay = [];
948
+
949
+ // Visible leafs with least/greatest index
950
+ var leastVisible = this.getNumLeafs() - 1;
951
+ var mostVisible = 0;
952
+
953
+ // Determine the thumbnails in view
954
+ for (i=0; i<leafMap.length; i++) {
955
+ leafBottom += this.thumbPadding + leafMap[i].height;
956
+ var topInView = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
957
+ var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
958
+ var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
959
+ if (topInView || bottomInView || middleInView) {
960
+ rowsToDisplay.push(i);
961
+ if (leafMap[i].leafs[0].num < leastVisible) {
962
+ leastVisible = leafMap[i].leafs[0].num;
963
+ }
964
+ if (leafMap[i].leafs[leafMap[i].leafs.length - 1].num > mostVisible) {
965
+ mostVisible = leafMap[i].leafs[leafMap[i].leafs.length - 1].num;
966
+ }
967
+ }
968
+ if (leafTop > leafMap[i].top) { leafMap[i].top = leafTop; }
969
+ leafTop = leafBottom;
970
+ }
971
+
972
+ // create a buffer of preloaded rows before and after the visible rows
973
+ var firstRow = rowsToDisplay[0];
974
+ var lastRow = rowsToDisplay[rowsToDisplay.length-1];
975
+ for (i=1; i<this.thumbRowBuffer+1; i++) {
976
+ if (lastRow+i < leafMap.length) { rowsToDisplay.push(lastRow+i); }
977
+ }
978
+ for (i=1; i<this.thumbRowBuffer+1; i++) {
979
+ if (firstRow-i >= 0) { rowsToDisplay.push(firstRow-i); }
980
+ }
981
+
982
+ // Create the thumbnail divs and images (lazy loaded)
983
+ var j;
984
+ var row;
985
+ var left;
986
+ var index;
987
+ var div;
988
+ var link;
989
+ var img;
990
+ var page;
991
+ for (i=0; i<rowsToDisplay.length; i++) {
992
+ if (BookReader.util.notInArray(rowsToDisplay[i], this.displayedRows)) {
993
+ row = rowsToDisplay[i];
994
+
995
+ for (j=0; j<leafMap[row].leafs.length; j++) {
996
+ index = j;
997
+ leaf = leafMap[row].leafs[j].num;
998
+
999
+ leafWidth = this.thumbWidth;
1000
+ leafHeight = parseInt((this.getPageHeight(leaf)*this.thumbWidth)/this.getPageWidth(leaf), 10);
1001
+ leafTop = leafMap[row].top;
1002
+ left = leafMap[row].leafs[index].left + pageViewBuffer;
1003
+ if ('rl' == this.pageProgression){
1004
+ left = viewWidth - leafWidth - left;
1005
+ }
1006
+
1007
+ div = document.createElement("div");
1008
+ div.style.position = "absolute";
1009
+ div.className = 'BRpagedivthumb pagediv' + leaf;
1010
+
1011
+ left += this.thumbPadding;
1012
+ div.style.top = leafTop + 'px';
1013
+ div.style.left = left + 'px';
1014
+ div.style.width = leafWidth + 'px';
1015
+ div.style.height = leafHeight + 'px';
1016
+
1017
+ // link back to page
1018
+ link = document.createElement("a");
1019
+ $(link).data('leaf', leaf);
1020
+ link.addEventListener('mouseup', function(event) {
1021
+ self.firstIndex = $(this).data('leaf');
1022
+ if (self.prevReadMode === self.constMode1up
1023
+ || self.prevReadMode === self.constMode2up) {
1024
+ self.switchMode(self.prevReadMode);
1025
+ } else {
1026
+ self.switchMode(self.constMode1up);
1027
+ }
1028
+ event.preventDefault();
1029
+ event.stopPropagation();
1030
+ }, true);
1031
+ $(div).append(link);
1032
+
1033
+ this.refs.$brPageViewEl.append(div);
1034
+
1035
+ img = document.createElement("img");
1036
+ var thumbReduce = Math.floor(this.getPageWidth(leaf) / this.thumbWidth);
1037
+
1038
+ $(img).attr('src', this.imagesBaseURL + 'transparent.png')
1039
+ .css({'width': leafWidth+'px', 'height': leafHeight+'px' })
1040
+ .addClass('BRlazyload')
1041
+ // Store the URL of the image that will replace this one
1042
+ .data('srcURL', this._getPageURI(leaf, thumbReduce));
1043
+ $(link).append(img);
1044
+ }
1045
+ }
1046
+ }
1047
+
1048
+ // Remove thumbnails that are not to be displayed
1049
+ var k;
1050
+ for (i=0; i<this.displayedRows.length; i++) {
1051
+ if (BookReader.util.notInArray(this.displayedRows[i], rowsToDisplay)) {
1052
+ row = this.displayedRows[i];
1053
+ for (k=0; k<leafMap[row].leafs.length; k++) {
1054
+ index = leafMap[row].leafs[k].num;
1055
+ this.$('.pagediv'+index).remove();
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ // Update which page is considered current to make sure a visible page is the current one
1061
+ var currentIndex = this.currentIndex();
1062
+ if (currentIndex < leastVisible) {
1063
+ this.setCurrentIndex(leastVisible);
1064
+ } else if (currentIndex > mostVisible) {
1065
+ this.setCurrentIndex(mostVisible);
1066
+ }
1067
+ this.updateNavIndexThrottled();
1068
+
1069
+ this.displayedRows = rowsToDisplay.slice();
1070
+
1071
+ // Notify of new fragment, but only if we're currently displaying a leaf
1072
+ // Hack that fixes #365790
1073
+ if (this.displayedRows.length > 0) {
1074
+ this.trigger(BookReader.eventNames.fragmentChange);
1075
+ }
1076
+
1077
+ // remove previous highlights
1078
+ this.$('.BRpagedivthumb_highlight').removeClass('BRpagedivthumb_highlight');
1079
+
1080
+ // highlight current page
1081
+ this.$('.pagediv'+this.currentIndex()).addClass('BRpagedivthumb_highlight');
1082
+
1083
+ this.lazyLoadThumbnails();
1084
+
1085
+ this.updateToolbarZoom(this.reduce);
1086
+ };
1087
+
1088
+ BookReader.prototype.lazyLoadThumbnails = function() {
1089
+ // We check the complete property since load may not be fired if loading from the cache
1090
+ this.$('.BRlazyloading').filter('[complete=true]').removeClass('BRlazyloading');
1091
+
1092
+ var loading = this.$('.BRlazyloading').length;
1093
+ var toLoad = this.thumbMaxLoading - loading;
1094
+
1095
+ var self = this;
1096
+
1097
+ if (toLoad > 0) {
1098
+ // $$$ TODO load those near top (but not beyond) page view first
1099
+ this.refs.$brPageViewEl.find('img.BRlazyload').filter(':lt(' + toLoad + ')').each( function() {
1100
+ self.lazyLoadImage(this);
1101
+ });
1102
+ }
1103
+ };
1104
+
1105
+ BookReader.prototype.lazyLoadImage = function (dummyImage) {
1106
+ var img = new Image();
1107
+ var self = this;
1108
+
1109
+ $(img)
1110
+ .addClass('BRlazyloading')
1111
+ .one('load', function() {
1112
+ $(this).removeClass('BRlazyloading');
1113
+
1114
+ // $$$ Calling lazyLoadThumbnails here was causing stack overflow on IE so
1115
+ // we call the function after a slight delay. Also the img.complete property
1116
+ // is not yet set in IE8 inside this onload handler
1117
+ setTimeout(function() { self.lazyLoadThumbnails(); }, 100);
1118
+ })
1119
+ .one('error', function() {
1120
+ // Remove class so we no longer count as loading
1121
+ $(this).removeClass('BRlazyloading');
1122
+ })
1123
+
1124
+ //the width set with .attr is ignored by Internet Explorer, causing it to show the image at its original size
1125
+ //but with this one line of css, even IE shows the image at the proper size
1126
+ .css({
1127
+ 'width': $(dummyImage).width()+'px',
1128
+ 'height': $(dummyImage).height()+'px'
1129
+ })
1130
+ .attr({
1131
+ 'width': $(dummyImage).width(),
1132
+ 'height': $(dummyImage).height(),
1133
+ 'src': $(dummyImage).data('srcURL')
1134
+ });
1135
+
1136
+ // replace with the new img
1137
+ $(dummyImage).before(img).remove();
1138
+
1139
+ img = null; // tidy up closure
1140
+ };
1141
+
1142
+
1143
+ // drawLeafsTwoPage()
1144
+ //______________________________________________________________________________
1145
+ BookReader.prototype.drawLeafsTwoPage = function() {
1146
+ var $twoPageViewEl = this.refs.$brTwoPageView;
1147
+ var scrollTop = $twoPageViewEl.prop('scrollTop');
1148
+ var scrollBottom = scrollTop + $twoPageViewEl.height();
1149
+
1150
+ // $$$ we should use calculated values in this.twoPage (recalc if necessary)
1151
+
1152
+ var indexL = this.twoPage.currentIndexL;
1153
+
1154
+ var heightL = this._getPageHeight(indexL);
1155
+ var widthL = this._getPageWidth(indexL);
1156
+
1157
+ var leafEdgeWidthL = this.leafEdgeWidth(indexL);
1158
+ var leafEdgeWidthR = this.twoPage.edgeWidth - leafEdgeWidthL;
1159
+ var bookCoverDivWidth = this.twoPage.bookCoverDivWidth;
1160
+
1161
+ var middle = this.twoPage.middle; // $$$ getter instead?
1162
+ var top = this.twoPageTop();
1163
+ var bookCoverDivLeft = this.twoPage.bookCoverDivLeft;
1164
+
1165
+ this.twoPage.scaledWL = this.getPageWidth2UP(indexL);
1166
+ this.twoPage.gutter = this.twoPageGutter();
1167
+
1168
+ this.prefetchImg(indexL);
1169
+ $(this.prefetchedImgs[indexL]).css({
1170
+ position: 'absolute',
1171
+ left: this.twoPage.gutter-this.twoPage.scaledWL+'px',
1172
+ right: '',
1173
+ top: top+'px',
1174
+ height: this.twoPage.height +'px', // $$$ height forced the same for both pages
1175
+ width: this.twoPage.scaledWL + 'px',
1176
+ zIndex: 2
1177
+ }).appendTo($twoPageViewEl);
1178
+
1179
+ var indexR = this.twoPage.currentIndexR;
1180
+ var heightR = this._getPageHeight(indexR);
1181
+ var widthR = this._getPageWidth(indexR);
1182
+
1183
+ // $$$ should use getwidth2up?
1184
+ //var scaledWR = this.twoPage.height*widthR/heightR;
1185
+ this.twoPage.scaledWR = this.getPageWidth2UP(indexR);
1186
+ this.prefetchImg(indexR);
1187
+ $(this.prefetchedImgs[indexR]).css({
1188
+ position: 'absolute',
1189
+ left: this.twoPage.gutter+'px',
1190
+ right: '',
1191
+ top: top+'px',
1192
+ height: this.twoPage.height + 'px', // $$$ height forced the same for both pages
1193
+ width: this.twoPage.scaledWR + 'px',
1194
+ zIndex: 2
1195
+ }).appendTo($twoPageViewEl);
1196
+
1197
+
1198
+ this.displayedIndices = [this.twoPage.currentIndexL, this.twoPage.currentIndexR];
1199
+ this.setMouseHandlers2UP();
1200
+ this.twoPageSetCursor();
1201
+
1202
+ this.updatePageNumBox2UP();
1203
+ this.updateToolbarZoom(this.reduce);
1204
+ };
1205
+
1206
+ // updatePageNumBox2UP
1207
+ //______________________________________________________________________________
1208
+ BookReader.prototype.updatePageNumBox2UP = function() {
1209
+ // TODO see if this function is still needed
1210
+ this.trigger(BookReader.eventNames.fragmentChange);
1211
+ };
1212
+
1213
+ // drawLeafsThrottled()
1214
+ // A throttled version of drawLeafs
1215
+ //______________________________________________________________________________
1216
+ BookReader.prototype.drawLeafsThrottled = BookReader.util.throttle(
1217
+ BookReader.prototype.drawLeafs,
1218
+ 250 // 250 ms gives quick feedback, but doesn't eat cpu
1219
+ );
1220
+
1221
+
1222
+ // zoom(direction)
1223
+ //
1224
+ // Pass 1 to zoom in, anything else to zoom out
1225
+ //______________________________________________________________________________
1226
+ BookReader.prototype.zoom = function(direction) {
1227
+ switch (this.mode) {
1228
+ case this.constMode1up:
1229
+ if (direction == 1) {
1230
+ // XXX other cases
1231
+ this.zoom1up('in');
1232
+ } else {
1233
+ this.zoom1up('out');
1234
+ }
1235
+ break
1236
+ case this.constMode2up:
1237
+ if (direction == 1) {
1238
+ // XXX other cases
1239
+ this.zoom2up('in');
1240
+ } else {
1241
+ this.zoom2up('out');
1242
+ }
1243
+ break
1244
+ case this.constModeThumb:
1245
+ // XXX update zoomThumb for named directions
1246
+ this.zoomThumb(direction);
1247
+ break
1248
+ }
1249
+ return;
1250
+ };
1251
+
1252
+ // zoom1up(dir)
1253
+ //______________________________________________________________________________
1254
+ BookReader.prototype.zoom1up = function(direction) {
1255
+
1256
+ if (this.constMode2up == this.mode) { //can only zoom in 1-page mode
1257
+ this.switchMode(this.constMode1up);
1258
+ return;
1259
+ }
1260
+
1261
+ var reduceFactor = this.nextReduce(this.reduce, direction, this.onePage.reductionFactors);
1262
+
1263
+ if (this.reduce == reduceFactor.reduce) {
1264
+ // Already at this level
1265
+ return;
1266
+ }
1267
+
1268
+ this.reduce = reduceFactor.reduce; // $$$ incorporate into function
1269
+ this.onePage.autofit = reduceFactor.autofit;
1270
+
1271
+ this.pageScale = this.reduce; // preserve current reduce
1272
+
1273
+ this.resizePageView();
1274
+ this.updateToolbarZoom(this.reduce);
1275
+
1276
+ // Recalculate search hilites
1277
+ if (this.enableSearch) this.removeSearchHilites();
1278
+ if (this.enableSearch) this.updateSearchHilites();
1279
+
1280
+ };
1281
+
1282
+ // Resizes the inner container to fit within the visible space to prevent
1283
+ // the top toolbar and bottom navbar from clipping the visible book
1284
+ BookReader.prototype.resizeBRcontainer = function() {
1285
+ this.refs.$brContainer.css({
1286
+ top: this.getToolBarHeight(),
1287
+ bottom: this.getNavHeight(),
1288
+ });
1289
+ }
1290
+
1291
+ // resizePageView()
1292
+ //______________________________________________________________________________
1293
+ BookReader.prototype.resizePageView = function() {
1294
+ // $$$ This code assumes 1up mode
1295
+ // e.g. does not preserve position in thumbnail mode
1296
+ // See http://bugs.launchpad.net/bookreader/+bug/552972
1297
+ switch (this.mode) {
1298
+ case this.constMode1up:
1299
+ this.resizePageView1up(); // $$$ necessary in non-1up mode?
1300
+ break;
1301
+ case this.constMode2up:
1302
+ break;
1303
+ case this.constModeThumb:
1304
+ this.prepareThumbnailView( this.currentIndex() );
1305
+ break;
1306
+ default:
1307
+ alert('Resize not implemented for this mode');
1308
+ }
1309
+ };
1310
+
1311
+ // Resize the current one page view
1312
+ // Note this calls drawLeafs
1313
+ BookReader.prototype.resizePageView1up = function() {
1314
+ var i;
1315
+ var viewHeight = 0;
1316
+ var viewWidth = this.refs.$brContainer.prop('clientWidth');
1317
+ var oldScrollTop = this.refs.$brContainer.prop('scrollTop');
1318
+ var oldPageViewHeight = this.refs.$brPageViewEl.height();
1319
+ var oldPageViewWidth = this.refs.$brPageViewEl.width();
1320
+
1321
+ // May have come here after preparing the view, in which case the scrollTop and view height are not set
1322
+
1323
+ var scrollRatio = 0;
1324
+ if (oldScrollTop > 0) {
1325
+ // We have scrolled - implies view has been set up
1326
+ var oldCenterY = this.centerY1up();
1327
+ var oldCenterX = this.centerX1up();
1328
+ scrollRatio = oldCenterY / oldPageViewHeight;
1329
+ } else {
1330
+ // Have not scrolled, e.g. because in new container
1331
+
1332
+ // We set the scroll ratio so that the current index will still be considered the
1333
+ // current index in drawLeafsOnePage after we create the new view container
1334
+
1335
+ // Make sure this will count as current page after resize
1336
+ var fudgeFactor = (this.getPageHeight(this.currentIndex()) / this.reduce) * 0.6;
1337
+ var oldLeafTop = this.onePageGetPageTop(this.currentIndex()) + fudgeFactor;
1338
+ var oldViewDimensions = this.onePageCalculateViewDimensions(this.reduce, this.padding);
1339
+ scrollRatio = oldLeafTop / oldViewDimensions.height;
1340
+ }
1341
+
1342
+ // Recalculate 1up reduction factors
1343
+ this.onePageCalculateReductionFactors();
1344
+ // Update current reduce (if in autofit)
1345
+ if (this.onePage.autofit) {
1346
+ var reductionFactor = this.nextReduce(this.reduce, this.onePage.autofit, this.onePage.reductionFactors);
1347
+ this.reduce = reductionFactor.reduce;
1348
+ }
1349
+
1350
+ var viewDimensions = this.onePageCalculateViewDimensions(this.reduce, this.padding);
1351
+
1352
+ this.refs.$brPageViewEl.height(viewDimensions.height);
1353
+ this.refs.$brPageViewEl.width(viewDimensions.width);
1354
+
1355
+
1356
+ var newCenterY = scrollRatio*viewDimensions.height;
1357
+ var newTop = Math.max(0, Math.floor( newCenterY - this.refs.$brContainer.height()/2 ));
1358
+ this.refs.$brContainer.prop('scrollTop', newTop);
1359
+
1360
+ // We use clientWidth here to avoid miscalculating due to scroll bar
1361
+ var newCenterX = oldCenterX * (viewWidth / oldPageViewWidth);
1362
+ var newLeft = newCenterX - this.refs.$brContainer.prop('clientWidth') / 2;
1363
+ newLeft = Math.max(newLeft, 0);
1364
+ this.refs.$brContainer.prop('scrollLeft', newLeft);
1365
+
1366
+ this.refs.$brPageViewEl.empty();
1367
+ this.displayedIndices = [];
1368
+ this.drawLeafs();
1369
+
1370
+ if (this.enableSearch) {
1371
+ this.removeSearchHilites();
1372
+ this.updateSearchHilites();
1373
+ }
1374
+ };
1375
+
1376
+ // Calculate the dimensions for a one page view with images at the given reduce and padding
1377
+ BookReader.prototype.onePageCalculateViewDimensions = function(reduce, padding) {
1378
+ var viewWidth = 0;
1379
+ var viewHeight = 0;
1380
+ for (i=0; i<this.getNumLeafs(); i++) {
1381
+ viewHeight += parseInt(this._getPageHeight(i)/reduce) + padding;
1382
+ var width = parseInt(this._getPageWidth(i)/reduce);
1383
+ if (width>viewWidth) viewWidth=width;
1384
+ }
1385
+ return { width: viewWidth, height: viewHeight }
1386
+ };
1387
+
1388
+ // centerX1up()
1389
+ //______________________________________________________________________________
1390
+ // Returns the current offset of the viewport center in scaled document coordinates.
1391
+ BookReader.prototype.centerX1up = function() {
1392
+ var centerX;
1393
+ if (this.refs.$brPageViewEl.width() < this.refs.$brContainer.prop('clientWidth')) { // fully shown
1394
+ centerX = this.refs.$brPageViewEl.width();
1395
+ } else {
1396
+ centerX = this.refs.$brContainer.prop('scrollLeft') + this.refs.$brContainer.prop('clientWidth') / 2;
1397
+ }
1398
+ centerX = Math.floor(centerX);
1399
+ return centerX;
1400
+ };
1401
+
1402
+ // centerY1up()
1403
+ //______________________________________________________________________________
1404
+ // Returns the current offset of the viewport center in scaled document coordinates.
1405
+ BookReader.prototype.centerY1up = function() {
1406
+ var centerY = this.refs.$brContainer.prop('scrollTop') + this.refs.$brContainer.height() / 2;
1407
+ return Math.floor(centerY);
1408
+ };
1409
+
1410
+ // centerPageView()
1411
+ //______________________________________________________________________________
1412
+ BookReader.prototype.centerPageView = function() {
1413
+ var scrollWidth = this.refs.$brContainer.prop('scrollWidth');
1414
+ var clientWidth = this.refs.$brContainer.prop('clientWidth');
1415
+ if (scrollWidth > clientWidth) {
1416
+ this.refs.$brContainer.prop('scrollLeft', (scrollWidth-clientWidth)/2);
1417
+ }
1418
+ };
1419
+
1420
+ // zoom2up(direction)
1421
+ //______________________________________________________________________________
1422
+ BookReader.prototype.zoom2up = function(direction) {
1423
+
1424
+ // Hard stop autoplay
1425
+ this.stopFlipAnimations();
1426
+
1427
+ // Recalculate autofit factors
1428
+ this.twoPageCalculateReductionFactors();
1429
+
1430
+ // Get new zoom state
1431
+ var reductionFactor = this.nextReduce(this.reduce, direction, this.twoPage.reductionFactors);
1432
+ if ((this.reduce == reductionFactor.reduce) && (this.twoPage.autofit == reductionFactor.autofit)) {
1433
+ // Same zoom
1434
+ return;
1435
+ }
1436
+ this.twoPage.autofit = reductionFactor.autofit;
1437
+ this.reduce = reductionFactor.reduce;
1438
+ this.pageScale = this.reduce; // preserve current reduce
1439
+
1440
+ // Preserve view center position
1441
+ var oldCenter = this.twoPageGetViewCenter();
1442
+
1443
+ // If zooming in, reload imgs. DOM elements will be removed by prepareTwoPageView
1444
+ // $$$ An improvement would be to use the low res image until the larger one is loaded.
1445
+ if (1 == direction) {
1446
+ for (var img in this.prefetchedImgs) {
1447
+ delete this.prefetchedImgs[img];
1448
+ }
1449
+ }
1450
+
1451
+ // Prepare view with new center to minimize visual glitches
1452
+ this.prepareTwoPageView(oldCenter.percentageX, oldCenter.percentageY);
1453
+ };
1454
+
1455
+ BookReader.prototype.zoomThumb = function(direction) {
1456
+ var oldColumns = this.thumbColumns;
1457
+ switch (direction) {
1458
+ case -1:
1459
+ this.thumbColumns += 1;
1460
+ break;
1461
+ case 1:
1462
+ this.thumbColumns -= 1;
1463
+ break;
1464
+ }
1465
+
1466
+ // clamp
1467
+ if (this.thumbColumns < 2) {
1468
+ this.thumbColumns = 2;
1469
+ } else if (this.thumbColumns > 8) {
1470
+ this.thumbColumns = 8;
1471
+ }
1472
+
1473
+ if (this.thumbColumns != oldColumns) {
1474
+ this.prepareThumbnailView();
1475
+ }
1476
+ };
1477
+
1478
+ // Returns the width per thumbnail to display the requested number of columns
1479
+ // Note: #BRpageview must already exist since its width is used to calculate the
1480
+ // thumbnail width
1481
+ BookReader.prototype.getThumbnailWidth = function(thumbnailColumns) {
1482
+ var padding = (thumbnailColumns + 1) * this.thumbPadding;
1483
+ var width = (this.refs.$brPageViewEl.width() - padding) / (thumbnailColumns + 0.5); // extra 0.5 is for some space at sides
1484
+ return parseInt(width);
1485
+ };
1486
+
1487
+ // quantizeReduce(reduce)
1488
+ //______________________________________________________________________________
1489
+ // Quantizes the given reduction factor to closest power of two from set from 12.5% to 200%
1490
+ BookReader.prototype.quantizeReduce = function(reduce, reductionFactors) {
1491
+ var quantized = reductionFactors[0].reduce;
1492
+ var distance = Math.abs(reduce - quantized);
1493
+ for (var i = 1; i < reductionFactors.length; i++) {
1494
+ newDistance = Math.abs(reduce - reductionFactors[i].reduce);
1495
+ if (newDistance < distance) {
1496
+ distance = newDistance;
1497
+ quantized = reductionFactors[i].reduce;
1498
+ }
1499
+ }
1500
+
1501
+ return quantized;
1502
+ };
1503
+
1504
+ // reductionFactors should be array of sorted reduction factors
1505
+ // e.g. [ {reduce: 0.25, autofit: null}, {reduce: 0.3, autofit: 'width'}, {reduce: 1, autofit: null} ]
1506
+ BookReader.prototype.nextReduce = function(currentReduce, direction, reductionFactors) {
1507
+ // XXX add 'closest', to replace quantize function
1508
+
1509
+ if (direction === 'in') {
1510
+ var newReduceIndex = 0;
1511
+ for (var i = 1; i < reductionFactors.length; i++) {
1512
+ if (reductionFactors[i].reduce < currentReduce) {
1513
+ newReduceIndex = i;
1514
+ }
1515
+ }
1516
+ return reductionFactors[newReduceIndex];
1517
+ } else if (direction === 'out') { // zoom out
1518
+ var lastIndex = reductionFactors.length - 1;
1519
+ var newReduceIndex = lastIndex;
1520
+
1521
+ for (var i = lastIndex; i >= 0; i--) {
1522
+ if (reductionFactors[i].reduce > currentReduce) {
1523
+ newReduceIndex = i;
1524
+ }
1525
+ }
1526
+ return reductionFactors[newReduceIndex];
1527
+ } else if (direction === 'auto') {
1528
+ // Auto mode chooses the least reduction
1529
+ var choice = null;
1530
+ for (var i = 0; i < reductionFactors.length; i++) {
1531
+ if (reductionFactors[i].autofit === 'height' || reductionFactors[i].autofit === 'width') {
1532
+ if (choice === null || choice.reduce < reductionFactors[i].reduce) {
1533
+ choice = reductionFactors[i];
1534
+ }
1535
+ }
1536
+ }
1537
+ if (choice) {
1538
+ return choice;
1539
+ }
1540
+ } else if (direction === 'height' || direction === 'width') {
1541
+ // Asked for specific autofit mode
1542
+ for (var i = 0; i < reductionFactors.length; i++) {
1543
+ if (reductionFactors[i].autofit === direction) {
1544
+ return reductionFactors[i];
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
+ alert('Could not find reduction factor for direction ' + direction);
1550
+ return reductionFactors[0];
1551
+
1552
+ };
1553
+
1554
+ BookReader.prototype._reduceSort = function(a, b) {
1555
+ return a.reduce - b.reduce;
1556
+ };
1557
+
1558
+
1559
+ // jumpToPage()
1560
+ //______________________________________________________________________________
1561
+ // Attempts to jump to page. Returns true if page could be found, false otherwise.
1562
+ BookReader.prototype.jumpToPage = function(pageNum) {
1563
+ var pageIndex = this.parsePageString(pageNum);
1564
+
1565
+ if ('undefined' != typeof(pageIndex)) {
1566
+ this.jumpToIndex(pageIndex);
1567
+ return true;
1568
+ }
1569
+
1570
+ // Page not found
1571
+ return false;
1572
+ };
1573
+
1574
+ // jumpToIndex()
1575
+ //______________________________________________________________________________
1576
+ BookReader.prototype.jumpToIndex = function(index, pageX, pageY, noAnimate) {
1577
+ var self = this;
1578
+ var prevCurrentIndex = this.currentIndex();
1579
+
1580
+ // Not throttling is important to prevent race conditions with scroll
1581
+ this.updateNavIndexThrottled(index);
1582
+ this.trigger(BookReader.eventNames.stop);
1583
+
1584
+ if (this.constMode2up == this.mode) {
1585
+ // By checking against min/max we do nothing if requested index
1586
+ // is current
1587
+ if (index < Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
1588
+ this.flipBackToIndex(index);
1589
+ } else if (index > Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
1590
+ this.flipFwdToIndex(index);
1591
+ }
1592
+
1593
+ } else if (this.constModeThumb == this.mode) {
1594
+ var viewWidth = this.refs.$brContainer.prop('scrollWidth') - 20; // width minus buffer
1595
+ var i;
1596
+ var leafWidth = 0;
1597
+ var leafHeight = 0;
1598
+ var rightPos = 0;
1599
+ var bottomPos = 0;
1600
+ var rowHeight = 0;
1601
+ var leafTop = 0;
1602
+ var leafIndex = 0;
1603
+
1604
+ for (i=0; i<(index+1); i++) {
1605
+ leafWidth = this.thumbWidth;
1606
+ if (rightPos + (leafWidth + this.thumbPadding) > viewWidth){
1607
+ rightPos = 0;
1608
+ rowHeight = 0;
1609
+ leafIndex = 0;
1610
+ }
1611
+
1612
+ leafHeight = parseInt((this.getPageHeight(leafIndex)*this.thumbWidth)/this.getPageWidth(leafIndex), 10);
1613
+ if (leafHeight > rowHeight) { rowHeight = leafHeight; }
1614
+ if (leafIndex==0) { leafTop = bottomPos; }
1615
+ if (leafIndex==0) { bottomPos += this.thumbPadding + rowHeight; }
1616
+ rightPos += leafWidth + this.thumbPadding;
1617
+ leafIndex++;
1618
+ }
1619
+ this.firstIndex=index;
1620
+ if (this.refs.$brContainer.prop('scrollTop') == leafTop) {
1621
+ this.drawLeafs();
1622
+ } else {
1623
+ this.animating = true;
1624
+ this.refs.$brContainer.stop(true).animate({
1625
+ scrollTop: leafTop,
1626
+ }, 'fast', function() {
1627
+ self.animating = false;
1628
+ });
1629
+ }
1630
+ } else {
1631
+ // 1up
1632
+ var leafTop = this.onePageGetPageTop(index);
1633
+
1634
+ if (pageY) {
1635
+ var offset = parseInt( (pageY) / this.reduce);
1636
+ offset -= this.refs.$brContainer.prop('clientHeight') >> 1;
1637
+ leafTop += offset;
1638
+ } else {
1639
+ // Show page just a little below the top
1640
+ leafTop -= this.padding / 2;
1641
+ }
1642
+
1643
+ if (pageX) {
1644
+ var offset = parseInt( (pageX) / this.reduce);
1645
+ offset -= this.refs.$brContainer.prop('clientWidth') >> 1;
1646
+ leafLeft += offset;
1647
+ } else {
1648
+ // Preserve left position
1649
+ leafLeft = this.refs.$brContainer.scrollLeft();
1650
+ }
1651
+
1652
+ // Only animate for small distances
1653
+ if (!noAnimate && Math.abs(prevCurrentIndex - index) <= 4) {
1654
+ this.animating = true;
1655
+ this.refs.$brContainer.stop(true).animate({
1656
+ scrollTop: leafTop,
1657
+ scrollLeft: leafLeft,
1658
+ }, 'fast', function() {
1659
+ self.animating = false;
1660
+ });
1661
+ } else {
1662
+ this.refs.$brContainer.stop(true).prop('scrollTop', leafTop);
1663
+ }
1664
+ }
1665
+ };
1666
+
1667
+ // switchMode()
1668
+ //______________________________________________________________________________
1669
+ BookReader.prototype.switchMode = function(mode) {
1670
+ if (mode === this.mode) {
1671
+ return;
1672
+ }
1673
+
1674
+ if (!this.canSwitchToMode(mode)) {
1675
+ return;
1676
+ }
1677
+
1678
+ this.trigger(BookReader.eventNames.stop);
1679
+ if (this.enableSearch) this.removeSearchHilites();
1680
+
1681
+ if (this.mode === this.constMode1up || this.mode === this.constMode2up) {
1682
+ this.prevReadMode = this.mode;
1683
+ }
1684
+
1685
+ this.mode = mode;
1686
+
1687
+ // reinstate scale if moving from thumbnail view
1688
+ if (this.pageScale !== this.reduce) {
1689
+ this.reduce = this.pageScale;
1690
+ }
1691
+
1692
+ // $$$ TODO preserve center of view when switching between mode
1693
+ // See https://bugs.edge.launchpad.net/gnubook/+bug/416682
1694
+
1695
+ // XXX maybe better to preserve zoom in each mode
1696
+ if (this.constMode1up == mode) {
1697
+ this.onePageCalculateReductionFactors();
1698
+ this.reduce = this.quantizeReduce(this.reduce, this.onePage.reductionFactors);
1699
+ this.prepareOnePageView();
1700
+ } else if (this.constModeThumb == mode) {
1701
+ this.reduce = this.quantizeReduce(this.reduce, this.reductionFactors);
1702
+ this.prepareThumbnailView();
1703
+ } else {
1704
+ // $$$ why don't we save autofit?
1705
+ // this.twoPage.autofit = null; // Take zoom level from other mode
1706
+ this.twoPageCalculateReductionFactors();
1707
+ this.reduce = this.quantizeReduce(this.reduce, this.twoPage.reductionFactors);
1708
+ this.prepareTwoPageView();
1709
+ this.twoPageCenterView(0.5, 0.5); // $$$ TODO preserve center
1710
+ }
1711
+
1712
+ this.trigger(BookReader.eventNames.fragmentChange);
1713
+ };
1714
+
1715
+ BookReader.prototype.updateBrClasses = function() {
1716
+ var modeToClass = {};
1717
+ modeToClass[this.constMode1up] = 'BRmode1up';
1718
+ modeToClass[this.constMode2up] = 'BRmode2up';
1719
+ modeToClass[this.constModeThumb] = 'BRmodeThumb';
1720
+
1721
+ this.refs.$br
1722
+ .removeClass('BRmode1up BRmode2up BRmodeThumb')
1723
+ .addClass(modeToClass[this.mode]);
1724
+
1725
+ if (this.isFullscreen()) {
1726
+ this.refs.$br.addClass('fullscreenActive');
1727
+ $(document.body).addClass('BRfullscreenActive');
1728
+ } else {
1729
+ this.refs.$br.removeClass('fullscreenActive');
1730
+ $(document.body).removeClass('BRfullscreenActive');
1731
+ }
1732
+ };
1733
+
1734
+ BookReader.prototype.isFullscreen = function() {
1735
+ return this.isFullscreenActive;
1736
+ };
1737
+
1738
+ BookReader.prototype.toggleFullscreen = function() {
1739
+ if (this.isFullscreen()) {
1740
+ this.exitFullScreen();
1741
+ } else {
1742
+ this.enterFullscreen();
1743
+ }
1744
+ };
1745
+
1746
+ BookReader.prototype.enterFullscreen = function() {
1747
+ this.refs.$brContainer.css('opacity', 0);
1748
+
1749
+ var windowWidth = $(window).width();
1750
+ if (windowWidth <= this.onePageMinBreakpoint) {
1751
+ this.switchMode(this.constMode1up);
1752
+ }
1753
+
1754
+ this.isFullscreenActive = true;
1755
+ this.updateBrClasses();
1756
+
1757
+ this.resize();
1758
+ this.refs.$brContainer.animate({opacity: 1}, 400, 'linear');
1759
+
1760
+ this._fullscreenCloseHandler = function (e) {
1761
+ if (e.keyCode === 27) this.exitFullScreen();
1762
+ }.bind(this);
1763
+ $(document).keyup(this._fullscreenCloseHandler);
1764
+
1765
+ };
1766
+
1767
+ BookReader.prototype.exitFullScreen = function() {
1768
+ this.refs.$brContainer.css('opacity', 0);
1769
+
1770
+ $(document).unbind('keyup', this._fullscreenCloseHandler);
1771
+
1772
+ var windowWidth = $(window).width();
1773
+ if (windowWidth <= this.onePageMinBreakpoint) {
1774
+ this.switchMode(this.constMode2up);
1775
+ }
1776
+
1777
+ this.isFullscreenActive = false;
1778
+ this.updateBrClasses()
1779
+
1780
+ this.resize();
1781
+ this.refs.$brContainer.animate({opacity: 1}, 400, 'linear');
1782
+
1783
+ };
1784
+
1785
+
1786
+ //prepareOnePageView()
1787
+ // This is called when we switch to one page view
1788
+ //______________________________________________________________________________
1789
+ BookReader.prototype.prepareOnePageView = function() {
1790
+ var startLeaf = this.currentIndex();
1791
+
1792
+ this.refs.$brContainer.empty();
1793
+ this.refs.$brContainer.css({
1794
+ overflowY: 'scroll',
1795
+ overflowX: 'auto'
1796
+ });
1797
+
1798
+ this.refs.$brPageViewEl = $("<div class='BRpageview'></div>");
1799
+ this.refs.$brContainer.append(this.refs.$brPageViewEl);
1800
+
1801
+ // Attaches to first child - child must be present
1802
+ this.refs.$brContainer.dragscrollable();
1803
+ this.bindGestures(this.refs.$brContainer);
1804
+
1805
+ // $$$ keep select enabled for now since disabling it breaks keyboard
1806
+ // nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666)
1807
+ // BookReader.util.disableSelect(this.$('#BRpageview'));
1808
+
1809
+ this.resizePageView();
1810
+ this.jumpToIndex(startLeaf);
1811
+ this.updateBrClasses();
1812
+ };
1813
+
1814
+ //prepareThumbnailView()
1815
+ //______________________________________________________________________________
1816
+ BookReader.prototype.prepareThumbnailView = function() {
1817
+ this.refs.$brContainer.empty();
1818
+ this.refs.$brContainer.css({
1819
+ overflowY: 'scroll',
1820
+ overflowX: 'auto'
1821
+ });
1822
+
1823
+ this.refs.$brPageViewEl = $("<div class='BRpageview'></div>");
1824
+ this.refs.$brContainer.append(this.refs.$brPageViewEl);
1825
+ this.refs.$brContainer.dragscrollable({preventDefault:true});
1826
+
1827
+ this.bindGestures(this.refs.$brContainer);
1828
+
1829
+ // $$$ keep select enabled for now since disabling it breaks keyboard
1830
+ // nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666)
1831
+ // BookReader.util.disableSelect(this.$('#BRpageview'));
1832
+
1833
+ this.thumbWidth = this.getThumbnailWidth(this.thumbColumns);
1834
+ this.reduce = this.getPageWidth(0)/this.thumbWidth;
1835
+
1836
+ this.displayedRows = [];
1837
+
1838
+ // Draw leafs with current index directly in view (no animating to the index)
1839
+ this.drawLeafsThumbnail( this.currentIndex() );
1840
+ this.updateBrClasses();
1841
+ };
1842
+
1843
+ // prepareTwoPageView()
1844
+ //______________________________________________________________________________
1845
+ // Some decisions about two page view:
1846
+ //
1847
+ // Both pages will be displayed at the same height, even if they were different physical/scanned
1848
+ // sizes. This simplifies the animation (from a design as well as technical standpoint). We
1849
+ // examine the page aspect ratios (in calculateSpreadSize) and use the page with the most "normal"
1850
+ // aspect ratio to determine the height.
1851
+ //
1852
+ // The two page view div is resized to keep the middle of the book in the middle of the div
1853
+ // even as the page sizes change. To e.g. keep the middle of the book in the middle of the BRcontent
1854
+ // div requires adjusting the offset of BRtwpageview and/or scrolling in BRcontent.
1855
+ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPercentageY) {
1856
+ this.refs.$brContainer.empty();
1857
+ this.refs.$brContainer.css('overflow', 'auto');
1858
+
1859
+ // We want to display two facing pages. We may be missing
1860
+ // one side of the spread because it is the first/last leaf,
1861
+ // foldouts, missing pages, etc
1862
+
1863
+ var targetLeaf = this.firstIndex;
1864
+
1865
+ if (targetLeaf < this.firstDisplayableIndex()) {
1866
+ targetLeaf = this.firstDisplayableIndex();
1867
+ }
1868
+
1869
+ if (targetLeaf > this.lastDisplayableIndex()) {
1870
+ targetLeaf = this.lastDisplayableIndex();
1871
+ }
1872
+
1873
+ var currentSpreadIndices = this.getSpreadIndices(targetLeaf);
1874
+ this.twoPage.currentIndexL = currentSpreadIndices[0];
1875
+ this.twoPage.currentIndexR = currentSpreadIndices[1];
1876
+ this.firstIndex = this.twoPage.currentIndexL;
1877
+
1878
+ this.calculateSpreadSize(); //sets twoPage.width, twoPage.height and others
1879
+
1880
+ this.pruneUnusedImgs();
1881
+ this.prefetch(); // Preload images or reload if scaling has changed
1882
+
1883
+ // Add the two page view
1884
+ // $$$ Can we get everything set up and then append?
1885
+ var $twoPageViewEl = $('<div class="BRtwopageview"></div>');
1886
+ this.refs.$brTwoPageView = $twoPageViewEl;
1887
+ this.refs.$brContainer.append($twoPageViewEl);
1888
+
1889
+ // Attaches to first child, so must come after we add the page view
1890
+ this.refs.$brContainer.dragscrollable({preventDefault:true});
1891
+ this.bindGestures(this.refs.$brContainer);
1892
+
1893
+ // $$$ calculate first then set
1894
+ this.refs.$brTwoPageView.css({
1895
+ height: this.twoPage.totalHeight + 'px',
1896
+ width: this.twoPage.totalWidth + 'px',
1897
+ position: 'absolute'
1898
+ });
1899
+
1900
+ // If there will not be scrollbars (e.g. when zooming out) we center the book
1901
+ // since otherwise the book will be stuck off-center
1902
+ if (this.twoPage.totalWidth < this.refs.$brContainer.prop('clientWidth')) {
1903
+ centerPercentageX = 0.5;
1904
+ }
1905
+ if (this.twoPage.totalHeight < this.refs.$brContainer.prop('clientHeight')) {
1906
+ centerPercentageY = 0.5;
1907
+ }
1908
+
1909
+ this.twoPageCenterView(centerPercentageX, centerPercentageY);
1910
+
1911
+ this.twoPage.coverDiv = document.createElement('div');
1912
+ $(this.twoPage.coverDiv).attr('class', 'BRbookcover').css({
1913
+ width: this.twoPage.bookCoverDivWidth + 'px',
1914
+ height: this.twoPage.bookCoverDivHeight+'px',
1915
+ visibility: 'visible'
1916
+ }).appendTo(this.refs.$brTwoPageView);
1917
+
1918
+ this.leafEdgeR = document.createElement('div');
1919
+ this.leafEdgeR.className = 'BRleafEdgeR';
1920
+ $(this.leafEdgeR).css({
1921
+ width: this.twoPage.leafEdgeWidthR + 'px',
1922
+ height: this.twoPage.height + 'px',
1923
+ left: this.twoPage.gutter+this.twoPage.scaledWR+'px',
1924
+ top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px'
1925
+ }).appendTo(this.refs.$brTwoPageView);
1926
+
1927
+ this.leafEdgeL = document.createElement('div');
1928
+ this.leafEdgeL.className = 'BRleafEdgeL';
1929
+ $(this.leafEdgeL).css({
1930
+ width: this.twoPage.leafEdgeWidthL + 'px',
1931
+ height: this.twoPage.height + 'px',
1932
+ left: this.twoPage.bookCoverDivLeft+this.twoPage.coverInternalPadding+'px',
1933
+ top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px'
1934
+ }).appendTo(this.refs.$brTwoPageView);
1935
+
1936
+ div = document.createElement('div');
1937
+ $(div).attr('class', 'BRgutter').css({
1938
+ width: this.twoPage.bookSpineDivWidth+'px',
1939
+ height: this.twoPage.bookSpineDivHeight+'px',
1940
+ left: (this.twoPage.gutter - this.twoPage.bookSpineDivWidth*0.5)+'px',
1941
+ top: this.twoPage.bookSpineDivTop+'px'
1942
+ }).appendTo(this.refs.$brTwoPageView);
1943
+
1944
+ this.prepareTwoPagePopUp();
1945
+
1946
+ this.displayedIndices = [];
1947
+
1948
+ this.drawLeafsTwoPage();
1949
+ this.updateToolbarZoom(this.reduce);
1950
+
1951
+ this.prefetch();
1952
+
1953
+ if (this.enableSearch) {
1954
+ this.removeSearchHilites();
1955
+ this.updateSearchHilites();
1956
+ }
1957
+
1958
+ this.updateBrClasses();
1959
+ };
1960
+
1961
+ // prepareTwoPagePopUp()
1962
+ //
1963
+ // This function prepares the "View Page n" popup that shows while the mouse is
1964
+ // over the left/right "stack of sheets" edges. It also binds the mouse
1965
+ // events for these divs.
1966
+ //______________________________________________________________________________
1967
+ BookReader.prototype.prepareTwoPagePopUp = function() {
1968
+
1969
+ this.twoPagePopUp = document.createElement('div');
1970
+ this.twoPagePopUp.className = 'BRtwoPagePopUp';
1971
+ $(this.twoPagePopUp).css({
1972
+ zIndex: '1000'
1973
+ }).appendTo(this.refs.$brContainer);
1974
+ $(this.twoPagePopUp).hide();
1975
+
1976
+ $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseenter', this, function(e) {
1977
+ $(e.data.twoPagePopUp).show();
1978
+ });
1979
+
1980
+ $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseleave', this, function(e) {
1981
+ $(e.data.twoPagePopUp).hide();
1982
+ });
1983
+
1984
+ $(this.leafEdgeL).bind('click', this, function(e) {
1985
+ e.data.trigger(BookReader.eventNames.stop);
1986
+ var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
1987
+ e.data.jumpToIndex(jumpIndex);
1988
+ });
1989
+
1990
+ $(this.leafEdgeR).bind('click', this, function(e) {
1991
+ e.data.trigger(BookReader.eventNames.stop);
1992
+ var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
1993
+ e.data.jumpToIndex(jumpIndex);
1994
+ });
1995
+
1996
+ $(this.leafEdgeR).bind('mousemove', this, function(e) {
1997
+
1998
+ var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
1999
+ $(e.data.twoPagePopUp).text('View ' + e.data.getPageName(jumpIndex));
2000
+
2001
+ // $$$ TODO: Make sure popup is positioned so that it is in view
2002
+ // (https://bugs.edge.launchpad.net/gnubook/+bug/327456)
2003
+ $(e.data.twoPagePopUp).css({
2004
+ left: e.pageX- e.data.refs.$brContainer.offset().left + e.data.refs.$brContainer.scrollLeft() - 120 + 'px',
2005
+ top: e.pageY - e.data.refs.$brContainer.offset().top + e.data.refs.$brContainer.scrollTop() + 'px'
2006
+ });
2007
+ });
2008
+
2009
+ $(this.leafEdgeL).bind('mousemove', this, function(e) {
2010
+
2011
+ var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
2012
+ $(e.data.twoPagePopUp).text('View '+ e.data.getPageName(jumpIndex));
2013
+
2014
+ // $$$ TODO: Make sure popup is positioned so that it is in view
2015
+ // (https://bugs.edge.launchpad.net/gnubook/+bug/327456)
2016
+ $(e.data.twoPagePopUp).css({
2017
+ left: e.pageX - e.data.refs.$brContainer.offset().left + e.data.refs.$brContainer.scrollLeft() - $(e.data.twoPagePopUp).width() + 120 + 'px',
2018
+ top: e.pageY-e.data.refs.$brContainer.offset().top + e.data.refs.$brContainer.scrollTop() + 'px'
2019
+ });
2020
+ });
2021
+ };
2022
+
2023
+ // calculateSpreadSize()
2024
+ //______________________________________________________________________________
2025
+ // Calculates 2-page spread dimensions based on this.twoPage.currentIndexL and
2026
+ // this.twoPage.currentIndexR
2027
+ // This function sets this.twoPage.height, twoPage.width
2028
+
2029
+ BookReader.prototype.calculateSpreadSize = function() {
2030
+
2031
+ var firstIndex = this.twoPage.currentIndexL;
2032
+ var secondIndex = this.twoPage.currentIndexR;
2033
+
2034
+ // Calculate page sizes and total leaf width
2035
+ var spreadSize;
2036
+ if ( this.twoPage.autofit) {
2037
+ spreadSize = this.getIdealSpreadSize(firstIndex, secondIndex);
2038
+ } else {
2039
+ // set based on reduction factor
2040
+ spreadSize = this.getSpreadSizeFromReduce(firstIndex, secondIndex, this.reduce);
2041
+ }
2042
+
2043
+ // Both pages together
2044
+ this.twoPage.height = spreadSize.height;
2045
+ this.twoPage.width = spreadSize.width;
2046
+
2047
+ // Individual pages
2048
+ this.twoPage.scaledWL = this.getPageWidth2UP(firstIndex);
2049
+ this.twoPage.scaledWR = this.getPageWidth2UP(secondIndex);
2050
+
2051
+ // Leaf edges
2052
+ this.twoPage.edgeWidth = spreadSize.totalLeafEdgeWidth; // The combined width of both edges
2053
+ this.twoPage.leafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
2054
+ this.twoPage.leafEdgeWidthR = this.twoPage.edgeWidth - this.twoPage.leafEdgeWidthL;
2055
+
2056
+
2057
+ // Book cover
2058
+ // The width of the book cover div. The combined width of both pages, twice the width
2059
+ // of the book cover internal padding (2*10) and the page edges
2060
+ this.twoPage.bookCoverDivWidth = this.twoPageCoverWidth(this.twoPage.scaledWL + this.twoPage.scaledWR);
2061
+ // The height of the book cover div
2062
+ this.twoPage.bookCoverDivHeight = this.twoPage.height + 2 * this.twoPage.coverInternalPadding;
2063
+
2064
+
2065
+ // We calculate the total width and height for the div so that we can make the book
2066
+ // spine centered
2067
+ var leftGutterOffset = this.gutterOffsetForIndex(firstIndex);
2068
+ var leftWidthFromCenter = this.twoPage.scaledWL - leftGutterOffset + this.twoPage.leafEdgeWidthL;
2069
+ var rightWidthFromCenter = this.twoPage.scaledWR + leftGutterOffset + this.twoPage.leafEdgeWidthR;
2070
+ var largestWidthFromCenter = Math.max( leftWidthFromCenter, rightWidthFromCenter );
2071
+ this.twoPage.totalWidth = 2 * (largestWidthFromCenter + this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
2072
+ this.twoPage.totalHeight = this.twoPage.height + 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
2073
+
2074
+ // We want to minimize the unused space in two-up mode (maximize the amount of page
2075
+ // shown). We give width to the leaf edges and these widths change (though the sum
2076
+ // of the two remains constant) as we flip through the book. With the book
2077
+ // cover centered and fixed in the BRcontainer div the page images will meet
2078
+ // at the "gutter" which is generally offset from the center.
2079
+ this.twoPage.middle = this.twoPage.totalWidth >> 1;
2080
+ this.twoPage.gutter = this.twoPage.middle + this.gutterOffsetForIndex(firstIndex);
2081
+
2082
+ // The left edge of the book cover moves depending on the width of the pages
2083
+ // $$$ change to getter
2084
+ this.twoPage.bookCoverDivLeft = this.twoPage.gutter - this.twoPage.scaledWL - this.twoPage.leafEdgeWidthL - this.twoPage.coverInternalPadding;
2085
+ // The top edge of the book cover stays a fixed distance from the top
2086
+ this.twoPage.bookCoverDivTop = this.twoPage.coverExternalPadding;
2087
+
2088
+ // Book spine
2089
+ this.twoPage.bookSpineDivHeight = this.twoPage.height + 2*this.twoPage.coverInternalPadding;
2090
+ this.twoPage.bookSpineDivLeft = this.twoPage.middle - (this.twoPage.bookSpineDivWidth >> 1);
2091
+ this.twoPage.bookSpineDivTop = this.twoPage.bookCoverDivTop;
2092
+
2093
+
2094
+ this.reduce = spreadSize.reduce; // $$$ really set this here?
2095
+ };
2096
+
2097
+ BookReader.prototype.getIdealSpreadSize = function(firstIndex, secondIndex) {
2098
+ var ideal = {};
2099
+
2100
+ // We check which page is closest to a "normal" page and use that to set the height
2101
+ // for both pages. This means that foldouts and other odd size pages will be displayed
2102
+ // smaller than the nominal zoom amount.
2103
+ var canon5Dratio = 1.5;
2104
+
2105
+ var first = {
2106
+ height: this._getPageHeight(firstIndex),
2107
+ width: this._getPageWidth(firstIndex)
2108
+ };
2109
+
2110
+ var second = {
2111
+ height: this._getPageHeight(secondIndex),
2112
+ width: this._getPageWidth(secondIndex)
2113
+ };
2114
+
2115
+ var firstIndexRatio = first.height / first.width;
2116
+ var secondIndexRatio = second.height / second.width;
2117
+
2118
+ var ratio;
2119
+ if (Math.abs(firstIndexRatio - canon5Dratio) < Math.abs(secondIndexRatio - canon5Dratio)) {
2120
+ ratio = firstIndexRatio;
2121
+ } else {
2122
+ ratio = secondIndexRatio;
2123
+ }
2124
+
2125
+ var totalLeafEdgeWidth = parseInt(this.getNumLeafs() * 0.1);
2126
+ var maxLeafEdgeWidth = parseInt(this.refs.$brContainer.prop('clientWidth') * 0.1);
2127
+ ideal.totalLeafEdgeWidth = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
2128
+
2129
+ var widthOutsidePages = 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding) + ideal.totalLeafEdgeWidth;
2130
+ var heightOutsidePages = 2* (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
2131
+
2132
+ ideal.width = (this.refs.$brContainer.width() - widthOutsidePages) >> 1;
2133
+ ideal.width -= 10; // $$$ fudge factor
2134
+ ideal.height = this.refs.$brContainer.height() - heightOutsidePages;
2135
+
2136
+ ideal.height -= 15; // fudge factor
2137
+
2138
+ if (ideal.height/ratio <= ideal.width) {
2139
+ //use height
2140
+ ideal.width = parseInt(ideal.height/ratio);
2141
+ } else {
2142
+ //use width
2143
+ ideal.height = parseInt(ideal.width*ratio);
2144
+ }
2145
+
2146
+ // $$$ check this logic with large spreads
2147
+ ideal.reduce = ((first.height + second.height) / 2) / ideal.height;
2148
+
2149
+ return ideal;
2150
+ };
2151
+
2152
+ // getSpreadSizeFromReduce()
2153
+ //______________________________________________________________________________
2154
+ // Returns the spread size calculated from the reduction factor for the given pages
2155
+ BookReader.prototype.getSpreadSizeFromReduce = function(firstIndex, secondIndex, reduce) {
2156
+ var spreadSize = {};
2157
+ // $$$ Scale this based on reduce?
2158
+ var totalLeafEdgeWidth = parseInt(this.getNumLeafs() * 0.1);
2159
+ var maxLeafEdgeWidth = parseInt(this.refs.$brContainer.prop('clientWidth') * 0.1); // $$$ Assumes leaf edge width constant at all zoom levels
2160
+ spreadSize.totalLeafEdgeWidth = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
2161
+
2162
+ // $$$ Possibly incorrect -- we should make height "dominant"
2163
+ var nativeWidth = this._getPageWidth(firstIndex) + this._getPageWidth(secondIndex);
2164
+ var nativeHeight = this._getPageHeight(firstIndex) + this._getPageHeight(secondIndex);
2165
+ spreadSize.height = parseInt( (nativeHeight / 2) / this.reduce );
2166
+ spreadSize.width = parseInt( (nativeWidth / 2) / this.reduce );
2167
+ spreadSize.reduce = reduce;
2168
+
2169
+ return spreadSize;
2170
+ };
2171
+
2172
+ // twoPageGetAutofitReduce()
2173
+ //______________________________________________________________________________
2174
+ // Returns the current ideal reduction factor
2175
+ BookReader.prototype.twoPageGetAutofitReduce = function() {
2176
+ var spreadSize = this.getIdealSpreadSize(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
2177
+ return spreadSize.reduce;
2178
+ };
2179
+
2180
+ // twoPageIsZoomedIn
2181
+ //______________________________________________________________________________
2182
+ // Returns true if the pages extend past the edge of the view
2183
+ BookReader.prototype.twoPageIsZoomedIn = function() {
2184
+ var autofitReduce = this.twoPageGetAutofitReduce();
2185
+ var isZoomedIn = false;
2186
+ if (this.twoPage.autofit != 'auto') {
2187
+ if (this.reduce < this.twoPageGetAutofitReduce()) {
2188
+ isZoomedIn = true;
2189
+ }
2190
+ }
2191
+ return isZoomedIn;
2192
+ };
2193
+
2194
+ BookReader.prototype.onePageGetAutofitWidth = function() {
2195
+ var widthPadding = 20;
2196
+ return (this.getMedianPageSize().width + 0.0) / (this.refs.$brContainer.prop('clientWidth') - widthPadding * 2);
2197
+ };
2198
+
2199
+ BookReader.prototype.onePageGetAutofitHeight = function() {
2200
+ var availableHeight = this.refs.$brContainer.innerHeight();
2201
+ return (this.getMedianPageSize().height + 0.0) / (availableHeight - this.padding * 2); // make sure a little of adjacent pages show
2202
+ };
2203
+
2204
+ // Returns where the top of the page with given index should be in one page view
2205
+ BookReader.prototype.onePageGetPageTop = function(index)
2206
+ {
2207
+ var i;
2208
+ var leafTop = 0;
2209
+ var leafLeft = 0;
2210
+ var h;
2211
+ for (i=0; i<index; i++) {
2212
+ h = parseInt(this._getPageHeight(i)/this.reduce);
2213
+ leafTop += h + this.padding;
2214
+ }
2215
+ return leafTop;
2216
+ };
2217
+
2218
+ BookReader.prototype.getMedianPageSize = function() {
2219
+ if (this._medianPageSize) {
2220
+ return this._medianPageSize;
2221
+ }
2222
+
2223
+ // A little expensive but we just do it once
2224
+ var widths = [];
2225
+ var heights = [];
2226
+ for (var i = 0; i < this.getNumLeafs(); i++) {
2227
+ widths.push(this.getPageWidth(i));
2228
+ heights.push(this.getPageHeight(i));
2229
+ }
2230
+
2231
+ widths.sort();
2232
+ heights.sort();
2233
+
2234
+ this._medianPageSize = { width: widths[parseInt(widths.length / 2)], height: heights[parseInt(heights.length / 2)] };
2235
+ return this._medianPageSize;
2236
+ };
2237
+
2238
+ // Update the reduction factors for 1up mode given the available width and height. Recalculates
2239
+ // the autofit reduction factors.
2240
+ BookReader.prototype.onePageCalculateReductionFactors = function() {
2241
+ this.onePage.reductionFactors = this.reductionFactors.concat(
2242
+ [
2243
+ { reduce: this.onePageGetAutofitWidth(), autofit: 'width' },
2244
+ { reduce: this.onePageGetAutofitHeight(), autofit: 'height'}
2245
+ ]);
2246
+ this.onePage.reductionFactors.sort(this._reduceSort);
2247
+ };
2248
+
2249
+ BookReader.prototype.twoPageCalculateReductionFactors = function() {
2250
+ this.twoPage.reductionFactors = this.reductionFactors.concat(
2251
+ [
2252
+ { reduce: this.getIdealSpreadSize( this.twoPage.currentIndexL, this.twoPage.currentIndexR ).reduce,
2253
+ autofit: 'auto' }
2254
+ ]);
2255
+ this.twoPage.reductionFactors.sort(this._reduceSort);
2256
+ };
2257
+
2258
+ // twoPageSetCursor()
2259
+ //______________________________________________________________________________
2260
+ // Set the cursor for two page view
2261
+ BookReader.prototype.twoPageSetCursor = function() {
2262
+ var $twoPageViewEl = this.refs.$brTwoPageView;
2263
+ if ( ($twoPageViewEl.width() > this.refs.$brContainer.prop('clientWidth')) ||
2264
+ ($twoPageViewEl.height() > this.refs.$brContainer.prop('clientHeight')) ) {
2265
+ if (this.prefetchedImgs[this.twoPage.currentIndexL])
2266
+ this.prefetchedImgs[this.twoPage.currentIndexL].style.cursor = 'move';
2267
+ if (this.prefetchedImgs[this.twoPage.currentIndexR])
2268
+ this.prefetchedImgs[this.twoPage.currentIndexR].style.cursor = 'move';
2269
+ } else {
2270
+ if (this.prefetchedImgs[this.twoPage.currentIndexL])
2271
+ this.prefetchedImgs[this.twoPage.currentIndexL].style.cursor = '';
2272
+ if (this.prefetchedImgs[this.twoPage.currentIndexR])
2273
+ this.prefetchedImgs[this.twoPage.currentIndexR].style.cursor = '';
2274
+ }
2275
+ };
2276
+
2277
+ // currentIndex()
2278
+ //______________________________________________________________________________
2279
+ // Returns the currently active index.
2280
+ BookReader.prototype.currentIndex = function() {
2281
+ // $$$ we should be cleaner with our idea of which index is active in 1up/2up
2282
+ if (this.mode == this.constMode1up || this.mode == this.constModeThumb) {
2283
+ return this.firstIndex; // $$$ TODO page in center of view would be better
2284
+ } else if (this.mode == this.constMode2up) {
2285
+ // Only allow indices that are actually present in book
2286
+ return BookReader.util.clamp(this.firstIndex, 0, this.getNumLeafs() - 1);
2287
+ } else {
2288
+ throw 'currentIndex called for unimplemented mode ' + this.mode;
2289
+ }
2290
+ };
2291
+
2292
+ // setCurrentIndex(index)
2293
+ //______________________________________________________________________________
2294
+ // Sets the idea of current index without triggering other actions such as animation.
2295
+ // Compare to jumpToIndex which animates to that index
2296
+ BookReader.prototype.setCurrentIndex = function(index) {
2297
+ this.firstIndex = index;
2298
+ };
2299
+
2300
+
2301
+ // right()
2302
+ //______________________________________________________________________________
2303
+ // Flip the right page over onto the left
2304
+ BookReader.prototype.right = function() {
2305
+ if ('rl' != this.pageProgression) {
2306
+ // LTR
2307
+ this.next();
2308
+ } else {
2309
+ // RTL
2310
+ this.prev();
2311
+ }
2312
+ };
2313
+
2314
+ // rightmost()
2315
+ //______________________________________________________________________________
2316
+ // Flip to the rightmost page
2317
+ BookReader.prototype.rightmost = function() {
2318
+ if ('rl' != this.pageProgression) {
2319
+ this.last();
2320
+ } else {
2321
+ this.first();
2322
+ }
2323
+ };
2324
+
2325
+ // left()
2326
+ //______________________________________________________________________________
2327
+ // Flip the left page over onto the right.
2328
+ BookReader.prototype.left = function() {
2329
+ if ('rl' != this.pageProgression) {
2330
+ // LTR
2331
+ this.prev();
2332
+ } else {
2333
+ // RTL
2334
+ this.next();
2335
+ }
2336
+ };
2337
+
2338
+ // leftmost()
2339
+ //______________________________________________________________________________
2340
+ // Flip to the leftmost page
2341
+ BookReader.prototype.leftmost = function() {
2342
+ if ('rl' != this.pageProgression) {
2343
+ this.first();
2344
+ } else {
2345
+ this.last();
2346
+ }
2347
+ };
2348
+
2349
+ // next()
2350
+ //______________________________________________________________________________
2351
+ BookReader.prototype.next = function() {
2352
+ if (this.constMode2up == this.mode) {
2353
+ this.autoStop();
2354
+ this.flipFwdToIndex(null);
2355
+
2356
+ } else {
2357
+ if (this.firstIndex < this.lastDisplayableIndex()) {
2358
+ this.jumpToIndex(this.firstIndex+1);
2359
+ }
2360
+ }
2361
+ };
2362
+
2363
+ // prev()
2364
+ //______________________________________________________________________________
2365
+ BookReader.prototype.prev = function() {
2366
+ if (this.constMode2up == this.mode) {
2367
+ this.autoStop();
2368
+ this.flipBackToIndex(null);
2369
+ } else {
2370
+ if (this.firstIndex >= 1) {
2371
+ this.jumpToIndex(this.firstIndex-1);
2372
+ }
2373
+ }
2374
+ };
2375
+
2376
+ BookReader.prototype.first = function() {
2377
+ this.jumpToIndex(this.firstDisplayableIndex());
2378
+ };
2379
+
2380
+ BookReader.prototype.last = function() {
2381
+ this.jumpToIndex(this.lastDisplayableIndex());
2382
+ };
2383
+
2384
+ // scrollDown()
2385
+ //______________________________________________________________________________
2386
+ // Scrolls down one screen view
2387
+ BookReader.prototype.scrollDown = function() {
2388
+ if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) {
2389
+ if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) {
2390
+ // Whole pages are visible, scroll whole page only
2391
+ return this.next();
2392
+ }
2393
+
2394
+ this.refs.$brContainer.stop(true).animate(
2395
+ { scrollTop: '+=' + this._scrollAmount() + 'px'},
2396
+ 400, 'easeInOutExpo'
2397
+ );
2398
+ return true;
2399
+ } else {
2400
+ return false;
2401
+ }
2402
+ };
2403
+
2404
+ // scrollUp()
2405
+ //______________________________________________________________________________
2406
+ // Scrolls up one screen view
2407
+ BookReader.prototype.scrollUp = function() {
2408
+ if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) {
2409
+ if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) {
2410
+ // Whole pages are visible, scroll whole page only
2411
+ return this.prev();
2412
+ }
2413
+
2414
+ this.refs.$brContainer.stop(true).animate(
2415
+ { scrollTop: '-=' + this._scrollAmount() + 'px'},
2416
+ 400, 'easeInOutExpo'
2417
+ );
2418
+ return true;
2419
+ } else {
2420
+ return false;
2421
+ }
2422
+ };
2423
+
2424
+ // _scrollAmount()
2425
+ //______________________________________________________________________________
2426
+ // The amount to scroll vertically in integer pixels
2427
+ BookReader.prototype._scrollAmount = function() {
2428
+ if (this.constMode1up == this.mode) {
2429
+ // Overlap by % of page size
2430
+ return parseInt(this.refs.$brContainer.prop('clientHeight') - this.getPageHeight(this.currentIndex()) / this.reduce * 0.03);
2431
+ }
2432
+
2433
+ return parseInt(0.9 * this.refs.$brContainer.prop('clientHeight'));
2434
+ };
2435
+
2436
+
2437
+ // flipBackToIndex()
2438
+ //______________________________________________________________________________
2439
+ // to flip back one spread, pass index=null
2440
+ BookReader.prototype.flipBackToIndex = function(index) {
2441
+
2442
+ if (this.constMode1up == this.mode) return;
2443
+
2444
+ var leftIndex = this.twoPage.currentIndexL;
2445
+
2446
+ if (this.animating) return;
2447
+
2448
+ if (null != this.leafEdgeTmp) {
2449
+ alert('error: leafEdgeTmp should be null!');
2450
+ return;
2451
+ }
2452
+
2453
+ if (null == index) {
2454
+ index = leftIndex-2;
2455
+ }
2456
+
2457
+ this.updateNavIndex(index);
2458
+
2459
+ var previousIndices = this.getSpreadIndices(index);
2460
+
2461
+ if (previousIndices[0] < this.firstDisplayableIndex() || previousIndices[1] < this.firstDisplayableIndex()) {
2462
+ return;
2463
+ }
2464
+
2465
+ this.animating = true;
2466
+
2467
+ if ('rl' != this.pageProgression) {
2468
+ // Assume LTR and we are going backward
2469
+ this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);
2470
+ this.flipLeftToRight(previousIndices[0], previousIndices[1]);
2471
+ } else {
2472
+ // RTL and going backward
2473
+ var gutter = this.prepareFlipRightToLeft(previousIndices[0], previousIndices[1]);
2474
+ this.flipRightToLeft(previousIndices[0], previousIndices[1], gutter);
2475
+ }
2476
+ };
2477
+
2478
+ // flipLeftToRight()
2479
+ //______________________________________________________________________________
2480
+ // Flips the page on the left towards the page on the right
2481
+ BookReader.prototype.flipLeftToRight = function(newIndexL, newIndexR) {
2482
+
2483
+ var leftLeaf = this.twoPage.currentIndexL;
2484
+
2485
+ var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
2486
+ var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);
2487
+ var leafEdgeTmpW = oldLeafEdgeWidthL - newLeafEdgeWidthL;
2488
+
2489
+ var currWidthL = this.getPageWidth2UP(leftLeaf);
2490
+ var newWidthL = this.getPageWidth2UP(newIndexL);
2491
+ var newWidthR = this.getPageWidth2UP(newIndexR);
2492
+
2493
+ var top = this.twoPageTop();
2494
+ var gutter = this.twoPage.middle + this.gutterOffsetForIndex(newIndexL);
2495
+
2496
+ //animation strategy:
2497
+ // 0. remove search highlight, if any.
2498
+ // 1. create a new div, called leafEdgeTmp to represent the leaf edge between the leftmost edge
2499
+ // of the left leaf and where the user clicked in the leaf edge.
2500
+ // Note that if this function was triggered by left() and not a
2501
+ // mouse click, the width of leafEdgeTmp is very small (zero px).
2502
+ // 2. animate both leafEdgeTmp to the gutter (without changing its width) and animate
2503
+ // leftLeaf to width=0.
2504
+ // 3. When step 2 is finished, animate leafEdgeTmp to right-hand side of new right leaf
2505
+ // (left=gutter+newWidthR) while also animating the new right leaf from width=0 to
2506
+ // its new full width.
2507
+ // 4. After step 3 is finished, do the following:
2508
+ // - remove leafEdgeTmp from the dom.
2509
+ // - resize and move the right leaf edge (leafEdgeR) to left=gutter+newWidthR
2510
+ // and width=twoPage.edgeWidth-newLeafEdgeWidthL.
2511
+ // - resize and move the left leaf edge (leafEdgeL) to left=gutter-newWidthL-newLeafEdgeWidthL
2512
+ // and width=newLeafEdgeWidthL.
2513
+ // - resize the back cover (twoPage.coverDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
2514
+ // and width=newWidthL+newWidthR+twoPage.edgeWidth+20
2515
+ // - move new left leaf (newIndexL) forward to zindex=2 so it can receive clicks.
2516
+ // - remove old left and right leafs from the dom [pruneUnusedImgs()].
2517
+ // - prefetch new adjacent leafs.
2518
+ // - set up click handlers for both new left and right leafs.
2519
+ // - redraw the search highlight.
2520
+ // - update the pagenum box and the url.
2521
+
2522
+ var $twoPageViewEl = this.refs.$brTwoPageView;
2523
+ var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW;
2524
+
2525
+ this.leafEdgeTmp = document.createElement('div');
2526
+ this.leafEdgeTmp.className = 'BRleafEdgeTmp';
2527
+ $(this.leafEdgeTmp).css({
2528
+ width: leafEdgeTmpW + 'px',
2529
+ height: this.twoPage.height + 'px',
2530
+ left: leftEdgeTmpLeft + 'px',
2531
+ top: top+'px',
2532
+ zIndex:1000
2533
+ }).appendTo($twoPageViewEl);
2534
+
2535
+ $(this.leafEdgeL).css({
2536
+ width: newLeafEdgeWidthL+'px',
2537
+ left: gutter-currWidthL-newLeafEdgeWidthL+'px'
2538
+ });
2539
+
2540
+ // Left gets the offset of the current left leaf from the document
2541
+ var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
2542
+ // $$$ This seems very similar to the gutter. May be able to consolidate the logic.
2543
+ var right = $twoPageViewEl.prop('clientWidth') - left - $(this.prefetchedImgs[leftLeaf]).width() + $twoPageViewEl.offset().left - 2 + 'px';
2544
+
2545
+ // We change the left leaf to right positioning
2546
+ // $$$ This causes animation glitches during resize. See https://bugs.edge.launchpad.net/gnubook/+bug/328327
2547
+ $(this.prefetchedImgs[leftLeaf]).css({
2548
+ right: right,
2549
+ left: ''
2550
+ });
2551
+
2552
+ $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');
2553
+
2554
+ var self = this;
2555
+
2556
+ if (this.enableSearch) this.removeSearchHilites();
2557
+
2558
+ $(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, self.flipSpeed, 'easeInSine', function() {
2559
+
2560
+ $(self.leafEdgeTmp).animate({left: gutter+newWidthR+'px'}, self.flipSpeed, 'easeOutSine');
2561
+
2562
+ self.$('.BRgutter').css({left: (gutter - self.twoPage.bookSpineDivWidth*0.5)+'px'});
2563
+
2564
+ $(self.prefetchedImgs[newIndexR]).animate({width: newWidthR+'px'}, self.flipSpeed, 'easeOutSine', function() {
2565
+ $(self.prefetchedImgs[newIndexL]).css('zIndex', 2);
2566
+
2567
+ //jquery adds display:block to the element style, which interferes with our print css
2568
+ $(self.prefetchedImgs[newIndexL]).css('display', '');
2569
+ $(self.prefetchedImgs[newIndexR]).css('display', '');
2570
+
2571
+ $(self.leafEdgeR).css({
2572
+ // Moves the right leaf edge
2573
+ width: self.twoPage.edgeWidth-newLeafEdgeWidthL+'px',
2574
+ left: gutter+newWidthR+'px'
2575
+ });
2576
+
2577
+ $(self.leafEdgeL).css({
2578
+ // Moves and resizes the left leaf edge
2579
+ width: newLeafEdgeWidthL+'px',
2580
+ left: gutter-newWidthL-newLeafEdgeWidthL+'px'
2581
+ });
2582
+
2583
+ // Resizes the brown border div
2584
+ $(self.twoPage.coverDiv).css({
2585
+ width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
2586
+ left: gutter-newWidthL-newLeafEdgeWidthL-self.twoPage.coverInternalPadding+'px'
2587
+ });
2588
+
2589
+ $(self.leafEdgeTmp).remove();
2590
+ self.leafEdgeTmp = null;
2591
+
2592
+ // $$$ TODO refactor with opposite direction flip
2593
+
2594
+ self.twoPage.currentIndexL = newIndexL;
2595
+ self.twoPage.currentIndexR = newIndexR;
2596
+ self.twoPage.scaledWL = newWidthL;
2597
+ self.twoPage.scaledWR = newWidthR;
2598
+ self.twoPage.gutter = gutter;
2599
+
2600
+ self.firstIndex = self.twoPage.currentIndexL;
2601
+ self.displayedIndices = [newIndexL, newIndexR];
2602
+ self.pruneUnusedImgs();
2603
+ self.prefetch();
2604
+ self.animating = false;
2605
+
2606
+ if (self.enableSearch) self.updateSearchHilites2UP();
2607
+ self.updatePageNumBox2UP();
2608
+
2609
+ self.setMouseHandlers2UP();
2610
+ self.twoPageSetCursor();
2611
+
2612
+ if (self.animationFinishedCallback) {
2613
+ self.animationFinishedCallback();
2614
+ self.animationFinishedCallback = null;
2615
+ }
2616
+ });
2617
+ });
2618
+
2619
+ };
2620
+
2621
+ // flipFwdToIndex()
2622
+ //______________________________________________________________________________
2623
+ // Whether we flip left or right is dependent on the page progression
2624
+ // to flip forward one spread, pass index=null
2625
+ BookReader.prototype.flipFwdToIndex = function(index) {
2626
+
2627
+ if (this.animating) return;
2628
+
2629
+ if (null != this.leafEdgeTmp) {
2630
+ alert('error: leafEdgeTmp should be null!');
2631
+ return;
2632
+ }
2633
+
2634
+ if (null == index) {
2635
+ index = this.twoPage.currentIndexR+2; // $$$ assumes indices are continuous
2636
+ }
2637
+ if (index > this.lastDisplayableIndex()) return;
2638
+
2639
+ this.updateNavIndex(index);
2640
+
2641
+ this.animating = true;
2642
+
2643
+ var nextIndices = this.getSpreadIndices(index);
2644
+
2645
+ if ('rl' != this.pageProgression) {
2646
+ // We did not specify RTL
2647
+ var gutter = this.prepareFlipRightToLeft(nextIndices[0], nextIndices[1]);
2648
+ this.flipRightToLeft(nextIndices[0], nextIndices[1], gutter);
2649
+ } else {
2650
+ // RTL
2651
+ var gutter = this.prepareFlipLeftToRight(nextIndices[0], nextIndices[1]);
2652
+ this.flipLeftToRight(nextIndices[0], nextIndices[1]);
2653
+ }
2654
+ };
2655
+
2656
+ // flipRightToLeft(nextL, nextR, gutter)
2657
+ // $$$ better not to have to pass gutter in
2658
+ //______________________________________________________________________________
2659
+ // Flip from left to right and show the nextL and nextR indices on those sides
2660
+ BookReader.prototype.flipRightToLeft = function(newIndexL, newIndexR) {
2661
+ var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
2662
+ var oldLeafEdgeWidthR = this.twoPage.edgeWidth-oldLeafEdgeWidthL;
2663
+ var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);
2664
+ var newLeafEdgeWidthR = this.twoPage.edgeWidth-newLeafEdgeWidthL;
2665
+
2666
+ var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
2667
+
2668
+ var top = this.twoPageTop();
2669
+ var scaledW = this.getPageWidth2UP(this.twoPage.currentIndexR);
2670
+
2671
+ var middle = this.twoPage.middle;
2672
+ var gutter = middle + this.gutterOffsetForIndex(newIndexL);
2673
+
2674
+ var $twoPageViewEl = this.refs.$brTwoPageView;
2675
+
2676
+ this.leafEdgeTmp = document.createElement('div');
2677
+ this.leafEdgeTmp.className = 'BRleafEdgeTmp';
2678
+ $(this.leafEdgeTmp).css({
2679
+ width: leafEdgeTmpW + 'px',
2680
+ height: this.twoPage.height + 'px',
2681
+ left: gutter+scaledW+'px',
2682
+ top: top+'px',
2683
+ zIndex:1000
2684
+ }).appendTo($twoPageViewEl);
2685
+
2686
+ var currWidthL = this.getPageWidth2UP(this.twoPage.currentIndexL);
2687
+ var currWidthR = this.getPageWidth2UP(this.twoPage.currentIndexR);
2688
+ var newWidthL = this.getPageWidth2UP(newIndexL);
2689
+ var newWidthR = this.getPageWidth2UP(newIndexR);
2690
+
2691
+ $(this.leafEdgeR).css({width: newLeafEdgeWidthR+'px', left: gutter+newWidthR+'px' });
2692
+
2693
+ var self = this; // closure-tastic!
2694
+
2695
+ var speed = this.flipSpeed;
2696
+
2697
+ if (this.enableSearch) this.removeSearchHilites();
2698
+
2699
+ $(this.leafEdgeTmp).animate({left: gutter}, speed, 'easeInSine');
2700
+ $(this.prefetchedImgs[this.twoPage.currentIndexR]).animate({width: '0px'}, speed, 'easeInSine', function() {
2701
+ self.$('BRgutter').css({left: (gutter - self.twoPage.bookSpineDivWidth*0.5)+'px'});
2702
+ $(self.leafEdgeTmp).animate({left: gutter-newWidthL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');
2703
+ $(self.prefetchedImgs[newIndexL]).animate({width: newWidthL+'px'}, speed, 'easeOutSine', function() {
2704
+ $(self.prefetchedImgs[newIndexR]).css('zIndex', 2);
2705
+
2706
+ //jquery adds display:block to the element style, which interferes with our print css
2707
+ $(self.prefetchedImgs[newIndexL]).css('display', '');
2708
+ $(self.prefetchedImgs[newIndexR]).css('display', '');
2709
+
2710
+ $(self.leafEdgeL).css({
2711
+ width: newLeafEdgeWidthL+'px',
2712
+ left: gutter-newWidthL-newLeafEdgeWidthL+'px'
2713
+ });
2714
+
2715
+ // Resizes the book cover
2716
+ $(self.twoPage.coverDiv).css({
2717
+ width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
2718
+ left: gutter - newWidthL - newLeafEdgeWidthL - self.twoPage.coverInternalPadding + 'px'
2719
+ });
2720
+
2721
+ $(self.leafEdgeTmp).remove();
2722
+ self.leafEdgeTmp = null;
2723
+
2724
+ self.twoPage.currentIndexL = newIndexL;
2725
+ self.twoPage.currentIndexR = newIndexR;
2726
+ self.twoPage.scaledWL = newWidthL;
2727
+ self.twoPage.scaledWR = newWidthR;
2728
+ self.twoPage.gutter = gutter;
2729
+
2730
+ self.firstIndex = self.twoPage.currentIndexL;
2731
+ self.displayedIndices = [newIndexL, newIndexR];
2732
+ self.pruneUnusedImgs();
2733
+ self.prefetch();
2734
+ self.animating = false;
2735
+
2736
+
2737
+ if (self.enableSearch) self.updateSearchHilites2UP();
2738
+ self.updatePageNumBox2UP();
2739
+
2740
+ self.setMouseHandlers2UP();
2741
+ self.twoPageSetCursor();
2742
+
2743
+ if (self.animationFinishedCallback) {
2744
+ self.animationFinishedCallback();
2745
+ self.animationFinishedCallback = null;
2746
+ }
2747
+ });
2748
+ });
2749
+ };
2750
+
2751
+ // setMouseHandlers2UP
2752
+ //______________________________________________________________________________
2753
+ BookReader.prototype.setMouseHandlers2UP = function() {
2754
+ this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexL],
2755
+ { self: this },
2756
+ function(e) {
2757
+ if (e.which == 3) {
2758
+ // right click
2759
+ if (e.data.self.protected) {
2760
+ return false;
2761
+ }
2762
+ return true;
2763
+ }
2764
+
2765
+ if (! e.data.self.twoPageIsZoomedIn()) {
2766
+ e.data.self.trigger(BookReader.eventNames.stop);
2767
+ e.data.self.left();
2768
+ }
2769
+ e.preventDefault();
2770
+ }
2771
+ );
2772
+
2773
+ this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexR],
2774
+ { self: this },
2775
+ function(e) {
2776
+ if (e.which == 3) {
2777
+ // right click
2778
+ return !e.data.self.protected;
2779
+ }
2780
+
2781
+ if (! e.data.self.twoPageIsZoomedIn()) {
2782
+ e.data.self.trigger(BookReader.eventNames.stop);
2783
+ e.data.self.right();
2784
+ }
2785
+ e.preventDefault();
2786
+ }
2787
+ );
2788
+ };
2789
+
2790
+ // prefetchImg()
2791
+ //______________________________________________________________________________
2792
+ BookReader.prototype.prefetchImg = function(index) {
2793
+ var pageURI = this._getPageURI(index);
2794
+
2795
+ // Load image if not loaded or URI has changed (e.g. due to scaling)
2796
+ var loadImage = false;
2797
+ if (undefined == this.prefetchedImgs[index]) {
2798
+ loadImage = true;
2799
+ } else if (pageURI != this.prefetchedImgs[index].uri) {
2800
+ loadImage = true;
2801
+ }
2802
+
2803
+ if (loadImage) {
2804
+ var img = document.createElement("img");
2805
+ $(img).addClass('BRpageimage').addClass('BRnoselect');
2806
+ if (index < 0 || index > (this.getNumLeafs() - 1) ) {
2807
+ // Facing page at beginning or end, or beyond
2808
+ $(img).addClass('BRemptypage');
2809
+ }
2810
+ img.src = pageURI;
2811
+ img.uri = pageURI; // browser may rewrite src so we stash raw URI here
2812
+ this.prefetchedImgs[index] = img;
2813
+ }
2814
+ };
2815
+
2816
+
2817
+ // prepareFlipLeftToRight()
2818
+ //
2819
+ //______________________________________________________________________________
2820
+ //
2821
+ // Prepare to flip the left page towards the right. This corresponds to moving
2822
+ // backward when the page progression is left to right.
2823
+ BookReader.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
2824
+ this.prefetchImg(prevL);
2825
+ this.prefetchImg(prevR);
2826
+
2827
+ var height = this._getPageHeight(prevL);
2828
+ var width = this._getPageWidth(prevL);
2829
+ var middle = this.twoPage.middle;
2830
+ var top = this.twoPageTop();
2831
+ var scaledW = this.twoPage.height*width/height; // $$$ assumes height of page is dominant
2832
+
2833
+ // The gutter is the dividing line between the left and right pages.
2834
+ // It is offset from the middle to create the illusion of thickness to the pages
2835
+ var gutter = middle + this.gutterOffsetForIndex(prevL);
2836
+
2837
+ var leftCSS = {
2838
+ position: 'absolute',
2839
+ left: gutter-scaledW+'px',
2840
+ right: '', // clear right property
2841
+ top: top+'px',
2842
+ height: this.twoPage.height,
2843
+ width: scaledW+'px',
2844
+ zIndex: 1
2845
+ };
2846
+
2847
+ $(this.prefetchedImgs[prevL]).css(leftCSS);
2848
+
2849
+ var $twoPageViewEl = this.refs.$brTwoPageView;
2850
+ $twoPageViewEl.append(this.prefetchedImgs[prevL]);
2851
+
2852
+ var rightCSS = {
2853
+ position: 'absolute',
2854
+ left: gutter+'px',
2855
+ right: '',
2856
+ top: top+'px',
2857
+ height: this.twoPage.height,
2858
+ width: '0',
2859
+ zIndex: 2
2860
+ };
2861
+
2862
+ $(this.prefetchedImgs[prevR]).css(rightCSS);
2863
+
2864
+ $twoPageViewEl.append(this.prefetchedImgs[prevR]);
2865
+ };
2866
+
2867
+ // $$$ mang we're adding an extra pixel in the middle. See https://bugs.edge.launchpad.net/gnubook/+bug/411667
2868
+ // prepareFlipRightToLeft()
2869
+ //______________________________________________________________________________
2870
+ BookReader.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
2871
+ // Prefetch images
2872
+ this.prefetchImg(nextL);
2873
+ this.prefetchImg(nextR);
2874
+
2875
+ var height = this._getPageHeight(nextR);
2876
+ var width = this._getPageWidth(nextR);
2877
+ var middle = this.twoPage.middle;
2878
+ var top = this.twoPageTop();
2879
+ var scaledW = this.twoPage.height*width/height;
2880
+
2881
+ var gutter = middle + this.gutterOffsetForIndex(nextL);
2882
+
2883
+ $(this.prefetchedImgs[nextR]).css({
2884
+ position: 'absolute',
2885
+ left: gutter+'px',
2886
+ top: top+'px',
2887
+ height: this.twoPage.height,
2888
+ width: scaledW+'px',
2889
+ zIndex: 1
2890
+ });
2891
+
2892
+ var $twoPageViewEl = this.refs.$brTwoPageView;
2893
+ $twoPageViewEl.append(this.prefetchedImgs[nextR]);
2894
+
2895
+ height = this._getPageHeight(nextL);
2896
+ width = this._getPageWidth(nextL);
2897
+ scaledW = this.twoPage.height*width/height;
2898
+
2899
+ $(this.prefetchedImgs[nextL]).css({
2900
+ position: 'absolute',
2901
+ right: $twoPageViewEl.prop('clientWidth')-gutter+'px',
2902
+ top: top+'px',
2903
+ height: this.twoPage.height,
2904
+ width: 0+'px', // Start at 0 width, then grow to the left
2905
+ zIndex: 2
2906
+ });
2907
+
2908
+ $twoPageViewEl.append(this.prefetchedImgs[nextL]);
2909
+ };
2910
+
2911
+ // getNextLeafs() -- NOT RTL AWARE
2912
+ //______________________________________________________________________________
2913
+ // BookReader.prototype.getNextLeafs = function(o) {
2914
+ // //TODO: we might have two left or two right leafs in a row (damaged book)
2915
+ // //For now, assume that leafs are contiguous.
2916
+ //
2917
+ // //return [this.twoPage.currentIndexL+2, this.twoPage.currentIndexL+3];
2918
+ // o.L = this.twoPage.currentIndexL+2;
2919
+ // o.R = this.twoPage.currentIndexL+3;
2920
+ // }
2921
+
2922
+ // getprevLeafs() -- NOT RTL AWARE
2923
+ //______________________________________________________________________________
2924
+ // BookReader.prototype.getPrevLeafs = function(o) {
2925
+ // //TODO: we might have two left or two right leafs in a row (damaged book)
2926
+ // //For now, assume that leafs are contiguous.
2927
+ //
2928
+ // //return [this.twoPage.currentIndexL-2, this.twoPage.currentIndexL-1];
2929
+ // o.L = this.twoPage.currentIndexL-2;
2930
+ // o.R = this.twoPage.currentIndexL-1;
2931
+ // }
2932
+
2933
+ // pruneUnusedImgs()
2934
+ //______________________________________________________________________________
2935
+ BookReader.prototype.pruneUnusedImgs = function() {
2936
+ for (var key in this.prefetchedImgs) {
2937
+ if ((key != this.twoPage.currentIndexL) && (key != this.twoPage.currentIndexR)) {
2938
+ $(this.prefetchedImgs[key]).remove();
2939
+ }
2940
+ if ((key < this.twoPage.currentIndexL-4) || (key > this.twoPage.currentIndexR+4)) {
2941
+ delete this.prefetchedImgs[key];
2942
+ }
2943
+ }
2944
+ };
2945
+
2946
+ // prefetch()
2947
+ //______________________________________________________________________________
2948
+ BookReader.prototype.prefetch = function() {
2949
+ // $$$ We should check here if the current indices have finished
2950
+ // loading (with some timeout) before loading more page images
2951
+ // See https://bugs.edge.launchpad.net/bookreader/+bug/511391
2952
+
2953
+ // prefetch visible pages first
2954
+ this.prefetchImg(this.twoPage.currentIndexL);
2955
+ this.prefetchImg(this.twoPage.currentIndexR);
2956
+
2957
+ var adjacentPagesToLoad = 3;
2958
+
2959
+ var lowCurrent = Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
2960
+ var highCurrent = Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
2961
+
2962
+ var start = Math.max(lowCurrent - adjacentPagesToLoad, 0);
2963
+ var end = Math.min(highCurrent + adjacentPagesToLoad, this.getNumLeafs() - 1);
2964
+
2965
+ // Load images spreading out from current
2966
+ for (var i = 1; i <= adjacentPagesToLoad; i++) {
2967
+ var goingDown = lowCurrent - i;
2968
+ if (goingDown >= start) {
2969
+ this.prefetchImg(goingDown);
2970
+ }
2971
+ var goingUp = highCurrent + i;
2972
+ if (goingUp <= end) {
2973
+ this.prefetchImg(goingUp);
2974
+ }
2975
+ }
2976
+ };
2977
+
2978
+ // getPageWidth2UP()
2979
+ //______________________________________________________________________________
2980
+ BookReader.prototype.getPageWidth2UP = function(index) {
2981
+ // We return the width based on the dominant height
2982
+ var height = this._getPageHeight(index);
2983
+ var width = this._getPageWidth(index);
2984
+ return Math.floor(this.twoPage.height*width/height); // $$$ we assume width is relative to current spread
2985
+ };
2986
+
2987
+ // twoPageGutter()
2988
+ //______________________________________________________________________________
2989
+ // Returns the position of the gutter (line between the page images)
2990
+ BookReader.prototype.twoPageGutter = function() {
2991
+ return this.twoPage.middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
2992
+ };
2993
+
2994
+ // twoPageTop()
2995
+ //______________________________________________________________________________
2996
+ // Returns the offset for the top of the page images
2997
+ BookReader.prototype.twoPageTop = function() {
2998
+ return this.twoPage.coverExternalPadding + this.twoPage.coverInternalPadding; // $$$ + border?
2999
+ };
3000
+
3001
+ // twoPageCoverWidth()
3002
+ //______________________________________________________________________________
3003
+ // Returns the width of the cover div given the total page width
3004
+ BookReader.prototype.twoPageCoverWidth = function(totalPageWidth) {
3005
+ return totalPageWidth + this.twoPage.edgeWidth + 2*this.twoPage.coverInternalPadding;
3006
+ };
3007
+
3008
+ // twoPageGetViewCenter()
3009
+ //______________________________________________________________________________
3010
+ // Returns the percentage offset into twopageview div at the center of container div
3011
+ // { percentageX: float, percentageY: float }
3012
+ BookReader.prototype.twoPageGetViewCenter = function() {
3013
+ var center = {};
3014
+
3015
+ var containerOffset = this.refs.$brContainer.offset();
3016
+ var viewOffset = this.refs.$brTwoPageView.offset();
3017
+ center.percentageX = (containerOffset.left - viewOffset.left + (this.refs.$brContainer.prop('clientWidth') >> 1)) / this.twoPage.totalWidth;
3018
+ center.percentageY = (containerOffset.top - viewOffset.top + (this.refs.$brContainer.prop('clientHeight') >> 1)) / this.twoPage.totalHeight;
3019
+
3020
+ return center;
3021
+ };
3022
+
3023
+ // twoPageCenterView(percentageX, percentageY)
3024
+ //______________________________________________________________________________
3025
+ // Centers the point given by percentage from left,top of twopageview
3026
+ BookReader.prototype.twoPageCenterView = function(percentageX, percentageY) {
3027
+ if ('undefined' == typeof(percentageX)) {
3028
+ percentageX = 0.5;
3029
+ }
3030
+ if ('undefined' == typeof(percentageY)) {
3031
+ percentageY = 0.5;
3032
+ }
3033
+
3034
+ var viewWidth = this.refs.$brTwoPageView.width();
3035
+ var containerClientWidth = this.refs.$brContainer.prop('clientWidth');
3036
+ var intoViewX = percentageX * viewWidth;
3037
+
3038
+ var viewHeight = this.refs.$brTwoPageView.height();
3039
+ var containerClientHeight = this.refs.$brContainer.prop('clientHeight');
3040
+ var intoViewY = percentageY * viewHeight;
3041
+
3042
+ if (viewWidth < containerClientWidth) {
3043
+ // Can fit width without scrollbars - center by adjusting offset
3044
+ this.refs.$brTwoPageView.css('left', (containerClientWidth >> 1) - intoViewX + 'px');
3045
+ } else {
3046
+ // Need to scroll to center
3047
+ this.refs.$brTwoPageView.css('left', 0);
3048
+ this.refs.$brContainer.scrollLeft(intoViewX - (containerClientWidth >> 1));
3049
+ }
3050
+
3051
+ if (viewHeight < containerClientHeight) {
3052
+ // Fits with scrollbars - add offset
3053
+ this.refs.$brTwoPageView.css('top', (containerClientHeight >> 1) - intoViewY + 'px');
3054
+ } else {
3055
+ this.refs.$brTwoPageView.css('top', 0);
3056
+ this.refs.$brContainer.scrollTop(intoViewY - (containerClientHeight >> 1));
3057
+ }
3058
+ };
3059
+
3060
+ // twoPageFlipAreaHeight
3061
+ //______________________________________________________________________________
3062
+ // Returns the integer height of the click-to-flip areas at the edges of the book
3063
+ BookReader.prototype.twoPageFlipAreaHeight = function() {
3064
+ return parseInt(this.twoPage.height);
3065
+ };
3066
+
3067
+ // twoPageFlipAreaWidth
3068
+ //______________________________________________________________________________
3069
+ // Returns the integer width of the flip areas
3070
+ BookReader.prototype.twoPageFlipAreaWidth = function() {
3071
+ var max = 100; // $$$ TODO base on view width?
3072
+ var min = 10;
3073
+
3074
+ var width = this.twoPage.width * 0.15;
3075
+ return parseInt(BookReader.util.clamp(width, min, max));
3076
+ };
3077
+
3078
+ // twoPageFlipAreaTop
3079
+ //______________________________________________________________________________
3080
+ // Returns integer top offset for flip areas
3081
+ BookReader.prototype.twoPageFlipAreaTop = function() {
3082
+ return parseInt(this.twoPage.bookCoverDivTop + this.twoPage.coverInternalPadding);
3083
+ };
3084
+
3085
+ // twoPageLeftFlipAreaLeft
3086
+ //______________________________________________________________________________
3087
+ // Left offset for left flip area
3088
+ BookReader.prototype.twoPageLeftFlipAreaLeft = function() {
3089
+ return parseInt(this.twoPage.gutter - this.twoPage.scaledWL);
3090
+ };
3091
+
3092
+ // twoPageRightFlipAreaLeft
3093
+ //______________________________________________________________________________
3094
+ // Left offset for right flip area
3095
+ BookReader.prototype.twoPageRightFlipAreaLeft = function() {
3096
+ return parseInt(this.twoPage.gutter + this.twoPage.scaledWR - this.twoPageFlipAreaWidth());
3097
+ };
3098
+
3099
+ // setHilightCss2UP()
3100
+ //______________________________________________________________________________
3101
+ //position calculation shared between search and text-to-speech functions
3102
+ BookReader.prototype.setHilightCss2UP = function(div, index, left, right, top, bottom) {
3103
+ // We calculate the reduction factor for the specific page because it can be different
3104
+ // for each page in the spread
3105
+ var height = this._getPageHeight(index);
3106
+ var width = this._getPageWidth(index);
3107
+ var reduce = this.twoPage.height/height;
3108
+ var scaledW = parseInt(width*reduce);
3109
+
3110
+ var gutter = this.twoPageGutter();
3111
+ var pageL;
3112
+ if ('L' == this.getPageSide(index)) {
3113
+ pageL = gutter-scaledW;
3114
+ } else {
3115
+ pageL = gutter;
3116
+ }
3117
+ var pageT = this.twoPageTop();
3118
+
3119
+ $(div).css({
3120
+ width: (right-left)*reduce + 'px',
3121
+ height: (bottom-top)*reduce + 'px',
3122
+ left: pageL+left*reduce + 'px',
3123
+ top: pageT+top*reduce +'px'
3124
+ });
3125
+ };
3126
+
3127
+
3128
+ // autoToggle()
3129
+ //______________________________________________________________________________
3130
+ BookReader.prototype.autoToggle = function() {
3131
+
3132
+ this.trigger(BookReader.eventNames.stop);
3133
+
3134
+ var bComingFrom1up = false;
3135
+ if (2 != this.mode) {
3136
+ bComingFrom1up = true;
3137
+ this.switchMode(this.constMode2up);
3138
+ }
3139
+
3140
+ // Change to autofit if book is too large
3141
+ if (this.reduce < this.twoPageGetAutofitReduce()) {
3142
+ this.zoom2up('auto');
3143
+ }
3144
+
3145
+ var self = this;
3146
+ if (null == this.autoTimer) {
3147
+ this.flipSpeed = 2000;
3148
+
3149
+ // $$$ Draw events currently cause layout problems when they occur during animation.
3150
+ // There is a specific problem when changing from 1-up immediately to autoplay in RTL so
3151
+ // we workaround for now by not triggering immediate animation in that case.
3152
+ // See https://bugs.launchpad.net/gnubook/+bug/328327
3153
+ if (('rl' == this.pageProgression) && bComingFrom1up) {
3154
+ // don't flip immediately -- wait until timer fires
3155
+ } else {
3156
+ // flip immediately
3157
+ this.flipFwdToIndex();
3158
+ }
3159
+
3160
+ this.$('.play').hide();
3161
+ this.$('.pause').show();
3162
+ this.autoTimer=setInterval(function(){
3163
+ if (self.animating) {return;}
3164
+
3165
+ if (Math.max(self.twoPage.currentIndexL, self.twoPage.currentIndexR) >= self.lastDisplayableIndex()) {
3166
+ self.flipBackToIndex(1); // $$$ really what we want?
3167
+ } else {
3168
+ self.flipFwdToIndex();
3169
+ }
3170
+ },5000);
3171
+ } else {
3172
+ this.autoStop();
3173
+ }
3174
+ };
3175
+
3176
+ // autoStop()
3177
+ //______________________________________________________________________________
3178
+ // Stop autoplay mode, allowing animations to finish
3179
+ BookReader.prototype.autoStop = function() {
3180
+ if (null != this.autoTimer) {
3181
+ clearInterval(this.autoTimer);
3182
+ this.flipSpeed = 'fast';
3183
+ this.$('.pause').hide();
3184
+ this.$('.play').show();
3185
+ this.autoTimer = null;
3186
+ }
3187
+ };
3188
+
3189
+ // stopFlipAnimations
3190
+ //______________________________________________________________________________
3191
+ // Immediately stop flip animations. Callbacks are triggered.
3192
+ BookReader.prototype.stopFlipAnimations = function() {
3193
+
3194
+ this.autoStop(); // Clear timers
3195
+
3196
+ // Stop animation, clear queue, trigger callbacks
3197
+ if (this.leafEdgeTmp) {
3198
+ $(this.leafEdgeTmp).stop(false, true);
3199
+ }
3200
+ jQuery.each(this.prefetchedImgs, function() {
3201
+ $(this).stop(false, true);
3202
+ });
3203
+
3204
+ // And again since animations also queued in callbacks
3205
+ if (this.leafEdgeTmp) {
3206
+ $(this.leafEdgeTmp).stop(false, true);
3207
+ }
3208
+ jQuery.each(this.prefetchedImgs, function() {
3209
+ $(this).stop(false, true);
3210
+ });
3211
+
3212
+ };
3213
+
3214
+ // keyboardNavigationIsDisabled(event)
3215
+ // - returns true if keyboard navigation should be disabled for the event
3216
+ //______________________________________________________________________________
3217
+ BookReader.prototype.keyboardNavigationIsDisabled = function(event) {
3218
+ return event.target.tagName == "INPUT";
3219
+ };
3220
+
3221
+ // gutterOffsetForIndex
3222
+ //______________________________________________________________________________
3223
+ //
3224
+ // Returns the gutter offset for the spread containing the given index.
3225
+ // This function supports RTL
3226
+ BookReader.prototype.gutterOffsetForIndex = function(pindex) {
3227
+
3228
+ // To find the offset of the gutter from the middle we calculate our percentage distance
3229
+ // through the book (0..1), remap to (-0.5..0.5) and multiply by the total page edge width
3230
+ var offset = parseInt(((pindex / this.getNumLeafs()) - 0.5) * this.twoPage.edgeWidth);
3231
+
3232
+ // But then again for RTL it's the opposite
3233
+ if ('rl' == this.pageProgression) {
3234
+ offset = -offset;
3235
+ }
3236
+
3237
+ return offset;
3238
+ };
3239
+
3240
+ // leafEdgeWidth
3241
+ //______________________________________________________________________________
3242
+ // Returns the width of the leaf edge div for the page with index given
3243
+ BookReader.prototype.leafEdgeWidth = function(pindex) {
3244
+ // $$$ could there be single pixel rounding errors for L vs R?
3245
+ if ((this.getPageSide(pindex) == 'L') && (this.pageProgression != 'rl')) {
3246
+ return parseInt( (pindex/this.getNumLeafs()) * this.twoPage.edgeWidth + 0.5);
3247
+ } else {
3248
+ return parseInt( (1 - pindex/this.getNumLeafs()) * this.twoPage.edgeWidth + 0.5);
3249
+ }
3250
+ };
3251
+
3252
+ // jumpIndexForLeftEdgePageX
3253
+ //______________________________________________________________________________
3254
+ // Returns the target jump leaf given a page coordinate (inside the left page edge div)
3255
+ BookReader.prototype.jumpIndexForLeftEdgePageX = function(pageX) {
3256
+ if ('rl' != this.pageProgression) {
3257
+ // LTR - flipping backward
3258
+ var jumpIndex = this.twoPage.currentIndexL - ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
3259
+
3260
+ // browser may have resized the div due to font size change -- see https://bugs.launchpad.net/gnubook/+bug/333570
3261
+ jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexL - 2);
3262
+ return jumpIndex;
3263
+
3264
+ } else {
3265
+ var jumpIndex = this.twoPage.currentIndexL + ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
3266
+ jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexL + 2, this.lastDisplayableIndex());
3267
+ return jumpIndex;
3268
+ }
3269
+ };
3270
+
3271
+ // jumpIndexForRightEdgePageX
3272
+ //______________________________________________________________________________
3273
+ // Returns the target jump leaf given a page coordinate (inside the right page edge div)
3274
+ BookReader.prototype.jumpIndexForRightEdgePageX = function(pageX) {
3275
+ if ('rl' != this.pageProgression) {
3276
+ // LTR
3277
+ var jumpIndex = this.twoPage.currentIndexR + (pageX - $(this.leafEdgeR).offset().left) * 10;
3278
+ jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexR + 2, this.lastDisplayableIndex());
3279
+ return jumpIndex;
3280
+ } else {
3281
+ var jumpIndex = this.twoPage.currentIndexR - (pageX - $(this.leafEdgeR).offset().left) * 10;
3282
+ jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexR - 2);
3283
+ return jumpIndex;
3284
+ }
3285
+ };
3286
+
3287
+
3288
+ // initNavbar
3289
+ //______________________________________________________________________________
3290
+ // Initialize the navigation bar.
3291
+ // $$$ this could also add the base elements to the DOM, so disabling the nav bar
3292
+ // could be as simple as not calling this function
3293
+ BookReader.prototype.initNavbar = function() {
3294
+ // Setup nav / chapter / search results bar
3295
+ this.refs.$BRnav = $(
3296
+ "<div class=\"BRnav BRnavDesktop\">"
3297
+
3298
+ +" <div class=\"BRnavCntl BRnavCntlBtm BRdn js-tooltip\" title=\"Toogle toolbars\"></div>"
3299
+
3300
+ +" <div class=\"BRnavpos\">"
3301
+ +" <div class=\"BRpager\"></div>"
3302
+ +" <div class=\"BRnavline\">"
3303
+ +" </div>"
3304
+ +" </div>"
3305
+ +" <div class=\"BRpage\">"
3306
+
3307
+ // Note, it's important for there to not be whitespace
3308
+ + "<span class='BRcurrentpage'></span>"
3309
+ + "<button class=\"BRicon book_left js-tooltip\"></button>"
3310
+ + "<button class=\"BRicon book_right js-tooltip\"></button>"
3311
+ + "<button class=\"BRicon onepg desktop-only js-tooltip\"></button>"
3312
+ + "<button class=\"BRicon twopg desktop-only js-tooltip\"></button>"
3313
+ + "<button class=\"BRicon thumb desktop-only js-tooltip\"></button>"
3314
+
3315
+ // zoomx`
3316
+ + "<button class='BRicon zoom_out desktop-only js-tooltip'></button>"
3317
+ + "<button class='BRicon zoom_in desktop-only js-tooltip'></button>"
3318
+ + "<button class='BRicon full js-tooltip'></button>"
3319
+ +" </div>"
3320
+
3321
+ +"</div>"
3322
+ );
3323
+
3324
+ this.refs.$br.append(this.refs.$BRnav);
3325
+
3326
+ var self = this;
3327
+ this.$('.BRpager').slider({
3328
+ animate: true,
3329
+ min: 0,
3330
+ max: this.getNumLeafs() - 1,
3331
+ value: this.currentIndex(),
3332
+ range: "min"
3333
+ })
3334
+ .bind('slide', function(event, ui) {
3335
+ self.updateNavPageNum(ui.value);
3336
+ return true;
3337
+ })
3338
+ .bind('slidechange', function(event, ui) {
3339
+ self.updateNavPageNum(ui.value);
3340
+ // recursion prevention for jumpToIndex
3341
+ if ( $(this).data('swallowchange') ) {
3342
+ $(this).data('swallowchange', false);
3343
+ } else {
3344
+ self.jumpToIndex(ui.value);
3345
+ }
3346
+ return true;
3347
+ });
3348
+
3349
+ this.updateNavPageNum(this.currentIndex());
3350
+
3351
+ return this.refs.$BRnav;
3352
+ };
3353
+
3354
+ // initEmbedNavbar
3355
+ //______________________________________________________________________________
3356
+ // Initialize the navigation bar when embedded
3357
+ BookReader.prototype.initEmbedNavbar = function() {
3358
+ // IA-specific
3359
+ var thisLink = (window.location + '')
3360
+ .replace('?ui=embed','')
3361
+ .replace('/stream/', '/details/')
3362
+ .replace('#', '/')
3363
+ ;
3364
+
3365
+ var logoHtml = '';
3366
+ if (this.showLogo) {
3367
+ logoHtml = "<a class='logo' href='" + this.logoURL + "' 'target='_blank' ></a>";
3368
+ }
3369
+
3370
+ this.refs.$BRnav = $('<div class="BRnav BRnavEmbed">'
3371
+ + logoHtml
3372
+ + "<span class='BRembedreturn'>"
3373
+ + "<a href='" + thisLink + "' target='_blank'>"+this.bookTitle+"</a>"
3374
+ + "</span>"
3375
+ + "<span class='BRtoolbarbuttons'>"
3376
+ + '<button class="BRicon book_left"></button>'
3377
+ + '<button class="BRicon book_right"></button>'
3378
+ + '<button class="BRicon full"></button>'
3379
+ + "</span>"
3380
+ + '</div>');
3381
+
3382
+ this.refs.$br.append(this.refs.$BRnav);
3383
+ };
3384
+
3385
+
3386
+ BookReader.prototype.getNavPageNumString = function(index, excludePrefix) {
3387
+ excludePrefix = excludePrefix === undefined ? false : true;
3388
+ var pageNum = this.getPageNum(index);
3389
+ var pageStr;
3390
+ if (pageNum && pageNum[0] == 'n') { // funny index
3391
+ pageStr = index + 1 + ' / ' + this.getNumLeafs(); // Accessible index starts at 0 (alas) so we add 1 to make human
3392
+ } else {
3393
+ pageStr = pageNum + ' of ' + this.maxPageNum;
3394
+ if (!excludePrefix) pageStr = 'Page ' + pageStr;
3395
+ }
3396
+ return pageStr;
3397
+ }
3398
+ BookReader.prototype.updateNavPageNum = function(index) {
3399
+ this.$('.BRcurrentpage').text(this.getNavPageNumString(index));
3400
+ };
3401
+
3402
+ /*
3403
+ * Update the nav bar display - does not cause navigation.
3404
+ */
3405
+ BookReader.prototype.updateNavIndex = function(index) {
3406
+ // We want to update the value, but normally moving the slider
3407
+ // triggers jumpToIndex which triggers this method
3408
+ index = index !== undefined ? index : this.currentIndex();
3409
+ this.$('.BRpager').data('swallowchange', true).slider('value', index);
3410
+ };
3411
+
3412
+ BookReader.prototype.updateNavIndexDebounced = BookReader.util.debounce(BookReader.prototype.updateNavIndex, 500);
3413
+
3414
+ BookReader.prototype.updateNavIndexThrottled = BookReader.util.throttle(BookReader.prototype.updateNavIndex, 500, false);
3415
+
3416
+
3417
+
3418
+ /**
3419
+ * This method builds the html for the toolbar. It can be decorated to extend
3420
+ * the toolbar.
3421
+ * @return {jqueryElement}
3422
+ */
3423
+ BookReader.prototype.buildToolbarElement = function() {
3424
+ var logoHtml = '';
3425
+ if (this.showLogo) {
3426
+ logoHtml = "<span class='BRtoolbarSection BRtoolbarSectionLogo'>"
3427
+ + "<a class='logo' href='" + this.logoURL + "'></a>"
3428
+ + "</span>";
3429
+ }
3430
+
3431
+ // Add large screen navigation
3432
+ this.refs.$BRtoolbar = $(
3433
+ "<div class='BRtoolbar header'>"
3434
+ + "<div class='BRtoolbarbuttons'>"
3435
+ + "<div class='BRtoolbarLeft'>"
3436
+ + logoHtml
3437
+ + "<span class='BRtoolbarSection BRtoolbarSectionTitle'></span>"
3438
+ + "</div>"
3439
+
3440
+ + "<div class='BRtoolbarRight'>"
3441
+ + "<span class='BRtoolbarSection BRtoolbarSectionInfo'>"
3442
+ + "<button class='BRpill info js-tooltip'>Info</button>"
3443
+ + "<button class='BRpill share js-tooltip'>Share</button>"
3444
+ + "</span>"
3445
+ // + "<span class='BRtoolbarSection BRtoolbarSectionMenu'>"
3446
+ // TODO actual hamburger menu
3447
+ // + "<button class='BRpill BRtoolbarHamburger'>"
3448
+ // + "<img src='"+this.imagesBaseURL+"icon_hamburger.svg' />"
3449
+ // + "<div class='BRhamburgerDrawer'><ul><li>hi</li></ul></div>"
3450
+ // + "</button>"
3451
+ // + "</span>"
3452
+ + "</div>" // end BRtoolbarRight
3453
+ + "</div>"
3454
+ + "</div>"
3455
+ );
3456
+
3457
+ var $titleSectionEl = this.refs.$BRtoolbar.find('.BRtoolbarSectionTitle');
3458
+
3459
+ if (this.bookUrl && this.options.enableBookTitleLink) {
3460
+ $titleSectionEl.append(
3461
+ $('<a>')
3462
+ .attr({'href': this.bookUrl, 'title': this.bookUrlTitle})
3463
+ .addClass('BRreturn')
3464
+ .html(this.bookUrlText || this.bookTitle)
3465
+ )
3466
+ } else if (this.bookTitle) {
3467
+ $titleSectionEl.append(this.bookUrlText || this.bookTitle);
3468
+ }
3469
+
3470
+ // var $hamburger = this.refs.$BRtoolbar.find('BRtoolbarHamburger');
3471
+ return this.refs.$BRtoolbar;
3472
+ }
3473
+
3474
+
3475
+
3476
+ BookReader.prototype.initToolbar = function(mode, ui) {
3477
+ var self = this;
3478
+
3479
+ this.refs.$br.append(this.buildToolbarElement());
3480
+
3481
+ this.$('.BRnavCntl').addClass('BRup');
3482
+ this.$('.pause').hide();
3483
+
3484
+ this.updateToolbarZoom(this.reduce); // Pretty format
3485
+
3486
+ // We build in mode 2
3487
+ this.refs.$BRtoolbar.append();
3488
+
3489
+ // Hide mode buttons and autoplay if 2up is not available
3490
+ // $$$ if we end up with more than two modes we should show the applicable buttons
3491
+ if ( !this.canSwitchToMode(this.constMode2up) ) {
3492
+ this.$('.two_page_mode, .play, .pause').hide();
3493
+ }
3494
+ if ( !this.canSwitchToMode(this.constModeThumb) ) {
3495
+ this.$('.thumbnail_mode').hide();
3496
+ }
3497
+
3498
+ // Hide one page button if it is the only mode available
3499
+ if ( ! (this.canSwitchToMode(this.constMode2up) || this.canSwitchToMode(this.constModeThumb)) ) {
3500
+ this.$('.one_page_mode').hide();
3501
+ }
3502
+
3503
+ $('<div style="display: none;"></div>').append(
3504
+ this.blankShareDiv()
3505
+ ).append(
3506
+ this.blankInfoDiv()
3507
+ ).appendTo(this.refs.$br);
3508
+
3509
+ this.$('.BRinfo .BRfloatTitle a')
3510
+ .attr({'href': this.bookUrl})
3511
+ .text(this.bookTitle)
3512
+ .addClass('title');
3513
+
3514
+ // These functions can be overridden
3515
+ this.buildInfoDiv(this.$('.BRinfo'));
3516
+ this.buildShareDiv(this.$('.BRshare'));
3517
+
3518
+
3519
+ this.$('.share').colorbox({
3520
+ inline: true,
3521
+ opacity: "0.5",
3522
+ href: this.$('.BRshare'),
3523
+ onLoad: function() {
3524
+ self.trigger(BookReader.eventNames.stop);
3525
+ self.$('.BRpageviewValue').val(window.location.href);
3526
+ }
3527
+ });
3528
+ this.$('.info').colorbox({
3529
+ inline: true,
3530
+ opacity: "0.5",
3531
+ href: this.$('.BRinfo'),
3532
+ onLoad: function() {
3533
+ self.trigger(BookReader.eventNames.stop);
3534
+ }
3535
+ });
3536
+ };
3537
+
3538
+ BookReader.prototype.blankInfoDiv = function() {
3539
+ return $([
3540
+ '<div class="BRfloat BRinfo">',
3541
+ '<div class="BRfloatHead">About this book',
3542
+ '<button class="floatShut" href="javascript:;" onclick="$.fn.colorbox.close();"><span class="shift">Close</span></a>',
3543
+ '</div>',
3544
+ '<div class="BRfloatBody">',
3545
+ '<div class="BRfloatCover">',
3546
+ '</div>',
3547
+ '<div class="BRfloatMeta">',
3548
+ '<div class="BRfloatTitle">',
3549
+ '<h2><a/></h2>',
3550
+ '</div>',
3551
+ '</div>',
3552
+ '</div>',
3553
+ '<div class="BRfloatFoot">',
3554
+ '<a href="https://openlibrary.org/dev/docs/bookreader">About the BookReader</a>',
3555
+ '</div>',
3556
+ '</div>'].join('\n')
3557
+ );
3558
+ };
3559
+
3560
+ BookReader.prototype.blankShareDiv = function() {
3561
+ return $([
3562
+ '<div class="BRfloat BRshare">',
3563
+ '<div class="BRfloatHead">',
3564
+ 'Share',
3565
+ '<button class="floatShut" href="javascript:;" onclick="$.fn.colorbox.close();"><span class="shift">Close</span></a>',
3566
+ '</div>',
3567
+ '</div>'].join('\n')
3568
+ );
3569
+ };
3570
+
3571
+
3572
+ // updateToolbarZoom(reduce)
3573
+ //______________________________________________________________________________
3574
+ // Update the displayed zoom factor based on reduction factor
3575
+ BookReader.prototype.updateToolbarZoom = function(reduce) {
3576
+ var value;
3577
+ var autofit = null;
3578
+
3579
+ // $$$ TODO preserve zoom/fit for each mode
3580
+ if (this.mode == this.constMode2up) {
3581
+ autofit = this.twoPage.autofit;
3582
+ } else {
3583
+ autofit = this.onePage.autofit;
3584
+ }
3585
+
3586
+ if (autofit) {
3587
+ value = autofit.slice(0,1).toUpperCase() + autofit.slice(1);
3588
+ } else {
3589
+ value = (100 / reduce).toFixed(2);
3590
+ // Strip trailing zeroes and decimal if all zeroes
3591
+ value = value.replace(/0+$/,'');
3592
+ value = value.replace(/\.$/,'');
3593
+ value += '%';
3594
+ }
3595
+ this.$('.BRzoom').text(value);
3596
+ };
3597
+
3598
+ // bindNavigationHandlers
3599
+ //______________________________________________________________________________
3600
+ // Bind navigation handlers
3601
+ BookReader.prototype.bindNavigationHandlers = function() {
3602
+ var self = this;
3603
+
3604
+ // Note the mobile plugin attaches itself to body, so we need to select outside
3605
+ var jIcons = this.$('.BRicon').add('.BRmobileMenu .BRicon');
3606
+
3607
+ jIcons.filter('.onepg').bind('click', function(e) {
3608
+ self.switchMode(self.constMode1up);
3609
+ });
3610
+
3611
+ jIcons.filter('.twopg').bind('click', function(e) {
3612
+ self.switchMode(self.constMode2up);
3613
+ });
3614
+
3615
+ jIcons.filter('.thumb').bind('click', function(e) {
3616
+ self.switchMode(self.constModeThumb);
3617
+ });
3618
+
3619
+ jIcons.filter('.fit').bind('fit', function(e) {
3620
+ // XXXmang implement autofit zoom
3621
+ });
3622
+
3623
+ jIcons.filter('.book_left').click(function(e) {
3624
+ self.trigger(BookReader.eventNames.stop);
3625
+ self.left();
3626
+ return false;
3627
+ });
3628
+
3629
+ jIcons.filter('.book_right').click(function(e) {
3630
+ self.trigger(BookReader.eventNames.stop);
3631
+ self.right();
3632
+ return false;
3633
+ });
3634
+
3635
+ jIcons.filter('.book_up').bind('click', function(e) {
3636
+ if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
3637
+ self.scrollUp();
3638
+ } else {
3639
+ self.prev();
3640
+ }
3641
+ return false;
3642
+ });
3643
+
3644
+ jIcons.filter('.book_down').bind('click', function(e) {
3645
+ if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
3646
+ self.scrollDown();
3647
+ } else {
3648
+ self.next();
3649
+ }
3650
+ return false;
3651
+ });
3652
+
3653
+ jIcons.filter('.play').click(function(e) {
3654
+ self.autoToggle();
3655
+ return false;
3656
+ });
3657
+
3658
+ jIcons.filter('.pause').click(function(e) {
3659
+ self.autoToggle();
3660
+ return false;
3661
+ });
3662
+
3663
+ jIcons.filter('.book_top').click(function(e) {
3664
+ self.first();
3665
+ return false;
3666
+ });
3667
+
3668
+ jIcons.filter('.book_bottom').click(function(e) {
3669
+ self.last();
3670
+ return false;
3671
+ });
3672
+
3673
+ jIcons.filter('.book_leftmost').click(function(e) {
3674
+ self.leftmost();
3675
+ return false;
3676
+ });
3677
+
3678
+ jIcons.filter('.book_rightmost').click(function(e) {
3679
+ self.rightmost();
3680
+ return false;
3681
+ });
3682
+
3683
+ jIcons.filter('.zoom_in').bind('click', function() {
3684
+ self.trigger(BookReader.eventNames.stop);
3685
+ self.zoom(1);
3686
+ return false;
3687
+ });
3688
+
3689
+ jIcons.filter('.zoom_out').bind('click', function() {
3690
+ self.trigger(BookReader.eventNames.stop);
3691
+ self.zoom(-1);
3692
+ return false;
3693
+ });
3694
+
3695
+ jIcons.filter('.full').bind('click', function() {
3696
+ if (self.ui == 'embed') {
3697
+ var url = self.$('.BRembedreturn a').attr('href');
3698
+ window.open(url);
3699
+ } else {
3700
+ self.toggleFullscreen();
3701
+ }
3702
+ });
3703
+
3704
+ var $brNavCntlBtmEl = this.$('.BRnavCntlBtm');
3705
+ var $brNavCntlTopEl = this.$('.BRnavCntlTop');
3706
+
3707
+ this.$('.BRnavCntl').click(
3708
+ function(){
3709
+ var promises = [];
3710
+ // TODO don't use magic constants
3711
+ // TODO move this to a function
3712
+ if ($brNavCntlBtmEl.hasClass('BRdn')) {
3713
+ if (self.refs.$BRtoolbar)
3714
+ promises.push(self.refs.$BRtoolbar.animate(
3715
+ {top: self.getToolBarHeight() * -1}
3716
+ ).promise());
3717
+ promises.push(self.$('.BRnav').animate({bottom: self.getNavHeight() * -1}).promise());
3718
+ $brNavCntlBtmEl.addClass('BRup').removeClass('BRdn');
3719
+ $brNavCntlTopEl.addClass('BRdn').removeClass('BRup');
3720
+ self.$('.BRnavCntlBtm.BRnavCntl').animate({height:'45px'});
3721
+ self.$('.BRnavCntl').delay(1000).animate({opacity:.75}, 1000);
3722
+ } else {
3723
+ if (self.refs.$BRtoolbar)
3724
+ promises.push(self.refs.$BRtoolbar.animate({top:0}).promise());
3725
+ promises.push(self.$('.BRnav').animate({bottom:0}).promise());
3726
+ $brNavCntlBtmEl.addClass('BRdn').removeClass('BRup');
3727
+ $brNavCntlTopEl.addClass('BRup').removeClass('BRdn');
3728
+ self.$('.BRnavCntlBtm.BRnavCntl').animate({height:'30px'});
3729
+ self.$('.BRvavCntl').animate({opacity:1})
3730
+ };
3731
+ $.when.apply($, promises).done(function() {
3732
+ // Only do full resize in auto mode and need to recalc. size
3733
+ if (self.mode == self.constMode2up && self.twoPage.autofit != null
3734
+ && self.twoPage.autofit != 'none') {
3735
+ self.resize();
3736
+ } else if (self.mode == self.constMode1up && self.onePage.autofit != null
3737
+ && self.onePage.autofit != 'none') {
3738
+ self.resize();
3739
+ } else {
3740
+ // Don't do a full resize to avoid redrawing images
3741
+ self.resizeBRcontainer();
3742
+ }
3743
+ });
3744
+ }
3745
+ );
3746
+ $brNavCntlBtmEl.mouseover(function(){
3747
+ if ($(this).hasClass('BRup')) {
3748
+ self.$('.BRnavCntl').animate({opacity:1},250);
3749
+ }
3750
+ }).mouseleave(function(){
3751
+ if ($(this).hasClass('BRup')) {
3752
+ self.$('.BRnavCntl').animate({opacity:.75},250);
3753
+ }
3754
+ });
3755
+ $brNavCntlTopEl.mouseover(function(){
3756
+ if ($(this).hasClass('BRdn')) {
3757
+ self.$('.BRnavCntl').animate({opacity:1},250);
3758
+ }
3759
+ }).mouseleave(function(){
3760
+ if ($(this).hasClass('BRdn')) {
3761
+ self.$('.BRnavCntl').animate({opacity:.75},250);
3762
+ }
3763
+ });
3764
+
3765
+ this.initSwipeData();
3766
+
3767
+ $(document).off('mousemove.navigation', this.el);
3768
+ $(document).on(
3769
+ 'mousemove.navigation',
3770
+ this.el,
3771
+ { 'br': this },
3772
+ this.navigationMousemoveHandler
3773
+ );
3774
+
3775
+ $(document).off('mousedown.swipe', '.BRpageimage');
3776
+ $(document).on(
3777
+ 'mousedown.swipe',
3778
+ '.BRpageimage',
3779
+ { 'br': this },
3780
+ this.swipeMousedownHandler
3781
+ );
3782
+
3783
+ this.bindMozTouchHandlers();
3784
+ };
3785
+
3786
+ // unbindNavigationHandlers
3787
+ //______________________________________________________________________________
3788
+ // Unbind navigation handlers
3789
+ BookReader.prototype.unbindNavigationHandlers = function() {
3790
+ $(document).off('mousemove.navigation', this.el);
3791
+ };
3792
+
3793
+ // navigationMousemoveHandler
3794
+ //______________________________________________________________________________
3795
+ // Handle mousemove related to navigation. Bind at #BookReader level to allow autohide.
3796
+ BookReader.prototype.navigationMousemoveHandler = function(event) {
3797
+ // $$$ possibly not great to be calling this for every mousemove
3798
+
3799
+ if (event.data['br'].uiAutoHide) {
3800
+ // TODO look into these magic numbers: 75 and 76
3801
+ var navkey = $(document).height() - 75;
3802
+ if ((event.pageY < 76) || (event.pageY > navkey)) {
3803
+ // inside or near navigation elements
3804
+ event.data['br'].hideNavigation();
3805
+ } else {
3806
+ event.data['br'].showNavigation();
3807
+ }
3808
+ }
3809
+ };
3810
+
3811
+ BookReader.prototype.initSwipeData = function(clientX, clientY) {
3812
+ /*
3813
+ * Based on the really quite awesome "Today's Guardian" at http://guardian.gyford.com/
3814
+ */
3815
+ this._swipe = {
3816
+ mightBeSwiping: false,
3817
+ didSwipe: false,
3818
+ mightBeDraggin: false,
3819
+ didDrag: false,
3820
+ startTime: (new Date).getTime(),
3821
+ startX: clientX,
3822
+ startY: clientY,
3823
+ lastX: clientX,
3824
+ lastY: clientY,
3825
+ deltaX: 0,
3826
+ deltaY: 0,
3827
+ deltaT: 0
3828
+ }
3829
+ };
3830
+
3831
+ BookReader.prototype.swipeMousedownHandler = function(event) {
3832
+ var self = event.data['br'];
3833
+
3834
+ // We should be the last bubble point for the page images
3835
+ // Disable image drag and select, but keep right-click
3836
+ if (event.which == 3) {
3837
+ return !self.protected;
3838
+ }
3839
+
3840
+ $(event.target).bind('mouseout.swipe',
3841
+ { 'br': self},
3842
+ self.swipeMouseupHandler
3843
+ ).bind('mouseup.swipe',
3844
+ { 'br': self},
3845
+ self.swipeMouseupHandler
3846
+ ).bind('mousemove.swipe',
3847
+ { 'br': self },
3848
+ self.swipeMousemoveHandler
3849
+ );
3850
+
3851
+ self.initSwipeData(event.clientX, event.clientY);
3852
+ self._swipe.mightBeSwiping = true;
3853
+ self._swipe.mightBeDragging = true;
3854
+
3855
+ event.preventDefault();
3856
+ event.returnValue = false;
3857
+ event.cancelBubble = true;
3858
+ return false;
3859
+ };
3860
+
3861
+ BookReader.prototype.swipeMousemoveHandler = function(event) {
3862
+ var self = event.data['br'];
3863
+ var _swipe = self._swipe;
3864
+ if (! _swipe.mightBeSwiping) {
3865
+ return;
3866
+ }
3867
+
3868
+ // Update swipe data
3869
+ _swipe.deltaX = event.clientX - _swipe.startX;
3870
+ _swipe.deltaY = event.clientY - _swipe.startY;
3871
+ _swipe.deltaT = (new Date).getTime() - _swipe.startTime;
3872
+
3873
+ var absX = Math.abs(_swipe.deltaX);
3874
+ var absY = Math.abs(_swipe.deltaY);
3875
+
3876
+ // Minimum distance in the amount of tim to trigger the swipe
3877
+ var minSwipeLength = Math.min(self.refs.$br.width() / 5, 80);
3878
+ var maxSwipeTime = 400;
3879
+
3880
+ // Check for horizontal swipe
3881
+ if (absX > absY && (absX > minSwipeLength) && _swipe.deltaT < maxSwipeTime) {
3882
+ _swipe.mightBeSwiping = false; // only trigger once
3883
+ _swipe.didSwipe = true;
3884
+ if (self.mode == self.constMode2up) {
3885
+ if (_swipe.deltaX < 0) {
3886
+ self.right();
3887
+ } else {
3888
+ self.left();
3889
+ }
3890
+ }
3891
+ }
3892
+
3893
+ if ( _swipe.deltaT > maxSwipeTime && !_swipe.didSwipe) {
3894
+ if (_swipe.mightBeDragging) {
3895
+ // Dragging
3896
+ _swipe.didDrag = true;
3897
+ self.refs.$brContainer
3898
+ .scrollTop(self.refs.$brContainer.scrollTop() - event.clientY + _swipe.lastY)
3899
+ .scrollLeft(self.refs.$brContainer.scrollLeft() - event.clientX + _swipe.lastX);
3900
+ }
3901
+ }
3902
+ _swipe.lastX = event.clientX;
3903
+ _swipe.lastY = event.clientY;
3904
+
3905
+ event.preventDefault();
3906
+ event.returnValue = false;
3907
+ event.cancelBubble = true;
3908
+ return false;
3909
+ };
3910
+ BookReader.prototype.swipeMouseupHandler = function(event) {
3911
+ var _swipe = event.data['br']._swipe;
3912
+ _swipe.mightBeSwiping = false;
3913
+ _swipe.mightBeDragging = false;
3914
+
3915
+ $(event.target).unbind('mouseout.swipe').unbind('mouseup.swipe').unbind('mousemove.swipe');
3916
+
3917
+ if (_swipe.didSwipe || _swipe.didDrag) {
3918
+ // Swallow event if completed swipe gesture
3919
+ event.preventDefault();
3920
+ event.returnValue = false;
3921
+ event.cancelBubble = true;
3922
+ return false;
3923
+ }
3924
+ return true;
3925
+ };
3926
+ BookReader.prototype.bindMozTouchHandlers = function() {
3927
+ var self = this;
3928
+
3929
+ // Currently only want touch handlers in 2up
3930
+ this.refs.$br.bind('MozTouchDown', function(event) {
3931
+ if (this.mode == self.constMode2up) {
3932
+ event.preventDefault();
3933
+ }
3934
+ })
3935
+ .bind('MozTouchMove', function(event) {
3936
+ if (this.mode == self.constMode2up) {
3937
+ event.preventDefault();
3938
+ }
3939
+ })
3940
+ .bind('MozTouchUp', function(event) {
3941
+ if (this.mode == self.constMode2up) {
3942
+ event.preventDefault();
3943
+ }
3944
+ });
3945
+ };
3946
+
3947
+ // navigationIsVisible
3948
+ //______________________________________________________________________________
3949
+ // Returns true if the navigation elements are currently visible
3950
+ BookReader.prototype.navigationIsVisible = function() {
3951
+ // $$$ doesn't account for transitioning states, nav must be fully visible to return true
3952
+ var toolpos = this.refs.$BRtoolbar.offset();
3953
+ var tooltop = toolpos.top;
3954
+ return tooltop == 0;
3955
+ };
3956
+
3957
+ // hideNavigation
3958
+ //______________________________________________________________________________
3959
+ // Hide navigation elements, if visible
3960
+ BookReader.prototype.hideNavigation = function() {
3961
+ // Check if navigation is showing
3962
+ if (this.navigationIsVisible()) {
3963
+ var toolbarHeight = this.getToolBarHeight();
3964
+ var navbarHeight = this.getNavHeight();
3965
+ this.refs.$BRtoolbar.animate({top: toolbarHeight * -1});
3966
+ this.refs.$BRnav.animate({bottom: navbarHeight * -1});
3967
+ }
3968
+ };
3969
+
3970
+ // showNavigation
3971
+ //______________________________________________________________________________
3972
+ // Show navigation elements
3973
+ BookReader.prototype.showNavigation = function() {
3974
+ // Check if navigation is hidden
3975
+ if (!this.navigationIsVisible()) {
3976
+ this.refs.$BRtoolbar.animate({top:0});
3977
+ this.refs.$BRnav.animate({bottom:0});
3978
+ }
3979
+ };
3980
+
3981
+
3982
+ // firstDisplayableIndex
3983
+ //______________________________________________________________________________
3984
+ // Returns the index of the first visible page, dependent on the mode.
3985
+ // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
3986
+ // this function when we can as part of https://bugs.launchpad.net/gnubook/+bug/296788
3987
+ BookReader.prototype.firstDisplayableIndex = function() {
3988
+ if (this.mode != this.constMode2up) {
3989
+ return 0;
3990
+ }
3991
+
3992
+ if ('rl' != this.pageProgression) {
3993
+ // LTR
3994
+ if (this.getPageSide(0) == 'L') {
3995
+ return 0;
3996
+ } else {
3997
+ return -1;
3998
+ }
3999
+ } else {
4000
+ // RTL
4001
+ if (this.getPageSide(0) == 'R') {
4002
+ return 0;
4003
+ } else {
4004
+ return -1;
4005
+ }
4006
+ }
4007
+ };
4008
+
4009
+ // lastDisplayableIndex
4010
+ //______________________________________________________________________________
4011
+ // Returns the index of the last visible page, dependent on the mode.
4012
+ // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
4013
+ // this function when we can as pa rt of https://bugs.launchpad.net/gnubook/+bug/296788
4014
+ BookReader.prototype.lastDisplayableIndex = function() {
4015
+
4016
+ var lastIndex = this.getNumLeafs() - 1;
4017
+
4018
+ if (this.mode != this.constMode2up) {
4019
+ return lastIndex;
4020
+ }
4021
+
4022
+ if ('rl' != this.pageProgression) {
4023
+ // LTR
4024
+ if (this.getPageSide(lastIndex) == 'R') {
4025
+ return lastIndex;
4026
+ } else {
4027
+ return lastIndex + 1;
4028
+ }
4029
+ } else {
4030
+ // RTL
4031
+ if (this.getPageSide(lastIndex) == 'L') {
4032
+ return lastIndex;
4033
+ } else {
4034
+ return lastIndex + 1;
4035
+ }
4036
+ }
4037
+ };
4038
+
4039
+ // Parameter related functions
4040
+
4041
+ // updateFromParams(params)
4042
+ //________
4043
+ // Update ourselves from the params object.
4044
+ //
4045
+ BookReader.prototype.updateFromParams = function(params) {
4046
+ if ('undefined' != typeof(params.mode)) {
4047
+ this.switchMode(params.mode);
4048
+ }
4049
+
4050
+ // $$$ process /zoom
4051
+ var pageFound = false;
4052
+ // We only respect page if index is not set
4053
+ if ('undefined' != typeof(params.index)) {
4054
+ pageFound = true;
4055
+ if (params.index != this.currentIndex()) {
4056
+ this.jumpToIndex(params.index);
4057
+ }
4058
+ } else if ('undefined' != typeof(params.page)) {
4059
+ pageFound = true;
4060
+ // $$$ this assumes page numbers are unique
4061
+ if (params.page != this.getPageNum(this.currentIndex())) {
4062
+ this.jumpToPage(params.page);
4063
+ }
4064
+ }
4065
+
4066
+ // process /search
4067
+ if (this.enableSearch && 'undefined' != typeof(params.search)) {
4068
+ if (this.searchTerm != params.search) {
4069
+ this.search(params.search, {goToFirstResult: !pageFound});
4070
+ this.$('.BRsearchInput').val(params.search);
4071
+ }
4072
+ }
4073
+
4074
+ // $$$ process /region
4075
+ // $$$ process /highlight
4076
+
4077
+ // $$$ process /theme
4078
+ if (this.enableThemesPlugin && 'undefined' != typeof(params.theme)) {
4079
+ this.updateTheme(params.theme);
4080
+ }
4081
+ };
4082
+
4083
+
4084
+ // getPageIndex(pageNum)
4085
+ //________
4086
+ // Returns the *highest* index the given page number, or undefined
4087
+ BookReader.prototype.getPageIndex = function(pageNum) {
4088
+ var pageIndices = this.getPageIndices(pageNum);
4089
+
4090
+ if (pageIndices.length > 0) {
4091
+ return pageIndices[pageIndices.length - 1];
4092
+ }
4093
+
4094
+ return undefined;
4095
+ };
4096
+
4097
+ // getPageIndices(pageNum)
4098
+ //________
4099
+ // Returns an array (possibly empty) of the indices with the given page number
4100
+ BookReader.prototype.getPageIndices = function(pageNum) {
4101
+ var indices = [];
4102
+
4103
+ // Check for special "nXX" page number
4104
+ if (pageNum.slice(0,1) == 'n') {
4105
+ try {
4106
+ var pageIntStr = pageNum.slice(1, pageNum.length);
4107
+ var pageIndex = parseInt(pageIntStr);
4108
+ indices.push(pageIndex);
4109
+ return indices;
4110
+ } catch(err) {
4111
+ // Do nothing... will run through page names and see if one matches
4112
+ }
4113
+ }
4114
+
4115
+ var i;
4116
+ for (i=0; i<this.getNumLeafs(); i++) {
4117
+ if (this.getPageNum(i) == pageNum) {
4118
+ indices.push(i);
4119
+ }
4120
+ }
4121
+
4122
+ return indices;
4123
+ };
4124
+
4125
+ // getPageName(index)
4126
+ //________
4127
+ // Returns the name of the page as it should be displayed in the user interface
4128
+ BookReader.prototype.getPageName = function(index) {
4129
+ return 'Page ' + this.getPageNum(index);
4130
+ };
4131
+
4132
+
4133
+ // canSwitchToMode
4134
+ //________
4135
+ // Returns true if we can switch to the requested mode
4136
+ BookReader.prototype.canSwitchToMode = function(mode) {
4137
+ if (mode == this.constMode2up || mode == this.constModeThumb) {
4138
+ // check there are enough pages to display
4139
+ // $$$ this is a workaround for the mis-feature that we can't display
4140
+ // short books in 2up mode
4141
+ if (this.getNumLeafs() < 2) {
4142
+ return false;
4143
+ }
4144
+ }
4145
+
4146
+ return true;
4147
+ };
4148
+
4149
+ // _getPageWidth
4150
+ //--------
4151
+ // Returns the page width for the given index, or first or last page if out of range
4152
+ BookReader.prototype._getPageWidth = function(index) {
4153
+ // Synthesize a page width for pages not actually present in book.
4154
+ // May or may not be the best approach.
4155
+ // If index is out of range we return the width of first or last page
4156
+ index = BookReader.util.clamp(index, 0, this.getNumLeafs() - 1);
4157
+ return this.getPageWidth(index);
4158
+ };
4159
+
4160
+ // _getPageHeight
4161
+ //--------
4162
+ // Returns the page height for the given index, or first or last page if out of range
4163
+ BookReader.prototype._getPageHeight = function(index) {
4164
+ index = BookReader.util.clamp(index, 0, this.getNumLeafs() - 1);
4165
+ return this.getPageHeight(index);
4166
+ };
4167
+
4168
+ // _getPageURI
4169
+ //--------
4170
+ // Returns the page URI or transparent image if out of range
4171
+ BookReader.prototype._getPageURI = function(index, reduce, rotate) {
4172
+ if (index < 0 || index >= this.getNumLeafs()) { // Synthesize page
4173
+ return this.imagesBaseURL + "transparent.png";
4174
+ }
4175
+
4176
+ if ('undefined' == typeof(reduce)) {
4177
+ // reduce not passed in
4178
+ // $$$ this probably won't work for thumbnail mode
4179
+ var ratio = this.getPageHeight(index) / this.twoPage.height;
4180
+ var scale;
4181
+ // $$$ we make an assumption here that the scales are available pow2 (like kakadu)
4182
+ if (ratio < 2) {
4183
+ scale = 1;
4184
+ } else if (ratio < 4) {
4185
+ scale = 2;
4186
+ } else if (ratio < 8) {
4187
+ scale = 4;
4188
+ } else if (ratio < 16) {
4189
+ scale = 8;
4190
+ } else if (ratio < 32) {
4191
+ scale = 16;
4192
+ } else {
4193
+ scale = 32;
4194
+ }
4195
+ reduce = scale;
4196
+ }
4197
+
4198
+ return this.getPageURI(index, reduce, rotate);
4199
+ };
4200
+
4201
+
4202
+ // showProgressPopup
4203
+ //______________________________________________________________________________
4204
+ BookReader.prototype.showProgressPopup = function(msg) {
4205
+ if (this.popup) return;
4206
+
4207
+ this.popup = document.createElement("div");
4208
+ $(this.popup).css({
4209
+ top: (this.refs.$br.height()*0.5-100) + 'px',
4210
+ left: (this.refs.$br.width()-300)*0.5 + 'px'
4211
+ }).prop('className', 'BRprogresspopup');
4212
+
4213
+ var bar = document.createElement("div");
4214
+ $(bar).css({
4215
+ height: '20px'
4216
+ }).prop('className', 'BRprogressbar');
4217
+ $(this.popup).append(bar);
4218
+
4219
+ if (msg) {
4220
+ var msgdiv = document.createElement("div");
4221
+ msgdiv.innerHTML = msg;
4222
+ $(this.popup).append(msgdiv);
4223
+ }
4224
+
4225
+ $(this.popup).appendTo(this.refs.$br);
4226
+ };
4227
+
4228
+ // removeProgressPopup
4229
+ //______________________________________________________________________________
4230
+ BookReader.prototype.removeProgressPopup = function() {
4231
+ $(this.popup).remove();
4232
+ this.$('.BRprogresspopup').remove();
4233
+ this.popup=null;
4234
+ };
4235
+
4236
+
4237
+ BookReader.prototype.buildShareDiv = function(jShareDiv) {
4238
+ var pageView = document.location + '';
4239
+ var bookView = (pageView + '').replace(/#.*/,'');
4240
+ var self = this;
4241
+
4242
+ var embedHtml = '';
4243
+ if (this.getEmbedCode) {
4244
+ embedHtml = [
4245
+ '<div class="share-embed">',
4246
+ '<p class="share-embed-prompt">Copy and paste one of these options to share this book elsewhere.</p>',
4247
+ '<form method="post" action="">',
4248
+ '<fieldset class="fieldset-share-pageview">',
4249
+ '<label for="pageview">Link to this page view</label>',
4250
+ '<input type="text" name="pageview" class="BRpageviewValue" value="' + pageView + '"/>',
4251
+ '</fieldset>',
4252
+ '<fieldset class="fieldset-share-book-link">',
4253
+ '<label for="booklink">Link to the book</label>',
4254
+ '<input type="text" name="booklink" class="booklink" value="' + bookView + '"/>',
4255
+ '</fieldset>',
4256
+ '<fieldset class="fieldset-embed">',
4257
+ '<label for="iframe">Embed a mini Book Reader</label>',
4258
+ '<fieldset class="sub">',
4259
+ '<label class="sub">',
4260
+ '<input type="radio" name="pages" value="' + this.constMode1up + '" checked="checked"/>',
4261
+ '1 page',
4262
+ '</label>',
4263
+ '<label class="sub">',
4264
+ '<input type="radio" name="pages" value="' + this.constMode2up + '"/>',
4265
+ '2 pages',
4266
+ '</label>',
4267
+ '<label class="sub">',
4268
+ '<input type="checkbox" name="thispage" value="thispage"/>',
4269
+ 'Open to this page?',
4270
+ '</label>',
4271
+ '</fieldset>',
4272
+ '<textarea cols="30" rows="4" name="iframe" class="BRframeEmbed"></textarea>',
4273
+ '</fieldset>',
4274
+ '</form>',
4275
+ '</div>'
4276
+ ].join('\n');
4277
+ }
4278
+
4279
+ var jForm = $([
4280
+ '<div class="share-title">Share this book</div>',
4281
+ '<div class="share-social">',
4282
+ '<label class="sub open-to-this-page">',
4283
+ '<input class="thispage-social" type="checkbox" />',
4284
+ 'Open to this page?',
4285
+ '</label>',
4286
+ '<div><button class="BRaction share facebook-share-button"><i class="BRicon fb" /> Facebook</button></div>',
4287
+ '<div><button class="BRaction share twitter-share-button"><i class="BRicon twitter" /> Twitter</button></div>',
4288
+ '<div><button class="BRaction share email-share-button"><i class="BRicon email" /> Email</button></div>',
4289
+ '</div>',
4290
+ embedHtml,
4291
+ '<div class="BRfloatFoot">',
4292
+ '<button class="share-finished" type="button" onclick="$.fn.colorbox.close();">Finished</button>',
4293
+ '</div>'
4294
+ ].join('\n'));
4295
+
4296
+ jForm.appendTo(jShareDiv);
4297
+
4298
+ jForm.find('.fieldset-embed input').bind('change', function() {
4299
+ var form = $(this).parents('form:first');
4300
+ var params = {};
4301
+ params.mode = $(form.find('.fieldset-embed input[name=pages]:checked')).val();
4302
+ if (form.find('.fieldset-embed input[name=thispage]').prop('checked')) {
4303
+ params.page = self.getPageNum(self.currentIndex());
4304
+ }
4305
+
4306
+ if (self.getEmbedCode) {
4307
+ // $$$ changeable width/height to be added to share UI
4308
+ var frameWidth = "480px";
4309
+ var frameHeight = "430px";
4310
+ form.find('.BRframeEmbed').val(self.getEmbedCode(frameWidth, frameHeight, params));
4311
+ }
4312
+ });
4313
+
4314
+ jForm.find('input, textarea').bind('focus', function() {
4315
+ this.select();
4316
+ });
4317
+
4318
+ // Bind share buttons
4319
+
4320
+ // Use url without hashes
4321
+ jForm.find('.facebook-share-button').click(function(){
4322
+ var params = $.param({ u: self._getSocialShareUrl() });
4323
+ var url = 'https://www.facebook.com/sharer.php?' + params;
4324
+ self.createPopup(url, 600, 400, 'Share')
4325
+ });
4326
+ jForm.find('.twitter-share-button').click(function(){
4327
+ var params = $.param({
4328
+ url: self._getSocialShareUrl(),
4329
+ text: self.bookTitle
4330
+ });
4331
+ var url = 'https://twitter.com/intent/tweet?' + params;
4332
+ self.createPopup(url, 600, 400, 'Share')
4333
+ });
4334
+ jForm.find('.email-share-button').click(function(){
4335
+ var body = self.bookTitle + "\n\n" + self._getSocialShareUrl();
4336
+ window.location.href = 'mailto:?subject=' + encodeURI(self.bookTitle) + '&body=' + encodeURI(body);
4337
+ });
4338
+
4339
+ jForm.find('input[name=thispage]').trigger('change');
4340
+
4341
+ jForm.appendTo(jShareDiv);
4342
+ };
4343
+
4344
+ BookReader.prototype._getSocialShareUrl = function() {
4345
+ var shareThisPage = this.$('.thispage-social').prop('checked');
4346
+ if (shareThisPage) {
4347
+ return window.location.href;
4348
+ } else {
4349
+ return document.location.protocol + "//" + window.location.hostname + window.location.pathname;
4350
+ }
4351
+ };
4352
+
4353
+ /**
4354
+ * @param JInfoDiv DOM element. Appends info to this element
4355
+ * Can be overridden or extended
4356
+ */
4357
+ BookReader.prototype.buildInfoDiv = function(jInfoDiv) {
4358
+ // Remove these legacy elements
4359
+ jInfoDiv.find('.BRfloatBody, .BRfloatCover, .BRfloatFoot').remove();
4360
+
4361
+ var $leftCol = $("<div class=\"BRinfoLeftCol\"></div>");
4362
+ if (this.thumbnail) {
4363
+ $leftCol.append($("<div class=\"BRimageW\">"
4364
+ +" <img src=\""+this.thumbnail+"\" "
4365
+ +" alt=\""+BookReader.util.escapeHTML(this.bookTitle)+"\" />"
4366
+ +"</div>"));
4367
+ }
4368
+
4369
+ var $rightCol = $("<div class=\"BRinfoRightCol\">");
4370
+
4371
+ // A loop to build fields
4372
+ var extraClass;
4373
+ for (var i = 0; i < this.metadata.length; i++) {
4374
+ extraClass = this.metadata[i].extraValueClass || '';
4375
+ $rightCol.append($("<div class=\"BRinfoValueWrapper\">"
4376
+ +" <div class=\"BRinfoLabel\">"
4377
+ + this.metadata[i].label
4378
+ +" </div>"
4379
+ +" <div class=\"BRinfoValue " + extraClass + "\">"
4380
+ + this.metadata[i].value
4381
+ +" </div>"
4382
+ +"</div>"));
4383
+ }
4384
+
4385
+ var moreInfoText;
4386
+ if (this.bookUrlMoreInfo) {
4387
+ moreInfoText = this.bookUrlMoreInfo;
4388
+ } else if (this.bookTitle) {
4389
+ moreInfoText = this.bookTitle;
4390
+ }
4391
+
4392
+ if (moreInfoText && this.bookUrl) {
4393
+ $rightCol.append($("<div class=\"BRinfoValueWrapper\">"
4394
+ +"<div class=\"BRinfoMoreInfoWrapper\">"
4395
+ +" <a class=\"BRinfoMoreInfo\" href=\""+this.bookUrl+"\">"
4396
+ + moreInfoText
4397
+ +" </a>"
4398
+ +"</div>"
4399
+ +"</div>"));
4400
+ }
4401
+
4402
+
4403
+ var footerEl = "<div class=\"BRfloatFoot BRinfoFooter\"></div>";
4404
+
4405
+ var children = [
4406
+ $leftCol,
4407
+ $rightCol,
4408
+ '<br style="clear:both"/>'
4409
+ ];
4410
+ var childrenEl = $('<div class="BRinfoW mv20-lg">').append(children);
4411
+
4412
+ jInfoDiv.append(
4413
+ childrenEl,
4414
+ $(footerEl)
4415
+ ).addClass('wide');
4416
+ };
4417
+
4418
+ // Can be overriden
4419
+ BookReader.prototype.initUIStrings = function() {
4420
+ // Navigation handlers will be bound after all UI is in place -- makes moving icons between
4421
+ // the toolbar and nav bar easier
4422
+
4423
+ // Setup tooltips -- later we could load these from a file for i18n
4424
+ var titles = { '.logo': 'Go to Archive.org', // $$$ update after getting OL record
4425
+ '.zoom_in': 'Zoom in',
4426
+ '.zoom_out': 'Zoom out',
4427
+ '.onepg': 'One-page view',
4428
+ '.twopg': 'Two-page view',
4429
+ '.thumb': 'Thumbnail view',
4430
+ '.print': 'Print this page',
4431
+ '.embed': 'Embed BookReader',
4432
+ '.link': 'Link to this book (and page)',
4433
+ '.bookmark': 'Bookmark this page',
4434
+ '.read': 'Read this book aloud',
4435
+ '.share': 'Share this book',
4436
+ '.info': 'About this book',
4437
+ '.full': 'Toggle fullscreen',
4438
+ '.book_left': 'Flip left',
4439
+ '.book_right': 'Flip right',
4440
+ '.book_up': 'Page up',
4441
+ '.book_down': 'Page down',
4442
+ '.play': 'Play',
4443
+ '.pause': 'Pause',
4444
+ '.BRdn': 'Show/hide nav bar', // Would have to keep updating on state change to have just "Hide nav bar"
4445
+ '.BRup': 'Show/hide nav bar',
4446
+ '.book_top': 'First page',
4447
+ '.book_bottom': 'Last page'
4448
+ };
4449
+ if ('rl' == this.pageProgression) {
4450
+ titles['.book_leftmost'] = 'Last page';
4451
+ titles['.book_rightmost'] = 'First page';
4452
+ } else { // LTR
4453
+ titles['.book_leftmost'] = 'First page';
4454
+ titles['.book_rightmost'] = 'Last page';
4455
+ }
4456
+
4457
+ for (var icon in titles) {
4458
+ if (titles.hasOwnProperty(icon)) {
4459
+ this.$(icon).prop('title', titles[icon]);
4460
+ }
4461
+ }
4462
+ }
4463
+
4464
+ /**
4465
+ * Helper opens a popup window. On mobile it only opens a new tab. Used for share.
4466
+ */
4467
+ BookReader.prototype.createPopup = function(href, width, height, name) {
4468
+ // Fixes dual-screen position
4469
+ var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left;
4470
+ var dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top;
4471
+
4472
+ var win_w = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
4473
+ var win_h = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
4474
+
4475
+ var left = ((win_w / 2) - (width / 2)) + dualScreenLeft,
4476
+ top = ((win_h / 2) - (height / 2)) + dualScreenTop,
4477
+ url = href,
4478
+ opts = 'status=1' +
4479
+ ',width=' + width +
4480
+ ',height=' + height +
4481
+ ',top=' + top +
4482
+ ',left=' + left;
4483
+
4484
+ window.open(url, name, opts);
4485
+ };
4486
+
4487
+ /**
4488
+ * Reloads images. Useful when some images might have failed.
4489
+ */
4490
+ BookReader.prototype.reloadImages = function() {
4491
+ this.refs.$brContainer.find('img').each(function(index, elem) {
4492
+ if (!elem.complete || elem.naturalHeight === 0) {
4493
+ var src = elem.src;
4494
+ elem.src = '';
4495
+ setTimeout(function() {
4496
+ elem.src = src;
4497
+ }, 1000);
4498
+ }
4499
+ });
4500
+ };
4501
+
4502
+ BookReader.prototype.getToolBarHeight = function() {
4503
+ if (this.refs.$BRtoolbar && this.refs.$BRtoolbar.css('display') === 'block') {
4504
+ return (this.refs.$BRtoolbar.outerHeight() + parseInt(this.refs.$BRtoolbar.css('top')));
4505
+ } else {
4506
+ return 0;
4507
+ }
4508
+ }
4509
+
4510
+ /**
4511
+ * @param {boolean} ignoreDisplay - bypass the display check
4512
+ * @return {Number}
4513
+ */
4514
+ BookReader.prototype.getNavHeight = function() {
4515
+ if (this.refs.$BRnav) {
4516
+ var outerHeight = this.refs.$BRnav.outerHeight();
4517
+ var bottom = parseInt(this.refs.$BRnav.css('bottom'));
4518
+ if (!isNaN(outerHeight) && !isNaN(bottom)) {
4519
+ return outerHeight + bottom;
4520
+ }
4521
+ }
4522
+ return 0;
4523
+ }
4524
+
4525
+ //------------------------------------------------------------------------------
4526
+ // Basic Usage built-in Methods (can be overridden through options)
4527
+ // This implementation uses options.data value for populating BookReader
4528
+
4529
+ /**
4530
+ * @return {Number} the total number of leafs (like an array length)
4531
+ */
4532
+ BookReader.prototype.getNumLeafs = function() {
4533
+ // For deprecated interface support, if numLeafs is set, use that.
4534
+ if (this.numLeafs !== undefined)
4535
+ return this.numLeafs;
4536
+ return this._getDataFlattened().length;
4537
+ };
4538
+
4539
+ /**
4540
+ * @param {Number} index
4541
+ * @return {Number|undefined}
4542
+ */
4543
+ BookReader.prototype.getPageWidth = function(index) {
4544
+ return this._getDataProp(index, 'width');
4545
+ };
4546
+
4547
+ /**
4548
+ * @param {Number} index
4549
+ * @return {Number|undefined}
4550
+ */
4551
+ BookReader.prototype.getPageHeight = function(index) {
4552
+ return this._getDataProp(index, 'height');
4553
+ };
4554
+
4555
+ /**
4556
+ * @param {Number} index
4557
+ * @param {Number} reduce - not used in default implementation
4558
+ * @param {Number} rotate - not used in default implementation
4559
+ * @return {Number|undefined}
4560
+ */
4561
+ BookReader.prototype.getPageURI = function(index, reduce, rotate) {
4562
+ return this._getDataProp(index, 'uri');
4563
+ };
4564
+
4565
+ /**
4566
+ * @param {Number} index
4567
+ * @return {String} - L or R
4568
+ */
4569
+ BookReader.prototype.getPageSide = function(index) {
4570
+ var pageSide = this._getDataProp(index, 'pageSide');
4571
+ if (!pageSide) {
4572
+ // Fallback for invalid values
4573
+ pageSide = index % 2 === 0 ? 'R' : 'L';
4574
+ }
4575
+ return pageSide;
4576
+ };
4577
+
4578
+ /**
4579
+ * @param {Number} index
4580
+ * @return {String}
4581
+ */
4582
+ BookReader.prototype.getPageNum = function(index) {
4583
+ var pageNum = this._getDataProp(index, 'pageNum');
4584
+ if (pageNum === undefined) {
4585
+ pageNum = 'n' + index;
4586
+ }
4587
+ return pageNum;
4588
+ };
4589
+
4590
+ /**
4591
+ * @param {Number} pindex
4592
+ * @return {Array} - eg [0, 1]
4593
+ */
4594
+ BookReader.prototype.getSpreadIndices = function(pindex) {
4595
+ var spreadIndices = [null, null];
4596
+ if ('rl' == this.pageProgression) {
4597
+ // Right to Left
4598
+ if (this.getPageSide(pindex) == 'R') {
4599
+ spreadIndices[1] = pindex;
4600
+ spreadIndices[0] = pindex + 1;
4601
+ } else {
4602
+ // Given index was LHS
4603
+ spreadIndices[0] = pindex;
4604
+ spreadIndices[1] = pindex - 1;
4605
+ }
4606
+ } else {
4607
+ // Left to right
4608
+ if (this.getPageSide(pindex) == 'L') {
4609
+ spreadIndices[0] = pindex;
4610
+ spreadIndices[1] = pindex + 1;
4611
+ } else {
4612
+ // Given index was RHS
4613
+ spreadIndices[1] = pindex;
4614
+ spreadIndices[0] = pindex - 1;
4615
+ }
4616
+ }
4617
+ return spreadIndices;
4618
+ };
4619
+
4620
+ /**
4621
+ * Override if "leafNum" does not correspond to "index"
4622
+ * @param {Number} index
4623
+ * @return {Number}
4624
+ */
4625
+ BookReader.prototype.leafNumToIndex = function(index) {
4626
+ return index;
4627
+ };
4628
+
4629
+ /**
4630
+ * Create a params object from the current parameters.
4631
+ * @return {Object}
4632
+ */
4633
+ BookReader.prototype.paramsFromCurrent = function() {
4634
+ var params = {};
4635
+
4636
+ var index = this.currentIndex();
4637
+ var pageNum = this.getPageNum(index);
4638
+ if ((pageNum === 0) || pageNum) {
4639
+ params.page = pageNum;
4640
+ }
4641
+
4642
+ params.index = index;
4643
+ params.mode = this.mode;
4644
+
4645
+ // $$$ highlight
4646
+ // $$$ region
4647
+
4648
+ // search
4649
+ if (this.enableSearch) {
4650
+ params.search = this.searchTerm;
4651
+ }
4652
+
4653
+ return params;
4654
+ };
4655
+
4656
+ /**
4657
+ * Return an object with configuration parameters from a fragment string.
4658
+ *
4659
+ * Fragments are formatted as a URL path but may be used outside of URLs as a
4660
+ * serialization format for BookReader parameters
4661
+ *
4662
+ * @see http://openlibrary.org/dev/docs/bookurls for fragment syntax
4663
+ *
4664
+ * @param {String} fragment initial # is allowed for backwards compatibility
4665
+ * but is deprecated
4666
+ * @return {Object}
4667
+ */
4668
+ BookReader.prototype.paramsFromFragment = function(fragment) {
4669
+ var params = {};
4670
+
4671
+ // For backwards compatibility we allow an initial # character
4672
+ // (as from window.location.hash) but don't require it
4673
+ if (fragment.substr(0, 1) == '#') {
4674
+ fragment = fragment.substr(1);
4675
+ }
4676
+
4677
+ // Simple #nn syntax
4678
+ var oldStyleLeafNum = parseInt( /^\d+$/.exec(fragment) );
4679
+ if ( !isNaN(oldStyleLeafNum) ) {
4680
+ params.index = oldStyleLeafNum;
4681
+
4682
+ // Done processing if using old-style syntax
4683
+ return params;
4684
+ }
4685
+
4686
+ // Split into key-value pairs
4687
+ var urlArray = fragment.split('/');
4688
+ var urlHash = {};
4689
+ for (var i = 0; i < urlArray.length; i += 2) {
4690
+ urlHash[urlArray[i]] = urlArray[i+1];
4691
+ }
4692
+
4693
+ // Mode
4694
+ if ('1up' == urlHash['mode']) {
4695
+ params.mode = this.constMode1up;
4696
+ } else if ('2up' == urlHash['mode']) {
4697
+ params.mode = this.constMode2up;
4698
+ } else if ('thumb' == urlHash['mode']) {
4699
+ params.mode = this.constModeThumb;
4700
+ }
4701
+
4702
+ // Index and page
4703
+ if ('undefined' != typeof(urlHash['page'])) {
4704
+ // page was set -- may not be int
4705
+ params.page = urlHash['page'];
4706
+ }
4707
+
4708
+ // $$$ process /region
4709
+ // $$$ process /search
4710
+
4711
+ if (urlHash['search'] != undefined) {
4712
+ params.search = BookReader.util.decodeURIComponentPlus(urlHash['search']);
4713
+ }
4714
+
4715
+ // $$$ process /highlight
4716
+
4717
+ // $$$ process /theme
4718
+ if (urlHash['theme'] != undefined) {
4719
+ params.theme = urlHash['theme']
4720
+ }
4721
+ return params;
4722
+ };
4723
+
4724
+ /**
4725
+ * Create a fragment string from the params object.
4726
+ *
4727
+ * Fragments are formatted as a URL path but may be used outside of URLs as a
4728
+ * serialization format for BookReader parameters
4729
+ *
4730
+ * @see https://openlibrary.org/dev/docs/bookurls for fragment syntax
4731
+ *
4732
+ * @param {Object} params
4733
+ * @return {String}
4734
+ */
4735
+ BookReader.prototype.fragmentFromParams = function(params) {
4736
+ var separator = '/';
4737
+
4738
+ var fragments = [];
4739
+
4740
+ if ('undefined' != typeof(params.page)) {
4741
+ fragments.push('page', params.page);
4742
+ } else {
4743
+ if ('undefined' != typeof(params.index)) {
4744
+ // Don't have page numbering but we do have the index
4745
+ fragments.push('page', 'n' + params.index);
4746
+ }
4747
+ }
4748
+
4749
+ // $$$ highlight
4750
+ // $$$ region
4751
+
4752
+ // mode
4753
+ if ('undefined' != typeof(params.mode)) {
4754
+ if (params.mode == this.constMode1up) {
4755
+ fragments.push('mode', '1up');
4756
+ } else if (params.mode == this.constMode2up) {
4757
+ fragments.push('mode', '2up');
4758
+ } else if (params.mode == this.constModeThumb) {
4759
+ fragments.push('mode', 'thumb');
4760
+ } else {
4761
+ throw 'fragmentFromParams called with unknown mode ' + params.mode;
4762
+ }
4763
+ }
4764
+
4765
+ // search
4766
+ if (params.search) {
4767
+ fragments.push('search', params.search);
4768
+ }
4769
+
4770
+ return BookReader.util.encodeURIComponentPlus(fragments.join(separator)).replace(/%2F/g, '/');
4771
+ };
4772
+
4773
+ /**
4774
+ * Parses the pageString format
4775
+ * @param {string} pageNum
4776
+ * @return {number|undefined}
4777
+ */
4778
+ BookReader.prototype.parsePageString = function(pageNum) {
4779
+ var pageIndex;
4780
+ // Check for special "leaf"
4781
+ var re = new RegExp('^leaf(\\d+)');
4782
+ var leafMatch = re.exec(pageNum);
4783
+ if (leafMatch) {
4784
+ pageIndex = this.leafNumToIndex(parseInt(leafMatch[1], 10));
4785
+ if (pageIndex === null) {
4786
+ pageIndex = undefined; // to match return type of getPageIndex
4787
+ }
4788
+ } else {
4789
+ pageIndex = this.getPageIndex(pageNum);
4790
+ }
4791
+ return pageIndex;
4792
+ }
4793
+
4794
+ /**
4795
+ * Helper. Flatten the nested structure (make 1d array),
4796
+ * and also add pageSide prop
4797
+ * @return {Array}
4798
+ */
4799
+ BookReader.prototype._getDataFlattened = function() {
4800
+ if (this._getDataFlattened.cached && this._getDataFlattened.cached[1] === this.data.length)
4801
+ return this._getDataFlattened.cached[0];
4802
+
4803
+ var flattend = [];
4804
+ var prevPageSide = null;
4805
+ for (var i = 0; i < this.data.length; i++) {
4806
+ for (var j = 0; j < this.data[i].length; j++) {
4807
+ if (prevPageSide === null) {
4808
+ this.data[i][j].pageSide = this.data[i].length === 2 ? 'L' : 'R';
4809
+ } else {
4810
+ this.data[i][j].pageSide = prevPageSide === 'L' ? 'R' : 'L';
4811
+ }
4812
+ prevPageSide = this.data[i][j].pageSide;
4813
+ flattend.push(this.data[i][j]);
4814
+ }
4815
+ }
4816
+ // length is used as a cache breaker
4817
+ this._getDataFlattened.cached = [flattend, this.data.length];
4818
+ return flattend;
4819
+ };
4820
+
4821
+ /**
4822
+ * Helper. Return a prop for a given index
4823
+ * @param {Number} index
4824
+ * @param {String} prop
4825
+ * @return {Array}
4826
+ */
4827
+ BookReader.prototype._getDataProp = function(index, prop) {
4828
+ var dataf = this._getDataFlattened();
4829
+ if (isNaN(index) || index < 0 || index >= dataf.length)
4830
+ return;
4831
+ if ('undefined' == typeof(dataf[index][prop]))
4832
+ return;
4833
+ return dataf[index][prop];
4834
+ };
4835
+
4836
+ BookReader.prototype.$ = function(selector) {
4837
+ return this.refs.$br.find(selector);
4838
+ }
4839
+
4840
+ //------------------------------------------------------------------------------
4841
+
4842
+ // Fix for deprecated method
4843
+ jQuery.curCSS = function(element, prop, val) {
4844
+ return jQuery(element).css(prop, val);
4845
+ };
4846
+
4847
+ return BookReader;
4848
+
4849
+ })(jQuery);