@khanacademy/perseus 0.0.1 → 0.1.0

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 (503) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/dist/es/index.js +15 -1
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +11934 -0
  6. package/dist/index.js +15 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/strings.js +148 -1
  9. package/package.json +40 -4
  10. package/src/README.md +7 -0
  11. package/src/__stories__/item-renderer.stories.jsx +27 -0
  12. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  13. package/src/__testdata__/item-renderer_testdata.js +161 -0
  14. package/src/__testdata__/renderer_testdata.js +123 -0
  15. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
  16. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
  17. package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
  18. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
  19. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
  20. package/src/__tests__/a11y_test.jsx +304 -0
  21. package/src/__tests__/item-renderer_test.jsx +892 -0
  22. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  23. package/src/__tests__/mock-widget.jsx +35 -0
  24. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  25. package/src/__tests__/pure-markdown_test.js +552 -0
  26. package/src/__tests__/renderability_test.jsx +481 -0
  27. package/src/__tests__/renderer-api_test.jsx +333 -0
  28. package/src/__tests__/renderer_test.jsx +1792 -0
  29. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  30. package/src/__tests__/test-items/image-item.js +25 -0
  31. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  32. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  33. package/src/__tests__/test-items/table-item.js +30 -0
  34. package/src/__tests__/tex-wrangler_test.js +83 -0
  35. package/src/__tests__/util_test.js +41 -0
  36. package/src/a11y.js +46 -0
  37. package/src/all-widgets.js +9 -0
  38. package/src/article-renderer.jsx +215 -0
  39. package/src/asset-context.js +30 -0
  40. package/src/basic-widgets.js +13 -0
  41. package/src/components/__stories__/button-group.stories.jsx +61 -0
  42. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  43. package/src/components/__stories__/graph.stories.jsx +27 -0
  44. package/src/components/__stories__/graphie.stories.jsx +28 -0
  45. package/src/components/__stories__/hud.stories.jsx +36 -0
  46. package/src/components/__stories__/icon.stories.jsx +56 -0
  47. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  48. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  49. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  50. package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
  51. package/src/components/__stories__/lint.stories.jsx +40 -0
  52. package/src/components/__stories__/math-input.stories.jsx +29 -0
  53. package/src/components/__stories__/math-output.stories.jsx +24 -0
  54. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  55. package/src/components/__stories__/number-input.stories.jsx +42 -0
  56. package/src/components/__stories__/possible-answers.stories.jsx +22 -0
  57. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  58. package/src/components/__stories__/range-input.stories.jsx +28 -0
  59. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  60. package/src/components/__stories__/sortable.stories.jsx +73 -0
  61. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  62. package/src/components/__stories__/svg-image.stories.jsx +60 -0
  63. package/src/components/__stories__/tex.stories.jsx +18 -0
  64. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  65. package/src/components/__stories__/text-input.stories.jsx +34 -0
  66. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  67. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  68. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  69. package/src/components/__testdata__/sorter_testdata.js +20 -0
  70. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  71. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  72. package/src/components/__tests__/number-input_test.jsx +112 -0
  73. package/src/components/__tests__/sortable_test.jsx +234 -0
  74. package/src/components/__tests__/sorter_test.jsx +111 -0
  75. package/src/components/__tests__/zoomable_test.jsx +388 -0
  76. package/src/components/button-group.jsx +131 -0
  77. package/src/components/constants.js +15 -0
  78. package/src/components/fixed-to-responsive.jsx +163 -0
  79. package/src/components/graph.jsx +414 -0
  80. package/src/components/graphie-classes.jsx +148 -0
  81. package/src/components/graphie-movables.jsx +342 -0
  82. package/src/components/graphie.jsx +353 -0
  83. package/src/components/highlighting/highlightable-content.jsx +245 -0
  84. package/src/components/highlighting/highlights.js +208 -0
  85. package/src/components/highlighting/ranges.js +299 -0
  86. package/src/components/highlighting/types.js +73 -0
  87. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  88. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  89. package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
  90. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  91. package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
  92. package/src/components/highlighting/ui/types.js +21 -0
  93. package/src/components/highlighting/ui/util.js +166 -0
  94. package/src/components/highlighting/word-indexer.jsx +155 -0
  95. package/src/components/hud.jsx +179 -0
  96. package/src/components/icon.jsx +149 -0
  97. package/src/components/image-loader.jsx +175 -0
  98. package/src/components/info-tip/info-tip.jsx +149 -0
  99. package/src/components/info-tip.jsx +38 -0
  100. package/src/components/inline-icon.jsx +81 -0
  101. package/src/components/input-with-examples.jsx +231 -0
  102. package/src/components/lint.jsx +456 -0
  103. package/src/components/math-input.jsx +282 -0
  104. package/src/components/math-output.jsx +128 -0
  105. package/src/components/multi-button-group.jsx +137 -0
  106. package/src/components/new-tooltip/README.md +110 -0
  107. package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
  108. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
  109. package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
  110. package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
  111. package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
  112. package/src/components/new-tooltip/__tests__/util_test.js +47 -0
  113. package/src/components/new-tooltip/flexible-position.jsx +182 -0
  114. package/src/components/new-tooltip/new-tooltip.jsx +399 -0
  115. package/src/components/new-tooltip/style-constants.js +13 -0
  116. package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
  117. package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
  118. package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
  119. package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
  120. package/src/components/new-tooltip/types.js +183 -0
  121. package/src/components/new-tooltip/util.js +27 -0
  122. package/src/components/number-input.jsx +253 -0
  123. package/src/components/possible-answers.jsx +36 -0
  124. package/src/components/prop-check-box.jsx +78 -0
  125. package/src/components/range-input.jsx +61 -0
  126. package/src/components/simple-keypad-input.jsx +91 -0
  127. package/src/components/sortable.jsx +872 -0
  128. package/src/components/stub-tag-editor.jsx +53 -0
  129. package/src/components/svg-image.jsx +762 -0
  130. package/src/components/tex-buttons.jsx +322 -0
  131. package/src/components/tex.jsx +70 -0
  132. package/src/components/text-input.jsx +82 -0
  133. package/src/components/text-list-editor.jsx +157 -0
  134. package/src/components/tooltip.jsx +395 -0
  135. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  136. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  137. package/src/components/visibility-observer/visibility-observer.js +109 -0
  138. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  139. package/src/components/zoomable-tex.jsx +70 -0
  140. package/src/components/zoomable.jsx +304 -0
  141. package/src/components.js +17 -0
  142. package/src/dependencies.js +22 -0
  143. package/src/error-boundary.jsx +48 -0
  144. package/src/extra-widgets.js +75 -0
  145. package/src/gorgon/README +40 -0
  146. package/src/gorgon/__tests__/matcher_test.js +497 -0
  147. package/src/gorgon/__tests__/rule_test.js +101 -0
  148. package/src/gorgon/__tests__/rules_test.js +487 -0
  149. package/src/gorgon/__tests__/selector-parser_test.js +52 -0
  150. package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
  151. package/src/gorgon/gorgon.js +290 -0
  152. package/src/gorgon/proptypes.js +29 -0
  153. package/src/gorgon/rule.js +413 -0
  154. package/src/gorgon/rules/absolute-url.js +24 -0
  155. package/src/gorgon/rules/all-rules.js +72 -0
  156. package/src/gorgon/rules/blockquoted-math.js +10 -0
  157. package/src/gorgon/rules/blockquoted-widget.js +10 -0
  158. package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
  159. package/src/gorgon/rules/extra-content-spacing.js +12 -0
  160. package/src/gorgon/rules/heading-level-1.js +14 -0
  161. package/src/gorgon/rules/heading-level-skip.js +20 -0
  162. package/src/gorgon/rules/heading-sentence-case.js +11 -0
  163. package/src/gorgon/rules/heading-title-case.js +63 -0
  164. package/src/gorgon/rules/image-alt-text.js +21 -0
  165. package/src/gorgon/rules/image-in-table.js +10 -0
  166. package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
  167. package/src/gorgon/rules/image-widget.js +50 -0
  168. package/src/gorgon/rules/link-click-here.js +11 -0
  169. package/src/gorgon/rules/lint-utils.js +48 -0
  170. package/src/gorgon/rules/long-paragraph.js +14 -0
  171. package/src/gorgon/rules/math-adjacent.js +10 -0
  172. package/src/gorgon/rules/math-align-extra-break.js +11 -0
  173. package/src/gorgon/rules/math-align-linebreaks.js +43 -0
  174. package/src/gorgon/rules/math-empty.js +10 -0
  175. package/src/gorgon/rules/math-font-size.js +12 -0
  176. package/src/gorgon/rules/math-frac.js +10 -0
  177. package/src/gorgon/rules/math-nested.js +11 -0
  178. package/src/gorgon/rules/math-starts-with-space.js +12 -0
  179. package/src/gorgon/rules/math-text-empty.js +10 -0
  180. package/src/gorgon/rules/math-without-dollars.js +14 -0
  181. package/src/gorgon/rules/nested-lists.js +11 -0
  182. package/src/gorgon/rules/profanity.js +10 -0
  183. package/src/gorgon/rules/table-missing-cells.js +20 -0
  184. package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
  185. package/src/gorgon/rules/unescaped-dollar.js +10 -0
  186. package/src/gorgon/rules/widget-in-table.js +10 -0
  187. package/src/gorgon/selector.js +506 -0
  188. package/src/gorgon/tree-transformer.js +588 -0
  189. package/src/hint-renderer.jsx +159 -0
  190. package/src/hints-renderer.jsx +359 -0
  191. package/src/icon-paths.js +120 -0
  192. package/src/index.js +149 -2
  193. package/src/init.js +49 -0
  194. package/src/interaction-tracker.js +60 -0
  195. package/src/interactive2/__tests__/movable-mock.js +48 -0
  196. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  197. package/src/interactive2/interactive-util.js +152 -0
  198. package/src/interactive2/movable-helper-methods.js +160 -0
  199. package/src/interactive2/movable-line-options.js +392 -0
  200. package/src/interactive2/movable-line.js +322 -0
  201. package/src/interactive2/movable-point-options.js +190 -0
  202. package/src/interactive2/movable-point.jsx +604 -0
  203. package/src/interactive2/movable-polygon-options.js +351 -0
  204. package/src/interactive2/movable-polygon.js +367 -0
  205. package/src/interactive2/movable.js +273 -0
  206. package/src/interactive2/objective_.js +79 -0
  207. package/src/interactive2/types.js +3 -0
  208. package/src/interactive2/wrapped-defaults.js +86 -0
  209. package/src/interactive2/wrapped-ellipse.js +87 -0
  210. package/src/interactive2/wrapped-line.js +109 -0
  211. package/src/interactive2/wrapped-path.js +47 -0
  212. package/src/interactive2.js +91 -0
  213. package/src/item-renderer.jsx +518 -0
  214. package/src/item-version.js +12 -0
  215. package/src/jipt-paragraphs.js +44 -0
  216. package/src/jquery.mobile.vmouse.js +545 -0
  217. package/src/keypad-context.js +33 -0
  218. package/src/loading-context.js +20 -0
  219. package/src/logging/log.js +107 -0
  220. package/src/logging/log_test.js +41 -0
  221. package/src/mixins/changeable.jsx +115 -0
  222. package/src/mixins/editor-jsonify.jsx +13 -0
  223. package/src/mixins/provide-keypad.jsx +131 -0
  224. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  225. package/src/mixins/widget-prop-blacklist.js +36 -0
  226. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  227. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  228. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
  229. package/src/multi-items/__tests__/items_test.js +245 -0
  230. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  231. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  232. package/src/multi-items/__tests__/shapes_test.js +44 -0
  233. package/src/multi-items/__tests__/trees_test.js +316 -0
  234. package/src/multi-items/item-types.js +55 -0
  235. package/src/multi-items/items.js +176 -0
  236. package/src/multi-items/multi-renderer.jsx +542 -0
  237. package/src/multi-items/prop-type-builders.js +105 -0
  238. package/src/multi-items/shape-types.js +47 -0
  239. package/src/multi-items/shapes.js +53 -0
  240. package/src/multi-items/tree-types.js +42 -0
  241. package/src/multi-items/trees.js +345 -0
  242. package/src/multi-items.js +76 -0
  243. package/src/not-gorgon.js +129 -0
  244. package/src/perseus-api.jsx +270 -0
  245. package/src/perseus-env.js +26 -0
  246. package/src/perseus-error.js +19 -0
  247. package/src/perseus-markdown.jsx +434 -0
  248. package/src/perseus-types.js +1597 -0
  249. package/src/pure-markdown.js +346 -0
  250. package/src/question-paragraph.jsx +30 -0
  251. package/src/renderability.jsx +108 -0
  252. package/src/renderer.jsx +1931 -0
  253. package/src/server-item-renderer.jsx +467 -0
  254. package/src/sigfigs.jsx +492 -0
  255. package/src/styles/articles.less +748 -0
  256. package/src/styles/constants.js +119 -0
  257. package/src/styles/global-constants.js +350 -0
  258. package/src/styles/global-styles.js +307 -0
  259. package/src/styles/khan-exercise.css +1304 -0
  260. package/src/styles/media-queries.js +48 -0
  261. package/src/styles/perseus-renderer.less +1008 -0
  262. package/src/styles/shared.js +132 -0
  263. package/src/styles/util.less +47 -0
  264. package/src/styles/variables.boostrap.less +301 -0
  265. package/src/styles/variables.less +68 -0
  266. package/src/styles/variables.shared.less +356 -0
  267. package/src/styles/widgets/categorizer.less +39 -0
  268. package/src/styles/widgets/definition.less +86 -0
  269. package/src/styles/widgets/dropdown.less +3 -0
  270. package/src/styles/widgets/expression.less +147 -0
  271. package/src/styles/widgets/graded-group.less +36 -0
  272. package/src/styles/widgets/grapher.less +11 -0
  273. package/src/styles/widgets/group.less +19 -0
  274. package/src/styles/widgets/image.less +29 -0
  275. package/src/styles/widgets/interactive-graph.less +61 -0
  276. package/src/styles/widgets/label-image.less +41 -0
  277. package/src/styles/widgets/matcher.less +8 -0
  278. package/src/styles/widgets/matrix.less +120 -0
  279. package/src/styles/widgets/measurer.less +20 -0
  280. package/src/styles/widgets/orderer.less +97 -0
  281. package/src/styles/widgets/passage.less +151 -0
  282. package/src/styles/widgets/plotter.less +33 -0
  283. package/src/styles/widgets/radio.less +52 -0
  284. package/src/styles/widgets/reaction-diagram.less +35 -0
  285. package/src/styles/widgets/sequence.less +5 -0
  286. package/src/styles/widgets/sortable.less +127 -0
  287. package/src/styles/widgets/table.less +56 -0
  288. package/src/styles/widgets/transformer.less +28 -0
  289. package/src/styles/widgets/unit.less +14 -0
  290. package/src/tex-wrangler.js +176 -0
  291. package/src/traversal.jsx +141 -0
  292. package/src/types.js +538 -0
  293. package/src/util/a11y.js +24 -0
  294. package/src/util/answer-types.js +862 -0
  295. package/src/util/answer-types_test.js +115 -0
  296. package/src/util/colors.js +94 -0
  297. package/src/util/fix-passage-refs.jsx +122 -0
  298. package/src/util/graph-utils.js +16 -0
  299. package/src/util/graphie.js +1585 -0
  300. package/src/util/interactive.js +4071 -0
  301. package/src/util/katex-preprocess.js +16 -0
  302. package/src/util/math.js +296 -0
  303. package/src/util/mobile-native-utils.js +14 -0
  304. package/src/util/mobile-native-utils_test.js +24 -0
  305. package/src/util/register-all-widgets-for-testing.js +12 -0
  306. package/src/util/scroll-utils.js +162 -0
  307. package/src/util/sizing-utils.js +46 -0
  308. package/src/util/tex.js +203 -0
  309. package/src/util/url-utils.js +36 -0
  310. package/src/util.js +990 -0
  311. package/src/version.js +11 -0
  312. package/src/widget-container.jsx +174 -0
  313. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  314. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  315. package/src/widgets/__stories__/definition.stories.jsx +38 -0
  316. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  317. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  318. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  319. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  320. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  321. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  322. package/src/widgets/__stories__/group.stories.jsx +15 -0
  323. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  324. package/src/widgets/__stories__/image.stories.jsx +62 -0
  325. package/src/widgets/__stories__/input-number.stories.jsx +90 -0
  326. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  327. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  328. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  329. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  330. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  331. package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
  332. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  333. package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
  334. package/src/widgets/__stories__/passage.stories.jsx +19 -0
  335. package/src/widgets/__stories__/radio.stories.jsx +82 -0
  336. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  337. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  338. package/src/widgets/__stories__/video.stories.jsx +15 -0
  339. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  340. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  341. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  342. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  343. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  344. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  345. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  346. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  347. package/src/widgets/__testdata__/group_testdata.js +156 -0
  348. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  349. package/src/widgets/__testdata__/image_testdata.js +38 -0
  350. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  351. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  352. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  353. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  354. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  355. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  356. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  357. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  358. package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
  359. package/src/widgets/__testdata__/passage_testdata.js +71 -0
  360. package/src/widgets/__testdata__/radio_testdata.js +210 -0
  361. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  362. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  363. package/src/widgets/__testdata__/video_testdata.js +22 -0
  364. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
  365. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  366. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
  367. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
  368. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  369. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  370. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
  371. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
  372. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
  373. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
  374. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
  375. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  376. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
  377. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  378. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  379. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
  380. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  381. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
  382. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  383. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
  384. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  385. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  386. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  387. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
  388. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
  389. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  390. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  391. package/src/widgets/__tests__/definition_test.js +152 -0
  392. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  393. package/src/widgets/__tests__/explanation_test.js +199 -0
  394. package/src/widgets/__tests__/expression_test.jsx +439 -0
  395. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
  396. package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
  397. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  398. package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
  399. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  400. package/src/widgets/__tests__/group_test.jsx +512 -0
  401. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  402. package/src/widgets/__tests__/image_test.jsx +65 -0
  403. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  404. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  405. package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
  406. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  407. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  408. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  409. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  410. package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
  411. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  412. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  413. package/src/widgets/__tests__/passage_test.jsx +133 -0
  414. package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
  415. package/src/widgets/__tests__/radio_test.jsx +651 -0
  416. package/src/widgets/__tests__/renderQuestion.jsx +65 -0
  417. package/src/widgets/__tests__/unit_test.jsx +144 -0
  418. package/src/widgets/__tests__/video_test.jsx +55 -0
  419. package/src/widgets/categorizer.jsx +338 -0
  420. package/src/widgets/cs-program.jsx +228 -0
  421. package/src/widgets/definition.jsx +141 -0
  422. package/src/widgets/dropdown.jsx +143 -0
  423. package/src/widgets/explanation.jsx +315 -0
  424. package/src/widgets/expression.jsx +654 -0
  425. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  426. package/src/widgets/graded-group-set.jsx +285 -0
  427. package/src/widgets/graded-group.jsx +448 -0
  428. package/src/widgets/grapher/util.jsx +819 -0
  429. package/src/widgets/grapher.jsx +658 -0
  430. package/src/widgets/group.jsx +213 -0
  431. package/src/widgets/iframe.jsx +199 -0
  432. package/src/widgets/image.jsx +309 -0
  433. package/src/widgets/input-number.jsx +410 -0
  434. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  435. package/src/widgets/interaction/color-picker.jsx +69 -0
  436. package/src/widgets/interaction/dash-picker.jsx +46 -0
  437. package/src/widgets/interaction/element-container.jsx +97 -0
  438. package/src/widgets/interaction.jsx +742 -0
  439. package/src/widgets/interactive-graph.jsx +2821 -0
  440. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  441. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  442. package/src/widgets/label-image/answer-choices.jsx +311 -0
  443. package/src/widgets/label-image/marker.jsx +365 -0
  444. package/src/widgets/label-image/types.js +33 -0
  445. package/src/widgets/label-image.jsx +841 -0
  446. package/src/widgets/label-image_test.js +840 -0
  447. package/src/widgets/lights-puzzle.jsx +356 -0
  448. package/src/widgets/matcher.jsx +271 -0
  449. package/src/widgets/matrix.jsx +615 -0
  450. package/src/widgets/measurer.jsx +215 -0
  451. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  452. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  453. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  454. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  455. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  456. package/src/widgets/molecule.jsx +172 -0
  457. package/src/widgets/number-line.jsx +858 -0
  458. package/src/widgets/numeric-input.jsx +622 -0
  459. package/src/widgets/orderer.jsx +753 -0
  460. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
  461. package/src/widgets/passage/get-line-height-for-node.js +10 -0
  462. package/src/widgets/passage/passage-markdown.jsx +416 -0
  463. package/src/widgets/passage-ref-target.jsx +70 -0
  464. package/src/widgets/passage-ref.jsx +198 -0
  465. package/src/widgets/passage.jsx +601 -0
  466. package/src/widgets/plotter.jsx +1198 -0
  467. package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
  468. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
  469. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
  470. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
  471. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
  472. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
  473. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
  474. package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
  475. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
  476. package/src/widgets/radio/base-radio.jsx +642 -0
  477. package/src/widgets/radio/choice-icon.jsx +468 -0
  478. package/src/widgets/radio/choice-none-above.jsx +54 -0
  479. package/src/widgets/radio/choice.jsx +692 -0
  480. package/src/widgets/radio/choice.jsx.fixture.js +64 -0
  481. package/src/widgets/radio/cross-out-button.jsx +136 -0
  482. package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
  483. package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
  484. package/src/widgets/radio/focus-ring.jsx +51 -0
  485. package/src/widgets/radio/option-status.jsx +89 -0
  486. package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
  487. package/src/widgets/radio/util.js +32 -0
  488. package/src/widgets/radio/util_test.js +19 -0
  489. package/src/widgets/radio/widget.jsx +470 -0
  490. package/src/widgets/radio.jsx +156 -0
  491. package/src/widgets/reaction-diagram.jsx +136 -0
  492. package/src/widgets/sequence.jsx +140 -0
  493. package/src/widgets/simulator.jsx +808 -0
  494. package/src/widgets/sorter.jsx +146 -0
  495. package/src/widgets/table.jsx +395 -0
  496. package/src/widgets/transformer.jsx +2870 -0
  497. package/src/widgets/unit.jsx +371 -0
  498. package/src/widgets/video-transcript-link.jsx +84 -0
  499. package/src/widgets/video-transcript-link_test.js +211 -0
  500. package/src/widgets/video.jsx +131 -0
  501. package/src/widgets.js +431 -0
  502. package/src/zoom.js +539 -0
  503. package/src/__tests__/index.test.js +0 -14
