@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,447 @@
1
+ /* eslint-disable @babel/no-invalid-this */
2
+ // @flow
3
+ /**
4
+ * Collection of classes for rendering the hint editor area,
5
+ * hint editor boxes, and hint previews
6
+ */
7
+ import {components, icons} from "@khanacademy/perseus";
8
+ import * as React from "react";
9
+ import _ from "underscore";
10
+
11
+ import DeviceFramer from "./components/device-framer.jsx";
12
+ import Editor from "./editor.jsx";
13
+ import IframeContentRenderer from "./iframe-content-renderer.jsx";
14
+
15
+ import type {
16
+ APIOptions,
17
+ WidgetDict,
18
+ ImageDict,
19
+ Hint,
20
+ ChangeHandler,
21
+ DeviceType,
22
+ } from "@khanacademy/perseus";
23
+
24
+ const {InfoTip, InlineIcon} = components;
25
+ const {iconCircleArrowDown, iconCircleArrowUp, iconPlus, iconTrash} = icons;
26
+
27
+ type ImageUploader = (file: string, callback: (url: string) => mixed) => mixed;
28
+
29
+ type HintEditorProps = {|
30
+ itemId?: string,
31
+ apiOptions?: APIOptions,
32
+ className: string,
33
+ imageUploader?: ImageUploader,
34
+ showMoveButtons?: boolean,
35
+ showRemoveButton?: boolean,
36
+ showTitle?: boolean,
37
+ content?: ?string,
38
+ replace?: ?boolean,
39
+ widgets?: ?WidgetDict,
40
+ images?: ?ImageDict,
41
+ isLast: boolean,
42
+ isFirst: boolean,
43
+ onMove: (direction: number) => mixed,
44
+ onRemove: () => mixed,
45
+ onChange: ChangeHandler,
46
+ __type?: "hint",
47
+ |};
48
+
49
+ /* Renders a hint editor box
50
+ *
51
+ * This includes:
52
+ * ~ A "Hint" title
53
+ * ~ the textarea for the hint
54
+ * ~ the "remove this hint" box
55
+ * ~ the move hint up/down arrows
56
+ */
57
+ export class HintEditor extends React.Component<HintEditorProps> {
58
+ static defaultProps: {|
59
+ className: string,
60
+ content: string,
61
+ replace: boolean,
62
+ showMoveButtons: boolean,
63
+ showRemoveButton: boolean,
64
+ showTitle: boolean,
65
+ |} = {
66
+ className: "",
67
+ content: "",
68
+ replace: false,
69
+ showMoveButtons: true,
70
+ showTitle: true,
71
+ showRemoveButton: true,
72
+ };
73
+
74
+ handleChange: (e: SyntheticInputEvent<>) => void = (
75
+ e: SyntheticInputEvent<>,
76
+ ) => {
77
+ this.props.onChange({replace: e.target.checked});
78
+ };
79
+
80
+ focus: () => void = () => {
81
+ // eslint-disable-next-line react/no-string-refs
82
+ this.refs.editor.focus();
83
+ };
84
+
85
+ getSaveWarnings: () => any = () => {
86
+ // eslint-disable-next-line react/no-string-refs
87
+ return this.refs.editor.getSaveWarnings();
88
+ };
89
+
90
+ serialize: (options: any) => any = (options: $FlowFixMe) => {
91
+ // eslint-disable-next-line react/no-string-refs
92
+ return this.refs.editor.serialize(options);
93
+ };
94
+
95
+ render(): React.Element<"div"> {
96
+ return (
97
+ <div className={"perseus-hint-editor " + this.props.className}>
98
+ {this.props.showTitle && <div className="pod-title">Hint</div>}
99
+ <Editor
100
+ // eslint-disable-next-line react/no-string-refs
101
+ ref="editor"
102
+ // Using the AssessmentItem content ID as the key
103
+ // ensures that when the user navigates to another
104
+ // item in the Sidebar, the question editor is
105
+ // re-rendered by React.
106
+ key={this.props.itemId}
107
+ apiOptions={this.props.apiOptions}
108
+ widgets={this.props.widgets || undefined}
109
+ content={this.props.content || undefined}
110
+ images={this.props.images}
111
+ replace={this.props.replace}
112
+ placeholder="Type your hint here..."
113
+ imageUploader={this.props.imageUploader}
114
+ onChange={this.props.onChange}
115
+ />
116
+ <div className="hint-controls-container clearfix">
117
+ {this.props.showMoveButtons && (
118
+ <span className="reorder-hints">
119
+ <button
120
+ type="button"
121
+ className={this.props.isLast ? "hidden" : ""}
122
+ onClick={_.partial(this.props.onMove, 1)}
123
+ >
124
+ <InlineIcon {...iconCircleArrowDown} />
125
+ </button>{" "}
126
+ <button
127
+ type="button"
128
+ className={this.props.isFirst ? "hidden" : ""}
129
+ onClick={_.partial(this.props.onMove, -1)}
130
+ >
131
+ <InlineIcon {...iconCircleArrowUp} />
132
+ </button>{" "}
133
+ {this.props.isLast && (
134
+ <InfoTip>
135
+ <p>
136
+ The last hint is automatically bolded.
137
+ </p>
138
+ </InfoTip>
139
+ )}
140
+ </span>
141
+ )}
142
+ <input
143
+ type="checkbox"
144
+ checked={this.props.replace}
145
+ onChange={this.handleChange}
146
+ />
147
+ Replace previous hint
148
+ {this.props.showRemoveButton && (
149
+ <button
150
+ type="button"
151
+ className="remove-hint simple-button orange"
152
+ onClick={this.props.onRemove}
153
+ >
154
+ <InlineIcon {...iconTrash} />
155
+ Remove this hint{" "}
156
+ </button>
157
+ )}
158
+ </div>
159
+ </div>
160
+ );
161
+ }
162
+ }
163
+
164
+ type CombinedHintEditorProps = {|
165
+ itemId?: string,
166
+ apiOptions?: APIOptions,
167
+ deviceType: DeviceType,
168
+ frameSource: string,
169
+ imageUploader?: ImageUploader,
170
+ highlightLint?: boolean,
171
+ isLast: boolean,
172
+ isFirst: boolean,
173
+ hint: Hint,
174
+ pos: number, // position
175
+ contentPaths: $ReadOnlyArray<string>,
176
+ onMove: (direction: number) => mixed,
177
+ onRemove: () => mixed,
178
+ onChange: ChangeHandler,
179
+ |};
180
+
181
+ /* A single hint-row containing a hint editor and preview */
182
+ class CombinedHintEditor extends React.Component<CombinedHintEditorProps> {
183
+ static defaultProps = {
184
+ highlightLint: false,
185
+ };
186
+
187
+ componentDidMount() {
188
+ this.updatePreview();
189
+ }
190
+
191
+ componentDidUpdate() {
192
+ this.updatePreview();
193
+ }
194
+
195
+ updatePreview = () => {
196
+ const shouldBold =
197
+ this.props.isLast && !/\*\*/.test(this.props.hint.content);
198
+
199
+ // eslint-disable-next-line react/no-string-refs
200
+ this.refs.frame.sendNewData({
201
+ type: "hint",
202
+ data: {
203
+ hint: this.props.hint,
204
+ bold: shouldBold,
205
+ pos: this.props.pos,
206
+ apiOptions: this.props.apiOptions,
207
+ linterContext: {
208
+ contentType: "hint",
209
+ highlightLint: this.props.highlightLint,
210
+ paths: this.props.contentPaths,
211
+ },
212
+ },
213
+ });
214
+ };
215
+
216
+ getSaveWarnings = () => {
217
+ // eslint-disable-next-line react/no-string-refs
218
+ return this.refs.editor.getSaveWarnings();
219
+ };
220
+
221
+ serialize = (options: $FlowFixMe) => {
222
+ // eslint-disable-next-line react/no-string-refs
223
+ return this.refs.editor.serialize(options);
224
+ };
225
+
226
+ focus = () => {
227
+ // eslint-disable-next-line react/no-string-refs
228
+ this.refs.editor.focus();
229
+ };
230
+
231
+ render(): React.Node {
232
+ const isMobile =
233
+ this.props.deviceType === "phone" ||
234
+ this.props.deviceType === "tablet";
235
+ return (
236
+ <div
237
+ className={
238
+ "perseus-combined-hint-editor " + "perseus-editor-row"
239
+ }
240
+ >
241
+ <div className="perseus-editor-left-cell">
242
+ <HintEditor
243
+ // eslint-disable-next-line react/no-string-refs
244
+ ref="editor"
245
+ itemId={this.props.itemId}
246
+ isFirst={this.props.isFirst}
247
+ isLast={this.props.isLast}
248
+ widgets={this.props.hint.widgets}
249
+ content={this.props.hint.content}
250
+ images={this.props.hint.images}
251
+ replace={this.props.hint.replace}
252
+ imageUploader={this.props.imageUploader}
253
+ onChange={this.props.onChange}
254
+ onRemove={this.props.onRemove}
255
+ onMove={this.props.onMove}
256
+ apiOptions={this.props.apiOptions}
257
+ />
258
+ </div>
259
+ <div className="perseus-editor-right-cell">
260
+ <DeviceFramer
261
+ deviceType={this.props.deviceType}
262
+ nochrome={true}
263
+ >
264
+ <IframeContentRenderer
265
+ // eslint-disable-next-line react/no-string-refs
266
+ ref="frame"
267
+ content={this.props.frameSource}
268
+ datasetKey="mobile"
269
+ datasetValue={isMobile}
270
+ seamless={true}
271
+ />
272
+ </DeviceFramer>
273
+ </div>
274
+ </div>
275
+ );
276
+ }
277
+ }
278
+
279
+ type CombinedHintsEditorProps = {|
280
+ apiOptions?: APIOptions,
281
+ deviceType: DeviceType,
282
+ frameSource: string,
283
+ imageUploader?: ImageUploader,
284
+ highlightLint?: boolean,
285
+ hints: $ReadOnlyArray<Hint>,
286
+ onChange: ChangeHandler,
287
+
288
+ // The content ID of the AssessmentItem being edited. It may not be set
289
+ // for non-content library exercise questions.
290
+ itemId?: string,
291
+ |};
292
+
293
+ /* The entire hints editing/preview area
294
+ *
295
+ * Includes:
296
+ * ~ All the hint edit boxes, move and remove buttons
297
+ * ~ All the hint previews
298
+ * ~ The "add a hint" button
299
+ */
300
+ class CombinedHintsEditor extends React.Component<CombinedHintsEditorProps> {
301
+ static HintEditor: typeof HintEditor = HintEditor;
302
+
303
+ static defaultProps: {|
304
+ highlightLint: boolean,
305
+ hints: $ReadOnlyArray<any>,
306
+ onChange: () => void,
307
+ |} = {
308
+ onChange: () => {},
309
+ hints: [],
310
+ highlightLint: false,
311
+ };
312
+
313
+ handleHintChange: (
314
+ i: number,
315
+ newProps: CombinedHintsEditorProps,
316
+ cb: () => mixed,
317
+ silent: boolean,
318
+ ) => void = (
319
+ i: number,
320
+ newProps: CombinedHintsEditorProps,
321
+ cb: () => mixed,
322
+ silent: boolean,
323
+ ) => {
324
+ // TODO(joel) - lens
325
+ const hints = _(this.props.hints).clone();
326
+ hints[i] = _.extend(
327
+ {},
328
+ this.serializeHint(i, {keepDeletedWidgets: true}),
329
+ newProps,
330
+ );
331
+
332
+ this.props.onChange({hints: hints}, cb, silent);
333
+ };
334
+
335
+ handleHintRemove: (i: number) => void = (i: number) => {
336
+ const hints = _(this.props.hints).clone();
337
+ hints.splice(i, 1);
338
+ this.props.onChange({hints: hints});
339
+ };
340
+
341
+ handleHintMove: (i: number, dir: number) => void = (
342
+ i: number,
343
+ dir: number,
344
+ ) => {
345
+ const hints = _(this.props.hints).clone();
346
+ const hint = hints.splice(i, 1)[0];
347
+ hints.splice(i + dir, 0, hint);
348
+ this.props.onChange({hints: hints}, () => {
349
+ // eslint-disable-next-line react/no-string-refs
350
+ this.refs["hintEditor" + (i + dir)].focus();
351
+ });
352
+ };
353
+
354
+ addHint: () => void = () => {
355
+ const hints = _(this.props.hints)
356
+ .clone()
357
+ .concat([{content: ""}]);
358
+ this.props.onChange({hints: hints}, () => {
359
+ const i = hints.length - 1;
360
+ // eslint-disable-next-line react/no-string-refs
361
+ this.refs["hintEditor" + i].focus();
362
+ });
363
+ };
364
+
365
+ getSaveWarnings: () => any = () => {
366
+ return _.chain(this.props.hints)
367
+ .map((hint, i) => {
368
+ return _.map(
369
+ // eslint-disable-next-line react/no-string-refs
370
+ this.refs["hintEditor" + i].getSaveWarnings(),
371
+ (issue) => "Hint " + (i + 1) + ": " + issue,
372
+ );
373
+ })
374
+ .flatten(true)
375
+ .value();
376
+ };
377
+
378
+ serialize: (options: any) => $ReadOnlyArray<string> = (
379
+ options: $FlowFixMe,
380
+ ) => {
381
+ return this.props.hints.map((hint, i) => {
382
+ return this.serializeHint(i, options);
383
+ });
384
+ };
385
+
386
+ serializeHint: (index: number, options: any) => string = (
387
+ index: number,
388
+ options: $FlowFixMe,
389
+ ): string => {
390
+ // eslint-disable-next-line react/no-string-refs
391
+ return this.refs["hintEditor" + index].serialize(options);
392
+ };
393
+
394
+ render(): React.Element<"div"> {
395
+ const {itemId, hints} = this.props;
396
+ const hintElems = _.map(
397
+ hints,
398
+ function (hint, i) {
399
+ return (
400
+ <CombinedHintEditor
401
+ ref={"hintEditor" + i}
402
+ key={"hintEditor" + i}
403
+ isFirst={i === 0}
404
+ isLast={i + 1 === hints.length}
405
+ itemId={itemId}
406
+ hint={hint}
407
+ pos={i}
408
+ imageUploader={this.props.imageUploader}
409
+ // eslint-disable-next-line react/jsx-no-bind
410
+ onChange={this.handleHintChange.bind(this, i)}
411
+ // eslint-disable-next-line react/jsx-no-bind
412
+ onRemove={this.handleHintRemove.bind(this, i)}
413
+ // eslint-disable-next-line react/jsx-no-bind
414
+ onMove={this.handleHintMove.bind(this, i)}
415
+ deviceType={this.props.deviceType}
416
+ apiOptions={this.props.apiOptions}
417
+ frameSource={this.props.frameSource}
418
+ highlightLint={this.props.highlightLint}
419
+ // TODO(CP-4838): what should be passed here?
420
+ contentPaths={[]}
421
+ />
422
+ );
423
+ },
424
+ this,
425
+ );
426
+
427
+ return (
428
+ <div className="perseus-hints-editor perseus-editor-table">
429
+ {hintElems}
430
+ <div className="perseus-editor-row">
431
+ <div className="add-hint-container perseus-editor-left-cell">
432
+ <button
433
+ type="button"
434
+ className="add-hint simple-button orange"
435
+ onClick={this.addHint}
436
+ >
437
+ <InlineIcon {...iconPlus} /> Add a hint
438
+ </button>
439
+ </div>
440
+ </div>
441
+ </div>
442
+ );
443
+ /* eslint-enable max-len */
444
+ }
445
+ }
446
+
447
+ export default CombinedHintsEditor;
package/src/i18n.jsx ADDED
@@ -0,0 +1,139 @@
1
+ // @flow
2
+ /**
3
+ * Functions for extracting data from items for use in i18n.
4
+ */
5
+ import {traverse, MultiItems, PerseusMarkdown} from "@khanacademy/perseus";
6
+ import _ from "underscore";
7
+
8
+ const {findContentNodesInItem, findHintNodesInItem, inferItemShape} =
9
+ MultiItems;
10
+
11
+ // Takes a renderer content and parses the markdown for images
12
+ function findImagesInContent(content, images) {
13
+ const parsed = PerseusMarkdown.parse(content);
14
+
15
+ PerseusMarkdown.traverseContent(parsed, function (node) {
16
+ if (node.type === "image") {
17
+ images.push(node.target);
18
+ }
19
+ });
20
+ }
21
+
22
+ // Background images in some widgets are annoying to deal with because
23
+ // sometimes the objects aren't full when there isn't an image. So, we do some
24
+ // extra checking to make sure we don't cause an error or push an empty image.
25
+ function handleBackgroundImage(graph, images) {
26
+ if (graph && graph.backgroundImage && graph.backgroundImage.url) {
27
+ images.push(graph.backgroundImage.url);
28
+ }
29
+ }
30
+
31
+ // The callback called for each widget. We check each of the areas of each
32
+ // widget where they contain a renderer for images by calling
33
+ // findImagesInContent. We don't have to recurse through child widgets, because
34
+ // traverseRendererDeep does that for us.
35
+ function widgetCallback(widgetInfo, images) {
36
+ if (!widgetInfo.options) {
37
+ return;
38
+ }
39
+
40
+ // TODO(emily/aria): Move this into the widget files, so we don't have the
41
+ // logic out here.
42
+ if (widgetInfo.type === "categorizer") {
43
+ _.each(widgetInfo.options.items, function (item) {
44
+ findImagesInContent(item, images);
45
+ });
46
+ _.each(widgetInfo.options.categories, function (category) {
47
+ findImagesInContent(category, images);
48
+ });
49
+ } else if (widgetInfo.type === "image") {
50
+ findImagesInContent(widgetInfo.options.title, images);
51
+ findImagesInContent(widgetInfo.options.caption, images);
52
+ } else if (widgetInfo.type === "matcher") {
53
+ _.each(widgetInfo.options.left, function (option) {
54
+ findImagesInContent(option, images);
55
+ });
56
+ _.each(widgetInfo.options.right, function (option) {
57
+ findImagesInContent(option, images);
58
+ });
59
+ _.each(widgetInfo.options.labels, function (label) {
60
+ findImagesInContent(label, images);
61
+ });
62
+ } else if (widgetInfo.type === "matrix") {
63
+ findImagesInContent(widgetInfo.options.prefix, images);
64
+ findImagesInContent(widgetInfo.options.suffix, images);
65
+ } else if (widgetInfo.type === "orderer") {
66
+ _.each(widgetInfo.options.options, function (option) {
67
+ findImagesInContent(option.content, images);
68
+ });
69
+ } else if (widgetInfo.type === "passage") {
70
+ findImagesInContent(widgetInfo.options.passageTitle, images);
71
+ } else if (widgetInfo.type === "radio") {
72
+ _.each(widgetInfo.options.choices, function (choice) {
73
+ findImagesInContent(choice.content, images);
74
+ });
75
+ } else if (widgetInfo.type === "sorter") {
76
+ _.each(widgetInfo.options.correct, function (option) {
77
+ findImagesInContent(option, images);
78
+ });
79
+ } else if (widgetInfo.type === "table") {
80
+ _.each(widgetInfo.options.headers, function (header) {
81
+ findImagesInContent(header, images);
82
+ });
83
+ }
84
+
85
+ if (widgetInfo.type === "grapher") {
86
+ handleBackgroundImage(widgetInfo.options.graph, images);
87
+ } else if (widgetInfo.type === "image") {
88
+ handleBackgroundImage(widgetInfo.options, images);
89
+ } else if (widgetInfo.type === "interactive-graph") {
90
+ handleBackgroundImage(widgetInfo.options, images);
91
+ } else if (widgetInfo.type === "measurer" && widgetInfo.options.image) {
92
+ images.push(widgetInfo.options.image.url);
93
+ } else if (widgetInfo.type === "plotter") {
94
+ images.push(widgetInfo.options.picUrl);
95
+ } else if (widgetInfo.type === "transformer") {
96
+ handleBackgroundImage(widgetInfo.options.graph, images);
97
+ }
98
+ }
99
+
100
+ function findImagesInRenderers(renderers) {
101
+ const images = [];
102
+
103
+ _.each(renderers, (renderer) => {
104
+ traverse(
105
+ renderer,
106
+ (content) => {
107
+ findImagesInContent(content, images);
108
+ },
109
+ (widget) => widgetCallback(widget, images),
110
+ );
111
+ });
112
+
113
+ return images;
114
+ }
115
+
116
+ // Calls findImagesInContent on all of the different content areas for
117
+ // assessment items
118
+ function findImagesInItemData(itemData: $FlowFixMe): $FlowFixMe {
119
+ let renderers = [];
120
+ if (itemData._multi) {
121
+ const shape = inferItemShape(itemData);
122
+ findContentNodesInItem(itemData, shape, (node) => renderers.push(node));
123
+ findHintNodesInItem(itemData, shape, (node) => renderers.push(node));
124
+ } else {
125
+ renderers = [itemData.question, ...itemData.hints];
126
+ }
127
+ return findImagesInRenderers(renderers);
128
+ }
129
+
130
+ // Calls findImagesInContent on all of the different content areas for
131
+ // articles
132
+ function findImagesInArticles(perseusContent: $FlowFixMe): $FlowFixMe {
133
+ return findImagesInRenderers(perseusContent);
134
+ }
135
+
136
+ export default {
137
+ findImagesInArticles: findImagesInArticles,
138
+ findImagesInItemData: findImagesInItemData,
139
+ };