@khanacademy/perseus-editor 0.0.1 → 0.1.0

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 (125) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/es/index.js +10 -1
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.js +10 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/strings.js +6 -1
  8. package/package.json +16 -4
  9. package/src/README.md +7 -0
  10. package/src/__stories__/editor.stories.js +33 -0
  11. package/src/__stories__/section-control-button.stories.jsx +27 -0
  12. package/src/__testdata__/input-number_testdata.js +28 -0
  13. package/src/__tests__/editor_test.jsx +123 -0
  14. package/src/__tests__/i18n_test.jsx +585 -0
  15. package/src/__tests__/traversal_test.jsx +378 -0
  16. package/src/all-editors.js +85 -0
  17. package/src/all-editors_test.js +17 -0
  18. package/src/all-widgets.js +10 -0
  19. package/src/article-editor.jsx +437 -0
  20. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  21. package/src/components/blur-input.jsx +62 -0
  22. package/src/components/checkbox.jsx +194 -0
  23. package/src/components/device-framer.jsx +91 -0
  24. package/src/components/drag-target.jsx +88 -0
  25. package/src/components/dropdown-option.jsx +318 -0
  26. package/src/components/form-wrapped-text-field.jsx +248 -0
  27. package/src/components/graph-settings.jsx +631 -0
  28. package/src/components/hover-behavior.jsx +178 -0
  29. package/src/components/json-editor.jsx +118 -0
  30. package/src/components/link.jsx +173 -0
  31. package/src/components/simple-button.jsx +86 -0
  32. package/src/components/sortable.jsx +186 -0
  33. package/src/components/util.js +39 -0
  34. package/src/components/viewport-resizer.jsx +73 -0
  35. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  36. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  37. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  38. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  39. package/src/diffs/__tests__/split-diff_test.js +34 -0
  40. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  41. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  42. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  43. package/src/diffs/article-diff.jsx +70 -0
  44. package/src/diffs/item-diff.jsx +77 -0
  45. package/src/diffs/renderer-diff.jsx +143 -0
  46. package/src/diffs/split-diff.jsx +30 -0
  47. package/src/diffs/string-array-diff.jsx +74 -0
  48. package/src/diffs/structured-item-diff.jsx +269 -0
  49. package/src/diffs/tags-diff.jsx +50 -0
  50. package/src/diffs/text-diff.jsx +193 -0
  51. package/src/diffs/widget-diff-performer.jsx +74 -0
  52. package/src/diffs/widget-diff.jsx +291 -0
  53. package/src/editor-page.jsx +351 -0
  54. package/src/editor.jsx +1364 -0
  55. package/src/hint-editor.jsx +447 -0
  56. package/src/i18n.jsx +139 -0
  57. package/src/iframe-content-renderer.jsx +201 -0
  58. package/src/index.js +27 -8
  59. package/src/item-editor.jsx +159 -0
  60. package/src/item-extras-editor.jsx +150 -0
  61. package/src/katex-error-view.jsx +87 -0
  62. package/src/multirenderer-editor.jsx +1073 -0
  63. package/src/section-control-button.jsx +53 -0
  64. package/src/stateful-article-editor.jsx +176 -0
  65. package/src/stateful-editor-page.jsx +90 -0
  66. package/src/styles/perseus-editor.less +1339 -0
  67. package/src/testing-widgets.js +12 -0
  68. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  69. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  70. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  71. package/src/widgets/categorizer-editor.jsx +103 -0
  72. package/src/widgets/cs-program-editor.jsx +258 -0
  73. package/src/widgets/definition-editor.jsx +76 -0
  74. package/src/widgets/dropdown-editor.jsx +201 -0
  75. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  76. package/src/widgets/example-graphie-widget.jsx +175 -0
  77. package/src/widgets/example-widget-editor.jsx +60 -0
  78. package/src/widgets/example-widget.jsx +153 -0
  79. package/src/widgets/explanation-editor.jsx +89 -0
  80. package/src/widgets/expression-editor.jsx +559 -0
  81. package/src/widgets/graded-group-editor.jsx +162 -0
  82. package/src/widgets/graded-group-set-editor.jsx +101 -0
  83. package/src/widgets/grapher-editor.jsx +154 -0
  84. package/src/widgets/group-editor.jsx +83 -0
  85. package/src/widgets/iframe-editor.jsx +200 -0
  86. package/src/widgets/image-editor.jsx +326 -0
  87. package/src/widgets/input-number-editor.jsx +269 -0
  88. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  89. package/src/widgets/interaction-editor.jsx +1388 -0
  90. package/src/widgets/interactive-graph-editor.jsx +320 -0
  91. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  92. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  93. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  94. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  95. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  96. package/src/widgets/label-image/answer-choices.jsx +231 -0
  97. package/src/widgets/label-image/behavior.jsx +92 -0
  98. package/src/widgets/label-image/marker.jsx +292 -0
  99. package/src/widgets/label-image/question-markers.jsx +181 -0
  100. package/src/widgets/label-image/select-image.jsx +86 -0
  101. package/src/widgets/label-image-editor.jsx +256 -0
  102. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  103. package/src/widgets/matcher-editor.jsx +147 -0
  104. package/src/widgets/matrix-editor.jsx +155 -0
  105. package/src/widgets/measurer-editor.jsx +245 -0
  106. package/src/widgets/molecule-editor.jsx +63 -0
  107. package/src/widgets/number-line-editor.jsx +498 -0
  108. package/src/widgets/numeric-input-editor.jsx +554 -0
  109. package/src/widgets/orderer-editor.jsx +166 -0
  110. package/src/widgets/passage-editor.jsx +115 -0
  111. package/src/widgets/passage-ref-editor.jsx +77 -0
  112. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  113. package/src/widgets/plotter-editor.jsx +537 -0
  114. package/src/widgets/radio/editor.jsx +399 -0
  115. package/src/widgets/reaction-diagram-editor.jsx +126 -0
  116. package/src/widgets/sequence-editor.jsx +205 -0
  117. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  118. package/src/widgets/simple-markdown-tester.jsx +91 -0
  119. package/src/widgets/simulator-editor.jsx +144 -0
  120. package/src/widgets/sorter-editor.jsx +100 -0
  121. package/src/widgets/table-editor.jsx +164 -0
  122. package/src/widgets/transformer-editor.jsx +1006 -0
  123. package/src/widgets/unit-editor.jsx +293 -0
  124. package/src/widgets/video-editor.jsx +74 -0
  125. package/src/__tests__/index.test.js +0 -15
