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

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 +325 -178
  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 +165 -30
  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 +26 -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,7 +20,9 @@ export class Component extends React.Component {
20
20
  classes: PropTypes.object.isRequired,
21
21
  attributes: PropTypes.object,
22
22
  onFocus: PropTypes.func,
23
- onBlur: PropTypes.func
23
+ onBlur: PropTypes.func,
24
+ maxImageWidth: PropTypes.number,
25
+ maxImageHeight: PropTypes.number
24
26
  };
25
27
 
26
28
  getWidth = percent => {
@@ -41,19 +43,11 @@ export class Component extends React.Component {
41
43
  applySizeData = () => {
42
44
  const { node, editor } = this.props;
43
45
 
44
- const resizePercent = node.data.get('resizePercent');
45
- log('[applySizeData]: resizePercent: ', resizePercent);
46
-
47
46
  let update = node.data;
48
47
 
49
- if (resizePercent) {
50
- update = update.set('width', this.getWidth(resizePercent));
51
- update = update.set('height', this.getHeight(resizePercent));
52
- } else {
53
- const w = update.get('width');
54
- if (w) {
55
- update = update.set('resizePercent', this.getPercentFromWidth(w));
56
- }
48
+ const w = update.get('width');
49
+ if (w) {
50
+ update = update.set('resizePercent', this.getPercentFromWidth(w));
57
51
  }
58
52
 
59
53
  log('[applySizeData] update: ', update);
@@ -63,8 +57,19 @@ export class Component extends React.Component {
63
57
  }
64
58
  };
65
59
 
60
+ initialiseResize = () => {
61
+ window.addEventListener('mousemove', this.startResizing, false);
62
+ window.addEventListener('mouseup', this.stopResizing, false);
63
+ };
64
+
66
65
  componentDidMount() {
67
66
  this.applySizeData();
67
+
68
+ const resizeHandle = this.resize;
69
+
70
+ if (resizeHandle) {
71
+ resizeHandle.addEventListener('mousedown', this.initialiseResize, false);
72
+ }
68
73
  }
69
74
 
70
75
  componentDidUpdate() {
@@ -79,14 +84,151 @@ export class Component extends React.Component {
79
84
  };
80
85
  }
81
86
 
87
+ loadImage = () => {
88
+ let { maxImageWidth, maxImageHeight } = this.props || {};
89
+ maxImageWidth = maxImageWidth || 700;
90
+ maxImageHeight = maxImageHeight || 900;
91
+
92
+ const box = this.img;
93
+
94
+ //on first load
95
+ if (!box.style.width || box.style.width === 'auto') {
96
+ const dimensions = {
97
+ width: (box && box.naturalWidth) || 100,
98
+ height: (box && box.naturalHeight) || 100
99
+ };
100
+
101
+ const { width, height } = this.updateImageDimensions(
102
+ dimensions,
103
+ {
104
+ width: dimensions.width < maxImageWidth ? dimensions.width : maxImageWidth,
105
+ height: dimensions.height < maxImageHeight ? dimensions.height : maxImageHeight
106
+ },
107
+ true
108
+ );
109
+
110
+ box.style.width = `${width}px`;
111
+ box.style.height = `${height}px`;
112
+
113
+ this.setState({
114
+ dimensions: { height: height, width: width }
115
+ });
116
+
117
+ const { node, editor } = this.props;
118
+
119
+ let update = node.data;
120
+
121
+ update = update.set('width', width);
122
+ update = update.set('height', height);
123
+
124
+ if (!update.equals(node.data)) {
125
+ editor.change(c => c.setNodeByKey(node.key, { data: update }));
126
+ }
127
+ }
128
+ };
129
+
130
+ startResizing = e => {
131
+ const bounds = e.target.getBoundingClientRect();
132
+ const box = this.img;
133
+ const dimensions = {
134
+ width: (box && box.naturalWidth) || 100,
135
+ height: (box && box.naturalHeight) || 100
136
+ };
137
+
138
+ const { width, height } = this.updateImageDimensions(
139
+ dimensions,
140
+ {
141
+ width: e.clientX - bounds.left,
142
+ height: e.clientY - bounds.top
143
+ },
144
+ true
145
+ );
146
+
147
+ const hasMinimumWidth = width > 50 && height > 50;
148
+ const hasDimensionsConstraints = width <= 700 && height <= 900;
149
+
150
+ if (hasMinimumWidth && hasDimensionsConstraints && box) {
151
+ box.style.width = `${width}px`;
152
+ box.style.height = `${height}px`;
153
+
154
+ this.setState({
155
+ dimensions: { height: height, width: width }
156
+ });
157
+
158
+ const { node, editor } = this.props;
159
+
160
+ let update = node.data;
161
+
162
+ update = update.set('width', width);
163
+ update = update.set('height', height);
164
+
165
+ if (!update.equals(node.data)) {
166
+ editor.change(c => c.setNodeByKey(node.key, { data: update }));
167
+ }
168
+ }
169
+ };
170
+
171
+ stopResizing = () => {
172
+ window.removeEventListener('mousemove', this.startResizing, false);
173
+ window.removeEventListener('mouseup', this.stopResizing, false);
174
+ };
175
+
176
+ updateImageDimensions = (initialDim, nextDim, keepAspectRatio, resizeType) => {
177
+ // if we want to keep image aspect ratio
178
+ if (keepAspectRatio) {
179
+ const imageAspectRatio = initialDim.width / initialDim.height;
180
+
181
+ if (resizeType === 'height') {
182
+ // if we want to change image height => we update the width accordingly
183
+ return {
184
+ width: nextDim.height * imageAspectRatio,
185
+ height: nextDim.height
186
+ };
187
+ }
188
+
189
+ // if we want to change image width => we update the height accordingly
190
+ return {
191
+ width: nextDim.width,
192
+ height: nextDim.width / imageAspectRatio
193
+ };
194
+ }
195
+
196
+ // if we don't want to keep aspect ratio, we just update both values
197
+ return {
198
+ width: nextDim.width,
199
+ height: nextDim.height
200
+ };
201
+ };
202
+
82
203
  render() {
83
204
  const { node, editor, classes, attributes, onFocus } = this.props;
84
205
  const active = editor.value.isFocused && editor.value.selection.hasEdgeIn(node);
85
206
  const src = node.data.get('src');
86
- const percent = node.data.get('percent');
87
207
  const loaded = node.data.get('loaded') !== false;
88
208
  const deleteStatus = node.data.get('deleteStatus');
209
+ const alignment = node.data.get('alignment');
210
+ const percent = node.data.get('percent');
211
+ let margin;
212
+ let justifyContent;
89
213
 
214
+ switch (alignment) {
215
+ case 'left':
216
+ justifyContent = 'flex-start';
217
+ margin = '0';
218
+ break;
219
+ case 'center':
220
+ justifyContent = 'center';
221
+ margin = '0 auto';
222
+ break;
223
+ case 'right':
224
+ justifyContent = 'flex-end';
225
+ margin = 'auto 0 0 auto ';
226
+ break;
227
+ default:
228
+ justifyContent = 'flex-start';
229
+ margin = '0';
230
+ break;
231
+ }
90
232
  log('[render] node.data:', node.data);
91
233
 
92
234
  const size = this.getSize(node.data);
@@ -95,7 +237,6 @@ export class Component extends React.Component {
95
237
 
96
238
  const className = classNames(
97
239
  classes.root,
98
- active && classes.active,
99
240
  !loaded && classes.loading,
100
241
  deleteStatus === 'pending' && classes.pendingDelete
101
242
  );
@@ -104,13 +245,30 @@ export class Component extends React.Component {
104
245
 
105
246
  return [
106
247
  <span key={'sp1'}>&nbsp;</span>,
107
- <div key={'comp'} onFocus={onFocus} className={className}>
248
+ <div key={'comp'} onFocus={onFocus} className={className} style={{ justifyContent }}>
108
249
  <LinearProgress
109
250
  mode="determinate"
110
251
  value={percent > 0 ? percent : 0}
111
252
  className={progressClasses}
112
253
  />
113
- <img src={src} {...attributes} ref={r => (this.img = r)} style={size} />
254
+ <div className={classes.imageContainer}>
255
+ <img
256
+ {...attributes}
257
+ className={classNames(classes.image, active && classes.active)}
258
+ ref={ref => {
259
+ this.img = ref;
260
+ }}
261
+ src={src}
262
+ style={size}
263
+ onLoad={this.loadImage}
264
+ />
265
+ <div
266
+ ref={ref => {
267
+ this.resize = ref;
268
+ }}
269
+ className={classNames(classes.resize, 'resize')}
270
+ />
271
+ </div>
114
272
  </div>,
115
273
  <span key={'sp2'}>&nbsp;</span>
116
274
  ];
@@ -135,7 +293,7 @@ const styles = theme => ({
135
293
  progress: {
136
294
  position: 'absolute',
137
295
  left: '0',
138
- width: '100%',
296
+ width: 'fit-content',
139
297
  top: '0%',
140
298
  transition: 'opacity 200ms linear'
141
299
  },
@@ -151,15 +309,38 @@ const styles = theme => ({
151
309
  root: {
152
310
  position: 'relative',
153
311
  border: 'solid 1px white',
154
- display: 'inline-block',
312
+ display: 'flex',
155
313
  transition: 'opacity 200ms linear'
156
314
  },
157
- active: {
158
- border: `solid 1px ${theme.palette.primary.main}`
159
- },
160
315
  delete: {
161
316
  position: 'absolute',
162
317
  right: 0
318
+ },
319
+ imageContainer: {
320
+ position: 'relative',
321
+ width: 'fit-content',
322
+ display: 'flex',
323
+ alignItems: 'center',
324
+
325
+ '&&:hover > .resize': {
326
+ display: 'block'
327
+ }
328
+ },
329
+ active: {
330
+ border: `solid 1px ${theme.palette.primary.main}`
331
+ },
332
+ resize: {
333
+ backgroundColor: theme.palette.primary.main,
334
+ cursor: 'col-resize',
335
+ height: '35px',
336
+ width: '5px',
337
+ borderRadius: 8,
338
+ marginLeft: '5px',
339
+ marginRight: '10px',
340
+ display: 'none'
341
+ },
342
+ drawableHeight: {
343
+ minHeight: 350
163
344
  }
164
345
  });
165
346
 
@@ -1,50 +1,56 @@
1
- import { MarkButton } from '../toolbar/toolbar-buttons';
2
-
3
1
  import PropTypes from 'prop-types';
4
2
  import React from 'react';
5
3
  import debug from 'debug';
6
4
  import { withStyles } from '@material-ui/core/styles';
7
5
 
6
+ import { MarkButton } from '../toolbar/toolbar-buttons';
7
+
8
8
  const log = debug('@pie-lib:editable-html:plugins:image:image-toolbar');
9
9
 
10
- const PercentButton = ({ percent, active, onClick }) => {
11
- const label = `${percent}%`;
10
+ const AlignmentButton = ({ alignment, active, onClick }) => {
12
11
  return (
13
- <MarkButton active={active} onToggle={() => onClick(percent)} label={label}>
14
- {label}
12
+ <MarkButton active={active} onToggle={() => onClick(alignment)} label={alignment}>
13
+ {alignment}
15
14
  </MarkButton>
16
15
  );
17
16
  };
18
17
 
19
- PercentButton.propTypes = {
20
- percent: PropTypes.number.isRequired,
18
+ AlignmentButton.propTypes = {
19
+ alignment: PropTypes.string.isRequired,
21
20
  active: PropTypes.bool.isRequired,
22
21
  onClick: PropTypes.func.isRequired
23
22
  };
24
23
 
25
24
  export class ImageToolbar extends React.Component {
26
25
  static propTypes = {
27
- percent: PropTypes.number.isRequired,
28
26
  onChange: PropTypes.func.isRequired,
29
27
  classes: PropTypes.object.isRequired
30
28
  };
31
29
 
32
- onPercentClick = percent => {
33
- log('[onPercentClick]: percent:', percent);
34
- this.props.onChange(percent);
30
+ onAlignmentClick = alignment => {
31
+ log('[onAlignmentClick]: alignment:', alignment);
32
+ this.props.onChange(alignment);
35
33
  };
36
34
 
37
35
  render() {
38
- const { classes, percent } = this.props;
36
+ const { classes, alignment } = this.props;
37
+
39
38
  return (
40
39
  <div className={classes.holder}>
41
- <PercentButton percent={25} active={percent === 25} onClick={this.onPercentClick} />
42
- <PercentButton percent={50} active={percent === 50} onClick={this.onPercentClick} />
43
- <PercentButton active={percent === 75} percent={75} onClick={this.onPercentClick} />
44
- <PercentButton
45
- percent={100}
46
- active={percent === 100 || !percent}
47
- onClick={this.onPercentClick}
40
+ <AlignmentButton
41
+ alignment={'left'}
42
+ active={alignment === 'left'}
43
+ onClick={this.onAlignmentClick}
44
+ />
45
+ <AlignmentButton
46
+ alignment={'center'}
47
+ active={alignment === 'center'}
48
+ onClick={this.onAlignmentClick}
49
+ />
50
+ <AlignmentButton
51
+ alignment={'right'}
52
+ active={alignment === 'right'}
53
+ onClick={this.onAlignmentClick}
48
54
  />
49
55
  </div>
50
56
  );
@@ -29,19 +29,18 @@ export default function ImagePlugin(opts) {
29
29
  },
30
30
  supports: node => node.object === 'inline' && node.type === 'image',
31
31
  customToolbar: (node, value, onToolbarDone) => {
32
- const percent = node.data.get('resizePercent');
33
-
34
- const onChange = resizePercent => {
32
+ const alignment = node.data.get('alignment');
33
+ const onChange = alignment => {
35
34
  const update = {
36
35
  ...node.data.toObject(),
37
- resizePercent
36
+ alignment
38
37
  };
39
38
 
40
39
  const change = value.change().setNodeByKey(node.key, { data: update });
41
40
  onToolbarDone(change, false);
42
41
  };
43
42
 
44
- const Tb = () => <ImageToolbar percent={percent || 100} onChange={onChange} />;
43
+ const Tb = () => <ImageToolbar alignment={alignment || 'left'} onChange={onChange} />;
45
44
  return Tb;
46
45
  },
47
46
  showDone: true
@@ -90,7 +89,9 @@ export default function ImagePlugin(opts) {
90
89
  {
91
90
  onDelete: opts.onDelete,
92
91
  onFocus: opts.onFocus,
93
- onBlur: opts.onBlur
92
+ onBlur: opts.onBlur,
93
+ maxImageWidth: opts.maxImageWidth,
94
+ maxImageHeight: opts.maxImageHeight
94
95
  },
95
96
  props
96
97
  );
@@ -135,7 +136,7 @@ export const serialization = {
135
136
  if (name !== 'img') return;
136
137
 
137
138
  log('deserialize: ', name);
138
- const style = el.style || { width: '', height: '' };
139
+ const style = el.style || { width: '', height: '', margin: '', justifyContent: '' };
139
140
  const width = parseInt(style.width.replace('px', ''), 10) || null;
140
141
  const height = parseInt(style.height.replace('px', ''), 10) || null;
141
142
 
@@ -146,7 +147,10 @@ export const serialization = {
146
147
  data: {
147
148
  src: el.getAttribute('src'),
148
149
  width,
149
- height
150
+ height,
151
+ margin: el.style.margin,
152
+ justifyContent: el.style.justifyContent,
153
+ alignment: el.getAttribute('alignment')
150
154
  }
151
155
  };
152
156
  log('return object: ', out);
@@ -159,6 +163,9 @@ export const serialization = {
159
163
  const src = data.get('src');
160
164
  const width = data.get('width');
161
165
  const height = data.get('height');
166
+ const alignment = data.get('alignment');
167
+ const margin = data.get('margin');
168
+ const justifyContent = data.get('margin');
162
169
  const style = {};
163
170
  if (width) {
164
171
  style.width = `${width}px`;
@@ -168,11 +175,35 @@ export const serialization = {
168
175
  style.height = `${height}px`;
169
176
  }
170
177
 
178
+ style.margin = margin;
179
+ style.justifyContent = justifyContent;
180
+
181
+ if (alignment) {
182
+ switch (alignment) {
183
+ case 'left':
184
+ style.justifyContent = 'flex-start';
185
+ style.margin = '0';
186
+ break;
187
+ case 'center':
188
+ style.justifyContent = 'center';
189
+ style.margin = '0 auto';
190
+ break;
191
+ case 'right':
192
+ style.justifyContent = 'flex-end';
193
+ style.margin = 'auto 0 0 auto';
194
+ break;
195
+ default:
196
+ style.justifyContent = 'flex-start';
197
+ break;
198
+ }
199
+ }
200
+
171
201
  style.objectFit = 'contain';
172
202
 
173
203
  const props = {
174
204
  src,
175
- style
205
+ style,
206
+ alignment
176
207
  };
177
208
 
178
209
  return <img {...props} />;
@@ -4,6 +4,7 @@ import BulletedListIcon from '@material-ui/icons/FormatListBulleted';
4
4
  import NumberedListIcon from '@material-ui/icons/FormatListNumbered';
5
5
  import ImagePlugin from './image';
6
6
  import MediaPlugin from './media';
7
+ import CharactersPlugin from './characters';
7
8
  import Italic from '@material-ui/icons/FormatItalic';
8
9
  import MathPlugin from './math';
9
10
  import React from 'react';
@@ -63,6 +64,7 @@ export const ALL_PLUGINS = [
63
64
  'numbered-list',
64
65
  'image',
65
66
  'math',
67
+ 'languageCharacters',
66
68
  'table',
67
69
  'video',
68
70
  'audio',
@@ -103,10 +105,11 @@ export const buildPlugins = (activePlugins, opts) => {
103
105
  addIf('video', MediaPlugin('video', opts.media)),
104
106
  addIf('audio', MediaPlugin('audio', opts.media)),
105
107
  addIf('math', mathPlugin),
108
+ ...opts.languageCharacters.map(config => addIf('languageCharacters', CharactersPlugin(config))),
106
109
  addIf('bulleted-list', List({ key: 'l', type: 'ul_list', icon: <BulletedListIcon /> })),
107
110
  addIf('numbered-list', List({ key: 'n', type: 'ol_list', icon: <NumberedListIcon /> })),
108
111
  ToolbarPlugin(opts.toolbar),
109
- SoftBreakPlugin(),
112
+ SoftBreakPlugin({ shift: true }),
110
113
  addIf('responseArea', respAreaPlugin)
111
114
  ]);
112
115
  };
@@ -1,4 +1,6 @@
1
1
  import React from 'react';
2
+ import { Data } from 'slate';
3
+ import Immutable from 'immutable';
2
4
  import PropTypes from 'prop-types';
3
5
  import EditList from 'slate-edit-list';
4
6
  import debug from 'debug';
@@ -44,15 +46,12 @@ export const serialization = {
44
46
  }
45
47
  };
46
48
 
47
- export default options => {
48
- const { type, icon } = options;
49
-
49
+ const createEditList = () => {
50
50
  const core = EditList({
51
51
  typeDefault: 'span'
52
52
  });
53
53
 
54
54
  // fix outdated schema
55
-
56
55
  if (core.schema && core.schema.blocks) {
57
56
  Object.keys(core.schema.blocks).forEach(key => {
58
57
  const block = core.schema.blocks[key];
@@ -65,6 +64,69 @@ export default options => {
65
64
  });
66
65
  }
67
66
 
67
+ /**
68
+ * This override of the core.changes.wrapInList is needed because the version
69
+ * of immutable that we have does not support getting the element at a specific
70
+ * index with a square bracket (list[0]). We have to use the list.get function instead
71
+ */
72
+
73
+ /**
74
+ * Returns the highest list of blocks that cover the current selection
75
+ */
76
+ const getHighestSelectedBlocks = value => {
77
+ const range = value.selection;
78
+ const document = value.document;
79
+
80
+ const startBlock = document.getClosestBlock(range.startKey);
81
+ const endBlock = document.getClosestBlock(range.endKey);
82
+
83
+ if (startBlock === endBlock) {
84
+ return Immutable.List([startBlock]);
85
+ }
86
+
87
+ const ancestor = document.getCommonAncestor(startBlock.key, endBlock.key);
88
+ const startPath = ancestor.getPath(startBlock.key);
89
+ const endPath = ancestor.getPath(endBlock.key);
90
+
91
+ return ancestor.nodes.slice(startPath.get(0), endPath.get(0) + 1);
92
+ };
93
+
94
+ /**
95
+ * Wrap the blocks in the current selection in a new list. Selected
96
+ * lists are merged together.
97
+ */
98
+ core.changes.wrapInList = function(change, type, data) {
99
+ const selectedBlocks = getHighestSelectedBlocks(change.value);
100
+
101
+ // Wrap in container
102
+ change.wrapBlock({ type: type, data: Data.create(data) }, { normalize: false });
103
+
104
+ // Wrap in list items
105
+ selectedBlocks.forEach(function(node) {
106
+ if (core.utils.isList(node)) {
107
+ // Merge its items with the created list
108
+ node.nodes.forEach(function(_ref) {
109
+ const key = _ref.key;
110
+ return change.unwrapNodeByKey(key, { normalize: false });
111
+ });
112
+ } else if (node.type !== 'list_item') {
113
+ change.wrapBlockByKey(node.key, 'list_item', {
114
+ normalize: false
115
+ });
116
+ }
117
+ });
118
+
119
+ return change.normalize();
120
+ };
121
+
122
+ return core;
123
+ };
124
+
125
+ export default options => {
126
+ const { type, icon } = options;
127
+
128
+ const core = createEditList();
129
+
68
130
  // eslint-disable-next-line react/display-name
69
131
  core.renderNode = props => {
70
132
  const { node, attributes, children } = props;
@@ -88,7 +150,7 @@ export default options => {
88
150
  return false;
89
151
  }
90
152
  const current = core.utils.getCurrentList(value);
91
- return current.type === type;
153
+ return current ? current.type === type : false;
92
154
  },
93
155
  onClick: (value, onChange) => {
94
156
  log('[onClick]', value);
@@ -143,42 +143,6 @@ export default function MathPlugin() {
143
143
 
144
144
  return <span {...props.attributes} dangerouslySetInnerHTML={{ __html: html }} />;
145
145
  }
146
- },
147
-
148
- normalizeNode: node => {
149
- if (node.object !== 'document') {
150
- return;
151
- }
152
-
153
- const addSpacesArray = [];
154
-
155
- const allElements = node.filterDescendants(d => d.type === 'math');
156
-
157
- allElements.forEach(el => {
158
- const prevText = node.getPreviousText(el.key);
159
- const lastCharIsNewLine = prevText.text[prevText.text.length - 1] === '\n';
160
-
161
- if (prevText.text.length === 0 || lastCharIsNewLine) {
162
- addSpacesArray.push({
163
- nr: lastCharIsNewLine ? 1 : 2,
164
- key: prevText.key
165
- });
166
- }
167
- });
168
-
169
- if (!addSpacesArray.length) {
170
- return;
171
- }
172
-
173
- return change => {
174
- change.withoutNormalization(() => {
175
- addSpacesArray.forEach(({ key, nr }) => {
176
- const node = change.value.document.getNode(key);
177
-
178
- change.insertTextByKey(key, node.text.length, '\u00A0'.repeat(nr));
179
- });
180
- });
181
- };
182
146
  }
183
147
  };
184
148
  }
@@ -208,6 +172,36 @@ const getTagName = el => {
208
172
  return ((el && el.tagName) || '').toLowerCase();
209
173
  };
210
174
 
175
+ /**
176
+ * Makes sure that strings that contain stuff like:
177
+ * x<y are not transformed into x by the DOMParser because it thinks
178
+ * that <y is the start of a dom element tag
179
+ * @param input
180
+ * @returns {*}
181
+ */
182
+ const lessThanHandling = input => {
183
+ const arrowSplit = input.split('<');
184
+
185
+ // if we don't have at least 2 characters there's no point in checking
186
+ if (input.length > 2) {
187
+ return arrowSplit.reduce((st, part) => {
188
+ /*
189
+ We check if this element resulted is:
190
+ div - continuation of a beginning of a HTML element
191
+ /div - closing of a HTML tag
192
+ br/> - beginning and closing of a html TAG
193
+ */
194
+ if (part.match(/<[a-zA-Z/][\s\S]*>/ig)) {
195
+ return `${st}${st ? '<' : ''}${part}`;
196
+ }
197
+
198
+ return `${st}${st ? '&lt;' : ''}${part}`;
199
+ }, '');
200
+ }
201
+
202
+ return input;
203
+ };
204
+
211
205
  export const serialization = {
212
206
  deserialize(el) {
213
207
  const tagName = getTagName(el);
@@ -269,7 +263,7 @@ export const serialization = {
269
263
  const l = object.data.get('latex');
270
264
  const wrapper = object.data.get('wrapper');
271
265
  log('[serialize] latex: ', l);
272
- const decoded = htmlDecode(l);
266
+ const decoded = htmlDecode(lessThanHandling(l));
273
267
  return (
274
268
  <span data-latex="" data-raw={decoded}>
275
269
  {wrapMath(decoded, wrapper)}