@@ -0,0 +1,654 @@
1
+ // @flow
2
+ import {components, consts} from "@khanacademy/math-input";
3
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
4
+ import classNames from "classnames";
5
+ import KAS from "kas";
6
+ import * as React from "react";
7
+ import _ from "underscore";
8
+
9
+ import InlineIcon from "../components/inline-icon.jsx";
10
+ import InputWithExamples from "../components/input-with-examples.jsx";
11
+ import MathInput from "../components/math-input.jsx";
12
+ import Tooltip from "../components/tooltip.jsx";
13
+ import {linterContextDefault} from "../gorgon/proptypes.js";
14
+ import {iconExclamationSign} from "../icon-paths.js";
15
+ import {Errors as PerseusErrors, Log} from "../logging/log.js";
16
+ import * as Changeable from "../mixins/changeable.jsx";
17
+ import {ApiOptions, ClassNames as ApiClassNames} from "../perseus-api.jsx";
18
+ import KhanAnswerTypes from "../util/answer-types.js";
19
+
20
+ import type {PerseusExpressionWidgetOptions} from "../perseus-types.js";
21
+ import type {PerseusScore, WidgetExports, WidgetProps} from "../types.js";
22
+
23
+ const {KeypadInput} = components;
24
+ const {KeypadTypes} = consts;
25
+
26
+ type InputPath = $ReadOnlyArray<string>;
27
+
28
+ const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
29
+
30
+ const insertBraces = (value) => {
31
+ // HACK(alex): Make sure that all LaTeX super/subscripts are wrapped
32
+ // in curly braces to avoid the mismatch between KAS and LaTeX sup/sub
33
+ // parsing.
34
+ //
35
+ // What exactly is this mismatch? Due to its heritage of parsing plain
36
+ // text math from <OldExpression />, KAS parses "x^12" as x^(12).
37
+ // This is both generally what the user expects to happen, and is
38
+ // consistent with other computer algebra systems. It is NOT
39
+ // consistent with LaTeX however, where x^12 is equivalent to x^{1}2.
40
+ //
41
+ // Since the only LaTeX we parse comes from MathQuill, this wouldn't
42
+ // be a problem if MathQuill just always gave us the latter version
43
+ // (with explicit braces). However, instead it always gives the former.
44
+ // This behavior is baked in pretty deep; my naive attempts at changing
45
+ // it triggered all sorts of confusing errors. So instead we just make
46
+ // sure to add in any missing braces before grading MathQuill input.
47
+ //
48
+ // TODO(alex): Properly hack MathQuill to always use explicit braces.
49
+ return value.replace(/([_^])([^{])/g, "$1{$2}");
50
+ };
51
+
52
+ type Rubric = PerseusExpressionWidgetOptions;
53
+
54
+ type ExternalProps = WidgetProps<RenderProps, Rubric>;
55
+
56
+ export type Props = {|
57
+ ...ExternalProps,
58
+ apiOptions: $NonMaybeType<ExternalProps["apiOptions"]>,
59
+ buttonSets: $NonMaybeType<ExternalProps["buttonSets"]>,
60
+ functions: $NonMaybeType<ExternalProps["functions"]>,
61
+ linterContext: $NonMaybeType<ExternalProps["linterContext"]>,
62
+ onBlur: $NonMaybeType<ExternalProps["onBlur"]>,
63
+ onFocus: $NonMaybeType<ExternalProps["onFocus"]>,
64
+ times: $NonMaybeType<ExternalProps["times"]>,
65
+ value: string,
66
+ |};
67
+
68
+ export type ExpressionState = {|
69
+ showErrorTooltip: boolean,
70
+ showErrorText: boolean,
71
+ |};
72
+
73
+ type DefaultProps = {|
74
+ apiOptions: Props["apiOptions"],
75
+ buttonSets: Props["buttonSets"],
76
+ functions: Props["functions"],
77
+ linterContext: Props["linterContext"],
78
+ onBlur: Props["onBlur"],
79
+ onFocus: Props["onFocus"],
80
+ times: Props["times"],
81
+ value: Props["value"],
82
+ |};
83
+
84
+ type OnInputErrorFunctionType = (any, any, any) => ?boolean;
85
+
86
+ // The new, MathQuill input expression widget
87
+ export class Expression extends React.Component<Props, ExpressionState> {
88
+ //#region Previously a class extension
89
+ /* Content creators input a list of answers which are matched from top to
90
+ * bottom. The intent is that they can include spcific solutions which should
91
+ * be graded as correct or incorrect (or ungraded!) first, then get more
92
+ * general.
93
+ *
94
+ * We iterate through each answer, trying to match it with the user's input
95
+ * using the following angorithm:
96
+ * - Try to parse the user's input. If it doesn't parse then return "not
97
+ * graded".
98
+ * - For each answer:
99
+ * ~ Try to validate the user's input against the answer. The answer is
100
+ * expected to parse.
101
+ * ~ If the user's input validates (the validator judges it "correct"), we've
102
+ * matched and can stop considering answers.
103
+ * - If there were no matches or the matching answer is considered "ungraded",
104
+ * show the user an error. TODO(joel) - what error?
105
+ * - Otherwise, pass through the resulting points and message.
106
+ */
107
+ static validate(
108
+ userInput: string,
109
+ rubric: Rubric,
110
+ onInputError: OnInputErrorFunctionType = function () {},
111
+ ): PerseusScore {
112
+ const options = _.clone(rubric);
113
+ _.extend(options, {
114
+ decimal_separator: i18n.getDecimalSeparator(),
115
+ });
116
+
117
+ const createValidator = (answer) => {
118
+ // We give options to KAS.parse here because it is parsing the
119
+ // solution answer, not the student answer, and we don't want a
120
+ // solution to work if the student is using a different language
121
+ // (different from the content creation language, ie. English).
122
+ const expression = KAS.parse(answer.value, rubric);
123
+ // An answer may not be parsed if the expression was defined
124
+ // incorrectly. For example if the answer is using a symbol defined
125
+ // in the function variables list for the expression.
126
+ if (!expression.parsed) {
127
+ /* istanbul ignore next */
128
+ Log.error(
129
+ "Unable to parse solution answer for expression",
130
+ PerseusErrors.InvalidInput,
131
+ {loggedMetadata: {rubric: JSON.stringify(rubric)}},
132
+ );
133
+ return null;
134
+ }
135
+ return KhanAnswerTypes.expression.createValidatorFunctional(
136
+ expression.expr,
137
+ _({}).extend(options, {
138
+ simplify: answer.simplify,
139
+ form: answer.form,
140
+ }),
141
+ );
142
+ };
143
+
144
+ // Find the first answer form that matches the user's input and that
145
+ // is considered correct. Also, track whether the input is
146
+ // considered "empty" for all answer forms, and keep the validation
147
+ // result for the first answer form for which the user's input was
148
+ // considered "ungraded".
149
+ // (Terminology reminder: the answer forms are provided by the
150
+ // assessment items; they are not the user's input. Each one might
151
+ // represent a correct answer, an incorrect one (if the exercise
152
+ // creator has predicted certain common wrong answers and wants to
153
+ // provide guidance via a message), or an ungraded one (same idea,
154
+ // but without giving the user an incorrect mark for the question).
155
+ let matchingAnswerForm;
156
+ let matchMessage;
157
+ let allEmpty = true;
158
+ let firstUngradedResult;
159
+ for (const answerForm of rubric.answerForms || []) {
160
+ const validate = createValidator(answerForm);
161
+ if (!validate) {
162
+ continue;
163
+ }
164
+
165
+ const result = validate(userInput);
166
+
167
+ // Short-circuit as soon as the user's input matches some answer
168
+ // (independently of whether the answer is correct)
169
+ if (result.correct) {
170
+ matchingAnswerForm = answerForm;
171
+ matchMessage = result.message || "";
172
+ break;
173
+ }
174
+
175
+ allEmpty = allEmpty && result.empty;
176
+ // If this answer form is correct and the user's input is considered
177
+ // "ungraded" for it, we'll want to keep the evaluation result for
178
+ // later. If the user's input doesn't match any answer forms, we'll
179
+ // show the message from this validation.
180
+ if (
181
+ answerForm.considered === "correct" &&
182
+ result.ungraded &&
183
+ !firstUngradedResult
184
+ ) {
185
+ firstUngradedResult = result;
186
+ }
187
+ }
188
+
189
+ // Now check to see if we matched any answer form at all, and if
190
+ // we did, whether it's considered correct, incorrect, or ungraded
191
+ if (!matchingAnswerForm) {
192
+ if (firstUngradedResult) {
193
+ // While we didn't directly match with any answer form, we
194
+ // did at some point get an "ungraded" validation result,
195
+ // which might indicate e.g. a mismatch in variable casing.
196
+ // We'll return "invalid", which will let the user try again
197
+ // with no penalty, and the hopefully helpful validation
198
+ // message.
199
+ return {
200
+ type: "invalid",
201
+ message: firstUngradedResult.message,
202
+ suppressAlmostThere:
203
+ firstUngradedResult.suppressAlmostThere,
204
+ };
205
+ }
206
+ if (allEmpty) {
207
+ // If everything graded as empty, it's invalid.
208
+ return {
209
+ type: "invalid",
210
+ message: null,
211
+ };
212
+ }
213
+ // We fell through all the possibilities and we're not empty,
214
+ // so the answer is considered incorrect.
215
+ return {
216
+ type: "points",
217
+ earned: 0,
218
+ total: 1,
219
+ };
220
+ }
221
+ if (matchingAnswerForm.considered === "ungraded") {
222
+ // We matched an ungraded answer form - return "invalid", which
223
+ // will let the user try again with no penalty
224
+ const apiResult = onInputError(
225
+ null, // Reserved for some widget identifier
226
+ userInput,
227
+ matchMessage,
228
+ );
229
+ return {
230
+ type: "invalid",
231
+ message: apiResult === false ? null : matchMessage,
232
+ };
233
+ }
234
+ // We matched a graded answer form, so we can now tell the user
235
+ // whether their input was correct or incorrect, and hand out
236
+ // points accordingly
237
+ // TODO(eater): Seems silly to translate result to this
238
+ // invalid/points thing and immediately translate it back in
239
+ // ItemRenderer.scoreInput()
240
+ return {
241
+ type: "points",
242
+ earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
243
+ total: 1,
244
+ message: matchMessage,
245
+ };
246
+ }
247
+
248
+ static getUserInputFromProps(props: Props): string {
249
+ return insertBraces(props.value);
250
+ }
251
+
252
+ static getOneCorrectAnswerFromRubric(rubric: Rubric): ?string {
253
+ const correctAnswers = (rubric.answerForms || []).filter(
254
+ (answerForm) => answerForm.considered === "correct",
255
+ );
256
+ if (correctAnswers.length === 0) {
257
+ return;
258
+ }
259
+ return correctAnswers[0].value;
260
+ }
261
+ //#endregion
262
+
263
+ static defaultProps: DefaultProps = {
264
+ value: "",
265
+ times: false,
266
+ functions: [],
267
+ buttonSets: ["basic", "trig", "prealgebra", "logarithms"],
268
+ onFocus: () => {},
269
+ onBlur: () => {},
270
+ apiOptions: ApiOptions.defaults,
271
+ linterContext: linterContextDefault,
272
+ };
273
+
274
+ displayName: string = "Expression";
275
+
276
+ state: ExpressionState = {
277
+ showErrorTooltip: false,
278
+ showErrorText: false,
279
+ };
280
+
281
+ componentDidMount: () => void = () => {
282
+ // TODO(scottgrant): This is a hack to remove the deprecated call to
283
+ // this.isMounted() but is still considered an anti-pattern.
284
+ this._isMounted = true;
285
+ };
286
+
287
+ // Whenever the input value changes, attempt to parse it.
288
+ //
289
+ // Clear any errors if this parse succeeds, show an error within a second
290
+ // if it fails.
291
+ componentDidUpdate: (prevProps: Props) => void = (prevProps) => {
292
+ if (
293
+ !_.isEqual(this.props.value, prevProps.value) ||
294
+ !_.isEqual(this.props.functions, prevProps.functions)
295
+ ) {
296
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
297
+ // eslint-disable-next-line no-restricted-syntax
298
+ clearTimeout(this.errorTimeout);
299
+
300
+ if (this.parse(this.props.value, this.props).parsed) {
301
+ // eslint-disable-next-line react/no-did-update-set-state
302
+ this.setState({showErrorTooltip: false});
303
+ } else {
304
+ // Store timeout ID so that we can clear it above
305
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
306
+ // eslint-disable-next-line no-restricted-syntax
307
+ this.errorTimeout = setTimeout(() => {
308
+ const apiResult = this.props.apiOptions.onInputError(
309
+ null, // reserved for some widget identifier
310
+ this.props.value,
311
+ ERROR_MESSAGE,
312
+ );
313
+ if (apiResult !== false) {
314
+ this.setState({showErrorTooltip: true});
315
+ }
316
+ }, 2000);
317
+ }
318
+ }
319
+ };
320
+
321
+ componentWillUnmount: () => void = () => {
322
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
323
+ // eslint-disable-next-line no-restricted-syntax
324
+ clearTimeout(this.errorTimeout);
325
+
326
+ this._isMounted = false;
327
+ };
328
+
329
+ _isMounted: boolean = false;
330
+ errorTimeout: null | TimeoutID = null;
331
+
332
+ simpleValidate: (
333
+ rubric: Rubric,
334
+ onInputError: OnInputErrorFunctionType,
335
+ ) => PerseusScore = (rubric, onInputError) => {
336
+ onInputError = onInputError || function () {};
337
+ return Expression.validate(this.getUserInput(), rubric, onInputError);
338
+ };
339
+
340
+ getUserInput: () => string = () => {
341
+ return Expression.getUserInputFromProps(this.props);
342
+ };
343
+
344
+ change: (...args: any) => any | void = (...args: any) => {
345
+ return Changeable.change.apply(this, args);
346
+ };
347
+
348
+ parse: (value: string, props: Props) => any = (
349
+ value: string,
350
+ props: Props,
351
+ ) => {
352
+ // TODO(jack): Disable icu for content creators here, or
353
+ // make it so that solution answers with ','s or '.'s work
354
+ const options = _.pick(props || this.props, "functions");
355
+ _.extend(options, {
356
+ decimal_separator: i18n.getDecimalSeparator(),
357
+ });
358
+ return KAS.parse(insertBraces(value), options);
359
+ };
360
+
361
+ changeAndTrack: (e: any, cb: () => void) => void = (
362
+ e: any,
363
+ cb: () => void,
364
+ ) => {
365
+ this.change("value", e, cb);
366
+ this.props.trackInteraction();
367
+ };
368
+
369
+ _handleFocus: () => void = () => {
370
+ /* istanbul ignore next */
371
+ this.props.onFocus([]);
372
+ };
373
+
374
+ _handleBlur: () => void = () => {
375
+ /* istanbul ignore next */
376
+ this.props.onBlur([]);
377
+ };
378
+
379
+ focus: () => boolean = () => {
380
+ if (this.props.apiOptions.customKeypad) {
381
+ // eslint-disable-next-line react/no-string-refs
382
+ this.refs.input.focus();
383
+ } else {
384
+ // The buttons are often on top of text you're trying to read, so
385
+ // don't focus the editor automatically.
386
+ }
387
+
388
+ return true;
389
+ };
390
+
391
+ focusInputPath: (inputPath: InputPath) => void = (inputPath: InputPath) => {
392
+ // eslint-disable-next-line react/no-string-refs
393
+ this.refs.input.focus();
394
+ };
395
+
396
+ blurInputPath: (inputPath: InputPath) => void = (inputPath: InputPath) => {
397
+ // eslint-disable-next-line react/no-string-refs
398
+ this.refs.input.blur();
399
+ };
400
+
401
+ // HACK(joel)
402
+ insert: (text: string) => void = (text: string) => {
403
+ if (!this.props.apiOptions.staticRender) {
404
+ // eslint-disable-next-line react/no-string-refs
405
+ this.refs.input.insert(text);
406
+ }
407
+ };
408
+
409
+ getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<any>> = () => {
410
+ // The widget itself is an input, so we return a single empty list to
411
+ // indicate this.
412
+ /* istanbul ignore next */
413
+ return [[]];
414
+ };
415
+
416
+ // TODO(Nicole): I believe this is going away and won't be needed anymore
417
+ getGrammarTypeForPath: (inputPath: string) => string = (
418
+ inputPath: string,
419
+ ) => {
420
+ /* istanbul ignore next */
421
+ return "expression";
422
+ };
423
+
424
+ setInputValue: (path: string, newValue: string, cb: () => void) => void = (
425
+ path: string,
426
+ newValue: string,
427
+ cb: () => void,
428
+ ) => {
429
+ this.props.onChange(
430
+ {
431
+ value: newValue,
432
+ },
433
+ cb,
434
+ );
435
+ };
436
+
437
+ render(): React.Node | React.Element<"div"> {
438
+ if (this.props.apiOptions.customKeypad) {
439
+ return (
440
+ <KeypadInput
441
+ // eslint-disable-next-line react/no-string-refs
442
+ ref="input"
443
+ value={this.props.value}
444
+ keypadElement={this.props.keypadElement}
445
+ onChange={this.changeAndTrack}
446
+ onFocus={() => {
447
+ // this.props.keypadElement should always be set
448
+ // when apiOptions.customKeypad is set, but how
449
+ // to convince Flow of this?
450
+ this.props.keypadElement?.configure(
451
+ this.props.keypadConfiguration,
452
+ () => {
453
+ if (this._isMounted) {
454
+ this._handleFocus();
455
+ }
456
+ },
457
+ );
458
+ }}
459
+ onBlur={this._handleBlur}
460
+ />
461
+ );
462
+ }
463
+ if (this.props.apiOptions.staticRender) {
464
+ // To make things slightly easier, we just use an InputWithExamples
465
+ // component to handle the static rendering, which is the same
466
+ // component used by InputNumber and NumericInput
467
+ return (
468
+ <InputWithExamples
469
+ // eslint-disable-next-line react/no-string-refs
470
+ ref="input"
471
+ value={this.props.value}
472
+ type="tex"
473
+ examples={[]}
474
+ shouldShowExamples={false}
475
+ onChange={this.changeAndTrack}
476
+ onFocus={this._handleFocus}
477
+ onBlur={this._handleBlur}
478
+ id={this.props.widgetId}
479
+ linterContext={this.props.linterContext}
480
+ />
481
+ );
482
+ }
483
+ // TODO(alex): Style this tooltip to be more consistent with other
484
+ // tooltips on the site; align to left middle (once possible)
485
+ const errorTooltip = (
486
+ <span className="error-tooltip" role="tooltip">
487
+ <Tooltip
488
+ className="error-text-container"
489
+ horizontalPosition="right"
490
+ horizontalAlign="left"
491
+ verticalPosition="top"
492
+ arrowSize={10}
493
+ borderColor="#fcc335"
494
+ show={this.state.showErrorText}
495
+ >
496
+ <span
497
+ className="error-icon"
498
+ data-test-id="test-error-icon"
499
+ onMouseEnter={() => {
500
+ this.setState({showErrorText: true});
501
+ }}
502
+ onMouseLeave={() => {
503
+ this.setState({showErrorText: false});
504
+ }}
505
+ onClick={() => {
506
+ // TODO(alex): Better error feedback for mobile
507
+ this.setState({
508
+ showErrorText: !this.state.showErrorText,
509
+ });
510
+ }}
511
+ >
512
+ <InlineIcon {...iconExclamationSign} />
513
+ </span>
514
+ <div className="error-text">{ERROR_MESSAGE}</div>
515
+ </Tooltip>
516
+ </span>
517
+ );
518
+
519
+ const className = classNames({
520
+ "perseus-widget-expression": true,
521
+ "show-error-tooltip": this.state.showErrorTooltip,
522
+ });
523
+
524
+ return (
525
+ <div className={className}>
526
+ <MathInput
527
+ // eslint-disable-next-line react/no-string-refs
528
+ ref="input"
529
+ className={ApiClassNames.INTERACTIVE}
530
+ value={this.props.value}
531
+ onChange={this.changeAndTrack}
532
+ convertDotToTimes={this.props.times}
533
+ buttonsVisible={this.props.buttonsVisible || "focused"}
534
+ buttonSets={this.props.buttonSets}
535
+ onFocus={this._handleFocus}
536
+ onBlur={this._handleBlur}
537
+ />
538
+ {this.state.showErrorTooltip && errorTooltip}
539
+ </div>
540
+ );
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Determine the keypad configuration parameters for the input, based on the
546
+ * provided properties.
547
+ *
548
+ * There are two configuration parameters to be passed to the keypad:
549
+ * (1) The keypad type. For the Expression widget, we always use the
550
+ * Expression keypad.
551
+ * (2) The extra keys; namely, any variables or constants (like Pi) that need
552
+ * to be included as keys on the keypad. These are scraped from the answer
553
+ * forms.
554
+ */
555
+ const keypadConfigurationForProps = (
556
+ widgetOptions: PerseusExpressionWidgetOptions,
557
+ ) => {
558
+ // Always use the Expression keypad, regardless of the button sets that have
559
+ // been enabled.
560
+ const keypadType = KeypadTypes.EXPRESSION;
561
+
562
+ // Extract any and all variables and constants from the answer forms.
563
+ const uniqueExtraVariables = {};
564
+ const uniqueExtraConstants = {};
565
+ for (const answerForm of widgetOptions.answerForms) {
566
+ const maybeExpr = KAS.parse(answerForm.value, widgetOptions);
567
+ if (maybeExpr.parsed) {
568
+ const expr = maybeExpr.expr;
569
+
570
+ // The keypad expects Greek letters to be capitalized (e.g., it
571
+ // requires `PI` instead of `pi`). Right now, it only supports Pi
572
+ // and Theta, so we special-case.
573
+ const isGreek = (symbol) => symbol === "pi" || symbol === "theta";
574
+ const toKey = (symbol) =>
575
+ isGreek(symbol) ? symbol.toUpperCase() : symbol;
576
+
577
+ for (const variable of expr.getVars()) {
578
+ uniqueExtraVariables[toKey(variable)] = true;
579
+ }
580
+ for (const constant of expr.getConsts()) {
581
+ uniqueExtraConstants[toKey(constant)] = true;
582
+ }
583
+ }
584
+ }
585
+
586
+ // TODO(charlie): Alert the keypad as to which of these symbols should be
587
+ // treated as functions.
588
+ const extraVariables = Object.keys(uniqueExtraVariables);
589
+ extraVariables.sort();
590
+
591
+ const extraConstants = Object.keys(uniqueExtraConstants);
592
+ extraConstants.sort();
593
+
594
+ const extraKeys = [...extraVariables, ...extraConstants];
595
+ if (!extraKeys.length) {
596
+ // If there are no extra symbols available, we include Pi anyway, so
597
+ // that the "extra symbols" button doesn't appear empty.
598
+ extraKeys.push("PI");
599
+ }
600
+
601
+ return {keypadType, extraKeys};
602
+ };
603
+
604
+ const propUpgrades = {
605
+ /* istanbul ignore next */
606
+ "1": (v0props: $FlowFixMe): PerseusExpressionWidgetOptions => ({
607
+ times: v0props.times,
608
+ buttonSets: v0props.buttonSets,
609
+ functions: v0props.functions,
610
+ buttonsVisible: v0props.buttonsVisible,
611
+
612
+ answerForms: [
613
+ {
614
+ considered: "correct",
615
+ form: v0props.form,
616
+ simplify: v0props.simplify,
617
+ value: v0props.value,
618
+ },
619
+ ],
620
+ }),
621
+ };
622
+
623
+ type RenderProps = {|
624
+ buttonSets: $FlowFixMe,
625
+ buttonsVisible?: "always" | "focused" | "never",
626
+ functions: $ReadOnlyArray<string>,
627
+ keypadConfiguration: {|
628
+ extraKeys: $ReadOnlyArray<any | string>,
629
+ keypadType: $FlowFixMe,
630
+ |},
631
+ times: boolean,
632
+ |};
633
+
634
+ export default ({
635
+ name: "expression",
636
+ displayName: "Expression / Equation",
637
+ defaultAlignment: "inline-block",
638
+ widget: Expression,
639
+ transform: (widgetOptions: PerseusExpressionWidgetOptions): RenderProps => {
640
+ const {times, functions, buttonSets, buttonsVisible} = widgetOptions;
641
+ return {
642
+ keypadConfiguration: keypadConfigurationForProps(widgetOptions),
643
+ times,
644
+ functions,
645
+ buttonSets,
646
+ buttonsVisible,
647
+ };
648
+ },
649
+ version: {major: 1, minor: 0},
650
+ propUpgrades: propUpgrades,
651
+
652
+ // For use by the editor
653
+ isLintable: true,
654
+ }: WidgetExports<typeof Expression>);