@pie-lib/editable-html 7.17.4-next.42 → 7.17.4-next.423

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 (95) hide show
  1. package/CHANGELOG.json +165 -0
  2. package/CHANGELOG.md +286 -0
  3. package/lib/editor.js +348 -179
  4. package/lib/editor.js.map +1 -1
  5. package/lib/index.js +63 -52
  6. package/lib/index.js.map +1 -1
  7. package/lib/parse-html.js.map +1 -1
  8. package/lib/plugins/characters/custom-popover.js +70 -0
  9. package/lib/plugins/characters/custom-popover.js.map +1 -0
  10. package/lib/plugins/characters/index.js +266 -0
  11. package/lib/plugins/characters/index.js.map +1 -0
  12. package/lib/plugins/characters/utils.js +382 -0
  13. package/lib/plugins/characters/utils.js.map +1 -0
  14. package/lib/plugins/image/component.js +251 -77
  15. package/lib/plugins/image/component.js.map +1 -1
  16. package/lib/plugins/image/image-toolbar.js +49 -63
  17. package/lib/plugins/image/image-toolbar.js.map +1 -1
  18. package/lib/plugins/image/index.js +56 -19
  19. package/lib/plugins/image/index.js.map +1 -1
  20. package/lib/plugins/image/insert-image-handler.js +9 -15
  21. package/lib/plugins/image/insert-image-handler.js.map +1 -1
  22. package/lib/plugins/index.js +20 -12
  23. package/lib/plugins/index.js.map +1 -1
  24. package/lib/plugins/list/index.js +82 -14
  25. package/lib/plugins/list/index.js.map +1 -1
  26. package/lib/plugins/math/index.js +50 -55
  27. package/lib/plugins/math/index.js.map +1 -1
  28. package/lib/plugins/media/index.js +26 -25
  29. package/lib/plugins/media/index.js.map +1 -1
  30. package/lib/plugins/media/media-dialog.js +45 -56
  31. package/lib/plugins/media/media-dialog.js.map +1 -1
  32. package/lib/plugins/media/media-toolbar.js +24 -30
  33. package/lib/plugins/media/media-toolbar.js.map +1 -1
  34. package/lib/plugins/media/media-wrapper.js +28 -35
  35. package/lib/plugins/media/media-wrapper.js.map +1 -1
  36. package/lib/plugins/respArea/drag-in-the-blank/choice.js +68 -46
  37. package/lib/plugins/respArea/drag-in-the-blank/choice.js.map +1 -1
  38. package/lib/plugins/respArea/drag-in-the-blank/index.js +12 -12
  39. package/lib/plugins/respArea/drag-in-the-blank/index.js.map +1 -1
  40. package/lib/plugins/respArea/explicit-constructed-response/index.js +10 -9
  41. package/lib/plugins/respArea/explicit-constructed-response/index.js.map +1 -1
  42. package/lib/plugins/respArea/icons/index.js +11 -11
  43. package/lib/plugins/respArea/icons/index.js.map +1 -1
  44. package/lib/plugins/respArea/index.js +58 -42
  45. package/lib/plugins/respArea/index.js.map +1 -1
  46. package/lib/plugins/respArea/inline-dropdown/index.js +8 -8
  47. package/lib/plugins/respArea/inline-dropdown/index.js.map +1 -1
  48. package/lib/plugins/respArea/utils.js +5 -5
  49. package/lib/plugins/respArea/utils.js.map +1 -1
  50. package/lib/plugins/table/icons/index.js +12 -12
  51. package/lib/plugins/table/icons/index.js.map +1 -1
  52. package/lib/plugins/table/index.js +83 -27
  53. package/lib/plugins/table/index.js.map +1 -1
  54. package/lib/plugins/table/table-toolbar.js +41 -50
  55. package/lib/plugins/table/table-toolbar.js.map +1 -1
  56. package/lib/plugins/toolbar/default-toolbar.js +14 -11
  57. package/lib/plugins/toolbar/default-toolbar.js.map +1 -1
  58. package/lib/plugins/toolbar/done-button.js +5 -5
  59. package/lib/plugins/toolbar/done-button.js.map +1 -1
  60. package/lib/plugins/toolbar/editor-and-toolbar.js +43 -43
  61. package/lib/plugins/toolbar/editor-and-toolbar.js.map +1 -1
  62. package/lib/plugins/toolbar/index.js +5 -5
  63. package/lib/plugins/toolbar/index.js.map +1 -1
  64. package/lib/plugins/toolbar/toolbar-buttons.js +49 -52
  65. package/lib/plugins/toolbar/toolbar-buttons.js.map +1 -1
  66. package/lib/plugins/toolbar/toolbar.js +60 -64
  67. package/lib/plugins/toolbar/toolbar.js.map +1 -1
  68. package/lib/plugins/utils.js +1 -1
  69. package/lib/plugins/utils.js.map +1 -1
  70. package/lib/serialization.js +32 -9
  71. package/lib/serialization.js.map +1 -1
  72. package/lib/theme.js.map +1 -1
  73. package/package.json +5 -5
  74. package/src/editor.jsx +189 -31
  75. package/src/index.jsx +20 -3
  76. package/src/plugins/characters/custom-popover.js +45 -0
  77. package/src/plugins/characters/index.jsx +244 -0
  78. package/src/plugins/characters/utils.js +448 -0
  79. package/src/plugins/image/component.jsx +202 -21
  80. package/src/plugins/image/image-toolbar.jsx +25 -20
  81. package/src/plugins/image/index.jsx +40 -9
  82. package/src/plugins/index.jsx +4 -1
  83. package/src/plugins/list/index.jsx +67 -5
  84. package/src/plugins/math/index.jsx +31 -37
  85. package/src/plugins/media/index.jsx +3 -0
  86. package/src/plugins/media/media-dialog.js +1 -1
  87. package/src/plugins/respArea/drag-in-the-blank/choice.jsx +28 -1
  88. package/src/plugins/respArea/explicit-constructed-response/index.jsx +3 -3
  89. package/src/plugins/respArea/index.jsx +51 -31
  90. package/src/plugins/table/index.jsx +61 -14
  91. package/src/plugins/toolbar/default-toolbar.jsx +8 -0
  92. package/src/plugins/toolbar/editor-and-toolbar.jsx +12 -4
  93. package/src/plugins/toolbar/toolbar-buttons.jsx +13 -2
  94. package/src/plugins/toolbar/toolbar.jsx +14 -4
  95. package/src/serialization.jsx +19 -3
