@internetarchive/bookreader 5.0.0-7 → 5.0.0-70-a1

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 (307) hide show
  1. package/.eslintrc.js +17 -15
  2. package/.github/workflows/node.js.yml +73 -10
  3. package/.github/workflows/npm-publish.yml +6 -20
  4. package/.testcaferc.js +10 -0
  5. package/BookReader/BookReader.css +396 -1129
  6. package/BookReader/BookReader.js +1 -1
  7. package/BookReader/BookReader.js.LICENSE.txt +20 -20
  8. package/BookReader/BookReader.js.map +1 -1
  9. package/BookReader/ia-bookreader-bundle.js +1788 -0
  10. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +19 -0
  11. package/BookReader/ia-bookreader-bundle.js.map +1 -0
  12. package/BookReader/icons/1up.svg +1 -1
  13. package/BookReader/icons/2up.svg +1 -1
  14. package/BookReader/icons/advance.svg +1 -1
  15. package/BookReader/icons/chevron-right.svg +1 -1
  16. package/BookReader/icons/close-circle-dark.svg +1 -1
  17. package/BookReader/icons/close-circle.svg +1 -1
  18. package/BookReader/icons/fullscreen.svg +1 -1
  19. package/BookReader/icons/fullscreen_exit.svg +1 -1
  20. package/BookReader/icons/hamburger.svg +1 -1
  21. package/BookReader/icons/left-arrow.svg +1 -1
  22. package/BookReader/icons/magnify-minus.svg +1 -1
  23. package/BookReader/icons/magnify-plus.svg +1 -1
  24. package/BookReader/icons/magnify.svg +1 -1
  25. package/BookReader/icons/pause.svg +1 -1
  26. package/BookReader/icons/play.svg +1 -1
  27. package/BookReader/icons/playback-speed.svg +1 -1
  28. package/BookReader/icons/read-aloud.svg +1 -1
  29. package/BookReader/icons/review.svg +1 -1
  30. package/BookReader/icons/thumbnails.svg +1 -1
  31. package/BookReader/icons/voice.svg +1 -0
  32. package/BookReader/icons/volume-full.svg +1 -1
  33. package/BookReader/images/BRicons.svg +3 -3
  34. package/BookReader/images/books_graphic.svg +1 -1
  35. package/BookReader/images/icon_book.svg +1 -1
  36. package/BookReader/images/icon_bookmark.svg +1 -1
  37. package/BookReader/images/icon_gear.svg +1 -1
  38. package/BookReader/images/icon_hamburger.svg +1 -1
  39. package/BookReader/images/icon_home.svg +1 -1
  40. package/BookReader/images/icon_info.svg +1 -1
  41. package/BookReader/images/icon_one_page.svg +1 -1
  42. package/BookReader/images/icon_pause.svg +1 -1
  43. package/BookReader/images/icon_play.svg +1 -1
  44. package/BookReader/images/icon_playback-rate.svg +1 -1
  45. package/BookReader/images/icon_search_button.svg +1 -1
  46. package/BookReader/images/icon_share.svg +1 -1
  47. package/BookReader/images/icon_skip-ahead.svg +1 -1
  48. package/BookReader/images/icon_skip-back.svg +1 -1
  49. package/BookReader/images/icon_speaker.svg +1 -1
  50. package/BookReader/images/icon_speaker_open.svg +1 -1
  51. package/BookReader/images/icon_thumbnails.svg +1 -1
  52. package/BookReader/images/icon_toc.svg +1 -1
  53. package/BookReader/images/icon_two_pages.svg +1 -1
  54. package/BookReader/images/marker_chap-off.svg +1 -1
  55. package/BookReader/images/marker_chap-on.svg +1 -1
  56. package/BookReader/images/marker_srch-on.svg +1 -1
  57. package/BookReader/jquery-3.js +2 -0
  58. package/BookReader/jquery-3.js.LICENSE.txt +24 -0
  59. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  60. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  61. package/BookReader/plugins/plugin.autoplay.js +1 -1
  62. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  63. package/BookReader/plugins/plugin.chapters.js +25 -1
  64. package/BookReader/plugins/plugin.chapters.js.LICENSE.txt +1 -0
  65. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  66. package/BookReader/plugins/plugin.iframe.js +1 -1
  67. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  68. package/BookReader/plugins/plugin.resume.js +1 -1
  69. package/BookReader/plugins/plugin.resume.js.map +1 -1
  70. package/BookReader/plugins/plugin.search.js +2 -1
  71. package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
  72. package/BookReader/plugins/plugin.search.js.map +1 -1
  73. package/BookReader/plugins/plugin.text_selection.js +2 -1
  74. package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
  75. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  76. package/BookReader/plugins/plugin.tts.js +1 -1
  77. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
  78. package/BookReader/plugins/plugin.tts.js.map +1 -1
  79. package/BookReader/plugins/plugin.url.js +1 -1
  80. package/BookReader/plugins/plugin.url.js.map +1 -1
  81. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  82. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  83. package/BookReader/webcomponents-bundle.js +3 -0
  84. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  85. package/BookReader/webcomponents-bundle.js.map +1 -0
  86. package/BookReaderDemo/BookReaderDemo.css +16 -19
  87. package/BookReaderDemo/BookReaderJSAdvanced.js +0 -3
  88. package/BookReaderDemo/BookReaderJSAutoplay.js +4 -1
  89. package/BookReaderDemo/BookReaderJSSimple.js +1 -0
  90. package/BookReaderDemo/IADemoBr.js +147 -0
  91. package/BookReaderDemo/demo-advanced.html +2 -2
  92. package/BookReaderDemo/demo-autoplay.html +2 -3
  93. package/BookReaderDemo/demo-embed-iframe-src.html +2 -1
  94. package/BookReaderDemo/demo-fullscreen-mobile.html +3 -5
  95. package/BookReaderDemo/demo-fullscreen.html +2 -4
  96. package/BookReaderDemo/demo-iiif.html +2 -1
  97. package/BookReaderDemo/demo-iiif.js +0 -1
  98. package/BookReaderDemo/demo-internetarchive.html +213 -17
  99. package/BookReaderDemo/demo-multiple.html +2 -1
  100. package/BookReaderDemo/demo-preview-pages.html +2 -1
  101. package/BookReaderDemo/demo-simple.html +2 -1
  102. package/BookReaderDemo/demo-vendor-fullscreen.html +2 -4
  103. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  104. package/BookReaderDemo/immersion-1up.html +2 -2
  105. package/BookReaderDemo/immersion-mode.html +2 -4
  106. package/BookReaderDemo/toggle_controls.html +3 -2
  107. package/BookReaderDemo/view_mode.html +2 -1
  108. package/BookReaderDemo/viewmode-cycle.html +2 -3
  109. package/CHANGELOG.md +279 -0
  110. package/README.md +14 -1
  111. package/babel.config.js +20 -0
  112. package/codecov.yml +6 -0
  113. package/index.html +4 -1
  114. package/jsconfig.json +19 -0
  115. package/netlify.toml +9 -0
  116. package/package.json +69 -60
  117. package/renovate.json +52 -0
  118. package/scripts/preversion.js +4 -1
  119. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  120. package/src/BookNavigator/assets/button-base.js +4 -2
  121. package/src/BookNavigator/assets/ia-logo.js +17 -0
  122. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  123. package/src/BookNavigator/assets/icon_close.js +1 -1
  124. package/src/BookNavigator/book-navigator.js +586 -0
  125. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  126. package/src/BookNavigator/bookmarks/bookmark-edit.js +3 -4
  127. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  128. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +3 -8
  129. package/src/BookNavigator/bookmarks/bookmarks-provider.js +27 -17
  130. package/src/BookNavigator/bookmarks/ia-bookmarks.js +116 -67
  131. package/src/BookNavigator/delete-modal-actions.js +1 -1
  132. package/src/BookNavigator/downloads/downloads-provider.js +36 -21
  133. package/src/BookNavigator/downloads/downloads.js +41 -25
  134. package/src/BookNavigator/search/search-provider.js +49 -27
  135. package/src/BookNavigator/search/search-results.js +23 -9
  136. package/src/BookNavigator/sharing.js +27 -0
  137. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +11 -10
  138. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +3 -3
  139. package/src/BookNavigator/volumes/volumes-provider.js +95 -0
  140. package/src/BookReader/BookModel.js +64 -34
  141. package/src/BookReader/DragScrollable.js +233 -0
  142. package/src/BookReader/Mode1Up.js +56 -351
  143. package/src/BookReader/Mode1UpLit.js +388 -0
  144. package/src/BookReader/Mode2Up.js +73 -1318
  145. package/src/BookReader/Mode2UpLit.js +776 -0
  146. package/src/BookReader/ModeCoordinateSpace.js +29 -0
  147. package/src/BookReader/ModeSmoothZoom.js +312 -0
  148. package/src/BookReader/ModeThumb.js +18 -12
  149. package/src/BookReader/Navbar/Navbar.js +12 -38
  150. package/src/BookReader/PageContainer.js +81 -6
  151. package/src/BookReader/ReduceSet.js +1 -1
  152. package/src/BookReader/Toolbar/Toolbar.js +10 -37
  153. package/src/BookReader/events.js +2 -3
  154. package/src/BookReader/options.js +24 -2
  155. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  156. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  157. package/src/BookReader/utils/SelectionObserver.js +43 -0
  158. package/src/BookReader/utils.js +118 -13
  159. package/src/BookReader.js +423 -1056
  160. package/src/assets/icons/magnify-minus.svg +3 -7
  161. package/src/assets/icons/magnify-plus.svg +3 -7
  162. package/src/assets/icons/voice.svg +1 -0
  163. package/src/css/BookReader.scss +1 -5
  164. package/src/css/_BRBookmarks.scss +1 -1
  165. package/src/css/_BRComponent.scss +1 -1
  166. package/src/css/_BRmain.scss +16 -0
  167. package/src/css/_BRnav.scss +11 -38
  168. package/src/css/_BRpages.scss +149 -40
  169. package/src/css/_BRsearch.scss +67 -21
  170. package/src/css/_TextSelection.scss +87 -27
  171. package/src/css/_colorbox.scss +2 -2
  172. package/src/css/_controls.scss +20 -7
  173. package/src/css/_icons.scss +1 -1
  174. package/src/ia-bookreader/ia-bookreader.js +224 -0
  175. package/src/plugins/plugin.archive_analytics.js +3 -3
  176. package/src/plugins/plugin.autoplay.js +5 -11
  177. package/src/plugins/plugin.chapters.js +211 -186
  178. package/src/plugins/plugin.resume.js +3 -3
  179. package/src/plugins/plugin.text_selection.js +464 -134
  180. package/src/plugins/plugin.vendor-fullscreen.js +4 -4
  181. package/src/plugins/search/plugin.search.js +142 -125
  182. package/src/plugins/search/utils.js +43 -0
  183. package/src/plugins/search/view.js +33 -58
  184. package/src/plugins/tts/AbstractTTSEngine.js +46 -37
  185. package/src/plugins/tts/FestivalTTSEngine.js +13 -14
  186. package/src/plugins/tts/PageChunk.js +15 -21
  187. package/src/plugins/tts/PageChunkIterator.js +8 -12
  188. package/src/plugins/tts/WebTTSEngine.js +87 -71
  189. package/src/plugins/tts/plugin.tts.js +95 -126
  190. package/src/plugins/tts/utils.js +0 -25
  191. package/src/plugins/url/UrlPlugin.js +193 -0
  192. package/src/plugins/{plugin.url.js → url/plugin.url.js} +45 -16
  193. package/src/util/browserSniffing.js +22 -0
  194. package/src/util/docCookies.js +21 -2
  195. package/tests/e2e/README.md +37 -0
  196. package/tests/e2e/autoplay.test.js +2 -2
  197. package/tests/e2e/base.test.js +8 -16
  198. package/tests/e2e/helpers/base.js +53 -48
  199. package/tests/e2e/helpers/debug.js +1 -1
  200. package/tests/e2e/helpers/params.js +17 -0
  201. package/tests/e2e/helpers/rightToLeft.js +8 -14
  202. package/tests/e2e/helpers/search.js +73 -0
  203. package/tests/e2e/models/Navigation.js +20 -37
  204. package/tests/e2e/rightToLeft.test.js +4 -5
  205. package/tests/e2e/viewmode.test.js +40 -33
  206. package/tests/jest/BookNavigator/book-navigator.test.js +658 -0
  207. package/tests/jest/BookNavigator/bookmarks/bookmark-button.test.js +43 -0
  208. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  209. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  210. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  211. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +67 -0
  212. package/tests/jest/BookNavigator/downloads/downloads.test.js +53 -0
  213. package/tests/jest/BookNavigator/search/search-provider.test.js +167 -0
  214. package/tests/{karma → jest}/BookNavigator/search/search-results.test.js +109 -60
  215. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +49 -0
  216. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  217. package/tests/jest/BookNavigator/volumes/volumes-provider.test.js +80 -0
  218. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +74 -14
  219. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +193 -0
  220. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  221. package/tests/jest/BookReader/Mode1UpLit.test.js +73 -0
  222. package/tests/jest/BookReader/Mode2Up.test.js +98 -0
  223. package/tests/jest/BookReader/Mode2UpLit.test.js +190 -0
  224. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +16 -0
  225. package/tests/jest/BookReader/ModeSmoothZoom.test.js +218 -0
  226. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  227. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +10 -10
  228. package/tests/{BookReader → jest/BookReader}/PageContainer.test.js +88 -6
  229. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  230. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +2 -2
  231. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  232. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  233. package/tests/jest/BookReader/utils/SelectionObserver.test.js +43 -0
  234. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  235. package/tests/jest/BookReader/utils.test.js +229 -0
  236. package/tests/jest/BookReader.keyboard.test.js +190 -0
  237. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +9 -1
  238. package/tests/{BookReader.test.js → jest/BookReader.test.js} +26 -37
  239. package/tests/{plugins → jest/plugins}/plugin.archive_analytics.test.js +2 -2
  240. package/tests/{plugins → jest/plugins}/plugin.autoplay.test.js +4 -4
  241. package/tests/jest/plugins/plugin.chapters.test.js +145 -0
  242. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +2 -2
  243. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +3 -3
  244. package/tests/jest/plugins/plugin.text_selection.test.js +317 -0
  245. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  246. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +25 -47
  247. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +39 -6
  248. package/tests/jest/plugins/search/utils.js +25 -0
  249. package/tests/jest/plugins/search/utils.test.js +29 -0
  250. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +9 -9
  251. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  252. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  253. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  254. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +47 -1
  255. package/tests/{plugins → jest/plugins}/tts/utils.test.js +1 -60
  256. package/tests/jest/plugins/url/UrlPlugin.test.js +190 -0
  257. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +53 -14
  258. package/tests/jest/setup.js +3 -0
  259. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  260. package/tests/jest/util/docCookies.test.js +24 -0
  261. package/tests/{util → jest/util}/strings.test.js +1 -1
  262. package/tests/{utils.js → jest/utils.js} +38 -0
  263. package/webpack.config.js +11 -6
  264. package/.babelrc +0 -12
  265. package/.dependabot/config.yml +0 -6
  266. package/.testcaferc.json +0 -5
  267. package/BookReader/bookreader-component-bundle.js +0 -1450
  268. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +0 -38
  269. package/BookReader/bookreader-component-bundle.js.map +0 -1
  270. package/BookReader/jquery-1.10.1.js +0 -2
  271. package/BookReader/jquery-1.10.1.js.LICENSE.txt +0 -24
  272. package/BookReader/plugins/plugin.menu_toggle.js +0 -2
  273. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  274. package/BookReader/plugins/plugin.mobile_nav.js +0 -2
  275. package/BookReader/plugins/plugin.mobile_nav.js.map +0 -1
  276. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  277. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  278. package/karma.conf.js +0 -23
  279. package/src/BookNavigator/BookModel.js +0 -14
  280. package/src/BookNavigator/BookNavigator.js +0 -446
  281. package/src/BookNavigator/assets/book-loader.js +0 -27
  282. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  283. package/src/BookNavigator/search/a-search-result.js +0 -55
  284. package/src/BookReader/DebugConsole.js +0 -54
  285. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  286. package/src/ItemNavigator/ItemNavigator.js +0 -376
  287. package/src/ItemNavigator/providers/sharing.js +0 -29
  288. package/src/css/_MobileNav.scss +0 -194
  289. package/src/dragscrollable-br.js +0 -261
  290. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  291. package/src/plugins/plugin.mobile_nav.js +0 -287
  292. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  293. package/tests/BookReader/DebugConsole.test.js +0 -25
  294. package/tests/BookReader/Mode1Up.test.js +0 -164
  295. package/tests/BookReader/Mode2Up.test.js +0 -247
  296. package/tests/BookReader/utils.test.js +0 -109
  297. package/tests/e2e/helpers/desktopSearch.js +0 -72
  298. package/tests/e2e/helpers/mobileSearch.js +0 -85
  299. package/tests/e2e/ia-production/ia-prod-base.js +0 -17
  300. package/tests/karma/BookNavigator/book-navigator.test.js +0 -132
  301. package/tests/karma/BookNavigator/search/search-provider.test.js +0 -23
  302. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
  303. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
  304. package/tests/plugins/plugin.chapters.test.js +0 -130
  305. package/tests/plugins/plugin.mobile_nav.test.js +0 -66
  306. package/tests/plugins/plugin.text_selection.test.js +0 -203
  307. package/tests/util/docCookies.test.js +0 -15
