@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,622 @@
1
+ /* eslint-disable react/prop-types */
2
+ // @flow
3
+
4
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
5
+ import classNames from "classnames";
6
+ import * as React from "react";
7
+ import _ from "underscore";
8
+
9
+ import InputWithExamples from "../components/input-with-examples.jsx";
10
+ import PossibleAnswers from "../components/possible-answers.jsx";
11
+ import SimpleKeypadInput from "../components/simple-keypad-input.jsx";
12
+ import {linterContextDefault} from "../gorgon/proptypes.js";
13
+ import {ApiOptions, ClassNames as ApiClassNames} from "../perseus-api.jsx";
14
+ import TexWrangler from "../tex-wrangler.js";
15
+ import KhanAnswerTypes from "../util/answer-types.js";
16
+ import KhanMath from "../util/math.js";
17
+
18
+ import type {
19
+ PerseusNumericInputWidgetOptions,
20
+ PerseusNumericInputAnswer,
21
+ } from "../perseus-types.js";
22
+ import type {PerseusScore, WidgetExports, WidgetProps} from "../types.js";
23
+
24
+ const ParseTex = TexWrangler.parseTex;
25
+
26
+ const answerFormButtons = [
27
+ {title: "Integers", value: "integer", content: "6"},
28
+ {title: "Decimals", value: "decimal", content: "0.75"},
29
+ {title: "Proper fractions", value: "proper", content: "\u2157"},
30
+ {
31
+ title: "Improper fractions",
32
+ value: "improper",
33
+ content: "\u2077\u2044\u2084",
34
+ },
35
+ {title: "Mixed numbers", value: "mixed", content: "1\u00BE"},
36
+ {title: "Numbers with \u03C0", value: "pi", content: "\u03C0"},
37
+ ];
38
+
39
+ const formExamples = {
40
+ integer: () => i18n._("an integer, like $6$"),
41
+ proper: (form) =>
42
+ form.simplify === "optional"
43
+ ? i18n._("a *proper* fraction, like $1/2$ or $6/10$")
44
+ : i18n._("a *simplified proper* fraction, like $3/5$"),
45
+ improper: (form) =>
46
+ form.simplify === "optional"
47
+ ? i18n._("an *improper* fraction, like $10/7$ or $14/8$")
48
+ : i18n._("a *simplified improper* fraction, like $7/4$"),
49
+ mixed: () => i18n._("a mixed number, like $1\\ 3/4$"),
50
+ decimal: () => i18n._("an *exact* decimal, like $0.75$"),
51
+ pi: () =>
52
+ i18n._(
53
+ "a multiple of pi, like $12\\ \\text{pi}$ or " +
54
+ "$2/3\\ \\text{pi}$",
55
+ ),
56
+ };
57
+
58
+ type UserInput = {|
59
+ currentValue: string,
60
+ |};
61
+
62
+ export type Rubric = PerseusNumericInputWidgetOptions;
63
+
64
+ type ExternalProps = WidgetProps<PerseusNumericInputWidgetOptions, Rubric>;
65
+
66
+ type Props = {|
67
+ ...ExternalProps,
68
+ size: $NonMaybeType<ExternalProps["size"]>,
69
+ rightAlign: $NonMaybeType<ExternalProps["rightAlign"]>,
70
+ apiOptions: $NonMaybeType<ExternalProps["apiOptions"]>,
71
+ coefficient: $NonMaybeType<ExternalProps["coefficient"]>,
72
+ answerForms: $NonMaybeType<ExternalProps["answerForms"]>,
73
+ labelText: $NonMaybeType<ExternalProps["labelText"]>,
74
+ linterContext: $NonMaybeType<ExternalProps["linterContext"]>,
75
+ multipleNumberInput: $NonMaybeType<ExternalProps["multipleNumberInput"]>,
76
+ currentValue: string,
77
+ |};
78
+
79
+ type DefaultProps = {|
80
+ currentValue: Props["currentValue"],
81
+ size: Props["size"],
82
+ rightAlign: Props["rightAlign"],
83
+ apiOptions: Props["apiOptions"],
84
+ coefficient: Props["coefficient"],
85
+ answerForms: Props["answerForms"],
86
+ labelText: Props["labelText"],
87
+ linterContext: Props["linterContext"],
88
+ multipleNumberInput: Props["multipleNumberInput"],
89
+ |};
90
+
91
+ type State = {|
92
+ // keeps track of the other set of values when switching
93
+ // between 0 and finite solutions
94
+ previousValues: $ReadOnlyArray<string>,
95
+ |};
96
+
97
+ export class NumericInput extends React.Component<Props, State> {
98
+ static defaultProps: DefaultProps = {
99
+ currentValue: "",
100
+ size: "normal",
101
+ rightAlign: false,
102
+ apiOptions: ApiOptions.defaults,
103
+ coefficient: false,
104
+ answerForms: [],
105
+ labelText: "",
106
+ linterContext: linterContextDefault,
107
+ multipleNumberInput: false,
108
+ };
109
+
110
+ static getUserInputFromProps(props: Props): UserInput {
111
+ return {
112
+ currentValue: props.currentValue,
113
+ };
114
+ }
115
+
116
+ static getOneCorrectAnswerFromRubric(
117
+ rubric: Rubric,
118
+ ): ?PerseusNumericInputAnswer {
119
+ const correctAnswers = _.filter(
120
+ rubric.answers,
121
+ (answer) => answer.status === "correct",
122
+ );
123
+ const answerStrings = _.map(correctAnswers, (answer) => {
124
+ // Figure out how this answer is supposed to be
125
+ // displayed
126
+ let format = "decimal";
127
+ if (answer.answerForms && answer.answerForms[0]) {
128
+ // NOTE(johnsullivan): This isn't exactly ideal, but
129
+ // it does behave well for all the currently known
130
+ // problems. See D14742 for some discussion on
131
+ // alternate strategies.
132
+ format = answer.answerForms[0];
133
+ }
134
+
135
+ let answerString = KhanMath.toNumericString(answer.value, format);
136
+ if (answer.maxError) {
137
+ answerString +=
138
+ " \u00B1 " +
139
+ KhanMath.toNumericString(answer.maxError, format);
140
+ }
141
+ return answerString;
142
+ });
143
+ if (answerStrings.length === 0) {
144
+ return;
145
+ }
146
+ return answerStrings[0];
147
+ }
148
+
149
+ static validate(useInput: UserInput, rubric: Rubric): PerseusScore {
150
+ const allAnswerForms = _.pluck(answerFormButtons, "value");
151
+
152
+ const createValidator = (answer) =>
153
+ KhanAnswerTypes.number.createValidatorFunctional(answer.value, {
154
+ message: answer.message,
155
+ simplify:
156
+ answer.status === "correct" ? answer.simplify : "optional",
157
+ inexact: true, // TODO(merlob) backfill / delete
158
+ maxError: answer.maxError,
159
+ forms:
160
+ answer.strict &&
161
+ answer.answerForms &&
162
+ answer.answerForms.length !== 0
163
+ ? answer.answerForms
164
+ : allAnswerForms,
165
+ });
166
+
167
+ // We may have received TeX; try to parse it before grading.
168
+ // If `currentValue` is not TeX, this should be a no-op.
169
+ const currentValue = ParseTex(useInput.currentValue);
170
+ const correctAnswers = _.where(rubric.answers, {status: "correct"});
171
+
172
+ const normalizedAnswerExpected = correctAnswers.every(
173
+ (answer) => Math.abs(answer.value) <= 1,
174
+ );
175
+
176
+ // Look through all correct answers for one that matches either
177
+ // precisely or approximately and return the appropriate message:
178
+ // - if precise, return the message that the answer came with
179
+ // - if it needs to be simplified, etc., show that message
180
+ let result = _.find(
181
+ _.map(correctAnswers, (answer) => {
182
+ // The coefficient is an attribute of the widget
183
+ let localValue = currentValue;
184
+ if (rubric.coefficient) {
185
+ if (!localValue) {
186
+ localValue = 1;
187
+ } else if (localValue === "-") {
188
+ localValue = -1;
189
+ }
190
+ }
191
+ const validate = createValidator(answer);
192
+ return validate(
193
+ maybeParsePercentInput(
194
+ localValue,
195
+ normalizedAnswerExpected,
196
+ ),
197
+ );
198
+ }),
199
+ (match) => match.correct || match.empty,
200
+ );
201
+
202
+ if (!result) {
203
+ // Otherwise, if the guess is not correct
204
+ const otherAnswers = [].concat(
205
+ _.where(rubric.answers, {status: "ungraded"}),
206
+ _.where(rubric.answers, {status: "wrong"}),
207
+ );
208
+
209
+ // Look through all other answers and if one matches either
210
+ // precisely or approximately return the answer's message
211
+ const match = _.find(otherAnswers, (answer) => {
212
+ const validate = createValidator(answer);
213
+ return validate(
214
+ maybeParsePercentInput(
215
+ currentValue,
216
+ normalizedAnswerExpected,
217
+ ),
218
+ ).correct;
219
+ });
220
+ result = {
221
+ empty: match ? match.status === "ungraded" : false,
222
+ correct: match ? match.status === "correct" : false,
223
+ message: match ? match.message : null,
224
+ guess: currentValue,
225
+ };
226
+ }
227
+
228
+ // TODO(eater): Seems silly to translate result to this
229
+ // invalid/points thing and immediately translate it
230
+ // back in ItemRenderer.scoreInput()
231
+ if (result.empty) {
232
+ return {
233
+ type: "invalid",
234
+ message: result.message,
235
+ };
236
+ }
237
+ return {
238
+ type: "points",
239
+ earned: result.correct ? 1 : 0,
240
+ total: 1,
241
+ message: result.message,
242
+ };
243
+ }
244
+
245
+ state: State = {
246
+ // keeps track of the other set of values when switching
247
+ // between 0 and finite solutions
248
+ previousValues: [""],
249
+ };
250
+
251
+ getAnswerBlurb: (Rubric) => [?React.Node, boolean] = (rubric) => {
252
+ let correct;
253
+ let answerBlurb;
254
+
255
+ /* Ignore this block because SAT is deprecated */
256
+ /* istanbul ignore next */
257
+ if (this.props.apiOptions.satStyling && rubric) {
258
+ const score = this.simpleValidate(rubric);
259
+ correct = score.type === "points" && score.earned === score.total;
260
+
261
+ if (!correct) {
262
+ const correctAnswers = _.filter(
263
+ rubric.answers,
264
+ (answer) => answer.status === "correct",
265
+ );
266
+ const answerStrings = _.map(correctAnswers, (answer) => {
267
+ // Figure out how this answer is supposed to be
268
+ // displayed
269
+ let format = "decimal";
270
+ if (answer.answerForms && answer.answerForms[0]) {
271
+ // NOTE(johnsullivan): This isn't exactly ideal, but
272
+ // it does behave well for all the currently known
273
+ // problems. See D14742 for some discussion on
274
+ // alternate strategies.
275
+ format = answer.answerForms[0];
276
+ }
277
+
278
+ let answerString = KhanMath.toNumericString(
279
+ answer.value,
280
+ format,
281
+ );
282
+ if (answer.maxError) {
283
+ answerString +=
284
+ " \u00B1 " +
285
+ KhanMath.toNumericString(answer.maxError, format);
286
+ }
287
+ return answerString;
288
+ });
289
+ answerBlurb = <PossibleAnswers answers={answerStrings} />;
290
+ }
291
+ }
292
+ return [answerBlurb, !!correct];
293
+ };
294
+
295
+ getClasses: (boolean, $FlowFixMe) => $FlowFixMe = (correct, rubric) => {
296
+ const classes = {};
297
+ classes["perseus-input-size-" + this.props.size] = true;
298
+ classes["perseus-input-right-align"] = this.props.rightAlign;
299
+ classes[ApiClassNames.CORRECT] =
300
+ rubric && correct && this.props.currentValue;
301
+ classes[ApiClassNames.INCORRECT] =
302
+ rubric && !correct && this.props.currentValue;
303
+ classes[ApiClassNames.UNANSWERED] = rubric && !this.props.currentValue;
304
+ return classes;
305
+ };
306
+
307
+ // TODO(Nicole, Jeremy): This is maybe never used and should be removed
308
+ examples: () => $ReadOnlyArray<string> = () => {
309
+ // if the set of specified forms are empty, allow all forms
310
+ const forms =
311
+ this.props.answerForms?.length !== 0
312
+ ? this.props.answerForms
313
+ : _.map(_.keys(formExamples), (name) => {
314
+ return {
315
+ name: name,
316
+ simplify: "required",
317
+ };
318
+ });
319
+
320
+ let examples = _.map(forms, (form) => {
321
+ return formExamples[form.name](form);
322
+ });
323
+ // Ensure no duplicate tooltip text from simplified and unsimplified
324
+ // versions of the same format
325
+ examples = _.uniq(examples);
326
+
327
+ return [i18n._("**Your answer should be** ")].concat(examples);
328
+ };
329
+
330
+ shouldShowExamples: () => boolean = () => {
331
+ const noFormsAccepted = this.props.answerForms?.length === 0;
332
+ // To check if all answer forms are accepted, we must first
333
+ // find the *names* of all accepted forms, and see if they are
334
+ // all present, ignoring duplicates
335
+ const answerFormNames = _.uniq(
336
+ this.props.answerForms?.map((form) => form.name),
337
+ );
338
+ const allFormsAccepted = answerFormNames.length >= _.size(formExamples);
339
+ return !noFormsAccepted && !allFormsAccepted;
340
+ };
341
+
342
+ simpleValidate: (Rubric) => PerseusScore = (rubric) => {
343
+ return NumericInput.validate(this.getUserInput(), rubric);
344
+ };
345
+
346
+ focus: () => boolean = () => {
347
+ // eslint-disable-next-line react/no-string-refs
348
+ this.refs.input.focus();
349
+ return true;
350
+ };
351
+
352
+ focusInputPath: ($FlowFixMe) => void = (inputPath) => {
353
+ /* istanbul ignore next */
354
+ // eslint-disable-next-line react/no-string-refs
355
+ this.refs.input.focus();
356
+ };
357
+
358
+ blurInputPath: ($FlowFixMe) => void = (inputPath) => {
359
+ // eslint-disable-next-line react/no-string-refs
360
+ this.refs.input.blur();
361
+ };
362
+
363
+ getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<string>> = () => {
364
+ // The widget itself is an input, so we return a single empty list to
365
+ // indicate this.
366
+ /* istanbul ignore next */
367
+ return [[]];
368
+ };
369
+
370
+ getGrammarTypeForPath: ($FlowFixMe) => string = (inputPath) => {
371
+ /* istanbul ignore next */
372
+ return "number";
373
+ };
374
+
375
+ setInputValue: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => void = (
376
+ path,
377
+ newValue,
378
+ cb,
379
+ ) => {
380
+ /* istanbul ignore next */
381
+ this.props.onChange(
382
+ {
383
+ currentValue: newValue,
384
+ },
385
+ cb,
386
+ );
387
+ };
388
+
389
+ getUserInput: () => UserInput = () => {
390
+ return NumericInput.getUserInputFromProps(this.props);
391
+ };
392
+
393
+ handleChange: (string, $FlowFixMe) => void = (newValue, cb) => {
394
+ this.props.onChange({currentValue: newValue}, cb);
395
+ this.props.trackInteraction();
396
+ };
397
+
398
+ _getInputType: () => "tex" | "text" = () => {
399
+ if (this.props.apiOptions.staticRender) {
400
+ return "tex";
401
+ }
402
+ return "text";
403
+ };
404
+
405
+ _handleFocus: () => void = () => {
406
+ this.props.onFocus([]);
407
+ // HACK(kevinb): We want to dismiss the feedback popover that webapp
408
+ // displays as soon as a user clicks in in the input field so we call
409
+ // interactionCallback directly.
410
+ const {interactionCallback} = this.props.apiOptions;
411
+ if (interactionCallback) {
412
+ interactionCallback();
413
+ }
414
+ };
415
+
416
+ _handleBlur: () => void = () => {
417
+ this.props.onBlur([]);
418
+ };
419
+
420
+ render(): React.Node {
421
+ const rubric = this.props.reviewModeRubric;
422
+ const answers = this.getAnswerBlurb(rubric);
423
+ const answerBlurb = answers[0];
424
+ const correct = answers[1];
425
+ const classes = this.getClasses(correct, rubric);
426
+
427
+ let labelText = this.props.labelText;
428
+ if (labelText == null || labelText === "") {
429
+ labelText = i18n._("Your answer:");
430
+ }
431
+
432
+ // To right align a custom keypad we need to wrap it.
433
+ const maybeRightAlignKeypadInput = (keypadInput) => {
434
+ return this.props.rightAlign ? (
435
+ <div className="perseus-input-right-align">{keypadInput}</div>
436
+ ) : (
437
+ keypadInput
438
+ );
439
+ };
440
+
441
+ if (this.props.apiOptions.customKeypad) {
442
+ // TODO(charlie): Support "Review Mode".
443
+ return maybeRightAlignKeypadInput(
444
+ <SimpleKeypadInput
445
+ // eslint-disable-next-line react/no-string-refs
446
+ ref="input"
447
+ value={this.props.currentValue}
448
+ keypadElement={this.props.keypadElement}
449
+ onChange={this.handleChange}
450
+ onFocus={this._handleFocus}
451
+ onBlur={this._handleBlur}
452
+ />,
453
+ );
454
+ }
455
+
456
+ const input = (
457
+ <InputWithExamples
458
+ // eslint-disable-next-line react/no-string-refs
459
+ ref="input"
460
+ value={this.props.currentValue}
461
+ onChange={this.handleChange}
462
+ className={classNames(classes)}
463
+ labelText={labelText}
464
+ type={this._getInputType()}
465
+ examples={this.examples()}
466
+ shouldShowExamples={this.shouldShowExamples()}
467
+ onFocus={this._handleFocus}
468
+ onBlur={this._handleBlur}
469
+ id={this.props.widgetId}
470
+ disabled={this.props.apiOptions.readOnly}
471
+ // $FlowFixMe[prop-missing]
472
+ highlightLint={this.props.highlightLint}
473
+ />
474
+ );
475
+
476
+ /* This only gets used with SAT Content */
477
+ /* istanbul ignore next if */
478
+ if (answerBlurb) {
479
+ return (
480
+ <span className="perseus-input-with-answer-blurb">
481
+ {input}
482
+ {answerBlurb}
483
+ </span>
484
+ );
485
+ }
486
+ if (this.props.apiOptions.satStyling) {
487
+ // NOTE(amy): the input widgets themselves already have
488
+ // a default aria label of "Your Answer", so we hide this
489
+ // redundant label from screen-readers.
490
+ return (
491
+ <label className="perseus-input-with-label">
492
+ <span className="perseus-input-label" aria-hidden="true">
493
+ {i18n.doNotTranslate("Answer:")}
494
+ </span>
495
+ {input}
496
+ </label>
497
+ );
498
+ }
499
+ return <div>{input}</div>;
500
+ }
501
+ }
502
+
503
+ // TODO(thomas): Currently we receive a list of lists of acceptable answer types
504
+ // and union them down into a single set. It's worth considering whether it
505
+ // wouldn't make more sense to have a single set of acceptable answer types for
506
+ // a given *problem* rather than for each possible [correct/wrong] *answer*.
507
+ // When should two answers to a problem take different answer types?
508
+ // See D27790 for more discussion.
509
+ export const unionAnswerForms: ($FlowFixMe) => $FlowFixMe = function (
510
+ answerFormsList,
511
+ ) {
512
+ // Takes a list of lists of answer forms, and returns a list of the forms
513
+ // in each of these lists in the same order that they're listed in the
514
+ // `formExamples` forms from above.
515
+
516
+ // uniqueBy takes a list of elements and a function which compares whether
517
+ // two elements are equal, and returns a list of unique elements. This is
518
+ // just a helper function here, but works generally.
519
+ const uniqueBy = function (list, iteratee) {
520
+ return _.reduce(
521
+ list,
522
+ (uniqueList, element) => {
523
+ // For each element, decide whether it's already in the list of
524
+ // unique items.
525
+ const inList = _.find(uniqueList, iteratee.bind(null, element));
526
+ if (inList) {
527
+ return uniqueList;
528
+ }
529
+ return uniqueList.concat([element]);
530
+ },
531
+ [],
532
+ );
533
+ };
534
+
535
+ // Pull out all of the forms from the different lists.
536
+ const allForms = _.flatten(answerFormsList);
537
+ // Pull out the unique forms using uniqueBy.
538
+ const uniqueForms = uniqueBy(allForms, _.isEqual);
539
+ // Sort them by the order they appear in the `formExamples` list.
540
+ return _.sortBy(uniqueForms, (form) => {
541
+ return _.keys(formExamples).indexOf(form.name);
542
+ });
543
+ };
544
+
545
+ type RenderProps = {|
546
+ answerForms: $ReadOnlyArray<{|
547
+ simplify: ?("required" | "correct" | "enforced"),
548
+ name: "integer" | "decimal" | "proper" | "improper" | "mixed" | "pi",
549
+ |}>,
550
+ labelText: string,
551
+ size: string,
552
+ coefficient: boolean,
553
+ rightAlign?: boolean,
554
+ static: boolean,
555
+ |};
556
+
557
+ // This function checks if the user inputted a percent value, parsing
558
+ // it as a number (and maybe scaling) so that it can be graded.
559
+ // NOTE(michaelpolyak): Unlike `KhanAnswerTypes.number.percent()` which
560
+ // can accept several input forms with or without "%", the decision
561
+ // to parse based on the presence of "%" in the input, is so that we
562
+ // don't accidently scale the user typed value before grading, CP-930.
563
+ export const maybeParsePercentInput: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
564
+ inputValue,
565
+ normalizedAnswerExpected,
566
+ ) => {
567
+ // If the input value is not a string ending with "%", then there's
568
+ // nothing more to do. The value will be graded as inputted by user.
569
+ if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
570
+ return inputValue;
571
+ }
572
+
573
+ const value = parseFloat(inputValue.slice(0, -1));
574
+ // If the input value stripped of the "%" cannot be parsed as a
575
+ // number (the slice is not really necessary for parseFloat to work
576
+ // if the string starts with a number) then return the original
577
+ // input for grading.
578
+ if (isNaN(value)) {
579
+ return inputValue;
580
+ }
581
+
582
+ // Next, if all correct answers are in the range of |0,1| then we
583
+ // scale the user typed value. We assume this is the correct thing
584
+ // to do since the input value ends with "%".
585
+ if (normalizedAnswerExpected) {
586
+ return value / 100;
587
+ }
588
+
589
+ // Otherwise, we return input valuåe (number) stripped of the "%".
590
+ return value;
591
+ };
592
+
593
+ const propsTransform = function (
594
+ widgetOptions: PerseusNumericInputWidgetOptions,
595
+ ): RenderProps {
596
+ const rendererProps = _.extend(_.omit(widgetOptions, "answers"), {
597
+ answerForms: unionAnswerForms(
598
+ // Pull out the name of each form and whether that form has
599
+ // required simplification.
600
+ _.map(widgetOptions.answers, (answer) => {
601
+ return _.map(answer.answerForms, (form) => {
602
+ return {
603
+ simplify: answer.simplify,
604
+ name: form,
605
+ };
606
+ });
607
+ }),
608
+ ),
609
+ });
610
+
611
+ return rendererProps;
612
+ };
613
+
614
+ export default ({
615
+ name: "numeric-input",
616
+ displayName: "Number text box",
617
+ defaultAlignment: "inline-block",
618
+ accessible: true,
619
+ widget: NumericInput,
620
+ transform: propsTransform,
621
+ isLintable: true,
622
+ }: WidgetExports<typeof NumericInput>);