@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.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +26 -0
- package/dist/es/index.css +1020 -0
- package/dist/es/index.js +9 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +1020 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +6 -1
- package/package.json +16 -4
- package/src/README.md +7 -0
- package/src/__stories__/editor.stories.js +33 -0
- package/src/__stories__/section-control-button.stories.jsx +27 -0
- package/src/__testdata__/input-number_testdata.js +28 -0
- package/src/__tests__/editor_test.jsx +123 -0
- package/src/__tests__/i18n_test.jsx +585 -0
- package/src/__tests__/traversal_test.jsx +378 -0
- package/src/all-editors.js +85 -0
- package/src/all-editors_test.js +17 -0
- package/src/all-widgets.js +10 -0
- package/src/article-editor.jsx +437 -0
- package/src/components/__stories__/blur-input.stories.jsx +24 -0
- package/src/components/blur-input.jsx +62 -0
- package/src/components/checkbox.jsx +194 -0
- package/src/components/device-framer.jsx +91 -0
- package/src/components/drag-target.jsx +88 -0
- package/src/components/dropdown-option.jsx +318 -0
- package/src/components/form-wrapped-text-field.jsx +248 -0
- package/src/components/graph-settings.jsx +631 -0
- package/src/components/hover-behavior.jsx +178 -0
- package/src/components/json-editor.jsx +121 -0
- package/src/components/link.jsx +173 -0
- package/src/components/simple-button.jsx +86 -0
- package/src/components/sortable.jsx +186 -0
- package/src/components/util.js +39 -0
- package/src/components/viewport-resizer.jsx +73 -0
- package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
- package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
- package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
- package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
- package/src/diffs/__tests__/split-diff_test.js +34 -0
- package/src/diffs/__tests__/string-array-diff_test.js +74 -0
- package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
- package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
- package/src/diffs/article-diff.jsx +70 -0
- package/src/diffs/item-diff.jsx +77 -0
- package/src/diffs/renderer-diff.jsx +143 -0
- package/src/diffs/split-diff.jsx +30 -0
- package/src/diffs/string-array-diff.jsx +75 -0
- package/src/diffs/structured-item-diff.jsx +269 -0
- package/src/diffs/tags-diff.jsx +50 -0
- package/src/diffs/text-diff.jsx +194 -0
- package/src/diffs/widget-diff-performer.jsx +74 -0
- package/src/diffs/widget-diff.jsx +291 -0
- package/src/editor-page.jsx +351 -0
- package/src/editor.jsx +1364 -0
- package/src/hint-editor.jsx +447 -0
- package/src/i18n.jsx +139 -0
- package/src/iframe-content-renderer.jsx +201 -0
- package/src/index.js +27 -8
- package/src/item-editor.jsx +159 -0
- package/src/item-extras-editor.jsx +150 -0
- package/src/katex-error-view.jsx +87 -0
- package/src/multirenderer-editor.jsx +1074 -0
- package/src/section-control-button.jsx +53 -0
- package/src/stateful-article-editor.jsx +176 -0
- package/src/stateful-editor-page.jsx +90 -0
- package/src/styles/perseus-editor.less +1339 -0
- package/src/testing-widgets.js +12 -0
- package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
- package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
- package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
- package/src/widgets/categorizer-editor.jsx +103 -0
- package/src/widgets/cs-program-editor.jsx +258 -0
- package/src/widgets/definition-editor.jsx +75 -0
- package/src/widgets/dropdown-editor.jsx +201 -0
- package/src/widgets/example-graphie-widget-editor.jsx +70 -0
- package/src/widgets/example-graphie-widget.jsx +175 -0
- package/src/widgets/example-widget-editor.jsx +60 -0
- package/src/widgets/example-widget.jsx +153 -0
- package/src/widgets/explanation-editor.jsx +89 -0
- package/src/widgets/expression-editor.jsx +560 -0
- package/src/widgets/graded-group-editor.jsx +162 -0
- package/src/widgets/graded-group-set-editor.jsx +101 -0
- package/src/widgets/grapher-editor.jsx +154 -0
- package/src/widgets/group-editor.jsx +83 -0
- package/src/widgets/iframe-editor.jsx +200 -0
- package/src/widgets/image-editor.jsx +326 -0
- package/src/widgets/input-number-editor.jsx +269 -0
- package/src/widgets/interaction/constraint-editor.jsx +147 -0
- package/src/widgets/interaction-editor.jsx +1389 -0
- package/src/widgets/interactive-graph-editor.jsx +322 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
- package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
- package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
- package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
- package/src/widgets/label-image/answer-choices.jsx +231 -0
- package/src/widgets/label-image/behavior.jsx +92 -0
- package/src/widgets/label-image/marker.jsx +292 -0
- package/src/widgets/label-image/question-markers.jsx +181 -0
- package/src/widgets/label-image/select-image.jsx +86 -0
- package/src/widgets/label-image-editor.jsx +257 -0
- package/src/widgets/lights-puzzle-editor.jsx +267 -0
- package/src/widgets/matcher-editor.jsx +147 -0
- package/src/widgets/matrix-editor.jsx +155 -0
- package/src/widgets/measurer-editor.jsx +245 -0
- package/src/widgets/molecule-editor.jsx +63 -0
- package/src/widgets/number-line-editor.jsx +498 -0
- package/src/widgets/numeric-input-editor.jsx +554 -0
- package/src/widgets/orderer-editor.jsx +166 -0
- package/src/widgets/passage-editor.jsx +115 -0
- package/src/widgets/passage-ref-editor.jsx +77 -0
- package/src/widgets/passage-ref-target-editor.jsx +47 -0
- package/src/widgets/plotter-editor.jsx +537 -0
- package/src/widgets/radio/editor.jsx +398 -0
- package/src/widgets/reaction-diagram-editor.jsx +127 -0
- package/src/widgets/sequence-editor.jsx +205 -0
- package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
- package/src/widgets/simple-markdown-tester.jsx +91 -0
- package/src/widgets/simulator-editor.jsx +144 -0
- package/src/widgets/sorter-editor.jsx +100 -0
- package/src/widgets/table-editor.jsx +164 -0
- package/src/widgets/transformer-editor.jsx +1006 -0
- package/src/widgets/unit-editor.jsx +293 -0
- package/src/widgets/video-editor.jsx +74 -0
- package/src/__tests__/index.test.js +0 -15
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Controlled component to display question image and create markers to
|
|
4
|
+
* label image with answers from list of choices.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Util, Dependencies, globalStyles} from "@khanacademy/perseus";
|
|
8
|
+
import {StyleSheet, css} from "aphrodite";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
|
|
11
|
+
import Marker from "./marker.jsx";
|
|
12
|
+
|
|
13
|
+
import type {MarkerType} from "@khanacademy/perseus";
|
|
14
|
+
|
|
15
|
+
const {colors, typography} = globalStyles;
|
|
16
|
+
|
|
17
|
+
type QuestionMarkersProps = {|
|
|
18
|
+
// The list of possible answers in a specific order.
|
|
19
|
+
choices: $ReadOnlyArray<string>,
|
|
20
|
+
|
|
21
|
+
// The question image properties.
|
|
22
|
+
imageUrl: string,
|
|
23
|
+
imageWidth: number,
|
|
24
|
+
imageHeight: number,
|
|
25
|
+
|
|
26
|
+
// The list of markers placed on the question image.
|
|
27
|
+
markers: $ReadOnlyArray<MarkerType>,
|
|
28
|
+
|
|
29
|
+
// Callback for when any of markers change.
|
|
30
|
+
onChange: (markers: $ReadOnlyArray<MarkerType>) => void,
|
|
31
|
+
|};
|
|
32
|
+
|
|
33
|
+
export default class QuestionMarkers extends React.Component<QuestionMarkersProps> {
|
|
34
|
+
// eslint-disable-next-line flowtype/no-mutable-array
|
|
35
|
+
_markers: Array<?Marker> = [];
|
|
36
|
+
|
|
37
|
+
openDropdownForMarkerIndices(indices: $ReadOnlyArray<number>) {
|
|
38
|
+
// Open answer selection dropdown for each of the specified markers.
|
|
39
|
+
indices.forEach((index) => {
|
|
40
|
+
if (this._markers[index]) {
|
|
41
|
+
this._markers[index].openDropdown();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
handleImageDoubleClick: (e: SyntheticMouseEvent<>) => void = (
|
|
47
|
+
e: SyntheticMouseEvent<>,
|
|
48
|
+
) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
|
|
51
|
+
const rect =
|
|
52
|
+
((e.currentTarget: any): HTMLElement).getBoundingClientRect();
|
|
53
|
+
|
|
54
|
+
// Calculate marker position on the question image as percentage offset
|
|
55
|
+
// rounded to one decimal place.
|
|
56
|
+
const x =
|
|
57
|
+
Math.round(((e.clientX - rect.left) / rect.width) * 1000) / 10;
|
|
58
|
+
const y =
|
|
59
|
+
Math.round(((e.clientY - rect.top) / rect.height) * 1000) / 10;
|
|
60
|
+
|
|
61
|
+
const {markers, onChange} = this.props;
|
|
62
|
+
|
|
63
|
+
// Add the new marker to the end of the list.
|
|
64
|
+
onChange([
|
|
65
|
+
...markers,
|
|
66
|
+
{
|
|
67
|
+
answers: [],
|
|
68
|
+
label: "",
|
|
69
|
+
x,
|
|
70
|
+
y,
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
render(): React.Element<"div"> {
|
|
76
|
+
const {choices, imageUrl, imageWidth, imageHeight, markers, onChange} =
|
|
77
|
+
this.props;
|
|
78
|
+
|
|
79
|
+
const staticUrl = Dependencies.getDependencies().staticUrl;
|
|
80
|
+
|
|
81
|
+
// TODO(michaelpolyak): If a Graphie image is specified, only its SVG
|
|
82
|
+
// layer will be rendered, as when rendering the full Graphie its
|
|
83
|
+
// dynamic content may overflow the widget editor, CP-253
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
<div className={css(styles.title)}>Markers</div>
|
|
87
|
+
|
|
88
|
+
<div className={css(styles.subtitle)}>
|
|
89
|
+
{imageUrl ? (
|
|
90
|
+
<span>
|
|
91
|
+
Double-click on the image to add a marker.
|
|
92
|
+
<br />
|
|
93
|
+
<br />
|
|
94
|
+
Markers are read by screen readers in the order that
|
|
95
|
+
you add them here, so add in a logical order for the
|
|
96
|
+
learner (e.g. sequentially, clockwise). You can test
|
|
97
|
+
order by using keyboard tabbing.
|
|
98
|
+
</span>
|
|
99
|
+
) : (
|
|
100
|
+
"Upload an image to place markers."
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{imageUrl && (
|
|
105
|
+
<div
|
|
106
|
+
className={css(styles.markersCanvas)}
|
|
107
|
+
style={{
|
|
108
|
+
maxWidth: imageWidth,
|
|
109
|
+
maxHeight: imageHeight,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<img
|
|
113
|
+
alt=""
|
|
114
|
+
className={css(styles.image)}
|
|
115
|
+
src={staticUrl(Util.getRealImageUrl(imageUrl))}
|
|
116
|
+
onDoubleClick={this.handleImageDoubleClick}
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
{markers.map((marker, index) => (
|
|
120
|
+
<Marker
|
|
121
|
+
{...marker}
|
|
122
|
+
choices={choices}
|
|
123
|
+
key={`${marker.x}.${marker.y}`}
|
|
124
|
+
// Update marker in the list.
|
|
125
|
+
onChange={(marker) =>
|
|
126
|
+
onChange([
|
|
127
|
+
...markers.slice(0, index),
|
|
128
|
+
marker,
|
|
129
|
+
...markers.slice(index + 1),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
// Remove marker from the list.
|
|
133
|
+
onRemove={() =>
|
|
134
|
+
onChange([
|
|
135
|
+
...markers.slice(0, index),
|
|
136
|
+
...markers.slice(index + 1),
|
|
137
|
+
])
|
|
138
|
+
}
|
|
139
|
+
ref={(node) => (this._markers[index] = node)}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const styles = StyleSheet.create({
|
|
150
|
+
title: {
|
|
151
|
+
...typography.bodyXsmallBold,
|
|
152
|
+
|
|
153
|
+
marginBottom: 6,
|
|
154
|
+
|
|
155
|
+
color: colors.gray17,
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
subtitle: {
|
|
159
|
+
...typography.labelSmall,
|
|
160
|
+
|
|
161
|
+
marginBottom: 12,
|
|
162
|
+
|
|
163
|
+
color: colors.gray68,
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
markersCanvas: {
|
|
167
|
+
position: "relative",
|
|
168
|
+
|
|
169
|
+
border: "solid 1px rgba(33, 36, 44, 0.16)",
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
image: {
|
|
173
|
+
// This makes `markersCanvas` have the same height as the image,
|
|
174
|
+
// enabling to place markers precisely as they're positioned using
|
|
175
|
+
// percentage offsets.
|
|
176
|
+
display: "block",
|
|
177
|
+
|
|
178
|
+
// Contain question image to the widget editor.
|
|
179
|
+
maxWidth: "100%",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Controlled component for selecting the question image, for answer labeling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {globalStyles} from "@khanacademy/perseus";
|
|
7
|
+
import Button from "@khanacademy/wonder-blocks-button";
|
|
8
|
+
import {StyleSheet, css} from "aphrodite";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
|
|
11
|
+
import FormWrappedTextField from "../../components/form-wrapped-text-field.jsx";
|
|
12
|
+
|
|
13
|
+
const {colors, typography} = globalStyles;
|
|
14
|
+
|
|
15
|
+
type SelectImageProps = {
|
|
16
|
+
// Callback for when image URL is changed.
|
|
17
|
+
onChange: (url: string) => void,
|
|
18
|
+
// The selected image URL.
|
|
19
|
+
url: string,
|
|
20
|
+
...
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SelectImage = ({
|
|
24
|
+
onChange,
|
|
25
|
+
url,
|
|
26
|
+
}: SelectImageProps): React.Element<"div"> => (
|
|
27
|
+
<div>
|
|
28
|
+
<div className={css(styles.title)}>Image</div>
|
|
29
|
+
|
|
30
|
+
<div className={css(styles.components)}>
|
|
31
|
+
<FormWrappedTextField
|
|
32
|
+
placeholder="URL"
|
|
33
|
+
grow={1}
|
|
34
|
+
onChange={(e) => onChange(e.target.value)}
|
|
35
|
+
value={url}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<div className={css(styles.spacer)} />
|
|
39
|
+
|
|
40
|
+
<Button
|
|
41
|
+
// TODO(michaelpolyak): Design calls for uploading an image from
|
|
42
|
+
// the widget. This functionality already exists in
|
|
43
|
+
// `perseus-one-package/item-controls.jsx`, it's not obvious if
|
|
44
|
+
// it can be triggered from within the widget, or requires
|
|
45
|
+
// a similar implementation locally, in which case consider
|
|
46
|
+
// refactoring `perseus-admin-package/image-upload-dialog.jsx`
|
|
47
|
+
// as common utility, CP-118
|
|
48
|
+
disabled={!url}
|
|
49
|
+
aria-label={
|
|
50
|
+
url
|
|
51
|
+
? ""
|
|
52
|
+
: "Not implemented. Use the 'Add Image' button in " +
|
|
53
|
+
"the editor to upload image, then copy the URL here."
|
|
54
|
+
}
|
|
55
|
+
onClick={() => onChange("")}
|
|
56
|
+
style={styles.btn}
|
|
57
|
+
>
|
|
58
|
+
{url ? "Remove" : "Upload"}
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const styles = StyleSheet.create({
|
|
65
|
+
title: {
|
|
66
|
+
...typography.bodyXsmallBold,
|
|
67
|
+
|
|
68
|
+
marginBottom: 6,
|
|
69
|
+
|
|
70
|
+
color: colors.gray17,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
components: {
|
|
74
|
+
display: "flex",
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
spacer: {
|
|
78
|
+
width: 16,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
btn: {
|
|
82
|
+
minWidth: 90,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export default SelectImage;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Direct image labeling widget editor.
|
|
4
|
+
*
|
|
5
|
+
* Label on image widget enables creating more natural, conceptual questions
|
|
6
|
+
* that involve the use of images, and enable learners to demonstrate their
|
|
7
|
+
* knowledge by directly interacting with the image.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {EditorJsonify, Util} from "@khanacademy/perseus";
|
|
11
|
+
import {StyleSheet, css} from "aphrodite";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
|
|
14
|
+
import FormWrappedTextField from "../components/form-wrapped-text-field.jsx";
|
|
15
|
+
|
|
16
|
+
import AnswerChoices from "./label-image/answer-choices.jsx";
|
|
17
|
+
import Behavior from "./label-image/behavior.jsx";
|
|
18
|
+
import QuestionMarkers from "./label-image/question-markers.jsx";
|
|
19
|
+
import SelectImage from "./label-image/select-image.jsx";
|
|
20
|
+
|
|
21
|
+
import type {MarkerType} from "@khanacademy/perseus";
|
|
22
|
+
|
|
23
|
+
type LabelImageEditorProps = {|
|
|
24
|
+
// List of answer choices to label question image with.
|
|
25
|
+
choices: $ReadOnlyArray<string>,
|
|
26
|
+
|
|
27
|
+
// The question image properties.
|
|
28
|
+
imageAlt: string,
|
|
29
|
+
imageUrl: string,
|
|
30
|
+
imageWidth: number,
|
|
31
|
+
imageHeight: number,
|
|
32
|
+
|
|
33
|
+
// The list of label markers on the question image.
|
|
34
|
+
markers: $ReadOnlyArray<MarkerType>,
|
|
35
|
+
|
|
36
|
+
// Whether multiple answer choices may be selected for markers.
|
|
37
|
+
multipleAnswers: boolean,
|
|
38
|
+
|
|
39
|
+
// Whether to hide answer choices from user instructions.
|
|
40
|
+
hideChoicesFromInstructions: boolean,
|
|
41
|
+
|
|
42
|
+
// Callback for when a widget prop is changed.
|
|
43
|
+
onChange: (options: Object) => void,
|
|
44
|
+
|};
|
|
45
|
+
|
|
46
|
+
class LabelImageEditor extends React.Component<LabelImageEditorProps> {
|
|
47
|
+
_questionMarkers: ?QuestionMarkers;
|
|
48
|
+
|
|
49
|
+
static defaultProps: {|
|
|
50
|
+
choices: $ReadOnlyArray<any>,
|
|
51
|
+
hideChoicesFromInstructions: boolean,
|
|
52
|
+
imageAlt: string,
|
|
53
|
+
imageHeight: number,
|
|
54
|
+
imageUrl: string,
|
|
55
|
+
imageWidth: number,
|
|
56
|
+
markers: $ReadOnlyArray<any>,
|
|
57
|
+
multipleAnswers: boolean,
|
|
58
|
+
|} = {
|
|
59
|
+
choices: [],
|
|
60
|
+
imageAlt: "",
|
|
61
|
+
imageUrl: "",
|
|
62
|
+
imageWidth: 0,
|
|
63
|
+
imageHeight: 0,
|
|
64
|
+
markers: [],
|
|
65
|
+
multipleAnswers: false,
|
|
66
|
+
hideChoicesFromInstructions: false,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
static widgetName: string = "label-image";
|
|
70
|
+
|
|
71
|
+
componentDidUpdate(prevProps: LabelImageEditorProps) {
|
|
72
|
+
const coordsToMarkers = {};
|
|
73
|
+
|
|
74
|
+
prevProps.markers.forEach(
|
|
75
|
+
(marker) => (coordsToMarkers[`${marker.x}.${marker.y}`] = marker),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Find the newly added marker indices.
|
|
79
|
+
const newIndices = this.props.markers
|
|
80
|
+
.map((marker, index) =>
|
|
81
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
82
|
+
coordsToMarkers.hasOwnProperty(`${marker.x}.${marker.y}`)
|
|
83
|
+
? -1
|
|
84
|
+
: index,
|
|
85
|
+
)
|
|
86
|
+
.filter((index) => index !== -1);
|
|
87
|
+
|
|
88
|
+
// Automatically reveal their dropdowns as a prompt to the content
|
|
89
|
+
// creator to select answers and set the ARIA label.
|
|
90
|
+
if (newIndices.length && this._questionMarkers) {
|
|
91
|
+
this._questionMarkers.openDropdownForMarkerIndices(newIndices);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getSaveWarnings: () => $ReadOnlyArray<any | string> = () => {
|
|
96
|
+
const {choices, imageAlt, imageUrl, markers} = this.props;
|
|
97
|
+
|
|
98
|
+
const warnings = [];
|
|
99
|
+
|
|
100
|
+
if (choices.length < 2) {
|
|
101
|
+
warnings.push("Question requires at least two answer choices");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!imageUrl) {
|
|
105
|
+
warnings.push("Image is not specified for question");
|
|
106
|
+
} else if (!imageAlt) {
|
|
107
|
+
warnings.push("Question image has no alt text");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!markers.length) {
|
|
111
|
+
warnings.push("Question has no markers, to label answers on image");
|
|
112
|
+
} else {
|
|
113
|
+
let numNoAnswers = 0;
|
|
114
|
+
let numNoLabels = 0;
|
|
115
|
+
|
|
116
|
+
for (const marker of markers) {
|
|
117
|
+
if (!marker.answers.length) {
|
|
118
|
+
numNoAnswers++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!marker.label) {
|
|
122
|
+
numNoLabels++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (numNoAnswers) {
|
|
127
|
+
warnings.push(
|
|
128
|
+
`Question has ${numNoAnswers} markers with no ` +
|
|
129
|
+
"answers selected",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (numNoLabels) {
|
|
134
|
+
warnings.push(
|
|
135
|
+
`Question has ${numNoLabels} markers with no ` +
|
|
136
|
+
"ARIA label",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return warnings;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
serialize(): any {
|
|
145
|
+
return EditorJsonify.serialize.call(this);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
handleImageChange: (url: string) => void = (url: string) => {
|
|
149
|
+
this.props.onChange({
|
|
150
|
+
imageUrl: url,
|
|
151
|
+
// Initially reset image size when URL is changed so it can be later
|
|
152
|
+
// measured.
|
|
153
|
+
imageWidth: 0,
|
|
154
|
+
imageHeight: 0,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (url) {
|
|
158
|
+
Util.getImageSize(url, (width, height) => {
|
|
159
|
+
this.props.onChange({
|
|
160
|
+
imageWidth: width,
|
|
161
|
+
imageHeight: height,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
handleAltChange: (alt: string) => void = (alt: string) => {
|
|
168
|
+
this.props.onChange({imageAlt: alt});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
handleChoicesChange: (choices: $ReadOnlyArray<string>) => void = (
|
|
172
|
+
choices: $ReadOnlyArray<string>,
|
|
173
|
+
) => {
|
|
174
|
+
this.props.onChange({choices});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
handleMarkersChange: (markers: $ReadOnlyArray<MarkerType>) => void = (
|
|
178
|
+
markers: $ReadOnlyArray<MarkerType>,
|
|
179
|
+
) => {
|
|
180
|
+
this.props.onChange({markers});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
handleBehaviorChange: (options: any) => void = (options: Object) => {
|
|
184
|
+
this.props.onChange(options);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
render(): React.Element<"div"> {
|
|
188
|
+
const {
|
|
189
|
+
choices,
|
|
190
|
+
imageAlt,
|
|
191
|
+
imageUrl,
|
|
192
|
+
imageWidth,
|
|
193
|
+
imageHeight,
|
|
194
|
+
markers,
|
|
195
|
+
multipleAnswers,
|
|
196
|
+
hideChoicesFromInstructions,
|
|
197
|
+
} = this.props;
|
|
198
|
+
|
|
199
|
+
const imageSelected = imageUrl && imageWidth > 0 && imageHeight > 0;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
<SelectImage onChange={this.handleImageChange} url={imageUrl} />
|
|
204
|
+
|
|
205
|
+
<div className={css(styles.smallSpacer)} />
|
|
206
|
+
|
|
207
|
+
{imageSelected && (
|
|
208
|
+
<FormWrappedTextField
|
|
209
|
+
placeholder="Alt text (for screen readers)"
|
|
210
|
+
onChange={(e) => this.handleAltChange(e.target.value)}
|
|
211
|
+
value={imageAlt}
|
|
212
|
+
width="100%"
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
<div className={css(styles.largeSpacer)} />
|
|
217
|
+
|
|
218
|
+
<QuestionMarkers
|
|
219
|
+
choices={choices}
|
|
220
|
+
imageUrl={imageSelected ? imageUrl : ""}
|
|
221
|
+
imageWidth={imageWidth}
|
|
222
|
+
imageHeight={imageHeight}
|
|
223
|
+
markers={markers}
|
|
224
|
+
onChange={this.handleMarkersChange}
|
|
225
|
+
ref={(node) => (this._questionMarkers = node)}
|
|
226
|
+
/>
|
|
227
|
+
|
|
228
|
+
<div className={css(styles.largeSpacer)} />
|
|
229
|
+
|
|
230
|
+
<AnswerChoices
|
|
231
|
+
choices={choices}
|
|
232
|
+
onChange={this.handleChoicesChange}
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
<div className={css(styles.largeSpacer)} />
|
|
236
|
+
|
|
237
|
+
<Behavior
|
|
238
|
+
multipleAnswers={multipleAnswers}
|
|
239
|
+
hideChoicesFromInstructions={hideChoicesFromInstructions}
|
|
240
|
+
onChange={this.handleBehaviorChange}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const styles = StyleSheet.create({
|
|
248
|
+
largeSpacer: {
|
|
249
|
+
height: 32,
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
smallSpacer: {
|
|
253
|
+
height: 16,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export default LabelImageEditor;
|