@khanacademy/perseus 19.1.4 → 19.1.6
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/dist/components/graphie.d.ts +9 -1
- package/dist/es/index.js +255 -122
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +261 -126
- package/dist/index.js.map +1 -1
- package/dist/interactive2/interactive-util.d.ts +1 -1
- package/dist/interactive2/movable-point-options.d.ts +5 -4
- package/dist/interactive2/movable.d.ts +90 -13
- package/dist/interactive2/types.d.ts +6 -0
- package/dist/util/graphie.d.ts +5 -0
- package/dist/widgets/expression.d.ts +4 -4
- package/package.json +11 -8
- package/.babelrc.js +0 -8
- package/.eslintrc.js +0 -12
- package/CHANGELOG.md +0 -1422
- package/src/__stories__/article-renderer.stories.tsx +0 -100
- package/src/__stories__/hints-renderer.stories.tsx +0 -55
- package/src/__stories__/server-item-renderer.stories.tsx +0 -44
- package/src/__testdata__/article-renderer.testdata.ts +0 -212
- package/src/__testdata__/graphie.testdata.ts +0 -51
- package/src/__testdata__/renderer.testdata.ts +0 -274
- package/src/__testdata__/server-item-renderer.testdata.ts +0 -171
- package/src/__tests__/__snapshots__/perseus-markdown.test.ts.snap +0 -507
- package/src/__tests__/__snapshots__/renderer.test.tsx.snap +0 -525
- package/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap +0 -285
- package/src/__tests__/a11y.test.ts +0 -303
- package/src/__tests__/article-renderer.test.tsx +0 -130
- package/src/__tests__/error-boundary.test.tsx +0 -76
- package/src/__tests__/extract-perseus-data.test.ts +0 -1027
- package/src/__tests__/init.test.ts +0 -10
- package/src/__tests__/jipt-paragraphs.test.ts +0 -32
- package/src/__tests__/mock-asset-loading-widget.tsx +0 -53
- package/src/__tests__/mock-widget.tsx +0 -33
- package/src/__tests__/perseus-markdown.test.ts +0 -764
- package/src/__tests__/renderability.test.ts +0 -480
- package/src/__tests__/renderer-api.test.tsx +0 -302
- package/src/__tests__/renderer.test.tsx +0 -1816
- package/src/__tests__/server-item-renderer.test.tsx +0 -582
- package/src/__tests__/test-items/image-item.ts +0 -24
- package/src/__tests__/test-items/input-number-1-item.ts +0 -26
- package/src/__tests__/test-items/input-number-2-item.ts +0 -38
- package/src/__tests__/test-items/table-item.ts +0 -29
- package/src/__tests__/tex-wrangler.test.ts +0 -82
- package/src/__tests__/util.test.ts +0 -40
- package/src/__tests__/widget-container.test.tsx +0 -126
- package/src/__tests__/widgets.test.ts +0 -91
- package/src/a11y.ts +0 -45
- package/src/all-widgets.ts +0 -9
- package/src/article-renderer.tsx +0 -237
- package/src/asset-context.ts +0 -30
- package/src/basic-widgets.ts +0 -16
- package/src/components/__stories__/button-group.stories.tsx +0 -55
- package/src/components/__stories__/fixed-to-responsive.stories.tsx +0 -141
- package/src/components/__stories__/graph.stories.tsx +0 -28
- package/src/components/__stories__/graphie.stories.tsx +0 -33
- package/src/components/__stories__/hud.stories.tsx +0 -35
- package/src/components/__stories__/icon.stories.tsx +0 -63
- package/src/components/__stories__/image-loader.stories.tsx +0 -63
- package/src/components/__stories__/info-tip.stories.tsx +0 -31
- package/src/components/__stories__/inline-icon.stories.tsx +0 -40
- package/src/components/__stories__/input-with-examples.stories.tsx +0 -53
- package/src/components/__stories__/lint.stories.tsx +0 -86
- package/src/components/__stories__/math-input.stories.tsx +0 -43
- package/src/components/__stories__/math-output.stories.tsx +0 -23
- package/src/components/__stories__/multi-button-group.stories.tsx +0 -66
- package/src/components/__stories__/number-input.stories.tsx +0 -41
- package/src/components/__stories__/prop-check-box.stories.tsx +0 -65
- package/src/components/__stories__/range-input.stories.tsx +0 -29
- package/src/components/__stories__/simple-keypad-input.stories.tsx +0 -27
- package/src/components/__stories__/sortable.stories.tsx +0 -77
- package/src/components/__stories__/stub-tag-editor.stories.tsx +0 -45
- package/src/components/__stories__/svg-image.stories.tsx +0 -74
- package/src/components/__stories__/tex.stories.tsx +0 -23
- package/src/components/__stories__/text-input.stories.tsx +0 -33
- package/src/components/__stories__/text-list-editor.stories.tsx +0 -32
- package/src/components/__stories__/tooltip.stories.tsx +0 -52
- package/src/components/__stories__/zoomable-tex.stories.tsx +0 -41
- package/src/components/__stories__/zoomable.stories.tsx +0 -49
- package/src/components/__testdata__/sorter.testdata.ts +0 -19
- package/src/components/__tests__/__snapshots__/sortable.test.tsx.snap +0 -163
- package/src/components/__tests__/__snapshots__/sorter.test.tsx.snap +0 -265
- package/src/components/__tests__/math-input.test.tsx +0 -147
- package/src/components/__tests__/number-input.test.tsx +0 -128
- package/src/components/__tests__/sortable.test.tsx +0 -269
- package/src/components/__tests__/sorter.test.tsx +0 -114
- package/src/components/__tests__/zoomable.test.tsx +0 -399
- package/src/components/button-group.tsx +0 -130
- package/src/components/constants.ts +0 -14
- package/src/components/fixed-to-responsive.tsx +0 -169
- package/src/components/graph.tsx +0 -444
- package/src/components/graphie-classes.ts +0 -153
- package/src/components/graphie-movables.ts +0 -338
- package/src/components/graphie.tsx +0 -392
- package/src/components/highlighting/highlightable-content.tsx +0 -240
- package/src/components/highlighting/highlights.ts +0 -207
- package/src/components/highlighting/ranges.ts +0 -304
- package/src/components/highlighting/types.ts +0 -73
- package/src/components/highlighting/ui/highlight-renderer.tsx +0 -172
- package/src/components/highlighting/ui/highlight-set-renderer.tsx +0 -185
- package/src/components/highlighting/ui/highlight-tooltip.tsx +0 -141
- package/src/components/highlighting/ui/highlighting-ui.tsx +0 -143
- package/src/components/highlighting/ui/selection-tracker.tsx +0 -186
- package/src/components/highlighting/ui/types.ts +0 -21
- package/src/components/highlighting/ui/util.ts +0 -165
- package/src/components/highlighting/word-indexer.tsx +0 -152
- package/src/components/hud.tsx +0 -167
- package/src/components/icon.tsx +0 -141
- package/src/components/image-loader.tsx +0 -177
- package/src/components/info-tip/info-tip.tsx +0 -144
- package/src/components/info-tip.tsx +0 -39
- package/src/components/inline-icon.tsx +0 -60
- package/src/components/input-with-examples.tsx +0 -249
- package/src/components/lint.tsx +0 -459
- package/src/components/math-input.tsx +0 -474
- package/src/components/math-output.tsx +0 -134
- package/src/components/multi-button-group.tsx +0 -135
- package/src/components/number-input.tsx +0 -249
- package/src/components/prop-check-box.tsx +0 -79
- package/src/components/range-input.tsx +0 -60
- package/src/components/simple-keypad-input.tsx +0 -88
- package/src/components/sortable.tsx +0 -958
- package/src/components/stub-tag-editor.tsx +0 -53
- package/src/components/svg-image.tsx +0 -779
- package/src/components/tex.tsx +0 -67
- package/src/components/text-input.tsx +0 -129
- package/src/components/text-list-editor.tsx +0 -166
- package/src/components/tooltip.tsx +0 -410
- package/src/components/visibility-observer/__stories__/visibility-observer.stories.tsx +0 -141
- package/src/components/visibility-observer/create-visibility-observer.ts +0 -108
- package/src/components/visibility-observer/get-scroll-parent.ts +0 -46
- package/src/components/visibility-observer/visibility-observer.tsx +0 -124
- package/src/components/zoomable-tex.tsx +0 -78
- package/src/components/zoomable.tsx +0 -304
- package/src/components.ts +0 -15
- package/src/definition-context.tsx +0 -59
- package/src/dependencies.ts +0 -40
- package/src/error-boundary.tsx +0 -70
- package/src/extra-widgets.ts +0 -75
- package/src/hint-renderer.tsx +0 -151
- package/src/hints-renderer.tsx +0 -372
- package/src/icon-paths.ts +0 -119
- package/src/index.ts +0 -171
- package/src/init.ts +0 -56
- package/src/interaction-tracker.ts +0 -62
- package/src/interactive2/__tests__/movable-mock.ts +0 -43
- package/src/interactive2/__tests__/movable-point.test.ts +0 -144
- package/src/interactive2/__tests__/movable.test.ts +0 -157
- package/src/interactive2/interactive-util.test.ts +0 -93
- package/src/interactive2/interactive-util.ts +0 -159
- package/src/interactive2/movable-helper-methods.ts +0 -166
- package/src/interactive2/movable-line-options.ts +0 -419
- package/src/interactive2/movable-line.ts +0 -307
- package/src/interactive2/movable-point-options.ts +0 -212
- package/src/interactive2/movable-point.tsx +0 -585
- package/src/interactive2/movable-polygon-options.ts +0 -389
- package/src/interactive2/movable-polygon.ts +0 -360
- package/src/interactive2/movable.ts +0 -279
- package/src/interactive2/objective_.ts +0 -79
- package/src/interactive2/types.ts +0 -2
- package/src/interactive2/wrapped-defaults.ts +0 -87
- package/src/interactive2/wrapped-ellipse.ts +0 -94
- package/src/interactive2/wrapped-line.ts +0 -116
- package/src/interactive2/wrapped-path.ts +0 -51
- package/src/interactive2.ts +0 -70
- package/src/item-version.ts +0 -10
- package/src/jipt-paragraphs.ts +0 -49
- package/src/jquery.mobile.vmouse.js +0 -545
- package/src/loading-context.ts +0 -21
- package/src/logging/log.test.ts +0 -39
- package/src/logging/log.ts +0 -97
- package/src/mixins/__tests__/widget-prop-denylist.test.ts +0 -21
- package/src/mixins/changeable.ts +0 -133
- package/src/mixins/editor-jsonify.ts +0 -13
- package/src/mixins/widget-jsonify-deprecated.ts +0 -23
- package/src/mixins/widget-prop-denylist.ts +0 -48
- package/src/multi-items/__stories__/multi-renderer.stories.tsx +0 -83
- package/src/multi-items/__testdata__/multi-renderer.testdata.ts +0 -172
- package/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +0 -1164
- package/src/multi-items/__tests__/items.test.ts +0 -240
- package/src/multi-items/__tests__/multi-renderer.test.tsx +0 -914
- package/src/multi-items/__tests__/prop-type-builders.test.ts +0 -133
- package/src/multi-items/__tests__/shapes.test.ts +0 -43
- package/src/multi-items/__tests__/trees.test.ts +0 -317
- package/src/multi-items/item-types.ts +0 -56
- package/src/multi-items/items.ts +0 -176
- package/src/multi-items/multi-renderer.tsx +0 -569
- package/src/multi-items/prop-type-builders.ts +0 -106
- package/src/multi-items/shape-types.ts +0 -52
- package/src/multi-items/shapes.ts +0 -52
- package/src/multi-items/tree-types.ts +0 -47
- package/src/multi-items/trees.ts +0 -360
- package/src/multi-items.ts +0 -75
- package/src/perseus-api.tsx +0 -188
- package/src/perseus-env.ts +0 -27
- package/src/perseus-error.ts +0 -17
- package/src/perseus-markdown.tsx +0 -405
- package/src/perseus-types.ts +0 -1366
- package/src/perseus-version.ts +0 -9
- package/src/question-paragraph.tsx +0 -34
- package/src/renderability.ts +0 -109
- package/src/renderer.tsx +0 -2000
- package/src/server-item-renderer.tsx +0 -495
- package/src/sigfigs.ts +0 -490
- package/src/styles/articles.less +0 -749
- package/src/styles/constants.ts +0 -94
- package/src/styles/global-constants.ts +0 -343
- package/src/styles/global-styles.ts +0 -306
- package/src/styles/khan-exercise.css +0 -1468
- package/src/styles/media-queries.ts +0 -47
- package/src/styles/perseus-renderer.less +0 -732
- package/src/styles/shared.ts +0 -130
- package/src/styles/util.less +0 -47
- package/src/styles/variables.boostrap.less +0 -277
- package/src/styles/variables.less +0 -56
- package/src/styles/variables.shared.less +0 -340
- package/src/styles/widgets/categorizer.less +0 -38
- package/src/styles/widgets/dropdown.less +0 -3
- package/src/styles/widgets/expression.less +0 -65
- package/src/styles/widgets/graded-group.less +0 -36
- package/src/styles/widgets/grapher.less +0 -12
- package/src/styles/widgets/group.less +0 -19
- package/src/styles/widgets/image.less +0 -54
- package/src/styles/widgets/interactive-graph.less +0 -64
- package/src/styles/widgets/label-image.less +0 -57
- package/src/styles/widgets/matcher.less +0 -8
- package/src/styles/widgets/matrix.less +0 -126
- package/src/styles/widgets/measurer.less +0 -20
- package/src/styles/widgets/orderer.less +0 -98
- package/src/styles/widgets/passage.less +0 -154
- package/src/styles/widgets/plotter.less +0 -31
- package/src/styles/widgets/radio.less +0 -58
- package/src/styles/widgets/reaction-diagram.less +0 -35
- package/src/styles/widgets/sequence.less +0 -5
- package/src/styles/widgets/sortable.less +0 -113
- package/src/styles/widgets/table.less +0 -57
- package/src/styles/widgets/unit.less +0 -15
- package/src/tex-wrangler.ts +0 -182
- package/src/translation-linter.ts +0 -126
- package/src/traversal.ts +0 -138
- package/src/types.ts +0 -513
- package/src/util/a11y.ts +0 -23
- package/src/util/answer-types.test.ts +0 -114
- package/src/util/answer-types.ts +0 -876
- package/src/util/colors.ts +0 -93
- package/src/util/drawing-transform.test.ts +0 -67
- package/src/util/drawing-transform.ts +0 -102
- package/src/util/extract-perseus-data.ts +0 -539
- package/src/util/fix-passage-refs.ts +0 -121
- package/src/util/geometry.test.ts +0 -75
- package/src/util/geometry.ts +0 -312
- package/src/util/graph-bounds.ts +0 -23
- package/src/util/graph-utils.ts +0 -15
- package/src/util/graphie.test.ts +0 -1277
- package/src/util/graphie.ts +0 -1566
- package/src/util/interactive.ts +0 -4172
- package/src/util/interval.ts +0 -4
- package/src/util/katex-preprocess.ts +0 -14
- package/src/util/math.test.ts +0 -11
- package/src/util/math.ts +0 -294
- package/src/util/mobile-native-utils.test.ts +0 -22
- package/src/util/mobile-native-utils.ts +0 -12
- package/src/util/parse-perseus-json.test.ts +0 -10
- package/src/util/parse-perseus-json.ts +0 -13
- package/src/util/react-render.tsx +0 -27
- package/src/util/register-all-widgets-for-testing.ts +0 -13
- package/src/util/scroll-utils.ts +0 -155
- package/src/util/sizing-utils.ts +0 -46
- package/src/util/tex.ts +0 -73
- package/src/util/url-utils.ts +0 -34
- package/src/util.graphie.test.ts +0 -45
- package/src/util.graphie.ts +0 -59
- package/src/util.ts +0 -966
- package/src/version.ts +0 -10
- package/src/widget-container.tsx +0 -199
- package/src/widgets/__stories__/categorizer.stories.tsx +0 -14
- package/src/widgets/__stories__/cs-program.stories.tsx +0 -14
- package/src/widgets/__stories__/definition.stories.tsx +0 -114
- package/src/widgets/__stories__/dropdown.stories.tsx +0 -14
- package/src/widgets/__stories__/explanation.stories.tsx +0 -18
- package/src/widgets/__stories__/expression.stories.tsx +0 -145
- package/src/widgets/__stories__/graded-group-set.stories.tsx +0 -31
- package/src/widgets/__stories__/graded-group.stories.tsx +0 -31
- package/src/widgets/__stories__/grapher.stories.tsx +0 -46
- package/src/widgets/__stories__/group.stories.tsx +0 -14
- package/src/widgets/__stories__/iframe.stories.tsx +0 -14
- package/src/widgets/__stories__/image.stories.tsx +0 -85
- package/src/widgets/__stories__/input-number.stories.tsx +0 -101
- package/src/widgets/__stories__/interaction.stories.tsx +0 -14
- package/src/widgets/__stories__/interactive-graph.stories.tsx +0 -60
- package/src/widgets/__stories__/label-image.stories.tsx +0 -130
- package/src/widgets/__stories__/matcher.stories.tsx +0 -14
- package/src/widgets/__stories__/matrix.stories.tsx +0 -14
- package/src/widgets/__stories__/number-line.stories.tsx +0 -14
- package/src/widgets/__stories__/numeric-input.stories.tsx +0 -105
- package/src/widgets/__stories__/orderer.stories.tsx +0 -18
- package/src/widgets/__stories__/passage-ref.stories.tsx +0 -33
- package/src/widgets/__stories__/passage.stories.tsx +0 -39
- package/src/widgets/__stories__/plotter.stories.tsx +0 -27
- package/src/widgets/__stories__/radio.stories.tsx +0 -86
- package/src/widgets/__stories__/random-widgets.stories.tsx +0 -59
- package/src/widgets/__stories__/sorter.stories.tsx +0 -14
- package/src/widgets/__stories__/test-keypad-context-wrapper.tsx +0 -66
- package/src/widgets/__stories__/unit.stories.tsx +0 -28
- package/src/widgets/__stories__/video-transcript-link.stories.tsx +0 -23
- package/src/widgets/__stories__/video.stories.tsx +0 -22
- package/src/widgets/__testdata__/base-radio.testdata.ts +0 -21
- package/src/widgets/__testdata__/categorizer.testdata.ts +0 -89
- package/src/widgets/__testdata__/cs-program.testdata.ts +0 -27
- package/src/widgets/__testdata__/dropdown.testdata.ts +0 -71
- package/src/widgets/__testdata__/explanation.testdata.ts +0 -85
- package/src/widgets/__testdata__/expression.testdata.ts +0 -198
- package/src/widgets/__testdata__/graded-group-set.testdata.ts +0 -356
- package/src/widgets/__testdata__/graded-group.testdata.ts +0 -77
- package/src/widgets/__testdata__/grapher.testdata.ts +0 -374
- package/src/widgets/__testdata__/group.testdata.ts +0 -153
- package/src/widgets/__testdata__/iframe.testdata.ts +0 -35
- package/src/widgets/__testdata__/image.testdata.ts +0 -36
- package/src/widgets/__testdata__/input-number.testdata.ts +0 -72
- package/src/widgets/__testdata__/interaction.testdata.ts +0 -262
- package/src/widgets/__testdata__/interactive-graph-random.testdata.ts +0 -269
- package/src/widgets/__testdata__/interactive-graph.testdata.ts +0 -644
- package/src/widgets/__testdata__/label-image.testdata.ts +0 -308
- package/src/widgets/__testdata__/matcher.testdata.ts +0 -33
- package/src/widgets/__testdata__/matrix.testdata.ts +0 -28
- package/src/widgets/__testdata__/number-line.testdata.ts +0 -30
- package/src/widgets/__testdata__/numeric-input.testdata.ts +0 -265
- package/src/widgets/__testdata__/orderer.testdata.ts +0 -123
- package/src/widgets/__testdata__/passage-ref.testdata.ts +0 -101
- package/src/widgets/__testdata__/passage.testdata.ts +0 -95
- package/src/widgets/__testdata__/plotter.testdata.ts +0 -36
- package/src/widgets/__testdata__/radio.testdata.ts +0 -324
- package/src/widgets/__testdata__/randomizers.ts +0 -51
- package/src/widgets/__testdata__/sorter.testdata.ts +0 -19
- package/src/widgets/__testdata__/video.testdata.ts +0 -61
- package/src/widgets/__tests__/__snapshots__/categorizer.test.ts.snap +0 -940
- package/src/widgets/__tests__/__snapshots__/cs-program.test.ts.snap +0 -67
- package/src/widgets/__tests__/__snapshots__/definition.test.ts.snap +0 -97
- package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +0 -133
- package/src/widgets/__tests__/__snapshots__/explanation.test.ts.snap +0 -425
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt.test.ts.snap +0 -729
- package/src/widgets/__tests__/__snapshots__/graded-group-set.test.ts.snap +0 -339
- package/src/widgets/__tests__/__snapshots__/graded-group.test.ts.snap +0 -801
- package/src/widgets/__tests__/__snapshots__/grapher.test.ts.snap +0 -2673
- package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +0 -1211
- package/src/widgets/__tests__/__snapshots__/iframe.test.ts.snap +0 -61
- package/src/widgets/__tests__/__snapshots__/image.test.ts.snap +0 -378
- package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +0 -495
- package/src/widgets/__tests__/__snapshots__/interaction.test.ts.snap +0 -292
- package/src/widgets/__tests__/__snapshots__/interactive-graph.test.ts.snap +0 -2889
- package/src/widgets/__tests__/__snapshots__/matcher.test.tsx.snap +0 -883
- package/src/widgets/__tests__/__snapshots__/matrix.test.ts.snap +0 -514
- package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +0 -814
- package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +0 -470
- package/src/widgets/__tests__/__snapshots__/orderer.test.ts.snap +0 -243
- package/src/widgets/__tests__/__snapshots__/passage-ref.test.ts.snap +0 -434
- package/src/widgets/__tests__/__snapshots__/passage.test.tsx.snap +0 -1956
- package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +0 -827
- package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +0 -4237
- package/src/widgets/__tests__/__snapshots__/unit.test.tsx.snap +0 -20
- package/src/widgets/__tests__/__snapshots__/video.test.ts.snap +0 -153
- package/src/widgets/__tests__/categorizer.test.ts +0 -203
- package/src/widgets/__tests__/cs-program.test.ts +0 -43
- package/src/widgets/__tests__/definition.test.ts +0 -136
- package/src/widgets/__tests__/dropdown.test.ts +0 -105
- package/src/widgets/__tests__/explanation.test.ts +0 -198
- package/src/widgets/__tests__/expression-mobile.test.tsx +0 -363
- package/src/widgets/__tests__/expression.test.tsx +0 -484
- package/src/widgets/__tests__/graded-group-set-jipt.test.ts +0 -30
- package/src/widgets/__tests__/graded-group-set.test.ts +0 -288
- package/src/widgets/__tests__/graded-group.test.ts +0 -232
- package/src/widgets/__tests__/grapher.cypress.ts +0 -576
- package/src/widgets/__tests__/grapher.test.ts +0 -33
- package/src/widgets/__tests__/group.test.tsx +0 -510
- package/src/widgets/__tests__/iframe.test.ts +0 -45
- package/src/widgets/__tests__/image.test.ts +0 -62
- package/src/widgets/__tests__/input-number.test.ts +0 -354
- package/src/widgets/__tests__/interaction.test.ts +0 -31
- package/src/widgets/__tests__/interactive-graph.cypress.ts +0 -815
- package/src/widgets/__tests__/interactive-graph.test.ts +0 -106
- package/src/widgets/__tests__/matcher.test.tsx +0 -176
- package/src/widgets/__tests__/matrix.test.ts +0 -82
- package/src/widgets/__tests__/number-line.test.ts +0 -79
- package/src/widgets/__tests__/numeric-input.test.ts +0 -575
- package/src/widgets/__tests__/orderer.test.ts +0 -73
- package/src/widgets/__tests__/passage-ref.test.ts +0 -168
- package/src/widgets/__tests__/passage.test.tsx +0 -224
- package/src/widgets/__tests__/plotter.test.tsx +0 -69
- package/src/widgets/__tests__/radio/base-radio.test.tsx +0 -470
- package/src/widgets/__tests__/radio/choice-icon.test.tsx +0 -90
- package/src/widgets/__tests__/radio/choice.test.tsx +0 -310
- package/src/widgets/__tests__/radio/focus-ring.test.tsx +0 -22
- package/src/widgets/__tests__/radio/option-status.test.tsx +0 -77
- package/src/widgets/__tests__/radio.test.ts +0 -807
- package/src/widgets/__tests__/randomizers.test.ts +0 -53
- package/src/widgets/__tests__/renderQuestion.tsx +0 -101
- package/src/widgets/__tests__/unit.test.tsx +0 -168
- package/src/widgets/__tests__/video.test.ts +0 -69
- package/src/widgets/categorizer.tsx +0 -344
- package/src/widgets/cs-program.tsx +0 -227
- package/src/widgets/definition.tsx +0 -121
- package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +0 -30
- package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +0 -70
- package/src/widgets/deprecated-standin/__tests__/deprecated-standin.test.ts +0 -93
- package/src/widgets/deprecated-standin/index.tsx +0 -58
- package/src/widgets/dropdown.tsx +0 -142
- package/src/widgets/explanation.tsx +0 -317
- package/src/widgets/expression.tsx +0 -707
- package/src/widgets/graded-group-answer-bar.tsx +0 -142
- package/src/widgets/graded-group-set.tsx +0 -338
- package/src/widgets/graded-group.tsx +0 -471
- package/src/widgets/grapher/util.tsx +0 -799
- package/src/widgets/grapher.tsx +0 -660
- package/src/widgets/group.tsx +0 -195
- package/src/widgets/iframe.tsx +0 -195
- package/src/widgets/image.tsx +0 -312
- package/src/widgets/input-number.tsx +0 -412
- package/src/widgets/interaction/arrow-picker.tsx +0 -38
- package/src/widgets/interaction/color-picker.tsx +0 -69
- package/src/widgets/interaction/dash-picker.tsx +0 -45
- package/src/widgets/interaction/element-container.tsx +0 -100
- package/src/widgets/interaction.tsx +0 -826
- package/src/widgets/interactive-graph.tsx +0 -2597
- package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +0 -91
- package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +0 -62
- package/src/widgets/label-image/__stories__/marker.stories.tsx +0 -171
- package/src/widgets/label-image/__tests__/answer-pill.test.tsx +0 -75
- package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +0 -39
- package/src/widgets/label-image/answer-choices.tsx +0 -88
- package/src/widgets/label-image/answer-pill.tsx +0 -103
- package/src/widgets/label-image/hide-answers-toggle.tsx +0 -40
- package/src/widgets/label-image/marker.tsx +0 -268
- package/src/widgets/label-image/strings.ts +0 -5
- package/src/widgets/label-image/types.ts +0 -27
- package/src/widgets/label-image.test.ts +0 -928
- package/src/widgets/label-image.tsx +0 -819
- package/src/widgets/lights-puzzle.tsx +0 -359
- package/src/widgets/matcher.tsx +0 -298
- package/src/widgets/matrix.tsx +0 -634
- package/src/widgets/measurer.tsx +0 -222
- package/src/widgets/molecule/__tests__/layout.test.ts +0 -162
- package/src/widgets/molecule/__tests__/parser.test.ts +0 -155
- package/src/widgets/molecule/molecule-drawing.ts +0 -173
- package/src/widgets/molecule/molecule-layout.ts +0 -362
- package/src/widgets/molecule/smiles-parser.ts +0 -296
- package/src/widgets/molecule.tsx +0 -172
- package/src/widgets/number-line.tsx +0 -862
- package/src/widgets/numeric-input.tsx +0 -576
- package/src/widgets/orderer.tsx +0 -786
- package/src/widgets/passage/__tests__/passage-markdown.test.ts +0 -376
- package/src/widgets/passage/passage-markdown.tsx +0 -475
- package/src/widgets/passage-ref-target.tsx +0 -76
- package/src/widgets/passage-ref.tsx +0 -213
- package/src/widgets/passage.tsx +0 -582
- package/src/widgets/plotter.tsx +0 -1194
- package/src/widgets/radio/__stories__/base-radio.stories.tsx +0 -196
- package/src/widgets/radio/__stories__/choice-icon.stories.tsx +0 -166
- package/src/widgets/radio/__stories__/choice-none-above.stories.tsx +0 -40
- package/src/widgets/radio/__stories__/choice.stories.tsx +0 -137
- package/src/widgets/radio/__stories__/focus-ring.stories.tsx +0 -43
- package/src/widgets/radio/__stories__/option-status.stories.tsx +0 -103
- package/src/widgets/radio/base-radio.tsx +0 -511
- package/src/widgets/radio/choice-a11y-text.ts +0 -61
- package/src/widgets/radio/choice-icon/choice-icon.tsx +0 -218
- package/src/widgets/radio/choice-icon/cross-out-line.tsx +0 -51
- package/src/widgets/radio/choice-icon/shared-styles.ts +0 -15
- package/src/widgets/radio/choice-none-above.tsx +0 -56
- package/src/widgets/radio/choice.jsx.fixture.ts +0 -45
- package/src/widgets/radio/choice.tsx +0 -359
- package/src/widgets/radio/focus-ring.tsx +0 -57
- package/src/widgets/radio/option-status.tsx +0 -98
- package/src/widgets/radio/util.test.ts +0 -18
- package/src/widgets/radio/util.ts +0 -31
- package/src/widgets/radio/widget.tsx +0 -520
- package/src/widgets/radio.ts +0 -157
- package/src/widgets/reaction-diagram.tsx +0 -144
- package/src/widgets/sequence.tsx +0 -142
- package/src/widgets/simulator.tsx +0 -829
- package/src/widgets/sorter.tsx +0 -155
- package/src/widgets/table.tsx +0 -393
- package/src/widgets/unit.tsx +0 -374
- package/src/widgets/video-transcript-link.test.tsx +0 -213
- package/src/widgets/video-transcript-link.tsx +0 -84
- package/src/widgets/video.tsx +0 -135
- package/src/widgets.ts +0 -489
- package/src/zoom.ts +0 -555
- package/tsconfig-build.json +0 -26
- package/tsconfig-build.tsbuildinfo +0 -1
package/src/renderer.tsx
DELETED
|
@@ -1,2000 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @khanacademy/ts-no-error-suppressions */
|
|
2
|
-
/* eslint-disable react/no-unsafe */
|
|
3
|
-
import * as PerseusLinter from "@khanacademy/perseus-linter";
|
|
4
|
-
import {entries} from "@khanacademy/wonder-stuff-core";
|
|
5
|
-
import classNames from "classnames";
|
|
6
|
-
import $ from "jquery";
|
|
7
|
-
import * as React from "react";
|
|
8
|
-
import ReactDOM from "react-dom";
|
|
9
|
-
import _ from "underscore";
|
|
10
|
-
|
|
11
|
-
import AssetContext from "./asset-context";
|
|
12
|
-
import SvgImage from "./components/svg-image";
|
|
13
|
-
import TeX from "./components/tex";
|
|
14
|
-
import Zoomable from "./components/zoomable";
|
|
15
|
-
import ZoomableTeX from "./components/zoomable-tex";
|
|
16
|
-
import {DefinitionProvider} from "./definition-context";
|
|
17
|
-
import {getDependencies} from "./dependencies";
|
|
18
|
-
import ErrorBoundary from "./error-boundary";
|
|
19
|
-
import InteractionTracker from "./interaction-tracker";
|
|
20
|
-
import Objective from "./interactive2/objective_";
|
|
21
|
-
import JiptParagraphs from "./jipt-paragraphs";
|
|
22
|
-
import {Errors, Log} from "./logging/log";
|
|
23
|
-
import {ClassNames as ApiClassNames, ApiOptions} from "./perseus-api";
|
|
24
|
-
import {PerseusError} from "./perseus-error";
|
|
25
|
-
import PerseusMarkdown from "./perseus-markdown";
|
|
26
|
-
import QuestionParagraph from "./question-paragraph";
|
|
27
|
-
import TranslationLinter from "./translation-linter";
|
|
28
|
-
import Util from "./util";
|
|
29
|
-
import preprocessTex from "./util/katex-preprocess";
|
|
30
|
-
import WidgetContainer from "./widget-container";
|
|
31
|
-
import * as Widgets from "./widgets";
|
|
32
|
-
|
|
33
|
-
import type {DependenciesContext} from "./dependencies";
|
|
34
|
-
import type {
|
|
35
|
-
PerseusRenderer,
|
|
36
|
-
PerseusWidget,
|
|
37
|
-
PerseusWidgetOptions,
|
|
38
|
-
} from "./perseus-types";
|
|
39
|
-
import type {
|
|
40
|
-
APIOptions,
|
|
41
|
-
APIOptionsWithDefaults,
|
|
42
|
-
FilterCriterion,
|
|
43
|
-
FocusPath,
|
|
44
|
-
PerseusScore,
|
|
45
|
-
PerseusWidgetMap,
|
|
46
|
-
WidgetProps,
|
|
47
|
-
} from "./types";
|
|
48
|
-
import type {LinterContextProps} from "@khanacademy/perseus-linter";
|
|
49
|
-
|
|
50
|
-
import "./styles/perseus-renderer.less";
|
|
51
|
-
|
|
52
|
-
const {mapObject, mapObjectFromArray} = Objective;
|
|
53
|
-
|
|
54
|
-
const rContainsNonWhitespace = /\S/;
|
|
55
|
-
const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
|
|
56
|
-
|
|
57
|
-
const noopOnRender = () => {};
|
|
58
|
-
|
|
59
|
-
const SHOULD_CLEAR_WIDGETS_PROP_LIST = ["content", "problemNum", "widgets"];
|
|
60
|
-
|
|
61
|
-
const makeContainerId = (id: string) => "container:" + id;
|
|
62
|
-
|
|
63
|
-
// Check if one focus path / id path is a prefix of another
|
|
64
|
-
// The focus path null will never be a prefix of any non-null
|
|
65
|
-
// path, since it represents no focus.
|
|
66
|
-
// Otherwise, prefix is calculated by whether every array
|
|
67
|
-
// element in the prefix is present in the same position in the
|
|
68
|
-
// wholeArray path.
|
|
69
|
-
const isIdPathPrefix = function (
|
|
70
|
-
prefixArray: FocusPath,
|
|
71
|
-
wholeArray: FocusPath,
|
|
72
|
-
) {
|
|
73
|
-
if (prefixArray === null || wholeArray === null) {
|
|
74
|
-
return prefixArray === wholeArray;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// @ts-expect-error - TS2345 - Argument of type 'readonly string[] | undefined' is not assignable to parameter of type 'Collection<any>'.
|
|
78
|
-
return _.every(prefixArray, (elem: string, i: number) => {
|
|
79
|
-
if (wholeArray != null) {
|
|
80
|
-
return _.isEqual(elem, wholeArray[i]);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
type WidgetState = {
|
|
86
|
-
isMobile?: boolean;
|
|
87
|
-
inTable?: boolean;
|
|
88
|
-
key?: number;
|
|
89
|
-
paragraphIndex?: number;
|
|
90
|
-
foundFullWidth?: boolean;
|
|
91
|
-
baseElements?: any;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type SetWidgetPropsFn = (
|
|
95
|
-
id: string,
|
|
96
|
-
newProps: any,
|
|
97
|
-
cb: () => boolean,
|
|
98
|
-
// Widgets can call `onChange` with `silent` set to `true` to prevent
|
|
99
|
-
// interaction events from being triggered in listeners.
|
|
100
|
-
silent?: boolean,
|
|
101
|
-
) => void;
|
|
102
|
-
|
|
103
|
-
// The return type for getUserInput. Widgets have full control of what is
|
|
104
|
-
// returned so it's not easily typed (some widgets return a scalar (string),
|
|
105
|
-
// some return a custom-built object, some delegate to
|
|
106
|
-
// `WidgetJsonifyDeprecated` which returns an object containing widget props
|
|
107
|
-
// (filtered by deprecated keys)).
|
|
108
|
-
type WidgetUserInput = any;
|
|
109
|
-
|
|
110
|
-
type SerializedState = {
|
|
111
|
-
[id: string]: any;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export type Widget = {
|
|
115
|
-
focus: () =>
|
|
116
|
-
| {
|
|
117
|
-
id: string;
|
|
118
|
-
path: FocusPath;
|
|
119
|
-
}
|
|
120
|
-
| boolean;
|
|
121
|
-
getDOMNodeForPath: (path: FocusPath) => Element | Text | null;
|
|
122
|
-
deselectIncorrectSelectedChoices?: () => void;
|
|
123
|
-
restoreSerializedState: (props: any, callback: () => void) => any;
|
|
124
|
-
// TODO(jeremy): I think this return value is wrong. The widget
|
|
125
|
-
// getSerializedState should just return _its_ serialized state, not a
|
|
126
|
-
// key/value list of all widget states (i think!)
|
|
127
|
-
// Returns widget state that can be passed back to `restoreSerializedState`
|
|
128
|
-
// to put the widget back into exactly the same state. If the widget does
|
|
129
|
-
// not implement this function, the renderer simply returns all of the
|
|
130
|
-
// widget's props.
|
|
131
|
-
getSerializedState?: () => SerializedState; // SUSPECT,
|
|
132
|
-
getGrammarTypeForPath: (path: FocusPath) => string;
|
|
133
|
-
blurInputPath?: (path: FocusPath) => null;
|
|
134
|
-
focusInputPath?: (path: FocusPath) => null;
|
|
135
|
-
getInputPaths?: () => ReadonlyArray<FocusPath>;
|
|
136
|
-
setInputValue?: (
|
|
137
|
-
path: FocusPath,
|
|
138
|
-
newValue: string,
|
|
139
|
-
// TODO(jeremy): I think this is actually a callback
|
|
140
|
-
focus?: () => unknown,
|
|
141
|
-
) => void;
|
|
142
|
-
getUserInput?: () => WidgetUserInput | null | undefined;
|
|
143
|
-
simpleValidate?: (
|
|
144
|
-
options?: any,
|
|
145
|
-
onOutputError?: (
|
|
146
|
-
widgetId: any,
|
|
147
|
-
value: string,
|
|
148
|
-
message?: string | null | undefined,
|
|
149
|
-
) => unknown | null | undefined,
|
|
150
|
-
) => PerseusScore;
|
|
151
|
-
showRationalesForCurrentlySelectedChoices?: (options?: any) => void;
|
|
152
|
-
examples?: () => ReadonlyArray<string>;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
type Props = Partial<React.ContextType<typeof DependenciesContext>> & {
|
|
156
|
-
apiOptions?: APIOptions;
|
|
157
|
-
alwaysUpdate?: boolean;
|
|
158
|
-
findExternalWidgets: any;
|
|
159
|
-
highlightedWidgets?: ReadonlyArray<any>;
|
|
160
|
-
images: PerseusRenderer["images"];
|
|
161
|
-
keypadElement?: any; // TODO(kevinb): add proper types,
|
|
162
|
-
onInteractWithWidget: (id: string) => void;
|
|
163
|
-
onRender: (node?: any) => void;
|
|
164
|
-
problemNum?: number;
|
|
165
|
-
questionCompleted?: boolean;
|
|
166
|
-
reviewMode?: boolean | null | undefined;
|
|
167
|
-
content: PerseusRenderer["content"];
|
|
168
|
-
serializedState?: any;
|
|
169
|
-
// Callback which is called when serialized state changes with the new
|
|
170
|
-
// serialized state.
|
|
171
|
-
onSerializedStateUpdated: (serializedState: {
|
|
172
|
-
[key: string]: any;
|
|
173
|
-
}) => unknown;
|
|
174
|
-
// If linterContext.highlightLint is true, then content will be passed
|
|
175
|
-
// to the linter and any warnings will be highlighted in the rendered
|
|
176
|
-
// output.
|
|
177
|
-
linterContext: LinterContextProps;
|
|
178
|
-
legacyPerseusLint?: ReadonlyArray<string>;
|
|
179
|
-
widgets: PerseusRenderer["widgets"];
|
|
180
|
-
// Skip adding paragraph class
|
|
181
|
-
inline?: boolean;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
type State = {
|
|
185
|
-
translationLintErrors: ReadonlyArray<string>;
|
|
186
|
-
widgetInfo: Readonly<{
|
|
187
|
-
[id: string]: PerseusWidget | null | undefined;
|
|
188
|
-
}>;
|
|
189
|
-
widgetProps: Readonly<{
|
|
190
|
-
[id: string]: any | null | undefined;
|
|
191
|
-
}>;
|
|
192
|
-
jiptContent: any;
|
|
193
|
-
lastUsedWidgetId: string | null | undefined;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
type Context = LinterContextProps & {
|
|
197
|
-
content: string;
|
|
198
|
-
widgets: {
|
|
199
|
-
[id: string]: any;
|
|
200
|
-
};
|
|
201
|
-
// This is inexact because LinterContextProps is inexact
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
type DefaultProps = {
|
|
205
|
-
alwaysUpdate: Props["alwaysUpdate"];
|
|
206
|
-
content: Props["content"];
|
|
207
|
-
findExternalWidgets: Props["findExternalWidgets"];
|
|
208
|
-
highlightedWidgets: Props["highlightedWidgets"];
|
|
209
|
-
images: Props["images"];
|
|
210
|
-
linterContext: Props["linterContext"];
|
|
211
|
-
onInteractWithWidget: Props["onInteractWithWidget"];
|
|
212
|
-
onRender: Props["onRender"];
|
|
213
|
-
onSerializedStateUpdated: Props["onSerializedStateUpdated"];
|
|
214
|
-
questionCompleted: Props["questionCompleted"];
|
|
215
|
-
reviewMode: Props["reviewMode"];
|
|
216
|
-
serializedState: Props["serializedState"];
|
|
217
|
-
widgets: Props["widgets"];
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
class Renderer extends React.Component<Props, State> {
|
|
221
|
-
_currentFocus: FocusPath | null | undefined;
|
|
222
|
-
// @ts-expect-error - TS2564 - Property '_foundTextNodes' has no initializer and is not definitely assigned in the constructor.
|
|
223
|
-
_foundTextNodes: boolean;
|
|
224
|
-
// @ts-expect-error - TS2564 - Property '_interactionTrackers' has no initializer and is not definitely assigned in the constructor.
|
|
225
|
-
_interactionTrackers: {
|
|
226
|
-
[id: string]: InteractionTracker<any>;
|
|
227
|
-
};
|
|
228
|
-
// @ts-expect-error - TS2564 - Property '_isMounted' has no initializer and is not definitely assigned in the constructor.
|
|
229
|
-
_isMounted: boolean;
|
|
230
|
-
// @ts-expect-error - TS2564 - Property '_isTwoColumn' has no initializer and is not definitely assigned in the constructor.
|
|
231
|
-
_isTwoColumn: boolean;
|
|
232
|
-
|
|
233
|
-
// The i18n linter.
|
|
234
|
-
_translationLinter: TranslationLinter;
|
|
235
|
-
|
|
236
|
-
_widgetContainers: Map<string, WidgetContainer> = new Map();
|
|
237
|
-
|
|
238
|
-
lastRenderedMarkdown: React.ReactNode;
|
|
239
|
-
// @ts-expect-error - TS2564 - Property 'reuseMarkdown' has no initializer and is not definitely assigned in the constructor.
|
|
240
|
-
reuseMarkdown: boolean;
|
|
241
|
-
// @ts-expect-error - TS2564 - Property 'translationIndex' has no initializer and is not definitely assigned in the constructor.
|
|
242
|
-
translationIndex: number;
|
|
243
|
-
// @ts-expect-error - TS2564 - Property 'widgetIds' has no initializer and is not definitely assigned in the constructor.
|
|
244
|
-
widgetIds: Array<string>;
|
|
245
|
-
|
|
246
|
-
static defaultProps: DefaultProps = {
|
|
247
|
-
content: "",
|
|
248
|
-
widgets: {},
|
|
249
|
-
images: {},
|
|
250
|
-
highlightedWidgets: [],
|
|
251
|
-
questionCompleted: false,
|
|
252
|
-
// onRender may be called multiple times per render, for example
|
|
253
|
-
// if there are multiple images or TeX pieces within `content`.
|
|
254
|
-
// It is a good idea to debounce any functions passed here.
|
|
255
|
-
onRender: noopOnRender,
|
|
256
|
-
onInteractWithWidget: function () {},
|
|
257
|
-
findExternalWidgets: () => [],
|
|
258
|
-
alwaysUpdate: false,
|
|
259
|
-
reviewMode: false,
|
|
260
|
-
serializedState: null,
|
|
261
|
-
onSerializedStateUpdated: () => {},
|
|
262
|
-
linterContext: PerseusLinter.linterContextDefault,
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
constructor(props: Props) {
|
|
266
|
-
super(props);
|
|
267
|
-
this._translationLinter = new TranslationLinter();
|
|
268
|
-
|
|
269
|
-
this.state = {
|
|
270
|
-
jiptContent: null,
|
|
271
|
-
|
|
272
|
-
// TranslationLinter is async and currently does not contain a
|
|
273
|
-
// location. This is a list of error strings TranslationLinter
|
|
274
|
-
// detected on its last run.
|
|
275
|
-
translationLintErrors: [],
|
|
276
|
-
|
|
277
|
-
// The ID of the last widget the user interacted with. We'll
|
|
278
|
-
// use this to set the `isLastUsedWidget` flag on the
|
|
279
|
-
// corresponding widget.
|
|
280
|
-
lastUsedWidgetId: null,
|
|
281
|
-
|
|
282
|
-
...this._getInitialWidgetState(props),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
componentDidMount() {
|
|
287
|
-
this._isMounted = true;
|
|
288
|
-
|
|
289
|
-
// figure out why we're passing an empty object
|
|
290
|
-
// @ts-expect-error - TS2345 - Argument of type '{}' is not assignable to parameter of type 'Props'.
|
|
291
|
-
this.handleRender({});
|
|
292
|
-
this._currentFocus = null;
|
|
293
|
-
|
|
294
|
-
// TODO(emily): actually make the serializedState prop work like a
|
|
295
|
-
// controlled prop, instead of manually calling .restoreSerializedState
|
|
296
|
-
// at the right times.
|
|
297
|
-
if (this.props.serializedState) {
|
|
298
|
-
this.restoreSerializedState(this.props.serializedState);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (this.props.linterContext.highlightLint) {
|
|
302
|
-
// Get i18n lint errors asynchronously. If there are lint errors,
|
|
303
|
-
// this component will be rerendered.
|
|
304
|
-
this._translationLinter.runLinter(
|
|
305
|
-
this.props.content,
|
|
306
|
-
this.handletranslationLintErrors,
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
312
|
-
if (
|
|
313
|
-
!_.isEqual(
|
|
314
|
-
_.pick(this.props, SHOULD_CLEAR_WIDGETS_PROP_LIST),
|
|
315
|
-
_.pick(nextProps, SHOULD_CLEAR_WIDGETS_PROP_LIST),
|
|
316
|
-
)
|
|
317
|
-
) {
|
|
318
|
-
this.setState(this._getInitialWidgetState(nextProps));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
shouldComponentUpdate(nextProps: Props, nextState: State): any | boolean {
|
|
323
|
-
// QUESTION(jeremy): Shouldn't we look at `nextProps` here? Otherwise
|
|
324
|
-
// we're always looking "one render behind".
|
|
325
|
-
if (this.props.alwaysUpdate) {
|
|
326
|
-
// TOTAL hacks so that findWidgets doesn't break
|
|
327
|
-
// when one widget updates without the other.
|
|
328
|
-
// See passage-refs inside radios, which was why
|
|
329
|
-
// this was introduced.
|
|
330
|
-
// I'm sorry!
|
|
331
|
-
// TODO(aria): cry
|
|
332
|
-
//
|
|
333
|
-
// HACK(djf): I've also set this alwaysUpdate property from
|
|
334
|
-
// async-renderer.jsx in the manticore-package. I'm doing this
|
|
335
|
-
// to work around an infinite loop of some sort that started
|
|
336
|
-
// happening in manticore after the React 16 update. After
|
|
337
|
-
// clicking around in the manticore exercise editor (for example)
|
|
338
|
-
// the UI would freeze up, and the debugger would show that
|
|
339
|
-
// we were always deep in a recursion on the propsChanged line
|
|
340
|
-
// below. There is some kind of timing issue causing some kind
|
|
341
|
-
// of infinite loop, but by avoiding the time-consuming deep
|
|
342
|
-
// equal comparisons on our props (which are often huge) I can
|
|
343
|
-
// no longer reproduce the bug.
|
|
344
|
-
// TODO(djf): Remove this comment
|
|
345
|
-
// https://khanacademy.atlassian.net/browse/CP-834 is resolved.
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
const stateChanged = !_.isEqual(this.state, nextState);
|
|
349
|
-
const propsChanged = !_.isEqual(this.props, nextProps);
|
|
350
|
-
return propsChanged || stateChanged;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
UNSAFE_componentWillUpdate(nextProps: Props, nextState: State) {
|
|
354
|
-
const oldJipt = this.shouldRenderJiptPlaceholder(
|
|
355
|
-
this.props,
|
|
356
|
-
this.state,
|
|
357
|
-
);
|
|
358
|
-
const newJipt = this.shouldRenderJiptPlaceholder(nextProps, nextState);
|
|
359
|
-
const oldContent = this.getContent(this.props, this.state);
|
|
360
|
-
const newContent = this.getContent(nextProps, nextState);
|
|
361
|
-
const oldHighlightedWidgets = this.props.highlightedWidgets;
|
|
362
|
-
const newHighlightedWidgets = nextProps.highlightedWidgets;
|
|
363
|
-
|
|
364
|
-
// TODO(jared): This seems to be a perfect overlap with
|
|
365
|
-
// "shouldComponentUpdate" -- can we just remove this
|
|
366
|
-
// componentWillUpdate and the reuseMarkdown attr?
|
|
367
|
-
this.reuseMarkdown =
|
|
368
|
-
!oldJipt &&
|
|
369
|
-
!newJipt &&
|
|
370
|
-
oldContent === newContent &&
|
|
371
|
-
_.isEqual(
|
|
372
|
-
this.state.translationLintErrors,
|
|
373
|
-
nextState.translationLintErrors,
|
|
374
|
-
) &&
|
|
375
|
-
// If we are running the linter then we need to know when
|
|
376
|
-
// widgets have changed because we need for force the linter to
|
|
377
|
-
// run when that happens. Note: don't do identity comparison here:
|
|
378
|
-
// it can cause frequent re-renders that break MathJax somehow
|
|
379
|
-
(!this.props.linterContext.highlightLint ||
|
|
380
|
-
_.isEqual(this.props.widgets, nextProps.widgets)) &&
|
|
381
|
-
// If the linter is turned on or off, we have to rerender
|
|
382
|
-
this.props.linterContext.highlightLint ===
|
|
383
|
-
nextProps.linterContext.highlightLint &&
|
|
384
|
-
// yes, this is identity array comparison, but these are passed
|
|
385
|
-
// in from state in the item-renderer, so they should be
|
|
386
|
-
// identity equal unless something changed, and it's expensive
|
|
387
|
-
// to loop through them to look for differences.
|
|
388
|
-
// Technically, we could reuse the markdown when this changes,
|
|
389
|
-
// but to do that we'd have to do more expensive checking of
|
|
390
|
-
// whether a widget should be highlighted in the common case
|
|
391
|
-
// where this array hasn't changed, so we just redo the whole
|
|
392
|
-
// render if this changed
|
|
393
|
-
oldHighlightedWidgets === newHighlightedWidgets;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
componentDidUpdate(prevProps: Props, prevState: State) {
|
|
397
|
-
this.handleRender(prevProps);
|
|
398
|
-
// We even do this if we did reuse the markdown because
|
|
399
|
-
// we might need to update the widget props on this render,
|
|
400
|
-
// even though we have the same widgets.
|
|
401
|
-
// WidgetContainers don't update their widgets' props when
|
|
402
|
-
// they are re-rendered, so even if they've been
|
|
403
|
-
// re-rendered we need to call these methods on them.
|
|
404
|
-
_.each(this.widgetIds, (id) => {
|
|
405
|
-
const container = this._widgetContainers.get(makeContainerId(id));
|
|
406
|
-
container && container.replaceWidgetProps(this.getWidgetProps(id));
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
this.props.serializedState &&
|
|
411
|
-
!_.isEqual(this.props.serializedState, this.getSerializedState())
|
|
412
|
-
) {
|
|
413
|
-
this.restoreSerializedState(this.props.serializedState);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (this.props.linterContext.highlightLint) {
|
|
417
|
-
// Get i18n lint errors asynchronously. If lint errors have changed
|
|
418
|
-
// since the last run, this component will be rerendered.
|
|
419
|
-
this._translationLinter.runLinter(
|
|
420
|
-
this.props.content,
|
|
421
|
-
this.handletranslationLintErrors,
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
componentWillUnmount() {
|
|
427
|
-
// Clean out the list of widgetIds when unmounting, as this list is
|
|
428
|
-
// meant to be consistent with the refs controlled by the renderer, and
|
|
429
|
-
// refs are also cleared out during unmounting.
|
|
430
|
-
// (This may not be totally necessary, but mobile clients have been
|
|
431
|
-
// seeing JS errors due to an inconsistency between the list of
|
|
432
|
-
// widgetIds and the child refs of the renderer.
|
|
433
|
-
// See: https://phabricator.khanacademy.org/D32420.)
|
|
434
|
-
this.widgetIds = [];
|
|
435
|
-
|
|
436
|
-
if (this.translationIndex != null) {
|
|
437
|
-
// NOTE(jeremy): Since the translationIndex is simply the array
|
|
438
|
-
// index of each renderer, we can't remove Renderers from this
|
|
439
|
-
// list, rather, we simply null out the entry (which means that
|
|
440
|
-
// this array's growth is unbounded until a page reload).
|
|
441
|
-
getDependencies().rendererTranslationComponents.removeComponentAtIndex(
|
|
442
|
-
this.translationIndex,
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
this._isMounted = false;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
getApiOptions: () => APIOptionsWithDefaults = () => {
|
|
450
|
-
return {
|
|
451
|
-
...ApiOptions.defaults,
|
|
452
|
-
...this.props.apiOptions,
|
|
453
|
-
};
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
_getInitialWidgetState: (props: Props) => {
|
|
457
|
-
widgetInfo: State["widgetInfo"];
|
|
458
|
-
widgetProps: State["widgetProps"];
|
|
459
|
-
} = (props: Props) => {
|
|
460
|
-
const allWidgetInfo = this._getAllWidgetsInfo(props);
|
|
461
|
-
return {
|
|
462
|
-
widgetInfo: allWidgetInfo,
|
|
463
|
-
widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props),
|
|
464
|
-
};
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
// @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
|
|
468
|
-
_getAllWidgetsInfo: (props: Props) => {
|
|
469
|
-
[key: string]: PerseusWidget;
|
|
470
|
-
} = (props: Props) => {
|
|
471
|
-
// @ts-expect-error - TS2345 - Argument of type '{ [key: string]: PerseusWidget; }' is not assignable to parameter of type 'Partial<Record<string, CategorizerWidget>>'.
|
|
472
|
-
return mapObject(props.widgets, (widgetInfo, widgetId) => {
|
|
473
|
-
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
474
|
-
const newValues: Record<string, any> = {};
|
|
475
|
-
|
|
476
|
-
if (!widgetInfo.type) {
|
|
477
|
-
newValues.type = widgetId.split(" ")[0];
|
|
478
|
-
}
|
|
479
|
-
if (!widgetInfo.alignment) {
|
|
480
|
-
newValues.alignment = "default";
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
widgetInfo = _.extend({}, widgetInfo, newValues);
|
|
484
|
-
}
|
|
485
|
-
return Widgets.upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
486
|
-
});
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
_getAllWidgetsStartProps: (
|
|
490
|
-
allWidgetInfo: PerseusWidgetMap,
|
|
491
|
-
props: Props,
|
|
492
|
-
) => PerseusWidgetMap = (allWidgetInfo, props) => {
|
|
493
|
-
const {apiOptions, problemNum} = props;
|
|
494
|
-
const widgetsStartProps: PerseusWidgetMap = {};
|
|
495
|
-
entries(allWidgetInfo).forEach(([key, widgetInfo]) => {
|
|
496
|
-
widgetsStartProps[key] = Widgets.getRendererPropsForWidgetInfo(
|
|
497
|
-
widgetInfo,
|
|
498
|
-
problemNum,
|
|
499
|
-
);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// Call the onWidgetStartProps callback if it exists
|
|
503
|
-
apiOptions?.onWidgetStartProps?.(widgetsStartProps);
|
|
504
|
-
|
|
505
|
-
return widgetsStartProps;
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
// This is only used in _getWidgetInfo as a fallback if widgetId
|
|
509
|
-
// doesn't exist in this.state.widgetInfo. It doesn't get run as
|
|
510
|
-
// part of the happy path.
|
|
511
|
-
// TODO(LP-10713): Refactor how we handle widgetIds that don't exist
|
|
512
|
-
// in this.state.widgetInfo.
|
|
513
|
-
_getDefaultWidgetInfo: (widgetId: string) => any = (widgetId: string) => {
|
|
514
|
-
const widgetIdParts = Util.rTypeFromWidgetId.exec(widgetId);
|
|
515
|
-
if (widgetIdParts == null) {
|
|
516
|
-
// We should probably return null here since there's no
|
|
517
|
-
// widget with the given id.
|
|
518
|
-
// NOTE(jeremy): Further, the widgetId we were given does not even
|
|
519
|
-
// look like a widget ID (ie. `widget-type \d+`). I can't figure
|
|
520
|
-
// out how to trigger this line of code though.
|
|
521
|
-
/* c8 ignore next line */
|
|
522
|
-
return {};
|
|
523
|
-
}
|
|
524
|
-
return {
|
|
525
|
-
type: widgetIdParts[1],
|
|
526
|
-
graded: true,
|
|
527
|
-
options: {},
|
|
528
|
-
};
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
_getWidgetInfo: (widgetId: string) => PerseusWidget = (
|
|
532
|
-
widgetId: string,
|
|
533
|
-
): PerseusWidget => {
|
|
534
|
-
return (
|
|
535
|
-
this.state.widgetInfo[widgetId] ||
|
|
536
|
-
this._getDefaultWidgetInfo(widgetId)
|
|
537
|
-
);
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
renderWidget: (
|
|
541
|
-
impliedType: string,
|
|
542
|
-
id: string,
|
|
543
|
-
state: WidgetState,
|
|
544
|
-
) => null | React.ReactNode = (
|
|
545
|
-
impliedType: string,
|
|
546
|
-
id: string,
|
|
547
|
-
state: WidgetState,
|
|
548
|
-
) => {
|
|
549
|
-
const widgetInfo = this.state.widgetInfo[id];
|
|
550
|
-
|
|
551
|
-
if (widgetInfo && widgetInfo.alignment === "full-width") {
|
|
552
|
-
state.foundFullWidth = true;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (widgetInfo) {
|
|
556
|
-
const type = (widgetInfo && widgetInfo.type) || impliedType;
|
|
557
|
-
const shouldHighlight = _.contains(
|
|
558
|
-
// @ts-expect-error - TS2345 - Argument of type 'readonly any[] | undefined' is not assignable to parameter of type 'Collection<any>'.
|
|
559
|
-
this.props.highlightedWidgets,
|
|
560
|
-
id,
|
|
561
|
-
);
|
|
562
|
-
|
|
563
|
-
// By this point we should have no duplicates, which are
|
|
564
|
-
// filtered out in this.render(), so we shouldn't have to
|
|
565
|
-
// worry about using this widget key and ref:
|
|
566
|
-
return (
|
|
567
|
-
<WidgetContainer
|
|
568
|
-
key={makeContainerId(id)}
|
|
569
|
-
id={id}
|
|
570
|
-
ref={(node) => {
|
|
571
|
-
const containerId = makeContainerId(id);
|
|
572
|
-
if (node != null) {
|
|
573
|
-
this._widgetContainers.set(containerId, node);
|
|
574
|
-
} else {
|
|
575
|
-
this._widgetContainers.delete(containerId);
|
|
576
|
-
}
|
|
577
|
-
}}
|
|
578
|
-
type={type}
|
|
579
|
-
initialProps={this.getWidgetProps(id)}
|
|
580
|
-
shouldHighlight={shouldHighlight}
|
|
581
|
-
linterContext={PerseusLinter.pushContextStack(
|
|
582
|
-
this.props.linterContext,
|
|
583
|
-
"widget",
|
|
584
|
-
)}
|
|
585
|
-
/>
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
return null;
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
getWidgetProps: (id: string) => WidgetProps<any, PerseusWidgetOptions> = (
|
|
592
|
-
id,
|
|
593
|
-
) => {
|
|
594
|
-
const apiOptions = this.getApiOptions();
|
|
595
|
-
const widgetProps = this.state.widgetProps[id] || {};
|
|
596
|
-
|
|
597
|
-
// The widget needs access to its "rubric" at all times when in review
|
|
598
|
-
// mode (which is really just part of its widget info).
|
|
599
|
-
let reviewModeRubric = null;
|
|
600
|
-
const widgetInfo = this.state.widgetInfo[id];
|
|
601
|
-
if (this.props.reviewMode && widgetInfo) {
|
|
602
|
-
reviewModeRubric = widgetInfo.options;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (!this._interactionTrackers) {
|
|
606
|
-
this._interactionTrackers = {};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
let interactionTracker = this._interactionTrackers[id];
|
|
610
|
-
if (!interactionTracker) {
|
|
611
|
-
interactionTracker = this._interactionTrackers[id] =
|
|
612
|
-
new InteractionTracker(
|
|
613
|
-
apiOptions.trackInteraction,
|
|
614
|
-
// @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
|
|
615
|
-
widgetInfo && widgetInfo.type,
|
|
616
|
-
id,
|
|
617
|
-
// @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
|
|
618
|
-
Widgets.getTracking(widgetInfo && widgetInfo.type),
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return {
|
|
623
|
-
...widgetProps,
|
|
624
|
-
widgetId: id,
|
|
625
|
-
alignment: widgetInfo && widgetInfo.alignment,
|
|
626
|
-
// When determining if a widget is static, we verify that the widget is not an
|
|
627
|
-
// exercise question by verifying that it has no problem number.
|
|
628
|
-
static: widgetInfo && widgetInfo.static && !this.props.problemNum,
|
|
629
|
-
problemNum: this.props.problemNum,
|
|
630
|
-
apiOptions: this.getApiOptions(),
|
|
631
|
-
keypadElement: this.props.keypadElement,
|
|
632
|
-
questionCompleted: this.props.questionCompleted,
|
|
633
|
-
onFocus: _.partial(this._onWidgetFocus, id),
|
|
634
|
-
onBlur: _.partial(this._onWidgetBlur, id),
|
|
635
|
-
findWidgets: this.findWidgets,
|
|
636
|
-
reviewModeRubric: reviewModeRubric,
|
|
637
|
-
onChange: (newProps, cb, silent = false) => {
|
|
638
|
-
this._setWidgetProps(id, newProps, cb, silent);
|
|
639
|
-
},
|
|
640
|
-
trackInteraction: interactionTracker.track,
|
|
641
|
-
isLastUsedWidget: id === this.state.lastUsedWidgetId,
|
|
642
|
-
};
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Serializes the questions state so it can be recovered.
|
|
647
|
-
*
|
|
648
|
-
* The return value of this function can be sent to the
|
|
649
|
-
* `restoreSerializedState` method to restore this state.
|
|
650
|
-
*
|
|
651
|
-
* If an instance of widgetProps is passed in, it generates the serialized
|
|
652
|
-
* state from that instead of the current widget props.
|
|
653
|
-
*/
|
|
654
|
-
getSerializedState: (widgetProps?: any) => {
|
|
655
|
-
[id: string]: any;
|
|
656
|
-
} = (
|
|
657
|
-
widgetProps: any,
|
|
658
|
-
): {
|
|
659
|
-
[id: string]: any;
|
|
660
|
-
} => {
|
|
661
|
-
return mapObject(
|
|
662
|
-
widgetProps || this.state.widgetProps,
|
|
663
|
-
(props, widgetId) => {
|
|
664
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
665
|
-
if (widget && widget.getSerializedState) {
|
|
666
|
-
return widget.getSerializedState();
|
|
667
|
-
}
|
|
668
|
-
return props;
|
|
669
|
-
},
|
|
670
|
-
);
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
restoreSerializedState: (
|
|
674
|
-
serializedState: SerializedState,
|
|
675
|
-
callback?: () => void,
|
|
676
|
-
) => void = (serializedState: SerializedState, callback?: () => void) => {
|
|
677
|
-
// Do some basic validation on the serialized state (just make sure the
|
|
678
|
-
// widget IDs are what we expect).
|
|
679
|
-
const serializedWidgetIds = _.keys(serializedState);
|
|
680
|
-
const widgetPropIds = _.keys(this.state.widgetProps);
|
|
681
|
-
|
|
682
|
-
// If the two lists of IDs match (ignoring order)
|
|
683
|
-
if (
|
|
684
|
-
serializedWidgetIds.length !== widgetPropIds.length ||
|
|
685
|
-
_.intersection(serializedWidgetIds, widgetPropIds).length !==
|
|
686
|
-
serializedWidgetIds.length
|
|
687
|
-
) {
|
|
688
|
-
Log.error(
|
|
689
|
-
"Refusing to restore bad serialized state:",
|
|
690
|
-
Errors.Internal,
|
|
691
|
-
{
|
|
692
|
-
loggedMetadata: {
|
|
693
|
-
serializedState: JSON.stringify(serializedState),
|
|
694
|
-
currentProps: JSON.stringify(this.state.widgetProps),
|
|
695
|
-
},
|
|
696
|
-
},
|
|
697
|
-
);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// We want to wait until any children widgets who have a
|
|
702
|
-
// restoreSerializedState function also call their own callbacks before
|
|
703
|
-
// we declare that the operation is finished.
|
|
704
|
-
let numCallbacks = 1;
|
|
705
|
-
const fireCallback = () => {
|
|
706
|
-
--numCallbacks;
|
|
707
|
-
if (callback && numCallbacks === 0) {
|
|
708
|
-
callback();
|
|
709
|
-
}
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
this.setState(
|
|
713
|
-
{
|
|
714
|
-
widgetProps: mapObject(serializedState, (props, widgetId) => {
|
|
715
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
716
|
-
if (widget && widget.restoreSerializedState) {
|
|
717
|
-
// Note that we probably can't call
|
|
718
|
-
// `this.change()/this.props.onChange()` in this
|
|
719
|
-
// function, so we take the return value and use
|
|
720
|
-
// that as props if necessary so that
|
|
721
|
-
// `restoreSerializedState` in a widget can
|
|
722
|
-
// change the props as well as state.
|
|
723
|
-
// If a widget has no props to change, it can
|
|
724
|
-
// safely return null.
|
|
725
|
-
++numCallbacks;
|
|
726
|
-
const restoreResult = widget.restoreSerializedState(
|
|
727
|
-
props,
|
|
728
|
-
fireCallback,
|
|
729
|
-
);
|
|
730
|
-
return _.extend(
|
|
731
|
-
{},
|
|
732
|
-
this.state.widgetProps[widgetId],
|
|
733
|
-
restoreResult,
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
return props;
|
|
737
|
-
}),
|
|
738
|
-
},
|
|
739
|
-
() => {
|
|
740
|
-
// Wait until all components have rendered. In React 16 setState
|
|
741
|
-
// callback fires immediately after this componentDidUpdate, and
|
|
742
|
-
// there is no guarantee that parent/siblings components have
|
|
743
|
-
// finished rendering.
|
|
744
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
745
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
746
|
-
setTimeout(fireCallback, 0);
|
|
747
|
-
},
|
|
748
|
-
);
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Tell each of the radio widgets to show rationales for each of the
|
|
753
|
-
* currently selected choices inside of them. If the widget is correct, it
|
|
754
|
-
* shows rationales for all of the choices. This also disables interaction
|
|
755
|
-
* with the choices that we show rationales for.
|
|
756
|
-
*/
|
|
757
|
-
showRationalesForCurrentlySelectedChoices: () => void = () => {
|
|
758
|
-
Object.keys(this.props.widgets).forEach((widgetId) => {
|
|
759
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
760
|
-
if (widget && widget.showRationalesForCurrentlySelectedChoices) {
|
|
761
|
-
widget.showRationalesForCurrentlySelectedChoices(
|
|
762
|
-
this._getWidgetInfo(widgetId).options,
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Tells each of the radio widgets to deselect any of the incorrect choices
|
|
770
|
-
* that are currently selected (leaving correct choices still selected).
|
|
771
|
-
*/
|
|
772
|
-
deselectIncorrectSelectedChoices: () => void = () => {
|
|
773
|
-
// TODO(emily): this has the exact same structure as
|
|
774
|
-
// showRationalesForCurrentlySelectedChoices above. Maybe DRY this up.
|
|
775
|
-
Object.keys(this.props.widgets).forEach((widgetId) => {
|
|
776
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
777
|
-
if (widget && widget.deselectIncorrectSelectedChoices) {
|
|
778
|
-
widget.deselectIncorrectSelectedChoices();
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Allows inter-widget communication.
|
|
785
|
-
*
|
|
786
|
-
* This function yields this Renderer's own internal widgets, and it's used
|
|
787
|
-
* in two places.
|
|
788
|
-
*
|
|
789
|
-
* First, we expose our own internal widgets to each other by giving them
|
|
790
|
-
* a `findWidgets` function that, in turn, calls this function.
|
|
791
|
-
*
|
|
792
|
-
* Second, we expose our own internal widgets to this Renderer's parent,
|
|
793
|
-
* by allowing it to call this function directly. That way, it can hook us
|
|
794
|
-
* up to other Renderers on the page, by writing a `findExternalWidgets`
|
|
795
|
-
* prop that calls each other Renderer's `findInternalWidgets` function.
|
|
796
|
-
*
|
|
797
|
-
* Takes a `filterCriterion` on which widgets to return.
|
|
798
|
-
* `filterCriterion` can be one of:
|
|
799
|
-
* * A string widget id
|
|
800
|
-
* * A string widget type
|
|
801
|
-
* * a function from (id, widgetInfo, widgetComponent) to true or false
|
|
802
|
-
*
|
|
803
|
-
* Returns an array of the matching widget components.
|
|
804
|
-
*
|
|
805
|
-
* If you need to do logic with more than the components, it is possible
|
|
806
|
-
* to do such logic inside the filter, rather than on the result array.
|
|
807
|
-
*
|
|
808
|
-
* See the passage-ref widget for an example.
|
|
809
|
-
*
|
|
810
|
-
* "Remember: abilities are not inherently good or evil, it's how you use
|
|
811
|
-
* them." ~ Kyle Katarn
|
|
812
|
-
* Please use this one with caution.
|
|
813
|
-
*/
|
|
814
|
-
findInternalWidgets: (
|
|
815
|
-
filterCriterion: FilterCriterion,
|
|
816
|
-
) => ReadonlyArray<Widget | null | undefined> = (
|
|
817
|
-
filterCriterion: FilterCriterion,
|
|
818
|
-
) => {
|
|
819
|
-
let filterFunc;
|
|
820
|
-
// Convenience filters:
|
|
821
|
-
// "interactive-graph 3" will give you [[interactive-graph 3]]
|
|
822
|
-
// "interactive-graph" will give you all interactive-graphs
|
|
823
|
-
if (typeof filterCriterion === "string") {
|
|
824
|
-
if (filterCriterion.indexOf(" ") !== -1) {
|
|
825
|
-
const widgetId = filterCriterion;
|
|
826
|
-
filterFunc = (
|
|
827
|
-
id: string,
|
|
828
|
-
widgetInfo: PerseusWidget,
|
|
829
|
-
widget?: Widget | null,
|
|
830
|
-
) => id === widgetId;
|
|
831
|
-
} else {
|
|
832
|
-
const widgetType = filterCriterion;
|
|
833
|
-
filterFunc = (
|
|
834
|
-
id: string,
|
|
835
|
-
widgetInfo: PerseusWidget,
|
|
836
|
-
widget?: Widget | null,
|
|
837
|
-
) => {
|
|
838
|
-
return widgetInfo.type === widgetType;
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
} else {
|
|
842
|
-
filterFunc = filterCriterion;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const results: ReadonlyArray<Widget | null | undefined> = this.widgetIds
|
|
846
|
-
.filter((id: string) => {
|
|
847
|
-
const widgetInfo = this._getWidgetInfo(id);
|
|
848
|
-
const widget = this.getWidgetInstance(id);
|
|
849
|
-
return filterFunc(id, widgetInfo, widget);
|
|
850
|
-
})
|
|
851
|
-
.map(this.getWidgetInstance);
|
|
852
|
-
|
|
853
|
-
return results;
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Allows inter-widget communication.
|
|
858
|
-
*
|
|
859
|
-
* Includes both widgets internal to this Renderer, and external widgets
|
|
860
|
-
* exposed by the `findExternalWidgets` prop.
|
|
861
|
-
*
|
|
862
|
-
* See `findInteralWidgets` for more information.
|
|
863
|
-
*/
|
|
864
|
-
findWidgets: (filterCriterion: FilterCriterion) => any = (
|
|
865
|
-
filterCriterion: FilterCriterion,
|
|
866
|
-
) => {
|
|
867
|
-
return [
|
|
868
|
-
...this.findInternalWidgets(filterCriterion),
|
|
869
|
-
...this.props.findExternalWidgets(filterCriterion),
|
|
870
|
-
];
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
getWidgetInstance: (id: string) => Widget | null | undefined = (id) => {
|
|
874
|
-
const ref = this._widgetContainers.get(makeContainerId(id));
|
|
875
|
-
if (!ref) {
|
|
876
|
-
return null;
|
|
877
|
-
}
|
|
878
|
-
return ref.getWidget();
|
|
879
|
-
};
|
|
880
|
-
|
|
881
|
-
_onWidgetFocus: (id: string, focusPath?: ReadonlyArray<string>) => void = (
|
|
882
|
-
id: string,
|
|
883
|
-
focusPath: ReadonlyArray<string> = [],
|
|
884
|
-
) => {
|
|
885
|
-
if (!_.isArray(focusPath)) {
|
|
886
|
-
throw new PerseusError(
|
|
887
|
-
"widget props.onFocus focusPath must be an Array, " +
|
|
888
|
-
"but was" +
|
|
889
|
-
JSON.stringify(focusPath),
|
|
890
|
-
Errors.Internal,
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
this._setCurrentFocus([id].concat(focusPath));
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
_onWidgetBlur: (id: string, blurPath: FocusPath) => void = (
|
|
897
|
-
id: string,
|
|
898
|
-
blurPath: FocusPath,
|
|
899
|
-
) => {
|
|
900
|
-
const blurringFocusPath = this._currentFocus;
|
|
901
|
-
|
|
902
|
-
// Failsafe: abort if ID is different, because focus probably happened
|
|
903
|
-
// before blur
|
|
904
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
905
|
-
const fullPath = [id].concat(blurPath);
|
|
906
|
-
if (!_.isEqual(fullPath, blurringFocusPath)) {
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Wait until after any new focus events fire this tick before
|
|
911
|
-
// declaring that nothing is focused.
|
|
912
|
-
// If a different widget was focused, we'll see an onBlur event
|
|
913
|
-
// now, but then an onFocus event on a different element before
|
|
914
|
-
// this callback is executed
|
|
915
|
-
_.defer(() => {
|
|
916
|
-
if (_.isEqual(this._currentFocus, blurringFocusPath)) {
|
|
917
|
-
this._setCurrentFocus(null);
|
|
918
|
-
}
|
|
919
|
-
});
|
|
920
|
-
};
|
|
921
|
-
|
|
922
|
-
getContent: (props: Props, state: State) => any = (
|
|
923
|
-
props: Props,
|
|
924
|
-
state: State,
|
|
925
|
-
) => {
|
|
926
|
-
return state.jiptContent || props.content;
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
shouldRenderJiptPlaceholder: (props: Props, state: State) => boolean = (
|
|
930
|
-
props: Props,
|
|
931
|
-
state: State,
|
|
932
|
-
): boolean => {
|
|
933
|
-
// TODO(aria): Pass this in via webapp as an apiOption
|
|
934
|
-
return (
|
|
935
|
-
getDependencies().JIPT.useJIPT &&
|
|
936
|
-
state.jiptContent == null &&
|
|
937
|
-
props.content.indexOf("crwdns") !== -1
|
|
938
|
-
);
|
|
939
|
-
};
|
|
940
|
-
|
|
941
|
-
replaceJiptContent: (content: string, paragraphIndex: number) => void = (
|
|
942
|
-
content: string,
|
|
943
|
-
paragraphIndex: number,
|
|
944
|
-
) => {
|
|
945
|
-
if (paragraphIndex == null) {
|
|
946
|
-
// we're not translating paragraph-wise; replace the whole content
|
|
947
|
-
// (we could also theoretically check for apiOptions.isArticle
|
|
948
|
-
// here, which is what causes paragraphIndex to not be null)
|
|
949
|
-
this.setState({
|
|
950
|
-
jiptContent: content,
|
|
951
|
-
});
|
|
952
|
-
} else {
|
|
953
|
-
// This is the same regex we use in perseus/translate.py to find
|
|
954
|
-
// code blocks. We use it to count entire code blocks as
|
|
955
|
-
// paragraphs.
|
|
956
|
-
const codeFenceRegex =
|
|
957
|
-
/^\s*(`{3,}|~{3,})\s*(\S+)?\s*\n([\s\S]+?)\s*\1\s*$/;
|
|
958
|
-
|
|
959
|
-
if (codeFenceRegex.test(content)) {
|
|
960
|
-
// If a paragraph is a code block, we're going to treat it as a
|
|
961
|
-
// single paragraph even if it has double-newlines in it, so
|
|
962
|
-
// skip the next two checks.
|
|
963
|
-
} else if (/\S\n\s*\n\S/.test(content)) {
|
|
964
|
-
// Our "render the exact same QuestionParagraphs each time"
|
|
965
|
-
// strategy will fail if we allow translating a paragraph
|
|
966
|
-
// to more than one paragraph. This hack renders as a single
|
|
967
|
-
// paragraph and lets the translator know to not use \n\n,
|
|
968
|
-
// hopefully. We can't wait for linting because we can't
|
|
969
|
-
// safely render the node.
|
|
970
|
-
// TODO(aria): Check for the max number of backticks or tildes
|
|
971
|
-
// in the content, and just render a red code block of the
|
|
972
|
-
// content here instead?
|
|
973
|
-
content =
|
|
974
|
-
"$\\large{\\red{\\text{Please translate each " +
|
|
975
|
-
"paragraph to a single paragraph.}}}$";
|
|
976
|
-
} else if (/^\s*$/.test(content)) {
|
|
977
|
-
// We similarly can't have an all-whitespace paragraph, or
|
|
978
|
-
// we will parse it as the closing of the previous paragraph
|
|
979
|
-
content =
|
|
980
|
-
"$\\large{\\red{\\text{Translated paragraph is " +
|
|
981
|
-
"currently empty}}}$";
|
|
982
|
-
}
|
|
983
|
-
// Split the paragraphs; we have to use getContent() in case
|
|
984
|
-
// nothing has been translated yet (in which case we just have
|
|
985
|
-
// this.props.content)
|
|
986
|
-
const allContent = this.getContent(this.props, this.state);
|
|
987
|
-
const paragraphs = JiptParagraphs.parseToArray(allContent);
|
|
988
|
-
paragraphs[paragraphIndex] = content;
|
|
989
|
-
this.setState({
|
|
990
|
-
jiptContent: JiptParagraphs.joinFromArray(paragraphs),
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
};
|
|
994
|
-
|
|
995
|
-
// wrap top-level elements in a QuestionParagraph, mostly
|
|
996
|
-
// for appropriate spacing and other css
|
|
997
|
-
// @ts-expect-error - TS2322 - Type '(ast: any, state: WidgetState) => never[] | JSX.Element' is not assignable to type '(ast: any, state: WidgetState) => ReactElement<any, string | JSXElementConstructor<any>>'.
|
|
998
|
-
outputMarkdown: (ast: any, state: WidgetState) => React.ReactElement = (
|
|
999
|
-
ast: any,
|
|
1000
|
-
state: WidgetState,
|
|
1001
|
-
) => {
|
|
1002
|
-
if (_.isArray(ast)) {
|
|
1003
|
-
// This is duplicated from simple-markdown
|
|
1004
|
-
// TODO(aria): Don't duplicate this logic
|
|
1005
|
-
const oldKey = state.key;
|
|
1006
|
-
const result = [];
|
|
1007
|
-
|
|
1008
|
-
// map nestedOutput over the ast, except group any text
|
|
1009
|
-
// nodes together into a single string output.
|
|
1010
|
-
// NOTE(aria): These are never strings--always QuestionParagraphs
|
|
1011
|
-
// TODO(aria): We probably don't need this string logic here.
|
|
1012
|
-
let lastWasString = false;
|
|
1013
|
-
for (let i = 0; i < ast.length; i++) {
|
|
1014
|
-
state.key = i;
|
|
1015
|
-
state.paragraphIndex = i;
|
|
1016
|
-
const nodeOut = this.outputMarkdown(ast[i], state);
|
|
1017
|
-
const isString = typeof nodeOut === "string";
|
|
1018
|
-
// NOTE(jeremy): As far as I can tell, this if is _never_
|
|
1019
|
-
// reached. As noted above, these are always QuestionParagraphs
|
|
1020
|
-
// now.
|
|
1021
|
-
/* c8 ignore if */
|
|
1022
|
-
if (typeof nodeOut === "string" && lastWasString) {
|
|
1023
|
-
// @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
|
|
1024
|
-
result[result.length - 1] += nodeOut;
|
|
1025
|
-
} else {
|
|
1026
|
-
// @ts-expect-error - TS2345 - Argument of type 'ReactElement<any, string | JSXElementConstructor<any>>' is not assignable to parameter of type 'never'.
|
|
1027
|
-
result.push(nodeOut);
|
|
1028
|
-
}
|
|
1029
|
-
lastWasString = isString;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
state.key = oldKey;
|
|
1033
|
-
return result;
|
|
1034
|
-
}
|
|
1035
|
-
// !!! WARNING: Mutative hacks! mutates `this._foundTextNodes`:
|
|
1036
|
-
// because I wrote a bad interface to simple-markdown.js' `output`
|
|
1037
|
-
this._foundTextNodes = false;
|
|
1038
|
-
state.foundFullWidth = false;
|
|
1039
|
-
const output = this.outputNested(ast, state);
|
|
1040
|
-
|
|
1041
|
-
// In Jipt-land, we need to render the exact same outer
|
|
1042
|
-
// QuestionParagraph nodes always. This means the number of
|
|
1043
|
-
// paragraphs needs to stay the same, and we can't modify
|
|
1044
|
-
// the classnames on the QuestionParagraphs or we'll destroy
|
|
1045
|
-
// the crowdin classnames. So we just only use the
|
|
1046
|
-
// 'paragraph' classname from the QuestionParagraph.
|
|
1047
|
-
// If this becomes a problem it would be easy to fix by wrapping
|
|
1048
|
-
// the nodes in an extra layer (hopefully only for jipt) that
|
|
1049
|
-
// handles the jipt classnames, and let this layer handle the
|
|
1050
|
-
// dynamic classnames.
|
|
1051
|
-
// We can't render the classes the first time and leave them
|
|
1052
|
-
// the same because we don't know at the time of the first
|
|
1053
|
-
// render whether they are full-bleed or centered, since they
|
|
1054
|
-
// only contain crowdin IDs like `crwdns:972384209:0...`
|
|
1055
|
-
let className;
|
|
1056
|
-
if (this.translationIndex != null) {
|
|
1057
|
-
className = null;
|
|
1058
|
-
} else {
|
|
1059
|
-
className = classNames({
|
|
1060
|
-
"perseus-paragraph-centered": !this._foundTextNodes,
|
|
1061
|
-
// There is only one node being rendered,
|
|
1062
|
-
// and it's a full-width widget.
|
|
1063
|
-
"perseus-paragraph-full-width":
|
|
1064
|
-
state.foundFullWidth && ast.content.length === 1,
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
return (
|
|
1069
|
-
<QuestionParagraph
|
|
1070
|
-
key={state.key}
|
|
1071
|
-
className={className}
|
|
1072
|
-
translationIndex={this.translationIndex}
|
|
1073
|
-
paragraphIndex={state.paragraphIndex}
|
|
1074
|
-
inline={this.props.inline}
|
|
1075
|
-
>
|
|
1076
|
-
<ErrorBoundary>{output}</ErrorBoundary>
|
|
1077
|
-
</QuestionParagraph>
|
|
1078
|
-
);
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
|
-
// output non-top-level nodes or arrays
|
|
1082
|
-
outputNested: (ast: any, state: WidgetState) => React.ReactElement = (
|
|
1083
|
-
ast: any,
|
|
1084
|
-
state: WidgetState,
|
|
1085
|
-
) => {
|
|
1086
|
-
if (_.isArray(ast)) {
|
|
1087
|
-
// This is duplicated from simple-markdown
|
|
1088
|
-
// TODO(aria): Don't duplicate this logic
|
|
1089
|
-
const oldKey = state.key;
|
|
1090
|
-
const result: Array<string | React.ReactNode> = [];
|
|
1091
|
-
|
|
1092
|
-
// map nestedOutput over the ast, except group any text
|
|
1093
|
-
// nodes together into a single string output.
|
|
1094
|
-
let lastWasString = false;
|
|
1095
|
-
for (let i = 0; i < ast.length; i++) {
|
|
1096
|
-
state.key = i;
|
|
1097
|
-
const nodeOut = this.outputNested(ast[i], state);
|
|
1098
|
-
const isString = typeof nodeOut === "string";
|
|
1099
|
-
if (typeof nodeOut === "string" && lastWasString) {
|
|
1100
|
-
/**
|
|
1101
|
-
* We know that last was string, but TypeScript can't see this
|
|
1102
|
-
* refinement.
|
|
1103
|
-
*/
|
|
1104
|
-
result[result.length - 1] += nodeOut;
|
|
1105
|
-
} else {
|
|
1106
|
-
result.push(nodeOut);
|
|
1107
|
-
}
|
|
1108
|
-
lastWasString = isString;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
state.key = oldKey;
|
|
1112
|
-
return result;
|
|
1113
|
-
}
|
|
1114
|
-
return this.outputNode(ast, this.outputNested, state);
|
|
1115
|
-
};
|
|
1116
|
-
|
|
1117
|
-
// output individual AST nodes [not arrays]
|
|
1118
|
-
outputNode: (
|
|
1119
|
-
node: any,
|
|
1120
|
-
nestedOutput: any,
|
|
1121
|
-
state: WidgetState,
|
|
1122
|
-
) =>
|
|
1123
|
-
| any
|
|
1124
|
-
| null
|
|
1125
|
-
| React.ReactElement<React.ComponentProps<"div">>
|
|
1126
|
-
| React.ReactElement<React.ComponentProps<"span">>
|
|
1127
|
-
| React.ReactNode = (
|
|
1128
|
-
node: any,
|
|
1129
|
-
nestedOutput: any,
|
|
1130
|
-
state: WidgetState,
|
|
1131
|
-
) => {
|
|
1132
|
-
const apiOptions = this.getApiOptions();
|
|
1133
|
-
const imagePlaceholder = apiOptions.imagePlaceholder;
|
|
1134
|
-
|
|
1135
|
-
if (node.type === "widget") {
|
|
1136
|
-
const widgetPlaceholder = apiOptions.widgetPlaceholder;
|
|
1137
|
-
|
|
1138
|
-
if (widgetPlaceholder) {
|
|
1139
|
-
return widgetPlaceholder;
|
|
1140
|
-
}
|
|
1141
|
-
// Widgets can contain text nodes, so we don't center them with
|
|
1142
|
-
// markdown magic here.
|
|
1143
|
-
// Instead, we center them with css magic in articles.less
|
|
1144
|
-
// /cry(aria)
|
|
1145
|
-
this._foundTextNodes = true;
|
|
1146
|
-
|
|
1147
|
-
if (_.contains(this.widgetIds, node.id)) {
|
|
1148
|
-
// We don't want to render a duplicate widget key/ref,
|
|
1149
|
-
// as this causes problems with react (for obvious
|
|
1150
|
-
// reasons). Instead we just notify the
|
|
1151
|
-
// hopefully-content-creator that they need to change the
|
|
1152
|
-
// widget id.
|
|
1153
|
-
return (
|
|
1154
|
-
<span key={state.key} className="renderer-widget-error">
|
|
1155
|
-
{[
|
|
1156
|
-
"Widget [[",
|
|
1157
|
-
"☃",
|
|
1158
|
-
" ",
|
|
1159
|
-
node.id,
|
|
1160
|
-
"]] already exists.",
|
|
1161
|
-
].join("")}
|
|
1162
|
-
</span>
|
|
1163
|
-
);
|
|
1164
|
-
}
|
|
1165
|
-
this.widgetIds.push(node.id);
|
|
1166
|
-
return this.renderWidget(node.widgetType, node.id, state);
|
|
1167
|
-
}
|
|
1168
|
-
if (node.type === "blockMath") {
|
|
1169
|
-
// We render math here instead of in perseus-markdown.jsx
|
|
1170
|
-
// because we need to pass it our onRender callback.
|
|
1171
|
-
const content = preprocessTex(node.content);
|
|
1172
|
-
|
|
1173
|
-
const innerStyle = {
|
|
1174
|
-
// HACK(benkomalo): we only want horizontal scrolling, but
|
|
1175
|
-
// overflowX: 'auto' causes a vertical scrolling scrollbar
|
|
1176
|
-
// as well, despite the parent and child elements having
|
|
1177
|
-
// the exact same height. Force it to not scroll by
|
|
1178
|
-
// applying overflowY: 'hidden'
|
|
1179
|
-
overflowX: "auto",
|
|
1180
|
-
overflowY: "hidden",
|
|
1181
|
-
|
|
1182
|
-
// HACK(kevinb): overflowY: 'hidden' inadvertently clips the
|
|
1183
|
-
// top and bottom of some fractions. We add padding to the
|
|
1184
|
-
// top and bottom to avoid the clipping and then correct for
|
|
1185
|
-
// the padding by adding equal but opposite margins.
|
|
1186
|
-
paddingTop: 10,
|
|
1187
|
-
paddingBottom: 10,
|
|
1188
|
-
marginTop: -10,
|
|
1189
|
-
marginBottom: -10,
|
|
1190
|
-
} as const;
|
|
1191
|
-
|
|
1192
|
-
if (apiOptions.isMobile) {
|
|
1193
|
-
// The style for the body of articles and exercises on mobile is
|
|
1194
|
-
// to have a 16px margin. When a user taps to zoom math we'd
|
|
1195
|
-
// like the math to extend all the way to the edge of the page/
|
|
1196
|
-
// To achieve this affect we nest the Zoomable component in two
|
|
1197
|
-
// nested divs. The outer div has a negative margin to
|
|
1198
|
-
// counteract the margin on main perseus container. The inner
|
|
1199
|
-
// div adds the margin back as padding so that when the math is
|
|
1200
|
-
// scaled out it's inset from the edge of the page. When the
|
|
1201
|
-
// TeX component is full size it will extend to the edge of the
|
|
1202
|
-
// page if it's larger than the page.
|
|
1203
|
-
//
|
|
1204
|
-
// TODO(kevinb) automatically determine the margin size
|
|
1205
|
-
const margin = 16;
|
|
1206
|
-
const outerStyle = {
|
|
1207
|
-
marginLeft: -margin,
|
|
1208
|
-
marginRight: -margin,
|
|
1209
|
-
} as const;
|
|
1210
|
-
const horizontalPadding = {
|
|
1211
|
-
paddingLeft: margin,
|
|
1212
|
-
paddingRight: margin,
|
|
1213
|
-
} as const;
|
|
1214
|
-
|
|
1215
|
-
const mobileInnerStyle = {
|
|
1216
|
-
...innerStyle,
|
|
1217
|
-
...styles.mobileZoomableParentFix,
|
|
1218
|
-
} as const;
|
|
1219
|
-
|
|
1220
|
-
return (
|
|
1221
|
-
<div
|
|
1222
|
-
key={state.key}
|
|
1223
|
-
className="perseus-block-math"
|
|
1224
|
-
style={outerStyle}
|
|
1225
|
-
>
|
|
1226
|
-
<ErrorBoundary>
|
|
1227
|
-
<div
|
|
1228
|
-
className="perseus-block-math-inner"
|
|
1229
|
-
style={{
|
|
1230
|
-
...mobileInnerStyle,
|
|
1231
|
-
...horizontalPadding,
|
|
1232
|
-
}}
|
|
1233
|
-
>
|
|
1234
|
-
<ZoomableTeX>{content}</ZoomableTeX>
|
|
1235
|
-
</div>
|
|
1236
|
-
</ErrorBoundary>
|
|
1237
|
-
</div>
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
return (
|
|
1241
|
-
<div key={state.key} className="perseus-block-math">
|
|
1242
|
-
<ErrorBoundary>
|
|
1243
|
-
<div
|
|
1244
|
-
className="perseus-block-math-inner"
|
|
1245
|
-
style={innerStyle}
|
|
1246
|
-
>
|
|
1247
|
-
<AssetContext.Consumer>
|
|
1248
|
-
{({setAssetStatus}) => (
|
|
1249
|
-
<TeX setAssetStatus={setAssetStatus}>
|
|
1250
|
-
{content}
|
|
1251
|
-
</TeX>
|
|
1252
|
-
)}
|
|
1253
|
-
</AssetContext.Consumer>
|
|
1254
|
-
</div>
|
|
1255
|
-
</ErrorBoundary>
|
|
1256
|
-
</div>
|
|
1257
|
-
);
|
|
1258
|
-
}
|
|
1259
|
-
if (node.type === "math") {
|
|
1260
|
-
// Replace uses of \begin{align}...\end{align} which KaTeX doesn't
|
|
1261
|
-
// support (yet) with \begin{aligned}...\end{aligned} which renders
|
|
1262
|
-
// the same is supported by KaTeX. It does the same for align*.
|
|
1263
|
-
// TODO(kevinb) update content to use aligned instead of align.
|
|
1264
|
-
const tex = node.content.replace(/\{align[*]?\}/g, "{aligned}");
|
|
1265
|
-
|
|
1266
|
-
// We render math here instead of in perseus-markdown.jsx
|
|
1267
|
-
// because we need to pass it our onRender callback.
|
|
1268
|
-
return (
|
|
1269
|
-
<span
|
|
1270
|
-
key={state.key}
|
|
1271
|
-
style={{
|
|
1272
|
-
// If math is directly next to text, don't let it
|
|
1273
|
-
// wrap to the next line
|
|
1274
|
-
whiteSpace: "nowrap",
|
|
1275
|
-
}}
|
|
1276
|
-
>
|
|
1277
|
-
<ErrorBoundary>
|
|
1278
|
-
{/* We add extra empty spans around the math to make it not
|
|
1279
|
-
wrap (I don't know why this works, but it does) */}
|
|
1280
|
-
<span />
|
|
1281
|
-
<AssetContext.Consumer>
|
|
1282
|
-
{({setAssetStatus}) => (
|
|
1283
|
-
<TeX
|
|
1284
|
-
onRender={this.props.onRender}
|
|
1285
|
-
setAssetStatus={setAssetStatus}
|
|
1286
|
-
>
|
|
1287
|
-
{tex}
|
|
1288
|
-
</TeX>
|
|
1289
|
-
)}
|
|
1290
|
-
</AssetContext.Consumer>
|
|
1291
|
-
<span />
|
|
1292
|
-
</ErrorBoundary>
|
|
1293
|
-
</span>
|
|
1294
|
-
);
|
|
1295
|
-
}
|
|
1296
|
-
if (node.type === "image") {
|
|
1297
|
-
if (imagePlaceholder) {
|
|
1298
|
-
return imagePlaceholder;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// We need to add width and height to images from our
|
|
1302
|
-
// props.images mapping.
|
|
1303
|
-
|
|
1304
|
-
// We do a _.has check here to avoid weird things like
|
|
1305
|
-
// 'toString' or '__proto__' as a url.
|
|
1306
|
-
const extraAttrs = _.has(this.props.images, node.target)
|
|
1307
|
-
? this.props.images[node.target]
|
|
1308
|
-
: null;
|
|
1309
|
-
|
|
1310
|
-
// The width of a table column is determined by the widest table
|
|
1311
|
-
// cell within that column, but responsive images constrain
|
|
1312
|
-
// themselves to the width of their parent containers. Thus,
|
|
1313
|
-
// responsive images don't do very well within tables. To avoid
|
|
1314
|
-
// haphazard sizing, simply make images within tables unresponsive.
|
|
1315
|
-
// TODO(alex): Make tables themselves responsive.
|
|
1316
|
-
const responsive = !state.inTable;
|
|
1317
|
-
return (
|
|
1318
|
-
<ErrorBoundary key={state.key}>
|
|
1319
|
-
<AssetContext.Consumer>
|
|
1320
|
-
{({setAssetStatus}) => (
|
|
1321
|
-
<SvgImage
|
|
1322
|
-
setAssetStatus={setAssetStatus}
|
|
1323
|
-
// @ts-expect-error - TS2322 - Type 'string | null | undefined' is not assignable to type 'string | undefined'.
|
|
1324
|
-
src={PerseusMarkdown.sanitizeUrl(node.target)}
|
|
1325
|
-
alt={node.alt}
|
|
1326
|
-
title={node.title}
|
|
1327
|
-
responsive={responsive}
|
|
1328
|
-
onUpdate={this.props.onRender}
|
|
1329
|
-
zoomToFullSizeOnMobile={
|
|
1330
|
-
apiOptions.isMobile && apiOptions.isArticle
|
|
1331
|
-
}
|
|
1332
|
-
{...extraAttrs}
|
|
1333
|
-
/>
|
|
1334
|
-
)}
|
|
1335
|
-
</AssetContext.Consumer>
|
|
1336
|
-
</ErrorBoundary>
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
if (node.type === "columns") {
|
|
1340
|
-
// Note that we have two columns. This is so we can put
|
|
1341
|
-
// a className on the outer renderer content for SAT.
|
|
1342
|
-
// TODO(aria): See if there is a better way we can do
|
|
1343
|
-
// things like this
|
|
1344
|
-
this._isTwoColumn = true;
|
|
1345
|
-
// but then render normally:
|
|
1346
|
-
return (
|
|
1347
|
-
<ErrorBoundary key={state.key}>
|
|
1348
|
-
{PerseusMarkdown.ruleOutput(node, nestedOutput, state)}
|
|
1349
|
-
</ErrorBoundary>
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
if (node.type === "text") {
|
|
1353
|
-
if (rContainsNonWhitespace.test(node.content)) {
|
|
1354
|
-
this._foundTextNodes = true;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// Used by the translator portal to replace image URLs with
|
|
1358
|
-
// placeholders, see preprocessWidgets in manticore-utils.js
|
|
1359
|
-
// for more details.
|
|
1360
|
-
if (imagePlaceholder && rImageURL.test(node.content)) {
|
|
1361
|
-
return imagePlaceholder;
|
|
1362
|
-
}
|
|
1363
|
-
return node.content;
|
|
1364
|
-
}
|
|
1365
|
-
if (node.type === "table" || node.type === "titledTable") {
|
|
1366
|
-
const output = PerseusMarkdown.ruleOutput(node, nestedOutput, {
|
|
1367
|
-
...state,
|
|
1368
|
-
isMobile: apiOptions.isMobile,
|
|
1369
|
-
inTable: true,
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
if (!apiOptions.isMobile) {
|
|
1373
|
-
return output;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
const margin = 16;
|
|
1377
|
-
const outerStyle = {
|
|
1378
|
-
marginLeft: -margin,
|
|
1379
|
-
marginRight: -margin,
|
|
1380
|
-
} as const;
|
|
1381
|
-
const innerStyle = {
|
|
1382
|
-
paddingLeft: 0,
|
|
1383
|
-
paddingRight: 0,
|
|
1384
|
-
} as const;
|
|
1385
|
-
const mobileInnerStyle = {
|
|
1386
|
-
...innerStyle,
|
|
1387
|
-
...styles.mobileZoomableParentFix,
|
|
1388
|
-
} as const;
|
|
1389
|
-
|
|
1390
|
-
const wrappedOutput = (
|
|
1391
|
-
<div style={{...mobileInnerStyle, overflowX: "auto"}}>
|
|
1392
|
-
<ErrorBoundary>
|
|
1393
|
-
<Zoomable animateHeight={true}>{output}</Zoomable>
|
|
1394
|
-
</ErrorBoundary>
|
|
1395
|
-
</div>
|
|
1396
|
-
);
|
|
1397
|
-
|
|
1398
|
-
// TODO(benkomalo): how should we deal with tappable items inside
|
|
1399
|
-
// of tables?
|
|
1400
|
-
return <div style={outerStyle}>{wrappedOutput}</div>;
|
|
1401
|
-
}
|
|
1402
|
-
// If it's a "normal" or "simple" markdown node, just
|
|
1403
|
-
// output it using its output rule.
|
|
1404
|
-
return (
|
|
1405
|
-
<ErrorBoundary key={state.key}>
|
|
1406
|
-
{PerseusMarkdown.ruleOutput(node, nestedOutput, state)}
|
|
1407
|
-
</ErrorBoundary>
|
|
1408
|
-
);
|
|
1409
|
-
};
|
|
1410
|
-
|
|
1411
|
-
handleRender: (prevProps: Props) => void = (prevProps: Props) => {
|
|
1412
|
-
const onRender = this.props.onRender;
|
|
1413
|
-
const oldOnRender = prevProps.onRender;
|
|
1414
|
-
|
|
1415
|
-
// In the common case of no callback specified, avoid this work.
|
|
1416
|
-
if (onRender !== noopOnRender || oldOnRender !== noopOnRender) {
|
|
1417
|
-
// @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'find' does not exist on type 'JQueryStatic'.
|
|
1418
|
-
const $images = $(ReactDOM.findDOMNode(this)).find("img");
|
|
1419
|
-
|
|
1420
|
-
// Fire callback on image load...
|
|
1421
|
-
if (oldOnRender !== noopOnRender) {
|
|
1422
|
-
$images.off("load", oldOnRender);
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
if (onRender !== noopOnRender) {
|
|
1426
|
-
$images.on("load", onRender);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
// ...as well as right now (non-image, non-TeX or image from cache)
|
|
1431
|
-
onRender();
|
|
1432
|
-
};
|
|
1433
|
-
|
|
1434
|
-
// Sets the current focus path
|
|
1435
|
-
// If the new focus path is not a prefix of the old focus path,
|
|
1436
|
-
// we send an onChangeFocus event back to our parent.
|
|
1437
|
-
_setCurrentFocus: (path: FocusPath) => void = (path: FocusPath) => {
|
|
1438
|
-
const apiOptions = this.getApiOptions();
|
|
1439
|
-
|
|
1440
|
-
// We don't do this when the new path is a prefix because
|
|
1441
|
-
// that prefix is already focused (we're just in a more specific
|
|
1442
|
-
// area of it). This makes it safe to call _setCurrentFocus
|
|
1443
|
-
// whenever a widget is interacted with--we won't wipe out
|
|
1444
|
-
// our focus state if we are already focused on a subpart
|
|
1445
|
-
// of that widget (i.e. a transformation NumberInput inside
|
|
1446
|
-
// of a transformer widget).
|
|
1447
|
-
if (!isIdPathPrefix(path, this._currentFocus)) {
|
|
1448
|
-
const prevFocus = this._currentFocus;
|
|
1449
|
-
|
|
1450
|
-
if (prevFocus) {
|
|
1451
|
-
this.blurPath(prevFocus);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
this._currentFocus = path;
|
|
1455
|
-
apiOptions.onFocusChange(this._currentFocus, prevFocus);
|
|
1456
|
-
}
|
|
1457
|
-
};
|
|
1458
|
-
|
|
1459
|
-
focus: () => boolean | null | undefined = () => {
|
|
1460
|
-
let id;
|
|
1461
|
-
let focusResult;
|
|
1462
|
-
for (let i = 0; i < this.widgetIds.length; i++) {
|
|
1463
|
-
const widgetId = this.widgetIds[i];
|
|
1464
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
1465
|
-
const widgetFocusResult = widget && widget.focus && widget.focus();
|
|
1466
|
-
if (widgetFocusResult) {
|
|
1467
|
-
id = widgetId;
|
|
1468
|
-
focusResult = widgetFocusResult;
|
|
1469
|
-
break;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
if (id) {
|
|
1474
|
-
// reconstruct a {path, element} focus object
|
|
1475
|
-
let path;
|
|
1476
|
-
if (typeof focusResult === "object") {
|
|
1477
|
-
// TODO(jeremy): I am 99% sure this path is no longer possible.
|
|
1478
|
-
// In D10274, focus management sometimes returned an object
|
|
1479
|
-
// with a `path` and `element` key. But later in D11387 and
|
|
1480
|
-
// D13664 things have been changed and seemingly removed the
|
|
1481
|
-
// object return value.
|
|
1482
|
-
// The result of focus was a {path, id} object itself
|
|
1483
|
-
path = [id].concat(focusResult.path || []);
|
|
1484
|
-
Log.error(
|
|
1485
|
-
"Renderer received a focus result of type 'object' " +
|
|
1486
|
-
"instead of the expected type 'boolean'",
|
|
1487
|
-
Errors.Internal,
|
|
1488
|
-
{
|
|
1489
|
-
loggedMetadata: {
|
|
1490
|
-
focusResult: JSON.stringify(focusResult),
|
|
1491
|
-
currentProps: JSON.stringify(
|
|
1492
|
-
this.state.widgetProps,
|
|
1493
|
-
),
|
|
1494
|
-
},
|
|
1495
|
-
},
|
|
1496
|
-
);
|
|
1497
|
-
} else {
|
|
1498
|
-
// The result of focus was true or the like; just
|
|
1499
|
-
// construct a root focus object
|
|
1500
|
-
path = [id];
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
this._setCurrentFocus(path);
|
|
1504
|
-
return true;
|
|
1505
|
-
}
|
|
1506
|
-
};
|
|
1507
|
-
|
|
1508
|
-
getDOMNodeForPath: (path: FocusPath) => Element | Text | null | undefined =
|
|
1509
|
-
(path: FocusPath) => {
|
|
1510
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1511
|
-
const widgetId = _.first(path);
|
|
1512
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1513
|
-
const interWidgetPath = _.rest(path);
|
|
1514
|
-
|
|
1515
|
-
// Widget handles parsing of the interWidgetPath. If the path is empty
|
|
1516
|
-
// beyond the widgetID, as a special case we just return the widget's
|
|
1517
|
-
// DOM node.
|
|
1518
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
1519
|
-
const getNode = widget && widget.getDOMNodeForPath;
|
|
1520
|
-
if (getNode) {
|
|
1521
|
-
return getNode(interWidgetPath);
|
|
1522
|
-
}
|
|
1523
|
-
if (interWidgetPath.length === 0) {
|
|
1524
|
-
// @ts-expect-error - TS2345 - Argument of type 'Widget | null | undefined' is not assignable to parameter of type 'ReactInstance | null | undefined'.
|
|
1525
|
-
return ReactDOM.findDOMNode(widget);
|
|
1526
|
-
}
|
|
1527
|
-
};
|
|
1528
|
-
|
|
1529
|
-
getGrammarTypeForPath: (path: FocusPath) => string | null | undefined = (
|
|
1530
|
-
path: FocusPath,
|
|
1531
|
-
) => {
|
|
1532
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1533
|
-
const widgetId = _.first(path);
|
|
1534
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1535
|
-
const interWidgetPath = _.rest(path);
|
|
1536
|
-
|
|
1537
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
1538
|
-
if (widget && widget.getGrammarTypeForPath) {
|
|
1539
|
-
return widget.getGrammarTypeForPath(interWidgetPath);
|
|
1540
|
-
}
|
|
1541
|
-
};
|
|
1542
|
-
|
|
1543
|
-
getInputPaths: () => ReadonlyArray<FocusPath> = () => {
|
|
1544
|
-
const inputPaths: Array<FocusPath> = [];
|
|
1545
|
-
_.each(this.widgetIds, (widgetId: string) => {
|
|
1546
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
1547
|
-
if (widget && widget.getInputPaths) {
|
|
1548
|
-
// Grab all input paths and add widgetID to the front
|
|
1549
|
-
const widgetInputPaths = widget.getInputPaths();
|
|
1550
|
-
// Prefix paths with their widgetID and add to collective
|
|
1551
|
-
// list of paths.
|
|
1552
|
-
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
|
|
1553
|
-
_.each(widgetInputPaths, (inputPath: string) => {
|
|
1554
|
-
const relativeInputPath = [widgetId].concat(inputPath);
|
|
1555
|
-
inputPaths.push(relativeInputPath);
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
});
|
|
1559
|
-
|
|
1560
|
-
return inputPaths;
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
|
-
focusPath: (path: FocusPath) => void = (path: FocusPath) => {
|
|
1564
|
-
// No need to focus if it's already focused
|
|
1565
|
-
if (_.isEqual(this._currentFocus, path)) {
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
if (this._currentFocus) {
|
|
1569
|
-
// Unfocus old path, if exists
|
|
1570
|
-
this.blurPath(this._currentFocus);
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1574
|
-
const widgetId = _.first(path);
|
|
1575
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1576
|
-
const interWidgetPath = _.rest(path);
|
|
1577
|
-
|
|
1578
|
-
// Widget handles parsing of the interWidgetPath
|
|
1579
|
-
const focusWidget = this.getWidgetInstance(widgetId);
|
|
1580
|
-
if (focusWidget && focusWidget.focusInputPath) {
|
|
1581
|
-
focusWidget.focusInputPath(interWidgetPath);
|
|
1582
|
-
}
|
|
1583
|
-
};
|
|
1584
|
-
|
|
1585
|
-
blurPath: (path: FocusPath) => void = (path: FocusPath) => {
|
|
1586
|
-
// No need to blur if it's not focused
|
|
1587
|
-
if (!_.isEqual(this._currentFocus, path)) {
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1592
|
-
const widgetId = _.first(path);
|
|
1593
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1594
|
-
const interWidgetPath = _.rest(path);
|
|
1595
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
1596
|
-
// We might be in the editor and blurring a widget that no
|
|
1597
|
-
// longer exists, so only blur if we actually found the widget
|
|
1598
|
-
if (widget) {
|
|
1599
|
-
const blurWidget = this.getWidgetInstance(widgetId);
|
|
1600
|
-
if (blurWidget && blurWidget.blurInputPath) {
|
|
1601
|
-
// Widget handles parsing of the interWidgetPath
|
|
1602
|
-
blurWidget.blurInputPath(interWidgetPath);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
};
|
|
1606
|
-
|
|
1607
|
-
blur: () => void = () => {
|
|
1608
|
-
if (this._currentFocus) {
|
|
1609
|
-
this.blurPath(this._currentFocus);
|
|
1610
|
-
}
|
|
1611
|
-
};
|
|
1612
|
-
|
|
1613
|
-
// Serializes widget state. Seems to be used only by editors though.
|
|
1614
|
-
serialize: () => Record<any, any> = () => {
|
|
1615
|
-
const state: Record<string, any> = {};
|
|
1616
|
-
_.each(
|
|
1617
|
-
this.state.widgetInfo,
|
|
1618
|
-
function (info, id) {
|
|
1619
|
-
// eslint-disable-next-line @babel/no-invalid-this
|
|
1620
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
1621
|
-
// eslint-disable-next-line @babel/no-invalid-this
|
|
1622
|
-
const widget = this.getWidgetInstance(id);
|
|
1623
|
-
const s = widget.serialize();
|
|
1624
|
-
if (!_.isEmpty(s)) {
|
|
1625
|
-
state[id] = s;
|
|
1626
|
-
}
|
|
1627
|
-
},
|
|
1628
|
-
this,
|
|
1629
|
-
);
|
|
1630
|
-
return state;
|
|
1631
|
-
};
|
|
1632
|
-
|
|
1633
|
-
emptyWidgets: () => any = () => {
|
|
1634
|
-
// @ts-expect-error - TS2345 - Argument of type '(id: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
|
|
1635
|
-
return _.filter(this.widgetIds, (id) => {
|
|
1636
|
-
const widgetInfo = this._getWidgetInfo(id);
|
|
1637
|
-
if (widgetInfo.static) {
|
|
1638
|
-
// Static widgets shouldn't count as empty
|
|
1639
|
-
return false;
|
|
1640
|
-
}
|
|
1641
|
-
const widget = this.getWidgetInstance(id);
|
|
1642
|
-
if (widget && widget.simpleValidate) {
|
|
1643
|
-
const score: PerseusScore = widget.simpleValidate(
|
|
1644
|
-
widgetInfo.options,
|
|
1645
|
-
// @ts-expect-error - TS2345 - Argument of type 'null' is not assignable to parameter of type '((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined'.
|
|
1646
|
-
null,
|
|
1647
|
-
);
|
|
1648
|
-
return Util.scoreIsEmpty(score);
|
|
1649
|
-
}
|
|
1650
|
-
});
|
|
1651
|
-
};
|
|
1652
|
-
|
|
1653
|
-
_setWidgetProps: SetWidgetPropsFn = (id, newProps, cb, silent) => {
|
|
1654
|
-
this.setState(
|
|
1655
|
-
(prevState) => {
|
|
1656
|
-
const widgetProps = {
|
|
1657
|
-
...prevState.widgetProps,
|
|
1658
|
-
[id]: {
|
|
1659
|
-
...prevState.widgetProps[id],
|
|
1660
|
-
...newProps,
|
|
1661
|
-
},
|
|
1662
|
-
} as const;
|
|
1663
|
-
|
|
1664
|
-
// Update the `lastUsedWidgetId` to this widget - unless we're
|
|
1665
|
-
// in silent mode. We only want to track the last widget that
|
|
1666
|
-
// was actually _used_, and silent updates generally don't come
|
|
1667
|
-
// from _usage_.
|
|
1668
|
-
const lastUsedWidgetId = silent
|
|
1669
|
-
? prevState.lastUsedWidgetId
|
|
1670
|
-
: id;
|
|
1671
|
-
|
|
1672
|
-
if (!silent) {
|
|
1673
|
-
this.props.onSerializedStateUpdated(
|
|
1674
|
-
this.getSerializedState(widgetProps),
|
|
1675
|
-
);
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
return {
|
|
1679
|
-
lastUsedWidgetId,
|
|
1680
|
-
widgetProps,
|
|
1681
|
-
};
|
|
1682
|
-
},
|
|
1683
|
-
() => {
|
|
1684
|
-
// Wait until all components have rendered. In React 16 setState
|
|
1685
|
-
// callback fires immediately after this componentDidUpdate, and
|
|
1686
|
-
// there is no guarantee that parent/siblings components have
|
|
1687
|
-
// finished rendering.
|
|
1688
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
1689
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
1690
|
-
setTimeout(() => {
|
|
1691
|
-
const cbResult = cb && cb();
|
|
1692
|
-
if (!silent) {
|
|
1693
|
-
this.props.onInteractWithWidget(id);
|
|
1694
|
-
}
|
|
1695
|
-
if (cbResult !== false) {
|
|
1696
|
-
// TODO(jack): For some reason, some widgets don't always
|
|
1697
|
-
// end up in refs here, which is repro-able if you make an
|
|
1698
|
-
// [[ orderer 1 ]] and copy-paste this, then change it to
|
|
1699
|
-
// be an [[ orderer 2 ]]. The resulting Renderer ends up
|
|
1700
|
-
// with an "orderer 2" ref but not an "orderer 1" ref.
|
|
1701
|
-
// @_@??
|
|
1702
|
-
// TODO(jack): Figure out why this is happening and fix it
|
|
1703
|
-
// As far as I can tell, this is only an issue in the
|
|
1704
|
-
// editor-page, so doing this shouldn't break clients
|
|
1705
|
-
// hopefully
|
|
1706
|
-
this._setCurrentFocus([id]);
|
|
1707
|
-
}
|
|
1708
|
-
}, 0);
|
|
1709
|
-
},
|
|
1710
|
-
);
|
|
1711
|
-
};
|
|
1712
|
-
|
|
1713
|
-
setInputValue: (
|
|
1714
|
-
path: FocusPath,
|
|
1715
|
-
newValue: string,
|
|
1716
|
-
focus?: () => unknown,
|
|
1717
|
-
) => void = (path, newValue, focus) => {
|
|
1718
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1719
|
-
const widgetId = _.first(path);
|
|
1720
|
-
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
1721
|
-
const interWidgetPath = _.rest(path);
|
|
1722
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
1723
|
-
|
|
1724
|
-
// Widget handles parsing of the interWidgetPath.
|
|
1725
|
-
widget?.setInputValue?.(interWidgetPath, newValue, focus);
|
|
1726
|
-
};
|
|
1727
|
-
|
|
1728
|
-
/**
|
|
1729
|
-
* Returns an array of the widget `.getUserInput()` results
|
|
1730
|
-
*/
|
|
1731
|
-
getUserInput: () => ReadonlyArray<WidgetUserInput | null | undefined> =
|
|
1732
|
-
() => {
|
|
1733
|
-
return this.widgetIds.map((id: string) => {
|
|
1734
|
-
const widget = this.getWidgetInstance(id);
|
|
1735
|
-
if (widget && widget.getUserInput) {
|
|
1736
|
-
// TODO(Jeremy): Add the widget ID in here so we can more
|
|
1737
|
-
// easily correlate it to the widget state.
|
|
1738
|
-
return widget.getUserInput();
|
|
1739
|
-
}
|
|
1740
|
-
});
|
|
1741
|
-
};
|
|
1742
|
-
|
|
1743
|
-
/**
|
|
1744
|
-
* Returns an array of all widget IDs in the order they occur in
|
|
1745
|
-
* the content.
|
|
1746
|
-
*/
|
|
1747
|
-
getWidgetIds: () => ReadonlyArray<string> = () => {
|
|
1748
|
-
return this.widgetIds;
|
|
1749
|
-
};
|
|
1750
|
-
|
|
1751
|
-
/**
|
|
1752
|
-
* Returns the result of `.getUserInput()` for each widget, in
|
|
1753
|
-
* a map from widgetId to userInput.
|
|
1754
|
-
* NOTE(jeremy): This function is hauntingly similar to `getUserInput` with
|
|
1755
|
-
* the major difference being that this function returns a map of
|
|
1756
|
-
* `widgetID` => UserInput and `getUserInput` simply returns an array. It
|
|
1757
|
-
* would be trivial to map between the results of each of these functions,
|
|
1758
|
-
* so we should aim to remove one of these functions.
|
|
1759
|
-
*/
|
|
1760
|
-
getUserInputForWidgets: () => {
|
|
1761
|
-
[widgetId: string]: WidgetUserInput | null | undefined;
|
|
1762
|
-
} = () => {
|
|
1763
|
-
return mapObjectFromArray(this.widgetIds, (id) => {
|
|
1764
|
-
const widget = this.getWidgetInstance(id);
|
|
1765
|
-
if (widget && widget.getUserInput) {
|
|
1766
|
-
return widget.getUserInput();
|
|
1767
|
-
}
|
|
1768
|
-
});
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
/**
|
|
1772
|
-
* Returns an object mapping from widget ID to perseus-style score.
|
|
1773
|
-
* The keys of this object are the values of the array returned
|
|
1774
|
-
* from `getWidgetIds`.
|
|
1775
|
-
*/
|
|
1776
|
-
scoreWidgets: () => {
|
|
1777
|
-
[widgetId: string]: PerseusScore;
|
|
1778
|
-
} = () => {
|
|
1779
|
-
const widgetProps = this.state.widgetInfo;
|
|
1780
|
-
const onInputError = this.getApiOptions().onInputError;
|
|
1781
|
-
|
|
1782
|
-
const gradedWidgetIds = _.filter(this.widgetIds, (id) => {
|
|
1783
|
-
const props = widgetProps[id];
|
|
1784
|
-
const widgetIsGraded: boolean =
|
|
1785
|
-
props?.graded == null || props.graded;
|
|
1786
|
-
const widgetIsStatic = !!props?.static;
|
|
1787
|
-
// Ungraded widgets or widgets set to static shouldn't be graded.
|
|
1788
|
-
return widgetIsGraded && !widgetIsStatic;
|
|
1789
|
-
});
|
|
1790
|
-
|
|
1791
|
-
const widgetScores: Record<string, PerseusScore> = {};
|
|
1792
|
-
_.each(gradedWidgetIds, (id) => {
|
|
1793
|
-
const props = widgetProps[id];
|
|
1794
|
-
const widget = this.getWidgetInstance(id);
|
|
1795
|
-
// widget can be undefined if it hasn't yet been rendered
|
|
1796
|
-
if (widget && widget.simpleValidate) {
|
|
1797
|
-
widgetScores[id] = widget.simpleValidate(
|
|
1798
|
-
{...props?.options, scoring: true},
|
|
1799
|
-
onInputError,
|
|
1800
|
-
);
|
|
1801
|
-
}
|
|
1802
|
-
});
|
|
1803
|
-
|
|
1804
|
-
return widgetScores;
|
|
1805
|
-
};
|
|
1806
|
-
|
|
1807
|
-
/**
|
|
1808
|
-
* Grades the content.
|
|
1809
|
-
*/
|
|
1810
|
-
score: () => PerseusScore = () => {
|
|
1811
|
-
return _.reduce(this.scoreWidgets(), Util.combineScores, Util.noScore);
|
|
1812
|
-
};
|
|
1813
|
-
|
|
1814
|
-
guessAndScore: () => [any, PerseusScore] = () => {
|
|
1815
|
-
const totalGuess = this.getUserInput();
|
|
1816
|
-
const totalScore = this.score();
|
|
1817
|
-
|
|
1818
|
-
return [totalGuess, totalScore];
|
|
1819
|
-
};
|
|
1820
|
-
|
|
1821
|
-
examples: () => ReadonlyArray<string> | null | undefined = () => {
|
|
1822
|
-
const widgetIds = this.widgetIds;
|
|
1823
|
-
const examples = _.compact(
|
|
1824
|
-
_.map(widgetIds, (widgetId) => {
|
|
1825
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
1826
|
-
return widget != null && widget.examples
|
|
1827
|
-
? widget.examples()
|
|
1828
|
-
: null;
|
|
1829
|
-
}),
|
|
1830
|
-
);
|
|
1831
|
-
|
|
1832
|
-
// no widgets with examples
|
|
1833
|
-
if (!examples.length) {
|
|
1834
|
-
return null;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
const allEqual = _.all(examples, function (example) {
|
|
1838
|
-
return _.isEqual(examples[0], example);
|
|
1839
|
-
});
|
|
1840
|
-
|
|
1841
|
-
// some widgets have different examples
|
|
1842
|
-
// TODO(alex): handle this better
|
|
1843
|
-
if (!allEqual) {
|
|
1844
|
-
return null;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
return examples[0];
|
|
1848
|
-
};
|
|
1849
|
-
|
|
1850
|
-
// TranslationLinter callback
|
|
1851
|
-
handletranslationLintErrors: (lintErrors: ReadonlyArray<string>) => void = (
|
|
1852
|
-
lintErrors: ReadonlyArray<string>,
|
|
1853
|
-
) => {
|
|
1854
|
-
if (!this._isMounted) {
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
this.setState({
|
|
1859
|
-
translationLintErrors: lintErrors,
|
|
1860
|
-
});
|
|
1861
|
-
};
|
|
1862
|
-
|
|
1863
|
-
render(): React.ReactNode {
|
|
1864
|
-
const apiOptions = this.getApiOptions();
|
|
1865
|
-
|
|
1866
|
-
if (this.reuseMarkdown) {
|
|
1867
|
-
return this.lastRenderedMarkdown;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
const content = this.getContent(this.props, this.state);
|
|
1871
|
-
// `this.widgetIds` is appended to in `this.outputMarkdown`:
|
|
1872
|
-
this.widgetIds = [];
|
|
1873
|
-
|
|
1874
|
-
if (this.shouldRenderJiptPlaceholder(this.props, this.state)) {
|
|
1875
|
-
// Crowdin's JIPT (Just in place translation) uses a fake language
|
|
1876
|
-
// with language tag "en-pt" where the value of the translations
|
|
1877
|
-
// look like: {crwdns2657085:0}{crwdne2657085:0} where it keeps the
|
|
1878
|
-
// {crowdinId:ngettext variant}. We detect whether the current
|
|
1879
|
-
// content matches this, so we can take over rendering of
|
|
1880
|
-
// the perseus content as the translators interact with jipt.
|
|
1881
|
-
// We search for only part of the tag that crowdin uses to guard
|
|
1882
|
-
// against them changing the format on us. The full tag it looks
|
|
1883
|
-
// for can be found in https://cdn.crowdin.net/jipt/jipt.js
|
|
1884
|
-
// globalPhrase var.
|
|
1885
|
-
|
|
1886
|
-
// If we haven't already added this component to the registry do so
|
|
1887
|
-
// now. showHints() may cause this component to be rerendered
|
|
1888
|
-
// before jipt has a chance to replace its contents, so this check
|
|
1889
|
-
// will keep us from adding the component to the registry a second
|
|
1890
|
-
// time.
|
|
1891
|
-
if (!this.translationIndex) {
|
|
1892
|
-
this.translationIndex =
|
|
1893
|
-
getDependencies().rendererTranslationComponents.addComponent(
|
|
1894
|
-
this,
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
// For articles, we add jipt data to individual paragraphs. For
|
|
1899
|
-
// exercises, we add it to the renderer and let translators
|
|
1900
|
-
// translate the entire thing. For the article equivalent of
|
|
1901
|
-
// this if block, search this file for where we render a
|
|
1902
|
-
// QuestionParagraph, and see the `isJipt:` parameter sent to
|
|
1903
|
-
// PerseusMarkdown.parse()
|
|
1904
|
-
if (!apiOptions.isArticle) {
|
|
1905
|
-
// We now need to output this tag, as jipt looks for it to be
|
|
1906
|
-
// able to replace it with a translation that it runs an ajax
|
|
1907
|
-
// call to get. We add a data attribute with the index to the
|
|
1908
|
-
// Persues.TranslationComponent registry so that when jipt
|
|
1909
|
-
// calls its before_dom_insert we can lookup this component by
|
|
1910
|
-
// this attribute and render the text with markdown.
|
|
1911
|
-
return (
|
|
1912
|
-
<DefinitionProvider>
|
|
1913
|
-
<div
|
|
1914
|
-
data-perseus-component-index={this.translationIndex}
|
|
1915
|
-
>
|
|
1916
|
-
{content}
|
|
1917
|
-
</div>
|
|
1918
|
-
</DefinitionProvider>
|
|
1919
|
-
);
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
// Hacks:
|
|
1924
|
-
// We use mutable state here to figure out whether the output
|
|
1925
|
-
// had two columns.
|
|
1926
|
-
// It is updated to true by `this.outputMarkdown` if a
|
|
1927
|
-
// column break is found
|
|
1928
|
-
// TODO(aria): We now have a state variable threaded through
|
|
1929
|
-
// simple-markdown output. We should mutate it instead of
|
|
1930
|
-
// state on this component to do this in a less hacky way.
|
|
1931
|
-
this._isTwoColumn = false;
|
|
1932
|
-
|
|
1933
|
-
// Parse the string of markdown to a parse tree
|
|
1934
|
-
const parsedMarkdown = this.props.inline
|
|
1935
|
-
? PerseusMarkdown.parseInline(content, {
|
|
1936
|
-
// Recognize crowdin IDs while translating articles
|
|
1937
|
-
// (This should never be hit by exercises, though if you
|
|
1938
|
-
// decide you want to add a check that this is an article,
|
|
1939
|
-
// go for it.)
|
|
1940
|
-
isJipt: this.translationIndex != null,
|
|
1941
|
-
})
|
|
1942
|
-
: PerseusMarkdown.parse(content, {
|
|
1943
|
-
isJipt: this.translationIndex != null,
|
|
1944
|
-
});
|
|
1945
|
-
|
|
1946
|
-
// Optionally apply the linter to the parse tree
|
|
1947
|
-
if (this.props.linterContext.highlightLint) {
|
|
1948
|
-
// If highlightLint is true and lint is detected, this call
|
|
1949
|
-
// will modify the parse tree by adding lint nodes that will
|
|
1950
|
-
// serve to highlight the lint when rendered
|
|
1951
|
-
const context: Context = {
|
|
1952
|
-
content: this.props.content,
|
|
1953
|
-
widgets: this.props.widgets,
|
|
1954
|
-
...this.props.linterContext,
|
|
1955
|
-
};
|
|
1956
|
-
|
|
1957
|
-
PerseusLinter.runLinter(parsedMarkdown, context, true);
|
|
1958
|
-
|
|
1959
|
-
// Apply the lint errors from the last TranslationLinter run.
|
|
1960
|
-
// TODO(joshuan): Support overlapping dots.
|
|
1961
|
-
this._translationLinter.applyLintErrors(parsedMarkdown, [
|
|
1962
|
-
...this.state.translationLintErrors,
|
|
1963
|
-
...(this.props.legacyPerseusLint || []),
|
|
1964
|
-
]);
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
// Render the linted markdown parse tree with React components
|
|
1968
|
-
const markdownContents = this.outputMarkdown(parsedMarkdown, {
|
|
1969
|
-
baseElements: apiOptions.baseElements,
|
|
1970
|
-
});
|
|
1971
|
-
|
|
1972
|
-
const className = classNames({
|
|
1973
|
-
[ApiClassNames.RENDERER]: true,
|
|
1974
|
-
[ApiClassNames.RESPONSIVE_RENDERER]: true,
|
|
1975
|
-
[ApiClassNames.TWO_COLUMN_RENDERER]: this._isTwoColumn,
|
|
1976
|
-
});
|
|
1977
|
-
|
|
1978
|
-
this.lastRenderedMarkdown = (
|
|
1979
|
-
<DefinitionProvider>
|
|
1980
|
-
<div className={className}>{markdownContents}</div>
|
|
1981
|
-
</DefinitionProvider>
|
|
1982
|
-
);
|
|
1983
|
-
return this.lastRenderedMarkdown;
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
const styles = {
|
|
1988
|
-
mobileZoomableParentFix: {
|
|
1989
|
-
// NOTE(abdul): There is an issue where transform animations will
|
|
1990
|
-
// cause the Zoomable component to disappear when running on the
|
|
1991
|
-
// native app on iPad (iOS 13). I found some answers online that recommend
|
|
1992
|
-
// transforming the parent in 3D space. Doing this forces hardware
|
|
1993
|
-
// acceleration, which causes the process to run on the GPU. It's not
|
|
1994
|
-
// clear to me why this fixes the issue, but it's suggested online
|
|
1995
|
-
// to people dealing with similar disappearance and flickering issues.
|
|
1996
|
-
transform: "translate3d(0,0,0)",
|
|
1997
|
-
},
|
|
1998
|
-
} as const;
|
|
1999
|
-
|
|
2000
|
-
export default Renderer;
|