@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,622 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
// @flow
|
|
3
|
+
|
|
4
|
+
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
5
|
+
import classNames from "classnames";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import _ from "underscore";
|
|
8
|
+
|
|
9
|
+
import InputWithExamples from "../components/input-with-examples.jsx";
|
|
10
|
+
import PossibleAnswers from "../components/possible-answers.jsx";
|
|
11
|
+
import SimpleKeypadInput from "../components/simple-keypad-input.jsx";
|
|
12
|
+
import {linterContextDefault} from "../gorgon/proptypes.js";
|
|
13
|
+
import {ApiOptions, ClassNames as ApiClassNames} from "../perseus-api.jsx";
|
|
14
|
+
import TexWrangler from "../tex-wrangler.js";
|
|
15
|
+
import KhanAnswerTypes from "../util/answer-types.js";
|
|
16
|
+
import KhanMath from "../util/math.js";
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
PerseusNumericInputWidgetOptions,
|
|
20
|
+
PerseusNumericInputAnswer,
|
|
21
|
+
} from "../perseus-types.js";
|
|
22
|
+
import type {PerseusScore, WidgetExports, WidgetProps} from "../types.js";
|
|
23
|
+
|
|
24
|
+
const ParseTex = TexWrangler.parseTex;
|
|
25
|
+
|
|
26
|
+
const answerFormButtons = [
|
|
27
|
+
{title: "Integers", value: "integer", content: "6"},
|
|
28
|
+
{title: "Decimals", value: "decimal", content: "0.75"},
|
|
29
|
+
{title: "Proper fractions", value: "proper", content: "\u2157"},
|
|
30
|
+
{
|
|
31
|
+
title: "Improper fractions",
|
|
32
|
+
value: "improper",
|
|
33
|
+
content: "\u2077\u2044\u2084",
|
|
34
|
+
},
|
|
35
|
+
{title: "Mixed numbers", value: "mixed", content: "1\u00BE"},
|
|
36
|
+
{title: "Numbers with \u03C0", value: "pi", content: "\u03C0"},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const formExamples = {
|
|
40
|
+
integer: () => i18n._("an integer, like $6$"),
|
|
41
|
+
proper: (form) =>
|
|
42
|
+
form.simplify === "optional"
|
|
43
|
+
? i18n._("a *proper* fraction, like $1/2$ or $6/10$")
|
|
44
|
+
: i18n._("a *simplified proper* fraction, like $3/5$"),
|
|
45
|
+
improper: (form) =>
|
|
46
|
+
form.simplify === "optional"
|
|
47
|
+
? i18n._("an *improper* fraction, like $10/7$ or $14/8$")
|
|
48
|
+
: i18n._("a *simplified improper* fraction, like $7/4$"),
|
|
49
|
+
mixed: () => i18n._("a mixed number, like $1\\ 3/4$"),
|
|
50
|
+
decimal: () => i18n._("an *exact* decimal, like $0.75$"),
|
|
51
|
+
pi: () =>
|
|
52
|
+
i18n._(
|
|
53
|
+
"a multiple of pi, like $12\\ \\text{pi}$ or " +
|
|
54
|
+
"$2/3\\ \\text{pi}$",
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type UserInput = {|
|
|
59
|
+
currentValue: string,
|
|
60
|
+
|};
|
|
61
|
+
|
|
62
|
+
export type Rubric = PerseusNumericInputWidgetOptions;
|
|
63
|
+
|
|
64
|
+
type ExternalProps = WidgetProps<PerseusNumericInputWidgetOptions, Rubric>;
|
|
65
|
+
|
|
66
|
+
type Props = {|
|
|
67
|
+
...ExternalProps,
|
|
68
|
+
size: $NonMaybeType<ExternalProps["size"]>,
|
|
69
|
+
rightAlign: $NonMaybeType<ExternalProps["rightAlign"]>,
|
|
70
|
+
apiOptions: $NonMaybeType<ExternalProps["apiOptions"]>,
|
|
71
|
+
coefficient: $NonMaybeType<ExternalProps["coefficient"]>,
|
|
72
|
+
answerForms: $NonMaybeType<ExternalProps["answerForms"]>,
|
|
73
|
+
labelText: $NonMaybeType<ExternalProps["labelText"]>,
|
|
74
|
+
linterContext: $NonMaybeType<ExternalProps["linterContext"]>,
|
|
75
|
+
multipleNumberInput: $NonMaybeType<ExternalProps["multipleNumberInput"]>,
|
|
76
|
+
currentValue: string,
|
|
77
|
+
|};
|
|
78
|
+
|
|
79
|
+
type DefaultProps = {|
|
|
80
|
+
currentValue: Props["currentValue"],
|
|
81
|
+
size: Props["size"],
|
|
82
|
+
rightAlign: Props["rightAlign"],
|
|
83
|
+
apiOptions: Props["apiOptions"],
|
|
84
|
+
coefficient: Props["coefficient"],
|
|
85
|
+
answerForms: Props["answerForms"],
|
|
86
|
+
labelText: Props["labelText"],
|
|
87
|
+
linterContext: Props["linterContext"],
|
|
88
|
+
multipleNumberInput: Props["multipleNumberInput"],
|
|
89
|
+
|};
|
|
90
|
+
|
|
91
|
+
type State = {|
|
|
92
|
+
// keeps track of the other set of values when switching
|
|
93
|
+
// between 0 and finite solutions
|
|
94
|
+
previousValues: $ReadOnlyArray<string>,
|
|
95
|
+
|};
|
|
96
|
+
|
|
97
|
+
export class NumericInput extends React.Component<Props, State> {
|
|
98
|
+
static defaultProps: DefaultProps = {
|
|
99
|
+
currentValue: "",
|
|
100
|
+
size: "normal",
|
|
101
|
+
rightAlign: false,
|
|
102
|
+
apiOptions: ApiOptions.defaults,
|
|
103
|
+
coefficient: false,
|
|
104
|
+
answerForms: [],
|
|
105
|
+
labelText: "",
|
|
106
|
+
linterContext: linterContextDefault,
|
|
107
|
+
multipleNumberInput: false,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
static getUserInputFromProps(props: Props): UserInput {
|
|
111
|
+
return {
|
|
112
|
+
currentValue: props.currentValue,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static getOneCorrectAnswerFromRubric(
|
|
117
|
+
rubric: Rubric,
|
|
118
|
+
): ?PerseusNumericInputAnswer {
|
|
119
|
+
const correctAnswers = _.filter(
|
|
120
|
+
rubric.answers,
|
|
121
|
+
(answer) => answer.status === "correct",
|
|
122
|
+
);
|
|
123
|
+
const answerStrings = _.map(correctAnswers, (answer) => {
|
|
124
|
+
// Figure out how this answer is supposed to be
|
|
125
|
+
// displayed
|
|
126
|
+
let format = "decimal";
|
|
127
|
+
if (answer.answerForms && answer.answerForms[0]) {
|
|
128
|
+
// NOTE(johnsullivan): This isn't exactly ideal, but
|
|
129
|
+
// it does behave well for all the currently known
|
|
130
|
+
// problems. See D14742 for some discussion on
|
|
131
|
+
// alternate strategies.
|
|
132
|
+
format = answer.answerForms[0];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let answerString = KhanMath.toNumericString(answer.value, format);
|
|
136
|
+
if (answer.maxError) {
|
|
137
|
+
answerString +=
|
|
138
|
+
" \u00B1 " +
|
|
139
|
+
KhanMath.toNumericString(answer.maxError, format);
|
|
140
|
+
}
|
|
141
|
+
return answerString;
|
|
142
|
+
});
|
|
143
|
+
if (answerStrings.length === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
return answerStrings[0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static validate(useInput: UserInput, rubric: Rubric): PerseusScore {
|
|
150
|
+
const allAnswerForms = _.pluck(answerFormButtons, "value");
|
|
151
|
+
|
|
152
|
+
const createValidator = (answer) =>
|
|
153
|
+
KhanAnswerTypes.number.createValidatorFunctional(answer.value, {
|
|
154
|
+
message: answer.message,
|
|
155
|
+
simplify:
|
|
156
|
+
answer.status === "correct" ? answer.simplify : "optional",
|
|
157
|
+
inexact: true, // TODO(merlob) backfill / delete
|
|
158
|
+
maxError: answer.maxError,
|
|
159
|
+
forms:
|
|
160
|
+
answer.strict &&
|
|
161
|
+
answer.answerForms &&
|
|
162
|
+
answer.answerForms.length !== 0
|
|
163
|
+
? answer.answerForms
|
|
164
|
+
: allAnswerForms,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// We may have received TeX; try to parse it before grading.
|
|
168
|
+
// If `currentValue` is not TeX, this should be a no-op.
|
|
169
|
+
const currentValue = ParseTex(useInput.currentValue);
|
|
170
|
+
const correctAnswers = _.where(rubric.answers, {status: "correct"});
|
|
171
|
+
|
|
172
|
+
const normalizedAnswerExpected = correctAnswers.every(
|
|
173
|
+
(answer) => Math.abs(answer.value) <= 1,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Look through all correct answers for one that matches either
|
|
177
|
+
// precisely or approximately and return the appropriate message:
|
|
178
|
+
// - if precise, return the message that the answer came with
|
|
179
|
+
// - if it needs to be simplified, etc., show that message
|
|
180
|
+
let result = _.find(
|
|
181
|
+
_.map(correctAnswers, (answer) => {
|
|
182
|
+
// The coefficient is an attribute of the widget
|
|
183
|
+
let localValue = currentValue;
|
|
184
|
+
if (rubric.coefficient) {
|
|
185
|
+
if (!localValue) {
|
|
186
|
+
localValue = 1;
|
|
187
|
+
} else if (localValue === "-") {
|
|
188
|
+
localValue = -1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const validate = createValidator(answer);
|
|
192
|
+
return validate(
|
|
193
|
+
maybeParsePercentInput(
|
|
194
|
+
localValue,
|
|
195
|
+
normalizedAnswerExpected,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
}),
|
|
199
|
+
(match) => match.correct || match.empty,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (!result) {
|
|
203
|
+
// Otherwise, if the guess is not correct
|
|
204
|
+
const otherAnswers = [].concat(
|
|
205
|
+
_.where(rubric.answers, {status: "ungraded"}),
|
|
206
|
+
_.where(rubric.answers, {status: "wrong"}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Look through all other answers and if one matches either
|
|
210
|
+
// precisely or approximately return the answer's message
|
|
211
|
+
const match = _.find(otherAnswers, (answer) => {
|
|
212
|
+
const validate = createValidator(answer);
|
|
213
|
+
return validate(
|
|
214
|
+
maybeParsePercentInput(
|
|
215
|
+
currentValue,
|
|
216
|
+
normalizedAnswerExpected,
|
|
217
|
+
),
|
|
218
|
+
).correct;
|
|
219
|
+
});
|
|
220
|
+
result = {
|
|
221
|
+
empty: match ? match.status === "ungraded" : false,
|
|
222
|
+
correct: match ? match.status === "correct" : false,
|
|
223
|
+
message: match ? match.message : null,
|
|
224
|
+
guess: currentValue,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// TODO(eater): Seems silly to translate result to this
|
|
229
|
+
// invalid/points thing and immediately translate it
|
|
230
|
+
// back in ItemRenderer.scoreInput()
|
|
231
|
+
if (result.empty) {
|
|
232
|
+
return {
|
|
233
|
+
type: "invalid",
|
|
234
|
+
message: result.message,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
type: "points",
|
|
239
|
+
earned: result.correct ? 1 : 0,
|
|
240
|
+
total: 1,
|
|
241
|
+
message: result.message,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
state: State = {
|
|
246
|
+
// keeps track of the other set of values when switching
|
|
247
|
+
// between 0 and finite solutions
|
|
248
|
+
previousValues: [""],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
getAnswerBlurb: (Rubric) => [?React.Node, boolean] = (rubric) => {
|
|
252
|
+
let correct;
|
|
253
|
+
let answerBlurb;
|
|
254
|
+
|
|
255
|
+
/* Ignore this block because SAT is deprecated */
|
|
256
|
+
/* istanbul ignore next */
|
|
257
|
+
if (this.props.apiOptions.satStyling && rubric) {
|
|
258
|
+
const score = this.simpleValidate(rubric);
|
|
259
|
+
correct = score.type === "points" && score.earned === score.total;
|
|
260
|
+
|
|
261
|
+
if (!correct) {
|
|
262
|
+
const correctAnswers = _.filter(
|
|
263
|
+
rubric.answers,
|
|
264
|
+
(answer) => answer.status === "correct",
|
|
265
|
+
);
|
|
266
|
+
const answerStrings = _.map(correctAnswers, (answer) => {
|
|
267
|
+
// Figure out how this answer is supposed to be
|
|
268
|
+
// displayed
|
|
269
|
+
let format = "decimal";
|
|
270
|
+
if (answer.answerForms && answer.answerForms[0]) {
|
|
271
|
+
// NOTE(johnsullivan): This isn't exactly ideal, but
|
|
272
|
+
// it does behave well for all the currently known
|
|
273
|
+
// problems. See D14742 for some discussion on
|
|
274
|
+
// alternate strategies.
|
|
275
|
+
format = answer.answerForms[0];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let answerString = KhanMath.toNumericString(
|
|
279
|
+
answer.value,
|
|
280
|
+
format,
|
|
281
|
+
);
|
|
282
|
+
if (answer.maxError) {
|
|
283
|
+
answerString +=
|
|
284
|
+
" \u00B1 " +
|
|
285
|
+
KhanMath.toNumericString(answer.maxError, format);
|
|
286
|
+
}
|
|
287
|
+
return answerString;
|
|
288
|
+
});
|
|
289
|
+
answerBlurb = <PossibleAnswers answers={answerStrings} />;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return [answerBlurb, !!correct];
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
getClasses: (boolean, $FlowFixMe) => $FlowFixMe = (correct, rubric) => {
|
|
296
|
+
const classes = {};
|
|
297
|
+
classes["perseus-input-size-" + this.props.size] = true;
|
|
298
|
+
classes["perseus-input-right-align"] = this.props.rightAlign;
|
|
299
|
+
classes[ApiClassNames.CORRECT] =
|
|
300
|
+
rubric && correct && this.props.currentValue;
|
|
301
|
+
classes[ApiClassNames.INCORRECT] =
|
|
302
|
+
rubric && !correct && this.props.currentValue;
|
|
303
|
+
classes[ApiClassNames.UNANSWERED] = rubric && !this.props.currentValue;
|
|
304
|
+
return classes;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// TODO(Nicole, Jeremy): This is maybe never used and should be removed
|
|
308
|
+
examples: () => $ReadOnlyArray<string> = () => {
|
|
309
|
+
// if the set of specified forms are empty, allow all forms
|
|
310
|
+
const forms =
|
|
311
|
+
this.props.answerForms?.length !== 0
|
|
312
|
+
? this.props.answerForms
|
|
313
|
+
: _.map(_.keys(formExamples), (name) => {
|
|
314
|
+
return {
|
|
315
|
+
name: name,
|
|
316
|
+
simplify: "required",
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
let examples = _.map(forms, (form) => {
|
|
321
|
+
return formExamples[form.name](form);
|
|
322
|
+
});
|
|
323
|
+
// Ensure no duplicate tooltip text from simplified and unsimplified
|
|
324
|
+
// versions of the same format
|
|
325
|
+
examples = _.uniq(examples);
|
|
326
|
+
|
|
327
|
+
return [i18n._("**Your answer should be** ")].concat(examples);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
shouldShowExamples: () => boolean = () => {
|
|
331
|
+
const noFormsAccepted = this.props.answerForms?.length === 0;
|
|
332
|
+
// To check if all answer forms are accepted, we must first
|
|
333
|
+
// find the *names* of all accepted forms, and see if they are
|
|
334
|
+
// all present, ignoring duplicates
|
|
335
|
+
const answerFormNames = _.uniq(
|
|
336
|
+
this.props.answerForms?.map((form) => form.name),
|
|
337
|
+
);
|
|
338
|
+
const allFormsAccepted = answerFormNames.length >= _.size(formExamples);
|
|
339
|
+
return !noFormsAccepted && !allFormsAccepted;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
simpleValidate: (Rubric) => PerseusScore = (rubric) => {
|
|
343
|
+
return NumericInput.validate(this.getUserInput(), rubric);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
focus: () => boolean = () => {
|
|
347
|
+
// eslint-disable-next-line react/no-string-refs
|
|
348
|
+
this.refs.input.focus();
|
|
349
|
+
return true;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
focusInputPath: ($FlowFixMe) => void = (inputPath) => {
|
|
353
|
+
/* istanbul ignore next */
|
|
354
|
+
// eslint-disable-next-line react/no-string-refs
|
|
355
|
+
this.refs.input.focus();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
blurInputPath: ($FlowFixMe) => void = (inputPath) => {
|
|
359
|
+
// eslint-disable-next-line react/no-string-refs
|
|
360
|
+
this.refs.input.blur();
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<string>> = () => {
|
|
364
|
+
// The widget itself is an input, so we return a single empty list to
|
|
365
|
+
// indicate this.
|
|
366
|
+
/* istanbul ignore next */
|
|
367
|
+
return [[]];
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
getGrammarTypeForPath: ($FlowFixMe) => string = (inputPath) => {
|
|
371
|
+
/* istanbul ignore next */
|
|
372
|
+
return "number";
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
setInputValue: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => void = (
|
|
376
|
+
path,
|
|
377
|
+
newValue,
|
|
378
|
+
cb,
|
|
379
|
+
) => {
|
|
380
|
+
/* istanbul ignore next */
|
|
381
|
+
this.props.onChange(
|
|
382
|
+
{
|
|
383
|
+
currentValue: newValue,
|
|
384
|
+
},
|
|
385
|
+
cb,
|
|
386
|
+
);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
getUserInput: () => UserInput = () => {
|
|
390
|
+
return NumericInput.getUserInputFromProps(this.props);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
handleChange: (string, $FlowFixMe) => void = (newValue, cb) => {
|
|
394
|
+
this.props.onChange({currentValue: newValue}, cb);
|
|
395
|
+
this.props.trackInteraction();
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
_getInputType: () => "tex" | "text" = () => {
|
|
399
|
+
if (this.props.apiOptions.staticRender) {
|
|
400
|
+
return "tex";
|
|
401
|
+
}
|
|
402
|
+
return "text";
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
_handleFocus: () => void = () => {
|
|
406
|
+
this.props.onFocus([]);
|
|
407
|
+
// HACK(kevinb): We want to dismiss the feedback popover that webapp
|
|
408
|
+
// displays as soon as a user clicks in in the input field so we call
|
|
409
|
+
// interactionCallback directly.
|
|
410
|
+
const {interactionCallback} = this.props.apiOptions;
|
|
411
|
+
if (interactionCallback) {
|
|
412
|
+
interactionCallback();
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
_handleBlur: () => void = () => {
|
|
417
|
+
this.props.onBlur([]);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
render(): React.Node {
|
|
421
|
+
const rubric = this.props.reviewModeRubric;
|
|
422
|
+
const answers = this.getAnswerBlurb(rubric);
|
|
423
|
+
const answerBlurb = answers[0];
|
|
424
|
+
const correct = answers[1];
|
|
425
|
+
const classes = this.getClasses(correct, rubric);
|
|
426
|
+
|
|
427
|
+
let labelText = this.props.labelText;
|
|
428
|
+
if (labelText == null || labelText === "") {
|
|
429
|
+
labelText = i18n._("Your answer:");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// To right align a custom keypad we need to wrap it.
|
|
433
|
+
const maybeRightAlignKeypadInput = (keypadInput) => {
|
|
434
|
+
return this.props.rightAlign ? (
|
|
435
|
+
<div className="perseus-input-right-align">{keypadInput}</div>
|
|
436
|
+
) : (
|
|
437
|
+
keypadInput
|
|
438
|
+
);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
if (this.props.apiOptions.customKeypad) {
|
|
442
|
+
// TODO(charlie): Support "Review Mode".
|
|
443
|
+
return maybeRightAlignKeypadInput(
|
|
444
|
+
<SimpleKeypadInput
|
|
445
|
+
// eslint-disable-next-line react/no-string-refs
|
|
446
|
+
ref="input"
|
|
447
|
+
value={this.props.currentValue}
|
|
448
|
+
keypadElement={this.props.keypadElement}
|
|
449
|
+
onChange={this.handleChange}
|
|
450
|
+
onFocus={this._handleFocus}
|
|
451
|
+
onBlur={this._handleBlur}
|
|
452
|
+
/>,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const input = (
|
|
457
|
+
<InputWithExamples
|
|
458
|
+
// eslint-disable-next-line react/no-string-refs
|
|
459
|
+
ref="input"
|
|
460
|
+
value={this.props.currentValue}
|
|
461
|
+
onChange={this.handleChange}
|
|
462
|
+
className={classNames(classes)}
|
|
463
|
+
labelText={labelText}
|
|
464
|
+
type={this._getInputType()}
|
|
465
|
+
examples={this.examples()}
|
|
466
|
+
shouldShowExamples={this.shouldShowExamples()}
|
|
467
|
+
onFocus={this._handleFocus}
|
|
468
|
+
onBlur={this._handleBlur}
|
|
469
|
+
id={this.props.widgetId}
|
|
470
|
+
disabled={this.props.apiOptions.readOnly}
|
|
471
|
+
// $FlowFixMe[prop-missing]
|
|
472
|
+
highlightLint={this.props.highlightLint}
|
|
473
|
+
/>
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
/* This only gets used with SAT Content */
|
|
477
|
+
/* istanbul ignore next if */
|
|
478
|
+
if (answerBlurb) {
|
|
479
|
+
return (
|
|
480
|
+
<span className="perseus-input-with-answer-blurb">
|
|
481
|
+
{input}
|
|
482
|
+
{answerBlurb}
|
|
483
|
+
</span>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (this.props.apiOptions.satStyling) {
|
|
487
|
+
// NOTE(amy): the input widgets themselves already have
|
|
488
|
+
// a default aria label of "Your Answer", so we hide this
|
|
489
|
+
// redundant label from screen-readers.
|
|
490
|
+
return (
|
|
491
|
+
<label className="perseus-input-with-label">
|
|
492
|
+
<span className="perseus-input-label" aria-hidden="true">
|
|
493
|
+
{i18n.doNotTranslate("Answer:")}
|
|
494
|
+
</span>
|
|
495
|
+
{input}
|
|
496
|
+
</label>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return <div>{input}</div>;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// TODO(thomas): Currently we receive a list of lists of acceptable answer types
|
|
504
|
+
// and union them down into a single set. It's worth considering whether it
|
|
505
|
+
// wouldn't make more sense to have a single set of acceptable answer types for
|
|
506
|
+
// a given *problem* rather than for each possible [correct/wrong] *answer*.
|
|
507
|
+
// When should two answers to a problem take different answer types?
|
|
508
|
+
// See D27790 for more discussion.
|
|
509
|
+
export const unionAnswerForms: ($FlowFixMe) => $FlowFixMe = function (
|
|
510
|
+
answerFormsList,
|
|
511
|
+
) {
|
|
512
|
+
// Takes a list of lists of answer forms, and returns a list of the forms
|
|
513
|
+
// in each of these lists in the same order that they're listed in the
|
|
514
|
+
// `formExamples` forms from above.
|
|
515
|
+
|
|
516
|
+
// uniqueBy takes a list of elements and a function which compares whether
|
|
517
|
+
// two elements are equal, and returns a list of unique elements. This is
|
|
518
|
+
// just a helper function here, but works generally.
|
|
519
|
+
const uniqueBy = function (list, iteratee) {
|
|
520
|
+
return _.reduce(
|
|
521
|
+
list,
|
|
522
|
+
(uniqueList, element) => {
|
|
523
|
+
// For each element, decide whether it's already in the list of
|
|
524
|
+
// unique items.
|
|
525
|
+
const inList = _.find(uniqueList, iteratee.bind(null, element));
|
|
526
|
+
if (inList) {
|
|
527
|
+
return uniqueList;
|
|
528
|
+
}
|
|
529
|
+
return uniqueList.concat([element]);
|
|
530
|
+
},
|
|
531
|
+
[],
|
|
532
|
+
);
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Pull out all of the forms from the different lists.
|
|
536
|
+
const allForms = _.flatten(answerFormsList);
|
|
537
|
+
// Pull out the unique forms using uniqueBy.
|
|
538
|
+
const uniqueForms = uniqueBy(allForms, _.isEqual);
|
|
539
|
+
// Sort them by the order they appear in the `formExamples` list.
|
|
540
|
+
return _.sortBy(uniqueForms, (form) => {
|
|
541
|
+
return _.keys(formExamples).indexOf(form.name);
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
type RenderProps = {|
|
|
546
|
+
answerForms: $ReadOnlyArray<{|
|
|
547
|
+
simplify: ?("required" | "correct" | "enforced"),
|
|
548
|
+
name: "integer" | "decimal" | "proper" | "improper" | "mixed" | "pi",
|
|
549
|
+
|}>,
|
|
550
|
+
labelText: string,
|
|
551
|
+
size: string,
|
|
552
|
+
coefficient: boolean,
|
|
553
|
+
rightAlign?: boolean,
|
|
554
|
+
static: boolean,
|
|
555
|
+
|};
|
|
556
|
+
|
|
557
|
+
// This function checks if the user inputted a percent value, parsing
|
|
558
|
+
// it as a number (and maybe scaling) so that it can be graded.
|
|
559
|
+
// NOTE(michaelpolyak): Unlike `KhanAnswerTypes.number.percent()` which
|
|
560
|
+
// can accept several input forms with or without "%", the decision
|
|
561
|
+
// to parse based on the presence of "%" in the input, is so that we
|
|
562
|
+
// don't accidently scale the user typed value before grading, CP-930.
|
|
563
|
+
export const maybeParsePercentInput: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
|
|
564
|
+
inputValue,
|
|
565
|
+
normalizedAnswerExpected,
|
|
566
|
+
) => {
|
|
567
|
+
// If the input value is not a string ending with "%", then there's
|
|
568
|
+
// nothing more to do. The value will be graded as inputted by user.
|
|
569
|
+
if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
|
|
570
|
+
return inputValue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const value = parseFloat(inputValue.slice(0, -1));
|
|
574
|
+
// If the input value stripped of the "%" cannot be parsed as a
|
|
575
|
+
// number (the slice is not really necessary for parseFloat to work
|
|
576
|
+
// if the string starts with a number) then return the original
|
|
577
|
+
// input for grading.
|
|
578
|
+
if (isNaN(value)) {
|
|
579
|
+
return inputValue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Next, if all correct answers are in the range of |0,1| then we
|
|
583
|
+
// scale the user typed value. We assume this is the correct thing
|
|
584
|
+
// to do since the input value ends with "%".
|
|
585
|
+
if (normalizedAnswerExpected) {
|
|
586
|
+
return value / 100;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Otherwise, we return input valuåe (number) stripped of the "%".
|
|
590
|
+
return value;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const propsTransform = function (
|
|
594
|
+
widgetOptions: PerseusNumericInputWidgetOptions,
|
|
595
|
+
): RenderProps {
|
|
596
|
+
const rendererProps = _.extend(_.omit(widgetOptions, "answers"), {
|
|
597
|
+
answerForms: unionAnswerForms(
|
|
598
|
+
// Pull out the name of each form and whether that form has
|
|
599
|
+
// required simplification.
|
|
600
|
+
_.map(widgetOptions.answers, (answer) => {
|
|
601
|
+
return _.map(answer.answerForms, (form) => {
|
|
602
|
+
return {
|
|
603
|
+
simplify: answer.simplify,
|
|
604
|
+
name: form,
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
}),
|
|
608
|
+
),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return rendererProps;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
export default ({
|
|
615
|
+
name: "numeric-input",
|
|
616
|
+
displayName: "Number text box",
|
|
617
|
+
defaultAlignment: "inline-block",
|
|
618
|
+
accessible: true,
|
|
619
|
+
widget: NumericInput,
|
|
620
|
+
transform: propsTransform,
|
|
621
|
+
isLintable: true,
|
|
622
|
+
}: WidgetExports<typeof NumericInput>);
|