@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.
- package/CHANGELOG.md +43 -167
- package/NEXT.CHANGELOG.json +1 -0
- package/package.json +10 -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 +303 -49
- 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 +4 -1
- 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 +13 -6
- 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/lib/editor.js +0 -1094
- package/lib/editor.js.map +0 -1
- package/lib/index.js +0 -253
- package/lib/index.js.map +0 -1
- package/lib/parse-html.js +0 -16
- package/lib/parse-html.js.map +0 -1
- package/lib/plugins/characters/custom-popper.js +0 -73
- package/lib/plugins/characters/custom-popper.js.map +0 -1
- package/lib/plugins/characters/index.js +0 -300
- package/lib/plugins/characters/index.js.map +0 -1
- package/lib/plugins/characters/utils.js +0 -381
- package/lib/plugins/characters/utils.js.map +0 -1
- package/lib/plugins/html/icons/index.js +0 -38
- package/lib/plugins/html/icons/index.js.map +0 -1
- package/lib/plugins/html/index.js +0 -76
- package/lib/plugins/html/index.js.map +0 -1
- package/lib/plugins/image/alt-dialog.js +0 -129
- package/lib/plugins/image/alt-dialog.js.map +0 -1
- package/lib/plugins/image/component.js +0 -419
- package/lib/plugins/image/component.js.map +0 -1
- package/lib/plugins/image/image-toolbar.js +0 -177
- package/lib/plugins/image/image-toolbar.js.map +0 -1
- package/lib/plugins/image/index.js +0 -262
- package/lib/plugins/image/index.js.map +0 -1
- package/lib/plugins/image/insert-image-handler.js +0 -152
- package/lib/plugins/image/insert-image-handler.js.map +0 -1
- package/lib/plugins/index.js +0 -143
- package/lib/plugins/index.js.map +0 -1
- package/lib/plugins/list/index.js +0 -204
- package/lib/plugins/list/index.js.map +0 -1
- package/lib/plugins/math/index.js +0 -419
- package/lib/plugins/math/index.js.map +0 -1
- package/lib/plugins/media/index.js +0 -384
- package/lib/plugins/media/index.js.map +0 -1
- package/lib/plugins/media/media-dialog.js +0 -668
- package/lib/plugins/media/media-dialog.js.map +0 -1
- package/lib/plugins/media/media-toolbar.js +0 -101
- package/lib/plugins/media/media-toolbar.js.map +0 -1
- package/lib/plugins/media/media-wrapper.js +0 -93
- package/lib/plugins/media/media-wrapper.js.map +0 -1
- package/lib/plugins/respArea/drag-in-the-blank/choice.js +0 -251
- package/lib/plugins/respArea/drag-in-the-blank/choice.js.map +0 -1
- package/lib/plugins/respArea/drag-in-the-blank/index.js +0 -97
- package/lib/plugins/respArea/drag-in-the-blank/index.js.map +0 -1
- package/lib/plugins/respArea/explicit-constructed-response/index.js +0 -55
- package/lib/plugins/respArea/explicit-constructed-response/index.js.map +0 -1
- package/lib/plugins/respArea/icons/index.js +0 -95
- package/lib/plugins/respArea/icons/index.js.map +0 -1
- package/lib/plugins/respArea/index.js +0 -293
- package/lib/plugins/respArea/index.js.map +0 -1
- package/lib/plugins/respArea/inline-dropdown/index.js +0 -70
- package/lib/plugins/respArea/inline-dropdown/index.js.map +0 -1
- package/lib/plugins/respArea/utils.js +0 -110
- package/lib/plugins/respArea/utils.js.map +0 -1
- package/lib/plugins/table/icons/index.js +0 -69
- package/lib/plugins/table/icons/index.js.map +0 -1
- package/lib/plugins/table/index.js +0 -499
- package/lib/plugins/table/index.js.map +0 -1
- package/lib/plugins/table/table-toolbar.js +0 -158
- package/lib/plugins/table/table-toolbar.js.map +0 -1
- package/lib/plugins/toolbar/default-toolbar.js +0 -174
- package/lib/plugins/toolbar/default-toolbar.js.map +0 -1
- package/lib/plugins/toolbar/done-button.js +0 -50
- package/lib/plugins/toolbar/done-button.js.map +0 -1
- package/lib/plugins/toolbar/editor-and-toolbar.js +0 -287
- package/lib/plugins/toolbar/editor-and-toolbar.js.map +0 -1
- package/lib/plugins/toolbar/index.js +0 -34
- package/lib/plugins/toolbar/index.js.map +0 -1
- package/lib/plugins/toolbar/toolbar-buttons.js +0 -161
- package/lib/plugins/toolbar/toolbar-buttons.js.map +0 -1
- package/lib/plugins/toolbar/toolbar.js +0 -352
- package/lib/plugins/toolbar/toolbar.js.map +0 -1
- package/lib/plugins/utils.js +0 -62
- package/lib/plugins/utils.js.map +0 -1
- package/lib/serialization.js +0 -488
- package/lib/serialization.js.map +0 -1
- package/lib/theme.js +0 -9
- package/lib/theme.js.map +0 -1
- 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,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
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
+
isEditedInHtmlMode: this.state.isEditedInHtmlMode,
|
|
192
263
|
toggleHtmlMode: this.toggleHtmlMode,
|
|
193
|
-
handleAlertDialog: this.
|
|
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
|
-
((
|
|
287
|
+
((node, done) => {
|
|
288
|
+
const src = node.data.get('src');
|
|
289
|
+
|
|
210
290
|
props.imageSupport.delete(src, (e) => {
|
|
211
|
-
|
|
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:
|
|
256
|
-
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.
|
|
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
|
-
|
|
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
|
|
641
|
+
const relatedTarget = event.relatedTarget;
|
|
642
|
+
const toolbarElement = this.toolbarRef && relatedTarget?.closest(`[class*="${this.toolbarRef.className}"]`);
|
|
476
643
|
|
|
477
|
-
|
|
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.
|
|
483
|
-
|
|
484
|
-
this.
|
|
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 >
|
|
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
|
|
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.
|
|
818
|
+
: this.state.isEditedInHtmlMode;
|
|
819
|
+
|
|
820
|
+
if (isEditedInHtmlMode != this.state.isEditedInHtmlMode) {
|
|
821
|
+
this.handlePlugins(this.props);
|
|
822
|
+
}
|
|
617
823
|
|
|
618
|
-
this.setState({ value,
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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={
|
|
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);
|