@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,353 @@
1
+ // @flow
2
+ import $ from "jquery";
3
+ import PropTypes from "prop-types";
4
+ import * as React from "react";
5
+ import ReactDOM from "react-dom";
6
+ import _ from "underscore";
7
+
8
+ import InteractiveUtil from "../interactive2/interactive-util.js";
9
+ import {Errors, Log} from "../logging/log.js";
10
+ import Util from "../util.js";
11
+ import GraphUtils from "../util/graph-utils.js";
12
+
13
+ import GraphieClasses from "./graphie-classes.jsx";
14
+ import Movables from "./graphie-movables.jsx";
15
+
16
+ const GraphieMovable = GraphieClasses.GraphieMovable;
17
+
18
+ const createGraphie = GraphUtils.createGraphie;
19
+ const {deepEq, nestedMap} = Util;
20
+ const {assert} = InteractiveUtil;
21
+
22
+ type Props = $FlowFixMe;
23
+
24
+ class Graphie extends React.Component<Props> {
25
+ static propTypes = {
26
+ addMouseLayer: PropTypes.bool,
27
+ box: PropTypes.arrayOf(PropTypes.number).isRequired,
28
+ children: PropTypes.node,
29
+ isMobile: PropTypes.bool,
30
+ onClick: PropTypes.func,
31
+ onMouseDown: PropTypes.func,
32
+ onMouseMove: PropTypes.func,
33
+ onMouseUp: PropTypes.func,
34
+ options: PropTypes.shape({
35
+ snapStep: PropTypes.arrayOf(PropTypes.number),
36
+ }),
37
+ range: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
38
+ responsive: PropTypes.bool,
39
+ setDrawingAreaAvailable: PropTypes.func,
40
+ setup: PropTypes.func.isRequired,
41
+ };
42
+
43
+ static defaultProps: $FlowFixMe = {
44
+ range: [
45
+ [-10, 10],
46
+ [-10, 10],
47
+ ],
48
+ options: {},
49
+ responsive: false,
50
+ addMouseLayer: true,
51
+ };
52
+
53
+ componentDidMount() {
54
+ this._setupGraphie();
55
+ this._updateMovables();
56
+ }
57
+
58
+ shouldComponentUpdate(nextProps: Props): boolean {
59
+ // NOTE(jeresig): Prior to the React 16 upgrade this was using deepEq
60
+ // However it started to throw errors. I switched to use Underscore's
61
+ // isEqual method instead, hopefully having a similar effect.
62
+ return !_.isEqual(this.props, nextProps);
63
+ }
64
+
65
+ componentDidUpdate(prevProps: Props) {
66
+ // If someone changes the setup function passed in, we should
67
+ // technically setup graphie again. But that's definitely an
68
+ // anti-pattern, since it is most-likely caused by passing in an
69
+ // anonymous function rather than a "real" change, and re-rendering
70
+ // in that case would cause us to constantly re-setup graphie, which
71
+ // would have horrible performance implications. In order to avoid
72
+ // those, we just warn here.
73
+ if (this.props.setup !== prevProps.setup) {
74
+ Log.error(
75
+ "<Graphie> was given a new setup function. " +
76
+ "This is a bad idea; please refactor your code to give " +
77
+ "the same setup function reference to <Graphie> on " +
78
+ "every render.",
79
+ Errors.Internal,
80
+ );
81
+ }
82
+ if (
83
+ !deepEq(this.props.options, prevProps.options) ||
84
+ !deepEq(this.props.box, prevProps.box) ||
85
+ !deepEq(this.props.range, prevProps.range)
86
+ ) {
87
+ this._setupGraphie();
88
+ }
89
+ this._updateMovables();
90
+ }
91
+
92
+ /**
93
+ * Allow parents of the <Graphie> component to grab a reference to the
94
+ * underlying graphie object using
95
+ * `this.refs.graphieComponent.getGraphie()`
96
+ *
97
+ * This shouldn't be necessary for 90% of cases, but the power is there.
98
+ * Use it for good and not evil.
99
+ */
100
+ getGraphie: () => $FlowFixMe = () => {
101
+ // $FlowFixMe[prop-missing]
102
+ return this._graphie;
103
+ };
104
+
105
+ // bounds-checked range
106
+ _range: () => $ReadOnlyArray<[number, number]> = () => {
107
+ return _.map(this.props.range, (dimRange) => {
108
+ if (dimRange[0] >= dimRange[1]) {
109
+ return [-10, 10];
110
+ }
111
+ return dimRange;
112
+ });
113
+ };
114
+
115
+ _box: () => $ReadOnlyArray<number> = () => {
116
+ return _.map(this.props.box, (pixelDim) => {
117
+ // 340 = default size in the editor. exact value
118
+ // is arbitrary; this is just a safety check.
119
+ return pixelDim > 0 ? pixelDim : 340;
120
+ });
121
+ };
122
+
123
+ _scale: () => $ReadOnlyArray<number> = () => {
124
+ const box = this._box();
125
+ const range = this._range();
126
+ return _.map(box, (pixelDim, i) => {
127
+ const unitDim = range[i][1] - range[i][0];
128
+ return pixelDim / unitDim;
129
+ });
130
+ };
131
+
132
+ _setupGraphie: () => void = () => {
133
+ this._removeMovables();
134
+
135
+ // eslint-disable-next-line react/no-string-refs
136
+ const graphieDiv = ReactDOM.findDOMNode(this.refs.graphieDiv);
137
+ $(graphieDiv).empty();
138
+ // $FlowFixMe[prop-missing]
139
+ const graphie = (this._graphie = createGraphie(graphieDiv));
140
+
141
+ // This has to be called before addMouseLayer. You can re-init
142
+ // with graphInit later if you prefer
143
+ graphie.init({
144
+ range: this._range(),
145
+ scale: this._scale(),
146
+ isMobile: this.props.isMobile,
147
+ });
148
+ // Only add the mouselayer if we actually want one.
149
+ if (this.props.addMouseLayer) {
150
+ graphie.addMouseLayer({
151
+ onClick: this.props.onClick,
152
+ onMouseDown: this.props.onMouseDown,
153
+ onMouseUp: this.props.onMouseUp,
154
+ onMouseMove: this.props.onMouseMove,
155
+ setDrawingAreaAvailable: this.props.setDrawingAreaAvailable,
156
+ });
157
+ }
158
+
159
+ graphie.snap = this.props.options.snapStep || [1, 1];
160
+
161
+ if (this.props.responsive) {
162
+ // Overwrite fixed styles set in init()
163
+ // TODO(alex): Either make this component always responsive by
164
+ // itself, or always wrap it in other components so that it is.
165
+ $(graphieDiv).css({width: "100%", height: "100%"});
166
+ graphie.raphael.setSize("100%", "100%");
167
+ }
168
+
169
+ this.props.setup(
170
+ graphie,
171
+ _.extend(
172
+ {
173
+ range: this._range(),
174
+ scale: this._scale(),
175
+ },
176
+ this.props.options,
177
+ ),
178
+ );
179
+ };
180
+
181
+ _removeMovables: () => void = () => {
182
+ // _.invoke works even when this._movables is undefined
183
+ // $FlowFixMe[prop-missing]
184
+ _.invoke(this._movables, "remove");
185
+ // $FlowFixMe[prop-missing]
186
+ this._movables = {};
187
+ };
188
+
189
+ _renderMovables: (
190
+ children: $ReadOnlyArray<$FlowFixMe>,
191
+ $FlowFixMe,
192
+ ) => React.Node = (children, options) => {
193
+ // Each leaf of `children` is a movable descriptor created by a call to
194
+ // some `GraphieMovable`, such as `MovablePoint`.
195
+ //
196
+ // This function takes these descriptors and renders them into
197
+ // on-screen movables, or updates on-screen movables for
198
+ // descriptors when possible.
199
+ //
200
+ // If there is no movable with that key already, this descriptor is
201
+ // stored in this._movables and promoted to an on-screen movable by
202
+ // calling `child.add(graphie)`.
203
+ //
204
+ // If a movable of the same type with the same key exists already,
205
+ // we take `child.props` and give them to the already-existing
206
+ // on-screen movable, and call `movable.modify()`
207
+
208
+ const graphie = options.graphie;
209
+ const oldMovables = options.oldMovables;
210
+ const newMovables = options.newMovables; /* output parameter */
211
+
212
+ const renderChildren = (elem) => {
213
+ _.each(elem.movableProps, (prop) => {
214
+ // Render the children, and save the results of that
215
+ // render to the appropriate props
216
+ elem.props[prop] = this._renderMovables(
217
+ elem.props[prop],
218
+ options,
219
+ );
220
+ });
221
+ };
222
+
223
+ // Add/modify movables
224
+
225
+ // We want to keep track of whether we have added a new svg element,
226
+ // because if we have, then we need to call .toFront() on any svg
227
+ // elements occurring afterwards. If this happens, we set
228
+ // `areMovablesOutOfOrder` to true:
229
+ let areMovablesOutOfOrder = false;
230
+ return nestedMap(children, (childDescriptor) => {
231
+ if (!childDescriptor) {
232
+ // Still increment the key to avoid cascading key changes
233
+ // on hiding/unhiding children, i.e. by using
234
+ // {someBoolean && <MovablePoint />}
235
+ options.nextKey++;
236
+ // preserve the null/undefined in the resulting array
237
+ return childDescriptor;
238
+ }
239
+
240
+ // Instantiate the descriptor to turn it into a real Movable
241
+ const child = new childDescriptor.type(childDescriptor.props);
242
+ assert(
243
+ child instanceof GraphieMovable,
244
+ "All children of a Graphie component must be Graphie " +
245
+ "movables",
246
+ );
247
+
248
+ // Give each child a key
249
+ const keyProp = childDescriptor.key;
250
+ const key = keyProp == null ? "_no_id_" + options.nextKey : keyProp;
251
+ options.nextKey++;
252
+ const ref = childDescriptor.ref;
253
+
254
+ // We render our children first. This allows us to replace any
255
+ // `movableProps` on our child with the on-screen movables
256
+ // corresponding with those descriptors.
257
+ renderChildren(child);
258
+
259
+ const prevMovable = oldMovables[key];
260
+ if (!prevMovable) {
261
+ // We're creating a new child
262
+ child.add(graphie);
263
+ areMovablesOutOfOrder = true;
264
+
265
+ newMovables[key] = child;
266
+ } else if (child.constructor === prevMovable.constructor) {
267
+ // We're updating an old child
268
+ prevMovable.props = child.props;
269
+ const modifyResult = prevMovable.modify(graphie);
270
+ if (modifyResult === "reordered") {
271
+ areMovablesOutOfOrder = true;
272
+ }
273
+
274
+ newMovables[key] = prevMovable;
275
+ } else {
276
+ // We're destroying an old child and replacing it
277
+ // with a new child of a different type
278
+
279
+ // This generally is a bad idea, so warn about it if this
280
+ // is being caused by implicit keys
281
+ if (keyProp == null) {
282
+ Log.error(
283
+ "Replacing a <Graphie> child with a " +
284
+ "child of a different type. Please add keys " +
285
+ "to your <Graphie> children",
286
+ Errors.Internal,
287
+ );
288
+ }
289
+
290
+ prevMovable.remove();
291
+ child.add(graphie);
292
+ areMovablesOutOfOrder = true;
293
+
294
+ newMovables[key] = child;
295
+ }
296
+
297
+ if (areMovablesOutOfOrder) {
298
+ newMovables[key].toFront();
299
+ }
300
+
301
+ if (ref) {
302
+ // $FlowFixMe[prop-missing]
303
+ this.movables[ref] = newMovables[key];
304
+ }
305
+
306
+ return newMovables[key];
307
+ });
308
+ };
309
+
310
+ // Sort of like react diffing, but for movables
311
+ _updateMovables: () => void = () => {
312
+ // $FlowFixMe[prop-missing]
313
+ const graphie = this._graphie;
314
+
315
+ // $FlowFixMe[prop-missing]
316
+ const oldMovables = this._movables;
317
+ const newMovables = {};
318
+ // $FlowFixMe[prop-missing]
319
+ this._movables = newMovables;
320
+ // $FlowFixMe[prop-missing]
321
+ this.movables = {};
322
+
323
+ this._renderMovables(this.props.children, {
324
+ nextKey: 1,
325
+ graphie: graphie,
326
+ oldMovables: oldMovables,
327
+ newMovables: newMovables,
328
+ });
329
+
330
+ // Remove any movables that no longer exist in the child array
331
+ _.each(oldMovables, (oldMovable, key) => {
332
+ if (!newMovables[key]) {
333
+ oldMovable.remove();
334
+ }
335
+ });
336
+ };
337
+
338
+ render(): React.Node {
339
+ return (
340
+ <div className="graphie-container">
341
+ {/* eslint-disable-next-line react/no-string-refs */}
342
+ <div className="graphie" ref="graphieDiv" />
343
+ </div>
344
+ );
345
+ }
346
+ }
347
+
348
+ // Attach Graphie.createClass and Graphie.createSimpleClass
349
+ _.extend(Graphie, GraphieClasses);
350
+ // Attach the Movable react components for easy reference
351
+ _.extend(Graphie, Movables);
352
+
353
+ export default Graphie;
@@ -0,0 +1,245 @@
1
+ // @flow
2
+ /**
3
+ * The entry point for highlighting!
4
+ *
5
+ * Given a content node and a set of serialized highlights, this component
6
+ * renders the content with the given highlights applied. The content's DOM is
7
+ * not modified by this process, so any content should be safe to pass to this
8
+ * component.
9
+ *
10
+ * This component also renders controls for the user to add/remove/update
11
+ * highlights. When the user does so, this component will update the full set
12
+ * of highlights, serialize them, and call `onSerializedHighlightsUpdate` with
13
+ * new set of serialized highlights, which you can then persist and send back
14
+ * down to this component. Easy peasy!
15
+ */
16
+ import {StyleSheet, css} from "aphrodite";
17
+ import * as React from "react";
18
+
19
+ import {
20
+ addHighlight,
21
+ buildHighlight,
22
+ deserializeHighlight,
23
+ serializeHighlight,
24
+ } from "./highlights.js";
25
+ import HighlightingUI from "./ui/highlighting-ui.jsx";
26
+ import WordIndexer from "./word-indexer.jsx";
27
+
28
+ import type {
29
+ DOMHighlight,
30
+ DOMHighlightSet,
31
+ SerializedHighlightSet,
32
+ DOMRange,
33
+ } from "./types.js";
34
+
35
+ type HighlightableContentProps = {
36
+ // The highlightable content itself. Highlights will be defined relative to
37
+ // the content specified here.
38
+ children?: React.Element<any>,
39
+ // Whether the highlights are user-editable. If false, highlights are
40
+ // read-only.
41
+ editable: boolean,
42
+ // Whether highlighting is currently enabled. If false, highlights are not
43
+ // visible and are read-only (regardless of the `editable` prop), and this
44
+ // component is effectively a no-op.
45
+ //
46
+ // NOTE(mdr): The purpose of the `enabled` prop, as opposed to, say, just
47
+ // *not* wrapping the content, is to enable quick toggles between
48
+ // enabled/disabled while maintaining the same component structure.
49
+ // Forcing React to rebuild the entire component tree is not kind!
50
+ enabled: boolean,
51
+ // When the user attempts to add/remove/update a highlight, this callback
52
+ // will be called with a newly-updated full set of highlights that reflects
53
+ // the user's intent.
54
+ onSerializedHighlightsUpdate: (
55
+ serializedHighlights: SerializedHighlightSet,
56
+ ) => mixed,
57
+ // The set of highlights to apply to the given content.
58
+ serializedHighlights: SerializedHighlightSet,
59
+ ...
60
+ };
61
+
62
+ type HighlightableContentState = {
63
+ // A cached list of DOMRanges, each representing one of the content's
64
+ // semantic words. Sorted in document order.
65
+ wordRanges: $ReadOnlyArray<DOMRange>,
66
+ ...
67
+ };
68
+
69
+ class HighlightableContent extends React.PureComponent<
70
+ HighlightableContentProps,
71
+ HighlightableContentState,
72
+ > {
73
+ // References to the mounted container and content divs.
74
+ _container: ?HTMLElement;
75
+ _content: ?HTMLElement;
76
+
77
+ state: HighlightableContentState = {
78
+ wordRanges: [],
79
+ };
80
+
81
+ _buildHighlight(highlightRange: DOMRange): ?DOMHighlight {
82
+ // TODO(mdr): If _buildHighlight starts getting called more often, we
83
+ // might want to cache the DOMHighlightSet instead of recomputing
84
+ // it here.
85
+ return buildHighlight(
86
+ this._getDOMHighlights(),
87
+ this.state.wordRanges,
88
+ highlightRange,
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Take the highlights from props, and deserialize them into DOMHighlights,
94
+ * according to the latest cache of word ranges.
95
+ */
96
+ _getDOMHighlights(): DOMHighlightSet {
97
+ const {serializedHighlights} = this.props;
98
+ const {wordRanges} = this.state;
99
+
100
+ const domHighlights = {};
101
+ for (const key of Object.keys(serializedHighlights)) {
102
+ domHighlights[key] = deserializeHighlight(
103
+ serializedHighlights[key],
104
+ wordRanges,
105
+ );
106
+ }
107
+ return domHighlights;
108
+ }
109
+
110
+ /**
111
+ * Add the given DOMHighlight to the current set.
112
+ */
113
+ _handleAddHighlight: (highlight: DOMHighlight) => void = (
114
+ highlight: DOMHighlight,
115
+ ) => {
116
+ const newDomHighlights = addHighlight(
117
+ this._getDOMHighlights(),
118
+ highlight,
119
+ );
120
+
121
+ const newSerializedHighlights = {};
122
+ for (const key of Object.keys(newDomHighlights)) {
123
+ newSerializedHighlights[key] = serializeHighlight(
124
+ newDomHighlights[key],
125
+ );
126
+ }
127
+
128
+ this.props.onSerializedHighlightsUpdate(newSerializedHighlights);
129
+ };
130
+
131
+ /**
132
+ * Remove the given highlight from the list, and call our callback with the
133
+ * new set of highlights.
134
+ */
135
+ _handleRemoveHighlight: (keyToRemove: string) => void = (
136
+ keyToRemove: string,
137
+ ) => {
138
+ const {serializedHighlights} = this.props;
139
+ const newSerializedHighlights = {...serializedHighlights};
140
+ delete newSerializedHighlights[keyToRemove];
141
+ this.props.onSerializedHighlightsUpdate(newSerializedHighlights);
142
+ };
143
+
144
+ /**
145
+ * When our WordIndexer sends us a new cache of word ranges, store it in
146
+ * our component state.
147
+ */
148
+ _handleWordsUpdate: (wordRanges: $ReadOnlyArray<DOMRange>) => void = (
149
+ wordRanges: $ReadOnlyArray<DOMRange>,
150
+ ) => {
151
+ this.setState({wordRanges});
152
+ };
153
+
154
+ render(): React.Element<"div"> {
155
+ const highlights = this._getDOMHighlights();
156
+
157
+ // NOTE(mdr): This lambda is rebuilt every time this component updates,
158
+ // so every update to HighlightableContent triggers an update in
159
+ // the child HighlightingUI and SelectionTracker, even if the
160
+ // behavior hasn't changed.
161
+ //
162
+ // Over-updating is preferable to under-updating here, because some
163
+ // updates in this component's props/state *do* affect
164
+ // `buildHighlight`'s behavior, and *should* trigger an update.
165
+ //
166
+ // A more performant approach would be to cache this function
167
+ // object until its implicitly-bound inputs change. If profiling
168
+ // leads us to implement such caching, this draft might be a good
169
+ // starting point: https://phabricator.khanacademy.org/D35623?id=170698
170
+ const buildHighlight = (r: DOMRange) => this._buildHighlight(r);
171
+
172
+ return (
173
+ <div
174
+ className={css(styles.container)}
175
+ ref={(container) => (this._container = container)}
176
+ >
177
+ <div>
178
+ {this.props.enabled && this._container && this._content && (
179
+ <HighlightingUI
180
+ buildHighlight={buildHighlight}
181
+ contentNode={this._content}
182
+ editable={this.props.editable}
183
+ highlights={highlights}
184
+ offsetParent={this._container}
185
+ zIndexes={{
186
+ // The content has a z-index of 1, so, to
187
+ // be below the content, use z-index of 0.
188
+ //
189
+ // TODO(mdr): Is this way of passing around
190
+ // `z-index: 0` actually worth the
191
+ // trouble? We used to have more, but
192
+ // now we don't need them...
193
+ belowContent: 0,
194
+ }}
195
+ onAddHighlight={this._handleAddHighlight}
196
+ onRemoveHighlight={this._handleRemoveHighlight}
197
+ />
198
+ )}
199
+ </div>
200
+ <div
201
+ className={css(styles.content)}
202
+ ref={(content) => (this._content = content)}
203
+ >
204
+ <WordIndexer onWordsUpdate={this._handleWordsUpdate}>
205
+ {this.props.children}
206
+ </WordIndexer>
207
+ </div>
208
+ </div>
209
+ );
210
+ }
211
+ }
212
+
213
+ const styles = StyleSheet.create({
214
+ // We render content with z-index 1, and highlights with z-index 0, so that
215
+ // content will receive the browser's native pointer events. (Highlights
216
+ // have their own magic code to manage hover detection, despite being
217
+ // behind the content.)
218
+
219
+ container: {
220
+ // Position the container and give it a z-index, to scope the z-indexes
221
+ // inside by creating a stacking context.
222
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
223
+ //
224
+ // This way, content's z-index of 1, and highlights' z-index of 0, and
225
+ // tooltips' z-index of 2 don't interact with anything external;
226
+ // external UI will see `HighlightableContent` as a single flat layer
227
+ // with a z-index of 0. This removes the need for us to coordinate our
228
+ // choices of z-index with external UI.
229
+ position: "relative",
230
+ zIndex: 0,
231
+ },
232
+
233
+ content: {
234
+ // NOTE(mdr): Creating a stacking context for content of z-index 1 may
235
+ // be problematic if the content and external page want to
236
+ // interleave layers between each other, e.g., if the application UI
237
+ // wants to render something above some parts of our content, but
238
+ // below other parts. It's not relevant for the current Passage-only
239
+ // call site, but could be relevant someday…
240
+ position: "relative",
241
+ zIndex: 1,
242
+ },
243
+ });
244
+
245
+ export default HighlightableContent;