@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,437 @@
1
+ /* eslint-disable jsx-a11y/anchor-is-valid */
2
+ // @flow
3
+
4
+ /**
5
+ * An article editor. Articles are long-form pieces of content, composed of
6
+ * multiple (Renderer) sections concatenated together.
7
+ */
8
+
9
+ import {
10
+ components,
11
+ icons,
12
+ ApiOptions,
13
+ Changeable,
14
+ Dependencies,
15
+ Errors,
16
+ PerseusError,
17
+ } from "@khanacademy/perseus";
18
+ import * as React from "react";
19
+ import _ from "underscore";
20
+
21
+ import DeviceFramer from "./components/device-framer.jsx";
22
+ import JsonEditor from "./components/json-editor.jsx";
23
+ import Editor from "./editor.jsx";
24
+ import IframeContentRenderer from "./iframe-content-renderer.jsx";
25
+ import SectionControlButton from "./section-control-button.jsx";
26
+
27
+ import type {APIOptions} from "@khanacademy/perseus";
28
+
29
+ const {HUD, InlineIcon} = components;
30
+ const {iconCircleArrowDown, iconCircleArrowUp, iconPlus, iconTrash} = icons;
31
+
32
+ type RendererProps = {|
33
+ content?: string,
34
+ widgets?: Object,
35
+ images?: Object,
36
+ |};
37
+
38
+ type JsonType = RendererProps | $ReadOnlyArray<RendererProps>;
39
+ type DefaultProps = {|
40
+ contentPaths?: $ReadOnlyArray<string>,
41
+ json: JsonType,
42
+ mode: "diff" | "edit" | "json" | "preview",
43
+ screen: "phone" | "tablet" | "desktop",
44
+ sectionImageUploadGenerator: (i: number) => React$Element<"span">,
45
+ useNewStyles: boolean,
46
+ |};
47
+ type PerseusArticleEditorProps = {|
48
+ ...DefaultProps,
49
+ apiOptions?: APIOptions,
50
+ frameSource: string,
51
+ imageUploader?: (string, (string) => mixed) => mixed,
52
+ ...Changeable.ChangeableProps,
53
+ |};
54
+
55
+ type State = {|
56
+ highlightLint: boolean,
57
+ |};
58
+ export default class ArticleEditor extends React.Component<
59
+ PerseusArticleEditorProps,
60
+ State,
61
+ > {
62
+ static defaultProps: DefaultProps = {
63
+ contentPaths: [],
64
+ // $FlowFixMe[incompatible-exact]
65
+ json: [{}],
66
+ mode: "edit",
67
+ screen: "desktop",
68
+ sectionImageUploadGenerator: () => <span />,
69
+ useNewStyles: false,
70
+ };
71
+
72
+ state: State = {
73
+ highlightLint: true,
74
+ };
75
+
76
+ componentDidMount() {
77
+ this._updatePreviewFrames();
78
+ }
79
+
80
+ componentDidUpdate() {
81
+ this._updatePreviewFrames();
82
+ }
83
+
84
+ _updatePreviewFrames() {
85
+ if (this.props.mode === "preview") {
86
+ // eslint-disable-next-line react/no-string-refs
87
+ this.refs["frame-all"].sendNewData({
88
+ type: "article-all",
89
+ data: this._sections().map((section, i) => {
90
+ return this._apiOptionsForSection(section, i);
91
+ }),
92
+ });
93
+ } else if (this.props.mode === "edit") {
94
+ this._sections().forEach((section, i) => {
95
+ // eslint-disable-next-line react/no-string-refs
96
+ this.refs["frame-" + i].sendNewData({
97
+ type: "article",
98
+ data: this._apiOptionsForSection(section, i),
99
+ });
100
+ });
101
+ }
102
+ }
103
+
104
+ _apiOptionsForSection(section: RendererProps, sectionIndex: number): any {
105
+ // eslint-disable-next-line react/no-string-refs
106
+ const editor = this.refs[`editor${sectionIndex}`];
107
+ return {
108
+ apiOptions: {
109
+ ...ApiOptions.defaults,
110
+ ...this.props.apiOptions,
111
+
112
+ // Alignment options are always available in article
113
+ // editors
114
+ showAlignmentOptions: true,
115
+ isArticle: true,
116
+ },
117
+ json: section,
118
+ useNewStyles: this.props.useNewStyles,
119
+ linterContext: {
120
+ contentType: "article",
121
+ highlightLint: this.state.highlightLint,
122
+ paths: this.props.contentPaths,
123
+ },
124
+ legacyPerseusLint: editor ? editor.getSaveWarnings() : [],
125
+ };
126
+ }
127
+
128
+ _sections(): $ReadOnlyArray<RendererProps> {
129
+ return Array.isArray(this.props.json)
130
+ ? this.props.json
131
+ : [this.props.json];
132
+ }
133
+
134
+ _renderEditor(): React$Element<"div"> {
135
+ const {imageUploader, sectionImageUploadGenerator} = this.props;
136
+
137
+ const apiOptions = {
138
+ ...ApiOptions.defaults,
139
+ ...this.props.apiOptions,
140
+
141
+ // Alignment options are always available in article editors
142
+ showAlignmentOptions: true,
143
+ isArticle: true,
144
+ };
145
+
146
+ const {KatexProvider} = Dependencies.getDependencies();
147
+
148
+ const sections = this._sections();
149
+
150
+ return (
151
+ <div className="perseus-editor-table">
152
+ {sections.map((section, i) => {
153
+ return [
154
+ <div className="perseus-editor-row" key={i}>
155
+ <div className="perseus-editor-left-cell">
156
+ <div className="pod-title">
157
+ Section {i + 1}
158
+ <div
159
+ style={{
160
+ display: "inline-block",
161
+ float: "right",
162
+ }}
163
+ >
164
+ {sectionImageUploadGenerator(i)}
165
+ <SectionControlButton
166
+ icon={iconPlus}
167
+ onClick={() => {
168
+ this._handleAddSectionAfter(i);
169
+ }}
170
+ title="Add a new section after this one"
171
+ />
172
+ {i + 1 < sections.length && (
173
+ <SectionControlButton
174
+ icon={iconCircleArrowDown}
175
+ onClick={() => {
176
+ this._handleMoveSectionLater(
177
+ i,
178
+ );
179
+ }}
180
+ title="Move this section down"
181
+ />
182
+ )}
183
+ {i > 0 && (
184
+ <SectionControlButton
185
+ icon={iconCircleArrowUp}
186
+ onClick={() => {
187
+ this._handleMoveSectionEarlier(
188
+ i,
189
+ );
190
+ }}
191
+ title="Move this section up"
192
+ />
193
+ )}
194
+ <SectionControlButton
195
+ icon={iconTrash}
196
+ onClick={() => {
197
+ const msg =
198
+ "Are you sure you " +
199
+ "want to delete section " +
200
+ (i + 1) +
201
+ "?";
202
+ /* eslint-disable no-alert */
203
+ if (confirm(msg)) {
204
+ this._handleRemoveSection(
205
+ i,
206
+ );
207
+ }
208
+ /* eslint-enable no-alert */
209
+ }}
210
+ title="Delete this section"
211
+ />
212
+ </div>
213
+ </div>
214
+ <KatexProvider>
215
+ <Editor
216
+ {...section}
217
+ apiOptions={apiOptions}
218
+ imageUploader={imageUploader}
219
+ onChange={_.partial(
220
+ this._handleEditorChange,
221
+ i,
222
+ )}
223
+ placeholder="Type your section text here..."
224
+ ref={"editor" + i}
225
+ />
226
+ </KatexProvider>
227
+ </div>
228
+
229
+ <div className="editor-preview">
230
+ {this._renderIframePreview(i, true)}
231
+ </div>
232
+ </div>,
233
+ ];
234
+ })}
235
+ {this._renderAddSection()}
236
+ {this._renderLinterHUD()}
237
+ </div>
238
+ );
239
+ /* eslint-enable max-len */
240
+ }
241
+
242
+ _renderAddSection(): React$Element<"div"> {
243
+ return (
244
+ <div className="perseus-editor-row">
245
+ <div className="perseus-editor-left-cell">
246
+ <a
247
+ href="#"
248
+ className="simple-button orange"
249
+ onClick={() => {
250
+ this._handleAddSectionAfter(
251
+ this._sections().length - 1,
252
+ );
253
+ }}
254
+ >
255
+ <InlineIcon {...iconPlus} /> Add a section
256
+ </a>
257
+ </div>
258
+ </div>
259
+ );
260
+ }
261
+
262
+ _renderLinterHUD(): React$Element<any> {
263
+ return (
264
+ <HUD
265
+ message="Style warnings"
266
+ enabled={this.state.highlightLint}
267
+ onClick={() => {
268
+ this.setState({
269
+ highlightLint: !this.state.highlightLint,
270
+ });
271
+ }}
272
+ />
273
+ );
274
+ }
275
+
276
+ _renderIframePreview(
277
+ i: number | string,
278
+ nochrome: boolean,
279
+ ): React$Element<any> {
280
+ const isMobile =
281
+ this.props.screen === "phone" || this.props.screen === "tablet";
282
+
283
+ return (
284
+ <DeviceFramer deviceType={this.props.screen} nochrome={nochrome}>
285
+ <IframeContentRenderer
286
+ ref={"frame-" + i}
287
+ key={this.props.screen}
288
+ content={this.props.frameSource}
289
+ datasetKey="mobile"
290
+ datasetValue={isMobile}
291
+ seamless={nochrome}
292
+ />
293
+ </DeviceFramer>
294
+ );
295
+ }
296
+
297
+ _renderPreviewMode(): React$Element<"div"> {
298
+ return (
299
+ <div className="standalone-preview">
300
+ {this._renderIframePreview("all", false)}
301
+ </div>
302
+ );
303
+ }
304
+
305
+ _handleJsonChange: (newJson: JsonType) => void = (newJson) => {
306
+ this.props.onChange({json: newJson});
307
+ };
308
+
309
+ _handleEditorChange: (i: number, newProps: RendererProps) => void = (
310
+ i,
311
+ newProps,
312
+ ) => {
313
+ const sections = _.clone(this._sections());
314
+ sections[i] = _.extend({}, sections[i], newProps);
315
+ this.props.onChange({json: sections});
316
+ };
317
+
318
+ _handleMoveSectionEarlier(i: number) {
319
+ if (i === 0) {
320
+ return;
321
+ }
322
+ const sections = _.clone(this._sections());
323
+ const section = sections[i];
324
+ sections.splice(i, 1);
325
+ sections.splice(i - 1, 0, section);
326
+ this.props.onChange({
327
+ json: sections,
328
+ });
329
+ }
330
+
331
+ _handleMoveSectionLater(i: number) {
332
+ const sections = _.clone(this._sections());
333
+ if (i + 1 === sections.length) {
334
+ return;
335
+ }
336
+ const section = sections[i];
337
+ sections.splice(i, 1);
338
+ sections.splice(i + 1, 0, section);
339
+ this.props.onChange({
340
+ json: sections,
341
+ });
342
+ }
343
+
344
+ _handleAddSectionAfter(i: number) {
345
+ // We do a full serialization here because we
346
+ // might be copying widgets:
347
+ const sections = _.clone(this.serialize());
348
+ // Here we do magic to allow you to copy-paste
349
+ // things from the previous section into the new
350
+ // section while preserving widgets.
351
+ // To enable this, we preserve the widgets
352
+ // object for the new section, but wipe out
353
+ // the content.
354
+ const newSection =
355
+ i >= 0
356
+ ? {
357
+ widgets: sections[i].widgets,
358
+ }
359
+ : {};
360
+ sections.splice(i + 1, 0, newSection);
361
+ this.props.onChange({
362
+ json: sections,
363
+ });
364
+ }
365
+
366
+ _handleRemoveSection(i: number) {
367
+ const sections = _.clone(this._sections());
368
+ sections.splice(i, 1);
369
+ this.props.onChange({
370
+ json: sections,
371
+ });
372
+ }
373
+
374
+ serialize(): JsonType {
375
+ if (this.props.mode === "edit") {
376
+ return this._sections().map((section, i) => {
377
+ // eslint-disable-next-line react/no-string-refs
378
+ return this.refs["editor" + i].serialize();
379
+ });
380
+ }
381
+ if (this.props.mode === "preview" || this.props.mode === "json") {
382
+ return this.props.json;
383
+ }
384
+ throw new PerseusError(
385
+ "Could not serialize; mode " + this.props.mode + " not found",
386
+ Errors.Internal,
387
+ );
388
+ }
389
+
390
+ /**
391
+ * Returns an array, with one element be section.
392
+ * Each element is an array of lint warnings present in that section.
393
+ *
394
+ * This function can currently only be called in edit mode.
395
+ */
396
+ getSaveWarnings(): $ReadOnlyArray<RendererProps> {
397
+ if (this.props.mode !== "edit") {
398
+ // TODO(joshuan): We should be able to get save warnings in
399
+ // preview mode.
400
+ throw new PerseusError(
401
+ "Can only get save warnings in edit mode.",
402
+ Errors.NotAllowed,
403
+ );
404
+ }
405
+
406
+ return this._sections().map((section, i) => {
407
+ // eslint-disable-next-line react/no-string-refs
408
+ return this.refs["editor" + i].getSaveWarnings();
409
+ });
410
+ }
411
+
412
+ render(): React.Node {
413
+ return (
414
+ <div className="framework-perseus perseus-article-editor">
415
+ {this.props.mode === "edit" && this._renderEditor()}
416
+
417
+ {this.props.mode === "preview" && this._renderPreviewMode()}
418
+
419
+ {this.props.mode === "json" && (
420
+ <div className="json-editor">
421
+ <div className="json-editor-warning">
422
+ <span>
423
+ Warning: Editing in this mode can lead to broken
424
+ articles!
425
+ </span>
426
+ </div>
427
+ <JsonEditor
428
+ multiLine={true}
429
+ onChange={this._handleJsonChange}
430
+ value={this.props.json}
431
+ />
432
+ </div>
433
+ )}
434
+ </div>
435
+ );
436
+ }
437
+ }
@@ -0,0 +1,24 @@
1
+ // @flow
2
+
3
+ import {action} from "@storybook/addon-actions";
4
+ import * as React from "react";
5
+
6
+ import BlurInput from "../blur-input.jsx";
7
+
8
+ export default {
9
+ title: "Perseus/Editor/Components/Blur Input",
10
+ };
11
+
12
+ export const Default = (): React.Node => {
13
+ const [value, setValue] = React.useState("");
14
+
15
+ return (
16
+ <BlurInput
17
+ value={value}
18
+ onChange={(newValue) => {
19
+ action("onChange")(newValue);
20
+ setValue(newValue);
21
+ }}
22
+ />
23
+ );
24
+ };
@@ -0,0 +1,62 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import type {CSSProperties} from "aphrodite";
5
+
6
+ type Props = {|
7
+ value: string,
8
+ onChange: (string) => void,
9
+ className?: string,
10
+ style?: CSSProperties,
11
+ |};
12
+
13
+ type State = {|
14
+ value: string,
15
+ |};
16
+
17
+ /* You know when you want to propagate input to a parent...
18
+ * but then that parent does something with the input...
19
+ * then changing the props of the input...
20
+ * on every keystroke...
21
+ * so if some input is invalid or incomplete...
22
+ * the input gets reset or otherwise effed...
23
+ *
24
+ * This is the solution.
25
+ *
26
+ * Enough melodrama. Its an input that only sends changes
27
+ * to its parent on blur.
28
+ */
29
+ // eslint-disable-next-line react/no-unsafe
30
+ class BlurInput extends React.Component<Props, State> {
31
+ constructor(props: Props) {
32
+ super(props);
33
+ this.state = {value: this.props.value};
34
+ }
35
+
36
+ UNSAFE_componentWillReceiveProps(nextProps: Props) {
37
+ this.setState({value: nextProps.value});
38
+ }
39
+
40
+ handleChange: (e: $FlowFixMe) => void = (e) => {
41
+ this.setState({value: e.target.value});
42
+ };
43
+
44
+ handleBlur: (e: $FlowFixMe) => void = (e) => {
45
+ this.props.onChange(e.target.value);
46
+ };
47
+
48
+ render(): React.Element<"input"> {
49
+ return (
50
+ <input
51
+ className={this.props.className}
52
+ style={this.props.style}
53
+ type="text"
54
+ value={this.state.value}
55
+ onChange={this.handleChange}
56
+ onBlur={this.handleBlur}
57
+ />
58
+ );
59
+ }
60
+ }
61
+
62
+ export default BlurInput;