@pie-lib/editable-html 11.1.1 → 11.2.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +43 -167
  2. package/NEXT.CHANGELOG.json +1 -0
  3. package/package.json +10 -6
  4. package/src/__tests__/editor.test.jsx +363 -0
  5. package/src/__tests__/serialization.test.js +291 -0
  6. package/src/__tests__/utils.js +36 -0
  7. package/src/block-tags.js +17 -0
  8. package/src/constants.js +7 -0
  9. package/src/editor.jsx +303 -49
  10. package/src/index.jsx +19 -10
  11. package/src/plugins/characters/index.jsx +11 -3
  12. package/src/plugins/characters/utils.js +12 -12
  13. package/src/plugins/css/icons/index.jsx +17 -0
  14. package/src/plugins/css/index.jsx +346 -0
  15. package/src/plugins/customPlugin/index.jsx +85 -0
  16. package/src/plugins/html/index.jsx +9 -6
  17. package/src/plugins/image/__tests__/__snapshots__/component.test.jsx.snap +51 -0
  18. package/src/plugins/image/__tests__/__snapshots__/image-toolbar-logic.test.jsx.snap +27 -0
  19. package/src/plugins/image/__tests__/__snapshots__/image-toolbar.test.jsx.snap +44 -0
  20. package/src/plugins/image/__tests__/component.test.jsx +41 -0
  21. package/src/plugins/image/__tests__/image-toolbar-logic.test.jsx +42 -0
  22. package/src/plugins/image/__tests__/image-toolbar.test.jsx +11 -0
  23. package/src/plugins/image/__tests__/index.test.js +95 -0
  24. package/src/plugins/image/__tests__/insert-image-handler.test.js +113 -0
  25. package/src/plugins/image/__tests__/mock-change.js +15 -0
  26. package/src/plugins/image/index.jsx +2 -1
  27. package/src/plugins/image/insert-image-handler.js +13 -6
  28. package/src/plugins/index.jsx +248 -5
  29. package/src/plugins/list/__tests__/index.test.js +54 -0
  30. package/src/plugins/list/index.jsx +130 -0
  31. package/src/plugins/math/__tests__/__snapshots__/index.test.jsx.snap +48 -0
  32. package/src/plugins/math/__tests__/index.test.jsx +245 -0
  33. package/src/plugins/math/index.jsx +87 -56
  34. package/src/plugins/media/__tests__/index.test.js +75 -0
  35. package/src/plugins/media/index.jsx +3 -2
  36. package/src/plugins/media/media-dialog.js +106 -57
  37. package/src/plugins/rendering/index.js +31 -0
  38. package/src/plugins/respArea/drag-in-the-blank/choice.jsx +4 -1
  39. package/src/plugins/respArea/explicit-constructed-response/index.jsx +10 -8
  40. package/src/plugins/respArea/index.jsx +53 -7
  41. package/src/plugins/respArea/inline-dropdown/index.jsx +13 -6
  42. package/src/plugins/respArea/math-templated/index.jsx +104 -0
  43. package/src/plugins/respArea/utils.jsx +11 -0
  44. package/src/plugins/table/CustomTablePlugin.js +113 -0
  45. package/src/plugins/table/__tests__/__snapshots__/table-toolbar.test.jsx.snap +44 -0
  46. package/src/plugins/table/__tests__/index.test.jsx +401 -0
  47. package/src/plugins/table/__tests__/table-toolbar.test.jsx +42 -0
  48. package/src/plugins/table/index.jsx +46 -59
  49. package/src/plugins/table/table-toolbar.jsx +39 -2
  50. package/src/plugins/textAlign/icons/index.jsx +139 -0
  51. package/src/plugins/textAlign/index.jsx +23 -0
  52. package/src/plugins/toolbar/__tests__/__snapshots__/default-toolbar.test.jsx.snap +923 -0
  53. package/src/plugins/toolbar/__tests__/__snapshots__/editor-and-toolbar.test.jsx.snap +20 -0
  54. package/src/plugins/toolbar/__tests__/__snapshots__/toolbar-buttons.test.jsx.snap +36 -0
  55. package/src/plugins/toolbar/__tests__/__snapshots__/toolbar.test.jsx.snap +46 -0
  56. package/src/plugins/toolbar/__tests__/default-toolbar.test.jsx +94 -0
  57. package/src/plugins/toolbar/__tests__/editor-and-toolbar.test.jsx +37 -0
  58. package/src/plugins/toolbar/__tests__/toolbar-buttons.test.jsx +51 -0
  59. package/src/plugins/toolbar/__tests__/toolbar.test.jsx +106 -0
  60. package/src/plugins/toolbar/default-toolbar.jsx +82 -20
  61. package/src/plugins/toolbar/done-button.jsx +3 -1
  62. package/src/plugins/toolbar/editor-and-toolbar.jsx +18 -13
  63. package/src/plugins/toolbar/toolbar-buttons.jsx +52 -11
  64. package/src/plugins/toolbar/toolbar.jsx +31 -8
  65. package/src/serialization.jsx +213 -38
  66. package/README.md +0 -45
  67. package/deploy.sh +0 -16
  68. package/lib/editor.js +0 -1094
  69. package/lib/editor.js.map +0 -1
  70. package/lib/index.js +0 -253
  71. package/lib/index.js.map +0 -1
  72. package/lib/parse-html.js +0 -16
  73. package/lib/parse-html.js.map +0 -1
  74. package/lib/plugins/characters/custom-popper.js +0 -73
  75. package/lib/plugins/characters/custom-popper.js.map +0 -1
  76. package/lib/plugins/characters/index.js +0 -300
  77. package/lib/plugins/characters/index.js.map +0 -1
  78. package/lib/plugins/characters/utils.js +0 -381
  79. package/lib/plugins/characters/utils.js.map +0 -1
  80. package/lib/plugins/html/icons/index.js +0 -38
  81. package/lib/plugins/html/icons/index.js.map +0 -1
  82. package/lib/plugins/html/index.js +0 -76
  83. package/lib/plugins/html/index.js.map +0 -1
  84. package/lib/plugins/image/alt-dialog.js +0 -129
  85. package/lib/plugins/image/alt-dialog.js.map +0 -1
  86. package/lib/plugins/image/component.js +0 -419
  87. package/lib/plugins/image/component.js.map +0 -1
  88. package/lib/plugins/image/image-toolbar.js +0 -177
  89. package/lib/plugins/image/image-toolbar.js.map +0 -1
  90. package/lib/plugins/image/index.js +0 -262
  91. package/lib/plugins/image/index.js.map +0 -1
  92. package/lib/plugins/image/insert-image-handler.js +0 -152
  93. package/lib/plugins/image/insert-image-handler.js.map +0 -1
  94. package/lib/plugins/index.js +0 -143
  95. package/lib/plugins/index.js.map +0 -1
  96. package/lib/plugins/list/index.js +0 -204
  97. package/lib/plugins/list/index.js.map +0 -1
  98. package/lib/plugins/math/index.js +0 -419
  99. package/lib/plugins/math/index.js.map +0 -1
  100. package/lib/plugins/media/index.js +0 -384
  101. package/lib/plugins/media/index.js.map +0 -1
  102. package/lib/plugins/media/media-dialog.js +0 -668
  103. package/lib/plugins/media/media-dialog.js.map +0 -1
  104. package/lib/plugins/media/media-toolbar.js +0 -101
  105. package/lib/plugins/media/media-toolbar.js.map +0 -1
  106. package/lib/plugins/media/media-wrapper.js +0 -93
  107. package/lib/plugins/media/media-wrapper.js.map +0 -1
  108. package/lib/plugins/respArea/drag-in-the-blank/choice.js +0 -251
  109. package/lib/plugins/respArea/drag-in-the-blank/choice.js.map +0 -1
  110. package/lib/plugins/respArea/drag-in-the-blank/index.js +0 -97
  111. package/lib/plugins/respArea/drag-in-the-blank/index.js.map +0 -1
  112. package/lib/plugins/respArea/explicit-constructed-response/index.js +0 -55
  113. package/lib/plugins/respArea/explicit-constructed-response/index.js.map +0 -1
  114. package/lib/plugins/respArea/icons/index.js +0 -95
  115. package/lib/plugins/respArea/icons/index.js.map +0 -1
  116. package/lib/plugins/respArea/index.js +0 -293
  117. package/lib/plugins/respArea/index.js.map +0 -1
  118. package/lib/plugins/respArea/inline-dropdown/index.js +0 -70
  119. package/lib/plugins/respArea/inline-dropdown/index.js.map +0 -1
  120. package/lib/plugins/respArea/utils.js +0 -110
  121. package/lib/plugins/respArea/utils.js.map +0 -1
  122. package/lib/plugins/table/icons/index.js +0 -69
  123. package/lib/plugins/table/icons/index.js.map +0 -1
  124. package/lib/plugins/table/index.js +0 -499
  125. package/lib/plugins/table/index.js.map +0 -1
  126. package/lib/plugins/table/table-toolbar.js +0 -158
  127. package/lib/plugins/table/table-toolbar.js.map +0 -1
  128. package/lib/plugins/toolbar/default-toolbar.js +0 -174
  129. package/lib/plugins/toolbar/default-toolbar.js.map +0 -1
  130. package/lib/plugins/toolbar/done-button.js +0 -50
  131. package/lib/plugins/toolbar/done-button.js.map +0 -1
  132. package/lib/plugins/toolbar/editor-and-toolbar.js +0 -287
  133. package/lib/plugins/toolbar/editor-and-toolbar.js.map +0 -1
  134. package/lib/plugins/toolbar/index.js +0 -34
  135. package/lib/plugins/toolbar/index.js.map +0 -1
  136. package/lib/plugins/toolbar/toolbar-buttons.js +0 -161
  137. package/lib/plugins/toolbar/toolbar-buttons.js.map +0 -1
  138. package/lib/plugins/toolbar/toolbar.js +0 -352
  139. package/lib/plugins/toolbar/toolbar.js.map +0 -1
  140. package/lib/plugins/utils.js +0 -62
  141. package/lib/plugins/utils.js.map +0 -1
  142. package/lib/serialization.js +0 -488
  143. package/lib/serialization.js.map +0 -1
  144. package/lib/theme.js +0 -9
  145. package/lib/theme.js.map +0 -1
  146. package/playground/image/data.js +0 -59
  147. package/playground/image/index.html +0 -22
  148. package/playground/image/index.jsx +0 -81
  149. package/playground/index.html +0 -25
  150. package/playground/mathquill/index.html +0 -22
  151. package/playground/mathquill/index.jsx +0 -155
  152. package/playground/package.json +0 -15
  153. package/playground/prod-test/index.html +0 -22
  154. package/playground/prod-test/index.jsx +0 -28
  155. package/playground/schema-override/data.js +0 -29
  156. package/playground/schema-override/image-plugin.jsx +0 -41
  157. package/playground/schema-override/index.html +0 -21
  158. package/playground/schema-override/index.jsx +0 -97
  159. package/playground/serialization/data.js +0 -29
  160. package/playground/serialization/image-plugin.jsx +0 -41
  161. package/playground/serialization/index.html +0 -22
  162. package/playground/serialization/index.jsx +0 -12
  163. package/playground/static.json +0 -3
  164. package/playground/table-examples.html +0 -70
  165. package/playground/webpack.config.js +0 -42
  166. package/static.json +0 -1