@@ -20,14 +20,17 @@ const removeDialogs = () => {
20
20
  export const insertDialog = props => {
21
21
  const newEl = document.createElement('div');
22
22
  const { type, callback, ...rest } = props;
23
+ const initialBodyOverflow = document.body.style.overflow;
23
24
 
24
25
  removeDialogs();
25
26
 
26
27
  newEl.className = 'insert-media-dialog';
28
+ document.body.style.overflow = 'hidden';
27
29
 
28
30
  const handleClose = (val, data) => {
29
31
  callback(val, data);
30
32
  newEl.remove();
33
+ document.body.style.overflow = initialBodyOverflow;
31
34
  };
32
35
 
33
36
  const el = (
@@ -348,7 +348,7 @@ export class MediaDialog extends React.Component {
348
348
  Cancel
349
349
  </Button>
350
350
  <Button
351
- disabled={invalid || url === null}
351
+ disabled={invalid || url === null || url === undefined}
352
352
  onClick={() => this.handleDone(true)}
353
353
  color="primary"
354
354
  >
@@ -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,11 @@ 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
+
55
+ }
38
56
  const parentNode = value.document.getParent(lastText.key);
39
57
 
40
58
  if (parentNode) {
@@ -66,7 +84,7 @@ export default function ResponseAreaPlugin(opts) {
66
84
  name: 'response_area',
67
85
  toolbar,
68
86
  filterPlugins: (node, plugins) => {
69
- if (node.type === 'explicit_constructed_response') {
87
+ if (node.type === 'explicit_constructed_response' || node.type === 'drag_in_the_blank') {
70
88
  return [];
71
89
  }
72
90
 
@@ -84,8 +102,19 @@ export default function ResponseAreaPlugin(opts) {
84
102
 
85
103
  if (n.type === 'explicit_constructed_response') {
86
104
  const data = n.data.toJSON();
105
+ let error;
106
+
107
+ if (opts.error) {
108
+ error = opts.error();
109
+ }
87
110
 
88
- return <ExplicitConstructedResponse attributes={attributes} value={data.value} />;
111
+ return (
112
+ <ExplicitConstructedResponse
113
+ attributes={attributes}
114
+ value={data.value}
115
+ error={error && error[data.index] && error[data.index][0]}
116
+ />
117
+ );
89
118
  }
90
119
 
91
120
  if (n.type === 'drag_in_the_blank') {
@@ -102,7 +131,7 @@ export default function ResponseAreaPlugin(opts) {
102
131
  return <InlineDropdown attributes={attributes} selectedItem={data.value} />;
103
132
  }
104
133
  },
105
- onChange(change) {
134
+ onChange(change, editor) {
106
135
  const type = opts.type.replace(/-/g, '_');
107
136
 
108
137
  if (isUndefined(lastIndexMap[type])) {
@@ -118,41 +147,32 @@ export default function ResponseAreaPlugin(opts) {
118
147
  }
119
148
  });
120
149
  }
121
- },
122
- normalizeNode: node => {
123
- if (node.object !== 'document') {
150
+
151
+ if (!editor.value) {
124
152
  return;
125
153
  }
126
154
 
127
- const addSpacesArray = [];
155
+ const currentRespAreaList = change.value.document.filterDescendants(isOfCurrentType);
156
+ const oldRespAreaList = editor.value.document.filterDescendants(isOfCurrentType);
128
157
 
129
- const allElements = node.filterDescendants(d => elTypesArray.indexOf(d.type) >= 0);
158
+ if (currentRespAreaList.size >= opts.maxResponseAreas) {
159
+ toolbar.disabled = true;
160
+ } else {
161
+ toolbar.disabled = false;
162
+ }
130
163
 
131
- allElements.forEach(el => {
132
- const prevText = node.getPreviousText(el.key);
133
- const lastCharIsNewLine = prevText.text[prevText.text.length - 1] === '\n';
164
+ const arrayToFilter =
165
+ oldRespAreaList.size > currentRespAreaList.size ? oldRespAreaList : currentRespAreaList;
166
+ const arrayToUseForFilter =
167
+ arrayToFilter === oldRespAreaList ? currentRespAreaList : oldRespAreaList;
134
168
 
135
- if (prevText.text.length === 0 || lastCharIsNewLine) {
136
- addSpacesArray.push({
137
- nr: lastCharIsNewLine ? 1 : 2,
138
- key: prevText.key
139
- });
140
- }
141
- });
169
+ const elementsWithChangedStatus = arrayToFilter.filter(
170
+ d => !arrayToUseForFilter.find(e => e.data.get('index') === d.data.get('index'))
171
+ );
142
172
 
143
- if (!addSpacesArray.length) {
144
- return;
173
+ if (elementsWithChangedStatus.size && oldRespAreaList.size > currentRespAreaList.size) {
174
+ opts.onHandleAreaChange(elementsWithChangedStatus);
145
175
  }
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
176
  },
157
177
  onDrop(event, change, editor) {
158
178
  const closestEl = event.target.closest('[data-key]');
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import EditTable from 'slate-edit-table';
3
- import { Block, Inline } from 'slate';
3
+ import { Block } from 'slate';
4
4
  import debug from 'debug';
5
5
  import GridOn from '@material-ui/icons/GridOn';
6
6
  import TableToolbar from './table-toolbar';
@@ -78,6 +78,20 @@ TableCell.propTypes = {
78
78
  children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired
79
79
  };
80
80
 
81
+ export const moveFocusToBeginningOfTable = change => {
82
+ const addedTable = change.value.document.findDescendant(d => !!d.data && !!d.data.get('newTable'));
83
+
84
+ if (!addedTable) {
85
+ return;
86
+ }
87
+
88
+ change.collapseToStartOf(addedTable);
89
+
90
+ const update = addedTable.data.remove('newTable');
91
+
92
+ change.setNodeByKey(addedTable.key, { data: update });
93
+ };
94
+
81
95
  export default (opts, toolbarPlugins /* : {toolbar: {}}[] */) => {
82
96
  const core = EditTable({
83
97
  typeContent: 'div'
@@ -117,12 +131,32 @@ export default (opts, toolbarPlugins /* : {toolbar: {}}[] */) => {
117
131
  return ancestors.findLast(p => p.type === 'table');
118
132
  };
119
133
 
134
+ core.utils.createTableWithOptions = (row, columns, extra) => {
135
+ const createdTable = core.utils.createTable(row, columns);
136
+ const newTable = Block.create({
137
+ ...createdTable.toJSON(),
138
+ ...extra
139
+ });
140
+
141
+ return newTable;
142
+ };
143
+
120
144
  core.toolbar = {
121
145
  icon: <GridOn />,
122
146
  onClick: (value, onChange) => {
123
147
  log('insert table');
124
- const c = core.changes.insertTable(value.change(), 2, 2);
125
- onChange(c);
148
+ const change = value.change();
149
+ const newTable = core.utils.createTableWithOptions(2, 2, {
150
+ data: {
151
+ border: '1',
152
+ newTable: true
153
+ }
154
+ });
155
+
156
+ change.insertBlock(newTable);
157
+
158
+ moveFocusToBeginningOfTable(change);
159
+ onChange(change);
126
160
  },
127
161
  supports: (node, value) =>
128
162
  node && node.object === 'block' && core.utils.isSelectionInTable(value),
@@ -132,7 +166,7 @@ export default (opts, toolbarPlugins /* : {toolbar: {}}[] */) => {
132
166
  customToolbar: (node, value, onToolbarDone) => {
133
167
  log('[customToolbar] node.data: ', node.data);
134
168
 
135
- const tableBlock = core.utils.getTableBlock(value.document, node.key);
169
+ const tableBlock = core.utils.getTableBlock(value.document, node?.key);
136
170
  log('[customToolbar] tableBlock: ', tableBlock);
137
171
 
138
172
  const hasBorder = () =>
@@ -215,9 +249,16 @@ export default (opts, toolbarPlugins /* : {toolbar: {}}[] */) => {
215
249
  return;
216
250
  }
217
251
 
252
+ const tableAdded = node.findDescendant(d => d.data && d.data.get('newTable'));
253
+
254
+ if (!tableAdded) {
255
+ return;
256
+ }
257
+
258
+ const nodeToSearch = node.getParent(tableAdded.key) || node;
218
259
  let shouldAddTextAfterNode = false;
219
- const indexToNotHaveTableOn = node.nodes.size - 1;
220
- const indexOfLastTable = node.nodes.findLastIndex(d => d.type === 'table');
260
+ const indexToNotHaveTableOn = nodeToSearch.nodes.size - 1;
261
+ const indexOfLastTable = nodeToSearch.nodes.findLastIndex(d => d.type === 'table');
221
262
 
222
263
  // if the last table in the document is of type table, we need to do the change
223
264
  if (indexOfLastTable === indexToNotHaveTableOn) {
@@ -228,15 +269,13 @@ export default (opts, toolbarPlugins /* : {toolbar: {}}[] */) => {
228
269
  return;
229
270
  }
230
271
 
231
- const tableNode = node.nodes.get(indexOfLastTable);
232
-
233
272
  return change => {
234
273
  if (shouldAddTextAfterNode) {
235
- const tableJSON = tableNode.toJSON();
274
+ const tableJSON = tableAdded.toJSON();
236
275
 
237
276
  // we remove the table node because otherwise we can't add the empty block after it
238
277
  // we need a block that contains text in order to do it
239
- change.removeNodeByKey(tableNode.key);
278
+ change.removeNodeByKey(tableAdded.key);
240
279
 
241
280
  const newBlock = Block.create({
242
281
  object: 'block',
@@ -256,12 +295,20 @@ export default (opts, toolbarPlugins /* : {toolbar: {}}[] */) => {
256
295
  if (prevText) {
257
296
  // we move focus to the previous text
258
297
  change
259
- .moveFocusTo(prevText.key, prevText.text.length)
260
- .moveAnchorTo(prevText.key, prevText.text.length);
298
+ .moveFocusTo(prevText.key, prevText.text?.length)
299
+ .moveAnchorTo(prevText.key, prevText.text?.length);
261
300
  }
262
301
 
263
302
  // we insert the table block between the first block with text and the last block with text
264
- change.insertBlock(tableJSON);
303
+ change.insertBlock({
304
+ ...tableJSON,
305
+ data: {
306
+ ...tableJSON.data,
307
+ newTable: true
308
+ }
309
+ });
310
+
311
+ moveFocusToBeginningOfTable(change);
265
312
  });
266
313
  }
267
314
  };
@@ -282,7 +329,7 @@ export const parseStyleString = s => {
282
329
  return result;
283
330
  };
284
331
 
285
- export const reactAttributes = o => toStyleObject(o, { camelize: true });
332
+ export const reactAttributes = o => toStyleObject(o, { camelize: true, addUnits: false });
286
333
 
287
334
  const attributesToMap = el => (acc, attribute) => {
288
335
  const value = el.getAttribute(attribute);
@@ -14,25 +14,32 @@ const log = debug('@pie-lib:editable-html:plugins:toolbar');
14
14
  export const ToolbarButton = props => {
15
15
  const onToggle = () => {
16
16
  const c = props.onToggle(props.value.change(), props);
17
+
17
18
  props.onChange(c);
18
19
  };
19
20
 
20
21
  if (props.isMark) {
21
22
  const isActive = hasMark(props.value, props.type);
23
+
22
24
  log('[ToolbarButton] mark:isActive: ', isActive);
25
+
23
26
  return (
24
27
  <MarkButton active={isActive} label={props.type} onToggle={onToggle} mark={props.type}>
25
28
  {props.icon}
26
29
  </MarkButton>
27
30
  );
28
31
  } else {
32
+ const { disabled } = props;
29
33
  const isActive = props.isActive
30
34
  ? props.isActive(props.value, props.type)
31
35
  : hasBlock(props.value, props.type);
36
+
32
37
  log('[ToolbarButton] block:isActive: ', isActive);
38
+
33
39
  return (
34
40
  <Button
35
41
  active={isActive}
42
+ disabled={disabled}
36
43
  onClick={() => props.onClick(props.value, props.onChange)}
37
44
  extraStyles={props.buttonStyles}
38
45
  >
@@ -44,6 +51,7 @@ export const ToolbarButton = props => {
44
51
 
45
52
  const isActiveToolbarPlugin = props => plugin => {
46
53
  const isDisabled = (props[plugin.name] || {}).disabled;
54
+
47
55
  return plugin && plugin.toolbar && !isDisabled;
48
56
  };
49
57
 
@@ -19,6 +19,7 @@ export class EditorAndToolbar extends React.Component {
19
19
  onChange: PropTypes.func.isRequired,
20
20
  onDone: PropTypes.func.isRequired,
21
21
  onDataChange: PropTypes.func,
22
+ toolbarRef: PropTypes.func,
22
23
  focusedNode: SlatePropTypes.node,
23
24
  readOnly: PropTypes.bool,
24
25
  disableUnderline: PropTypes.bool,
@@ -27,7 +28,8 @@ export class EditorAndToolbar extends React.Component {
27
28
  pluginProps: PropTypes.object,
28
29
  toolbarOpts: PropTypes.shape({
29
30
  position: PropTypes.oneOf(['bottom', 'top']),
30
- alwaysVisible: PropTypes.bool
31
+ alwaysVisible: PropTypes.bool,
32
+ error: PropTypes.string
31
33
  })
32
34
  };
33
35
 
@@ -54,7 +56,8 @@ export class EditorAndToolbar extends React.Component {
54
56
  disableUnderline,
55
57
  pluginProps,
56
58
  toolbarOpts,
57
- onDataChange
59
+ onDataChange,
60
+ toolbarRef
58
61
  } = this.props;
59
62
 
60
63
  const inFocus = value.isFocused || (focusedNode !== null && focusedNode !== undefined);
@@ -82,7 +85,7 @@ export class EditorAndToolbar extends React.Component {
82
85
  );
83
86
 
84
87
  return (
85
- <div className={classes.root}>
88
+ <div className={classNames(classes.root, toolbarOpts && toolbarOpts.error && classes.error)}>
86
89
  <div className={holderNames}>
87
90
  <div className={classes.children}>{clonedChildren}</div>
88
91
  </div>
@@ -95,6 +98,7 @@ export class EditorAndToolbar extends React.Component {
95
98
  onChange={onChange}
96
99
  onDone={onDone}
97
100
  onDataChange={onDataChange}
101
+ toolbarRef={toolbarRef}
98
102
  pluginProps={pluginProps}
99
103
  toolbarOpts={toolbarOpts}
100
104
  />
@@ -114,7 +118,8 @@ const style = {
114
118
  wordBreak: 'break-word',
115
119
  overflow: 'visible',
116
120
  maxHeight: '500px',
117
- padding: '5px 0'
121
+ // needed in order to be able to put the focus before a void element when it is the first one in the editor
122
+ padding: '5px'
118
123
  }
119
124
  },
120
125
  children: {
@@ -211,6 +216,9 @@ const style = {
211
216
  backgroundColor: primary
212
217
  }
213
218
  }
219
+ },
220
+ error: {
221
+ border: '2px solid red'
214
222
  }
215
223
  };
216
224
 
@@ -15,6 +15,13 @@ const styles = () => ({
15
15
  },
16
16
  active: {
17
17
  color: 'black'
18
+ },
19
+ disabled: {
20
+ opacity: 0.7,
21
+ cursor: 'not-allowed',
22
+ '& :hover': {
23
+ color: 'grey'
24
+ }
18
25
  }
19
26
  });
20
27
 
@@ -26,6 +33,7 @@ export class RawButton extends React.Component {
26
33
  classes: PropTypes.object.isRequired,
27
34
  children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
28
35
  active: PropTypes.bool,
36
+ disabled: PropTypes.bool,
29
37
  extraStyles: PropTypes.object
30
38
  };
31
39
 
@@ -41,8 +49,11 @@ export class RawButton extends React.Component {
41
49
  };
42
50
 
43
51
  render() {
44
- const { active, classes, children, extraStyles } = this.props;
45
- const names = classNames(classes.button, active && classes.active);
52
+ const { active, classes, children, disabled, extraStyles } = this.props;
53
+ const names = classNames(classes.button, {
54
+ [classes.active]: active,
55
+ [classes.disabled]: disabled
56
+ });
46
57
 
47
58
  return (
48
59
  <div style={extraStyles} className={names} onMouseDown={this.onClick}>
@@ -1,13 +1,15 @@
1
- import { DoneButton } from './done-button';
1
+ import React from 'react';
2
+ import { Change } from 'slate';
2
3
  import Delete from '@material-ui/icons/Delete';
3
4
  import IconButton from '@material-ui/core/IconButton';
4
5
  import PropTypes from 'prop-types';
5
- import React from 'react';
6
6
  import classNames from 'classnames';
7
7
  import debug from 'debug';
8
8
  import SlatePropTypes from 'slate-prop-types';
9
9
  import debounce from 'lodash/debounce';
10
10
 
11
+ import { DoneButton } from './done-button';
12
+
11
13
  import { findSingleNode, findParentNode } from '../utils';
12
14
  import { withStyles } from '@material-ui/core/styles';
13
15
  import DefaultToolbar from './default-toolbar';
@@ -43,6 +45,7 @@ export class Toolbar extends React.Component {
43
45
  plugin: PropTypes.object,
44
46
  onImageClick: PropTypes.func,
45
47
  onDone: PropTypes.func.isRequired,
48
+ toolbarRef: PropTypes.func.isRequired,
46
49
  classes: PropTypes.object.isRequired,
47
50
  isFocused: PropTypes.bool,
48
51
  autoWidth: PropTypes.bool,
@@ -52,6 +55,7 @@ export class Toolbar extends React.Component {
52
55
  position: PropTypes.oneOf(['bottom', 'top']),
53
56
  alignment: PropTypes.oneOf(['left', 'right']),
54
57
  alwaysVisible: PropTypes.bool,
58
+ ref: PropTypes.func,
55
59
  showDone: PropTypes.bool
56
60
  }),
57
61
  onDataChange: PropTypes.func
@@ -133,7 +137,8 @@ export class Toolbar extends React.Component {
133
137
  autoWidth,
134
138
  onChange,
135
139
  isFocused,
136
- onDone
140
+ onDone,
141
+ toolbarRef
137
142
  } = this.props;
138
143
 
139
144
  const node = findSingleNode(value);
@@ -164,6 +169,11 @@ export class Toolbar extends React.Component {
164
169
  log('[render] plugin: ', plugin);
165
170
 
166
171
  const handleDone = (change, done) => {
172
+ // use handler only if this is an actual Slate Change
173
+ if (!(change instanceof Change)) {
174
+ return;
175
+ }
176
+
167
177
  let handler = onDone;
168
178
 
169
179
  if (plugin && plugin.toolbar && plugin.toolbar.customToolbar) {
@@ -223,7 +233,7 @@ export class Toolbar extends React.Component {
223
233
  });
224
234
 
225
235
  return (
226
- <div className={names} style={extraStyles} onClick={this.onClick}>
236
+ <div className={names} style={extraStyles} onClick={this.onClick} ref={toolbarRef}>
227
237
  {CustomToolbar ? (
228
238
  <CustomToolbar
229
239
  node={node}
@@ -58,7 +58,16 @@ export const parseStyleString = s => {
58
58
  return result;
59
59
  };
60
60
 
61
- export const reactAttributes = o => toStyleObject(o, { camelize: true });
61
+ export const getBase64 = file => {
62
+ return new Promise((resolve, reject) => {
63
+ const reader = new FileReader();
64
+ reader.readAsDataURL(file);
65
+ reader.onload = () => resolve(reader.result);
66
+ reader.onerror = error => reject(error);
67
+ });
68
+ };
69
+
70
+ export const reactAttributes = o => toStyleObject(o, { camelize: true, addUnits: false });
62
71
 
63
72
  const attributesToMap = el => (acc, attribute) => {
64
73
  const value = el.getAttribute(attribute);
@@ -340,7 +349,7 @@ serializer.deserialize = function deserialize(html) {
340
349
 
341
350
  let i;
342
351
 
343
- for (i = 0; i < 1000; i++) {
352
+ for (i = 0; i < 3000; i++) {
344
353
  json.schema.rules.push({
345
354
  match: { object: 'document' },
346
355
  nodes: [{ match: { object: 'block' } }]
@@ -356,7 +365,14 @@ serializer.deserialize = function deserialize(html) {
356
365
  return null;
357
366
  };
358
367
 
359
- export const htmlToValue = html => serializer.deserialize(html);
368
+ export const htmlToValue = html => {
369
+ try {
370
+ return serializer.deserialize(html);
371
+ } catch (e) {
372
+ console.log("Couldn't parse html: ", e);
373
+ return {};
374
+ }
375
+ };
360
376
 
361
377
  export const valueToHtml = value => serializer.serialize(value);
362
378