@pie-lib/editable-html 7.17.4-next.59 → 7.17.4-next.592

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 (99) hide show
  1. package/CHANGELOG.json +135 -0
  2. package/CHANGELOG.md +421 -0
  3. package/lib/editor.js +392 -172
  4. package/lib/editor.js.map +1 -1
  5. package/lib/index.js +66 -53
  6. package/lib/index.js.map +1 -1
  7. package/lib/parse-html.js.map +1 -1
  8. package/lib/plugins/characters/custom-popper.js +73 -0
  9. package/lib/plugins/characters/custom-popper.js.map +1 -0
  10. package/lib/plugins/characters/index.js +285 -0
  11. package/lib/plugins/characters/index.js.map +1 -0
  12. package/lib/plugins/characters/utils.js +381 -0
  13. package/lib/plugins/characters/utils.js.map +1 -0
  14. package/lib/plugins/image/alt-dialog.js +119 -0
  15. package/lib/plugins/image/alt-dialog.js.map +1 -0
  16. package/lib/plugins/image/component.js +253 -77
  17. package/lib/plugins/image/component.js.map +1 -1
  18. package/lib/plugins/image/image-toolbar.js +95 -61
  19. package/lib/plugins/image/image-toolbar.js.map +1 -1
  20. package/lib/plugins/image/index.js +62 -20
  21. package/lib/plugins/image/index.js.map +1 -1
  22. package/lib/plugins/image/insert-image-handler.js +9 -15
  23. package/lib/plugins/image/insert-image-handler.js.map +1 -1
  24. package/lib/plugins/index.js +20 -12
  25. package/lib/plugins/index.js.map +1 -1
  26. package/lib/plugins/list/index.js +82 -14
  27. package/lib/plugins/list/index.js.map +1 -1
  28. package/lib/plugins/math/index.js +50 -55
  29. package/lib/plugins/math/index.js.map +1 -1
  30. package/lib/plugins/media/index.js +71 -27
  31. package/lib/plugins/media/index.js.map +1 -1
  32. package/lib/plugins/media/media-dialog.js +248 -72
  33. package/lib/plugins/media/media-dialog.js.map +1 -1
  34. package/lib/plugins/media/media-toolbar.js +24 -30
  35. package/lib/plugins/media/media-toolbar.js.map +1 -1
  36. package/lib/plugins/media/media-wrapper.js +28 -35
  37. package/lib/plugins/media/media-wrapper.js.map +1 -1
  38. package/lib/plugins/respArea/drag-in-the-blank/choice.js +68 -46
  39. package/lib/plugins/respArea/drag-in-the-blank/choice.js.map +1 -1
  40. package/lib/plugins/respArea/drag-in-the-blank/index.js +12 -12
  41. package/lib/plugins/respArea/drag-in-the-blank/index.js.map +1 -1
  42. package/lib/plugins/respArea/explicit-constructed-response/index.js +10 -9
  43. package/lib/plugins/respArea/explicit-constructed-response/index.js.map +1 -1
  44. package/lib/plugins/respArea/icons/index.js +11 -11
  45. package/lib/plugins/respArea/icons/index.js.map +1 -1
  46. package/lib/plugins/respArea/index.js +58 -42
  47. package/lib/plugins/respArea/index.js.map +1 -1
  48. package/lib/plugins/respArea/inline-dropdown/index.js +8 -8
  49. package/lib/plugins/respArea/inline-dropdown/index.js.map +1 -1
  50. package/lib/plugins/respArea/utils.js +5 -5
  51. package/lib/plugins/respArea/utils.js.map +1 -1
  52. package/lib/plugins/table/icons/index.js +12 -12
  53. package/lib/plugins/table/icons/index.js.map +1 -1
  54. package/lib/plugins/table/index.js +83 -27
  55. package/lib/plugins/table/index.js.map +1 -1
  56. package/lib/plugins/table/table-toolbar.js +41 -50
  57. package/lib/plugins/table/table-toolbar.js.map +1 -1
  58. package/lib/plugins/toolbar/default-toolbar.js +19 -13
  59. package/lib/plugins/toolbar/default-toolbar.js.map +1 -1
  60. package/lib/plugins/toolbar/done-button.js +5 -5
  61. package/lib/plugins/toolbar/done-button.js.map +1 -1
  62. package/lib/plugins/toolbar/editor-and-toolbar.js +62 -45
  63. package/lib/plugins/toolbar/editor-and-toolbar.js.map +1 -1
  64. package/lib/plugins/toolbar/index.js +6 -5
  65. package/lib/plugins/toolbar/index.js.map +1 -1
  66. package/lib/plugins/toolbar/toolbar-buttons.js +49 -52
  67. package/lib/plugins/toolbar/toolbar-buttons.js.map +1 -1
  68. package/lib/plugins/toolbar/toolbar.js +64 -62
  69. package/lib/plugins/toolbar/toolbar.js.map +1 -1
  70. package/lib/plugins/utils.js +1 -1
  71. package/lib/plugins/utils.js.map +1 -1
  72. package/lib/serialization.js +32 -9
  73. package/lib/serialization.js.map +1 -1
  74. package/lib/theme.js.map +1 -1
  75. package/package.json +7 -6
  76. package/src/editor.jsx +226 -26
  77. package/src/index.jsx +22 -5
  78. package/src/plugins/characters/custom-popper.js +48 -0
  79. package/src/plugins/characters/index.jsx +268 -0
  80. package/src/plugins/characters/utils.js +447 -0
  81. package/src/plugins/image/alt-dialog.jsx +69 -0
  82. package/src/plugins/image/component.jsx +204 -21
  83. package/src/plugins/image/image-toolbar.jsx +68 -22
  84. package/src/plugins/image/index.jsx +47 -9
  85. package/src/plugins/index.jsx +4 -1
  86. package/src/plugins/list/index.jsx +67 -5
  87. package/src/plugins/math/index.jsx +31 -37
  88. package/src/plugins/media/index.jsx +49 -6
  89. package/src/plugins/media/media-dialog.js +261 -89
  90. package/src/plugins/respArea/drag-in-the-blank/choice.jsx +28 -1
  91. package/src/plugins/respArea/explicit-constructed-response/index.jsx +3 -3
  92. package/src/plugins/respArea/index.jsx +50 -31
  93. package/src/plugins/table/index.jsx +63 -14
  94. package/src/plugins/toolbar/default-toolbar.jsx +20 -2
  95. package/src/plugins/toolbar/editor-and-toolbar.jsx +50 -11
  96. package/src/plugins/toolbar/index.jsx +1 -0
  97. package/src/plugins/toolbar/toolbar-buttons.jsx +13 -2
  98. package/src/plugins/toolbar/toolbar.jsx +18 -3
  99. package/src/serialization.jsx +19 -3