@@ -0,0 +1,29 @@
1
+ import { calcScreenDPI } from './utils';
2
+
3
+ /**
4
+ * There are a few different "coordinate spaces" at play in BR:
5
+ * (1) World units: i.e. inches. Unless otherwise stated, all computations
6
+ * are done in world units.
7
+ * (2) Rendered Pixels: i.e. img.width = '300'. Note this does _not_ take
8
+ * into account zoom scaling.
9
+ * (3) Visible Pixels: Just rendered pixels, but taking into account scaling.
10
+ */
11
+ export class ModeCoordinateSpace {
12
+ screenDPI = calcScreenDPI();
13
+
14
+ /**
15
+ * @param {{ scale: number }} mode
16
+ */
17
+ constructor(mode) {
18
+ this.mode = mode;
19
+ }
20
+
21
+ worldUnitsToRenderedPixels = (/** @type {number} */inches) => inches * this.screenDPI;
22
+ renderedPixelsToWorldUnits = (/** @type {number} */px) => px / this.screenDPI;
23
+
24
+ renderedPixelsToVisiblePixels = (/** @type {number} */px) => px * this.mode.scale;
25
+ visiblePixelsToRenderedPixels = (/** @type {number} */px) => px / this.mode.scale;
26
+
27
+ worldUnitsToVisiblePixels = (/** @type {number} */px) => this.renderedPixelsToVisiblePixels(this.worldUnitsToRenderedPixels(px));
28
+ visiblePixelsToWorldUnits = (/** @type {number} */px) => this.renderedPixelsToWorldUnits(this.visiblePixelsToRenderedPixels(px));
29
+ }
@@ -0,0 +1,312 @@
1
+ // @ts-check
2
+ import interact from 'interactjs';
3
+ import { isIOS, isSamsungInternet } from '../util/browserSniffing.js';
4
+ import { sleep } from './utils.js';
5
+ /** @typedef {import('./utils/HTMLDimensionsCacher.js').HTMLDimensionsCacher} HTMLDimensionsCacher */
6
+
7
+ /**
8
+ * @typedef {object} SmoothZoomable
9
+ * @property {HTMLElement} $container
10
+ * @property {HTMLElement} $visibleWorld
11
+ * @property {import("./options.js").AutoFitValues} autoFit
12
+ * @property {number} scale
13
+ * @property {HTMLDimensionsCacher} htmlDimensionsCacher
14
+ * @property {function(): void} [attachScrollListeners]
15
+ * @property {function(): void} [detachScrollListeners]
16
+ */
17
+
18
+ /** Manages pinch-zoom, ctrl-wheel, and trackpad pinch smooth zooming. */
19
+ export class ModeSmoothZoom {
20
+ /** Position (in unit-less, [0, 1] coordinates) in client to scale around */
21
+ scaleCenter = { x: 0.5, y: 0.5 };
22
+
23
+ /** @param {SmoothZoomable} mode */
24
+ constructor(mode) {
25
+ /** @type {SmoothZoomable} */
26
+ this.mode = mode;
27
+
28
+ /** Whether a pinch is currently happening */
29
+ this.pinching = false;
30
+ /** Non-null when a scale has been enqueued/is being processed by the buffer function */
31
+ this.pinchMoveFrame = null;
32
+ /** Promise for the current/enqueued pinch move frame. Resolves when it is complete. */
33
+ this.pinchMoveFramePromise = Promise.resolve();
34
+ this.oldScale = 1;
35
+ /** @type {{ scale: number, clientX: number, clientY: number }}} */
36
+ this.lastEvent = null;
37
+ this.attached = false;
38
+
39
+ /** @type {function(function(): void): any} */
40
+ this.bufferFn = window.requestAnimationFrame.bind(window);
41
+ }
42
+
43
+ attach() {
44
+ if (this.attached) return;
45
+
46
+ this.attachCtrlZoom();
47
+
48
+ // GestureEvents work only on Safari; they're too glitchy to use
49
+ // fully, but they can sometimes help error correct when interact
50
+ // misses an end/start event on Safari due to Safari bugs.
51
+ this.mode.$container.addEventListener('gesturestart', this._pinchStart);
52
+ this.mode.$container.addEventListener('gesturechange', this._preventEvent);
53
+ this.mode.$container.addEventListener('gestureend', this._pinchEnd);
54
+
55
+ if (isIOS()) {
56
+ this.touchesMonitor = new TouchesMonitor(this.mode.$container);
57
+ this.touchesMonitor.attach();
58
+ }
59
+
60
+ this.mode.$container.style.touchAction = "pan-x pan-y";
61
+
62
+ // The pinch listeners
63
+ this.interact = interact(this.mode.$container);
64
+ this.interact.gesturable({
65
+ listeners: {
66
+ start: this._pinchStart,
67
+ end: this._pinchEnd,
68
+ }
69
+ });
70
+ if (isSamsungInternet()) {
71
+ // Samsung internet pinch-zoom will not work unless we disable
72
+ // all touch actions. So use interact.js' built-in drag support
73
+ // to handle moving on that browser.
74
+ this.mode.$container.style.touchAction = "none";
75
+ this.interact
76
+ .draggable({
77
+ inertia: {
78
+ resistance: 2,
79
+ minSpeed: 100,
80
+ allowResume: true,
81
+ },
82
+ listeners: { move: this._dragMove }
83
+ });
84
+ }
85
+
86
+
87
+ this.attached = true;
88
+ }
89
+
90
+ detach() {
91
+ this.detachCtrlZoom();
92
+
93
+ // GestureEvents work only on Safari; they interfere with Hammer,
94
+ // so block them.
95
+ this.mode.$container.removeEventListener('gesturestart', this._pinchStart);
96
+ this.mode.$container.removeEventListener('gesturechange', this._preventEvent);
97
+ this.mode.$container.removeEventListener('gestureend', this._pinchEnd);
98
+
99
+ this.touchesMonitor?.detach?.();
100
+
101
+ // The pinch listeners
102
+ this.interact.unset();
103
+ interact.removeDocument(document);
104
+
105
+ this.attached = false;
106
+ }
107
+
108
+ /** @param {Event} ev */
109
+ _preventEvent = (ev) => {
110
+ ev.preventDefault();
111
+ return false;
112
+ }
113
+
114
+ _pinchStart = async () => {
115
+ // Safari calls gesturestart twice!
116
+ if (this.pinching) return;
117
+ if (isIOS()) {
118
+ // Safari sometimes causes a pinch to trigger when there's only one touch!
119
+ await sleep(0); // touches monitor can receive the touch event late
120
+ if (this.touchesMonitor.touches < 2) return;
121
+ }
122
+ this.pinching = true;
123
+
124
+ // Do this in case the pinchend hasn't fired yet.
125
+ this.oldScale = 1;
126
+ this.mode.$visibleWorld.classList.add("BRsmooth-zooming");
127
+ this.mode.$visibleWorld.style.willChange = "transform";
128
+ this.mode.autoFit = "none";
129
+ this.detachCtrlZoom();
130
+ this.mode.detachScrollListeners?.();
131
+
132
+ this.interact.gesturable({
133
+ listeners: {
134
+ start: this._pinchStart,
135
+ move: this._pinchMove,
136
+ end: this._pinchEnd,
137
+ }
138
+ });
139
+ }
140
+
141
+ /** @param {{ scale: number, clientX: number, clientY: number }}} e */
142
+ _pinchMove = async (e) => {
143
+ if (!this.pinching) return;
144
+ this.lastEvent = {
145
+ scale: e.scale,
146
+ clientX: e.clientX,
147
+ clientY: e.clientY,
148
+ };
149
+ if (!this.pinchMoveFrame) {
150
+ // Buffer these events; only update the scale when request animation fires
151
+ this.pinchMoveFrame = this.bufferFn(this._drawPinchZoomFrame);
152
+ }
153
+ }
154
+
155
+ _pinchEnd = async () => {
156
+ if (!this.pinching) return;
157
+ this.pinching = false;
158
+ this.interact.gesturable({
159
+ listeners: {
160
+ start: this._pinchStart,
161
+ end: this._pinchEnd,
162
+ }
163
+ });
164
+ // Want this to happen after the pinchMoveFrame,
165
+ // if one is in progress; otherwise setting oldScale
166
+ // messes up the transform.
167
+ await this.pinchMoveFramePromise;
168
+ this.scaleCenter = { x: 0.5, y: 0.5 };
169
+ this.oldScale = 1;
170
+ this.mode.$visibleWorld.classList.remove("BRsmooth-zooming");
171
+ this.mode.$visibleWorld.style.willChange = "auto";
172
+ this.attachCtrlZoom();
173
+ this.mode.attachScrollListeners?.();
174
+ }
175
+
176
+ _drawPinchZoomFrame = async () => {
177
+ // Because of the buffering/various timing locks,
178
+ // this can be called after the pinch has ended, which
179
+ // results in a janky zoom after the pinch.
180
+ if (!this.pinching) {
181
+ this.pinchMoveFrame = null;
182
+ return;
183
+ }
184
+
185
+ this.mode.$container.style.overflow = "hidden";
186
+ this.pinchMoveFramePromiseRes = null;
187
+ this.pinchMoveFramePromise = new Promise(
188
+ (res) => (this.pinchMoveFramePromiseRes = res)
189
+ );
190
+ this.updateScaleCenter({
191
+ clientX: this.lastEvent.clientX,
192
+ clientY: this.lastEvent.clientY,
193
+ });
194
+ const curScale = this.mode.scale;
195
+ const newScale = curScale * this.lastEvent.scale / this.oldScale;
196
+
197
+ if (curScale != newScale) {
198
+ this.mode.scale = newScale;
199
+ await this.pinchMoveFramePromise;
200
+ }
201
+ this.mode.$container.style.overflow = "auto";
202
+ this.oldScale = this.lastEvent.scale;
203
+ this.pinchMoveFrame = null;
204
+ }
205
+
206
+ _dragMove = async (e) => {
207
+ if (this.pinching) {
208
+ await this._pinchEnd();
209
+ }
210
+ this.mode.$container.scrollTop -= e.dy;
211
+ this.mode.$container.scrollLeft -= e.dx;
212
+ }
213
+
214
+ /** @private */
215
+ attachCtrlZoom() {
216
+ window.addEventListener("wheel", this._handleCtrlWheel, { passive: false });
217
+ }
218
+
219
+ /** @private */
220
+ detachCtrlZoom() {
221
+ window.removeEventListener("wheel", this._handleCtrlWheel);
222
+ }
223
+
224
+ /** @param {WheelEvent} ev **/
225
+ _handleCtrlWheel = (ev) => {
226
+ if (!ev.ctrlKey) return;
227
+ ev.preventDefault();
228
+ const zoomMultiplier =
229
+ // Zooming on macs was painfully slow; likely due to their better
230
+ // trackpads. Give them a higher zoom rate.
231
+ /Mac/i.test(navigator.platform)
232
+ ? 0.045
233
+ : // This worked well for me on Windows
234
+ 0.03;
235
+
236
+ // Zoom around the cursor
237
+ this.updateScaleCenter(ev);
238
+ this.mode.autoFit = "none";
239
+ this.mode.scale *= 1 - Math.sign(ev.deltaY) * zoomMultiplier;
240
+ }
241
+
242
+ /**
243
+ * @param {object} param0
244
+ * @param {number} param0.clientX
245
+ * @param {number} param0.clientY
246
+ */
247
+ updateScaleCenter({ clientX, clientY }) {
248
+ const bc = this.mode.htmlDimensionsCacher.boundingClientRect;
249
+ this.scaleCenter = {
250
+ x: (clientX - bc.left) / this.mode.htmlDimensionsCacher.clientWidth,
251
+ y: (clientY - bc.top) / this.mode.htmlDimensionsCacher.clientHeight,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * @param {number} newScale
257
+ * @param {number} oldScale
258
+ */
259
+ updateViewportOnZoom(newScale, oldScale) {
260
+ const container = this.mode.$container;
261
+ const { scrollTop: T, scrollLeft: L } = container;
262
+ const W = this.mode.htmlDimensionsCacher.clientWidth;
263
+ const H = this.mode.htmlDimensionsCacher.clientHeight;
264
+
265
+ // Scale factor change
266
+ const F = newScale / oldScale;
267
+
268
+ // Where in the viewport the zoom is centered on
269
+ const XPOS = this.scaleCenter.x;
270
+ const YPOS = this.scaleCenter.y;
271
+ const oldCenter = {
272
+ x: L + XPOS * W,
273
+ y: T + YPOS * H,
274
+ };
275
+ const newCenter = {
276
+ x: F * oldCenter.x,
277
+ y: F * oldCenter.y,
278
+ };
279
+
280
+ container.scrollTop = newCenter.y - YPOS * H;
281
+ container.scrollLeft = newCenter.x - XPOS * W;
282
+ this.pinchMoveFramePromiseRes?.();
283
+ }
284
+ }
285
+
286
+ export class TouchesMonitor {
287
+ /**
288
+ * @param {HTMLElement} container
289
+ */
290
+ constructor(container) {
291
+ /** @type {HTMLElement} */
292
+ this.container = container;
293
+ this.touches = 0;
294
+ }
295
+
296
+ attach() {
297
+ this.container.addEventListener("touchstart", this._updateTouchCount);
298
+ this.container.addEventListener("touchend", this._updateTouchCount);
299
+ }
300
+
301
+ detach() {
302
+ this.container.removeEventListener("touchstart", this._updateTouchCount);
303
+ this.container.removeEventListener("touchend", this._updateTouchCount);
304
+ }
305
+
306
+ /**
307
+ * @param {TouchEvent} ev
308
+ */
309
+ _updateTouchCount = (ev) => {
310
+ this.touches = ev.touches.length;
311
+ }
312
+ }
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
  import { notInArray, clamp } from './utils.js';
3
3
  import { EVENTS } from './events.js';
4
+ import { DragScrollable } from './DragScrollable.js';
4
5
  /** @typedef {import('../BookREader.js').default} BookReader */
5
6
  /** @typedef {import('./BookModel.js').PageIndex} PageIndex */
6
7
  /** @typedef {import('./BookModel.js').BookModel} BookModel */
@@ -131,7 +132,7 @@ export class ModeThumb {
131
132
  for (let i = 1; i < this.br.thumbRowBuffer; i++) {
132
133
  if (firstRow - i >= 0) { rowsToDisplay.push(firstRow - i); }
133
134
  }
134
- rowsToDisplay.sort();
135
+ rowsToDisplay.sort((a, b) => a - b);
135
136
 
136
137
  // Create the thumbnail divs and images (lazy loaded)
137
138
  for (const row of rowsToDisplay) {
@@ -176,7 +177,7 @@ export class ModeThumb {
176
177
  // shift viewModeOrder after clicking on thumbsnail leaf
177
178
  const nextModeID = this.br.viewModeOrder.shift();
178
179
  this.br.viewModeOrder.push(nextModeID);
179
- this.br.updateViewModeButton($('.viewmode'), 'twopg', 'Two-page view');
180
+ this.br._components.navbar.updateViewModeButton($('.viewmode'), 'twopg', 'Two-page view');
180
181
 
181
182
  this.br.trigger(EVENTS.fragmentChange);
182
183
  event.stopPropagation();
@@ -214,8 +215,6 @@ export class ModeThumb {
214
215
 
215
216
  // highlight current page
216
217
  this.br.$('.pagediv' + this.br.currentIndex()).addClass('BRpagedivthumb_highlight');
217
-
218
- this.br.updateToolbarZoom(this.br.reduce);
219
218
  }
220
219
 
221
220
  /**
@@ -234,20 +233,27 @@ export class ModeThumb {
234
233
  }
235
234
 
236
235
  /**
237
- * @param {1 | -1} direction
236
+ * @param {'in' | 'out'} direction
238
237
  */
239
238
  zoom(direction) {
240
239
  const oldColumns = this.br.thumbColumns;
241
240
  switch (direction) {
242
- case -1:
243
- this.br.thumbColumns += 1;
244
- break;
245
- case 1:
241
+ case 'in':
246
242
  this.br.thumbColumns -= 1;
247
243
  break;
244
+ case 'out':
245
+ this.br.thumbColumns += 1;
246
+ break;
247
+ default:
248
+ console.error(`Unsupported direction: ${direction}`);
248
249
  }
249
250
 
250
- this.br.thumbColumns = clamp(this.br.thumbColumns, 2, 8);
251
+ // Limit zoom in/out columns
252
+ this.br.thumbColumns = clamp(
253
+ this.br.thumbColumns,
254
+ this.br.options.thumbMinZoomColumns,
255
+ this.br.options.thumbMaxZoomColumns
256
+ );
251
257
 
252
258
  if (this.br.thumbColumns != oldColumns) {
253
259
  this.br.displayedRows = []; /* force a gallery redraw */
@@ -279,7 +285,7 @@ export class ModeThumb {
279
285
 
280
286
  this.br.refs.$brPageViewEl = $("<div class='BRpageview'></div>");
281
287
  this.br.refs.$brContainer.append(this.br.refs.$brPageViewEl);
282
- this.br.refs.$brContainer.dragscrollable({preventDefault:true});
288
+ this.dragScrollable = this.dragScrollable || new DragScrollable(this.br.refs.$brContainer[0], {preventDefault: true});
283
289
 
284
290
  this.br.bindGestures(this.br.refs.$brContainer);
285
291
 
@@ -330,7 +336,7 @@ export class ModeThumb {
330
336
  } else {
331
337
  this.br.animating = true;
332
338
  this.br.refs.$brContainer.stop(true)
333
- .animate({ scrollTop: leafTop }, 'fast', () => { this.br.animating = false });
339
+ .animate({ scrollTop: leafTop }, 'fast', () => { this.br.animating = false; });
334
340
  }
335
341
  }
336
342
  }
@@ -4,6 +4,7 @@ import 'jquery-ui/ui/widget.js';
4
4
  import 'jquery-ui/ui/widgets/mouse.js';
5
5
  import 'jquery-ui/ui/widgets/slider.js';
6
6
  import { EVENTS } from '../events.js';
7
+ import { throttle } from '../utils.js';
7
8
 
8
9
  export class Navbar {
9
10
  /**
@@ -27,6 +28,8 @@ export class Navbar {
27
28
  this.maximumControls = [
28
29
  'book_left', 'book_right', 'zoom_in', 'zoom_out', 'onepg', 'twopg', 'thumb'
29
30
  ];
31
+
32
+ this.updateNavIndexThrottled = throttle(this.updateNavIndex.bind(this), 250, false);
30
33
  }
31
34
 
32
35
  controlFor(controlName) {
@@ -38,7 +41,7 @@ export class Navbar {
38
41
  return `<li>
39
42
  <button class="BRicon ${option.className}" title="${option.label}">
40
43
  <div class="icon icon-${option.iconClassName}"></div>
41
- <span class="tooltip">${option.label}</span>
44
+ <span class="BRtooltip">${option.label}</span>
42
45
  </button>
43
46
  </li>`;
44
47
  }
@@ -127,7 +130,7 @@ export class Navbar {
127
130
  .removeClass()
128
131
  .addClass(`icon icon-${iconClass}`)
129
132
  .end()
130
- .find('.tooltip')
133
+ .find('.BRtooltip')
131
134
  .text(tooltipText);
132
135
  }
133
136
 
@@ -213,7 +216,7 @@ export class Navbar {
213
216
  const $slider = this.$root.find('.BRpager').slider({
214
217
  animate: true,
215
218
  min: 0,
216
- max: br.getNumLeafs() - 1,
219
+ max: br.book.getNumLeafs() - 1,
217
220
  value: br.currentIndex(),
218
221
  range: "min"
219
222
  });
@@ -240,35 +243,6 @@ export class Navbar {
240
243
  return this.$nav;
241
244
  }
242
245
 
243
- /**
244
- * Initialize the navigation bar when embedded
245
- */
246
- initEmbed() {
247
- const { br } = this;
248
- // IA-specific
249
- const thisLink = (window.location + '')
250
- .replace('?ui=embed','')
251
- .replace('/stream/', '/details/')
252
- .replace('#', '/');
253
- const logoHtml = br.showLogo ? `<a class="logo" href="${br.logoURL}" target="_blank"></a>` : '';
254
-
255
- br.refs.$BRfooter = this.$root = $('<div class="BRfooter"></div>');
256
- br.refs.$BRnav = this.$nav = $(
257
- `<div class="BRnav BRnavEmbed">
258
- ${logoHtml}
259
- <span class="BRembedreturn">
260
- <a href="${thisLink}" target="_blank">${br.bookTitle}</a>
261
- </span>
262
- <span class="BRtoolbarbuttons">
263
- <button class="BRicon book_left"></button>
264
- <button class="BRicon book_right"></button>
265
- <button class="BRicon full"></button>
266
- </span>
267
- </div>`);
268
- this.$root.append(this.$nav);
269
- br.refs.$br.append(this.$root);
270
- }
271
-
272
246
  /**
273
247
  * Returns the textual representation of the current page for the navbar
274
248
  * @param {number} index
@@ -277,16 +251,16 @@ export class Navbar {
277
251
  getNavPageNumString(index) {
278
252
  const { br } = this;
279
253
  // Accessible index starts at 0 (alas) so we add 1 to make human
280
- const pageNum = br.getPageNum(index);
281
- const pageType = br.getPageProp(index, 'pageType');
282
- const numLeafs = br.getNumLeafs();
254
+ const pageNum = br.book.getPageNum(index);
255
+ const pageType = br.book.getPageProp(index, 'pageType');
256
+ const numLeafs = br.book.getNumLeafs();
283
257
 
284
258
  if (!this.maxPageNum) {
285
259
  // Calculate Max page num (used for pagination display)
286
260
  let maxPageNum = 0;
287
261
  let pageNumVal;
288
262
  for (let i = 0; i < numLeafs; i++) {
289
- pageNumVal = br.getPageNum(i);
263
+ pageNumVal = br.book.getPageNum(i);
290
264
  if (!isNaN(pageNumVal) && pageNumVal > maxPageNum) {
291
265
  maxPageNum = pageNumVal;
292
266
  }
@@ -331,9 +305,9 @@ export function getNavPageNumHtml(index, numLeafs, pageNum, pageType, maxPageNum
331
305
 
332
306
  if (!pageIsAsserted) {
333
307
  const pageIndex = index + 1;
334
- return `Page (${pageIndex} of ${numLeafs})`; // Page (8 of 10)
308
+ return `(${pageIndex} of ${numLeafs})`; // Page (8 of 10)
335
309
  }
336
310
 
337
311
  const bookLengthLabel = maxPageNum ? ` of ${maxPageNum}` : '';
338
- return `Page ${pageNum}${bookLengthLabel}`;
312
+ return `${pageNum}${bookLengthLabel}`;
339
313
  }
@@ -46,11 +46,17 @@ export class PageContainer {
46
46
  const alreadyLoaded = this.imageCache.imageLoaded(this.page.index, reduce);
47
47
  const nextBestLoadedReduce = !alreadyLoaded && this.imageCache.getBestLoadedReduce(this.page.index, reduce);
48
48
 
49
- // Add the actual, highres image
49
+ // Create high res image
50
+ const $newImg = this.imageCache.image(this.page.index, reduce);
51
+
52
+ // Avoid removing/re-adding the image if it's already there
53
+ // This can be called quite a bit, so we need to be fast
54
+ if (this.$img?.[0].src == $newImg[0].src) {
55
+ return this;
56
+ }
57
+
50
58
  this.$img?.remove();
51
- this.$img = this.imageCache
52
- .image(this.page.index, reduce)
53
- .prependTo(this.$container);
59
+ this.$img = $newImg.prependTo(this.$container);
54
60
 
55
61
  const backgroundLayers = [];
56
62
  if (!alreadyLoaded) {
@@ -58,14 +64,14 @@ export class PageContainer {
58
64
  backgroundLayers.push(`url("${this.loadingImage}") center/20px no-repeat`);
59
65
  }
60
66
  if (nextBestLoadedReduce) {
61
- backgroundLayers.push(`url("${this.page.getURI(nextBestLoadedReduce, 0)}") center/100% no-repeat`);
67
+ backgroundLayers.push(`url("${this.page.getURI(nextBestLoadedReduce, 0)}") center/100% 100% no-repeat`);
62
68
  }
63
69
 
64
70
  if (!alreadyLoaded) {
65
71
  this.$img
66
72
  .css('background', backgroundLayers.join(','))
67
73
  .one('loadend', async (ev) => {
68
- $(ev.target).css({ 'background': '' })
74
+ $(ev.target).css({ 'background': '' });
69
75
  $(ev.target).parent().removeClass('BRpageloading');
70
76
  });
71
77
  }
@@ -73,3 +79,72 @@ export class PageContainer {
73
79
  return this;
74
80
  }
75
81
  }
82
+
83
+
84
+ /**
85
+ * @param {PageModel} page
86
+ * @param {string} className
87
+ */
88
+ export function createSVGPageLayer(page, className) {
89
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
90
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
91
+ svg.setAttribute("viewBox", `0 0 ${page.width} ${page.height}`);
92
+ svg.setAttribute('class', `BRPageLayer ${className}`);
93
+ svg.setAttribute('preserveAspectRatio', 'none');
94
+ return svg;
95
+ }
96
+
97
+ /**
98
+ * @param {PageModel} page
99
+ * @param {string} className
100
+ */
101
+ export function createDIVPageLayer(page, className) {
102
+ const div = document.createElement("div");
103
+ div.style.width = `${page.width}px`;
104
+ div.style.height = `${page.height}px`;
105
+ div.setAttribute('class', `BRPageLayer ${className}`);
106
+ return div;
107
+ }
108
+
109
+ /**
110
+ * @param {{ l: number, r: number, b: number, t: number }} box
111
+ */
112
+ export function boxToSVGRect({ l: left, r: right, b: bottom, t: top }) {
113
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
114
+ rect.setAttribute("x", left.toString());
115
+ rect.setAttribute("y", top.toString());
116
+ rect.setAttribute("width", (right - left).toString());
117
+ rect.setAttribute("height", (bottom - top).toString());
118
+
119
+ // Some style; corner radius 4px. Can't set this in CSS yet
120
+ rect.setAttribute("rx", "4");
121
+ rect.setAttribute("ry", "4");
122
+ return rect;
123
+ }
124
+
125
+ /**
126
+ * @param {string} layerClass
127
+ * @param {Array<{ l: number, r: number, b: number, t: number }>} boxes
128
+ * @param {PageModel} page
129
+ * @param {HTMLElement} containerEl
130
+ * @param {string[]} [rectClasses] CSS classes to add to the rects
131
+ */
132
+ export function renderBoxesInPageContainerLayer(layerClass, boxes, page, containerEl, rectClasses = null) {
133
+ const mountedSvg = containerEl.querySelector(`.${layerClass}`);
134
+ // Create the layer if it's not there
135
+ const svg = mountedSvg || createSVGPageLayer(page, layerClass);
136
+ if (!mountedSvg) {
137
+ // Insert after the image if the image is already loaded.
138
+ const imgEl = containerEl.querySelector('.BRpageimage');
139
+ if (imgEl) $(svg).insertAfter(imgEl);
140
+ else $(svg).prependTo(containerEl);
141
+ }
142
+
143
+ for (const [i, box] of boxes.entries()) {
144
+ const rect = boxToSVGRect(box);
145
+ if (rectClasses) {
146
+ rect.setAttribute('class', rectClasses[i]);
147
+ }
148
+ svg.appendChild(rect);
149
+ }
150
+ }
@@ -18,7 +18,7 @@ export const Pow2ReduceSet = {
18
18
  decr(n) {
19
19
  return 2 ** (Math.log2(n) - 1);
20
20
  }
21
- }
21
+ };
22
22
 
23
23
  export const NAMED_REDUCE_SETS = {
24
24
  pow2: Pow2ReduceSet,