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