@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,293 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // @flow
3
+ import * as KAS from "@khanacademy/kas";
4
+ import {
5
+ components,
6
+ displaySigFigs,
7
+ icons,
8
+ Changeable,
9
+ EditorJsonify,
10
+ } from "@khanacademy/perseus";
11
+ import PropTypes from "prop-types";
12
+ import * as React from "react";
13
+ import _ from "underscore";
14
+
15
+ const {InlineIcon, NumberInput} = components;
16
+ const {iconOk, iconRemove} = icons;
17
+
18
+ // TODO(joel): teach KAS how to accept an answer only if it's expressed in
19
+ // terms of a certain type.
20
+ // TODO(joel): Allow sigfigs within a range rather than an exact expected
21
+ // value?
22
+
23
+ const ALL = ("all": "all");
24
+ const SOME = ("some": "some");
25
+ const MAX_SIGFIGS = 10;
26
+
27
+ const sigfigPrint = function (num, sigfigs) {
28
+ return displaySigFigs(num, sigfigs, -MAX_SIGFIGS, false);
29
+ };
30
+
31
+ // Extract the primitive units from a unit expression. This first simplifies
32
+ // `expr` to a `Mul` like "5 kg m / s^2" then removes the first term.
33
+ const primUnits = function (expr) {
34
+ return expr.simplify().asMul().partition()[1].flatten().simplify();
35
+ };
36
+
37
+ type UnitExampleProps = $FlowFixMe;
38
+ type UnitExampleState = $FlowFixMe;
39
+
40
+ // Show the name of a unit and whether it's recognized by KAS.
41
+ //
42
+ // In the future I plan for this to show an example of a thing that would be
43
+ // accepted in that unit.
44
+ class UnitExample extends React.Component<UnitExampleProps, UnitExampleState> {
45
+ // TODO(jangmi, CP-3288): Remove usage of `UNSAFE_componentWillMount`
46
+ UNSAFE_componentWillMount() {
47
+ this._checkValidity(this.props);
48
+ }
49
+
50
+ UNSAFE_componentWillReceiveProps(nextProps) {
51
+ this._checkValidity(nextProps);
52
+ }
53
+
54
+ _checkValidity = ({name, original, sigfigs}) => {
55
+ const parseResult = KAS.unitParse(name);
56
+ let solvedExample = "";
57
+
58
+ // A unit is valid if it parses and is equivalent to the original.
59
+ let valid = true;
60
+
61
+ if (parseResult.parsed && original) {
62
+ const x = new KAS.Var("x");
63
+ const {unit} = parseResult;
64
+ const equality = new KAS.Eq(original, "=", new KAS.Mul(x, unit));
65
+ try {
66
+ const answer = equality.solveLinearEquationForVariable(x);
67
+
68
+ // The third parameter is the least significant decimal place.
69
+ // I.e. the index of the last place you care about
70
+ // (543210.(-1)(-2)(-3) etc). We use -10 because that should
71
+ // always be safe since we only care up to maximum 10 decimal
72
+ // places.
73
+ solvedExample = sigfigPrint(answer.eval(), sigfigs);
74
+
75
+ valid = KAS.compare(primUnits(original), primUnits(unit)).equal;
76
+ } catch (e) {
77
+ valid = false;
78
+ }
79
+ } else {
80
+ valid = false;
81
+ }
82
+
83
+ this.setState({
84
+ valid,
85
+ solvedExample,
86
+ });
87
+ };
88
+
89
+ render(): React.Node {
90
+ let icon;
91
+ if (this.state.valid) {
92
+ icon = (
93
+ <span>
94
+ <span className="unit-example-okay">
95
+ <InlineIcon {...iconOk} />
96
+ </span>
97
+ {this.state.solvedExample}
98
+ </span>
99
+ );
100
+ } else {
101
+ icon = (
102
+ <span className="unit-example-not-okay">
103
+ <InlineIcon {...iconRemove} />
104
+ </span>
105
+ );
106
+ }
107
+
108
+ return (
109
+ <div>
110
+ {icon} {this.props.name}
111
+ </div>
112
+ );
113
+ }
114
+ }
115
+
116
+ type UnitInputEditorProps = $FlowFixMe;
117
+
118
+ class UnitInputEditor extends React.Component<UnitInputEditorProps> {
119
+ parsed: ?boolean;
120
+ groupId: string;
121
+ original: ?$FlowFixMe;
122
+
123
+ static propTypes = {
124
+ ...Changeable.propTypes,
125
+ value: PropTypes.string,
126
+ acceptingUnits: PropTypes.arrayOf(PropTypes.string),
127
+ accepting: PropTypes.oneOf([ALL, SOME]),
128
+ sigfigs: PropTypes.number,
129
+ };
130
+
131
+ static widgetName: "unit-input" = "unit-input";
132
+
133
+ static defaultProps: UnitInputEditorProps = {
134
+ value: "5x10^5 kg m / s^2",
135
+ accepting: ALL,
136
+ sigfigs: 3,
137
+ };
138
+
139
+ constructor(props: UnitExampleProps) {
140
+ super(props);
141
+
142
+ this.groupId = _.uniqueId("accepting");
143
+ this._doOriginal(props);
144
+ }
145
+
146
+ UNSAFE_componentWillReceiveProps(nextProps: UnitInputEditorProps) {
147
+ this._doOriginal(nextProps);
148
+ }
149
+
150
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
151
+ return Changeable.change.apply(this, args);
152
+ };
153
+
154
+ handleAcceptingUnitsChange: (SyntheticInputEvent<>) => void = (event) => {
155
+ const acceptingUnits = event.target.value
156
+ .split(",")
157
+ .map((str) => str.trim())
158
+ .filter((str) => str !== "");
159
+ this.change({acceptingUnits});
160
+ };
161
+
162
+ handleSigfigChange: (number) => void = (sigfigs) => {
163
+ this.change({sigfigs});
164
+ };
165
+
166
+ _checkSigfigValidity: (number) => boolean = (sigfigs) => {
167
+ return sigfigs > 0 && sigfigs <= MAX_SIGFIGS;
168
+ };
169
+
170
+ _setAccepting: (typeof SOME | typeof ALL) => void = (val) => {
171
+ this.change({accepting: val});
172
+ };
173
+
174
+ _doOriginal: (UnitExampleProps) => void = (props) => {
175
+ const tryParse = KAS.unitParse(props.value);
176
+ this.parsed = false;
177
+
178
+ // Only update this state if the unit parsed *and* it has a magnitude
179
+ // attached to it. KAS can also parse units without magnitudes ("1.2
180
+ // g" vs "g").
181
+ if (tryParse.parsed && tryParse.type === "unitMagnitude") {
182
+ this.original = tryParse.expr;
183
+ this.parsed = true;
184
+ }
185
+ };
186
+
187
+ onChange: (SyntheticInputEvent<>) => void = (event) => {
188
+ this.props.onChange({value: event.target.value});
189
+ };
190
+
191
+ getSaveWarnings: () => $ReadOnlyArray<string> = () => {
192
+ const {value, accepting, acceptingUnits} = this.props;
193
+ const warnings = [];
194
+
195
+ const tryParse = KAS.unitParse(value);
196
+ if (!tryParse.parsed) {
197
+ warnings.push("Answer did not parse");
198
+ }
199
+
200
+ if (accepting === SOME && acceptingUnits.length === 0) {
201
+ warnings.push("There are no accepted units");
202
+ }
203
+
204
+ return warnings;
205
+ };
206
+
207
+ serialize: () => $FlowFixMe = () => {
208
+ return EditorJsonify.serialize.call(this);
209
+ };
210
+
211
+ render(): React.Node {
212
+ const {acceptingUnits = [], accepting} = this.props;
213
+ let acceptingElem = null;
214
+ if (accepting === SOME) {
215
+ const unitsArr = acceptingUnits.map((name, i) => (
216
+ <UnitExample
217
+ name={name}
218
+ original={this.original || null}
219
+ sigfigs={this.props.sigfigs}
220
+ key={i}
221
+ />
222
+ ));
223
+
224
+ acceptingElem = (
225
+ <div>
226
+ <input
227
+ type="text"
228
+ defaultValue={acceptingUnits.join(", ")}
229
+ onChange={this.handleAcceptingUnitsChange}
230
+ />{" "}
231
+ (comma-separated)
232
+ {unitsArr}
233
+ </div>
234
+ );
235
+ }
236
+
237
+ return (
238
+ <div className="unit-editor">
239
+ <div>
240
+ <input
241
+ value={this.props.value}
242
+ className="unit-editor-canonical"
243
+ onChange={this.onChange}
244
+ />{" "}
245
+ {this.parsed ? (
246
+ <span className="unit-example-okay">
247
+ <InlineIcon {...iconOk} />
248
+ </span>
249
+ ) : (
250
+ <span className="unit-example-not-okay">
251
+ <InlineIcon {...iconRemove} />
252
+ </span>
253
+ )}
254
+ </div>
255
+
256
+ <div>
257
+ Significant Figures:{" "}
258
+ <NumberInput
259
+ value={this.props.sigfigs}
260
+ onChange={this.handleSigfigChange}
261
+ checkValidity={this._checkSigfigValidity}
262
+ useArrowKeys
263
+ />
264
+ </div>
265
+
266
+ <div>
267
+ <label>
268
+ <input
269
+ type="radio"
270
+ name={this.groupId}
271
+ onChange={() => this._setAccepting(ALL)}
272
+ checked={this.props.accepting === ALL}
273
+ />
274
+ {" Any equivalent unit "}
275
+ </label>
276
+ <label>
277
+ <input
278
+ type="radio"
279
+ name={this.groupId}
280
+ onChange={() => this._setAccepting(SOME)}
281
+ checked={this.props.accepting === SOME}
282
+ />
283
+ {" Only these units "}
284
+ </label>
285
+ </div>
286
+
287
+ {acceptingElem}
288
+ </div>
289
+ );
290
+ }
291
+ }
292
+
293
+ export default UnitInputEditor;
@@ -0,0 +1,74 @@
1
+ // @flow
2
+ import {components, Changeable, EditorJsonify} from "@khanacademy/perseus";
3
+ import PropTypes from "prop-types";
4
+ import * as React from "react";
5
+ import _ from "underscore";
6
+
7
+ import BlurInput from "../components/blur-input.jsx";
8
+
9
+ const {InfoTip} = components;
10
+
11
+ const KA_VIDEO_URL = /khanacademy\.org\/.*\/v\/(.*)$/;
12
+
13
+ /**
14
+ * Turns Khan Academy URLs into the KA slugs, if possible. Any other URLs are
15
+ * returned unchanged.
16
+ */
17
+ function getSlugFromUrl(url) {
18
+ const match = KA_VIDEO_URL.exec(url);
19
+ if (match) {
20
+ return match[1];
21
+ }
22
+ return url;
23
+ }
24
+
25
+ type Props = $FlowFixMe;
26
+
27
+ /**
28
+ * This is the main editor for this widget, to specify all the options.
29
+ */
30
+ class VideoEditor extends React.Component<Props> {
31
+ static propTypes = {
32
+ ...Changeable.propTypes,
33
+ location: PropTypes.string,
34
+ onChange: PropTypes.func,
35
+ };
36
+
37
+ static widgetName: "video" = "video";
38
+
39
+ static defaultProps: Props = {
40
+ location: "",
41
+ };
42
+
43
+ _handleUrlChange: (string) => void = (url) => {
44
+ this.props.onChange({location: getSlugFromUrl(url)});
45
+ };
46
+
47
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
48
+ return Changeable.change.apply(this, args);
49
+ };
50
+
51
+ serialize: () => $FlowFixMe = () => {
52
+ return EditorJsonify.serialize.call(this);
53
+ };
54
+
55
+ render(): React.Node {
56
+ return (
57
+ <div>
58
+ <label>
59
+ KA Video Slug:{" "}
60
+ <BlurInput
61
+ value={this.props.location}
62
+ style={{width: 290}}
63
+ onChange={this._handleUrlChange}
64
+ />
65
+ <InfoTip>
66
+ KA video URLs will be converted to just the slug.
67
+ </InfoTip>
68
+ </label>
69
+ </div>
70
+ );
71
+ }
72
+ }
73
+
74
+ export default VideoEditor;
@@ -1,15 +0,0 @@
1
- // @flow
2
- import {bar} from "../index.js";
3
-
4
- describe("#foo", () => {
5
- it("should return foo", () => {
6
- // Arrange
7
- const logSpy = jest.spyOn(console, "log");
8
-
9
- // Act
10
- bar();
11
-
12
- // Assert
13
- expect(logSpy).toHaveBeenCalledWith("foo() = foo");
14
- });
15
- });