@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,873 @@
1
+ /* eslint-disable @babel/no-invalid-this, react/forbid-prop-types, react/no-unsafe, react/sort-comp */
2
+ // @flow
3
+ import {CircularSpinner} from "@khanacademy/wonder-blocks-progress-spinner";
4
+ import {StyleSheet, css} from "aphrodite";
5
+ import $ from "jquery";
6
+ import PropTypes from "prop-types";
7
+ import * as React from "react";
8
+ import ReactDOM from "react-dom";
9
+ import _ from "underscore";
10
+
11
+ import {getDependencies} from "../dependencies.js";
12
+ import Gorgon from "../gorgon/gorgon.js";
13
+ import {linterContextProps, linterContextDefault} from "../gorgon/proptypes.js";
14
+ import {ClassNames as ApiClassNames} from "../perseus-api.jsx";
15
+ import Renderer from "../renderer.jsx";
16
+ import Util from "../util.js";
17
+
18
+ import type {LinterContextProps} from "../types";
19
+
20
+ const HORIZONTAL = "horizontal";
21
+ const VERTICAL = "vertical";
22
+
23
+ const STATIC = "static";
24
+ const DRAGGING = "dragging";
25
+ const ANIMATING = "animating";
26
+ const DISABLED = "disabled";
27
+
28
+ // Augment the given position with the scroll position of the offset parent.
29
+ const addOffsetParentScroll = ($el, position) => {
30
+ // NOTE(slim): $(el).offsetParent() is NOT equivalent to $el[0].offsetParent.
31
+ // Specifically, when `el` is not positioned, then the native HTMLElement.offsetParent
32
+ // returns the closest table element, which in this case is the <td> rendered by the
33
+ // Perseus Matcher widget.
34
+ // Conversely, jQuery's offsetParent() always returns the closest positioned ancestor,
35
+ // even when the element is not positioned.
36
+ const $offsetParent = $el.offsetParent();
37
+ return {
38
+ top: position.top + $offsetParent.scrollTop(),
39
+ left: position.left + $offsetParent.scrollLeft(),
40
+ };
41
+ };
42
+
43
+ // A placeholder that appears in the sortable whenever an item is dragged.
44
+ class Placeholder extends React.Component<$FlowFixMe> {
45
+ static propTypes = {
46
+ layout: PropTypes.oneOf([HORIZONTAL, VERTICAL]),
47
+ width: PropTypes.number.isRequired,
48
+ height: PropTypes.number.isRequired,
49
+ };
50
+
51
+ render(): React.Node {
52
+ const {layout} = this.props;
53
+ const className = css(
54
+ styles.card,
55
+ styles.placeholder,
56
+ layout === HORIZONTAL && styles.horizontalCard,
57
+ );
58
+ const style: $FlowFixMe = {
59
+ width: this.props.width,
60
+ height: this.props.height,
61
+ };
62
+
63
+ if (this.props.margin != null) {
64
+ style.margin = this.props.margin;
65
+ }
66
+
67
+ return <li className={className} style={style} />;
68
+ }
69
+ }
70
+
71
+ type DraggableProps = $FlowFixMe;
72
+ type DraggableState = $FlowFixMe;
73
+
74
+ // A draggable item in the sortable. Can be in one of four states:
75
+ // Static: The item is not being interacted with.
76
+ // Dragging: The item is being dragged.
77
+ // Animating: The item has been released, and is moving to its destination.
78
+ // Disabled: The item cannot be interacted with.
79
+ //
80
+ // Usual flow: Static -> Dragging -> Animating -> Static
81
+ // [Dis|en]abling: Static|Dragging|Animating -> Disabled -> Static
82
+ class Draggable extends React.Component<DraggableProps, DraggableState> {
83
+ // Handler returned by requestAnimationFrame.
84
+ animationFrameRequest = null;
85
+ isMouseMoveUpBound: boolean;
86
+ _mounted: boolean;
87
+
88
+ static propTypes = {
89
+ content: PropTypes.string.isRequired,
90
+ endPosition: PropTypes.object.isRequired,
91
+ includePadding: PropTypes.bool,
92
+ layout: PropTypes.oneOf([HORIZONTAL, VERTICAL]),
93
+ onAnimationEnd: PropTypes.func.isRequired,
94
+ onMouseDown: PropTypes.func.isRequired,
95
+ onMouseMove: PropTypes.func.isRequired,
96
+ onMouseUp: PropTypes.func.isRequired,
97
+ onRender: PropTypes.func.isRequired,
98
+ type: PropTypes.oneOf([STATIC, DRAGGING, ANIMATING, DISABLED]),
99
+ linterContext: linterContextProps,
100
+ };
101
+
102
+ static defaultProps = {
103
+ includePadding: true,
104
+ type: STATIC,
105
+ linterContext: linterContextDefault,
106
+ };
107
+
108
+ state: DraggableState = {
109
+ startPosition: {left: 0, top: 0},
110
+ startMouse: {left: 0, top: 0},
111
+ mouse: {left: 0, top: 0},
112
+ };
113
+
114
+ componentDidMount() {
115
+ this._mounted = true;
116
+ this.isMouseMoveUpBound = false;
117
+
118
+ // On touch devices, we set up our own touchmove handler because React
119
+ // all event subscriptions using `OnTouchMove` props are non-passive.
120
+ // See: https://github.com/facebook/react/issues/6436
121
+ // Also, we can't subscribe to `ontouchmove` within the `ontouchstart`
122
+ // handler because of a WebKit bug:
123
+ // https://github.com/atlassian/react-beautiful-dnd/issues/413 and
124
+ // https://bugs.webkit.org/show_bug.cgi?id=184250
125
+ document.addEventListener(
126
+ "touchmove",
127
+ this.onMouseMove,
128
+ // Not all browsers support passive events, and when they don't
129
+ // this third paramter is just a boolean. If we pass the "options"
130
+ // object, it's interpreted as `capture=true` (which we don't want!)
131
+ Util.supportsPassiveEvents() ? {passive: false} : false,
132
+ );
133
+ }
134
+
135
+ componentWillUnmount() {
136
+ this._mounted = false;
137
+ // Event handlers should be unbound before component unmounting, but
138
+ // just in case...
139
+ if (this.isMouseMoveUpBound) {
140
+ this.unbindMouseMoveUp();
141
+ }
142
+
143
+ if (this.animationFrameRequest) {
144
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
145
+ // eslint-disable-next-line no-restricted-syntax
146
+ cancelAnimationFrame(this.animationFrameRequest);
147
+ }
148
+
149
+ document.removeEventListener("touchmove", this.onMouseMove);
150
+ }
151
+
152
+ getCurrentPosition = () => {
153
+ return {
154
+ left:
155
+ this.state.startPosition.left +
156
+ this.state.mouse.left -
157
+ this.state.startMouse.left,
158
+ top:
159
+ this.state.startPosition.top +
160
+ this.state.mouse.top -
161
+ this.state.startMouse.top,
162
+ };
163
+ };
164
+
165
+ render(): React.Node {
166
+ const {includePadding, layout, type} = this.props;
167
+
168
+ // We need to keep backwards compatbility with rules specified directly
169
+ // in CSS. Hence the hacky tacking on of manual classNames.
170
+ // See sortable.less for details.
171
+ let className =
172
+ css(
173
+ styles.card,
174
+ styles.draggable,
175
+ layout === HORIZONTAL && styles.horizontalCard,
176
+ layout === VERTICAL && styles.verticalCard,
177
+ type === DRAGGING && styles.dragging,
178
+ type === DISABLED && styles.disabled,
179
+ !includePadding && styles.unpaddedCard,
180
+ ) +
181
+ " " +
182
+ ApiClassNames.INTERACTIVE +
183
+ " perseus-sortable-draggable";
184
+
185
+ if (!includePadding) {
186
+ className += " perseus-sortable-draggable-unpadded";
187
+ }
188
+
189
+ const style: $FlowFixMe = {
190
+ position: "static",
191
+ };
192
+
193
+ if (this.props.type === DRAGGING || this.props.type === ANIMATING) {
194
+ _.extend(style, {position: "absolute"}, this.getCurrentPosition());
195
+ }
196
+
197
+ if (this.props.width) {
198
+ style.width = this.props.width + 1; // Fix for non-integer widths
199
+ }
200
+ if (this.props.height) {
201
+ style.height = this.props.height;
202
+ }
203
+ if (this.props.margin != null) {
204
+ style.margin = this.props.margin;
205
+ }
206
+ return (
207
+ <li
208
+ className={className}
209
+ style={style}
210
+ onMouseDown={this.onMouseDown}
211
+ onTouchStart={this.onMouseDown}
212
+ onTouchMove={this.onMouseMove}
213
+ onTouchEnd={this.onMouseUp}
214
+ onTouchCancel={this.onMouseUp}
215
+ >
216
+ <Renderer
217
+ content={this.props.content}
218
+ linterContext={Gorgon.pushContextStack(
219
+ this.props.linterContext,
220
+ "draggable",
221
+ )}
222
+ onRender={this.props.onRender}
223
+ />
224
+ </li>
225
+ );
226
+ }
227
+
228
+ componentDidUpdate(prevProps) {
229
+ if (this.props.type === prevProps.type) {
230
+ return;
231
+ }
232
+
233
+ if (this.props.type === ANIMATING) {
234
+ // Start animating
235
+ const current = this.getCurrentPosition();
236
+ const duration =
237
+ 15 *
238
+ Math.sqrt(
239
+ Math.sqrt(
240
+ Math.pow(
241
+ this.props.endPosition.left - current.left,
242
+ 2,
243
+ ) +
244
+ Math.pow(
245
+ this.props.endPosition.top - current.top,
246
+ 2,
247
+ ),
248
+ ),
249
+ );
250
+
251
+ $(ReactDOM.findDOMNode(this)).animate(this.props.endPosition, {
252
+ duration: Math.max(duration, 1),
253
+ // Animating -> Static
254
+ complete: this.props.onAnimationEnd,
255
+ });
256
+ } else if (this.props.type === STATIC) {
257
+ // Ensure that any animations are done
258
+ $(ReactDOM.findDOMNode(this)).finish();
259
+ }
260
+ }
261
+
262
+ bindMouseMoveUp = () => {
263
+ this.isMouseMoveUpBound = true;
264
+ $(document).on("mousemove", this.onMouseMove);
265
+ $(document).on("mouseup", this.onMouseUp);
266
+ };
267
+
268
+ unbindMouseMoveUp = () => {
269
+ this.isMouseMoveUpBound = false;
270
+ $(document).off("mousemove", this.onMouseMove);
271
+ $(document).off("mouseup", this.onMouseUp);
272
+ };
273
+
274
+ onMouseDown = (event) => {
275
+ if (this.props.type !== STATIC) {
276
+ return;
277
+ }
278
+
279
+ if (
280
+ !(
281
+ event.button === 0 ||
282
+ (event.touches != null && event.touches.length === 1)
283
+ )
284
+ ) {
285
+ return;
286
+ }
287
+
288
+ event.preventDefault();
289
+ const loc = Util.extractPointerLocation(event);
290
+
291
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
292
+ // eslint-disable-next-line no-restricted-syntax
293
+ this.animationFrameRequest = requestAnimationFrame(() => {
294
+ const $el = $(ReactDOM.findDOMNode(this));
295
+
296
+ // jQuery.position() gets the position of the element wrt its offset parent,
297
+ // but subtracts the scroll position of the parent. We need to add that back.
298
+ const position = $el.position();
299
+ const startPosition = addOffsetParentScroll($el, position);
300
+
301
+ if (loc && this._mounted) {
302
+ this.setState(
303
+ {
304
+ startPosition,
305
+ // NOTE(slim): We don't bother adjusting `loc` because we only keep track of (mouse - startMouse) in
306
+ // this.getCurrentPosition(). Adjusting `loc` here requires us to also change how `loc` is used in
307
+ // onMouseMove and onMouseUp, and this requires reading from the DOM each time to measure the parent's scrollTop.
308
+ startMouse: loc,
309
+ mouse: loc,
310
+ dragging: true,
311
+ },
312
+ function () {
313
+ this.bindMouseMoveUp();
314
+
315
+ // Static -> Dragging
316
+ this.props.onMouseDown();
317
+ },
318
+ );
319
+ }
320
+ });
321
+ };
322
+
323
+ onMouseMove: ($FlowFixMe) => void = (event) => {
324
+ const notDragging =
325
+ this.props.type !== DRAGGING || !this.state.dragging;
326
+
327
+ if (notDragging) {
328
+ return;
329
+ }
330
+
331
+ event.preventDefault();
332
+ const loc = Util.extractPointerLocation(event);
333
+
334
+ if (loc) {
335
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
336
+ // eslint-disable-next-line no-restricted-syntax
337
+ this.animationFrameRequest = requestAnimationFrame(() => {
338
+ this.setState(
339
+ {
340
+ mouse: loc,
341
+ },
342
+ this.props.onMouseMove,
343
+ );
344
+ });
345
+ }
346
+ };
347
+
348
+ onMouseUp: ($FlowFixMe) => void = (event) => {
349
+ const notDragging =
350
+ this.props.type !== DRAGGING || !this.state.dragging;
351
+
352
+ if (notDragging) {
353
+ return;
354
+ }
355
+
356
+ event.preventDefault();
357
+ const loc = Util.extractPointerLocation(event);
358
+ if (loc) {
359
+ this.setState({dragging: false});
360
+ this.unbindMouseMoveUp();
361
+
362
+ // Dragging -> Animating
363
+ this.props.onMouseUp();
364
+ }
365
+ };
366
+ }
367
+ type SortableLayoutKind = "horizontal" | "vertical";
368
+ export type SortableOption = string;
369
+
370
+ type SortableProps = {|
371
+ constraints:
372
+ | {|
373
+ width: number,
374
+ height: number,
375
+ |}
376
+ | {...},
377
+ disabled: boolean,
378
+ layout: SortableLayoutKind,
379
+ margin: number,
380
+ onChange: ($FlowFixMe) => void,
381
+ onMeasure: ({|
382
+ widths: $ReadOnlyArray<number>,
383
+ heights: $ReadOnlyArray<number>,
384
+ |}) => void,
385
+ padding: boolean,
386
+ linterContext: LinterContextProps,
387
+ waitForKatexLoad: boolean,
388
+ options: $ReadOnlyArray<SortableOption>,
389
+ |};
390
+
391
+ type DefaultProps = {|
392
+ constraints: SortableProps["constraints"],
393
+ disabled: SortableProps["disabled"],
394
+ layout: SortableProps["layout"],
395
+ margin: SortableProps["margin"],
396
+ onChange: SortableProps["onChange"],
397
+ onMeasure: SortableProps["onMeasure"],
398
+ padding: SortableProps["padding"],
399
+ linterContext: SortableProps["linterContext"],
400
+ waitForKatexLoad: SortableProps["waitForKatexLoad"],
401
+ |};
402
+
403
+ type ItemState = "disabled" | "static" | "dragging" | "animating";
404
+ type SortableItem = {|
405
+ option: SortableOption,
406
+ key: number,
407
+ type: ItemState,
408
+ endPosition: $FlowFixMe,
409
+ width: number,
410
+ height: number,
411
+ |};
412
+
413
+ type SortableState = {|
414
+ items: $ReadOnlyArray<SortableItem>,
415
+ katex: null | any,
416
+ |};
417
+ class Sortable extends React.Component<SortableProps, SortableState> {
418
+ static defaultProps: DefaultProps = {
419
+ layout: HORIZONTAL,
420
+ padding: true,
421
+ disabled: false,
422
+ constraints: {},
423
+ onMeasure: function () {},
424
+ margin: 5,
425
+ onChange: function () {},
426
+ linterContext: linterContextDefault,
427
+ waitForKatexLoad: true,
428
+ };
429
+
430
+ constructor(props: SortableProps) {
431
+ super(props);
432
+ // Don't call this.setState() here!
433
+ this.state = {
434
+ items: Sortable.itemsFromProps(this.props),
435
+ katex: null,
436
+ };
437
+ }
438
+
439
+ UNSAFE_componentWillReceiveProps(nextProps: SortableProps) {
440
+ const prevProps = this.props;
441
+
442
+ if (!_.isEqual(nextProps.options, prevProps.options)) {
443
+ // Regenerate items
444
+ this.setState({
445
+ items: Sortable.itemsFromProps(nextProps),
446
+ });
447
+ } else if (
448
+ nextProps.layout !== prevProps.layout ||
449
+ nextProps.padding !== prevProps.padding ||
450
+ nextProps.disabled !== prevProps.disabled ||
451
+ !_.isEqual(nextProps.constraints, prevProps.constraints)
452
+ ) {
453
+ // Clear item measurements
454
+ this.setState({
455
+ items: Sortable.clearItemMeasurements(this.state.items),
456
+ });
457
+ }
458
+ }
459
+
460
+ componentDidUpdate() {
461
+ // Measure items if their dimensions have been reset
462
+ if (
463
+ this.state.items.length &&
464
+ !this.state.items[0].width &&
465
+ !this.state.items[0].height
466
+ ) {
467
+ // Measure on the next frame to allow items size to settle.
468
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
469
+ // eslint-disable-next-line no-restricted-syntax
470
+ setTimeout(() => {
471
+ this.measureItems();
472
+ }, 0);
473
+ }
474
+ }
475
+
476
+ componentDidMount() {
477
+ getDependencies()
478
+ .getKaTeX()
479
+ .then((katex) => {
480
+ this.setState({katex});
481
+ });
482
+ }
483
+
484
+ static itemsFromProps(props: {
485
+ disabled: boolean,
486
+ options: $ReadOnlyArray<SortableOption>,
487
+ ...
488
+ }): $ReadOnlyArray<SortableItem> {
489
+ const type: ItemState = props.disabled ? DISABLED : STATIC;
490
+ return props.options.map((option: SortableOption, i) => {
491
+ return {
492
+ option: option,
493
+ key: i,
494
+ type: type,
495
+ endPosition: {},
496
+ width: 0,
497
+ height: 0,
498
+ };
499
+ });
500
+ }
501
+
502
+ static clearItemMeasurements(
503
+ items: $ReadOnlyArray<SortableItem>,
504
+ ): $ReadOnlyArray<SortableItem> {
505
+ return items.map((item) => {
506
+ return {
507
+ ...item,
508
+ width: 0,
509
+ height: 0,
510
+ };
511
+ });
512
+ }
513
+
514
+ measureItems() {
515
+ // Measure all items and cache what their dimensions should be, taking
516
+ // into account constraints and the current layout. This allows syncing
517
+ // widths and heights for pretty rows/columns. Note that dimensions are
518
+ // explictly set on Draggables - this prevents them from changing size
519
+ // or shape while being dragged.
520
+
521
+ let items: SortableItem = _.clone(this.state.items);
522
+
523
+ // Fetches a jQuery list of elements for each item
524
+ const $items = _.map(
525
+ items,
526
+ function (item) {
527
+ // eslint-disable-next-line react/no-string-refs
528
+ return $(ReactDOM.findDOMNode(this.refs[item.key]));
529
+ },
530
+ this,
531
+ );
532
+
533
+ const widths: $ReadOnlyArray<number> = _.invoke($items, "outerWidth");
534
+ const heights: $ReadOnlyArray<number> = _.invoke($items, "outerHeight");
535
+
536
+ const {constraints, layout} = this.props;
537
+
538
+ let syncWidth: number | null = null;
539
+ if (constraints?.width) {
540
+ // Items must be at least as wide as the specified constraint
541
+ syncWidth = _.max(widths.concat(constraints.width));
542
+ } else if (layout === VERTICAL) {
543
+ // Sync widths to get a clean column
544
+ syncWidth = _.max(widths);
545
+ }
546
+
547
+ let syncHeight: number | null = null;
548
+ if (constraints?.height) {
549
+ // Items must be at least as high as the specified constraint
550
+ syncHeight = _.max(heights.concat(constraints.height));
551
+ } else if (layout === HORIZONTAL) {
552
+ // Sync widths to get a clean row
553
+ syncHeight = _.max(heights);
554
+ }
555
+
556
+ items = _.map(items, function (item, i) {
557
+ item.width = syncWidth || widths[i];
558
+ item.height = syncHeight || heights[i];
559
+ return item;
560
+ });
561
+
562
+ this.setState({items: items}, () => {
563
+ this.props.onMeasure &&
564
+ this.props.onMeasure({widths: widths, heights: heights});
565
+ });
566
+ }
567
+
568
+ // $FlowFixMe[signature-verification-failure]
569
+ remeasureItems = _.debounce(() => {
570
+ this.setState({
571
+ // Clear item measurements
572
+ items: Sortable.clearItemMeasurements(this.state.items),
573
+ });
574
+ }, 20);
575
+
576
+ render(): React.Node {
577
+ // We don't render the sortable until KaTeX has fully loaded, in case
578
+ // the draggables are rendering KaTeX content. This is un-optimal as
579
+ // we end up loading KaTeX even when we may not need it, however it
580
+ // helps to ensure that the dimensions of the draggables (and thus the
581
+ // sortable) will be correct when they render, if their contents are
582
+ // KaTeX-derived.
583
+ if (this.props.waitForKatexLoad && !this.state.katex) {
584
+ return <CircularSpinner />;
585
+ }
586
+ const cards = [];
587
+
588
+ const {layout} = this.props;
589
+ // We need to keep backwards compatbility with rules specified directly
590
+ // in CSS. See sortable.less for details.
591
+ const className = css(styles.sortable) + " perseus-sortable";
592
+
593
+ const syncWidth = this.props.constraints?.width || layout === VERTICAL;
594
+ const syncHeight =
595
+ this.props.constraints?.height || layout === HORIZONTAL;
596
+
597
+ _.each(
598
+ this.state.items,
599
+ function (item, i, items) {
600
+ const isLast = i === items.length - 1;
601
+ const isStatic = item.type === STATIC || item.type === DISABLED;
602
+ let margin;
603
+
604
+ if (this.props.layout === HORIZONTAL) {
605
+ margin = "0 " + this.props.margin + "px 0 0"; // right
606
+ } else if (this.props.layout === VERTICAL) {
607
+ margin = "0 0 " + this.props.margin + "px 0"; // bottom
608
+ }
609
+
610
+ cards.push(
611
+ <Draggable
612
+ content={item.option}
613
+ key={item.key}
614
+ type={item.type}
615
+ ref={item.key}
616
+ width={syncWidth ? item.width : undefined}
617
+ height={syncHeight ? item.height : undefined}
618
+ layout={layout}
619
+ includePadding={this.props.padding}
620
+ margin={isLast && isStatic ? 0 : margin}
621
+ endPosition={item.endPosition}
622
+ linterContext={Gorgon.pushContextStack(
623
+ this.props.linterContext,
624
+ "sortable",
625
+ )}
626
+ onRender={this.remeasureItems}
627
+ // eslint-disable-next-line react/jsx-no-bind
628
+ onMouseDown={this.onMouseDown.bind(this, item.key)}
629
+ // eslint-disable-next-line react/jsx-no-bind
630
+ onMouseMove={this.onMouseMove.bind(this, item.key)}
631
+ // eslint-disable-next-line react/jsx-no-bind
632
+ onMouseUp={this.onMouseUp.bind(this, item.key)}
633
+ // eslint-disable-next-line react/jsx-no-bind
634
+ onTouchMove={this.onMouseMove.bind(this, item.key)}
635
+ // eslint-disable-next-line react/jsx-no-bind
636
+ onTouchEnd={this.onMouseUp.bind(this, item.key)}
637
+ // eslint-disable-next-line react/jsx-no-bind
638
+ onTouchCancel={this.onMouseUp.bind(this, item.key)}
639
+ // eslint-disable-next-line react/jsx-no-bind
640
+ onAnimationEnd={this.onAnimationEnd.bind(
641
+ this,
642
+ item.key,
643
+ )}
644
+ />,
645
+ );
646
+
647
+ if (item.type === DRAGGING || item.type === ANIMATING) {
648
+ cards.push(
649
+ <Placeholder
650
+ key={"placeholder_" + item.key}
651
+ ref={"placeholder_" + item.key}
652
+ width={item.width}
653
+ height={item.height}
654
+ layout={layout}
655
+ margin={isLast ? 0 : margin}
656
+ />,
657
+ );
658
+ }
659
+ },
660
+ this,
661
+ );
662
+
663
+ return <ul className={className}>{cards}</ul>;
664
+ }
665
+
666
+ onMouseDown(key: SortableItem["key"]) {
667
+ // Static -> Dragging
668
+ const items = _.map(this.state.items, function (item) {
669
+ if (item.key === key) {
670
+ item.type = DRAGGING;
671
+ }
672
+ return item;
673
+ });
674
+
675
+ this.setState({items: items});
676
+ }
677
+
678
+ // This is public API and it's ok for components that use
679
+ // this sortable to call this.
680
+ // moveOptionToIndex takes an option and a desired index and
681
+ // will move that item to the desired index. This is useful
682
+ // for testing and other cases where dragging and dropping may
683
+ // not be available
684
+ moveOptionToIndex(option: SortableOption, index: number) {
685
+ const {items} = this.state;
686
+
687
+ if (index < 0 || index > items.length) {
688
+ throw new Error(`index ${index} out of bounds`);
689
+ }
690
+
691
+ const nextItems = _.clone(items);
692
+
693
+ const item = items.filter((item: SortableItem) => {
694
+ return item.option === option;
695
+ })[0];
696
+
697
+ if (item == null) {
698
+ throw new Error(`option ${option} not found`);
699
+ }
700
+
701
+ const currentIndex = items.findIndex((i: SortableItem) => {
702
+ return i.key === item.key;
703
+ });
704
+
705
+ nextItems.splice(currentIndex, 1);
706
+ nextItems.splice(index, 0, item);
707
+
708
+ this.setState({items: nextItems});
709
+ this.props.onChange && this.props.onChange({});
710
+ }
711
+
712
+ onMouseMove(key: SortableItem["key"]) {
713
+ // Dragging: Rearrange items based on draggable's position
714
+ // eslint-disable-next-line react/no-string-refs
715
+ const $draggable = $(ReactDOM.findDOMNode(this.refs[key]));
716
+ const $sortable = $(ReactDOM.findDOMNode(this));
717
+ const items = _.clone(this.state.items);
718
+ const item = _.findWhere(this.state.items, {key: key});
719
+ const margin = this.props.margin || 0;
720
+ const currentIndex = _.indexOf(items, item);
721
+ let newIndex = 0;
722
+
723
+ items.splice(currentIndex, 1);
724
+
725
+ if (this.props.layout === HORIZONTAL) {
726
+ const midWidth = $draggable.offset().left - $sortable.offset().left;
727
+ let sumWidth = 0;
728
+ let cardWidth;
729
+
730
+ _.each(items, function (item) {
731
+ cardWidth = item.width;
732
+ if (midWidth > sumWidth + cardWidth / 2) {
733
+ newIndex += 1;
734
+ }
735
+ sumWidth += cardWidth + margin;
736
+ });
737
+ } else {
738
+ const midHeight = $draggable.offset().top - $sortable.offset().top;
739
+ let sumHeight = 0;
740
+ let cardHeight;
741
+
742
+ _.each(items, function (item) {
743
+ cardHeight = item.height;
744
+ if (midHeight > sumHeight + cardHeight / 2) {
745
+ newIndex += 1;
746
+ }
747
+ sumHeight += cardHeight + margin;
748
+ });
749
+ }
750
+
751
+ if (newIndex !== currentIndex) {
752
+ items.splice(newIndex, 0, item);
753
+ this.setState({items: items});
754
+ }
755
+ }
756
+
757
+ onMouseUp(key: SortableItem["key"]) {
758
+ // Dragging -> Animating
759
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
760
+ // eslint-disable-next-line no-restricted-syntax
761
+ const nextAnimationFrame = requestAnimationFrame(() => {
762
+ const items = _.map(
763
+ this.state.items,
764
+ function (item) {
765
+ if (item.key === key) {
766
+ item.type = ANIMATING;
767
+ const $placeholder = $(
768
+ ReactDOM.findDOMNode(
769
+ // eslint-disable-next-line react/no-string-refs
770
+ this.refs["placeholder_" + key],
771
+ ),
772
+ );
773
+ const position = $placeholder.position();
774
+ const endPosition = addOffsetParentScroll(
775
+ $placeholder,
776
+ position,
777
+ );
778
+ item.endPosition = endPosition;
779
+ }
780
+ return item;
781
+ },
782
+ this,
783
+ );
784
+
785
+ this.setState({items: items});
786
+ // HACK: We need to know *that* the widget changed, but currently it's
787
+ // not set up in a nice way to tell us *how* it changed, since the
788
+ // permutation of the items is stored in state.
789
+ this.props.onChange && this.props.onChange({});
790
+ });
791
+
792
+ // $FlowFixMe[prop-missing]
793
+ this.animationFrameRequest = nextAnimationFrame;
794
+ }
795
+
796
+ onAnimationEnd(key: SortableItem["key"]) {
797
+ // Animating -> Static
798
+ const items = _.map(this.state.items, function (item) {
799
+ if (item.key === key) {
800
+ item.type = STATIC;
801
+ }
802
+ return item;
803
+ });
804
+
805
+ this.setState({items: items});
806
+ }
807
+
808
+ getOptions(): $ReadOnlyArray<SortableOption> {
809
+ return _.pluck(this.state.items, "option");
810
+ }
811
+ }
812
+
813
+ const styles = StyleSheet.create({
814
+ sortable: {
815
+ boxSizing: "border-box",
816
+ float: "left",
817
+
818
+ padding: 0,
819
+ margin: 0,
820
+ },
821
+
822
+ card: {
823
+ boxSizing: "border-box",
824
+ background: "#fff",
825
+ border: "1px solid #ddd",
826
+ borderRadius: 4,
827
+ cursor: "pointer",
828
+ minWidth: 25,
829
+ minHeight: 44,
830
+ padding: 10,
831
+
832
+ listStyleType: "none",
833
+
834
+ userSelect: "none",
835
+ touchAction: "none",
836
+ },
837
+
838
+ placeholder: {
839
+ background: "#ddd",
840
+ border: "1px solid #ccc",
841
+ },
842
+
843
+ draggable: {
844
+ textAlign: "center",
845
+ },
846
+
847
+ horizontalCard: {
848
+ float: "left",
849
+ cursor: "ew-resize",
850
+ },
851
+
852
+ verticalCard: {
853
+ maxWidth: "100%",
854
+ cursor: "ns-resize",
855
+ },
856
+
857
+ unpaddedCard: {
858
+ padding: 0,
859
+ },
860
+
861
+ dragging: {
862
+ background: "#ffedcd",
863
+ opacity: 0.8,
864
+ },
865
+
866
+ disabled: {
867
+ backgroundColor: "inherit",
868
+ border: "1px solid transparent",
869
+ cursor: "default",
870
+ },
871
+ });
872
+
873
+ export default Sortable;