@khanacademy/perseus-editor 0.0.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/es/index.css +1020 -0
  4. package/dist/es/index.js +9 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +1020 -0
  7. package/dist/index.js +9 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +6 -1
  10. package/package.json +16 -4
  11. package/src/README.md +7 -0
  12. package/src/__stories__/editor.stories.js +33 -0
  13. package/src/__stories__/section-control-button.stories.jsx +27 -0
  14. package/src/__testdata__/input-number_testdata.js +28 -0
  15. package/src/__tests__/editor_test.jsx +123 -0
  16. package/src/__tests__/i18n_test.jsx +585 -0
  17. package/src/__tests__/traversal_test.jsx +378 -0
  18. package/src/all-editors.js +85 -0
  19. package/src/all-editors_test.js +17 -0
  20. package/src/all-widgets.js +10 -0
  21. package/src/article-editor.jsx +437 -0
  22. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  23. package/src/components/blur-input.jsx +62 -0
  24. package/src/components/checkbox.jsx +194 -0
  25. package/src/components/device-framer.jsx +91 -0
  26. package/src/components/drag-target.jsx +88 -0
  27. package/src/components/dropdown-option.jsx +318 -0
  28. package/src/components/form-wrapped-text-field.jsx +248 -0
  29. package/src/components/graph-settings.jsx +631 -0
  30. package/src/components/hover-behavior.jsx +178 -0
  31. package/src/components/json-editor.jsx +121 -0
  32. package/src/components/link.jsx +173 -0
  33. package/src/components/simple-button.jsx +86 -0
  34. package/src/components/sortable.jsx +186 -0
  35. package/src/components/util.js +39 -0
  36. package/src/components/viewport-resizer.jsx +73 -0
  37. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  38. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  39. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  40. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  41. package/src/diffs/__tests__/split-diff_test.js +34 -0
  42. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  43. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  44. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  45. package/src/diffs/article-diff.jsx +70 -0
  46. package/src/diffs/item-diff.jsx +77 -0
  47. package/src/diffs/renderer-diff.jsx +143 -0
  48. package/src/diffs/split-diff.jsx +30 -0
  49. package/src/diffs/string-array-diff.jsx +75 -0
  50. package/src/diffs/structured-item-diff.jsx +269 -0
  51. package/src/diffs/tags-diff.jsx +50 -0
  52. package/src/diffs/text-diff.jsx +194 -0
  53. package/src/diffs/widget-diff-performer.jsx +74 -0
  54. package/src/diffs/widget-diff.jsx +291 -0
  55. package/src/editor-page.jsx +351 -0
  56. package/src/editor.jsx +1364 -0
  57. package/src/hint-editor.jsx +447 -0
  58. package/src/i18n.jsx +139 -0
  59. package/src/iframe-content-renderer.jsx +201 -0
  60. package/src/index.js +27 -8
  61. package/src/item-editor.jsx +159 -0
  62. package/src/item-extras-editor.jsx +150 -0
  63. package/src/katex-error-view.jsx +87 -0
  64. package/src/multirenderer-editor.jsx +1074 -0
  65. package/src/section-control-button.jsx +53 -0
  66. package/src/stateful-article-editor.jsx +176 -0
  67. package/src/stateful-editor-page.jsx +90 -0
  68. package/src/styles/perseus-editor.less +1339 -0
  69. package/src/testing-widgets.js +12 -0
  70. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  71. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  72. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  73. package/src/widgets/categorizer-editor.jsx +103 -0
  74. package/src/widgets/cs-program-editor.jsx +258 -0
  75. package/src/widgets/definition-editor.jsx +75 -0
  76. package/src/widgets/dropdown-editor.jsx +201 -0
  77. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  78. package/src/widgets/example-graphie-widget.jsx +175 -0
  79. package/src/widgets/example-widget-editor.jsx +60 -0
  80. package/src/widgets/example-widget.jsx +153 -0
  81. package/src/widgets/explanation-editor.jsx +89 -0
  82. package/src/widgets/expression-editor.jsx +560 -0
  83. package/src/widgets/graded-group-editor.jsx +162 -0
  84. package/src/widgets/graded-group-set-editor.jsx +101 -0
  85. package/src/widgets/grapher-editor.jsx +154 -0
  86. package/src/widgets/group-editor.jsx +83 -0
  87. package/src/widgets/iframe-editor.jsx +200 -0
  88. package/src/widgets/image-editor.jsx +326 -0
  89. package/src/widgets/input-number-editor.jsx +269 -0
  90. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  91. package/src/widgets/interaction-editor.jsx +1389 -0
  92. package/src/widgets/interactive-graph-editor.jsx +322 -0
  93. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  94. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  95. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  96. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  97. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  98. package/src/widgets/label-image/answer-choices.jsx +231 -0
  99. package/src/widgets/label-image/behavior.jsx +92 -0
  100. package/src/widgets/label-image/marker.jsx +292 -0
  101. package/src/widgets/label-image/question-markers.jsx +181 -0
  102. package/src/widgets/label-image/select-image.jsx +86 -0
  103. package/src/widgets/label-image-editor.jsx +257 -0
  104. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  105. package/src/widgets/matcher-editor.jsx +147 -0
  106. package/src/widgets/matrix-editor.jsx +155 -0
  107. package/src/widgets/measurer-editor.jsx +245 -0
  108. package/src/widgets/molecule-editor.jsx +63 -0
  109. package/src/widgets/number-line-editor.jsx +498 -0
  110. package/src/widgets/numeric-input-editor.jsx +554 -0
  111. package/src/widgets/orderer-editor.jsx +166 -0
  112. package/src/widgets/passage-editor.jsx +115 -0
  113. package/src/widgets/passage-ref-editor.jsx +77 -0
  114. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  115. package/src/widgets/plotter-editor.jsx +537 -0
  116. package/src/widgets/radio/editor.jsx +398 -0
  117. package/src/widgets/reaction-diagram-editor.jsx +127 -0
  118. package/src/widgets/sequence-editor.jsx +205 -0
  119. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  120. package/src/widgets/simple-markdown-tester.jsx +91 -0
  121. package/src/widgets/simulator-editor.jsx +144 -0
  122. package/src/widgets/sorter-editor.jsx +100 -0
  123. package/src/widgets/table-editor.jsx +164 -0
  124. package/src/widgets/transformer-editor.jsx +1006 -0
  125. package/src/widgets/unit-editor.jsx +293 -0
  126. package/src/widgets/video-editor.jsx +74 -0
  127. package/src/__tests__/index.test.js +0 -15