package/src/editor.jsx CHANGED
@@ -1,21 +1,22 @@
1
+ import React from 'react';
1
2
  import { Editor as SlateEditor, findNode, getEventRange, getEventTransfer } from 'slate-react';
2
3
  import SlateTypes from 'slate-prop-types';
3
-
4
- import isEqual from 'lodash/isEqual';
5
- import * as serialization from './serialization';
6
- import PropTypes from 'prop-types';
7
- import React from 'react';
8
4
  import { Value, Block, Inline } from 'slate';
9
- import { buildPlugins, ALL_PLUGINS, DEFAULT_PLUGINS } from './plugins';
5
+ import Plain from 'slate-plain-serializer';
6
+ import PropTypes from 'prop-types';
7
+ import isEqual from 'lodash/isEqual';
8
+ import classNames from 'classnames';
10
9
  import debug from 'debug';
11
10
  import { withStyles } from '@material-ui/core/styles';
12
- import classNames from 'classnames';
11
+
13
12
  import { color } from '@pie-lib/render-ui';
14
- import Plain from 'slate-plain-serializer';
15
13
  import { AlertDialog } from '@pie-lib/config-ui';
14
+ import { PreviewPrompt } from '@pie-lib/render-ui';
16
15
 
17
- import { getBase64 } from './serialization';
16
+ import { getBase64, htmlToValue } from './serialization';
18
17
  import InsertImageHandler from './plugins/image/insert-image-handler';
