@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,267 @@
1
+ /* eslint-disable react/sort-comp */
2
+ // @flow
3
+ import {components, Changeable, EditorJsonify} from "@khanacademy/perseus";
4
+ import PropTypes from "prop-types";
5
+ import * as React from "react";
6
+ import _ from "underscore";
7
+
8
+ const {InfoTip, NumberInput, PropCheckBox} = components;
9
+
10
+ const MAX_SIZE = 8;
11
+
12
+ // styling
13
+ const CELL_PADDING = 5;
14
+
15
+ const TABLE_STYLE = {
16
+ display: "table",
17
+ tableLayout: "fixed",
18
+ };
19
+
20
+ const ROW_STYLE = {
21
+ display: "table-row",
22
+ };
23
+
24
+ const CELL_STYLE = {
25
+ display: "table-cell",
26
+ padding: CELL_PADDING,
27
+ };
28
+
29
+ const BASE_TILE_STYLE = {
30
+ borderRadius: 10,
31
+ cursor: "pointer",
32
+ };
33
+
34
+ const PATTERNS = {
35
+ plus: () => [
36
+ [false, true, false],
37
+ [true, true, true],
38
+ [false, true, false],
39
+ ],
40
+ x: () => [
41
+ [true, false, true],
42
+ [false, true, false],
43
+ [true, false, true],
44
+ ],
45
+ "plus/x": (iter) => {
46
+ return iter % 2 ? PATTERNS.x() : PATTERNS.plus();
47
+ },
48
+ };
49
+
50
+ /**
51
+ * Clamps value to an integer in the range [min, max]
52
+ */
53
+ const clampToInt = function (value, min, max) {
54
+ value = Math.floor(value);
55
+ value = Math.max(value, min);
56
+ value = Math.min(value, max);
57
+ return value;
58
+ };
59
+
60
+ // Returns a copy of the tiles, with tiles flipped according to
61
+ // whether or not their y, x position satisfies the predicate
62
+ const flipTilesPredicate = (oldCells, predicate) => {
63
+ return _.map(oldCells, (row, y) => {
64
+ return _.map(row, (cell, x) => {
65
+ return predicate(y, x) ? !cell : cell;
66
+ });
67
+ });
68
+ };
69
+
70
+ type TileProps = $FlowFixMe;
71
+
72
+ // A single glowy cell
73
+ class Tile extends React.Component<TileProps> {
74
+ static propTypes = {
75
+ value: PropTypes.bool.isRequired,
76
+ size: PropTypes.number.isRequired,
77
+ };
78
+
79
+ render(): React.Node {
80
+ const color = this.props.value ? "#55dd55" : "#115511";
81
+ const style = _.extend({}, BASE_TILE_STYLE, {
82
+ width: this.props.size,
83
+ height: this.props.size,
84
+ backgroundColor: color,
85
+ });
86
+ return <div style={style} onClick={this._flip} />;
87
+ }
88
+
89
+ _flip = () => {
90
+ this.props.onChange(!this.props.value);
91
+ };
92
+ }
93
+
94
+ type TileGridProps = $FlowFixMe;
95
+
96
+ // A grid of glowy cells
97
+ class TileGrid extends React.Component<TileGridProps> {
98
+ static propTypes = {
99
+ cells: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.bool)).isRequired,
100
+ size: PropTypes.number.isRequired,
101
+ };
102
+
103
+ render(): React.Node {
104
+ return (
105
+ <div style={TABLE_STYLE} className="no-select">
106
+ {_.map(this.props.cells, (row, y) => {
107
+ return (
108
+ <div key={y} style={ROW_STYLE}>
109
+ {_.map(row, (cell, x) => {
110
+ return (
111
+ <div key={x} style={CELL_STYLE}>
112
+ <Tile
113
+ value={cell}
114
+ size={this.props.size}
115
+ onChange={_.partial(
116
+ this.props.onChange,
117
+ y,
118
+ x,
119
+ )}
120
+ />
121
+ </div>
122
+ );
123
+ })}
124
+ </div>
125
+ );
126
+ })}
127
+ </div>
128
+ );
129
+ }
130
+ }
131
+
132
+ type LightsPuzzleEditorProps = $FlowFixMe;
133
+
134
+ // The widget editor
135
+ class LightsPuzzleEditor extends React.Component<LightsPuzzleEditorProps> {
136
+ static propTypes = {
137
+ ...Changeable.propTypes,
138
+ startCells: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.bool)),
139
+ flipPattern: PropTypes.string.isRequired,
140
+ gradeIncompleteAsWrong: PropTypes.bool.isRequired,
141
+ };
142
+
143
+ static widgetName: "lights-puzzle" = "lights-puzzle";
144
+
145
+ static defaultProps: LightsPuzzleEditorProps = {
146
+ startCells: [
147
+ [false, false, false],
148
+ [false, false, false],
149
+ [false, false, false],
150
+ ],
151
+ flipPattern: "plus",
152
+ gradeIncompleteAsWrong: false,
153
+ };
154
+
155
+ _height: () => number = () => {
156
+ return this.props.startCells.length;
157
+ };
158
+
159
+ _width: () => number = () => {
160
+ if (this.props.startCells.length !== 0) {
161
+ return this.props.startCells[0].length;
162
+ }
163
+ return 0; // default to 0
164
+ };
165
+
166
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
167
+ return Changeable.change.apply(this, args);
168
+ };
169
+
170
+ render(): React.Node {
171
+ return (
172
+ <div>
173
+ <div>
174
+ Width:
175
+ <NumberInput
176
+ value={this._width()}
177
+ placeholder={5}
178
+ onChange={this._changeWidth}
179
+ />
180
+ {", "}
181
+ Height:
182
+ <NumberInput
183
+ value={this._height()}
184
+ placeholder={5}
185
+ onChange={this._changeHeight}
186
+ />
187
+ </div>
188
+ <div>
189
+ Flip pattern:
190
+ <select
191
+ value={this.props.flipPattern}
192
+ onChange={this._handlePatternChange}
193
+ >
194
+ {_.map(_.keys(PATTERNS), (pattern, i) => {
195
+ return (
196
+ <option value={pattern} key={i}>
197
+ {pattern}
198
+ </option>
199
+ );
200
+ })}
201
+ </select>
202
+ </div>
203
+ <div>
204
+ Grade incomplete puzzles as wrong:{" "}
205
+ <PropCheckBox
206
+ gradeIncompleteAsWrong={
207
+ this.props.gradeIncompleteAsWrong
208
+ }
209
+ onChange={this.props.onChange}
210
+ />
211
+ <InfoTip>
212
+ By default, incomplete puzzles are graded as empty.
213
+ </InfoTip>
214
+ </div>
215
+ <div>Starting configuration:</div>
216
+ <div style={{overflowX: "auto"}}>
217
+ <TileGrid
218
+ cells={this.props.startCells}
219
+ size={50}
220
+ onChange={this._switchTile}
221
+ />
222
+ </div>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ _handlePatternChange: (SyntheticInputEvent<>) => void = (e) => {
228
+ this.change("flipPattern", e.target.value);
229
+ };
230
+
231
+ _changeWidth: (number) => void = (newWidth) => {
232
+ newWidth = clampToInt(newWidth, 1, MAX_SIZE);
233
+ this._truncateCells(newWidth, this._height());
234
+ };
235
+
236
+ _changeHeight: (number) => void = (newHeight) => {
237
+ newHeight = clampToInt(newHeight, 1, MAX_SIZE);
238
+ this._truncateCells(this._width(), newHeight);
239
+ };
240
+
241
+ _truncateCells: (number, number) => void = (newWidth, newHeight) => {
242
+ const newCells = _.times(newHeight, (y) => {
243
+ return _.times(newWidth, (x) => {
244
+ // explicitly cast the result to a boolean with !!
245
+ return !!(
246
+ this.props.startCells[y] && this.props.startCells[y][x]
247
+ );
248
+ });
249
+ });
250
+
251
+ this.change({startCells: newCells});
252
+ };
253
+
254
+ _switchTile: (number, number) => void = (tileY, tileX) => {
255
+ const newCells = flipTilesPredicate(this.props.startCells, (y, x) => {
256
+ return y === tileY && x === tileX;
257
+ });
258
+
259
+ this.change({startCells: newCells});
260
+ };
261
+
262
+ serialize: () => $FlowFixMe = () => {
263
+ return EditorJsonify.serialize.call(this);
264
+ };
265
+ }
266
+
267
+ export default LightsPuzzleEditor;
@@ -0,0 +1,147 @@
1
+ /* eslint-disable react/forbid-prop-types, react/sort-comp */
2
+ // @flow
3
+ import {components} from "@khanacademy/perseus";
4
+ import PropTypes from "prop-types";
5
+ import * as React from "react";
6
+ import _ from "underscore";
7
+
8
+ const {InfoTip, PropCheckBox, TextListEditor} = components;
9
+
10
+ type Props = $FlowFixMe;
11
+
12
+ class MatcherEditor extends React.Component<Props> {
13
+ static propTypes = {
14
+ left: PropTypes.array,
15
+ right: PropTypes.array,
16
+ labels: PropTypes.array,
17
+ orderMatters: PropTypes.bool,
18
+ padding: PropTypes.bool,
19
+ };
20
+
21
+ static widgetName: "matcher" = "matcher";
22
+
23
+ static defaultProps: Props = {
24
+ left: ["$x$", "$y$", "$z$"],
25
+ right: ["$1$", "$2$", "$3$"],
26
+ labels: ["test", "label"],
27
+ orderMatters: false,
28
+ padding: true,
29
+ };
30
+
31
+ render(): React.Node {
32
+ return (
33
+ <div className="perseus-matcher-editor">
34
+ <div>
35
+ {" "}
36
+ Correct answer:{" "}
37
+ <InfoTip>
38
+ <p>
39
+ Enter the correct answers here. The preview on the
40
+ right will show the cards in a randomized order,
41
+ which is how the student will see them.
42
+ </p>
43
+ </InfoTip>
44
+ </div>
45
+ <div className="perseus-clearfix">
46
+ <TextListEditor
47
+ options={this.props.left}
48
+ onChange={(options, cb) => {
49
+ this.props.onChange({left: options}, cb);
50
+ }}
51
+ layout="vertical"
52
+ />
53
+ <TextListEditor
54
+ options={this.props.right}
55
+ onChange={(options, cb) => {
56
+ this.props.onChange({right: options}, cb);
57
+ }}
58
+ layout="vertical"
59
+ />
60
+ </div>
61
+ <span>
62
+ {" "}
63
+ Labels:{" "}
64
+ <InfoTip>
65
+ <p>These are entirely optional.</p>
66
+ </InfoTip>
67
+ </span>
68
+ <div>
69
+ <input
70
+ type="text"
71
+ defaultValue={this.props.labels[0]}
72
+ // eslint-disable-next-line react/jsx-no-bind
73
+ onChange={this.onLabelChange.bind(this, 0)}
74
+ />
75
+ <input
76
+ type="text"
77
+ defaultValue={this.props.labels[1]}
78
+ // eslint-disable-next-line react/jsx-no-bind
79
+ onChange={this.onLabelChange.bind(this, 1)}
80
+ />
81
+ </div>
82
+ <div>
83
+ <PropCheckBox
84
+ label="Order of the matched pairs matters:"
85
+ orderMatters={this.props.orderMatters}
86
+ onChange={this.props.onChange}
87
+ />
88
+ <InfoTip>
89
+ <p>
90
+ With this option enabled, only the order provided
91
+ above will be treated as correct. This is useful
92
+ when ordering is significant, such as in the context
93
+ of a proof.
94
+ </p>
95
+ <p>
96
+ If disabled, pairwise matching is sufficient. To
97
+ make this clear, the left column becomes fixed in
98
+ the provided order and only the cards in the right
99
+ column can be moved.
100
+ </p>
101
+ </InfoTip>
102
+ </div>
103
+ <div>
104
+ <PropCheckBox
105
+ label="Padding:"
106
+ padding={this.props.padding}
107
+ onChange={this.props.onChange}
108
+ />
109
+ <InfoTip>
110
+ <p>
111
+ Padding is good for text, but not needed for images.
112
+ </p>
113
+ </InfoTip>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ onLabelChange: (number, SyntheticInputEvent<>) => void = (index, e) => {
120
+ const labels = _.clone(this.props.labels);
121
+ labels[index] = e.target.value;
122
+ this.props.onChange({labels: labels});
123
+ };
124
+
125
+ getSaveWarnings: () => $ReadOnlyArray<string> = () => {
126
+ if (this.props.left.length !== this.props.right.length) {
127
+ return [
128
+ "The two halves of the matcher have different numbers" +
129
+ " of cards.",
130
+ ];
131
+ }
132
+ return [];
133
+ };
134
+
135
+ serialize: $FlowFixMe = () => {
136
+ return _.pick(
137
+ this.props,
138
+ "left",
139
+ "right",
140
+ "labels",
141
+ "orderMatters",
142
+ "padding",
143
+ );
144
+ };
145
+ }
146
+
147
+ export default MatcherEditor;
@@ -0,0 +1,155 @@
1
+ /* eslint-disable react/sort-comp */
2
+ // @flow
3
+ import {
4
+ components,
5
+ Changeable,
6
+ EditorJsonify,
7
+ MatrixWidget,
8
+ } from "@khanacademy/perseus";
9
+ import PropTypes from "prop-types";
10
+ import * as React from "react";
11
+ import _ from "underscore";
12
+
13
+ import Editor from "../editor.jsx";
14
+
15
+ const {RangeInput} = components;
16
+ const Matrix = MatrixWidget.widget;
17
+
18
+ // Really large matrices will cause issues with question formatting, so we
19
+ // have to cap it at some point.
20
+ const MAX_BOARD_SIZE = 6;
21
+
22
+ const getMatrixSize = function (matrix) {
23
+ const matrixSize = [1, 1];
24
+
25
+ // We need to find the widest row and tallest column to get the correct
26
+ // matrix size.
27
+ _(matrix).each((matrixRow, row) => {
28
+ let rowWidth = 0;
29
+ _(matrixRow).each((matrixCol, col) => {
30
+ if (matrixCol != null && matrixCol.toString().length) {
31
+ rowWidth = col + 1;
32
+ }
33
+ });
34
+
35
+ // Matrix width:
36
+ matrixSize[1] = Math.max(matrixSize[1], rowWidth);
37
+
38
+ // Matrix height:
39
+ if (rowWidth > 0) {
40
+ matrixSize[0] = Math.max(matrixSize[0], row + 1);
41
+ }
42
+ });
43
+ return matrixSize;
44
+ };
45
+
46
+ type Props = $FlowFixMe;
47
+
48
+ class MatrixEditor extends React.Component<Props> {
49
+ static propTypes = {
50
+ ...Changeable.propTypes,
51
+ matrixBoardSize: PropTypes.arrayOf(PropTypes.number).isRequired,
52
+ answers: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
53
+ prefix: PropTypes.string,
54
+ suffix: PropTypes.string,
55
+ cursorPosition: PropTypes.arrayOf(PropTypes.number),
56
+ };
57
+
58
+ static widgetName: "matrix" = "matrix";
59
+
60
+ static defaultProps: Props = {
61
+ matrixBoardSize: [3, 3],
62
+ answers: [[]],
63
+ prefix: "",
64
+ suffix: "",
65
+ cursorPosition: [0, 0],
66
+ };
67
+
68
+ render(): React.Node {
69
+ const matrixProps = _.extend(
70
+ {
71
+ numericInput: true,
72
+ onBlur: () => {},
73
+ onFocus: () => {},
74
+ trackInteraction: () => {},
75
+ },
76
+ this.props,
77
+ );
78
+ return (
79
+ <div className="perseus-matrix-editor">
80
+ <div className="perseus-widget-row">
81
+ {" "}
82
+ Max matrix size:{" "}
83
+ <RangeInput
84
+ value={this.props.matrixBoardSize}
85
+ onChange={this.onMatrixBoardSizeChange}
86
+ format={this.props.labelStyle}
87
+ useArrowKeys={true}
88
+ />
89
+ </div>
90
+ <div className="perseus-widget-row">
91
+ <Matrix {...matrixProps} />
92
+ </div>
93
+ <div className="perseus-widget-row">
94
+ {" "}
95
+ Matrix prefix:{" "}
96
+ <Editor
97
+ // eslint-disable-next-line react/no-string-refs
98
+ ref="prefix"
99
+ apiOptions={this.props.apiOptions}
100
+ content={this.props.prefix}
101
+ widgetEnabled={false}
102
+ onChange={(newProps) => {
103
+ this.change({prefix: newProps.content});
104
+ }}
105
+ />
106
+ </div>
107
+ <div className="perseus-widget-row">
108
+ {" "}
109
+ Matrix suffix:{" "}
110
+ <Editor
111
+ // eslint-disable-next-line react/no-string-refs
112
+ ref="suffix"
113
+ apiOptions={this.props.apiOptions}
114
+ content={this.props.suffix}
115
+ widgetEnabled={false}
116
+ onChange={(newProps) => {
117
+ this.change({suffix: newProps.content});
118
+ }}
119
+ />
120
+ </div>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
126
+ return Changeable.change.apply(this, args);
127
+ };
128
+
129
+ onMatrixBoardSizeChange: ([number, number]) => void = (range) => {
130
+ const matrixSize = getMatrixSize(this.props.answers);
131
+ if (range[0] !== null && range[1] !== null) {
132
+ range = [
133
+ Math.round(Math.min(Math.max(range[0], 1), MAX_BOARD_SIZE)),
134
+ Math.round(Math.min(Math.max(range[1], 1), MAX_BOARD_SIZE)),
135
+ ];
136
+ const answers = _(Math.min(range[0], matrixSize[0])).times(
137
+ (row) => {
138
+ return _(Math.min(range[1], matrixSize[1])).times((col) => {
139
+ return this.props.answers[row][col];
140
+ });
141
+ },
142
+ );
143
+ this.props.onChange({
144
+ matrixBoardSize: range,
145
+ answers: answers,
146
+ });
147
+ }
148
+ };
149
+
150
+ serialize: () => $FlowFixMe = () => {
151
+ return EditorJsonify.serialize.call(this);
152
+ };
153
+ }
154
+
155
+ export default MatrixEditor;