@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.
- package/CHANGELOG.json +12 -3322
- package/CHANGELOG.md +170 -100
- package/NEXT.CHANGELOG.json +1 -0
- package/lib/block-tags.js +25 -0
- package/lib/block-tags.js.map +1 -0
- package/lib/constants.js +16 -0
- package/lib/constants.js.map +1 -0
- package/lib/editor.js +352 -90
- package/lib/editor.js.map +1 -1
- package/lib/index.js +25 -9
- package/lib/index.js.map +1 -1
- package/lib/plugins/characters/index.js +8 -3
- package/lib/plugins/characters/index.js.map +1 -1
- package/lib/plugins/characters/utils.js +12 -12
- package/lib/plugins/characters/utils.js.map +1 -1
- package/lib/plugins/css/icons/index.js +37 -0
- package/lib/plugins/css/icons/index.js.map +1 -0
- package/lib/plugins/css/index.js +397 -0
- package/lib/plugins/css/index.js.map +1 -0
- package/lib/plugins/customPlugin/index.js +114 -0
- package/lib/plugins/customPlugin/index.js.map +1 -0
- package/lib/plugins/html/index.js +11 -7
- package/lib/plugins/html/index.js.map +1 -1
- package/lib/plugins/image/index.js +2 -1
- package/lib/plugins/image/index.js.map +1 -1
- package/lib/plugins/image/insert-image-handler.js +13 -4
- package/lib/plugins/image/insert-image-handler.js.map +1 -1
- package/lib/plugins/index.js +270 -11
- package/lib/plugins/index.js.map +1 -1
- package/lib/plugins/list/index.js +130 -0
- package/lib/plugins/list/index.js.map +1 -1
- package/lib/plugins/math/index.js +91 -56
- package/lib/plugins/math/index.js.map +1 -1
- package/lib/plugins/media/index.js +5 -2
- package/lib/plugins/media/index.js.map +1 -1
- package/lib/plugins/media/media-dialog.js +98 -57
- package/lib/plugins/media/media-dialog.js.map +1 -1
- package/lib/plugins/rendering/index.js +46 -0
- package/lib/plugins/rendering/index.js.map +1 -0
- package/lib/plugins/respArea/drag-in-the-blank/choice.js +45 -7
- package/lib/plugins/respArea/drag-in-the-blank/choice.js.map +1 -1
- package/lib/plugins/respArea/explicit-constructed-response/index.js +11 -9
- package/lib/plugins/respArea/explicit-constructed-response/index.js.map +1 -1
- package/lib/plugins/respArea/index.js +69 -21
- package/lib/plugins/respArea/index.js.map +1 -1
- package/lib/plugins/respArea/inline-dropdown/index.js +9 -4
- package/lib/plugins/respArea/inline-dropdown/index.js.map +1 -1
- package/lib/plugins/respArea/math-templated/index.js +130 -0
- package/lib/plugins/respArea/math-templated/index.js.map +1 -0
- package/lib/plugins/respArea/utils.js +16 -1
- package/lib/plugins/respArea/utils.js.map +1 -1
- package/lib/plugins/table/CustomTablePlugin.js +133 -0
- package/lib/plugins/table/CustomTablePlugin.js.map +1 -0
- package/lib/plugins/table/index.js +43 -59
- package/lib/plugins/table/index.js.map +1 -1
- package/lib/plugins/table/table-toolbar.js +33 -4
- package/lib/plugins/table/table-toolbar.js.map +1 -1
- package/lib/plugins/textAlign/icons/index.js +226 -0
- package/lib/plugins/textAlign/icons/index.js.map +1 -0
- package/lib/plugins/textAlign/index.js +34 -0
- package/lib/plugins/textAlign/index.js.map +1 -0
- package/lib/plugins/toolbar/default-toolbar.js +82 -27
- package/lib/plugins/toolbar/default-toolbar.js.map +1 -1
- package/lib/plugins/toolbar/done-button.js +5 -2
- package/lib/plugins/toolbar/done-button.js.map +1 -1
- package/lib/plugins/toolbar/editor-and-toolbar.js +18 -19
- package/lib/plugins/toolbar/editor-and-toolbar.js.map +1 -1
- package/lib/plugins/toolbar/toolbar-buttons.js +44 -11
- package/lib/plugins/toolbar/toolbar-buttons.js.map +1 -1
- package/lib/plugins/toolbar/toolbar.js +35 -11
- package/lib/plugins/toolbar/toolbar.js.map +1 -1
- package/lib/serialization.js +233 -44
- package/lib/serialization.js.map +1 -1
- package/lib/shared/alert-dialog.js +75 -0
- package/package.json +7 -6
- package/src/__tests__/editor.test.jsx +363 -0
- package/src/__tests__/serialization.test.js +291 -0
- package/src/__tests__/utils.js +36 -0
- package/src/block-tags.js +17 -0
- package/src/constants.js +7 -0
- package/src/editor.jsx +307 -52
- package/src/index.jsx +19 -10
- package/src/plugins/characters/index.jsx +11 -3
- package/src/plugins/characters/utils.js +12 -12
- package/src/plugins/css/icons/index.jsx +17 -0
- package/src/plugins/css/index.jsx +346 -0
- package/src/plugins/customPlugin/index.jsx +85 -0
- package/src/plugins/html/index.jsx +9 -6
- package/src/plugins/image/__tests__/__snapshots__/component.test.jsx.snap +51 -0
- package/src/plugins/image/__tests__/__snapshots__/image-toolbar-logic.test.jsx.snap +27 -0
- package/src/plugins/image/__tests__/__snapshots__/image-toolbar.test.jsx.snap +44 -0
- package/src/plugins/image/__tests__/component.test.jsx +41 -0
- package/src/plugins/image/__tests__/image-toolbar-logic.test.jsx +42 -0
- package/src/plugins/image/__tests__/image-toolbar.test.jsx +11 -0
- package/src/plugins/image/__tests__/index.test.js +95 -0
- package/src/plugins/image/__tests__/insert-image-handler.test.js +113 -0
- package/src/plugins/image/__tests__/mock-change.js +15 -0
- package/src/plugins/image/index.jsx +2 -1
- package/src/plugins/image/insert-image-handler.js +13 -6
- package/src/plugins/index.jsx +248 -5
- package/src/plugins/list/__tests__/index.test.js +54 -0
- package/src/plugins/list/index.jsx +130 -0
- package/src/plugins/math/__tests__/__snapshots__/index.test.jsx.snap +48 -0
- package/src/plugins/math/__tests__/index.test.jsx +245 -0
- package/src/plugins/math/index.jsx +87 -56
- package/src/plugins/media/__tests__/index.test.js +75 -0
- package/src/plugins/media/index.jsx +3 -2
- package/src/plugins/media/media-dialog.js +106 -57
- package/src/plugins/rendering/index.js +31 -0
- package/src/plugins/respArea/drag-in-the-blank/choice.jsx +35 -5
- package/src/plugins/respArea/explicit-constructed-response/index.jsx +10 -8
- package/src/plugins/respArea/index.jsx +53 -7
- package/src/plugins/respArea/inline-dropdown/index.jsx +12 -5
- package/src/plugins/respArea/math-templated/index.jsx +104 -0
- package/src/plugins/respArea/utils.jsx +11 -0
- package/src/plugins/table/CustomTablePlugin.js +113 -0
- package/src/plugins/table/__tests__/__snapshots__/table-toolbar.test.jsx.snap +44 -0
- package/src/plugins/table/__tests__/index.test.jsx +401 -0
- package/src/plugins/table/__tests__/table-toolbar.test.jsx +42 -0
- package/src/plugins/table/index.jsx +46 -59
- package/src/plugins/table/table-toolbar.jsx +39 -2
- package/src/plugins/textAlign/icons/index.jsx +139 -0
- package/src/plugins/textAlign/index.jsx +23 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/default-toolbar.test.jsx.snap +923 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/editor-and-toolbar.test.jsx.snap +20 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/toolbar-buttons.test.jsx.snap +36 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/toolbar.test.jsx.snap +46 -0
- package/src/plugins/toolbar/__tests__/default-toolbar.test.jsx +94 -0
- package/src/plugins/toolbar/__tests__/editor-and-toolbar.test.jsx +37 -0
- package/src/plugins/toolbar/__tests__/toolbar-buttons.test.jsx +51 -0
- package/src/plugins/toolbar/__tests__/toolbar.test.jsx +106 -0
- package/src/plugins/toolbar/default-toolbar.jsx +82 -20
- package/src/plugins/toolbar/done-button.jsx +3 -1
- package/src/plugins/toolbar/editor-and-toolbar.jsx +18 -13
- package/src/plugins/toolbar/toolbar-buttons.jsx +52 -11
- package/src/plugins/toolbar/toolbar.jsx +31 -8
- package/src/serialization.jsx +213 -38
- package/README.md +0 -45
- package/deploy.sh +0 -16
- package/playground/image/data.js +0 -59
- package/playground/image/index.html +0 -22
- package/playground/image/index.jsx +0 -81
- package/playground/index.html +0 -25
- package/playground/mathquill/index.html +0 -22
- package/playground/mathquill/index.jsx +0 -155
- package/playground/package.json +0 -15
- package/playground/prod-test/index.html +0 -22
- package/playground/prod-test/index.jsx +0 -28
- package/playground/schema-override/data.js +0 -29
- package/playground/schema-override/image-plugin.jsx +0 -41
- package/playground/schema-override/index.html +0 -21
- package/playground/schema-override/index.jsx +0 -97
- package/playground/serialization/data.js +0 -29
- package/playground/serialization/image-plugin.jsx +0 -41
- package/playground/serialization/index.html +0 -22
- package/playground/serialization/index.jsx +0 -12
- package/playground/static.json +0 -3
- package/playground/table-examples.html +0 -70
- package/playground/webpack.config.js +0 -42
- 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
|
|
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
|
-
|
|
12
|
+
|
|
13
13
|
import { color } from '@pie-lib/render-ui';
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
+
isEditedInHtmlMode: this.state.isEditedInHtmlMode,
|
|
192
264
|
toggleHtmlMode: this.toggleHtmlMode,
|
|
193
|
-
handleAlertDialog: this.
|
|
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
|
-
((
|
|
288
|
+
((node, done) => {
|
|
289
|
+
const src = node.data.get('src');
|
|
290
|
+
|
|
210
291
|
props.imageSupport.delete(src, (e) => {
|
|
211
|
-
|
|
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:
|
|
256
|
-
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.
|
|
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
|
-
|
|
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
|
|
642
|
+
const relatedTarget = event.relatedTarget;
|
|
643
|
+
const toolbarElement = this.toolbarRef && relatedTarget?.closest(`[class*="${this.toolbarRef.className}"]`);
|
|
476
644
|
|
|
477
|
-
|
|
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.
|
|
483
|
-
|
|
484
|
-
this.
|
|
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 >
|
|
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
|
|
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.
|
|
819
|
+
: this.state.isEditedInHtmlMode;
|
|
617
820
|
|
|
618
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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={
|
|
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);
|