@@ -1,14 +1,20 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import debug from 'debug';
4
+ import { color } from '@pie-lib/render-ui';
4
5
  import { withStyles } from '@material-ui/core/styles';
6
+ import Button from '@material-ui/core/Button';
5
7
  import Dialog from '@material-ui/core/Dialog';
8
+ import MuiTabs from '@material-ui/core/Tabs';
9
+ import MuiTab from '@material-ui/core/Tab';
6
10
  import DialogTitle from '@material-ui/core/DialogTitle';
7
11
  import DialogContent from '@material-ui/core/DialogContent';
8
12
  import DialogContentText from '@material-ui/core/DialogContentText';
9
13
  import DialogActions from '@material-ui/core/DialogActions';
10
- import Button from '@material-ui/core/Button';
11
14
  import TextField from '@material-ui/core/TextField';
15
+ import Typography from '@material-ui/core/Typography';
16
+ import IconButton from '@material-ui/core/IconButton';
17
+ import ActionDelete from '@material-ui/icons/Delete';
12
18
 
13
19
  const log = debug('@pie-lib:editable-html:plugins:media:dialog');
14
20
 
@@ -75,6 +81,10 @@ export class MediaDialog extends React.Component {
75
81
  edit: PropTypes.bool,
76
82
  disablePortal: PropTypes.bool,
77
83
  handleClose: PropTypes.func,
84
+ uploadSoundSupport: PropTypes.shape({
85
+ add: PropTypes.func,
86
+ delete: PropTypes.func
87
+ }),
78
88
  type: PropTypes.string,
79
89
  src: PropTypes.string,
80
90
  url: PropTypes.string,
@@ -98,7 +108,13 @@ export class MediaDialog extends React.Component {
98
108
  height: height || 315,
99
109
  invalid: false,
100
110
  starts: starts || 0,
101
- width: width || 560
111
+ width: width || 560,
112
+ tabValue: 0,
113
+ fileUpload: {
114
+ uploadIsLoading: false,
115
+ localUrl: '',
116
+ error: null
117
+ }
102
118
  };
103
119
  }