@@ -0,0 +1,162 @@
1
+ /* eslint-disable react/forbid-prop-types, react/sort-comp */
2
+ // @flow
3
+ import {components, icons, ApiOptions, Changeable} from "@khanacademy/perseus";
4
+ import {StyleSheet, css} from "aphrodite";
5
+ import PropTypes from "prop-types";
6
+ import * as React from "react";
7
+ import _ from "underscore";
8
+
9
+ import Editor from "../editor.jsx";
10
+
11
+ const {InlineIcon, TextInput} = components;
12
+ const {iconPlus, iconTrash} = icons;
13
+
14
+ type Props = $FlowFixMe;
15
+
16
+ class GradedGroupEditor extends React.Component<Props> {
17
+ static propTypes = {
18
+ ...Changeable.propTypes,
19
+ title: PropTypes.string,
20
+ content: PropTypes.string,
21
+ widgets: PropTypes.object,
22
+ images: PropTypes.object,
23
+ apiOptions: ApiOptions.propTypes,
24
+ };
25
+
26
+ static widgetName: "graded-group" = "graded-group";
27
+
28
+ static defaultProps: Props = {
29
+ title: "",
30
+ content: "",
31
+ widgets: {},
32
+ images: {},
33
+ hint: null,
34
+ };
35
+
36
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
37
+ return Changeable.change.apply(this, args);
38
+ };
39
+
40
+ handleAddHint: () => void = () => {
41
+ const hint = {content: ""};
42
+ this.props.onChange({hint}, () => {
43
+ // eslint-disable-next-line react/no-string-refs
44
+ this.refs["hint-editor"].focus();
45
+ });
46
+ };
47
+
48
+ handleRemoveHint: (SyntheticMouseEvent<>) => void = (e) => {
49
+ this.props.onChange({hint: null});
50
+ };
51
+
52
+ render(): React.Node {
53
+ return (
54
+ <div className="perseus-group-editor">
55
+ <div className="perseus-widget-row">
56
+ <label className={css(styles.title)}>
57
+ Title:{" "}
58
+ <TextInput
59
+ value={this.props.title}
60
+ className={css(styles.input)}
61
+ onChange={this.change("title")}
62
+ />
63
+ </label>
64
+ </div>
65
+ <Editor
66
+ // eslint-disable-next-line react/no-string-refs
67
+ ref="editor"
68
+ content={this.props.content}
69
+ widgets={this.props.widgets}
70
+ apiOptions={this.props.apiOptions}
71
+ images={this.props.images}
72
+ widgetEnabled={true}
73
+ immutableWidgets={false}
74
+ onChange={this.props.onChange}
75
+ warnNoPrompt={true}
76
+ warnNoWidgets={true}
77
+ />
78
+ {!this.props.hint && (
79
+ <button
80
+ type="button"
81
+ style={{marginTop: 10}}
82
+ className="add-hint simple-button orange"
83
+ onClick={this.handleAddHint}
84
+ >
85
+ <InlineIcon {...iconPlus} /> Add a hint
86
+ </button>
87
+ )}
88
+ {this.props.hint && (
89
+ <div className="perseus-hint-editor">
90
+ <div className={css(styles.hintsTitle)}>Hint</div>
91
+ <Editor
92
+ // eslint-disable-next-line react/no-string-refs
93
+ ref="hint-editor"
94
+ content={
95
+ this.props.hint ? this.props.hint.content : ""
96
+ }
97
+ widgets={
98
+ this.props.hint ? this.props.hint.widgets : {}
99
+ }
100
+ apiOptions={this.props.apiOptions}
101
+ images={this.props.hint && this.props.hint.images}
102
+ widgetEnabled={true}
103
+ immutableWidgets={false}
104
+ onChange={(props) => {
105
+ // Copy all props over from the existing hint
106
+ // and then add new props.
107
+ this.change(
108
+ "hint",
109
+ Object.assign({}, this.props.hint, props),
110
+ );
111
+ }}
112
+ />
113
+ <button
114
+ type="button"
115
+ className="remove-hint simple-button orange"
116
+ onClick={this.handleRemoveHint}
117
+ >
118
+ <InlineIcon {...iconTrash} /> Remove this hint
119
+ </button>
120
+ </div>
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ getSaveWarnings: () => $FlowFixMe = () => {
127
+ // eslint-disable-next-line react/no-string-refs
128
+ return this.refs.editor.getSaveWarnings();
129
+ };
130
+
131
+ serialize: () => {|title: string, hint: ?$FlowFixMe|} = () => {
132
+ return {
133
+ title: this.props.title,
134
+ // eslint-disable-next-line react/no-string-refs
135
+ ...this.refs.editor.serialize(),
136
+ hint:
137
+ // eslint-disable-next-line react/no-string-refs
138
+ this.refs["hint-editor"] &&
139
+ // eslint-disable-next-line react/no-string-refs
140
+ this.refs["hint-editor"].serialize(),
141
+ };
142
+ };
143
+ }
144
+
145
+ const styles = StyleSheet.create({
146
+ title: {
147
+ fontSize: 18,
148
+ fontWeight: "bold",
149
+ },
150
+
151
+ input: {
152
+ fontSize: 18,
153
+ },
154
+
155
+ hintsTitle: {
156
+ marginTop: 10,
157
+ fontSize: "110%",
158
+ fontWeight: "bold",
159
+ },
160
+ });
161
+
162
+ export default GradedGroupEditor;
@@ -0,0 +1,101 @@
1
+ /* eslint-disable react/forbid-prop-types, react/no-unsafe */
2
+ // @flow
3
+ import {ApiOptions, Changeable} from "@khanacademy/perseus";
4
+ import PropTypes from "prop-types";
5
+ import * as React from "react";
6
+ import _ from "underscore";
7
+
8
+ import GradedGroupEditor from "./graded-group-editor.jsx";
9
+
10
+ type Props = $FlowFixMe;
11
+
12
+ class GradedGroupSetEditor extends React.Component<Props> {
13
+ // eslint-disable-next-line flowtype/no-mutable-array
14
+ _editors: Array<$FlowFixMe>;
15
+
16
+ static propTypes = {
17
+ ...Changeable.propTypes,
18
+ apiOptions: ApiOptions.propTypes,
19
+ gradedGroups: PropTypes.array,
20
+ onChange: PropTypes.func.isRequired,
21
+ };
22
+
23
+ static widgetName: "graded-group-set" = "graded-group-set";
24
+
25
+ static defaultProps: Props = {
26
+ gradedGroups: [],
27
+ };
28
+
29
+ // TODO(jangmi, CP-3288): Remove usage of `UNSAFE_componentWillMount`
30
+ UNSAFE_componentWillMount() {
31
+ this._editors = [];
32
+ }
33
+
34
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
35
+ return Changeable.change.apply(this, args);
36
+ };
37
+
38
+ getSaveWarnings: () => $ReadOnlyArray<$FlowFixMe> = () => {
39
+ return [].concat(
40
+ ...this._editors.map((editor) =>
41
+ editor ? editor.getSaveWarnings() : [],
42
+ ),
43
+ );
44
+ };
45
+
46
+ serialize: () => {|gradedGroups: $FlowFixMe|} = () => {
47
+ return {
48
+ gradedGroups: this.props.gradedGroups,
49
+ };
50
+ };
51
+
52
+ renderGroups: () => React.Node = () => {
53
+ if (!this.props.gradedGroups) {
54
+ return null;
55
+ }
56
+ return this.props.gradedGroups.map((group, i) => (
57
+ <GradedGroupEditor
58
+ key={i}
59
+ ref={(el) => (this._editors[i] = el)}
60
+ {...group}
61
+ apiOptions={this.props.apiOptions}
62
+ widgetEnabled={true}
63
+ immutableWidgets={false}
64
+ onChange={(data) =>
65
+ this.change(
66
+ "gradedGroups",
67
+ setArrayItem(this.props.gradedGroups, i, {
68
+ ...this.props.gradedGroups[i],
69
+ ...data,
70
+ }),
71
+ )
72
+ }
73
+ />
74
+ ));
75
+ };
76
+
77
+ addGroup: () => void = () => {
78
+ const groups = this.props.gradedGroups || [];
79
+ this.change(
80
+ "gradedGroups",
81
+ groups.concat([GradedGroupEditor.defaultProps]),
82
+ );
83
+ };
84
+
85
+ render(): React.Node {
86
+ return (
87
+ <div className="perseus-group-editor">
88
+ {this.renderGroups()}
89
+ <button onClick={this.addGroup}>Add group</button>
90
+ </div>
91
+ );
92
+ }
93
+ }
94
+
95
+ const setArrayItem = (list, i, value) => [
96
+ ...list.slice(0, i),
97
+ value,
98
+ ...list.slice(i + 1),
99
+ ];
100
+
101
+ export default GradedGroupSetEditor;
@@ -0,0 +1,154 @@
1
+ /* eslint-disable react/sort-comp */
2
+ // @flow
3
+ import {
4
+ components,
5
+ Changeable,
6
+ GrapherUtil,
7
+ GrapherWidget,
8
+ SizingUtils,
9
+ } from "@khanacademy/perseus";
10
+ import * as React from "react";
11
+ import _ from "underscore";
12
+
13
+ import GraphSettings from "../components/graph-settings.jsx";
14
+
15
+ const {InfoTip, MultiButtonGroup} = components;
16
+ const {containerSizeClass, getInteractiveBoxFromSizeClass} = SizingUtils;
17
+ const Grapher = GrapherWidget.widget;
18
+ const {
19
+ DEFAULT_GRAPHER_PROPS,
20
+ allTypes,
21
+ chooseType,
22
+ defaultPlotProps,
23
+ getEquationString,
24
+ typeToButton,
25
+ } = GrapherUtil;
26
+
27
+ type Props = $FlowFixMe;
28
+
29
+ class GrapherEditor extends React.Component<Props> {
30
+ static propTypes = {
31
+ ...Changeable.propTypes,
32
+ };
33
+
34
+ static widgetName: "grapher" = "grapher";
35
+
36
+ static defaultProps: Props = {
37
+ correct: DEFAULT_GRAPHER_PROPS.plot,
38
+ graph: DEFAULT_GRAPHER_PROPS.graph,
39
+ availableTypes: DEFAULT_GRAPHER_PROPS.availableTypes,
40
+ };
41
+
42
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
43
+ return Changeable.change.apply(this, args);
44
+ };
45
+
46
+ render(): React.Node {
47
+ const sizeClass = containerSizeClass.SMALL;
48
+ let equationString;
49
+ let graph;
50
+ if (this.props.graph.valid === true) {
51
+ const graphProps = {
52
+ graph: this.props.graph,
53
+ plot: this.props.correct,
54
+ availableTypes: this.props.availableTypes,
55
+ onChange: (newProps, cb) => {
56
+ let correct = this.props.correct;
57
+ if (correct.type === newProps.plot?.type) {
58
+ correct = _.extend({}, correct, newProps.plot);
59
+ } else {
60
+ // Clear options from previous graph
61
+ correct = newProps.plot;
62
+ }
63
+ this.props.onChange({correct: correct}, cb);
64
+ },
65
+ trackInteraction: function () {},
66
+ };
67
+
68
+ graph = (
69
+ // NOTE(jeremy): This editor doesn't pass in a bunch of
70
+ // standard props that the Renderer provides normally (eg.
71
+ // alignment, findWidgets, etc).
72
+ // $FlowFixMe[prop-missing]
73
+ <Grapher
74
+ {...graphProps}
75
+ apiOptions={this.props.apiOptions}
76
+ containerSizeClass={sizeClass}
77
+ />
78
+ );
79
+ equationString = getEquationString(graphProps);
80
+ } else {
81
+ graph = (
82
+ <div className="perseus-error">{this.props.graph.valid}</div>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <div>
88
+ <div>
89
+ Correct answer{" "}
90
+ <InfoTip>
91
+ <p>
92
+ Graph the correct answer in the graph below and
93
+ ensure the equation or point coordinates displayed
94
+ represent the correct answer.
95
+ </p>
96
+ </InfoTip>{" "}
97
+ : {equationString}
98
+ </div>
99
+
100
+ <GraphSettings
101
+ editableSettings={["graph", "snap", "image"]}
102
+ box={getInteractiveBoxFromSizeClass(sizeClass)}
103
+ range={this.props.graph.range}
104
+ labels={this.props.graph.labels}
105
+ step={this.props.graph.step}
106
+ gridStep={this.props.graph.gridStep}
107
+ snapStep={this.props.graph.snapStep}
108
+ valid={this.props.graph.valid}
109
+ backgroundImage={this.props.graph.backgroundImage}
110
+ markings={this.props.graph.markings}
111
+ rulerLabel={this.props.graph.rulerLabel}
112
+ rulerTicks={this.props.graph.rulerTicks}
113
+ showTooltips={this.props.graph.showTooltips}
114
+ onChange={this.change("graph")}
115
+ />
116
+ <div className="perseus-widget-row">
117
+ <label>Available functions: </label>
118
+ <MultiButtonGroup
119
+ allowEmpty={false}
120
+ values={this.props.availableTypes}
121
+ buttons={_.map(allTypes, typeToButton)}
122
+ onChange={this.handleAvailableTypesChange}
123
+ />
124
+ </div>
125
+ {graph}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ handleAvailableTypesChange: ($FlowFixMe) => void = (newAvailableTypes) => {
131
+ let correct = this.props.correct;
132
+
133
+ // If the currently 'correct' type is removed from the list of types,
134
+ // we need to change it to avoid impossible questions.
135
+ if (!_.contains(newAvailableTypes, this.props.correct.type)) {
136
+ const graph = this.props.graph;
137
+ const newType = chooseType(newAvailableTypes);
138
+ correct = defaultPlotProps(newType, graph);
139
+ }
140
+ this.props.onChange({
141
+ availableTypes: newAvailableTypes,
142
+ correct: correct,
143
+ });
144
+ };
145
+
146
+ serialize: () => $FlowFixMe = () => {
147
+ return _.chain(this.props)
148
+ .pick("correct", "availableTypes")
149
+ .extend({graph: _.omit(this.props.graph, "box")})
150
+ .value();
151
+ };
152
+ }
153
+
154
+ export default GrapherEditor;
@@ -0,0 +1,83 @@
1
+ /* eslint-disable react/forbid-prop-types, react/sort-comp */
2
+ // @flow
3
+ import {ApiOptions, Changeable} from "@khanacademy/perseus";
4
+ import PropTypes from "prop-types";
5
+ import * as React from "react";
6
+ import _ from "underscore";
7
+
8
+ import Editor from "../editor.jsx";
9
+
10
+ type Props = $FlowFixMe;
11
+
12
+ class GroupEditor extends React.Component<Props> {
13
+ static propTypes = {
14
+ ...Changeable.propTypes,
15
+ content: PropTypes.string,
16
+ widgets: PropTypes.object,
17
+ images: PropTypes.object,
18
+ metadata: PropTypes.any,
19
+ apiOptions: ApiOptions.propTypes,
20
+ };
21
+
22
+ static widgetName: "group" = "group";
23
+
24
+ static defaultProps: Props = {
25
+ content: "",
26
+ widgets: {},
27
+ images: {},
28
+ // `undefined` instead of `null` so that getDefaultProps works for
29
+ // `the GroupMetadataEditor`
30
+ metadata: undefined,
31
+ };
32
+
33
+ render(): React.Node {
34
+ return (
35
+ <div className="perseus-group-editor">
36
+ <div>
37
+ {/* the metadata editor; used for tags on
38
+ khanacademy.org */}
39
+ {this._renderMetadataEditor()}
40
+ </div>
41
+ <Editor
42
+ // eslint-disable-next-line react/no-string-refs
43
+ ref="editor"
44
+ content={this.props.content}
45
+ widgets={this.props.widgets}
46
+ apiOptions={this.props.apiOptions}
47
+ images={this.props.images}
48
+ widgetEnabled={true}
49
+ immutableWidgets={false}
50
+ onChange={this.props.onChange}
51
+ />
52
+ </div>
53
+ );
54
+ }
55
+
56
+ _renderMetadataEditor: () => React.Node = () => {
57
+ const GroupMetadataEditor = this.props.apiOptions.GroupMetadataEditor;
58
+ return (
59
+ <GroupMetadataEditor
60
+ value={this.props.metadata}
61
+ onChange={this.change("metadata")}
62
+ />
63
+ );
64
+ };
65
+
66
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
67
+ return Changeable.change.apply(this, args);
68
+ };
69
+
70
+ getSaveWarnings: () => $ReadOnlyArray<$FlowFixMe> = () => {
71
+ // eslint-disable-next-line react/no-string-refs
72
+ return this.refs.editor.getSaveWarnings();
73
+ };
74
+
75
+ serialize: () => $FlowFixMe = () => {
76
+ // eslint-disable-next-line react/no-string-refs
77
+ return _.extend({}, this.refs.editor.serialize(), {
78
+ metadata: this.props.metadata,
79
+ });
80
+ };
81
+ }
82
+
83
+ export default GroupEditor;
@@ -0,0 +1,200 @@
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
+ import BlurInput from "../components/blur-input.jsx";
9
+
10
+ const {PropCheckBox} = components;
11
+
12
+ type PairEditorProps = $FlowFixMe;
13
+
14
+ /**
15
+ * This is used for editing a name/value pair.
16
+ */
17
+ class PairEditor extends React.Component<PairEditorProps> {
18
+ static propTypes = {
19
+ ...Changeable.propTypes,
20
+ name: PropTypes.string,
21
+ value: PropTypes.string,
22
+ };
23
+
24
+ static defaultProps: PairEditorProps = {
25
+ name: "",
26
+ value: "",
27
+ };
28
+
29
+ change = (...args) => {
30
+ return Changeable.change.apply(this, args);
31
+ };
32
+
33
+ render(): React.Node {
34
+ return (
35
+ <fieldset>
36
+ <label>
37
+ Name:
38
+ <BlurInput
39
+ value={this.props.name}
40
+ // $FlowFixMe[incompatible-type] single-param call returns a callback
41
+ onChange={this.change("name")}
42
+ />
43
+ </label>
44
+ <label>
45
+ Value:
46
+ <BlurInput
47
+ value={this.props.value}
48
+ // $FlowFixMe[incompatible-type] single-param call returns a callback
49
+ onChange={this.change("value")}
50
+ />
51
+ </label>
52
+ </fieldset>
53
+ );
54
+ }
55
+
56
+ serialize = () => {
57
+ return EditorJsonify.serialize.call(this);
58
+ };
59
+ }
60
+
61
+ type PairsEditorProps = $FlowFixMe;
62
+
63
+ /**
64
+ * This is used for editing a set of name/value pairs.
65
+ */
66
+ class PairsEditor extends React.Component<PairsEditorProps> {
67
+ static propTypes = {
68
+ ...Changeable.propTypes,
69
+ pairs: PropTypes.arrayOf(
70
+ PropTypes.shape({
71
+ name: PropTypes.string,
72
+ value: PropTypes.string,
73
+ }),
74
+ ).isRequired,
75
+ };
76
+
77
+ render(): React.Node {
78
+ const editors = _.map(this.props.pairs, (pair, i) => {
79
+ return (
80
+ <PairEditor
81
+ key={i}
82
+ name={pair.name}
83
+ value={pair.value}
84
+ // eslint-disable-next-line react/jsx-no-bind
85
+ onChange={this.handlePairChange.bind(this, i)}
86
+ />
87
+ );
88
+ });
89
+ return <div>{editors}</div>;
90
+ }
91
+
92
+ change = (...args) => {
93
+ return Changeable.change.apply(this, args);
94
+ };
95
+
96
+ handlePairChange = (pairIndex, pair) => {
97
+ // If they're both non empty, add a new one
98
+ const pairs = this.props.pairs.slice();
99
+ pairs[pairIndex] = pair;
100
+
101
+ const lastPair = pairs[pairs.length - 1];
102
+ if (lastPair.name && lastPair.value) {
103
+ pairs.push({name: "", value: ""});
104
+ }
105
+ this.change("pairs", pairs);
106
+ };
107
+
108
+ serialize = () => {
109
+ return EditorJsonify.serialize.call(this);
110
+ };
111
+ }
112
+
113
+ type IframeEditorProps = $FlowFixMe;
114
+
115
+ /**
116
+ * This is the main editor for this widget, to specify all the options.
117
+ */
118
+ class IframeEditor extends React.Component<IframeEditorProps> {
119
+ static propTypes = {
120
+ ...Changeable.propTypes,
121
+ };
122
+
123
+ static widgetName: "iframe" = "iframe";
124
+
125
+ static defaultProps: IframeEditorProps = {
126
+ url: "",
127
+ settings: [{name: "", value: ""}],
128
+ width: "400",
129
+ height: "400",
130
+ allowFullScreen: false,
131
+ allowTopNavigation: false,
132
+ };
133
+
134
+ change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
135
+ return Changeable.change.apply(this, args);
136
+ };
137
+
138
+ render(): React.Node {
139
+ return (
140
+ <div>
141
+ <div style={{fontWeight: "bold", textAlign: "center"}}>
142
+ This widget is deprecated! <br />
143
+ Try using the Video or CS Program widgets instead.
144
+ </div>
145
+ <label>
146
+ Url or Program ID:
147
+ <BlurInput
148
+ value={this.props.url}
149
+ onChange={this.change("url")}
150
+ />
151
+ </label>
152
+ <br />
153
+ <label>
154
+ Settings:
155
+ <PairsEditor
156
+ name="settings"
157
+ pairs={this.props.settings}
158
+ onChange={this.handleSettingsChange}
159
+ />
160
+ </label>
161
+ <br />
162
+ <label>
163
+ Width:
164
+ <BlurInput
165
+ value={this.props.width}
166
+ onChange={this.change("width")}
167
+ />
168
+ </label>
169
+ <label>
170
+ Height:
171
+ <BlurInput
172
+ value={this.props.height}
173
+ onChange={this.change("height")}
174
+ />
175
+ </label>
176
+ <PropCheckBox
177
+ label="Allow full screen"
178
+ allowFullScreen={this.props.allowFullScreen}
179
+ onChange={this.props.onChange}
180
+ />
181
+ <br />
182
+ <PropCheckBox
183
+ label="Allow iframe content to redirect the page"
184
+ allowTopNavigation={this.props.allowTopNavigation}
185
+ onChange={this.props.onChange}
186
+ />
187
+ </div>
188
+ );
189
+ }
190
+
191
+ handleSettingsChange: ($FlowFixMe) => void = (settings) => {
192
+ this.change({settings: settings.pairs});
193
+ };
194
+
195
+ serialize: () => $FlowFixMe = () => {
196
+ return EditorJsonify.serialize.call(this);
197
+ };
198
+ }
199
+
200
+ export default IframeEditor;