18
+ import * as serialization from './serialization';
19
+ import { buildPlugins, ALL_PLUGINS, DEFAULT_PLUGINS } from './plugins';
19
20
 
20
21
  export { ALL_PLUGINS, DEFAULT_PLUGINS, serialization };
21
22
 
@@ -46,6 +47,12 @@ const createToolbarOpts = (toolbarOpts, error, isHtmlMode) => {
46
47
  };
47
48
  };
48
49
 
50
+ /**
51
+ * The maximum number of characters the editor can support
52
+ * @type {number}
53
+ */
54
+ const MAX_CHARACTERS_LIMIT = 1000000;
55
+
49
56
  export class Editor extends React.Component {
50
57
  static propTypes = {
51
58
  autoFocus: PropTypes.bool,
@@ -70,6 +77,8 @@ export class Editor extends React.Component {
70
77
  }),
71
78
  charactersLimit: PropTypes.number,
72
79
  width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
80
+ minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
81
+ maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
73
82
  height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
74
83
  minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
75
84
  maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -82,12 +91,33 @@ export class Editor extends React.Component {
82
91
  disableUnderline: PropTypes.bool,
83
92
  autoWidthToolbar: PropTypes.bool,
84
93
  pluginProps: PropTypes.any,
94
+ // customPlugins should be inside pluginProps (a property inside pluginProps)
95
+ // customPlugins: PropTypes.arrayOf(
96
+ // PropTypes.shape({
97
+ // event: PropTypes.string,
98
+ // icon: PropTypes.string,
99
+ // iconType: PropTypes.string,
100
+ // iconAlt: PropTypes.string
101
+ // }),
102
+ // ),
85
103
  placeholder: PropTypes.string,
104
+ isEditor: PropTypes.bool,
86
105
  responseAreaProps: PropTypes.shape({
87
- type: PropTypes.oneOf(['explicit-constructed-response', 'inline-dropdown', 'drag-in-the-blank']),
106
+ type: PropTypes.oneOf([
107
+ 'explicit-constructed-response',
108
+ 'inline-dropdown',
109
+ 'drag-in-the-blank',
110
+ 'math-templated',
111
+ ]),
88
112
  options: PropTypes.object,
89
113
  respAreaToolbar: PropTypes.func,
90
114
  onHandleAreaChange: PropTypes.func,
115
+ maxResponseAreas: PropTypes.number,
116
+ error: PropTypes.any,
117
+ }),
118
+ extraCSSRules: PropTypes.shape({
119
+ names: PropTypes.arrayOf(PropTypes.string),
120
+ rules: PropTypes.string,
91
121
  }),
92
122
  languageCharactersProps: PropTypes.arrayOf(
93
123
  PropTypes.shape({
@@ -103,6 +133,7 @@ export class Editor extends React.Component {
103
133
  alwaysVisible: PropTypes.bool,
104
134
  showDone: PropTypes.bool,
105
135
  doneOn: PropTypes.string,
136
+ minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
106
137
  }),
107
138
  activePlugins: PropTypes.arrayOf((values) => {
108
139
  const allValid = values.every((v) => ALL_PLUGINS.includes(v));
@@ -127,6 +158,8 @@ export class Editor extends React.Component {
127
158
  toolbarOpts: defaultToolbarOpts,
128
159
  responseAreaProps: defaultResponseAreaProps,
129
160
  languageCharactersProps: defaultLanguageCharactersProps,
161
+ extraCSSRules: null,
162
+ isEditor: false,
130
163
  };
131
164
 
132
165
  constructor(props) {
@@ -136,29 +169,65 @@ export class Editor extends React.Component {
136
169
  toolbarOpts: createToolbarOpts(props.toolbarOpts, props.error),
137
170
  pendingImages: [],
138
171
  isHtmlMode: false,
139
- isEdited: false,
172
+ isEditedInHtmlMode: false,
173
+ focusToolbar: false,
140
174
  dialog: {
141
175
  open: false,
142
176
  },
143
177
  };
144
178
 
179
+ this.keyPadCharacterRef = React.createRef();
180
+ this.doneButtonRef = React.createRef();
181
+ this.keypadInteractionDetected = false;
182
+
145
183
  this.toggleHtmlMode = this.toggleHtmlMode.bind(this);
184
+ this.handleToolbarFocus = this.handleToolbarFocus.bind(this);
185
+ this.handleToolbarBlur = this.handleToolbarBlur.bind(this);
146
186
 
147
187
  this.onResize = () => {
148
- props.onChange(this.state.value, true);
188
+ if (!this.state.isHtmlMode && props.onChange) {
189
+ props.onChange(this.state.value, true);
190
+ }
149
191
  };
150
192
 
151
193
  this.handlePlugins(this.props);
152
194
  }
153
195
 
154
- handleAlertDialog = (open, extraDialogProps, callback) => {
196
+ handleToolbarFocus() {
197
+ if (this.state.focusToolbar) {
198
+ return;
199
+ }
200
+
201
+ this.setState({ focusToolbar: true });
202
+ }
203
+
204
+ setKeypadInteraction = (interacted) => {
205
+ this.keypadInteractionDetected = interacted;
206
+ };
207
+
208
+ handleToolbarBlur() {
209
+ setTimeout(() => {
210
+ if (!this.toolbarContainsFocus()) {
211
+ this.setState({ focusToolbar: false });
212
+ }
213
+ }, 0);
214
+ }
215
+
216
+ toolbarContainsFocus() {
217
+ if (!this.toolbarRef) return false;
218
+ const toolbarElement = this.toolbarRef;
219
+ const activeElement = document.activeElement;
220
+
221
+ return toolbarElement && toolbarElement.contains(activeElement);
222
+ }
223
+
224
+ handleDialog = (open, extraDialogProps = {}, callback) => {
155
225
  this.setState(
156
226
  {
157
227
  dialog: {
158
228
  open,
159
229
  ...extraDialogProps,
160
230
  },
161
- isEdited: false,
162
231
  },
163
232
  callback,
164
233
  );
@@ -168,6 +237,7 @@ export class Editor extends React.Component {
168
237
  this.setState(
169
238
  (prevState) => ({
170
239
  isHtmlMode: !prevState.isHtmlMode,
240
+ isEditedInHtmlMode: false,
171
241
  }),
172
242
  () => {
173
243
  const { error } = this.props;
@@ -187,28 +257,45 @@ export class Editor extends React.Component {
187
257
  };
188
258
 
189
259
  const htmlPluginOpts = {
260
+ currentValue: this.props.value,
190
261
  isHtmlMode: this.state.isHtmlMode,
191
- isEdited: this.state.isEdited,
262
+ isEditedInHtmlMode: this.state.isEditedInHtmlMode,
192
263
  toggleHtmlMode: this.toggleHtmlMode,
193
- handleAlertDialog: this.handleAlertDialog,
264
+ handleAlertDialog: this.handleDialog,
194
265
  };
266
+ let { customPlugins } = props.pluginProps || {};
267
+ customPlugins = customPlugins || [];
195
268
 
196
- this.plugins = buildPlugins(props.activePlugins, {
269
+ this.plugins = buildPlugins(props.activePlugins, customPlugins, {
197
270
  math: {
198
271
  onClick: this.onMathClick,
199
272
  onFocus: this.onPluginFocus,
200
273
  onBlur: this.onPluginBlur,
201
274
  ...props.mathMlOptions,
202
275
  },
276
+ textAlign: {
277
+ getValue: () => this.state.value,
278
+ onChange: this.onChange,
279
+ },
203
280
  html: htmlPluginOpts,
281
+ extraCSSRules: props.extraCSSRules || {},
204
282
  image: {
205
283
  disableImageAlignmentButtons: props.disableImageAlignmentButtons,
206
284
  onDelete:
207
285
  props.imageSupport &&
208
286
  props.imageSupport.delete &&
209
- ((src, done) => {
287
+ ((node, done) => {
288
+ const src = node.data.get('src');
289
+
210
290
  props.imageSupport.delete(src, (e) => {
211
- done(e, this.state.value);
291
+ const newPendingImages = this.state.pendingImages.filter((img) => img.key !== node.key);
292
+ const { scheduled: oldScheduled } = this.state;
293
+ const newState = {
294
+ pendingImages: newPendingImages,
295
+ scheduled: oldScheduled && newPendingImages.length === 0 ? false : oldScheduled,
296
+ };
297
+
298
+ this.setState(newState, () => done(e, this.state.value));
212
299
  });
213
300
  }),
214
301
  insertImageRequested:
@@ -252,8 +339,8 @@ export class Editor extends React.Component {
252
339
  }),
253
340
  onFocus: this.onPluginFocus,
254
341
  onBlur: this.onPluginBlur,
255
- maxImageWidth: this.props.maxImageWidth,
256
- maxImageHeight: this.props.maxImageHeight,
342
+ maxImageWidth: props.maxImageWidth,
343
+ maxImageHeight: props.maxImageHeight,
257
344
  },
258
345
  toolbar: {
259
346
  /**
@@ -267,7 +354,7 @@ export class Editor extends React.Component {
267
354
  const { nonEmpty } = props;
268
355
 
269
356
  log('[onDone]');
270
- this.setState({ toolbarInFocus: false, focusedNode: null });
357
+ this.setState({ toolbarInFocus: false, focusedNode: null, focusToolbar: false });
271
358
  this.editor.blur();
272
359
 
273
360
  if (nonEmpty && this.state.value.startText?.text?.length === 0) {
@@ -306,6 +393,8 @@ export class Editor extends React.Component {
306
393
  },
307
394
  },
308
395
  languageCharacters: props.languageCharactersProps,
396
+ keyPadCharacterRef: this.keyPadCharacterRef,
397
+ setKeypadInteraction: this.setKeypadInteraction,
309
398
  media: {
310
399
  focus: this.focus,
311
400
  createChange: () => this.state.value.change(),
@@ -325,6 +414,16 @@ export class Editor extends React.Component {
325
414
 
326
415
  window.addEventListener('resize', this.onResize);
327
416
 
417
+ const isResponseAreaEditor = this.props.className?.includes('response-area-editor');
418
+
419
+ if (isResponseAreaEditor && this.editor) {
420
+ const responseAreaEditor = document.querySelector(`[data-key="${this.editor.value.document.key}"]`);
421
+
422
+ if (responseAreaEditor) {
423
+ responseAreaEditor.setAttribute('aria-label', 'Answer');
424
+ }
425
+ }
426
+
328
427
  if (this.editor && this.props.autoFocus) {
329
428
  Promise.resolve().then(() => {
330
429
  if (this.editor) {
@@ -352,8 +451,11 @@ export class Editor extends React.Component {
352
451
 
353
452
  const differentCharacterProps = !isEqual(nextProps.languageCharactersProps, this.props.languageCharactersProps);
354
453
  const differentMathMlProps = !isEqual(nextProps.mathMlOptions, this.props.mathMlOptions);
454
+ const differentImageMaxDimensionsProps =
455
+ !isEqual(nextProps.maxImageWidth, this.props.maxImageWidth) ||
456
+ !isEqual(nextProps.maxImageHeight, this.props.maxImageHeight);
355
457
 
356
- if (differentCharacterProps || differentMathMlProps) {
458
+ if (differentCharacterProps || differentMathMlProps || differentImageMaxDimensionsProps) {
357
459
  this.handlePlugins(nextProps);
358
460
  }
359
461
 
@@ -374,10 +476,9 @@ export class Editor extends React.Component {
374
476
  // 2. We're currently in 'isHtmlMode' and the editor value has been modified.
375
477
  if (
376
478
  this.state.isHtmlMode !== prevState.isHtmlMode ||
377
- (this.state.isHtmlMode && !prevState.isEdited && this.state.isEdited)
479
+ (this.state.isHtmlMode && !prevState.isEditedInHtmlMode && this.state.isEditedInHtmlMode)
378
480
  ) {
379
481
  this.handlePlugins(this.props);
380
- this.onEditingDone();
381
482
  }
382
483
 
383
484
  const zeroWidthEls = document.querySelectorAll('[data-slate-zero-width="z"]');
@@ -420,23 +521,88 @@ export class Editor extends React.Component {
420
521
  };
421
522
 
422
523
  onEditingDone = () => {
423
- if (this.state.isHtmlMode) {
524
+ const { isHtmlMode, dialog, value, pendingImages } = this.state;
525
+
526
+ // Handling HTML mode and dialog state
527
+ if (isHtmlMode) {
528
+ // Early return if HTML mode is enabled
529
+ if (dialog?.open) return;
530
+
531
+ const currentValue = htmlToValue(value.document.text);
532
+ const previewText = this.renderHtmlPreviewContent();
533
+
534
+ this.openHtmlModeConfirmationDialog(currentValue, previewText);
424
535
  return;
425
536
  }
426
537
 
427
- const { pendingImages } = this.state;
428
-
429
538
  if (pendingImages.length) {
539
+ // schedule image processing
430
540
  this.setState({ scheduled: true });
431
541
  return;
432
542
  }
433
543
 
544
+ // Finalizing editing
434
545
  log('[onEditingDone]');
435
546
  this.setState({ pendingImages: [], stashedValue: null, focusedNode: null });
436
547
  log('[onEditingDone] value: ', this.state.value);
437
548
  this.props.onChange(this.state.value, true);
438
549
  };
439
550
 
551
+ /**
552
+ * Renders the HTML preview content to be displayed inside the dialog.
553
+ * This content includes the edited HTML and a prompt for the user.
554
+ */
555
+ renderHtmlPreviewContent = () => {
556
+ const { classes } = this.props;
557
+ return (
558
+ <div ref={(ref) => (this.elementRef = ref)}>
559
+ <div>Preview of Edited Html:</div>
560
+ <PreviewPrompt defaultClassName={classes.previewText} prompt={this.state.value.document.text} />
561
+ <div>Would you like to save these changes ?</div>
562
+ </div>
563
+ );
564
+ };
565
+
566
+ /**
567
+ * Opens a confirmation dialog in HTML mode, displaying the preview of the current HTML content
568
+ * and offering options to save or continue editing.
569
+ */
570
+ openHtmlModeConfirmationDialog = (currentValue, previewText) => {
571
+ this.setState({
572
+ dialog: {
573
+ open: true,
574
+ title: 'Content Preview & Save',
575
+ text: previewText,
576
+ onConfirmText: 'Save changes',
577
+ onCloseText: 'Continue editing',
578
+ onConfirm: () => {
579
+ this.handleHtmlModeSaveConfirmation(currentValue);
580
+ },
581
+ onClose: this.htmlModeContinueEditing,
582
+ },
583
+ });
584
+ };
585
+
586
+ /**
587
+ * Handles the save confirmation action in HTML mode. This updates the value to the confirmed
588
+ * content, updates value on props, and exits the HTML mode.
589
+ * @param {string} currentValue - The confirmed value of the HTML content to save.
590
+ */
591
+ handleHtmlModeSaveConfirmation = (currentValue) => {
592
+ this.setState({ value: currentValue });
593
+ this.props.onChange(currentValue, true);
594
+ this.handleDialog(false);
595
+ this.toggleHtmlMode();
596
+ };
597
+
598
+ /**
599
+ * Closes the dialog in HTML mode and allows the user to continue editing the html content.
600
+ * This function is invoked when the user opts to not save the current changes.
601
+ */
602
+ htmlModeContinueEditing = () => {
603
+ this.handleDialog(false);
604
+ };
605
+
440
606
  /**
441
607
  * Remove onResize event listener
442
608
  */
@@ -472,17 +638,35 @@ export class Editor extends React.Component {
472
638
 
473
639
  onBlur = (event) => {
474
640
  log('[onBlur]');
475
- const target = event.relatedTarget;
641
+ const relatedTarget = event.relatedTarget;
642
+ const toolbarElement = this.toolbarRef && relatedTarget?.closest(`[class*="${this.toolbarRef.className}"]`);
476
643
 
477
- const node = target ? findNode(target, this.state.value) : null;
644
+ // Check if relatedTarget is a done button
645
+ const isRawDoneButton =
646
+ this.doneButtonRef && relatedTarget?.closest(`[class*="${this.doneButtonRef.current?.className}"]`);
647
+
648
+ // Skip onBlur handling if relatedTarget is a button from the KeyPad characters
649
+ this.skipBlurHandling = this.keypadInteractionDetected && relatedTarget !== null;
650
+
651
+ if (toolbarElement && !isRawDoneButton && !this.state.focusToolbar) {
652
+ this.setState({
653
+ focusToolbar: true,
654
+ });
655
+ }
656
+
657
+ const node = relatedTarget ? findNode(relatedTarget, this.state.value) : null;
478
658
 
479
659
  log('[onBlur] node: ', node);
480
660
 
481
661
  return new Promise((resolve) => {
482
- this.setState(
483
- { preBlurValue: this.state.value, focusedNode: !node ? null : node },
484
- this.handleBlur.bind(this, resolve),
485
- );
662
+ if (!this.skipBlurHandling) {
663
+ this.setKeypadInteraction(false);
664
+ this.setState(
665
+ { preBlurValue: this.state.value, focusedNode: !node ? null : node },
666
+ this.handleBlur.bind(this, resolve),
667
+ );
668
+ }
669
+
486
670
  this.props.onBlur(event);
487
671
  });
488
672
  };
@@ -519,12 +703,18 @@ export class Editor extends React.Component {
519
703
  *
520
704
  * Note: The use of promises has been causing issues with MathQuill
521
705
  * */
522
- onFocus = () =>
706
+ onFocus = (event, change) =>
523
707
  new Promise((resolve) => {
524
708
  const editorDOM = document.querySelector(`[data-key="${this.state.value.document.key}"]`);
709
+ const isTouchDevice =
710
+ typeof window !== 'undefined' && ('ontouchstart' in window || navigator?.maxTouchPoints > 0);
525
711
 
526
712
  log('[onFocus]', document.activeElement);
527
713
 
714
+ if (this.keypadInteractionDetected && this.__TEMPORARY_CHANGE_DATA) {
715
+ this.__TEMPORARY_CHANGE_DATA = null;
716
+ }
717
+
528
718
  /**
529
719
  * This is a temporary hack - @see changeData below for some more information.
530
720
  */
@@ -534,7 +724,6 @@ export class Editor extends React.Component {
534
724
 
535
725
  if (domEl) {
536
726
  let change = this.state.value.change().setNodeByKey(key, { data });
537
-
538
727
  this.setState({ value: change.value }, () => {
539
728
  this.__TEMPORARY_CHANGE_DATA = null;
540
729
  });
@@ -555,11 +744,19 @@ export class Editor extends React.Component {
555
744
  this.stashValue();
556
745
  this.props.onFocus();
557
746
 
747
+ // Added for accessibility: Ensures the editor gains focus when tabbed to for improved keyboard navigation
748
+ const shouldFocusEditor = !this.keypadInteractionDetected && !isTouchDevice;
749
+
750
+ if (shouldFocusEditor) {
751
+ change?.focus();
752
+ }
753
+
558
754
  resolve();
559
755
  });
560
756
 
561
757
  stashValue = () => {
562
758
  log('[stashValue]');
759
+
563
760
  if (!this.state.stashedValue) {
564
761
  this.setState({ stashedValue: this.state.value });
565
762
  }
@@ -599,23 +796,32 @@ export class Editor extends React.Component {
599
796
 
600
797
  onChange = (change, done) => {
601
798
  log('[onChange]');
799
+ window.me = this;
602
800
 
603
801
  const { value } = change;
604
802
  const { charactersLimit } = this.props;
803
+ let limit = charactersLimit;
804
+ if (!limit || limit > MAX_CHARACTERS_LIMIT) {
805
+ limit = MAX_CHARACTERS_LIMIT;
806
+ }
605
807
 
606
- if (value && value.document && value.document.text && value.document.text.length > charactersLimit) {
808
+ if (value && value.document && value.document.text && value.document.text.length > limit) {
607
809
  return;
608
810
  }
609
811
 
610
812
  // Mark the editor as edited when in HTML mode and its content has changed.
611
813
  // This status will later be used to decide whether to prompt a warning to the user when exiting HTML mode.
612
- const isEdited = !this.state.isHtmlMode
814
+ const isEditedInHtmlMode = !this.state.isHtmlMode
613
815
  ? false
614
816
  : this.state.value.document.text !== value.document.text
615
817
  ? true
616
- : this.state.isEdited;
818
+ : this.state.isEditedInHtmlMode;
819
+
820
+ if (isEditedInHtmlMode != this.state.isEditedInHtmlMode) {
821
+ this.handlePlugins(this.props);
822
+ }
617
823
 
618
- this.setState({ value, isEdited }, () => {
824
+ this.setState({ value, isEditedInHtmlMode }, () => {
619
825
  log('[onChange], call done()');
620
826
 
621
827
  if (done) {
@@ -636,11 +842,19 @@ export class Editor extends React.Component {
636
842
  if (!v) {
637
843
  return;
638
844
  }
845
+ const calcRegex = /^calc\((.*)\)$/;
639
846
 
640
847
  if (typeof v === 'string') {
641
848
  if (v.endsWith('%')) {
642
849
  return undefined;
643
- } else if (v.endsWith('px') || v.endsWith('vh') || v.endsWith('vw')) {
850
+ } else if (
851
+ v.endsWith('px') ||
852
+ v.endsWith('vh') ||
853
+ v.endsWith('vw') ||
854
+ v.endsWith('ch') ||
855
+ v.endsWith('em') ||
856
+ v.match(calcRegex)
857
+ ) {
644
858
  return v;
645
859
  } else {
646
860
  const value = parseInt(v, 10);
@@ -650,15 +864,15 @@ export class Editor extends React.Component {
650
864
  if (typeof v === 'number') {
651
865
  return `${v}px`;
652
866
  }
653
-
654
- return;
655
867
  };
656
868
 
657
869
  buildSizeStyle() {
658
- const { width, minHeight, height, maxHeight } = this.props;
870
+ const { minWidth, width, maxWidth, minHeight, height, maxHeight } = this.props;
659
871
 
660
872
  return {
661
873
  width: this.valueToSize(width),
874
+ minWidth: this.valueToSize(minWidth),
875
+ maxWidth: this.valueToSize(maxWidth),
662
876
  height: this.valueToSize(height),
663
877
  minHeight: this.valueToSize(minHeight),
664
878
  maxHeight: this.valueToSize(maxHeight),
@@ -696,7 +910,7 @@ export class Editor extends React.Component {
696
910
  */
697
911
 
698
912
  // Uncomment this line to see the bug described above.
699
- // this.setState({changeData: {key, data}})
913
+ // this.setState({changeData: {key, data}})
700
914
 
701
915
  this.__TEMPORARY_CHANGE_DATA = { key, data };
702
916
  };
@@ -783,7 +997,7 @@ export class Editor extends React.Component {
783
997
  const { editor } = props;
784
998
  const { document } = editor.value;
785
999
 
786
- if (!editor.props.placeholder || document.text !== '' || document.nodes.size !== 1) {
1000
+ if (!editor.props.placeholder || document.text !== '' || document.nodes.size !== 1 || !document.isEmpty) {
787
1001
  return false;
788
1002
  }
789
1003
 
@@ -812,10 +1026,16 @@ export class Editor extends React.Component {
812
1026
  highlightShape,
813
1027
  classes,
814
1028
  className,
1029
+ isEditor,
815
1030
  placeholder,
816
1031
  pluginProps,
817
1032
  onKeyDown,
818
1033
  } = this.props;
1034
+ // We don't want to send customPlugins to slate.
1035
+ // Not sure if they would do any harm, but I think it's better to not send them.
1036
+ // We use custom plugins to be able to add custom buttons
1037
+ // eslint-disable-next-line no-unused-vars
1038
+ const { customPlugins, showParagraphs, separateParagraphs, ...otherPluginProps } = pluginProps || {};
819
1039
 
820
1040
  const { value, focusedNode, toolbarOpts, dialog, scheduled } = this.state;
821
1041
 
@@ -831,7 +1051,12 @@ export class Editor extends React.Component {
831
1051
  );
832
1052
 
833
1053
  return (
834
- <div ref={(ref) => (this.wrapperRef = ref)} style={{ width: sizeStyle.width }} className={names}>
1054
+ <div
1055
+ ref={(ref) => (this.wrapperRef = ref)}
1056
+ style={{ width: sizeStyle.width, minWidth: sizeStyle.minWidth, maxWidth: sizeStyle.maxWidth }}
1057
+ className={names}
1058
+ id={`editor-${value?.document?.key}`}
1059
+ >
835
1060
  {scheduled && <div className={classes.uploading}>Uploading image and then saving...</div>}
836
1061
  <SlateEditor
837
1062
  plugins={this.plugins}
@@ -846,7 +1071,11 @@ export class Editor extends React.Component {
846
1071
  this.toolbarRef = r;
847
1072
  }
848
1073
  }}
1074
+ doneButtonRef={this.doneButtonRef}
849
1075
  value={value}
1076
+ focusToolbar={this.state.focusToolbar}
1077
+ onToolbarFocus={this.handleToolbarFocus}
1078
+ onToolbarBlur={this.handleToolbarBlur}
850
1079
  focus={this.focus}
851
1080
  onKeyDown={onKeyDown}
852
1081
  onChange={this.onChange}
@@ -863,7 +1092,9 @@ export class Editor extends React.Component {
863
1092
  autoCorrect={spellCheck}
864
1093
  className={classNames(
865
1094
  {
866
- [classes.noPadding]: toolbarOpts && toolbarOpts.noBorder,
1095
+ [classes.noPadding]: toolbarOpts?.noPadding,
1096
+ [classes.showParagraph]: showParagraphs && !showParagraphs.disabled,
1097
+ [classes.separateParagraph]: separateParagraphs && !separateParagraphs.disabled,
867
1098
  },
868
1099
  classes.slateEditor,
869
1100
  )}
@@ -872,7 +1103,7 @@ export class Editor extends React.Component {
872
1103
  height: sizeStyle.height,
873
1104
  maxHeight: sizeStyle.maxHeight,
874
1105
  }}
875
- pluginProps={pluginProps}
1106
+ pluginProps={otherPluginProps}
876
1107
  toolbarOpts={toolbarOpts}
877
1108
  placeholder={placeholder}
878
1109
  renderPlaceholder={this.renderPlaceholder}
@@ -884,6 +1115,8 @@ export class Editor extends React.Component {
884
1115
  text={dialog.text}
885
1116
  onClose={dialog.onClose}
886
1117
  onConfirm={dialog.onConfirm}
1118
+ onConfirmText={dialog.onConfirmText}
1119
+ onCloseText={dialog.onCloseText}
887
1120
  />
888
1121
  </div>
889
1122
  );
@@ -931,12 +1164,33 @@ const styles = {
931
1164
  border: '1px solid #dfe2e5',
932
1165
  },
933
1166
  },
1167
+ showParagraph: {
1168
+ // a div that has a div after it
1169
+ '& > div:has(+ div)::after': {
1170
+ display: 'block',
1171
+ content: '"¶"',
1172
+ fontSize: '1em',
1173
+ color: '#146EB3',
1174
+ },
1175
+ },
1176
+ separateParagraph: {
1177
+ // a div that has a div after it
1178
+ '& > div:has(+ div)': {
1179
+ marginBottom: '1em',
1180
+ },
1181
+ },
934
1182
  toolbarOnTop: {
935
1183
  marginTop: '45px',
936
1184
  },
937
1185
  noPadding: {
938
1186
  padding: '0 !important',
939
1187
  },
1188
+ previewText: {
1189
+ marginBottom: '36px',
1190
+ marginTop: '6px',
1191
+ padding: '20px',
1192
+ backgroundColor: 'rgba(0,0,0,0.06)',
1193
+ },
940
1194
  };
941
1195
 
942
1196
  export default withStyles(styles)(Editor);