@khanacademy/perseus 0.0.1 → 0.2.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 (507) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/dist/es/index.css +4361 -0
  4. package/dist/es/index.js +7 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +4361 -0
  7. package/dist/index.js +7 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +148 -46
  10. package/package.json +50 -4
  11. package/src/README.md +35 -0
  12. package/src/__stories__/item-renderer.stories.jsx +27 -0
  13. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  14. package/src/__testdata__/item-renderer_testdata.js +161 -0
  15. package/src/__testdata__/renderer_testdata.js +123 -0
  16. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
  17. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
  18. package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
  19. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
  20. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
  21. package/src/__tests__/a11y_test.jsx +304 -0
  22. package/src/__tests__/item-renderer_test.jsx +892 -0
  23. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  24. package/src/__tests__/mock-widget.jsx +35 -0
  25. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  26. package/src/__tests__/pure-markdown_test.js +552 -0
  27. package/src/__tests__/renderability_test.jsx +481 -0
  28. package/src/__tests__/renderer-api_test.jsx +333 -0
  29. package/src/__tests__/renderer_test.jsx +1792 -0
  30. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  31. package/src/__tests__/test-items/image-item.js +25 -0
  32. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  33. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  34. package/src/__tests__/test-items/table-item.js +30 -0
  35. package/src/__tests__/tex-wrangler_test.js +83 -0
  36. package/src/__tests__/util_test.js +41 -0
  37. package/src/a11y.js +46 -0
  38. package/src/all-widgets.js +9 -0
  39. package/src/article-renderer.jsx +215 -0
  40. package/src/asset-context.js +30 -0
  41. package/src/basic-widgets.js +13 -0
  42. package/src/components/__stories__/button-group.stories.jsx +61 -0
  43. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  44. package/src/components/__stories__/graph.stories.jsx +27 -0
  45. package/src/components/__stories__/graphie.stories.jsx +28 -0
  46. package/src/components/__stories__/hud.stories.jsx +36 -0
  47. package/src/components/__stories__/icon.stories.jsx +56 -0
  48. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  49. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  50. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  51. package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
  52. package/src/components/__stories__/lint.stories.jsx +40 -0
  53. package/src/components/__stories__/math-input.stories.jsx +29 -0
  54. package/src/components/__stories__/math-output.stories.jsx +24 -0
  55. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  56. package/src/components/__stories__/number-input.stories.jsx +42 -0
  57. package/src/components/__stories__/possible-answers.stories.jsx +22 -0
  58. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  59. package/src/components/__stories__/range-input.stories.jsx +28 -0
  60. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  61. package/src/components/__stories__/sortable.stories.jsx +73 -0
  62. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  63. package/src/components/__stories__/svg-image.stories.jsx +60 -0
  64. package/src/components/__stories__/tex.stories.jsx +18 -0
  65. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  66. package/src/components/__stories__/text-input.stories.jsx +34 -0
  67. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  68. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  69. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  70. package/src/components/__testdata__/sorter_testdata.js +20 -0
  71. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  72. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  73. package/src/components/__tests__/number-input_test.jsx +112 -0
  74. package/src/components/__tests__/sortable_test.jsx +234 -0
  75. package/src/components/__tests__/sorter_test.jsx +111 -0
  76. package/src/components/__tests__/zoomable_test.jsx +388 -0
  77. package/src/components/button-group.jsx +131 -0
  78. package/src/components/constants.js +15 -0
  79. package/src/components/fixed-to-responsive.jsx +164 -0
  80. package/src/components/graph.jsx +415 -0
  81. package/src/components/graphie-classes.jsx +148 -0
  82. package/src/components/graphie-movables.jsx +342 -0
  83. package/src/components/graphie.jsx +353 -0
  84. package/src/components/highlighting/highlightable-content.jsx +245 -0
  85. package/src/components/highlighting/highlights.js +208 -0
  86. package/src/components/highlighting/ranges.js +299 -0
  87. package/src/components/highlighting/types.js +73 -0
  88. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  89. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  90. package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
  91. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  92. package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
  93. package/src/components/highlighting/ui/types.js +21 -0
  94. package/src/components/highlighting/ui/util.js +166 -0
  95. package/src/components/highlighting/word-indexer.jsx +155 -0
  96. package/src/components/hud.jsx +179 -0
  97. package/src/components/icon.jsx +149 -0
  98. package/src/components/image-loader.jsx +175 -0
  99. package/src/components/info-tip/info-tip.jsx +146 -0
  100. package/src/components/info-tip.jsx +38 -0
  101. package/src/components/inline-icon.jsx +81 -0
  102. package/src/components/input-with-examples.jsx +231 -0
  103. package/src/components/lint.jsx +454 -0
  104. package/src/components/math-input.jsx +282 -0
  105. package/src/components/math-output.jsx +128 -0
  106. package/src/components/multi-button-group.jsx +137 -0
  107. package/src/components/new-tooltip/README.md +110 -0
  108. package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
  109. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
  110. package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
  111. package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
  112. package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
  113. package/src/components/new-tooltip/__tests__/util_test.js +47 -0
  114. package/src/components/new-tooltip/flexible-position.jsx +182 -0
  115. package/src/components/new-tooltip/new-tooltip.jsx +399 -0
  116. package/src/components/new-tooltip/style-constants.js +13 -0
  117. package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
  118. package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
  119. package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
  120. package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
  121. package/src/components/new-tooltip/types.js +183 -0
  122. package/src/components/new-tooltip/util.js +27 -0
  123. package/src/components/number-input.jsx +253 -0
  124. package/src/components/possible-answers.jsx +36 -0
  125. package/src/components/prop-check-box.jsx +80 -0
  126. package/src/components/range-input.jsx +61 -0
  127. package/src/components/simple-keypad-input.jsx +91 -0
  128. package/src/components/sortable.jsx +873 -0
  129. package/src/components/stub-tag-editor.jsx +54 -0
  130. package/src/components/svg-image.jsx +762 -0
  131. package/src/components/tex-buttons.jsx +322 -0
  132. package/src/components/tex.jsx +70 -0
  133. package/src/components/text-input.jsx +82 -0
  134. package/src/components/text-list-editor.jsx +157 -0
  135. package/src/components/tooltip.jsx +395 -0
  136. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  137. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  138. package/src/components/visibility-observer/visibility-observer.js +109 -0
  139. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  140. package/src/components/zoomable-tex.jsx +70 -0
  141. package/src/components/zoomable.jsx +304 -0
  142. package/src/components.js +17 -0
  143. package/src/dependencies.js +22 -0
  144. package/src/error-boundary.jsx +48 -0
  145. package/src/extra-widgets.js +75 -0
  146. package/src/gorgon/README +40 -0
  147. package/src/gorgon/__tests__/matcher_test.js +497 -0
  148. package/src/gorgon/__tests__/rule_test.js +101 -0
  149. package/src/gorgon/__tests__/rules_test.js +487 -0
  150. package/src/gorgon/__tests__/selector-parser_test.js +52 -0
  151. package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
  152. package/src/gorgon/gorgon.js +290 -0
  153. package/src/gorgon/proptypes.js +29 -0
  154. package/src/gorgon/rule.js +413 -0
  155. package/src/gorgon/rules/absolute-url.js +24 -0
  156. package/src/gorgon/rules/all-rules.js +72 -0
  157. package/src/gorgon/rules/blockquoted-math.js +10 -0
  158. package/src/gorgon/rules/blockquoted-widget.js +10 -0
  159. package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
  160. package/src/gorgon/rules/extra-content-spacing.js +12 -0
  161. package/src/gorgon/rules/heading-level-1.js +14 -0
  162. package/src/gorgon/rules/heading-level-skip.js +20 -0
  163. package/src/gorgon/rules/heading-sentence-case.js +11 -0
  164. package/src/gorgon/rules/heading-title-case.js +63 -0
  165. package/src/gorgon/rules/image-alt-text.js +21 -0
  166. package/src/gorgon/rules/image-in-table.js +10 -0
  167. package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
  168. package/src/gorgon/rules/image-widget.js +50 -0
  169. package/src/gorgon/rules/link-click-here.js +11 -0
  170. package/src/gorgon/rules/lint-utils.js +48 -0
  171. package/src/gorgon/rules/long-paragraph.js +14 -0
  172. package/src/gorgon/rules/math-adjacent.js +10 -0
  173. package/src/gorgon/rules/math-align-extra-break.js +11 -0
  174. package/src/gorgon/rules/math-align-linebreaks.js +43 -0
  175. package/src/gorgon/rules/math-empty.js +10 -0
  176. package/src/gorgon/rules/math-font-size.js +12 -0
  177. package/src/gorgon/rules/math-frac.js +10 -0
  178. package/src/gorgon/rules/math-nested.js +11 -0
  179. package/src/gorgon/rules/math-starts-with-space.js +12 -0
  180. package/src/gorgon/rules/math-text-empty.js +10 -0
  181. package/src/gorgon/rules/math-without-dollars.js +14 -0
  182. package/src/gorgon/rules/nested-lists.js +11 -0
  183. package/src/gorgon/rules/profanity.js +10 -0
  184. package/src/gorgon/rules/table-missing-cells.js +20 -0
  185. package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
  186. package/src/gorgon/rules/unescaped-dollar.js +10 -0
  187. package/src/gorgon/rules/widget-in-table.js +10 -0
  188. package/src/gorgon/selector.js +506 -0
  189. package/src/gorgon/tree-transformer.js +588 -0
  190. package/src/hint-renderer.jsx +159 -0
  191. package/src/hints-renderer.jsx +359 -0
  192. package/src/icon-paths.js +120 -0
  193. package/src/index.js +149 -2
  194. package/src/init.js +49 -0
  195. package/src/interaction-tracker.js +60 -0
  196. package/src/interactive2/__tests__/movable-mock.js +48 -0
  197. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  198. package/src/interactive2/interactive-util.js +152 -0
  199. package/src/interactive2/movable-helper-methods.js +160 -0
  200. package/src/interactive2/movable-line-options.js +392 -0
  201. package/src/interactive2/movable-line.js +322 -0
  202. package/src/interactive2/movable-point-options.js +190 -0
  203. package/src/interactive2/movable-point.jsx +603 -0
  204. package/src/interactive2/movable-polygon-options.js +351 -0
  205. package/src/interactive2/movable-polygon.js +368 -0
  206. package/src/interactive2/movable.js +273 -0
  207. package/src/interactive2/objective_.js +79 -0
  208. package/src/interactive2/types.js +3 -0
  209. package/src/interactive2/wrapped-defaults.js +86 -0
  210. package/src/interactive2/wrapped-ellipse.js +87 -0
  211. package/src/interactive2/wrapped-line.js +109 -0
  212. package/src/interactive2/wrapped-path.js +47 -0
  213. package/src/interactive2.js +91 -0
  214. package/src/item-renderer.jsx +518 -0
  215. package/src/item-version.js +12 -0
  216. package/src/jipt-paragraphs.js +44 -0
  217. package/src/jquery.mobile.vmouse.js +545 -0
  218. package/src/keypad-context.js +33 -0
  219. package/src/loading-context.js +20 -0
  220. package/src/logging/log.js +107 -0
  221. package/src/logging/log_test.js +41 -0
  222. package/src/mixins/changeable.jsx +115 -0
  223. package/src/mixins/editor-jsonify.jsx +13 -0
  224. package/src/mixins/provide-keypad.jsx +129 -0
  225. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  226. package/src/mixins/widget-prop-blacklist.js +36 -0
  227. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  228. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  229. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
  230. package/src/multi-items/__tests__/items_test.js +245 -0
  231. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  232. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  233. package/src/multi-items/__tests__/shapes_test.js +44 -0
  234. package/src/multi-items/__tests__/trees_test.js +316 -0
  235. package/src/multi-items/item-types.js +55 -0
  236. package/src/multi-items/items.js +176 -0
  237. package/src/multi-items/multi-renderer.jsx +543 -0
  238. package/src/multi-items/prop-type-builders.js +105 -0
  239. package/src/multi-items/shape-types.js +47 -0
  240. package/src/multi-items/shapes.js +53 -0
  241. package/src/multi-items/tree-types.js +42 -0
  242. package/src/multi-items/trees.js +345 -0
  243. package/src/multi-items.js +76 -0
  244. package/src/not-gorgon.js +129 -0
  245. package/src/perseus-api.jsx +270 -0
  246. package/src/perseus-env.js +26 -0
  247. package/src/perseus-error.js +19 -0
  248. package/src/perseus-markdown.jsx +433 -0
  249. package/src/perseus-types.js +1597 -0
  250. package/src/pure-markdown.js +347 -0
  251. package/src/question-paragraph.jsx +30 -0
  252. package/src/renderability.jsx +108 -0
  253. package/src/renderer.jsx +1932 -0
  254. package/src/server-item-renderer.jsx +467 -0
  255. package/src/sigfigs.jsx +492 -0
  256. package/src/styles/articles.less +748 -0
  257. package/src/styles/constants.js +119 -0
  258. package/src/styles/global-constants.js +350 -0
  259. package/src/styles/global-styles.js +307 -0
  260. package/src/styles/khan-exercise.css +1304 -0
  261. package/src/styles/media-queries.js +48 -0
  262. package/src/styles/perseus-renderer.less +1007 -0
  263. package/src/styles/shared.js +132 -0
  264. package/src/styles/util.less +47 -0
  265. package/src/styles/variables.boostrap.less +301 -0
  266. package/src/styles/variables.less +68 -0
  267. package/src/styles/variables.shared.less +356 -0
  268. package/src/styles/widgets/categorizer.less +39 -0
  269. package/src/styles/widgets/definition.less +86 -0
  270. package/src/styles/widgets/dropdown.less +3 -0
  271. package/src/styles/widgets/expression.less +147 -0
  272. package/src/styles/widgets/graded-group.less +36 -0
  273. package/src/styles/widgets/grapher.less +11 -0
  274. package/src/styles/widgets/group.less +19 -0
  275. package/src/styles/widgets/image.less +29 -0
  276. package/src/styles/widgets/interactive-graph.less +61 -0
  277. package/src/styles/widgets/label-image.less +41 -0
  278. package/src/styles/widgets/matcher.less +8 -0
  279. package/src/styles/widgets/matrix.less +120 -0
  280. package/src/styles/widgets/measurer.less +20 -0
  281. package/src/styles/widgets/orderer.less +97 -0
  282. package/src/styles/widgets/passage.less +151 -0
  283. package/src/styles/widgets/plotter.less +33 -0
  284. package/src/styles/widgets/radio.less +52 -0
  285. package/src/styles/widgets/reaction-diagram.less +35 -0
  286. package/src/styles/widgets/sequence.less +5 -0
  287. package/src/styles/widgets/sortable.less +127 -0
  288. package/src/styles/widgets/table.less +56 -0
  289. package/src/styles/widgets/transformer.less +28 -0
  290. package/src/styles/widgets/unit.less +14 -0
  291. package/src/tex-wrangler.js +176 -0
  292. package/src/traversal.jsx +141 -0
  293. package/src/types.js +538 -0
  294. package/src/util/a11y.js +24 -0
  295. package/src/util/answer-types.js +862 -0
  296. package/src/util/answer-types_test.js +115 -0
  297. package/src/util/colors.js +94 -0
  298. package/src/util/fix-passage-refs.jsx +122 -0
  299. package/src/util/graph-utils.js +16 -0
  300. package/src/util/graphie.js +1586 -0
  301. package/src/util/interactive.js +4078 -0
  302. package/src/util/katex-preprocess.js +16 -0
  303. package/src/util/math.js +296 -0
  304. package/src/util/mobile-native-utils.js +14 -0
  305. package/src/util/mobile-native-utils_test.js +24 -0
  306. package/src/util/register-all-widgets-for-testing.js +12 -0
  307. package/src/util/scroll-utils.js +162 -0
  308. package/src/util/sizing-utils.js +46 -0
  309. package/src/util/tex.js +203 -0
  310. package/src/util/url-utils.js +36 -0
  311. package/src/util.js +990 -0
  312. package/src/version.js +11 -0
  313. package/src/widget-container.jsx +174 -0
  314. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  315. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  316. package/src/widgets/__stories__/definition.stories.jsx +38 -0
  317. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  318. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  319. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  320. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  321. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  322. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  323. package/src/widgets/__stories__/group.stories.jsx +15 -0
  324. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  325. package/src/widgets/__stories__/image.stories.jsx +62 -0
  326. package/src/widgets/__stories__/input-number.stories.jsx +90 -0
  327. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  328. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  329. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  330. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  331. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  332. package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
  333. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  334. package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
  335. package/src/widgets/__stories__/passage.stories.jsx +19 -0
  336. package/src/widgets/__stories__/radio.stories.jsx +82 -0
  337. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  338. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  339. package/src/widgets/__stories__/unit.stories.jsx +45 -0
  340. package/src/widgets/__stories__/video-transcript-link.stories.jsx +24 -0
  341. package/src/widgets/__stories__/video.stories.jsx +15 -0
  342. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  343. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  344. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  345. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  346. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  347. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  348. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  349. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  350. package/src/widgets/__testdata__/group_testdata.js +156 -0
  351. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  352. package/src/widgets/__testdata__/image_testdata.js +38 -0
  353. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  354. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  355. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  356. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  357. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  358. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  359. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  360. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  361. package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
  362. package/src/widgets/__testdata__/passage_testdata.js +71 -0
  363. package/src/widgets/__testdata__/radio_testdata.js +210 -0
  364. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  365. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  366. package/src/widgets/__testdata__/video_testdata.js +22 -0
  367. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
  368. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  369. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
  370. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
  371. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  372. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  373. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
  374. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
  375. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
  376. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
  377. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
  378. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  379. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
  380. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  381. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  382. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
  383. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  384. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
  385. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  386. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
  387. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  388. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  389. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  390. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
  391. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
  392. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  393. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  394. package/src/widgets/__tests__/definition_test.js +152 -0
  395. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  396. package/src/widgets/__tests__/explanation_test.js +199 -0
  397. package/src/widgets/__tests__/expression_test.jsx +439 -0
  398. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
  399. package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
  400. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  401. package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
  402. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  403. package/src/widgets/__tests__/group_test.jsx +512 -0
  404. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  405. package/src/widgets/__tests__/image_test.jsx +65 -0
  406. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  407. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  408. package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
  409. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  410. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  411. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  412. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  413. package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
  414. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  415. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  416. package/src/widgets/__tests__/passage_test.jsx +133 -0
  417. package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
  418. package/src/widgets/__tests__/radio_test.jsx +651 -0
  419. package/src/widgets/__tests__/renderQuestion.jsx +66 -0
  420. package/src/widgets/__tests__/unit_test.jsx +144 -0
  421. package/src/widgets/__tests__/video_test.jsx +55 -0
  422. package/src/widgets/categorizer.jsx +338 -0
  423. package/src/widgets/cs-program.jsx +227 -0
  424. package/src/widgets/definition.jsx +141 -0
  425. package/src/widgets/dropdown.jsx +143 -0
  426. package/src/widgets/explanation.jsx +315 -0
  427. package/src/widgets/expression.jsx +652 -0
  428. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  429. package/src/widgets/graded-group-set.jsx +285 -0
  430. package/src/widgets/graded-group.jsx +448 -0
  431. package/src/widgets/grapher/util.jsx +806 -0
  432. package/src/widgets/grapher.jsx +663 -0
  433. package/src/widgets/group.jsx +213 -0
  434. package/src/widgets/iframe.jsx +198 -0
  435. package/src/widgets/image.jsx +310 -0
  436. package/src/widgets/input-number.jsx +411 -0
  437. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  438. package/src/widgets/interaction/color-picker.jsx +69 -0
  439. package/src/widgets/interaction/dash-picker.jsx +46 -0
  440. package/src/widgets/interaction/element-container.jsx +97 -0
  441. package/src/widgets/interaction.jsx +742 -0
  442. package/src/widgets/interactive-graph.jsx +2820 -0
  443. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  444. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  445. package/src/widgets/label-image/answer-choices.jsx +311 -0
  446. package/src/widgets/label-image/marker.jsx +365 -0
  447. package/src/widgets/label-image/types.js +33 -0
  448. package/src/widgets/label-image.jsx +841 -0
  449. package/src/widgets/label-image_test.js +840 -0
  450. package/src/widgets/lights-puzzle.jsx +356 -0
  451. package/src/widgets/matcher.jsx +271 -0
  452. package/src/widgets/matrix.jsx +616 -0
  453. package/src/widgets/measurer.jsx +215 -0
  454. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  455. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  456. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  457. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  458. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  459. package/src/widgets/molecule.jsx +172 -0
  460. package/src/widgets/number-line.jsx +858 -0
  461. package/src/widgets/numeric-input.jsx +622 -0
  462. package/src/widgets/orderer.jsx +753 -0
  463. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
  464. package/src/widgets/passage/get-line-height-for-node.js +10 -0
  465. package/src/widgets/passage/passage-markdown.jsx +416 -0
  466. package/src/widgets/passage-ref-target.jsx +70 -0
  467. package/src/widgets/passage-ref.jsx +198 -0
  468. package/src/widgets/passage.jsx +601 -0
  469. package/src/widgets/plotter.jsx +1198 -0
  470. package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
  471. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
  472. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
  473. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
  474. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
  475. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
  476. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
  477. package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
  478. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
  479. package/src/widgets/radio/base-radio.jsx +642 -0
  480. package/src/widgets/radio/choice-icon.jsx +468 -0
  481. package/src/widgets/radio/choice-none-above.jsx +54 -0
  482. package/src/widgets/radio/choice.jsx +692 -0
  483. package/src/widgets/radio/choice.jsx.fixture.js +64 -0
  484. package/src/widgets/radio/cross-out-button.jsx +136 -0
  485. package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
  486. package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
  487. package/src/widgets/radio/focus-ring.jsx +51 -0
  488. package/src/widgets/radio/option-status.jsx +89 -0
  489. package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
  490. package/src/widgets/radio/util.js +32 -0
  491. package/src/widgets/radio/util_test.js +19 -0
  492. package/src/widgets/radio/widget.jsx +470 -0
  493. package/src/widgets/radio.jsx +156 -0
  494. package/src/widgets/reaction-diagram.jsx +137 -0
  495. package/src/widgets/sequence.jsx +140 -0
  496. package/src/widgets/simulator.jsx +808 -0
  497. package/src/widgets/sorter.jsx +146 -0
  498. package/src/widgets/table.jsx +394 -0
  499. package/src/widgets/transformer.jsx +2876 -0
  500. package/src/widgets/unit.jsx +372 -0
  501. package/src/widgets/video-transcript-link.jsx +84 -0
  502. package/src/widgets/video-transcript-link_test.js +211 -0
  503. package/src/widgets/video.jsx +126 -0
  504. package/src/widgets.js +431 -0
  505. package/src/zoom.js +539 -0
  506. package/README.md +0 -3
  507. package/src/__tests__/index.test.js +0 -14
