@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,179 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // @flow
3
+ /**
4
+ * This component, given a single DOMHighlight, draws highlight rectangles in
5
+ * the same absolute position as the highlighted content, as computed via
6
+ * `getClientRects`.
7
+ *
8
+ * TODO(mdr): Many things can affect the correct positioning of highlighting,
9
+ * and this component does not attempt to anticipate them. If we start
10
+ * using this highlighting library on content with a more dynamic layout,
11
+ * we should add a hook to allow the parent to `forceUpdate` the
12
+ * `HighlightRenderer`.
13
+ */
14
+ import {StyleSheet, css} from "aphrodite";
15
+ import * as React from "react";
16
+
17
+ import {
18
+ getClientRectsForTextInRange,
19
+ getRelativePosition,
20
+ getRelativeRect,
21
+ } from "./util.js";
22
+
23
+ import type {DOMHighlight, Position, Rect, ZIndexes} from "./types.js";
24
+
25
+ type HighlightRendererProps = {|
26
+ // The DOMHighlight to render.
27
+ highlight: DOMHighlight,
28
+
29
+ // A unique key corresponding to the given `highlight`.
30
+ highlightKey: string,
31
+
32
+ // This component's `offsetParent` element, which is the nearest ancestor
33
+ // with `position: relative`. This will enable us to choose the correct
34
+ // CSS coordinates to align highlights and tooltips with the target
35
+ // content.
36
+ offsetParent: Element,
37
+
38
+ // The z-indexes to use when rendering tooltips above content, and
39
+ // highlights below content.
40
+ zIndexes: ZIndexes,
41
+ |};
42
+
43
+ type HighlightRendererState = {
44
+ // The set of rectangles that cover this highlight's content, relative to
45
+ // the offset parent. This cache is updated on mount and on changes to
46
+ // the `highlight` and `offsetParent` props.
47
+ //
48
+ // We perform this caching because HighlightSetRenderer uses this
49
+ // component's `isHovered` function to access the rectangles every time the
50
+ // user's mouse moves, in order to check the hover state, and recomputing
51
+ // them on every mousemove seems like it could be expensive on older
52
+ // devices (though tbf that's just a gut instinct, not the result of
53
+ // testing on older devices).
54
+ cachedHighlightRects: $ReadOnlyArray<Rect>,
55
+ ...
56
+ };
57
+
58
+ class HighlightRenderer extends React.PureComponent<
59
+ HighlightRendererProps,
60
+ HighlightRendererState,
61
+ > {
62
+ state: HighlightRendererState = {
63
+ cachedHighlightRects: this._computeRects(this.props),
64
+ tooltipIsHovered: false,
65
+ };
66
+
67
+ UNSAFE_componentWillReceiveProps(nextProps: HighlightRendererProps) {
68
+ if (
69
+ this.props.highlight !== nextProps.highlight ||
70
+ this.props.offsetParent !== nextProps.offsetParent
71
+ ) {
72
+ this.setState({
73
+ cachedHighlightRects: this._computeRects(nextProps),
74
+ });
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Compute the set of rectangles that cover the highlighted content, with
80
+ * coordinates relative to the offset parent. That way, we can use them
81
+ * for CSS positioning.
82
+ */
83
+ _computeRects(props: HighlightRendererProps): $ReadOnlyArray<Rect> {
84
+ const {highlight, offsetParent} = props;
85
+
86
+ // Get the set of rectangles that covers the range's text, relative to
87
+ // the offset parent.
88
+ const clientRects = getClientRectsForTextInRange(highlight.domRange);
89
+ const {left, top} = offsetParent.getBoundingClientRect();
90
+ const relativeRects = clientRects.map((rect) =>
91
+ getRelativeRect(rect, {left, top}),
92
+ );
93
+
94
+ return relativeRects;
95
+ }
96
+
97
+ /**
98
+ * Return whether the given mouse position (coordinates relative to this
99
+ * component's offset parent) is hovering over the given rectangle
100
+ * (coordinates also relative to this component's offset parent).
101
+ */
102
+ _rectIsHovered(rect: Rect, mouseOffsetPosition: Position): boolean {
103
+ const positionWithinRect = getRelativePosition(mouseOffsetPosition, {
104
+ left: rect.left,
105
+ top: rect.top,
106
+ });
107
+
108
+ return (
109
+ 0 <= positionWithinRect.left &&
110
+ positionWithinRect.left < rect.width &&
111
+ 0 <= positionWithinRect.top &&
112
+ positionWithinRect.top < rect.height
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Return whether the given mouse position (coordinates relative to the
118
+ * viewport) is hovering over this highlight.
119
+ */
120
+ isHovered(mouseClientPosition: ?Position): boolean {
121
+ if (!mouseClientPosition) {
122
+ return false;
123
+ }
124
+
125
+ const {offsetParent} = this.props;
126
+ const {cachedHighlightRects} = this.state;
127
+
128
+ // Convert the client-relative mouse coordinates to be relative to the
129
+ // offset parent. That way, we can compare them to the cached highlight
130
+ // rectangles.
131
+ const {left, top} = offsetParent.getBoundingClientRect();
132
+ const mouseOffsetPosition = getRelativePosition(mouseClientPosition, {
133
+ left,
134
+ top,
135
+ });
136
+
137
+ return cachedHighlightRects.some((rect) =>
138
+ this._rectIsHovered(rect, mouseOffsetPosition),
139
+ );
140
+ }
141
+
142
+ render(): React.Element<"div"> {
143
+ const rects = this.state.cachedHighlightRects;
144
+
145
+ return (
146
+ <div>
147
+ {rects.map((rect, index) => (
148
+ <div
149
+ key={index}
150
+ className={css(styles.highlightRect)}
151
+ style={{
152
+ // NOTE(mdr): We apply `position: absolute` here
153
+ // rather than in Aphrodite styles, because
154
+ // Aphrodite styles are delayed. If this
155
+ // element temporarily has `position: static`,
156
+ // then it'll displace the content, and other
157
+ // highlights rendering during this update will
158
+ // measure the displaced content instead, oops!
159
+ position: "absolute",
160
+ width: rect.width,
161
+ height: rect.height,
162
+ top: rect.top,
163
+ left: rect.left,
164
+ zIndex: this.props.zIndexes.belowContent,
165
+ }}
166
+ />
167
+ ))}
168
+ </div>
169
+ );
170
+ }
171
+ }
172
+
173
+ const styles = StyleSheet.create({
174
+ highlightRect: {
175
+ background: "#fffabe", // highlighter yellow :)
176
+ },
177
+ });
178
+
179
+ export default HighlightRenderer;
@@ -0,0 +1,190 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // @flow
3
+ /**
4
+ * Render a set of highlights. See HighlightRenderer for more details about how
5
+ * each highlight is rendered.
6
+ *
7
+ * This component manages the state of the "Remove highlight" tooltip. To
8
+ * determine the currently-hovered highlight, it calls the `isHovered` method
9
+ * on each HighlightRenderer.
10
+ */
11
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
12
+ import * as React from "react";
13
+
14
+ import HighlightRenderer from "./highlight-renderer.jsx";
15
+ import HighlightTooltip from "./highlight-tooltip.jsx";
16
+
17
+ import type {DOMHighlightSet, Position, ZIndexes} from "./types.js";
18
+
19
+ type HighlightSetRendererProps = {|
20
+ // Whether the highlights are user-editable. If false, highlights are
21
+ // read-only.
22
+ editable: boolean,
23
+
24
+ // A set of highlights to render.
25
+ highlights: DOMHighlightSet,
26
+
27
+ // This component's `offsetParent` element, which is the nearest ancestor
28
+ // with `position: relative`. This will enable us to choose the correct
29
+ // CSS coordinates to align highlights and tooltips with the target
30
+ // content.
31
+ offsetParent: Element,
32
+
33
+ // A callback indicating that the user would like to remove the highlight
34
+ // with the given key.
35
+ onRemoveHighlight: (highlightKey: string) => mixed,
36
+
37
+ // The z-indexes to use when rendering tooltips above content, and
38
+ // highlights below content.
39
+ zIndexes: ZIndexes,
40
+ |};
41
+ type HighlightSetRendererState = {
42
+ // If the user is currently hovering over a highlight, this field contains
43
+ // its key.
44
+ hoveredHighlightKey: ?string,
45
+ // If the user is currently hovering over the "Remove highlight" tooltip,
46
+ // this field contains the key of the corresponding highlight.
47
+ hoveringTooltipFor: ?string,
48
+ ...
49
+ };
50
+
51
+ class HighlightSetRenderer extends React.PureComponent<
52
+ HighlightSetRendererProps,
53
+ HighlightSetRendererState,
54
+ > {
55
+ state: HighlightSetRendererState = {
56
+ hoveredHighlightKey: null,
57
+ hoveringTooltipFor: null,
58
+ };
59
+
60
+ // eslint-disable-next-line react/sort-comp
61
+ _highlightRenderers: {[highlightKey: string]: HighlightRenderer, ...} = {};
62
+
63
+ componentDidMount() {
64
+ this._updateEditListeners(false, this.props.editable);
65
+ }
66
+
67
+ UNSAFE_componentWillReceiveProps(nextProps: HighlightSetRendererProps) {
68
+ this._updateEditListeners(this.props.editable, nextProps.editable);
69
+
70
+ // If we were previously hovering over a highlight that has been
71
+ // removed, reset the hover state accordingly.
72
+ if (
73
+ this.state.hoveredHighlightKey != null &&
74
+ !(this.state.hoveredHighlightKey in nextProps.highlights)
75
+ ) {
76
+ this.setState({hoveredHighlightKey: null});
77
+ }
78
+
79
+ // Similarly, if we were previously hovering over the tooltip for a
80
+ // highlight that has been removed, reset the hover state accordingly.
81
+ if (
82
+ this.state.hoveringTooltipFor != null &&
83
+ !(this.state.hoveringTooltipFor in nextProps.highlights)
84
+ ) {
85
+ this.setState({hoveringTooltipFor: null});
86
+ }
87
+ }
88
+
89
+ componentWillUnmount() {
90
+ this._updateEditListeners(this.props.editable, false);
91
+ }
92
+
93
+ /**
94
+ * Given whether we were previously listening to mousemove events, and
95
+ * whether we will now listen to mousemove events, add or remove the
96
+ * listener accordingly.
97
+ */
98
+ _updateEditListeners(wasListening: boolean, willListen: boolean) {
99
+ if (!wasListening && willListen) {
100
+ window.addEventListener("mousemove", this._handleMouseMove);
101
+ } else if (wasListening && !willListen) {
102
+ window.removeEventListener("mousemove", this._handleMouseMove);
103
+
104
+ // Additionally, reset the hover state, to ensure that we don't
105
+ // keep old hover state around.
106
+ this.setState({
107
+ hoveredHighlightKey: null,
108
+ hoveringTooltipFor: null,
109
+ });
110
+ }
111
+ }
112
+
113
+ _handleMouseMove: (e: MouseEvent) => void = (e: MouseEvent) => {
114
+ const mouseClientPosition = {left: e.clientX, top: e.clientY};
115
+ const hoveredHighlightKey =
116
+ this._getHoveredHighlightKey(mouseClientPosition);
117
+ this.setState({hoveredHighlightKey});
118
+ };
119
+
120
+ _getHoveredHighlightKey(mouseClientPosition: Position): ?string {
121
+ // If we're hovering over the tooltip, the hovered highlight is the
122
+ // highlight that the tooltip is pointing to.
123
+ const {hoveringTooltipFor} = this.state;
124
+ if (typeof hoveringTooltipFor === "string") {
125
+ return hoveringTooltipFor;
126
+ }
127
+
128
+ // Otherwise, check each highlight renderer to see whether the current
129
+ // mouse position intersects any of the highlight rectangles.
130
+ const highlightKeys = Object.keys(this.props.highlights);
131
+ return highlightKeys.find((key) => {
132
+ const highlightRenderer = this._highlightRenderers[key];
133
+ return (
134
+ highlightRenderer &&
135
+ highlightRenderer.isHovered(mouseClientPosition)
136
+ );
137
+ });
138
+ }
139
+
140
+ _renderTooltip(): null | React.Node {
141
+ const {hoveredHighlightKey} = this.state;
142
+ if (hoveredHighlightKey == null) {
143
+ return null;
144
+ }
145
+
146
+ const hoveredHighlight = this.props.highlights[hoveredHighlightKey];
147
+
148
+ return (
149
+ <HighlightTooltip
150
+ label={i18n._("Remove highlight")}
151
+ focusNode={hoveredHighlight.domRange.endContainer}
152
+ focusOffset={hoveredHighlight.domRange.endOffset}
153
+ offsetParent={this.props.offsetParent}
154
+ onClick={() =>
155
+ this.props.onRemoveHighlight(hoveredHighlightKey)
156
+ }
157
+ onMouseEnter={() =>
158
+ this.setState({hoveringTooltipFor: hoveredHighlightKey})
159
+ }
160
+ onMouseLeave={() => this.setState({hoveringTooltipFor: null})}
161
+ />
162
+ );
163
+ }
164
+
165
+ render(): React.Element<"div"> {
166
+ return (
167
+ <div>
168
+ {Object.keys(this.props.highlights).map((key) => (
169
+ <HighlightRenderer
170
+ ref={(r) => {
171
+ if (r) {
172
+ this._highlightRenderers[key] = r;
173
+ } else {
174
+ delete this._highlightRenderers[key];
175
+ }
176
+ }}
177
+ key={key}
178
+ highlight={this.props.highlights[key]}
179
+ highlightKey={key}
180
+ offsetParent={this.props.offsetParent}
181
+ zIndexes={this.props.zIndexes}
182
+ />
183
+ ))}
184
+ {this.props.editable && this._renderTooltip()}
185
+ </div>
186
+ );
187
+ }
188
+ }
189
+
190
+ export default HighlightSetRenderer;
@@ -0,0 +1,153 @@
1
+ // @flow
2
+ /**
3
+ * A tooltip to point to the focus of a highlight.
4
+ *
5
+ * We use the NewTooltip component, which positions itself not by coordinate,
6
+ * but by watching the position of a target DOM element. So, we render a 0x0
7
+ * DOM element, absolute-position it to the position of the the highlight
8
+ * focus, and point the tooltip at that 0x0 element.
9
+ *
10
+ * This technique enables NewTooltip to use its existing visibility-watching
11
+ * and scroll-tracking logic to effectively "watch" the focus of this
12
+ * highlight. So, we get lots of the repositioning logic for free! But we _do_
13
+ * still have to notify the tooltip to remeasure itself when the highlight
14
+ * focus _changes_.
15
+ */
16
+ import {StyleSheet, css} from "aphrodite";
17
+ import * as React from "react";
18
+
19
+ import {Log} from "../../../logging/log.js";
20
+ import {colors} from "../../../styles/global-styles.js";
21
+ import NewTooltip from "../../new-tooltip/new-tooltip.jsx";
22
+
23
+ import {getRelativeRect} from "./util.js";
24
+
25
+ import type {Rect} from "./types.js";
26
+
27
+ type HighlightTooltipProps = {|
28
+ label: string,
29
+ onClick: () => mixed,
30
+ onMouseEnter?: () => mixed,
31
+ onMouseLeave?: () => mixed,
32
+
33
+ focusNode: Node,
34
+ focusOffset: number,
35
+ offsetParent: Element,
36
+ |};
37
+
38
+ class HighlightTooltip extends React.PureComponent<HighlightTooltipProps> {
39
+ _tooltip: ?React.ElementRef<typeof NewTooltip>;
40
+
41
+ componentDidUpdate(oldProps: HighlightTooltipProps) {
42
+ // If the highlight focus might've changed, then tell the tooltip to
43
+ // remeasure itself. For "Add highlight", this happens when an existing
44
+ // selection is updated, e.g., by using the Shift+RightArrow shortcut.
45
+ // For "Remove highlight", this can happen when we quickly switch from
46
+ // targeting one highlight to another: we reuse the same
47
+ // HighlightTooltip instance, for improved performance.
48
+ if (
49
+ this.props.focusNode !== oldProps.focusNode ||
50
+ this.props.focusOffset !== oldProps.focusOffset ||
51
+ this.props.offsetParent !== oldProps.offsetParent
52
+ ) {
53
+ this._tooltip?.remeasure();
54
+ }
55
+ }
56
+
57
+ _getFocusRect(): ?Rect {
58
+ const {focusNode, focusOffset, offsetParent} = this.props;
59
+
60
+ // Get a range of *just* the focus point of the selection.
61
+ const focusRange = document.createRange();
62
+ focusRange.setStart(focusNode, focusOffset);
63
+ focusRange.setEnd(focusNode, focusOffset);
64
+
65
+ // Then, get the bounding box of the collapsed range. This will be a
66
+ // zero-width rectangle, but still have positioning information, which
67
+ // we can use the position the tooltip.
68
+ //
69
+ // NOTE(mdr): If we used getClientBoundingRect here instead, Safari
70
+ // would return an unpositioned rect. But all tested browsers at
71
+ // time of writing (latest Chrome, Firefox, Safari) return at least
72
+ // one rectangle from getClientRects, and it's well-positioned.
73
+ const focusRect = focusRange.getClientRects()[0];
74
+
75
+ // NOTE(mdr): ...except in the case where the focus is between two
76
+ // paragraphs, in which case the focus range has no client rects?
77
+ // Not sure why, or in what browsers (only noticed in Chrome 60),
78
+ // but let's catch that case and bail out.
79
+ // TODO(mdr): Instead, we should somehow walk back the range until we
80
+ // find an appropriate position for the tooltip. Sounds tricky!
81
+ if (!focusRect) {
82
+ Log.log(
83
+ "[Highlighting] Known bug: Could not determine the focus " +
84
+ 'position, so did not show an "Add Highlight" tooltip. ' +
85
+ "https://app.asana.com/0/329800276300868/413878480039713 " +
86
+ "(see also /r/asana-links)",
87
+ );
88
+ return null;
89
+ }
90
+
91
+ // Compute the desired position of the tooltip relative to the offset
92
+ // parent.
93
+ const {left, top} = offsetParent.getBoundingClientRect();
94
+ const relativeFocusRect = getRelativeRect(
95
+ {
96
+ left: focusRect.left,
97
+ top: focusRect.top,
98
+ width: focusRect.width,
99
+ height: focusRect.height,
100
+ },
101
+ {left, top},
102
+ );
103
+
104
+ return relativeFocusRect;
105
+ }
106
+
107
+ render(): null | React.Node {
108
+ const focusRect = this._getFocusRect();
109
+ if (!focusRect) {
110
+ return null;
111
+ }
112
+
113
+ const content = (
114
+ <div className={css(styles.tooltipLabel)}>{this.props.label}</div>
115
+ );
116
+
117
+ return (
118
+ <NewTooltip
119
+ content={content}
120
+ color={colors.kaBlue}
121
+ inverted={true}
122
+ onClick={this.props.onClick}
123
+ onMouseEnter={this.props.onMouseEnter}
124
+ onMouseLeave={this.props.onMouseLeave}
125
+ toggleOnHover={false}
126
+ showOnMount={true}
127
+ ref={(e) => (this._tooltip = e)}
128
+ >
129
+ <div
130
+ style={{
131
+ position: "absolute",
132
+ left: focusRect.left,
133
+ top: focusRect.top,
134
+ height: focusRect.height,
135
+ }}
136
+ />
137
+ </NewTooltip>
138
+ );
139
+ }
140
+ }
141
+
142
+ const styles = StyleSheet.create({
143
+ tooltipLabel: {
144
+ // Unlike most tooltip contents, we don't want this label to be
145
+ // selectable. Selecting it is not only unhelpful, but dangerous: if
146
+ // the user accidentally selects the "Add highlight" tooltip text, then
147
+ // we lose our reference to the _actual_ text they want to highlight,
148
+ // and the "Add highlight" action fails.
149
+ userSelect: "none",
150
+ },
151
+ });
152
+
153
+ export default HighlightTooltip;
@@ -0,0 +1,163 @@
1
+ // @flow
2
+ /**
3
+ * This component, given a set of DOMHighlights, draws highlight rectangles in
4
+ * the same absolute position as the highlighted content, as computed by the
5
+ * range's `getClientRects` method.
6
+ *
7
+ * TODO(mdr): Many things can affect the correct positioning of highlighting,
8
+ * and this component does not attempt to anticipate them. If we start
9
+ * using this highlighting library on content with a more dynamic layout,
10
+ * we should add a hook to allow the parent to `forceUpdate` the
11
+ * `HighlightingUI`.
12
+ */
13
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
14
+ import * as React from "react";
15
+
16
+ import {rangesOverlap} from "../ranges.js";
17
+
18
+ import HighlightSetRenderer from "./highlight-set-renderer.jsx";
19
+ import HighlightTooltip from "./highlight-tooltip.jsx";
20
+ import SelectionTracker from "./selection-tracker.jsx";
21
+
22
+ import type {TrackedSelection} from "./selection-tracker.jsx";
23
+ import type {
24
+ DOMHighlight,
25
+ DOMHighlightSet,
26
+ DOMRange,
27
+ ZIndexes,
28
+ } from "./types.js";
29
+
30
+ type HighlightingUIProps = {|
31
+ // A function that builds a DOMHighlight from the given DOMRange, if
32
+ // possible. If it would not currently be valid to add a highlight over the
33
+ // given DOMRange, returns null.
34
+ buildHighlight: (range: DOMRange) => ?DOMHighlight,
35
+
36
+ // A Node that contains the highlightable content.
37
+ contentNode: Node,
38
+
39
+ // Whether the highlights are user-editable. If false, highlights are
40
+ // read-only.
41
+ editable: boolean,
42
+
43
+ // A set of highlights to render.
44
+ highlights: DOMHighlightSet,
45
+
46
+ // This component's `offsetParent` element, which is the nearest ancestor
47
+ // with `position: relative`. This will enable us to choose the correct
48
+ // CSS coordinates to align highlights and tooltips with the target
49
+ // content.
50
+ offsetParent: Element,
51
+
52
+ // A callback indicating that the user would like to add the given
53
+ // highlight to the current set of highlights.
54
+ onAddHighlight: (range: DOMHighlight) => mixed,
55
+
56
+ // A callback indicating that the user would like to remove the highlight
57
+ // with the given key.
58
+ onRemoveHighlight: (highlightKey: string) => mixed,
59
+
60
+ // The z-indexes to use when rendering tooltips above content, and
61
+ // highlights below content.
62
+ zIndexes: ZIndexes,
63
+ |};
64
+
65
+ class HighlightingUI extends React.PureComponent<HighlightingUIProps> {
66
+ _handleAddHighlight(highlightToAdd: DOMHighlight) {
67
+ this.props.onAddHighlight(highlightToAdd);
68
+
69
+ // Deselect the newly-highlighted text, by collapsing the selection
70
+ // to the end of the range.
71
+ const selection = document.getSelection();
72
+ if (selection) {
73
+ selection.collapseToEnd();
74
+ }
75
+ }
76
+
77
+ _selectionIsValid(trackedSelection: ?TrackedSelection): boolean {
78
+ if (!trackedSelection) {
79
+ return false;
80
+ }
81
+
82
+ const {contentNode} = this.props;
83
+
84
+ // Create a range over the content node.
85
+ const contentRange = new Range();
86
+ contentRange.selectNodeContents(contentNode);
87
+
88
+ // Create a range over the focus position.
89
+ const focusRange = new Range();
90
+ focusRange.setStart(
91
+ trackedSelection.focusNode,
92
+ trackedSelection.focusOffset,
93
+ );
94
+ focusRange.collapse(true /* to start */);
95
+
96
+ // Determine whether the content range contains the focus, by checking
97
+ // whether they intersect. Because the focus range is a single point,
98
+ // intersection is equivalent to being fully contained.
99
+ const contentContainsFocus = rangesOverlap(contentRange, focusRange);
100
+
101
+ // If the content contains the focus, this is a valid selection. Some
102
+ // parts of the range might go beyond the content, but that's okay; the
103
+ // corresponding DOMHighlight is already trimmed to only contain valid
104
+ // words. We're just checking that the tooltip we render will be inside
105
+ // the content, because rendering a tooltip outside the content would
106
+ // be weird.
107
+ const selectionIsValid = contentContainsFocus;
108
+
109
+ return selectionIsValid;
110
+ }
111
+
112
+ render(): React.Node {
113
+ return (
114
+ <SelectionTracker
115
+ buildHighlight={this.props.buildHighlight}
116
+ enabled={this.props.editable}
117
+ >
118
+ {(trackedSelection, userIsMouseSelecting) => (
119
+ <div>
120
+ <HighlightSetRenderer
121
+ editable={
122
+ /* An existing highlight is editable when the
123
+ * component is in editable mode, and there's no
124
+ * selection in progress. */
125
+ this.props.editable &&
126
+ !this._selectionIsValid(trackedSelection)
127
+ }
128
+ highlights={this.props.highlights}
129
+ offsetParent={this.props.offsetParent}
130
+ onRemoveHighlight={this.props.onRemoveHighlight}
131
+ zIndexes={this.props.zIndexes}
132
+ />
133
+ {this._selectionIsValid(trackedSelection) &&
134
+ !userIsMouseSelecting && (
135
+ <HighlightTooltip
136
+ label={i18n._("Add highlight")}
137
+ onClick={() =>
138
+ this._handleAddHighlight(
139
+ // TODO(mdr): We found a new Flow error when upgrading:
140
+ // "proposedHighlight (Cannot get `trackedSelection.proposedHighlight` because property `proposedHighlight` is missing in null or undefined [1].)"
141
+ // $FlowFixMe[incompatible-use](0.57.3->0.75.0)
142
+ trackedSelection.proposedHighlight,
143
+ )
144
+ }
145
+ // TODO(mdr): We found a new Flow error when upgrading:
146
+ // "focusNode (Cannot get `trackedSelection.focusNode` because property `focusNode` is missing in null or undefined [1].)"
147
+ // $FlowFixMe[incompatible-use](0.57.3->0.75.0)
148
+ focusNode={trackedSelection.focusNode}
149
+ // TODO(mdr): We found a new Flow error when upgrading:
150
+ // "focusOffset (Cannot get `trackedSelection.focusOffset` because property `focusOffset` is missing in null or undefined [1].)"
151
+ // $FlowFixMe[incompatible-use](0.57.3->0.75.0)
152
+ focusOffset={trackedSelection.focusOffset}
153
+ offsetParent={this.props.offsetParent}
154
+ />
155
+ )}
156
+ </div>
157
+ )}
158
+ </SelectionTracker>
159
+ );
160
+ }
161
+ }
162
+
163
+ export default HighlightingUI;