@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,841 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Direct image labeling widget.
|
|
4
|
+
*
|
|
5
|
+
* Label on image widget enables creating more natural, conceptual questions
|
|
6
|
+
* that involve the use of images, and enable learners to demonstrate their
|
|
7
|
+
* knowledge by directly interacting with the image.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
11
|
+
import {Popover, PopoverContentCore} from "@khanacademy/wonder-blocks-popover";
|
|
12
|
+
import {StyleSheet, css} from "aphrodite";
|
|
13
|
+
import classNames from "classnames";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import ReactDOM from "react-dom";
|
|
16
|
+
|
|
17
|
+
import AssetContext from "../asset-context.js";
|
|
18
|
+
import SvgImage from "../components/svg-image.jsx";
|
|
19
|
+
import Renderer from "../renderer.jsx";
|
|
20
|
+
import {typography} from "../styles/global-styles.js";
|
|
21
|
+
import mediaQueries from "../styles/media-queries.js";
|
|
22
|
+
|
|
23
|
+
import AnswerChoices from "./label-image/answer-choices.jsx";
|
|
24
|
+
import Marker from "./label-image/marker.jsx";
|
|
25
|
+
|
|
26
|
+
import type {ChangeableProps} from "../mixins/changeable.jsx";
|
|
27
|
+
import type {APIOptions, PerseusScore, WidgetExports} from "../types.js";
|
|
28
|
+
import type {
|
|
29
|
+
InteractiveMarkerType,
|
|
30
|
+
InteractiveMarkerScore,
|
|
31
|
+
} from "./label-image/types.js";
|
|
32
|
+
|
|
33
|
+
type MarkersState = {|markers: $ReadOnlyArray<InteractiveMarkerType>|};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents a direction vector.
|
|
37
|
+
* No diagonals allowed.
|
|
38
|
+
*/
|
|
39
|
+
type Direction =
|
|
40
|
+
| {|
|
|
41
|
+
x: 0,
|
|
42
|
+
y: 1 | -1,
|
|
43
|
+
|}
|
|
44
|
+
| {|
|
|
45
|
+
x: 1 | -1,
|
|
46
|
+
y: 0,
|
|
47
|
+
|}
|
|
48
|
+
| {|
|
|
49
|
+
x: 0,
|
|
50
|
+
y: 0,
|
|
51
|
+
|};
|
|
52
|
+
|
|
53
|
+
type Point = {|
|
|
54
|
+
x: number,
|
|
55
|
+
y: number,
|
|
56
|
+
|};
|
|
57
|
+
|
|
58
|
+
type LabelImageProps = {|
|
|
59
|
+
...ChangeableProps,
|
|
60
|
+
apiOptions: APIOptions,
|
|
61
|
+
// The list of possible answer choices.
|
|
62
|
+
choices: $ReadOnlyArray<string>,
|
|
63
|
+
// The question image properties.
|
|
64
|
+
imageAlt: string,
|
|
65
|
+
imageUrl: string,
|
|
66
|
+
imageWidth: number,
|
|
67
|
+
imageHeight: number,
|
|
68
|
+
// The list of label markers on the question image.
|
|
69
|
+
markers: $ReadOnlyArray<InteractiveMarkerType>,
|
|
70
|
+
// Whether multiple answer choices may be selected for markers.
|
|
71
|
+
multipleAnswers: boolean,
|
|
72
|
+
// Whether to hide answer choices from user instructions.
|
|
73
|
+
hideChoicesFromInstructions: boolean,
|
|
74
|
+
// Whether the question has been answered by the user.
|
|
75
|
+
questionCompleted: boolean,
|
|
76
|
+
|};
|
|
77
|
+
|
|
78
|
+
type LabelImageState = {|
|
|
79
|
+
// The user selected marker index, defaults to -1, no selection.
|
|
80
|
+
selectedMarkerIndex: number,
|
|
81
|
+
|
|
82
|
+
// Whether any of the markers were interacted with by the user.
|
|
83
|
+
markersInteracted: boolean,
|
|
84
|
+
|};
|
|
85
|
+
|
|
86
|
+
class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
87
|
+
// The rendered markers on the question image for labeling.
|
|
88
|
+
// eslint-disable-next-line flowtype/no-mutable-array
|
|
89
|
+
_markers: Array<?Marker>;
|
|
90
|
+
|
|
91
|
+
// The popup component containing the answer choices.
|
|
92
|
+
_selectedMarkerPopup: ?Popover;
|
|
93
|
+
|
|
94
|
+
// The rendered list of answer choices for the currently selected marker.
|
|
95
|
+
_answerChoices: ?AnswerChoices;
|
|
96
|
+
|
|
97
|
+
static gradeMarker(marker: InteractiveMarkerType): InteractiveMarkerScore {
|
|
98
|
+
const score = {
|
|
99
|
+
hasAnswers: false,
|
|
100
|
+
isCorrect: false,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (marker.selected && marker.selected.length > 0) {
|
|
104
|
+
score.hasAnswers = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (marker.answers.length > 0) {
|
|
108
|
+
if (
|
|
109
|
+
marker.selected &&
|
|
110
|
+
marker.selected.length === marker.answers.length
|
|
111
|
+
) {
|
|
112
|
+
// All correct answers are selected by the user.
|
|
113
|
+
score.isCorrect = marker.selected.every((choice) =>
|
|
114
|
+
marker.answers.includes(choice),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else if (!marker.selected || marker.selected.length === 0) {
|
|
118
|
+
// Correct as no answers should be selected by the user.
|
|
119
|
+
score.isCorrect = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return score;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static validate(
|
|
126
|
+
state: MarkersState,
|
|
127
|
+
rubric?: LabelImageProps,
|
|
128
|
+
): PerseusScore {
|
|
129
|
+
let numAnswered = 0;
|
|
130
|
+
let numCorrect = 0;
|
|
131
|
+
|
|
132
|
+
for (const marker of state.markers) {
|
|
133
|
+
const score = LabelImage.gradeMarker(marker);
|
|
134
|
+
|
|
135
|
+
if (score.hasAnswers) {
|
|
136
|
+
numAnswered++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (score.isCorrect) {
|
|
140
|
+
numCorrect++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// We expect all question markers to be answered before grading.
|
|
145
|
+
if (numAnswered !== state.markers.length) {
|
|
146
|
+
return {
|
|
147
|
+
type: "invalid",
|
|
148
|
+
message: null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
type: "points",
|
|
154
|
+
// Markers with no expected answers are graded as correct if user
|
|
155
|
+
// makes no answer selection.
|
|
156
|
+
earned: numCorrect === state.markers.length ? 1 : 0,
|
|
157
|
+
total: 1,
|
|
158
|
+
message: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Test whether point is contained within triangle.
|
|
164
|
+
*
|
|
165
|
+
* Implementation taken from: https://stackoverflow.com/a/2049593
|
|
166
|
+
*/
|
|
167
|
+
static pointInTriangle(p: Point, a: Point, b: Point, c: Point): boolean {
|
|
168
|
+
const sign = (p1, p2, p3) =>
|
|
169
|
+
(p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
|
|
170
|
+
|
|
171
|
+
const b1 = sign(p, a, b) < 0;
|
|
172
|
+
const b2 = sign(p, b, c) < 0;
|
|
173
|
+
const b3 = sign(p, c, a) < 0;
|
|
174
|
+
|
|
175
|
+
return b1 === b2 && b2 === b3;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Determine the image side given a marker position (as percent of size).
|
|
180
|
+
*/
|
|
181
|
+
static imageSideForMarkerPosition(
|
|
182
|
+
x: number,
|
|
183
|
+
y: number,
|
|
184
|
+
): "bottom" | "left" | "right" | "top" | "center" {
|
|
185
|
+
// Special handling for when marker is positioned near the horizontal
|
|
186
|
+
// edges of the image. We want to ensure the returned side would not
|
|
187
|
+
// result in a popup rendering that may overflow outside the page.
|
|
188
|
+
if (x < 20) {
|
|
189
|
+
return "left";
|
|
190
|
+
}
|
|
191
|
+
if (x > 80) {
|
|
192
|
+
return "right";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Points to define corners (and center) of a rectangle representing
|
|
196
|
+
// the constrained area of the image for which we want to determine the
|
|
197
|
+
// image side.
|
|
198
|
+
const tl = {x: 20, y: 0};
|
|
199
|
+
const tr = {x: 80, y: 0};
|
|
200
|
+
const br = {x: 80, y: 100};
|
|
201
|
+
const bl = {x: 20, y: 100};
|
|
202
|
+
const cp = {x: 50, y: 50};
|
|
203
|
+
|
|
204
|
+
// The triangles representing the sides to test.
|
|
205
|
+
const triangles = {
|
|
206
|
+
top: [tl, tr, cp],
|
|
207
|
+
right: [cp, tr, br],
|
|
208
|
+
bottom: [bl, cp, br],
|
|
209
|
+
left: [tl, cp, bl],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const p = {x, y};
|
|
213
|
+
|
|
214
|
+
// Test whether marker is positioned within one of the triangles
|
|
215
|
+
// representing the sides.
|
|
216
|
+
for (const side of Object.keys(triangles)) {
|
|
217
|
+
const corners = triangles[side];
|
|
218
|
+
|
|
219
|
+
if (LabelImage.pointInTriangle(p, ...corners)) {
|
|
220
|
+
return side;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fallback, if side was not found for marker position.
|
|
225
|
+
return "center";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Calculate the next marker to navigate to, from the "this marker".
|
|
230
|
+
*
|
|
231
|
+
* Given a cardinal navigation direction (in the x or y axis), return the
|
|
232
|
+
* next marker index to visit.
|
|
233
|
+
*/
|
|
234
|
+
static navigateToMarkerIndex(
|
|
235
|
+
navigateDirection: Direction,
|
|
236
|
+
markers: $ReadOnlyArray<InteractiveMarkerType>,
|
|
237
|
+
thisIndex: number,
|
|
238
|
+
): number {
|
|
239
|
+
const thisMarker = markers[thisIndex];
|
|
240
|
+
|
|
241
|
+
// Sort markers along the direction of navigation from "this" marker.
|
|
242
|
+
const sortedMarkers = markers
|
|
243
|
+
.map((otherMarker, index) => {
|
|
244
|
+
// Calculate distance and (normalized) vector to the marker from
|
|
245
|
+
// "this" marker.
|
|
246
|
+
const x = otherMarker.x - thisMarker.x;
|
|
247
|
+
const y = otherMarker.y - thisMarker.y;
|
|
248
|
+
|
|
249
|
+
const dist = Math.sqrt(x ** 2 + y ** 2);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
index,
|
|
253
|
+
dist,
|
|
254
|
+
dir: {
|
|
255
|
+
x: dist !== 0 ? x / dist : 0,
|
|
256
|
+
y: dist !== 0 ? y / dist : 0,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
})
|
|
260
|
+
.filter((marker) => {
|
|
261
|
+
if (marker.index === thisIndex) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Exclude any marker that has been answered correctly,
|
|
266
|
+
// as it should no longer be interacted with by the user.
|
|
267
|
+
return markers[marker.index].showCorrectness !== "correct";
|
|
268
|
+
})
|
|
269
|
+
.sort((a, b) => {
|
|
270
|
+
// Project distances to the two markers being sorted (from the
|
|
271
|
+
// "this" marker) along the direction of navigation. This will
|
|
272
|
+
// order by the "closest" marker in the direction of navigation,
|
|
273
|
+
// not the "absolute" closest marker to "this" marker.
|
|
274
|
+
// We round the calculated distances so that if markers are
|
|
275
|
+
// found to be positioned "close enough" to the "this" marker,
|
|
276
|
+
// they will be treated as coplanar with it.
|
|
277
|
+
const distA = Math.round(
|
|
278
|
+
a.dist *
|
|
279
|
+
(navigateDirection.x * a.dir.x +
|
|
280
|
+
navigateDirection.y * a.dir.y),
|
|
281
|
+
);
|
|
282
|
+
const distB = Math.round(
|
|
283
|
+
b.dist *
|
|
284
|
+
(navigateDirection.x * b.dir.x +
|
|
285
|
+
navigateDirection.y * b.dir.y),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
let dirA;
|
|
289
|
+
let dirB;
|
|
290
|
+
// Determine whether the two markers lie ahead or behind the
|
|
291
|
+
// "this" marker. A marker that lies in the same plane as the
|
|
292
|
+
// "this" marker will be considered to be behind it.
|
|
293
|
+
// Only navigation in the "x" or "y" axis is supported, no
|
|
294
|
+
// diagonals.
|
|
295
|
+
if (navigateDirection.x > 0) {
|
|
296
|
+
dirA = a.dir.x > 0 && distA !== 0;
|
|
297
|
+
dirB = b.dir.x > 0 && distB !== 0;
|
|
298
|
+
} else if (navigateDirection.x < 0) {
|
|
299
|
+
dirA = a.dir.x < 0 && distA !== 0;
|
|
300
|
+
dirB = b.dir.x < 0 && distB !== 0;
|
|
301
|
+
} else if (navigateDirection.y > 0) {
|
|
302
|
+
dirA = a.dir.y > 0 && distA !== 0;
|
|
303
|
+
dirB = b.dir.y > 0 && distB !== 0;
|
|
304
|
+
} else if (navigateDirection.y < 0) {
|
|
305
|
+
dirA = a.dir.y < 0 && distA !== 0;
|
|
306
|
+
dirB = b.dir.y < 0 && distB !== 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// If one of the markers is positioned behind, sort the other
|
|
310
|
+
// marker ahead of it in the order.
|
|
311
|
+
if (dirA !== dirB) {
|
|
312
|
+
if (dirA) {
|
|
313
|
+
return -1;
|
|
314
|
+
}
|
|
315
|
+
return 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Both markers are either positioned ahead or behind, sort the
|
|
319
|
+
// closest (or furthest if behind, to "wrap" navigation) marker
|
|
320
|
+
// ahead in the order.
|
|
321
|
+
return distA - distB;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Return the index in the provided markers array of the closest marker
|
|
325
|
+
// found along the direction of navigation, or "this" index if no other
|
|
326
|
+
// marker was found. This may happen if the user has answered all but
|
|
327
|
+
// one ("this" marker) of the markers correctly in the question.
|
|
328
|
+
return sortedMarkers.length > 0 ? sortedMarkers[0].index : thisIndex;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
constructor(props: LabelImageProps) {
|
|
332
|
+
super(props);
|
|
333
|
+
|
|
334
|
+
this._markers = [];
|
|
335
|
+
|
|
336
|
+
this.state = {
|
|
337
|
+
selectedMarkerIndex: -1,
|
|
338
|
+
markersInteracted: false,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
componentDidMount() {
|
|
343
|
+
document.addEventListener("click", this.handleDocumentClick, true);
|
|
344
|
+
document.addEventListener("keydown", this.handleDocumentKeyDown, true);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
componentWillUnmount() {
|
|
348
|
+
document.removeEventListener("click", this.handleDocumentClick, true);
|
|
349
|
+
document.removeEventListener(
|
|
350
|
+
"keydown",
|
|
351
|
+
this.handleDocumentKeyDown,
|
|
352
|
+
true,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
simpleValidate(rubric: LabelImageProps): PerseusScore {
|
|
357
|
+
return LabelImage.validate(this.getUserInput(), rubric);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getUserInput(): MarkersState {
|
|
361
|
+
const {markers} = this.props;
|
|
362
|
+
return {markers};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
showRationalesForCurrentlySelectedChoices(rubric: LabelImageProps) {
|
|
366
|
+
const {markers} = this.props;
|
|
367
|
+
const {onChange} = this.props;
|
|
368
|
+
|
|
369
|
+
const updatedMarkers = markers.map((marker) => {
|
|
370
|
+
const score = LabelImage.gradeMarker(marker);
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
...marker,
|
|
374
|
+
// Reveal correctness state for markers with answers.
|
|
375
|
+
showCorrectness: score.hasAnswers
|
|
376
|
+
? score.isCorrect
|
|
377
|
+
? "correct"
|
|
378
|
+
: "incorrect"
|
|
379
|
+
: undefined,
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Update Perseus widget state with user selected answers without
|
|
384
|
+
// triggering interaction events for listeners.
|
|
385
|
+
onChange({markers: updatedMarkers}, null, true);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
dismissMarkerPopup() {
|
|
389
|
+
const {selectedMarkerIndex: index} = this.state;
|
|
390
|
+
|
|
391
|
+
// No popup should be open if there's no selected marker.
|
|
392
|
+
if (index === -1) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.setState({selectedMarkerIndex: -1}, () => {
|
|
397
|
+
const marker = this._markers[index];
|
|
398
|
+
// Set focus on the just-deselected-marker, to enable to resume
|
|
399
|
+
// navigating between the markers using the keyboard.
|
|
400
|
+
if (marker) {
|
|
401
|
+
((ReactDOM.findDOMNode(marker): any): HTMLElement).focus();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
handleDocumentClick: (e: MouseEvent) => void = (e: MouseEvent) => {
|
|
407
|
+
// Dismiss open popup with answer choices if user clicks outside it.
|
|
408
|
+
if (this._selectedMarkerPopup && this._answerChoices) {
|
|
409
|
+
const answerChoices = ReactDOM.findDOMNode(this._answerChoices);
|
|
410
|
+
|
|
411
|
+
const targetElement = ((e.target: any): Node);
|
|
412
|
+
// HACK(michaelpolyak): We want to determine if the click target is
|
|
413
|
+
// contained within the popup layer. As there's no public interface
|
|
414
|
+
// to get this layer from the popup, we traverse several levels of
|
|
415
|
+
// answer choices parents to test whether the click target is
|
|
416
|
+
// contained within.
|
|
417
|
+
const containsEventTarget = (element, depth = 3) =>
|
|
418
|
+
element &&
|
|
419
|
+
(element.contains(targetElement) ||
|
|
420
|
+
(depth > 0 &&
|
|
421
|
+
containsEventTarget(element.parentElement, depth - 1)));
|
|
422
|
+
|
|
423
|
+
if (!containsEventTarget(answerChoices)) {
|
|
424
|
+
// Close popup and set focus back to the marker for which it was
|
|
425
|
+
// open.
|
|
426
|
+
this.dismissMarkerPopup();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
handleDocumentKeyDown: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
|
|
432
|
+
// Dismiss open popup with answer choices if user presses Escape key.
|
|
433
|
+
if (this._selectedMarkerPopup && e.keyCode === 27) {
|
|
434
|
+
// Ensure other listeners are not triggered on key down event that
|
|
435
|
+
// closes the popup, as this will also dismiss the modal that the
|
|
436
|
+
// widget may be rendered within.
|
|
437
|
+
e.stopPropagation();
|
|
438
|
+
|
|
439
|
+
// Close popup and set focus back to the marker for which it was
|
|
440
|
+
// open.
|
|
441
|
+
this.dismissMarkerPopup();
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
handleMarkerChange(index: number, marker: InteractiveMarkerType) {
|
|
446
|
+
const {markers, onChange} = this.props;
|
|
447
|
+
|
|
448
|
+
// Replace marker with a changed version at the specified index.
|
|
449
|
+
const updatedMarkers = [
|
|
450
|
+
...markers.slice(0, index),
|
|
451
|
+
{
|
|
452
|
+
...marker,
|
|
453
|
+
// Do not show correctness state if user changes answer
|
|
454
|
+
// selection for marker.
|
|
455
|
+
showCorrectness: undefined,
|
|
456
|
+
},
|
|
457
|
+
...markers.slice(index + 1),
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
// Update Perseus widget state with user selected answers.
|
|
461
|
+
onChange({markers: updatedMarkers});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
handleMarkerClick(index: number, e: MouseEvent) {
|
|
465
|
+
const {selectedMarkerIndex} = this.state;
|
|
466
|
+
|
|
467
|
+
e.preventDefault();
|
|
468
|
+
|
|
469
|
+
// Select the marker, revealing answer choices.
|
|
470
|
+
if (selectedMarkerIndex !== index) {
|
|
471
|
+
this.setState(
|
|
472
|
+
{
|
|
473
|
+
selectedMarkerIndex: index,
|
|
474
|
+
markersInteracted: true,
|
|
475
|
+
},
|
|
476
|
+
() => {
|
|
477
|
+
// A delay is required to allow answer choices to first render
|
|
478
|
+
// before trying to set focus on the first choice.
|
|
479
|
+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
480
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
481
|
+
setTimeout(() => {
|
|
482
|
+
if (this._answerChoices) {
|
|
483
|
+
this._answerChoices.focusAnswer();
|
|
484
|
+
}
|
|
485
|
+
}, 100);
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
} else {
|
|
489
|
+
// The answer choices are shown within a popup attached to the
|
|
490
|
+
// selected marker. Close the popup and set focus back to the
|
|
491
|
+
// marker.
|
|
492
|
+
this.dismissMarkerPopup();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
handleMarkerKeyDown(index: number, e: KeyboardEvent) {
|
|
497
|
+
const {markers} = this.props;
|
|
498
|
+
|
|
499
|
+
// One is the loneliest number.
|
|
500
|
+
if (markers.length < 2) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Only navigation in the "x" or "y" axis is supported, no diagonals.
|
|
505
|
+
const navigateDirection = {
|
|
506
|
+
ArrowUp: {x: 0, y: -1},
|
|
507
|
+
ArrowRight: {x: 1, y: 0},
|
|
508
|
+
ArrowDown: {x: 0, y: 1},
|
|
509
|
+
ArrowLeft: {x: -1, y: 0},
|
|
510
|
+
}[e.key];
|
|
511
|
+
|
|
512
|
+
if (!navigateDirection) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
e.preventDefault();
|
|
517
|
+
|
|
518
|
+
// Focus on the closest marker along the direction of navigation.
|
|
519
|
+
const marker =
|
|
520
|
+
this._markers[
|
|
521
|
+
LabelImage.navigateToMarkerIndex(
|
|
522
|
+
navigateDirection,
|
|
523
|
+
markers,
|
|
524
|
+
index,
|
|
525
|
+
)
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
if (marker) {
|
|
529
|
+
((ReactDOM.findDOMNode(marker): any): HTMLElement).focus();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
handleAnswerChoicesChangeForMarker(
|
|
534
|
+
index: number,
|
|
535
|
+
selection: $ReadOnlyArray<boolean>,
|
|
536
|
+
) {
|
|
537
|
+
const {choices, markers, multipleAnswers} = this.props;
|
|
538
|
+
|
|
539
|
+
// Compile the user selected answer choices.
|
|
540
|
+
const selected = choices.filter((choice, index) => selection[index]);
|
|
541
|
+
|
|
542
|
+
this.handleMarkerChange(index, {
|
|
543
|
+
...markers[index],
|
|
544
|
+
selected,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (!multipleAnswers) {
|
|
548
|
+
this.dismissMarkerPopup();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
renderAnswerChoicesForMarker(
|
|
553
|
+
index: number,
|
|
554
|
+
marker: InteractiveMarkerType,
|
|
555
|
+
): React.Element<"div"> {
|
|
556
|
+
const {choices, multipleAnswers} = this.props;
|
|
557
|
+
|
|
558
|
+
// The user selected answer choices.
|
|
559
|
+
const selected = marker.selected;
|
|
560
|
+
|
|
561
|
+
return (
|
|
562
|
+
<div
|
|
563
|
+
className={classNames(
|
|
564
|
+
"perseus-label-image-widget-answer-choices",
|
|
565
|
+
css(styles.scrollableChoices),
|
|
566
|
+
)}
|
|
567
|
+
>
|
|
568
|
+
<AnswerChoices
|
|
569
|
+
choices={choices.map((choice) => ({
|
|
570
|
+
content: choice,
|
|
571
|
+
checked: selected ? selected.includes(choice) : false,
|
|
572
|
+
}))}
|
|
573
|
+
multipleSelect={multipleAnswers}
|
|
574
|
+
onChange={(selection) =>
|
|
575
|
+
this.handleAnswerChoicesChangeForMarker(
|
|
576
|
+
index,
|
|
577
|
+
selection,
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
ref={(node) => (this._answerChoices = node)}
|
|
581
|
+
/>
|
|
582
|
+
</div>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
renderMarkers(): $ReadOnlyArray<React.Node> {
|
|
587
|
+
const {markers, questionCompleted} = this.props;
|
|
588
|
+
|
|
589
|
+
const {selectedMarkerIndex, markersInteracted} = this.state;
|
|
590
|
+
|
|
591
|
+
// Render all markers for widget.
|
|
592
|
+
return markers.map((marker, index): React.Node => {
|
|
593
|
+
const score = LabelImage.gradeMarker(marker);
|
|
594
|
+
|
|
595
|
+
const element = (
|
|
596
|
+
<Marker
|
|
597
|
+
{...marker}
|
|
598
|
+
// Once the question is answered, show markers
|
|
599
|
+
// with correct answers, otherwise passthrough
|
|
600
|
+
// the correctness state.
|
|
601
|
+
showCorrectness={
|
|
602
|
+
questionCompleted && score.hasAnswers && score.isCorrect
|
|
603
|
+
? "correct"
|
|
604
|
+
: marker.showCorrectness
|
|
605
|
+
}
|
|
606
|
+
showSelected={index === selectedMarkerIndex}
|
|
607
|
+
showPulsate={!markersInteracted}
|
|
608
|
+
key={`${marker.x}.${marker.y}`}
|
|
609
|
+
onClick={(e) => this.handleMarkerClick(index, e)}
|
|
610
|
+
onKeyDown={(e) => this.handleMarkerKeyDown(index, e)}
|
|
611
|
+
ref={(node) => (this._markers[index] = node)}
|
|
612
|
+
/>
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// The user selected marker is wrapped with a popup that shows its
|
|
616
|
+
// answer choices, otherwise it's returned as is.
|
|
617
|
+
if (index !== selectedMarkerIndex) {
|
|
618
|
+
return element;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Determine whether page is rendered in a narrow browser window.
|
|
622
|
+
const isNarrowPage = window.matchMedia(
|
|
623
|
+
mediaQueries.xsOrSmaller.replace("@media ", ""),
|
|
624
|
+
).matches;
|
|
625
|
+
|
|
626
|
+
let side;
|
|
627
|
+
// Position popup closest to the center, preferring it renders
|
|
628
|
+
// entirely within the image area.
|
|
629
|
+
if (isNarrowPage) {
|
|
630
|
+
side = marker.y > 50 ? "top" : "bottom";
|
|
631
|
+
} else {
|
|
632
|
+
// This mirrors the calculated side of where the marker is
|
|
633
|
+
// located within the image, so that popup appears closer to
|
|
634
|
+
// the image center.
|
|
635
|
+
side = {
|
|
636
|
+
left: "right",
|
|
637
|
+
top: "bottom",
|
|
638
|
+
right: "left",
|
|
639
|
+
bottom: "top",
|
|
640
|
+
center: "top",
|
|
641
|
+
}[LabelImage.imageSideForMarkerPosition(marker.x, marker.y)];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// TODO(michaelpolyak): Ideally we would always render markers
|
|
645
|
+
// wrapped in the popover. Setting `opened={false}` for those
|
|
646
|
+
// markers that are unselected (to hide their popup), this would
|
|
647
|
+
// keep the React tree more stable.
|
|
648
|
+
return (
|
|
649
|
+
<Popover
|
|
650
|
+
content={() => (
|
|
651
|
+
<PopoverContentCore style={styles.choicesPopover}>
|
|
652
|
+
{this.renderAnswerChoicesForMarker(index, marker)}
|
|
653
|
+
</PopoverContentCore>
|
|
654
|
+
)}
|
|
655
|
+
placement={side}
|
|
656
|
+
opened={true}
|
|
657
|
+
key={`${marker.x}.${marker.y}`}
|
|
658
|
+
ref={(node) => (this._selectedMarkerPopup = node)}
|
|
659
|
+
>
|
|
660
|
+
{element}
|
|
661
|
+
</Popover>
|
|
662
|
+
);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
renderInstructions(): React.Node {
|
|
667
|
+
const {
|
|
668
|
+
apiOptions: {isMobile},
|
|
669
|
+
choices,
|
|
670
|
+
multipleAnswers,
|
|
671
|
+
hideChoicesFromInstructions: hideChoices,
|
|
672
|
+
} = this.props;
|
|
673
|
+
|
|
674
|
+
const promptString = isMobile
|
|
675
|
+
? multipleAnswers
|
|
676
|
+
? i18n._(
|
|
677
|
+
"Tap each dot on the image to select all answers that apply.",
|
|
678
|
+
)
|
|
679
|
+
: i18n._("Tap each dot on the image to select an answer.")
|
|
680
|
+
: multipleAnswers
|
|
681
|
+
? i18n._(
|
|
682
|
+
"Click each dot on the image to select all answers that apply.",
|
|
683
|
+
)
|
|
684
|
+
: i18n._("Click each dot on the image to select an answer.");
|
|
685
|
+
|
|
686
|
+
const choicesString = i18n._("Choices:");
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
<div
|
|
690
|
+
className={classNames(
|
|
691
|
+
"perseus-label-image-widget-instructions",
|
|
692
|
+
css(styles.instructions),
|
|
693
|
+
)}
|
|
694
|
+
>
|
|
695
|
+
<div className={css(styles.instructionsCaption)}>
|
|
696
|
+
{promptString} {!hideChoices && choicesString}
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
{!hideChoices && (
|
|
700
|
+
<div className={css(styles.instructionsChoices)}>
|
|
701
|
+
{choices.map((choice, index) => (
|
|
702
|
+
<div
|
|
703
|
+
className={css(styles.instructionsChoice)}
|
|
704
|
+
key={index}
|
|
705
|
+
>
|
|
706
|
+
<Renderer content={choice} />
|
|
707
|
+
</div>
|
|
708
|
+
))}
|
|
709
|
+
</div>
|
|
710
|
+
)}
|
|
711
|
+
</div>
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
render(): React.Node {
|
|
716
|
+
const {imageAlt, imageUrl, imageWidth, imageHeight} = this.props;
|
|
717
|
+
|
|
718
|
+
const {selectedMarkerIndex} = this.state;
|
|
719
|
+
|
|
720
|
+
return (
|
|
721
|
+
<div>
|
|
722
|
+
{this.renderInstructions()}
|
|
723
|
+
<div
|
|
724
|
+
className={css(styles.markersCanvas)}
|
|
725
|
+
style={{
|
|
726
|
+
maxWidth: imageWidth,
|
|
727
|
+
maxHeight: imageHeight,
|
|
728
|
+
}}
|
|
729
|
+
>
|
|
730
|
+
<div
|
|
731
|
+
className={css(
|
|
732
|
+
styles.imageContainer,
|
|
733
|
+
// Ensure image interaction is disabled while answer
|
|
734
|
+
// choices popup is visible. This addresses specific
|
|
735
|
+
// case where user may click on question image to
|
|
736
|
+
// dismiss the popup. If the image is larger in size
|
|
737
|
+
// than its rendered in the widget, this would
|
|
738
|
+
// result in a zoom of the image.
|
|
739
|
+
selectedMarkerIndex !== -1 &&
|
|
740
|
+
styles.imageInteractionDisabled,
|
|
741
|
+
)}
|
|
742
|
+
>
|
|
743
|
+
<AssetContext.Consumer>
|
|
744
|
+
{({setAssetStatus}) => (
|
|
745
|
+
<SvgImage
|
|
746
|
+
alt={imageAlt}
|
|
747
|
+
src={imageUrl}
|
|
748
|
+
width={imageWidth}
|
|
749
|
+
height={imageHeight}
|
|
750
|
+
setAssetStatus={setAssetStatus}
|
|
751
|
+
/>
|
|
752
|
+
)}
|
|
753
|
+
</AssetContext.Consumer>
|
|
754
|
+
</div>
|
|
755
|
+
{this.renderMarkers()}
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const styles = StyleSheet.create({
|
|
763
|
+
instructions: {
|
|
764
|
+
paddingBottom: 16,
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
instructionsCaption: {
|
|
768
|
+
...typography.bodyXsmallBold,
|
|
769
|
+
|
|
770
|
+
paddingBottom: 16,
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
instructionsChoices: {
|
|
774
|
+
display: "flex",
|
|
775
|
+
flexWrap: "wrap",
|
|
776
|
+
|
|
777
|
+
margin: "-8px 0",
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
instructionsChoice: {
|
|
781
|
+
display: "flex",
|
|
782
|
+
alignItems: "center",
|
|
783
|
+
|
|
784
|
+
margin: "8px 0",
|
|
785
|
+
|
|
786
|
+
":not(:last-child)": {
|
|
787
|
+
"::after": {
|
|
788
|
+
content: "''",
|
|
789
|
+
display: "inline-block",
|
|
790
|
+
position: "relative",
|
|
791
|
+
|
|
792
|
+
width: 2,
|
|
793
|
+
height: 2,
|
|
794
|
+
|
|
795
|
+
marginLeft: 5,
|
|
796
|
+
marginRight: 5,
|
|
797
|
+
|
|
798
|
+
background: "rgba(33, 36, 44, 0.32)",
|
|
799
|
+
|
|
800
|
+
borderRadius: 2,
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
|
|
805
|
+
markersCanvas: {
|
|
806
|
+
position: "relative",
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
imageContainer: {
|
|
810
|
+
// Remove the additional height added due to white-spacing, this ensures
|
|
811
|
+
// markers canvas will have the same height as the question image.
|
|
812
|
+
display: "flex",
|
|
813
|
+
},
|
|
814
|
+
|
|
815
|
+
imageInteractionDisabled: {
|
|
816
|
+
pointerEvents: "none",
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
scrollableChoices: {
|
|
820
|
+
paddingTop: 8,
|
|
821
|
+
paddingBottom: 8,
|
|
822
|
+
|
|
823
|
+
// Enable scroll for answer choices within popup on mobile phones.
|
|
824
|
+
[mediaQueries.xsOrSmaller]: {
|
|
825
|
+
maxHeight: 250,
|
|
826
|
+
overflowY: "auto",
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
|
|
830
|
+
choicesPopover: {
|
|
831
|
+
padding: 0,
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
export default ({
|
|
836
|
+
name: "label-image",
|
|
837
|
+
displayName: "Label Image",
|
|
838
|
+
widget: LabelImage,
|
|
839
|
+
accessible: true,
|
|
840
|
+
isLintable: true,
|
|
841
|
+
}: WidgetExports<typeof LabelImage>);
|