@@ -0,0 +1,762 @@
1
+ /* eslint-disable no-useless-escape, react/no-unsafe */
2
+ // @flow
3
+ import classNames from "classnames";
4
+ import $ from "jquery";
5
+ import * as React from "react";
6
+ import _ from "underscore";
7
+
8
+ import {getDependencies} from "../dependencies.js";
9
+ import {Errors, Log} from "../logging/log.js";
10
+ import {PerseusError} from "../perseus-error.js";
11
+ import Util from "../util.js";
12
+ import * as Zoom from "../zoom.js";
13
+
14
+ import FixedToResponsive from "./fixed-to-responsive.jsx";
15
+ import Graphie from "./graphie.jsx";
16
+ import ImageLoader from "./image-loader.jsx";
17
+
18
+ import type {Alignment, Dimensions} from "../types.js";
19
+ import type {ImageProps} from "./image-loader.jsx";
20
+
21
+ // Minimum image width to make an image appear as zoomable.
22
+ const ZOOMABLE_THRESHOLD = 700;
23
+
24
+ // The global cache of label data. Its format is:
25
+ // {
26
+ // hash (e.g. "c21435944d2cf0c8f39d9059cb35836aa701d04a"): {
27
+ // loaded: a boolean of whether the data has been loaded or not
28
+ // dataCallbacks: a list of callbacks to call with the data when the data
29
+ // is loaded
30
+ // data: the other data for this hash
31
+ // },
32
+ // ...
33
+ // }
34
+ const labelDataCache = {};
35
+
36
+ // Write our own JSONP handler because all the other ones don't do things we
37
+ // need.
38
+ const doJSONP = function (url, options) {
39
+ options = {
40
+ callbackName: "callback",
41
+ success: $.noop,
42
+ error: $.noop,
43
+ ...options,
44
+ };
45
+
46
+ // Create the script
47
+ const script = document.createElement("script");
48
+ script.setAttribute("async", "");
49
+ script.setAttribute("src", url);
50
+
51
+ // A cleanup function to run when we're done.
52
+ function cleanup() {
53
+ document.head && document.head.removeChild(script);
54
+ delete window[options.callbackName];
55
+ }
56
+
57
+ // Add the global callback.
58
+ window[options.callbackName] = function () {
59
+ cleanup();
60
+ options.success.apply(null, arguments);
61
+ };
62
+
63
+ // Add the error handler.
64
+ script.addEventListener("error", function () {
65
+ cleanup();
66
+ options.error.apply(null, arguments);
67
+ });
68
+
69
+ // Insert the script to start the download.
70
+ document.head && document.head.appendChild(script);
71
+ };
72
+
73
+ // For offline exercises in the mobile app, we download the graphie data
74
+ // (svgs and localized data files) and serve them from the local file
75
+ // system (with file://). We replace urls that start with `web+graphie`
76
+ // in the perseus json with this `file+graphie` prefix to indicate that
77
+ // they should have the `file://` protocol instead of `https://`.
78
+ const svgLocalLabelsRegex = /^file\+graphie\:/;
79
+ const hashRegex = /\/([^/]+)$/;
80
+
81
+ function isImageProbablyPhotograph(imageUrl) {
82
+ // TODO(david): Do an inventory to refine this heuristic. For example, what
83
+ // % of .png images are illustrations?
84
+ return /\.(jpg|jpeg)$/i.test(imageUrl);
85
+ }
86
+
87
+ function getLocale() {
88
+ const {JIPT, kaLocale} = getDependencies();
89
+ return JIPT.useJIPT ? "en-pt" : kaLocale;
90
+ }
91
+
92
+ function shouldUseLocalizedData() {
93
+ return getLocale() !== "en";
94
+ }
95
+
96
+ // A regex to split at the last / of a URL, separating the base part from the
97
+ // hash. This is used to create the localized label data URLs.
98
+ const splitHashRegex = /\/(?=[^/]+$)/;
99
+
100
+ function getLocalizedDataUrl(url) {
101
+ // For local (cached) graphie images, they are already localized.
102
+ if (svgLocalLabelsRegex.test(url)) {
103
+ return Util.getDataUrl(url);
104
+ }
105
+ const [base, hash] = Util.getBaseUrl(url).split(splitHashRegex);
106
+ return `${base}/${getLocale()}/${hash}-data.json`;
107
+ }
108
+
109
+ // Get the hash from the url, which is just the filename
110
+ function getUrlHash(url) {
111
+ const match = url.match(hashRegex);
112
+ if (match == null) {
113
+ throw new PerseusError("not a valid URL", Errors.InvalidInput);
114
+ }
115
+ return match && match[1];
116
+ }
117
+
118
+ function defaultPreloader(dimensions: Dimensions) {
119
+ return (
120
+ <span
121
+ style={{
122
+ background: "url(/images/spinner.gif) no-repeat",
123
+ backgroundPosition: "center",
124
+ width: "100%",
125
+ height: "100%",
126
+ position: "absolute",
127
+ minWidth: "20px",
128
+ }}
129
+ />
130
+ );
131
+ }
132
+
133
+ type Props = {|
134
+ allowFullBleed?: boolean,
135
+ alt: string,
136
+ constrainHeight?: boolean,
137
+
138
+ extraGraphie?: {
139
+ box: $ReadOnlyArray<any>,
140
+ range: $ReadOnlyArray<any>,
141
+ labels: $ReadOnlyArray<any>,
142
+ ...
143
+ },
144
+
145
+ height?: number,
146
+
147
+ // When the DOM updates to replace the preloader with the image, or
148
+ // vice-versa, we trigger this callback.
149
+ onUpdate: () => void,
150
+ // If alt is provided, DO NOT set aria-hidden=true unless this override flag
151
+ // is set.
152
+ overrideAriaHidden?: boolean,
153
+ preloader?: (dimensions: Dimensions) => React.Node,
154
+
155
+ // By default, this component attempts to be responsive whenever
156
+ // possible (specifically, when width and height are passed in).
157
+ // You can expliclty force unresponsive behavior by *either*
158
+ // not passing in width/height *or* setting this prop to false.
159
+ // The difference is that forcing via this prop will result in
160
+ // explicit width and height styles being set on the rendered
161
+ // component.
162
+ responsive: boolean,
163
+
164
+ scale: number,
165
+ src: string,
166
+ title?: string,
167
+ trackInteraction?: () => void,
168
+ width?: number,
169
+
170
+ // Whether clicking this image will allow it to be fully zoomed in to
171
+ // its original size on click, and allow the user to scroll in that
172
+ // state. This also does some hacky viewport meta tag changing to
173
+ // ensure this works on mobile devices, so I (david@) don't recommend
174
+ // enabling this on desktop yet.
175
+ zoomToFullSizeOnMobile?: boolean,
176
+
177
+ // If provided, use AssetContext.Consumer, see renderer.jsx.
178
+ // If not, it defaults to a no-op.
179
+ setAssetStatus: (assetKey: string, loaded: boolean) => void,
180
+ |};
181
+
182
+ type Label = {|
183
+ coordinates: $ReadOnlyArray<any>,
184
+ content: string,
185
+ alignment: Alignment,
186
+ typesetAsMath: boolean,
187
+ |};
188
+
189
+ type LabelsRenderedMap = {[label: string]: boolean, ...};
190
+
191
+ type State = {|
192
+ // For labeled SVGs, when both dataLoaded and imageLoaded are true,
193
+ // indicates that loading has completed.
194
+ dataLoaded: boolean,
195
+
196
+ imageDimensions: ?[number, number],
197
+
198
+ // Used to indicate when a non-labeled SVG image is finished loading
199
+ // and for labeled SVGs, indicates that it's safe to set up the graphie
200
+ // containing the labels.
201
+ imageLoaded: boolean,
202
+
203
+ // Some graphies have labels which are rendered after the main image is
204
+ // loaded. This object keeps track of whether those labels have been
205
+ // rendered.
206
+ labelsRendered: LabelsRenderedMap,
207
+
208
+ labelDataIsLocalized: boolean,
209
+ labels: $ReadOnlyArray<Label>,
210
+ range: $ReadOnlyArray<any>,
211
+ |};
212
+
213
+ class SvgImage extends React.Component<Props, State> {
214
+ _isMounted: boolean;
215
+
216
+ static defaultProps: {|
217
+ constrainHeight: boolean,
218
+ onUpdate: () => void,
219
+ responsive: boolean,
220
+ scale: number,
221
+ setAssetStatus: (src: string, status: boolean) => void,
222
+ src: string,
223
+ zoomToFullSizeOnMobile: boolean,
224
+ |} = {
225
+ constrainHeight: false,
226
+ onUpdate: () => {},
227
+ responsive: true,
228
+ src: "",
229
+ scale: 1,
230
+ zoomToFullSizeOnMobile: false,
231
+ setAssetStatus: (src: string, status: boolean) => {},
232
+ };
233
+
234
+ constructor(props: Props) {
235
+ super(props);
236
+ props.setAssetStatus(props.src, false);
237
+
238
+ this._isMounted = false;
239
+
240
+ this.state = {
241
+ imageLoaded: false,
242
+ imageDimensions: null,
243
+ dataLoaded: false,
244
+ labelDataIsLocalized: false,
245
+ labels: [],
246
+ labelsRendered: {},
247
+ range: [
248
+ [0, 0],
249
+ [0, 0],
250
+ ],
251
+ };
252
+ }
253
+
254
+ componentDidMount() {
255
+ // TODO(scottgrant): This is a hack to remove the deprecated call to
256
+ // this.isMounted() but is still considered an anti-pattern.
257
+ this._isMounted = true;
258
+
259
+ if (Util.isLabeledSVG(this.props.src)) {
260
+ this.loadResources();
261
+ }
262
+ }
263
+
264
+ UNSAFE_componentWillReceiveProps(nextProps: Props) {
265
+ if (this.props.src !== nextProps.src) {
266
+ this.setState({
267
+ imageLoaded: false,
268
+ dataLoaded: false,
269
+ });
270
+ }
271
+ }
272
+
273
+ shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
274
+ // If the props changed, we definitely need to update
275
+ if (!_.isEqual(this.props, nextProps)) {
276
+ return true;
277
+ }
278
+
279
+ const wasLoaded = this.isLoadedInState(this.state);
280
+ const nextLoaded = this.isLoadedInState(nextState);
281
+
282
+ return wasLoaded !== nextLoaded;
283
+ }
284
+
285
+ componentDidUpdate(prevProps: Props, prevState: State) {
286
+ const wasLoaded = this.isLoadedInState(prevState);
287
+ const isLoaded = this.isLoadedInState(this.state);
288
+
289
+ if (Util.isLabeledSVG(this.props.src) && !isLoaded) {
290
+ this.loadResources();
291
+ }
292
+
293
+ if (!wasLoaded && isLoaded) {
294
+ this.props.setAssetStatus(this.props.src, true);
295
+ }
296
+ }
297
+
298
+ componentWillUnmount() {
299
+ this._isMounted = false;
300
+ }
301
+
302
+ // Check if all of the resources are loaded in a given state
303
+ isLoadedInState(state: State): boolean {
304
+ return Util.isLabeledSVG(this.props.src)
305
+ ? state.imageLoaded && state.dataLoaded
306
+ : state.imageLoaded;
307
+ }
308
+
309
+ loadResources() {
310
+ const hash = getUrlHash(this.props.src);
311
+
312
+ // We can't make multiple jsonp calls to the same file because their
313
+ // callbacks will collide with each other. Instead, we cache the data
314
+ // and only make the jsonp calls once.
315
+ if (labelDataCache[hash]) {
316
+ if (labelDataCache[hash].loaded) {
317
+ const {data, localized} = labelDataCache[hash];
318
+ this.onDataLoaded(data, localized);
319
+ } else {
320
+ labelDataCache[hash].dataCallbacks.push(this.onDataLoaded);
321
+ }
322
+ } else {
323
+ const cacheData = {
324
+ loaded: false,
325
+ dataCallbacks: [this.onDataLoaded],
326
+ data: null,
327
+ localized: shouldUseLocalizedData(),
328
+ };
329
+
330
+ labelDataCache[hash] = cacheData;
331
+
332
+ const retrieveData = (url, errorCallback) => {
333
+ doJSONP(url, {
334
+ callbackName: "svgData" + hash,
335
+ success: (data) => {
336
+ cacheData.data = data;
337
+ cacheData.loaded = true;
338
+
339
+ _.each(cacheData.dataCallbacks, (callback) => {
340
+ callback(cacheData.data, cacheData.localized);
341
+ });
342
+ },
343
+ error: errorCallback,
344
+ });
345
+ };
346
+
347
+ if (shouldUseLocalizedData()) {
348
+ retrieveData(
349
+ getLocalizedDataUrl(this.props.src),
350
+ (x, status, error) => {
351
+ cacheData.localized = false;
352
+
353
+ // If there is isn't any localized data, fall back to
354
+ // the original, unlocalized data
355
+ retrieveData(
356
+ Util.getDataUrl(this.props.src),
357
+ (x, status, error) => {
358
+ Log.error(
359
+ "Data load failed for svg-image",
360
+ Errors.Service,
361
+ {
362
+ cause: error,
363
+ loggedMetadata: {
364
+ dataUrl: Util.getDataUrl(
365
+ this.props.src,
366
+ ),
367
+ status,
368
+ },
369
+ },
370
+ );
371
+ },
372
+ );
373
+ },
374
+ );
375
+ } else {
376
+ retrieveData(
377
+ Util.getDataUrl(this.props.src),
378
+ (x, status, error) => {
379
+ Log.error(
380
+ "Data load failed for svg-image",
381
+ Errors.Service,
382
+ {
383
+ cause: error,
384
+ loggedMetadata: {
385
+ dataUrl: Util.getDataUrl(this.props.src),
386
+ status,
387
+ },
388
+ },
389
+ );
390
+ },
391
+ );
392
+ }
393
+ }
394
+ }
395
+
396
+ onDataLoaded: (
397
+ data: {labels: $ReadOnlyArray<any>, range: $ReadOnlyArray<any>, ...},
398
+ localized: boolean,
399
+ ) => void = (
400
+ data: {
401
+ labels: $ReadOnlyArray<any>,
402
+ range: $ReadOnlyArray<any>,
403
+ ...
404
+ },
405
+ localized: boolean,
406
+ ) => {
407
+ if (this._isMounted && data.labels && data.range) {
408
+ const labelsRendered: LabelsRenderedMap = data.labels.reduce(
409
+ (dict: LabelsRenderedMap, label) => ({
410
+ ...dict,
411
+ [label.content]: false,
412
+ }),
413
+ {},
414
+ );
415
+
416
+ this.setState({
417
+ dataLoaded: true,
418
+ labelDataIsLocalized: localized,
419
+ labelsRendered,
420
+ labels: data.labels,
421
+ range: data.range,
422
+ });
423
+ }
424
+ };
425
+
426
+ sizeProvided(): boolean {
427
+ return this.props.width != null && this.props.height != null;
428
+ }
429
+
430
+ onImageLoad: () => void = () => {
431
+ // Only need to do this if rendering a Graphie
432
+ if (this.sizeProvided()) {
433
+ // If width and height are provided, we don't need to calculate the
434
+ // size ourselves
435
+ this.setState({
436
+ imageLoaded: true,
437
+ });
438
+ } else {
439
+ Util.getImageSize(this.props.src, (width, height) => {
440
+ if (this._isMounted) {
441
+ this.setState({
442
+ imageLoaded: true,
443
+ imageDimensions: [width, height],
444
+ });
445
+ }
446
+ });
447
+ }
448
+ };
449
+
450
+ setupGraphie: (graphie: any, options: any) => void = (
451
+ graphie: any,
452
+ options: any,
453
+ ) => {
454
+ _.map(options.labels, (labelData) => {
455
+ const {JIPT} = getDependencies();
456
+ if (JIPT.useJIPT && this.state.labelDataIsLocalized) {
457
+ // If we're using JIPT translation and we got proper JIPT tags,
458
+ // render the labels as plain text (so JIPT can find them) and
459
+ // add some extra properties to the element so we can properly
460
+ // re-render the label once it is replaced with translated
461
+ // text.
462
+ const elem = graphie.label(
463
+ labelData.coordinates,
464
+ labelData.content,
465
+ labelData.alignment,
466
+ false,
467
+ );
468
+
469
+ getDependencies().svgImageJiptLabels.addLabel(
470
+ elem,
471
+ labelData.typesetAsMath,
472
+ );
473
+ } else if (labelData.coordinates) {
474
+ // Create labels from the data
475
+ // TODO(charlie): Some erroneous labels are being sent down
476
+ // without coordinates. They don't seem to have any content, so
477
+ // it seems fine to just ignore them (rather than error), but
478
+ // we should figure out why this is happening.
479
+ const label = graphie.label(
480
+ labelData.coordinates,
481
+ labelData.content,
482
+ labelData.alignment,
483
+ labelData.typesetAsMath,
484
+ {"font-size": 100 * this.props.scale + "%"},
485
+ );
486
+
487
+ // Convert absolute positioning css from pixels to percentages
488
+ // TODO(alex): Dynamically resize font-size as well. This
489
+ // almost certainly means listening to throttled window resize
490
+ // events.
491
+ const labelStyle = label[0].style;
492
+ let labelTop = this._tryGetPixels(labelStyle.top);
493
+ let labelLeft = this._tryGetPixels(labelStyle.left);
494
+ if (labelTop === null || labelLeft === null) {
495
+ // Graphie labels are supposed to have an explicit position,
496
+ // but to be on the safe side, let's fall back to using
497
+ // jQuery's position(). The reason we're not always using
498
+ // this is that in the presence of CSS transforms, it will
499
+ // give the rendered position, which may be scaled and
500
+ // not equal to the explicitly specified one.
501
+ const labelPosition = label.position();
502
+ labelTop = labelPosition.top;
503
+ labelLeft = labelPosition.left;
504
+ }
505
+ const svgHeight = (this.props.height || 0) * this.props.scale;
506
+ const svgWidth = (this.props.width || 0) * this.props.scale;
507
+ label.css({
508
+ top: (labelTop / svgHeight) * 100 + "%",
509
+ left: (labelLeft / svgWidth) * 100 + "%",
510
+ });
511
+
512
+ // Add back the styles to each of the labels
513
+ _.each(labelData.style, (styleValue, styleName) => {
514
+ label.css(styleName, styleValue);
515
+ });
516
+ }
517
+ this.setState({
518
+ labelsRendered: {
519
+ ...this.state.labelsRendered,
520
+ [labelData.content]: true,
521
+ },
522
+ });
523
+ });
524
+ };
525
+
526
+ // Try to parse a CSS value as pixels. Returns null if the parameter string
527
+ // does not contain a number followed by "px".
528
+ _tryGetPixels(value: string): null | number {
529
+ value = value || "";
530
+ // While this doesn't check that there are no other alphabetical
531
+ // characters prior to "px", that should be taken care of by the DOM,
532
+ // which won't accept invalid units.
533
+ if (!value.endsWith("px")) {
534
+ return null;
535
+ }
536
+ // parseFloat() ignores trailing non-numerical characters.
537
+ return parseFloat(value) || null;
538
+ }
539
+
540
+ _handleZoomClick: (e: SyntheticEvent<>) => void = (e: SyntheticEvent<>) => {
541
+ const $image = $(e.target);
542
+
543
+ // It's possible that the image is already displayed at its
544
+ // full size, but we don't really know that until we get a chance
545
+ // to measure it (just now, after the user clicks). We only zoom
546
+ // if there's more image to be shown.
547
+ //
548
+ // TODO(kevindangoor) If the window is narrow and the image is
549
+ // already displayed as wide as possible, we may want to do
550
+ // nothing in that case as well. Figuring this out correctly
551
+ // likely required accounting for the image alignment and margins.
552
+ if (
553
+ $image.width() < this.props.width ||
554
+ this.props.zoomToFullSizeOnMobile
555
+ ) {
556
+ Zoom.ZoomService.handleZoomClick(
557
+ e,
558
+ this.props.zoomToFullSizeOnMobile,
559
+ );
560
+ }
561
+ this.props.trackInteraction && this.props.trackInteraction();
562
+ };
563
+
564
+ handleUpdate: (status: string) => void = (status: string) => {
565
+ this.props.onUpdate();
566
+ // NOTE: Labeled SVG images use this.onImageLoad to set imageLoaded
567
+ // to true and we only use the imageLoaded state when isLabeledSVG
568
+ // is true setting imageLoaded true here shouldn't cause issues.
569
+ if (!Util.isLabeledSVG(this.props.src) && status === "loaded") {
570
+ this.setState({imageLoaded: true});
571
+ }
572
+ };
573
+
574
+ render(): React.Element<"div"> | React.Node {
575
+ const imageSrc = this.props.src;
576
+
577
+ // Props to send to all images
578
+ const imageProps: ImageProps = {
579
+ alt: this.props.alt,
580
+ title: this.props.title,
581
+ tabIndex: "0",
582
+ };
583
+ // If alt text is present, AND overrideAriaHidden is true,
584
+ // include aria-hidden to avoid screen reader from crawling texts
585
+ // both from the div meant for screen readers (see svg-image.jsx)
586
+ // and alt attribute.
587
+ if (this.props.alt && this.props.overrideAriaHidden) {
588
+ imageProps["aria-hidden"] = true;
589
+ }
590
+
591
+ const width = this.props.width && this.props.width * this.props.scale;
592
+ const height =
593
+ this.props.height && this.props.height * this.props.scale;
594
+ const dimensions = {
595
+ width: width,
596
+ height: height,
597
+ };
598
+
599
+ // To make an image responsive, we need to know what its width and
600
+ // height are in advance (before inserting it into the DOM) so that we
601
+ // can ensure it doesn't grow past those limits. We don't always have
602
+ // this information, especially in places where <Renderer /> is used
603
+ // to render inline Markdown images within a widget. See Radio, Sorter,
604
+ // Matcher, etc.
605
+ // TODO(alex): Make all of those image rendering locations aware of
606
+ // width+height so that they too can render responsively.
607
+ const responsive = this.props.responsive && !!(width && height);
608
+
609
+ // An additional <Graphie /> may be inserted after the image/graphie
610
+ // pair. Only used by the image widget, for its legacy labels support.
611
+ // Note that since the image widget always provides width and height
612
+ // data, extraGraphie can be ignored for unresponsive images.
613
+ // TODO(alex): Convert all existing uses of that to web+graphie. This
614
+ // is tricky because web+graphie doesn't support labels on non-graphie
615
+ // images.
616
+ let extraGraphie;
617
+ if (this.props.extraGraphie && this.props.extraGraphie.labels.length) {
618
+ extraGraphie = (
619
+ <Graphie
620
+ box={this.props.extraGraphie.box}
621
+ range={this.props.extraGraphie.range}
622
+ options={{labels: this.props.extraGraphie.labels}}
623
+ responsive={true}
624
+ addMouseLayer={false}
625
+ setup={this.setupGraphie}
626
+ />
627
+ );
628
+ }
629
+
630
+ // If preloader is undefined, we use the default. If it's
631
+ // null, there will be no preloader in use.
632
+ const preloaderBaseFunc =
633
+ this.props.preloader === undefined
634
+ ? defaultPreloader
635
+ : this.props.preloader;
636
+
637
+ const preloader = preloaderBaseFunc
638
+ ? () => preloaderBaseFunc(dimensions)
639
+ : null;
640
+
641
+ // Just use a normal image if a normal image is provided
642
+ if (!Util.isLabeledSVG(imageSrc)) {
643
+ if (responsive) {
644
+ const wrapperClasses = classNames({
645
+ zoomable: (width || 0) > ZOOMABLE_THRESHOLD,
646
+ "svg-image": true,
647
+ });
648
+
649
+ imageProps.onClick = this._handleZoomClick;
650
+
651
+ return (
652
+ <FixedToResponsive
653
+ className={wrapperClasses}
654
+ width={width}
655
+ height={height}
656
+ constrainHeight={this.props.constrainHeight}
657
+ allowFullBleed={
658
+ this.props.allowFullBleed &&
659
+ isImageProbablyPhotograph(imageSrc)
660
+ }
661
+ >
662
+ <ImageLoader
663
+ src={imageSrc}
664
+ imgProps={imageProps}
665
+ preloader={preloader}
666
+ onUpdate={this.handleUpdate}
667
+ />
668
+ {extraGraphie}
669
+ </FixedToResponsive>
670
+ );
671
+ }
672
+ imageProps.style = dimensions;
673
+ return (
674
+ <ImageLoader
675
+ src={imageSrc}
676
+ preloader={preloader}
677
+ imgProps={imageProps}
678
+ onUpdate={this.handleUpdate}
679
+ />
680
+ );
681
+ }
682
+
683
+ const imageUrl = Util.getSvgUrl(imageSrc);
684
+
685
+ let graphie;
686
+ // Since we only want to do the graphie setup once, we only render the
687
+ // graphie once everything is loaded
688
+ if (this.isLoadedInState(this.state)) {
689
+ // Use the provided width and height to size the graphie if
690
+ // possible, otherwise use our own calculated size
691
+ let box;
692
+ if (this.sizeProvided()) {
693
+ box = [width, height];
694
+ } else if (this.state.imageDimensions) {
695
+ box = [
696
+ this.state.imageDimensions[0] * this.props.scale,
697
+ this.state.imageDimensions[1] * this.props.scale,
698
+ ];
699
+ } else {
700
+ throw new PerseusError(
701
+ "svg-image has no dimensions",
702
+ Errors.InvalidInput,
703
+ {
704
+ metadata: {src: this.props.src},
705
+ },
706
+ );
707
+ }
708
+
709
+ const scale = [40 * this.props.scale, 40 * this.props.scale];
710
+
711
+ graphie = (
712
+ <Graphie
713
+ // eslint-disable-next-line react/no-string-refs
714
+ ref="graphie"
715
+ box={box}
716
+ scale={scale}
717
+ range={this.state.range}
718
+ options={_.pick(this.state, "labels")}
719
+ responsive={responsive}
720
+ addMouseLayer={false}
721
+ setup={this.setupGraphie}
722
+ />
723
+ );
724
+ }
725
+
726
+ if (responsive) {
727
+ return (
728
+ <FixedToResponsive
729
+ className="svg-image"
730
+ width={width}
731
+ height={height}
732
+ constrainHeight={this.props.constrainHeight}
733
+ >
734
+ <ImageLoader
735
+ src={imageUrl}
736
+ onLoad={this.onImageLoad}
737
+ onUpdate={this.handleUpdate}
738
+ preloader={preloader}
739
+ imgProps={imageProps}
740
+ />
741
+ {graphie}
742
+ {extraGraphie}
743
+ </FixedToResponsive>
744
+ );
745
+ }
746
+ imageProps.style = dimensions;
747
+ return (
748
+ <div className="unresponsive-svg-image" style={dimensions}>
749
+ <ImageLoader
750
+ src={imageUrl}
751
+ onLoad={this.onImageLoad}
752
+ onUpdate={this.handleUpdate}
753
+ preloader={preloader}
754
+ imgProps={imageProps}
755
+ />
756
+ {graphie}
757
+ </div>
758
+ );
759
+ }
760
+ }
761
+
762
+ export default SvgImage;