@@ -0,0 +1,201 @@
1
+ // @flow
2
+ import {components, icons, EditorJsonify} from "@khanacademy/perseus";
3
+ import PropTypes from "prop-types";
4
+ import * as React from "react";
5
+ import ReactDOM from "react-dom";
6
+ import _ from "underscore";
7
+
8
+ const {InfoTip, InlineIcon} = components;
9
+ const {iconPlus, iconTrash} = icons;
10
+
11
+ type Props = $FlowFixMe;
12
+
13
+ class DropdownEditor extends React.Component<Props> {
14
+ static propTypes = {
15
+ choices: PropTypes.arrayOf(
16
+ PropTypes.shape({
17
+ content: PropTypes.string,
18
+ correct: PropTypes.bool,
19
+ }),
20
+ ),
21
+ placeholder: PropTypes.string,
22
+ };
23
+
24
+ static widgetName: string = "dropdown";
25
+
26
+ static defaultProps: Props = {
27
+ placeholder: "",
28
+ choices: [
29
+ {
30
+ content: "",
31
+ correct: false,
32
+ },
33
+ ],
34
+ };
35
+
36
+ onPlaceholderChange: (SyntheticInputEvent<>) => void = (e) => {
37
+ const placeholder = e.target.value;
38
+ this.props.onChange({placeholder: placeholder});
39
+ };
40
+
41
+ onCorrectChange: (number, SyntheticInputEvent<>) => void = (
42
+ choiceIndex,
43
+ ) => {
44
+ const choices = _.map(this.props.choices, function (choice, i) {
45
+ return _.extend({}, choice, {
46
+ correct: i === choiceIndex,
47
+ });
48
+ });
49
+ this.props.onChange({choices: choices});
50
+ };
51
+
52
+ onContentChange: (number, SyntheticInputEvent<>) => void = (
53
+ choiceIndex,
54
+ e,
55
+ ) => {
56
+ const choices = this.props.choices.slice();
57
+ const choice = _.clone(choices[choiceIndex]);
58
+ choice.content = e.target.value;
59
+ choices[choiceIndex] = choice;
60
+ this.props.onChange({choices: choices});
61
+ };
62
+
63
+ addChoice: (SyntheticMouseEvent<>) => void = (e) => {
64
+ e.preventDefault();
65
+
66
+ const choices = this.props.choices;
67
+ const blankChoice = {content: "", correct: false};
68
+ this.props.onChange(
69
+ {
70
+ choices: choices.concat([blankChoice]),
71
+ },
72
+ this.focus.bind(this, choices.length),
73
+ );
74
+ };
75
+
76
+ removeChoice: (number, SyntheticMouseEvent<>) => void = (
77
+ choiceIndex,
78
+ e,
79
+ ) => {
80
+ e.preventDefault();
81
+ const choices = _(this.props.choices).clone();
82
+ choices.splice(choiceIndex, 1);
83
+ this.props.onChange({
84
+ choices: choices,
85
+ });
86
+ };
87
+
88
+ focus: (number) => boolean = (i) => {
89
+ // $FlowFixMe[incompatible-use]
90
+ // $FlowFixMe[prop-missing]
91
+ ReactDOM.findDOMNode(this.refs["editor" + i]).focus(); // eslint-disable-line react/no-string-refs
92
+ return true;
93
+ };
94
+
95
+ serialize: () => $FlowFixMe = () => {
96
+ return EditorJsonify.serialize.call(this);
97
+ };
98
+
99
+ render(): React.Node {
100
+ const dropdownGroupName = _.uniqueId("perseus_dropdown_");
101
+ return (
102
+ <div className="perseus-widget-dropdown">
103
+ <div className="dropdown-info">
104
+ Dropdown
105
+ <InfoTip>
106
+ <p>
107
+ The drop down is useful for making inequalities in a
108
+ custom format. We normally use the symbols {"<"},{" "}
109
+ {">"}, ≤, ≥ (in that order) which you can copy into
110
+ the choices. When possible, use the "multiple
111
+ choice" answer type instead.
112
+ </p>
113
+ </InfoTip>
114
+ </div>
115
+ <div className="dropdown-placeholder">
116
+ <input
117
+ type="text"
118
+ placeholder="Placeholder value"
119
+ value={this.props.placeholder}
120
+ onChange={this.onPlaceholderChange}
121
+ />
122
+ <InfoTip>
123
+ <p>
124
+ This value will appear as the drop down default. It
125
+ should give the user some indication of the values
126
+ available in the drop down itself, e.g.,
127
+ Yes/No/Maybe.
128
+ </p>
129
+ </InfoTip>
130
+ </div>
131
+ <div className="clearfix" />
132
+ <ul className="dropdown-choices">
133
+ {this.props.choices.map(function (choice, i) {
134
+ const checkedClass = choice.correct
135
+ ? "correct"
136
+ : "incorrect";
137
+
138
+ return (
139
+ <li key={"" + i}>
140
+ <div>
141
+ <input
142
+ ref={"radio" + i}
143
+ type="radio"
144
+ name={dropdownGroupName}
145
+ checked={
146
+ choice.correct ? "checked" : ""
147
+ }
148
+ // eslint-disable-next-line react/jsx-no-bind
149
+ onChange={this.onCorrectChange.bind(
150
+ this,
151
+ i,
152
+ )}
153
+ value={i}
154
+ />
155
+ <input
156
+ type="text"
157
+ ref={"editor" + i}
158
+ // eslint-disable-next-line react/jsx-no-bind
159
+ onChange={this.onContentChange.bind(
160
+ this,
161
+ i,
162
+ )}
163
+ className={checkedClass}
164
+ value={choice.content}
165
+ />
166
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
167
+ <a
168
+ href="#"
169
+ className="simple-button orange"
170
+ // eslint-disable-next-line react/jsx-no-bind
171
+ onClick={this.removeChoice.bind(
172
+ this,
173
+ i,
174
+ )}
175
+ >
176
+ <span className="remove-choice">
177
+ <InlineIcon {...iconTrash} />
178
+ </span>
179
+ </a>
180
+ </div>
181
+ </li>
182
+ );
183
+ }, this)}
184
+ </ul>
185
+
186
+ <div className="add-choice-container">
187
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
188
+ <a
189
+ href="#"
190
+ className="simple-button orange"
191
+ onClick={this.addChoice}
192
+ >
193
+ <InlineIcon {...iconPlus} /> Add a choice{" "}
194
+ </a>
195
+ </div>
196
+ </div>
197
+ );
198
+ }
199
+ }
200
+
201
+ export default DropdownEditor;
@@ -0,0 +1,70 @@
1
+ // @flow
2
+ import {Changeable, EditorJsonify} from "@khanacademy/perseus";
3
+ import * as React from "react";
4
+
5
+ import ExampleGraphie from "./example-graphie-widget.jsx";
6
+
7
+ const ExampleGraphieWidget = ExampleGraphie.widget;
8
+
9
+ type Props = $FlowFixMe;
10
+
11
+ /**
12
+ * This is the widget's editor. This is what shows up on the left side
13
+ * of the screen in the demo page. Only the question writer sees this.
14
+ */
15
+ class ExampleGraphieWidgetEditor extends React.Component<Props> {
16
+ static propTypes = {
17
+ ...Changeable.propTypes,
18
+ };
19
+
20
+ static widgetName: "example-graphie-widget" = "example-graphie-widget";
21
+
22
+ static defaultProps: Props = {
23
+ correct: [4, 4],
24
+ graph: {
25
+ box: [340, 340],
26
+ labels: ["x", "y"],
27
+ range: [
28
+ [-10, 10],
29
+ [-10, 10],
30
+ ],
31
+ step: [1, 1],
32
+ gridStep: [1, 1],
33
+ valid: true,
34
+ backgroundImage: null,
35
+ markings: "grid",
36
+ showProtractor: false,
37
+ },
38
+ };
39
+
40
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
41
+ return Changeable.change.apply(this, args);
42
+ };
43
+
44
+ handleChange: ($FlowFixMe) => void = (newProps) => {
45
+ if (newProps.coord) {
46
+ this.change({
47
+ correct: newProps.coord,
48
+ });
49
+ }
50
+ };
51
+
52
+ serialize: () => $FlowFixMe = () => {
53
+ return EditorJsonify.serialize.call(this);
54
+ };
55
+
56
+ render(): React.Node {
57
+ return (
58
+ <div>
59
+ <ExampleGraphieWidget
60
+ graph={this.props.graph}
61
+ coord={this.props.correct}
62
+ onChange={this.handleChange}
63
+ apiOptions={this.props.apiOptions}
64
+ />
65
+ </div>
66
+ );
67
+ }
68
+ }
69
+
70
+ export default ExampleGraphieWidgetEditor;
@@ -0,0 +1,175 @@
1
+ /* eslint-disable no-unused-vars, react/forbid-prop-types, react/sort-comp */
2
+ // @flow
3
+ /**
4
+ * This is an example graphie-using widget
5
+ *
6
+ * TODO(jack): Add more comments
7
+ */
8
+
9
+ import {number as knumber, point as kpoint} from "@khanacademy/kmath";
10
+ import {
11
+ components,
12
+ ApiOptions,
13
+ Changeable,
14
+ Util,
15
+ WidgetJsonifyDeprecated,
16
+ } from "@khanacademy/perseus";
17
+ import PropTypes from "prop-types";
18
+ import * as React from "react";
19
+ import _ from "underscore";
20
+
21
+ import type {Coord, WidgetExports} from "@khanacademy/perseus";
22
+
23
+ const {Graphie} = components;
24
+
25
+ // $FlowFixMe[prop-missing]
26
+ const MovablePoint = Graphie.MovablePoint;
27
+
28
+ type Props = $FlowFixMe;
29
+
30
+ /**
31
+ * This is the widget's renderer. It shows up in the right column
32
+ * in the demo, and is what is visible to users, and where
33
+ * users enter their answers.
34
+ */
35
+ class ExampleGraphieWidget extends React.Component<Props> {
36
+ static propTypes = {
37
+ ...Changeable.propTypes,
38
+ apiOptions: ApiOptions.propTypes,
39
+
40
+ graph: PropTypes.object.isRequired,
41
+ coord: PropTypes.arrayOf(PropTypes.number),
42
+ };
43
+
44
+ static defaultProps: Props = {
45
+ // We want to allow our coord to be null to test if the
46
+ // user has interacted with this widget yet when grading it
47
+ coord: null,
48
+ graph: {
49
+ box: [400, 400],
50
+ labels: ["x", "y"],
51
+ range: [
52
+ [-10, 10],
53
+ [-10, 10],
54
+ ],
55
+ step: [1, 1],
56
+ gridStep: [1, 1],
57
+ valid: true,
58
+ backgroundImage: null,
59
+ markings: "grid",
60
+ showProtractor: false,
61
+ },
62
+ };
63
+
64
+ /**
65
+ * This is the widget's grading function
66
+ */
67
+ static validate(state: $FlowFixMe, rubric: $FlowFixMe): $FlowFixMe {
68
+ if (state.coord == null) {
69
+ return {
70
+ type: "invalid",
71
+ message: null,
72
+ };
73
+ }
74
+ if (kpoint.equal(state.coord, rubric.correct)) {
75
+ return {
76
+ type: "points",
77
+ earned: 1,
78
+ total: 1,
79
+ message: null,
80
+ };
81
+ }
82
+ return {
83
+ type: "points",
84
+ earned: 0,
85
+ total: 1,
86
+ message: null,
87
+ };
88
+ }
89
+
90
+ getUserInput: () => $FlowFixMe = () => {
91
+ return WidgetJsonifyDeprecated.getUserInput.call(this);
92
+ };
93
+
94
+ render(): React.Node {
95
+ return (
96
+ <Graphie
97
+ // eslint-disable-next-line react/no-string-refs
98
+ ref="graphie"
99
+ box={this.props.graph.box}
100
+ range={this.props.graph.range}
101
+ options={this.props.graph}
102
+ setup={this.setupGraphie}
103
+ setDrawingAreaAvailable={
104
+ this.props.apiOptions.setDrawingAreaAvailable
105
+ }
106
+ >
107
+ <MovablePoint
108
+ pointSize={5}
109
+ coord={this.props.coord || [0, 0]}
110
+ constraints={[
111
+ MovablePoint.constraints.snap(),
112
+ MovablePoint.constraints.bound(),
113
+ ]}
114
+ onMove={this.movePoint}
115
+ />
116
+ </Graphie>
117
+ );
118
+ }
119
+
120
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
121
+ return Changeable.change.apply(this, args);
122
+ };
123
+
124
+ movePoint: (Coord) => void = (newCoord) => {
125
+ this.change({
126
+ coord: newCoord,
127
+ });
128
+ };
129
+
130
+ _getGridConfig: ($FlowFixMe) => $FlowFixMe = (options) => {
131
+ return _.map(options.step, function (step, i) {
132
+ return Util.gridDimensionConfig(
133
+ step,
134
+ options.range[i],
135
+ options.box[i],
136
+ options.gridStep[i],
137
+ );
138
+ });
139
+ };
140
+
141
+ setupGraphie: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
142
+ graphie,
143
+ options,
144
+ ) => {
145
+ const gridConfig = this._getGridConfig(options);
146
+ graphie.graphInit({
147
+ range: options.range,
148
+ scale: _.pluck(gridConfig, "scale"),
149
+ axisArrows: "<->",
150
+ labelFormat: function (s) {
151
+ return "\\small{" + s + "}";
152
+ },
153
+ gridStep: options.gridStep,
154
+ tickStep: _.pluck(gridConfig, "tickStep"),
155
+ labelStep: 1,
156
+ unityLabels: _.pluck(gridConfig, "unityLabel"),
157
+ });
158
+ graphie.label([0, options.range[1][1]], options.labels[1], "above");
159
+ };
160
+
161
+ simpleValidate: ($FlowFixMe) => $FlowFixMe = (rubric) => {
162
+ return ExampleGraphieWidget.validate(this.getUserInput(), rubric);
163
+ };
164
+ }
165
+
166
+ /**
167
+ * For this widget to work, we must export it.
168
+ * We also must import this file in src/all-widgets.js
169
+ */
170
+ export default ({
171
+ name: "example-graphie-widget",
172
+ displayName: "Example Graphie Widget",
173
+ hidden: true, // Hides this widget from the Perseus.Editor widget select
174
+ widget: ExampleGraphieWidget,
175
+ }: WidgetExports<typeof ExampleGraphieWidget>);
@@ -0,0 +1,60 @@
1
+ /* eslint-disable react/sort-comp */
2
+ // @flow
3
+ import {Changeable, EditorJsonify} from "@khanacademy/perseus";
4
+ import * as React from "react";
5
+
6
+ type Props = $FlowFixMe;
7
+
8
+ /**
9
+ * This is the widget's editor. This is what shows up on the left side
10
+ * of the screen in the demo. Only the question writer sees this.
11
+ */
12
+ class ExampleWidgetEditor extends React.Component<Props> {
13
+ static propTypes = {
14
+ ...Changeable.propTypes,
15
+ };
16
+
17
+ static widgetName: "example-widget" = "example-widget";
18
+
19
+ static defaultProps: Props = {
20
+ correct: "",
21
+ };
22
+
23
+ handleAnswerChange: (SyntheticInputEvent<>) => void = (event) => {
24
+ this.change({
25
+ correct: event.target.value,
26
+ });
27
+ };
28
+
29
+ render(): React.Node {
30
+ return (
31
+ <div>
32
+ <label>
33
+ Correct answer:
34
+ <input
35
+ value={this.props.correct}
36
+ onChange={this.handleAnswerChange}
37
+ // eslint-disable-next-line react/no-string-refs
38
+ ref="input"
39
+ />
40
+ </label>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
46
+ return Changeable.change.apply(this, args);
47
+ };
48
+
49
+ focus: () => boolean = () => {
50
+ // eslint-disable-next-line react/no-string-refs
51
+ this.refs.input.focus();
52
+ return true;
53
+ };
54
+
55
+ serialize: () => $FlowFixMe = () => {
56
+ return EditorJsonify.serialize.call(this);
57
+ };
58
+ }
59
+
60
+ export default ExampleWidgetEditor;
@@ -0,0 +1,153 @@
1
+ /* eslint-disable react/sort-comp */
2
+ // @flow
3
+ /**
4
+ * This is a simple number-entry widget
5
+ * It is not as powerful as number-input, but has a simpler, more
6
+ * representative structure as an example widget, and is easier to
7
+ * test new ideas on.
8
+ *
9
+ * TODO(jack): Add more comments
10
+ */
11
+ import {Changeable} from "@khanacademy/perseus";
12
+ import PropTypes from "prop-types";
13
+ import * as React from "react";
14
+ import _ from "underscore";
15
+
16
+ import type {WidgetExports} from "@khanacademy/perseus";
17
+
18
+ class TextInput extends React.Component<$FlowFixMe> {
19
+ render(): React.Node {
20
+ return (
21
+ <input
22
+ // eslint-disable-next-line react/no-string-refs
23
+ ref="input"
24
+ value={this.props.value || ""}
25
+ onChange={this.changeValue}
26
+ />
27
+ );
28
+ }
29
+
30
+ focus = () => {
31
+ // eslint-disable-next-line react/no-string-refs
32
+ this.refs.input.focus();
33
+ return true;
34
+ };
35
+
36
+ changeValue = (e) => {
37
+ // Translating from the js event e to the value
38
+ // of the textbox to send to onChange
39
+ this.props.onChange(e.target.value);
40
+ };
41
+ }
42
+
43
+ /**
44
+ * This is the widget's renderer. It shows up in the right column
45
+ * in the demo, and is what is visible to users, and where
46
+ * users enter their answers.
47
+ */
48
+ class ExampleWidget extends React.Component<$FlowFixMe> {
49
+ static propTypes = {
50
+ ...Changeable.propTypes,
51
+ value: PropTypes.string,
52
+ };
53
+
54
+ static defaultProps: $FlowFixMe = {
55
+ value: "",
56
+ };
57
+
58
+ /**
59
+ * This is the widget's grading function. simpleValidate generally
60
+ * defers to this function.
61
+ *
62
+ * value is usually the result of getUserInput on the widget
63
+ * rubric is the result of calling serialize() on the editor
64
+ */
65
+ static validate(value: string, rubric: $FlowFixMe): $FlowFixMe {
66
+ if (value === "") {
67
+ return {
68
+ type: "invalid",
69
+ message:
70
+ "It looks like you haven't answered all of the " +
71
+ "question yet.",
72
+ };
73
+ }
74
+ if (value === rubric.correct) {
75
+ return {
76
+ type: "points",
77
+ earned: 1,
78
+ total: 1,
79
+ message: null,
80
+ };
81
+ }
82
+ return {
83
+ type: "points",
84
+ earned: 0,
85
+ total: 1,
86
+ message: null,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Tell our parent to update our props.
92
+ */
93
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
94
+ return Changeable.change.apply(this, args);
95
+ };
96
+
97
+ render(): React.Node {
98
+ return (
99
+ <TextInput
100
+ // eslint-disable-next-line react/no-string-refs
101
+ ref="input"
102
+ value={this.props.value}
103
+ onChange={this.change("value")}
104
+ />
105
+ );
106
+ }
107
+
108
+ getUserInput: () => string = () => {
109
+ return this.props.value;
110
+ };
111
+
112
+ /**
113
+ * Widgets that are focusable should add a focus method that returns
114
+ * true if focusing succeeded. The first such widget found will be
115
+ * focused on page load.
116
+ */
117
+ focus: () => boolean = () => {
118
+ // eslint-disable-next-line react/no-string-refs
119
+ this.refs.input.focus();
120
+ return true;
121
+ };
122
+
123
+ /**
124
+ * simpleValidate is called for grading. Rubric is the result of calling
125
+ * getUserInput() on the editor that created this widget.
126
+ *
127
+ * Should return an object representing the grading result, such as
128
+ * {
129
+ * type: "points",
130
+ * earned: 1,
131
+ * total: 1,
132
+ * message: null
133
+ * }
134
+ */
135
+ simpleValidate: ($FlowFixMe) => $FlowFixMe = (rubric) => {
136
+ return ExampleWidget.validate(this.getUserInput(), rubric);
137
+ };
138
+ }
139
+
140
+ /**
141
+ * For this widget to work, we must import this file in src/all-widgets.js
142
+ */
143
+ export default ({
144
+ name: "example-widget",
145
+ displayName: "Example Widget",
146
+
147
+ // Tell the renderer what type of `display:` style we would like
148
+ // for the component wrapping this one.
149
+ defaultAlignment: "inline-block",
150
+
151
+ hidden: true, // Hides this widget from the Perseus.Editor widget select
152
+ widget: ExampleWidget,
153
+ }: WidgetExports<typeof ExampleWidget>);