@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,652 @@
1
+ /* eslint-disable react/prop-types */
2
+ // @flow
3
+ import * as KAS from "@khanacademy/kas";
4
+ import {KeypadInput, KeypadTypes} from "@khanacademy/math-input";
5
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
6
+ import classNames from "classnames";
7
+ import * as React from "react";
8
+ import _ from "underscore";
9
+
10
+ import InlineIcon from "../components/inline-icon.jsx";
11
+ import InputWithExamples from "../components/input-with-examples.jsx";
12
+ import MathInput from "../components/math-input.jsx";
13
+ import Tooltip from "../components/tooltip.jsx";
14
+ import {linterContextDefault} from "../gorgon/proptypes.js";
15
+ import {iconExclamationSign} from "../icon-paths.js";
16
+ import {Errors as PerseusErrors, Log} from "../logging/log.js";
17
+ import * as Changeable from "../mixins/changeable.jsx";
18
+ import {ApiOptions, ClassNames as ApiClassNames} from "../perseus-api.jsx";
19
+ import KhanAnswerTypes from "../util/answer-types.js";
20
+
21
+ import type {PerseusExpressionWidgetOptions} from "../perseus-types.js";
22
+ import type {PerseusScore, WidgetExports, WidgetProps} from "../types.js";
23
+
24
+ type InputPath = $ReadOnlyArray<string>;
25
+
26
+ const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
27
+
28
+ const insertBraces = (value) => {
29
+ // HACK(alex): Make sure that all LaTeX super/subscripts are wrapped
30
+ // in curly braces to avoid the mismatch between KAS and LaTeX sup/sub
31
+ // parsing.
32
+ //
33
+ // What exactly is this mismatch? Due to its heritage of parsing plain
34
+ // text math from <OldExpression />, KAS parses "x^12" as x^(12).
35
+ // This is both generally what the user expects to happen, and is
36
+ // consistent with other computer algebra systems. It is NOT
37
+ // consistent with LaTeX however, where x^12 is equivalent to x^{1}2.
38
+ //
39
+ // Since the only LaTeX we parse comes from MathQuill, this wouldn't
40
+ // be a problem if MathQuill just always gave us the latter version
41
+ // (with explicit braces). However, instead it always gives the former.
42
+ // This behavior is baked in pretty deep; my naive attempts at changing
43
+ // it triggered all sorts of confusing errors. So instead we just make
44
+ // sure to add in any missing braces before grading MathQuill input.
45
+ //
46
+ // TODO(alex): Properly hack MathQuill to always use explicit braces.
47
+ return value.replace(/([_^])([^{])/g, "$1{$2}");
48
+ };
49
+
50
+ type Rubric = PerseusExpressionWidgetOptions;
51
+
52
+ type ExternalProps = WidgetProps<RenderProps, Rubric>;
53
+
54
+ export type Props = {|
55
+ ...ExternalProps,
56
+ apiOptions: $NonMaybeType<ExternalProps["apiOptions"]>,
57
+ buttonSets: $NonMaybeType<ExternalProps["buttonSets"]>,
58
+ functions: $NonMaybeType<ExternalProps["functions"]>,
59
+ linterContext: $NonMaybeType<ExternalProps["linterContext"]>,
60
+ onBlur: $NonMaybeType<ExternalProps["onBlur"]>,
61
+ onFocus: $NonMaybeType<ExternalProps["onFocus"]>,
62
+ times: $NonMaybeType<ExternalProps["times"]>,
63
+ value: string,
64
+ |};
65
+
66
+ export type ExpressionState = {|
67
+ showErrorTooltip: boolean,
68
+ showErrorText: boolean,
69
+ |};
70
+
71
+ type DefaultProps = {|
72
+ apiOptions: Props["apiOptions"],
73
+ buttonSets: Props["buttonSets"],
74
+ functions: Props["functions"],
75
+ linterContext: Props["linterContext"],
76
+ onBlur: Props["onBlur"],
77
+ onFocus: Props["onFocus"],
78
+ times: Props["times"],
79
+ value: Props["value"],
80
+ |};
81
+
82
+ type OnInputErrorFunctionType = (any, any, any) => ?boolean;
83
+
84
+ // The new, MathQuill input expression widget
85
+ export class Expression extends React.Component<Props, ExpressionState> {
86
+ //#region Previously a class extension
87
+ /* Content creators input a list of answers which are matched from top to
88
+ * bottom. The intent is that they can include spcific solutions which should
89
+ * be graded as correct or incorrect (or ungraded!) first, then get more
90
+ * general.
91
+ *
92
+ * We iterate through each answer, trying to match it with the user's input
93
+ * using the following angorithm:
94
+ * - Try to parse the user's input. If it doesn't parse then return "not
95
+ * graded".
96
+ * - For each answer:
97
+ * ~ Try to validate the user's input against the answer. The answer is
98
+ * expected to parse.
99
+ * ~ If the user's input validates (the validator judges it "correct"), we've
100
+ * matched and can stop considering answers.
101
+ * - If there were no matches or the matching answer is considered "ungraded",
102
+ * show the user an error. TODO(joel) - what error?
103
+ * - Otherwise, pass through the resulting points and message.
104
+ */
105
+ static validate(
106
+ userInput: string,
107
+ rubric: Rubric,
108
+ onInputError: OnInputErrorFunctionType = function () {},
109
+ ): PerseusScore {
110
+ const options = _.clone(rubric);
111
+ _.extend(options, {
112
+ decimal_separator: i18n.getDecimalSeparator(),
113
+ });
114
+
115
+ const createValidator = (answer) => {
116
+ // We give options to KAS.parse here because it is parsing the
117
+ // solution answer, not the student answer, and we don't want a
118
+ // solution to work if the student is using a different language
119
+ // (different from the content creation language, ie. English).
120
+ const expression = KAS.parse(answer.value, rubric);
121
+ // An answer may not be parsed if the expression was defined
122
+ // incorrectly. For example if the answer is using a symbol defined
123
+ // in the function variables list for the expression.
124
+ if (!expression.parsed) {
125
+ /* istanbul ignore next */
126
+ Log.error(
127
+ "Unable to parse solution answer for expression",
128
+ PerseusErrors.InvalidInput,
129
+ {loggedMetadata: {rubric: JSON.stringify(rubric)}},
130
+ );
131
+ return null;
132
+ }
133
+ return KhanAnswerTypes.expression.createValidatorFunctional(
134
+ expression.expr,
135
+ _({}).extend(options, {
136
+ simplify: answer.simplify,
137
+ form: answer.form,
138
+ }),
139
+ );
140
+ };
141
+
142
+ // Find the first answer form that matches the user's input and that
143
+ // is considered correct. Also, track whether the input is
144
+ // considered "empty" for all answer forms, and keep the validation
145
+ // result for the first answer form for which the user's input was
146
+ // considered "ungraded".
147
+ // (Terminology reminder: the answer forms are provided by the
148
+ // assessment items; they are not the user's input. Each one might
149
+ // represent a correct answer, an incorrect one (if the exercise
150
+ // creator has predicted certain common wrong answers and wants to
151
+ // provide guidance via a message), or an ungraded one (same idea,
152
+ // but without giving the user an incorrect mark for the question).
153
+ let matchingAnswerForm;
154
+ let matchMessage;
155
+ let allEmpty = true;
156
+ let firstUngradedResult;
157
+ for (const answerForm of rubric.answerForms || []) {
158
+ const validate = createValidator(answerForm);
159
+ if (!validate) {
160
+ continue;
161
+ }
162
+
163
+ const result = validate(userInput);
164
+
165
+ // Short-circuit as soon as the user's input matches some answer
166
+ // (independently of whether the answer is correct)
167
+ if (result.correct) {
168
+ matchingAnswerForm = answerForm;
169
+ matchMessage = result.message || "";
170
+ break;
171
+ }
172
+
173
+ allEmpty = allEmpty && result.empty;
174
+ // If this answer form is correct and the user's input is considered
175
+ // "ungraded" for it, we'll want to keep the evaluation result for
176
+ // later. If the user's input doesn't match any answer forms, we'll
177
+ // show the message from this validation.
178
+ if (
179
+ answerForm.considered === "correct" &&
180
+ result.ungraded &&
181
+ !firstUngradedResult
182
+ ) {
183
+ firstUngradedResult = result;
184
+ }
185
+ }
186
+
187
+ // Now check to see if we matched any answer form at all, and if
188
+ // we did, whether it's considered correct, incorrect, or ungraded
189
+ if (!matchingAnswerForm) {
190
+ if (firstUngradedResult) {
191
+ // While we didn't directly match with any answer form, we
192
+ // did at some point get an "ungraded" validation result,
193
+ // which might indicate e.g. a mismatch in variable casing.
194
+ // We'll return "invalid", which will let the user try again
195
+ // with no penalty, and the hopefully helpful validation
196
+ // message.
197
+ return {
198
+ type: "invalid",
199
+ message: firstUngradedResult.message,
200
+ suppressAlmostThere:
201
+ firstUngradedResult.suppressAlmostThere,
202
+ };
203
+ }
204
+ if (allEmpty) {
205
+ // If everything graded as empty, it's invalid.
206
+ return {
207
+ type: "invalid",
208
+ message: null,
209
+ };
210
+ }
211
+ // We fell through all the possibilities and we're not empty,
212
+ // so the answer is considered incorrect.
213
+ return {
214
+ type: "points",
215
+ earned: 0,
216
+ total: 1,
217
+ };
218
+ }
219
+ if (matchingAnswerForm.considered === "ungraded") {
220
+ // We matched an ungraded answer form - return "invalid", which
221
+ // will let the user try again with no penalty
222
+ const apiResult = onInputError(
223
+ null, // Reserved for some widget identifier
224
+ userInput,
225
+ matchMessage,
226
+ );
227
+ return {
228
+ type: "invalid",
229
+ message: apiResult === false ? null : matchMessage,
230
+ };
231
+ }
232
+ // We matched a graded answer form, so we can now tell the user
233
+ // whether their input was correct or incorrect, and hand out
234
+ // points accordingly
235
+ // TODO(eater): Seems silly to translate result to this
236
+ // invalid/points thing and immediately translate it back in
237
+ // ItemRenderer.scoreInput()
238
+ return {
239
+ type: "points",
240
+ earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
241
+ total: 1,
242
+ message: matchMessage,
243
+ };
244
+ }
245
+
246
+ static getUserInputFromProps(props: Props): string {
247
+ return insertBraces(props.value);
248
+ }
249
+
250
+ static getOneCorrectAnswerFromRubric(rubric: Rubric): ?string {
251
+ const correctAnswers = (rubric.answerForms || []).filter(
252
+ (answerForm) => answerForm.considered === "correct",
253
+ );
254
+ if (correctAnswers.length === 0) {
255
+ return;
256
+ }
257
+ return correctAnswers[0].value;
258
+ }
259
+ //#endregion
260
+
261
+ static defaultProps: DefaultProps = {
262
+ value: "",
263
+ times: false,
264
+ functions: [],
265
+ buttonSets: ["basic", "trig", "prealgebra", "logarithms"],
266
+ onFocus: () => {},
267
+ onBlur: () => {},
268
+ apiOptions: ApiOptions.defaults,
269
+ linterContext: linterContextDefault,
270
+ };
271
+
272
+ displayName: string = "Expression";
273
+
274
+ state: ExpressionState = {
275
+ showErrorTooltip: false,
276
+ showErrorText: false,
277
+ };
278
+
279
+ componentDidMount: () => void = () => {
280
+ // TODO(scottgrant): This is a hack to remove the deprecated call to
281
+ // this.isMounted() but is still considered an anti-pattern.
282
+ this._isMounted = true;
283
+ };
284
+
285
+ // Whenever the input value changes, attempt to parse it.
286
+ //
287
+ // Clear any errors if this parse succeeds, show an error within a second
288
+ // if it fails.
289
+ componentDidUpdate: (prevProps: Props) => void = (prevProps) => {
290
+ if (
291
+ !_.isEqual(this.props.value, prevProps.value) ||
292
+ !_.isEqual(this.props.functions, prevProps.functions)
293
+ ) {
294
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
295
+ // eslint-disable-next-line no-restricted-syntax
296
+ clearTimeout(this.errorTimeout);
297
+
298
+ if (this.parse(this.props.value, this.props).parsed) {
299
+ // eslint-disable-next-line react/no-did-update-set-state
300
+ this.setState({showErrorTooltip: false});
301
+ } else {
302
+ // Store timeout ID so that we can clear it above
303
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
304
+ // eslint-disable-next-line no-restricted-syntax
305
+ this.errorTimeout = setTimeout(() => {
306
+ const apiResult = this.props.apiOptions.onInputError(
307
+ null, // reserved for some widget identifier
308
+ this.props.value,
309
+ ERROR_MESSAGE,
310
+ );
311
+ if (apiResult !== false) {
312
+ this.setState({showErrorTooltip: true});
313
+ }
314
+ }, 2000);
315
+ }
316
+ }
317
+ };
318
+
319
+ componentWillUnmount: () => void = () => {
320
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
321
+ // eslint-disable-next-line no-restricted-syntax
322
+ clearTimeout(this.errorTimeout);
323
+
324
+ this._isMounted = false;
325
+ };
326
+
327
+ _isMounted: boolean = false;
328
+ errorTimeout: null | TimeoutID = null;
329
+
330
+ simpleValidate: (
331
+ rubric: Rubric,
332
+ onInputError: OnInputErrorFunctionType,
333
+ ) => PerseusScore = (rubric, onInputError) => {
334
+ onInputError = onInputError || function () {};
335
+ return Expression.validate(this.getUserInput(), rubric, onInputError);
336
+ };
337
+
338
+ getUserInput: () => string = () => {
339
+ return Expression.getUserInputFromProps(this.props);
340
+ };
341
+
342
+ change: (...args: any) => any | void = (...args: any) => {
343
+ return Changeable.change.apply(this, args);
344
+ };
345
+
346
+ parse: (value: string, props: Props) => any = (
347
+ value: string,
348
+ props: Props,
349
+ ) => {
350
+ // TODO(jack): Disable icu for content creators here, or
351
+ // make it so that solution answers with ','s or '.'s work
352
+ const options = _.pick(props || this.props, "functions");
353
+ _.extend(options, {
354
+ decimal_separator: i18n.getDecimalSeparator(),
355
+ });
356
+ return KAS.parse(insertBraces(value), options);
357
+ };
358
+
359
+ changeAndTrack: (e: any, cb: () => void) => void = (
360
+ e: any,
361
+ cb: () => void,
362
+ ) => {
363
+ this.change("value", e, cb);
364
+ this.props.trackInteraction();
365
+ };
366
+
367
+ _handleFocus: () => void = () => {
368
+ /* istanbul ignore next */
369
+ this.props.onFocus([]);
370
+ };
371
+
372
+ _handleBlur: () => void = () => {
373
+ /* istanbul ignore next */
374
+ this.props.onBlur([]);
375
+ };
376
+
377
+ focus: () => boolean = () => {
378
+ if (this.props.apiOptions.customKeypad) {
379
+ // eslint-disable-next-line react/no-string-refs
380
+ this.refs.input.focus();
381
+ } else {
382
+ // The buttons are often on top of text you're trying to read, so
383
+ // don't focus the editor automatically.
384
+ }
385
+
386
+ return true;
387
+ };
388
+
389
+ focusInputPath: (inputPath: InputPath) => void = (inputPath: InputPath) => {
390
+ // eslint-disable-next-line react/no-string-refs
391
+ this.refs.input.focus();
392
+ };
393
+
394
+ blurInputPath: (inputPath: InputPath) => void = (inputPath: InputPath) => {
395
+ // eslint-disable-next-line react/no-string-refs
396
+ this.refs.input.blur();
397
+ };
398
+
399
+ // HACK(joel)
400
+ insert: (text: string) => void = (text: string) => {
401
+ if (!this.props.apiOptions.staticRender) {
402
+ // eslint-disable-next-line react/no-string-refs
403
+ this.refs.input.insert(text);
404
+ }
405
+ };
406
+
407
+ getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<any>> = () => {
408
+ // The widget itself is an input, so we return a single empty list to
409
+ // indicate this.
410
+ /* istanbul ignore next */
411
+ return [[]];
412
+ };
413
+
414
+ // TODO(Nicole): I believe this is going away and won't be needed anymore
415
+ getGrammarTypeForPath: (inputPath: string) => string = (
416
+ inputPath: string,
417
+ ) => {
418
+ /* istanbul ignore next */
419
+ return "expression";
420
+ };
421
+
422
+ setInputValue: (path: string, newValue: string, cb: () => void) => void = (
423
+ path: string,
424
+ newValue: string,
425
+ cb: () => void,
426
+ ) => {
427
+ this.props.onChange(
428
+ {
429
+ value: newValue,
430
+ },
431
+ cb,
432
+ );
433
+ };
434
+
435
+ render(): React.Node | React.Element<"div"> {
436
+ if (this.props.apiOptions.customKeypad) {
437
+ return (
438
+ <KeypadInput
439
+ // eslint-disable-next-line react/no-string-refs
440
+ ref="input"
441
+ value={this.props.value}
442
+ keypadElement={this.props.keypadElement}
443
+ onChange={this.changeAndTrack}
444
+ onFocus={() => {
445
+ // this.props.keypadElement should always be set
446
+ // when apiOptions.customKeypad is set, but how
447
+ // to convince Flow of this?
448
+ this.props.keypadElement?.configure(
449
+ this.props.keypadConfiguration,
450
+ () => {
451
+ if (this._isMounted) {
452
+ this._handleFocus();
453
+ }
454
+ },
455
+ );
456
+ }}
457
+ onBlur={this._handleBlur}
458
+ />
459
+ );
460
+ }
461
+ if (this.props.apiOptions.staticRender) {
462
+ // To make things slightly easier, we just use an InputWithExamples
463
+ // component to handle the static rendering, which is the same
464
+ // component used by InputNumber and NumericInput
465
+ return (
466
+ <InputWithExamples
467
+ // eslint-disable-next-line react/no-string-refs
468
+ ref="input"
469
+ value={this.props.value}
470
+ type="tex"
471
+ examples={[]}
472
+ shouldShowExamples={false}
473
+ onChange={this.changeAndTrack}
474
+ onFocus={this._handleFocus}
475
+ onBlur={this._handleBlur}
476
+ id={this.props.widgetId}
477
+ linterContext={this.props.linterContext}
478
+ />
479
+ );
480
+ }
481
+ // TODO(alex): Style this tooltip to be more consistent with other
482
+ // tooltips on the site; align to left middle (once possible)
483
+ const errorTooltip = (
484
+ <span className="error-tooltip" role="tooltip">
485
+ <Tooltip
486
+ className="error-text-container"
487
+ horizontalPosition="right"
488
+ horizontalAlign="left"
489
+ verticalPosition="top"
490
+ arrowSize={10}
491
+ borderColor="#fcc335"
492
+ show={this.state.showErrorText}
493
+ >
494
+ <span
495
+ className="error-icon"
496
+ data-test-id="test-error-icon"
497
+ onMouseEnter={() => {
498
+ this.setState({showErrorText: true});
499
+ }}
500
+ onMouseLeave={() => {
501
+ this.setState({showErrorText: false});
502
+ }}
503
+ onClick={() => {
504
+ // TODO(alex): Better error feedback for mobile
505
+ this.setState({
506
+ showErrorText: !this.state.showErrorText,
507
+ });
508
+ }}
509
+ >
510
+ <InlineIcon {...iconExclamationSign} />
511
+ </span>
512
+ <div className="error-text">{ERROR_MESSAGE}</div>
513
+ </Tooltip>
514
+ </span>
515
+ );
516
+
517
+ const className = classNames({
518
+ "perseus-widget-expression": true,
519
+ "show-error-tooltip": this.state.showErrorTooltip,
520
+ });
521
+
522
+ return (
523
+ <div className={className}>
524
+ <MathInput
525
+ // eslint-disable-next-line react/no-string-refs
526
+ ref="input"
527
+ className={ApiClassNames.INTERACTIVE}
528
+ value={this.props.value}
529
+ onChange={this.changeAndTrack}
530
+ convertDotToTimes={this.props.times}
531
+ buttonsVisible={this.props.buttonsVisible || "focused"}
532
+ buttonSets={this.props.buttonSets}
533
+ onFocus={this._handleFocus}
534
+ onBlur={this._handleBlur}
535
+ />
536
+ {this.state.showErrorTooltip && errorTooltip}
537
+ </div>
538
+ );
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Determine the keypad configuration parameters for the input, based on the
544
+ * provided properties.
545
+ *
546
+ * There are two configuration parameters to be passed to the keypad:
547
+ * (1) The keypad type. For the Expression widget, we always use the
548
+ * Expression keypad.
549
+ * (2) The extra keys; namely, any variables or constants (like Pi) that need
550
+ * to be included as keys on the keypad. These are scraped from the answer
551
+ * forms.
552
+ */
553
+ const keypadConfigurationForProps = (
554
+ widgetOptions: PerseusExpressionWidgetOptions,
555
+ ) => {
556
+ // Always use the Expression keypad, regardless of the button sets that have
557
+ // been enabled.
558
+ const keypadType = KeypadTypes.EXPRESSION;
559
+
560
+ // Extract any and all variables and constants from the answer forms.
561
+ const uniqueExtraVariables = {};
562
+ const uniqueExtraConstants = {};
563
+ for (const answerForm of widgetOptions.answerForms) {
564
+ const maybeExpr = KAS.parse(answerForm.value, widgetOptions);
565
+ if (maybeExpr.parsed) {
566
+ const expr = maybeExpr.expr;
567
+
568
+ // The keypad expects Greek letters to be capitalized (e.g., it
569
+ // requires `PI` instead of `pi`). Right now, it only supports Pi
570
+ // and Theta, so we special-case.
571
+ const isGreek = (symbol) => symbol === "pi" || symbol === "theta";
572
+ const toKey = (symbol) =>
573
+ isGreek(symbol) ? symbol.toUpperCase() : symbol;
574
+
575
+ for (const variable of expr.getVars()) {
576
+ uniqueExtraVariables[toKey(variable)] = true;
577
+ }
578
+ for (const constant of expr.getConsts()) {
579
+ uniqueExtraConstants[toKey(constant)] = true;
580
+ }
581
+ }
582
+ }
583
+
584
+ // TODO(charlie): Alert the keypad as to which of these symbols should be
585
+ // treated as functions.
586
+ const extraVariables = Object.keys(uniqueExtraVariables);
587
+ extraVariables.sort();
588
+
589
+ const extraConstants = Object.keys(uniqueExtraConstants);
590
+ extraConstants.sort();
591
+
592
+ const extraKeys = [...extraVariables, ...extraConstants];
593
+ if (!extraKeys.length) {
594
+ // If there are no extra symbols available, we include Pi anyway, so
595
+ // that the "extra symbols" button doesn't appear empty.
596
+ extraKeys.push("PI");
597
+ }
598
+
599
+ return {keypadType, extraKeys};
600
+ };
601
+
602
+ const propUpgrades = {
603
+ /* istanbul ignore next */
604
+ "1": (v0props: $FlowFixMe): PerseusExpressionWidgetOptions => ({
605
+ times: v0props.times,
606
+ buttonSets: v0props.buttonSets,
607
+ functions: v0props.functions,
608
+ buttonsVisible: v0props.buttonsVisible,
609
+
610
+ answerForms: [
611
+ {
612
+ considered: "correct",
613
+ form: v0props.form,
614
+ simplify: v0props.simplify,
615
+ value: v0props.value,
616
+ },
617
+ ],
618
+ }),
619
+ };
620
+
621
+ type RenderProps = {|
622
+ buttonSets: $FlowFixMe,
623
+ buttonsVisible?: "always" | "focused" | "never",
624
+ functions: $ReadOnlyArray<string>,
625
+ keypadConfiguration: {|
626
+ extraKeys: $ReadOnlyArray<any | string>,
627
+ keypadType: $FlowFixMe,
628
+ |},
629
+ times: boolean,
630
+ |};
631
+
632
+ export default ({
633
+ name: "expression",
634
+ displayName: "Expression / Equation",
635
+ defaultAlignment: "inline-block",
636
+ widget: Expression,
637
+ transform: (widgetOptions: PerseusExpressionWidgetOptions): RenderProps => {
638
+ const {times, functions, buttonSets, buttonsVisible} = widgetOptions;
639
+ return {
640
+ keypadConfiguration: keypadConfigurationForProps(widgetOptions),
641
+ times,
642
+ functions,
643
+ buttonSets,
644
+ buttonsVisible,
645
+ };
646
+ },
647
+ version: {major: 1, minor: 0},
648
+ propUpgrades: propUpgrades,
649
+
650
+ // For use by the editor
651
+ isLintable: true,
652
+ }: WidgetExports<typeof Expression>);