@pie-lib/editable-html 11.1.1 → 11.1.2-next.1595

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