104
120
 
@@ -220,13 +236,20 @@ export class MediaDialog extends React.Component {
220
236
 
221
237
  handleDone = val => {
222
238
  const { handleClose } = this.props;
239
+ const { tabValue, fileUpload } = this.state;
240
+ const isInsertURL = tabValue === 0;
223
241
 
224
242
  if (!val) {
243
+ if (fileUpload.localUrl) {
244
+ this.handleRemoveFile();
245
+ }
246
+
225
247
  handleClose(val);
226
- } else {
248
+ } else if (isInsertURL) {
227
249
  const { ends, height, url, urlToUse, formattedUrl, starts, width } = this.state;
228
250
 
229
251
  handleClose(val, {
252
+ tag: 'iframe',
230
253
  ends,
231
254
  height,
232
255
  starts,
@@ -235,13 +258,89 @@ export class MediaDialog extends React.Component {
235
258
  urlToUse,
236
259
  src: formattedUrl
237
260
  });
261
+ } else {
262
+ handleClose(val, {
263
+ tag: 'audio',
264
+ src: fileUpload.localUrl
265
+ });
238
266
  }
239
267
  };
240
268
 
269
+ handleUploadFile = async e => {
270
+ e.preventDefault();
271
+
272
+ this.setState({
273
+ fileUpload: {
274
+ ...this.state.fileUpload,
275
+ uploadIsLoading: true
276
+ }
277
+ });
278
+
279
+ const fileChosen = e.target.files[0];
280
+
281
+ const reader = new FileReader();
282
+
283
+ this.setState({
284
+ fileUpload: {
285
+ ...this.state.fileUpload,
286
+ uploadIsLoading: true
287
+ }
288
+ });
289
+
290
+ reader.onload = () => {
291
+ const dataURL = reader.result;
292
+
293
+ this.setState({
294
+ fileUpload: {
295
+ ...this.state.fileUpload,
296
+ localUrl: dataURL,
297
+ uploadIsLoading: false
298
+ }
299
+ });
300
+ };
301
+ reader.readAsDataURL(fileChosen);
302
+
303
+ this.props.uploadSoundSupport.add({
304
+ fileChosen,
305
+ done: e => {
306
+ console.log('add done: ', e);
307
+ }
308
+ });
309
+ };
310
+
311
+ handleRemoveFile = async () => {
312
+ this.props.uploadSoundSupport.delete(this.state.fileUpload.localUrl, e => {
313
+ console.log('delete done', e);
314
+ });
315
+
316
+ this.setState({
317
+ fileUpload: {
318
+ ...this.state.fileUpload,
319
+ localUrl: ''
320
+ }
321
+ });
322
+ };
323
+
241
324
  render() {
242
- const { classes, open, disablePortal, type, edit } = this.props;
243
- const { ends, height, invalid, starts, width, url, formattedUrl, updating } = this.state;
325
+ const { classes, open, disablePortal, type, edit, uploadSoundSupport } = this.props;
326
+ const {
327
+ ends,
328
+ height,
329
+ invalid,
330
+ starts,
331
+ width,
332
+ url,
333
+ formattedUrl,
334
+ updating,
335
+ tabValue,
336
+ fileUpload
337
+ } = this.state;
244
338
  const isYoutube = matchYoutubeUrl(url);
339
+ const isInsertURL = tabValue === 0;
340
+ const isUploadMedia = tabValue === 1;
341
+ const submitIsDisabled = isInsertURL
342
+ ? invalid || url === null || url === undefined
343
+ : !fileUpload.localUrl;
245
344
 
246
345
  return (
247
346
  <Dialog
@@ -255,103 +354,154 @@ export class MediaDialog extends React.Component {
255
354
  >
256
355
  <DialogTitle id="form-dialog-title">Insert {typeMap[type]}</DialogTitle>
257
356
  <DialogContent>
258
- <DialogContentText>
259
- {type === 'video' ? 'Insert YouTube or Vimeo URL' : 'Insert SoundCloud URL'}
260
- </DialogContentText>
261
- <TextField
262
- autoFocus
263
- error={invalid}
264
- helperText={invalid ? 'Invalid URL' : ''}
265
- margin="dense"
266
- id="name"
267
- label="URL"
268
- placeholder={`Paste URL of ${type}...`}
269
- type="text"
270
- onChange={this.urlChange}
271
- value={url}
272
- fullWidth
273
- />
274
- {type === 'video' && (
275
- <DialogContent
276
- classes={{
277
- root: classes.properties
278
- }}
279
- >
280
- <DialogContentText>Video Properties</DialogContentText>
281
- <TextField
282
- autoFocus
283
- margin="dense"
284
- id="width"
285
- label="Width"
286
- type="number"
287
- placeholder="Width"
288
- value={width}
289
- onChange={this.changeHandler('width')}
290
- />
291
- <TextField
292
- autoFocus
293
- margin="dense"
294
- id="height"
295
- label="Height"
296
- type="number"
297
- placeholder="Height"
298
- value={height}
299
- onChange={this.changeHandler('height')}
300
- />
301
- </DialogContent>
302
- )}
303
- {formattedUrl && (
304
- <iframe
305
- width={width}
306
- height={height}
307
- src={formattedUrl}
308
- frameBorder="0"
309
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
310
- allowFullScreen
311
- />
312
- )}
313
- {type === 'video' && (formattedUrl || updating) && !invalid && (
314
- <React.Fragment>
315
- <DialogContent
316
- classes={{
317
- root: classes.properties
357
+ <div>
358
+ <div className={classes.row}>
359
+ <MuiTabs
360
+ indicatorColor="primary"
361
+ value={tabValue}
362
+ onChange={(event, value) => {
363
+ this.setState({ tabValue: value });
318
364
  }}
319
365
  >
366
+ <MuiTab
367
+ label={type === 'video' ? 'Insert YouTube or Vimeo URL' : 'Insert SoundCloud URL'}
368
+ />
369
+ {uploadSoundSupport?.add && uploadSoundSupport?.delete && type !== 'video' ? (
370
+ <MuiTab label="Upload file" />
371
+ ) : null}
372
+ </MuiTabs>
373
+ </div>
374
+ {isInsertURL && (
375
+ <div>
320
376
  <TextField
321
377
  autoFocus
378
+ error={invalid}
379
+ helperText={invalid ? 'Invalid URL' : ''}
322
380
  margin="dense"
323
- id="starts"
324
- label="Starts"
325
- type="number"
326
- placeholder="Starts"
327
- value={starts}
328
- onChange={this.changeHandler('starts')}
381
+ id="name"
382
+ label="URL"
383
+ placeholder={`Paste URL of ${type}...`}
384
+ type="text"
385
+ onChange={this.urlChange}
386
+ value={url}
387
+ fullWidth
329
388
  />
330
- {isYoutube && (
331
- <TextField
332
- autoFocus
333
- margin="dense"
334
- id="ends"
335
- label="Ends"
336
- type="number"
337
- placeholder="Ends"
338
- value={ends}
339
- onChange={this.changeHandler('ends')}
389
+ {type === 'video' && (
390
+ <DialogContent
391
+ classes={{
392
+ root: classes.properties
393
+ }}
394
+ >
395
+ <DialogContentText>Video Properties</DialogContentText>
396
+ <TextField
397
+ autoFocus
398
+ margin="dense"
399
+ id="width"
400
+ label="Width"
401
+ type="number"
402
+ placeholder="Width"
403
+ value={width}
404
+ onChange={this.changeHandler('width')}
405
+ />
406
+ <TextField
407
+ autoFocus
408
+ margin="dense"
409
+ id="height"
410
+ label="Height"
411
+ type="number"
412
+ placeholder="Height"
413
+ value={height}
414
+ onChange={this.changeHandler('height')}
415
+ />
416
+ </DialogContent>
417
+ )}
418
+ {formattedUrl && (
419
+ <iframe
420
+ width={width}
421
+ height={height}
422
+ src={formattedUrl}
423
+ frameBorder="0"
424
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
425
+ allowFullScreen
340
426
  />
341
427
  )}
342
- </DialogContent>
343
- </React.Fragment>
344
- )}
428
+ {type === 'video' && (formattedUrl || updating) && !invalid && (
429
+ <React.Fragment>
430
+ <DialogContent
431
+ classes={{
432
+ root: classes.properties
433
+ }}
434
+ >
435
+ <TextField
436
+ autoFocus
437
+ margin="dense"
438
+ id="starts"
439
+ label="Starts"
440
+ type="number"
441
+ placeholder="Starts"
442
+ value={starts}
443
+ onChange={this.changeHandler('starts')}
444
+ />
445
+ {isYoutube && (
446
+ <TextField
447
+ autoFocus
448
+ margin="dense"
449
+ id="ends"
450
+ label="Ends"
451
+ type="number"
452
+ placeholder="Ends"
453
+ value={ends}
454
+ onChange={this.changeHandler('ends')}
455
+ />
456
+ )}
457
+ </DialogContent>
458
+ </React.Fragment>
459
+ )}
460
+ </div>
461
+ )}
462
+ {isUploadMedia && (
463
+ <div className={classes.uploadInput}>
464
+ {fileUpload.uploadIsLoading ? (
465
+ <Typography variant="subheading">Loading...</Typography>
466
+ ) : (
467
+ <div>
468
+ {fileUpload.localUrl ? (
469
+ <div className={classes.row}>
470
+ <audio controls="controls">
471
+ <source type="audio/mp3" src={fileUpload.localUrl} />
472
+ </audio>
473
+ <IconButton
474
+ aria-label="delete"
475
+ className={classes.deleteIcon}
476
+ onClick={this.handleRemoveFile}
477
+ >
478
+ <ActionDelete />
479
+ </IconButton>
480
+ </div>
481
+ ) : (
482
+ <input
483
+ accept="audio/*"
484
+ className={classes.input}
485
+ onChange={this.handleUploadFile}
486
+ type="file"
487
+ />
488
+ )}
489
+ {!!fileUpload.error && (
490
+ <Typography className={classes.error} variant="caption">
491
+ {fileUpload.error}
492
+ </Typography>
493
+ )}
494
+ </div>
495
+ )}
496
+ </div>
497
+ )}
498
+ </div>
345
499
  </DialogContent>
346
500
  <DialogActions>
347
501
  <Button onClick={() => this.handleDone(false)} color="primary">
348
502
  Cancel
349
503
  </Button>
350
- <Button
351
- disabled={invalid || url === null}
352
- onClick={() => this.handleDone(true)}
353
- color="primary"
354
- >
504
+ <Button disabled={submitIsDisabled} onClick={() => this.handleDone(true)} color="primary">
355
505
  {edit ? 'Update' : 'Insert'}
356
506
  </Button>
357
507
  </DialogActions>
@@ -366,6 +516,28 @@ const styles = () => ({
366
516
  },
367
517
  properties: {
368
518
  padding: 0
519
+ },
520
+ row: {
521
+ display: 'flex',
522
+ flexDirection: 'space-between'
523
+ },
524
+ rowItem: {
525
+ marginRight: '12px',
526
+ cursor: 'pointer'
527
+ },
528
+ active: {
529
+ color: color.primary(),
530
+ borderBottom: `2px solid ${color.primary()}`
531
+ },
532
+ uploadInput: {
533
+ marginTop: '12px'
534
+ },
535
+ error: {
536
+ marginTop: '12px',
537
+ color: 'red'
538
+ },
539
+ deleteIcon: {
540
+ marginLeft: '12px'
369
541
  }
370
542
  });
371
543
 
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import isUndefined from 'lodash/isUndefined';
4
4
  import { DragSource, DropTarget } from '@pie-lib/drag';
5
+ import { color } from '@pie-lib/render-ui';
5
6
  import { renderMath } from '@pie-lib/math-rendering';
6
7
  import { withStyles } from '@material-ui/core/styles';
7
8
  import classnames from 'classnames';
@@ -20,6 +21,9 @@ const useStyles = withStyles(theme => ({
20
21
  },
21
22
  incorrect: {
22
23
  border: 'solid 1px red'
24
+ },
25
+ selected: {
26
+ border: `2px solid ${color.primaryDark()} !important`
23
27
  }
24
28
  }));
25
29
 
@@ -30,9 +34,32 @@ export class BlankContent extends React.Component {
30
34
  isDragging: PropTypes.bool,
31
35
  isOver: PropTypes.bool,
32
36
  dragItem: PropTypes.object,
33
- value: PropTypes.object
37
+ value: PropTypes.object,
38
+ classes: PropTypes.object
34
39
  };
35
40
 
41
+ constructor(props) {
42
+ super(props);
43
+
44
+ this.handleClick = this.handleClick.bind(this);
45
+ }
46
+
47
+ componentDidMount() {
48
+ document.addEventListener('click', this.handleClick);
49
+ }
50
+
51
+ componentWillUnmount() {
52
+ document.removeEventListener('click', this.handleClick);
53
+ }
54
+
55
+ handleClick(event) {
56
+ const { classes } = this.props;
57
+
58
+ if (this.elementRef) {
59
+ this.elementRef.className = this.elementRef.contains(event.target) ? classes.selected : '';
60
+ }
61
+ }
62
+
36
63
  componentDidUpdate() {
37
64
  if (this.elementRef) {
38
65
  renderMath(this.elementRef);
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
 
4
4
  const ExplicitConstructedResponse = props => {
5
- const { attributes, value } = props;
5
+ const { attributes, value, error } = props;
6
6
 
7
7
  return (
8
8
  <span
@@ -23,7 +23,7 @@ const ExplicitConstructedResponse = props => {
23
23
  minHeight: '36px',
24
24
  height: '36px',
25
25
  background: '#FFF',
26
- border: '1px solid #C0C3CF',
26
+ border: `1px solid ${error ? 'red' : '#C0C3CF'}`,
27
27
  boxSizing: 'border-box',
28
28
  borderRadius: '3px',
29
29
  overflow: 'hidden',
@@ -39,7 +39,7 @@ const ExplicitConstructedResponse = props => {
39
39
 
40
40
  ExplicitConstructedResponse.propTypes = {
41
41
  attributes: PropTypes.object,
42
- value: PropTypes.object
42
+ value: PropTypes.string
43
43
  };
44
44
 
45
45
  export default ExplicitConstructedResponse;
@@ -11,9 +11,16 @@ import { ToolbarIcon } from './icons';
11
11
  const log = debug('@pie-lib:editable-html:plugins:respArea');
12
12
 
13
13
  const lastIndexMap = {};
14
- const elTypesArray = ['inline_dropdown', 'explicit_constructed_response', 'drag_in_the_blank'];
14
+ const elTypesMap = {
15
+ 'inline-dropdown': 'inline_dropdown',
16
+ 'explicit-constructed-response': 'explicit_constructed_response',
17
+ 'drag-in-the-blank': 'drag_in_the_blank'
18
+ };
19
+ const elTypesArray = Object.values(elTypesMap);
15
20
 
16
21
  export default function ResponseAreaPlugin(opts) {
22
+ const isOfCurrentType = d => d.type === opts.type || d.type === elTypesMap[opts.type];
23
+
17
24
  const toolbar = {
18
25
  icon: <ToolbarIcon />,
19
26
  buttonStyles: {
@@ -22,6 +29,12 @@ export default function ResponseAreaPlugin(opts) {
22
29
  onClick: (value, onChange) => {
23
30
  log('[toolbar] onClick');
24
31
  const change = value.change();
32
+ const currentRespAreaList = change.value.document.filterDescendants(isOfCurrentType);
33
+
34
+ if (currentRespAreaList.size >= opts.maxResponseAreas) {
35
+ return;
36
+ }
37
+
25
38
  const type = opts.type.replace(/-/g, '_');
26
39
  const prevIndex = lastIndexMap[type];
27
40
  const newIndex = prevIndex === 0 ? prevIndex : prevIndex + 1;
@@ -35,6 +48,10 @@ export default function ResponseAreaPlugin(opts) {
35
48
  } else {
36
49
  // If the markup is empty and there's no focus
37
50
  const lastText = value.document.getLastText();
51
+
52
+ if (!lastText) {
53
+ return;
54
+ }
38
55
  const parentNode = value.document.getParent(lastText.key);
39
56
 
40
57
  if (parentNode) {
@@ -66,7 +83,7 @@ export default function ResponseAreaPlugin(opts) {
66
83
  name: 'response_area',
67
84
  toolbar,
68
85
  filterPlugins: (node, plugins) => {
69
- if (node.type === 'explicit_constructed_response') {
86
+ if (node.type === 'explicit_constructed_response' || node.type === 'drag_in_the_blank') {
70
87
  return [];
71
88
  }
72
89
 
@@ -84,8 +101,19 @@ export default function ResponseAreaPlugin(opts) {
84
101
 
85
102
  if (n.type === 'explicit_constructed_response') {
86
103
  const data = n.data.toJSON();
104
+ let error;
87
105
 
88
- return <ExplicitConstructedResponse attributes={attributes} value={data.value} />;
106
+ if (opts.error) {
107
+ error = opts.error();
108
+ }
109
+
110
+ return (
111
+ <ExplicitConstructedResponse
112
+ attributes={attributes}
113
+ value={data.value}
114
+ error={error && error[data.index] && error[data.index][0]}
115
+ />
116
+ );
89
117
  }
90
118
 
91
119
  if (n.type === 'drag_in_the_blank') {
@@ -102,7 +130,7 @@ export default function ResponseAreaPlugin(opts) {
102
130
  return <InlineDropdown attributes={attributes} selectedItem={data.value} />;
103
131
  }
104
132
  },
105
- onChange(change) {
133
+ onChange(change, editor) {
106
134
  const type = opts.type.replace(/-/g, '_');
107
135
 
108
136
  if (isUndefined(lastIndexMap[type])) {
@@ -118,41 +146,32 @@ export default function ResponseAreaPlugin(opts) {
118
146
  }
119
147
  });
120
148
  }
121
- },
122
- normalizeNode: node => {
123
- if (node.object !== 'document') {
149
+
150
+ if (!editor.value) {
124
151
  return;
125
152
  }
126
153
 
127
- const addSpacesArray = [];
154
+ const currentRespAreaList = change.value.document.filterDescendants(isOfCurrentType);
155
+ const oldRespAreaList = editor.value.document.filterDescendants(isOfCurrentType);
128
156
 
129
- const allElements = node.filterDescendants(d => elTypesArray.indexOf(d.type) >= 0);
157
+ if (currentRespAreaList.size >= opts.maxResponseAreas) {
158
+ toolbar.disabled = true;
159
+ } else {
160
+ toolbar.disabled = false;
161
+ }
130
162
 
131
- allElements.forEach(el => {
132
- const prevText = node.getPreviousText(el.key);
133
- const lastCharIsNewLine = prevText.text[prevText.text.length - 1] === '\n';
163
+ const arrayToFilter =
164
+ oldRespAreaList.size > currentRespAreaList.size ? oldRespAreaList : currentRespAreaList;
165
+ const arrayToUseForFilter =
166
+ arrayToFilter === oldRespAreaList ? currentRespAreaList : oldRespAreaList;
134
167
 
135
- if (prevText.text.length === 0 || lastCharIsNewLine) {
136
- addSpacesArray.push({
137
- nr: lastCharIsNewLine ? 1 : 2,
138
- key: prevText.key
139
- });
140
- }
141
- });
168
+ const elementsWithChangedStatus = arrayToFilter.filter(
169
+ d => !arrayToUseForFilter.find(e => e.data.get('index') === d.data.get('index'))
170
+ );
142
171
 
143
- if (!addSpacesArray.length) {
144
- return;
172
+ if (elementsWithChangedStatus.size && oldRespAreaList.size > currentRespAreaList.size) {
173
+ opts.onHandleAreaChange(elementsWithChangedStatus);
145
174
  }
146
-
147
- return change => {
148
- change.withoutNormalization(() => {
149
- addSpacesArray.forEach(({ key, nr }) => {
150
- const node = change.value.document.getNode(key);
151
-
152
- change.insertTextByKey(key, node.text.length, '\u00A0'.repeat(nr));
153
- });
154
- });
155
- };
156
175
  },
157
176
  onDrop(event, change, editor) {
158
177
  const closestEl = event.target.closest('[data-key]');