@pie-lib/editable-html-tip-tap 1.0.2 → 1.0.3
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/lib/components/CharacterPicker.js +221 -0
- package/lib/components/EditableHtml.js +323 -0
- package/lib/components/MenuBar.js +693 -0
- package/lib/components/TiptapContainer.js +90 -0
- package/lib/components/buttons/done-button.js +53 -0
- package/lib/components/characters/characterUtils.js +112 -0
- package/lib/components/characters/custom-popper.js +73 -0
- package/lib/components/common/done-button.js +53 -0
- package/lib/components/icons/CssIcon.js +37 -0
- package/lib/components/icons/RespArea.js +95 -0
- package/lib/components/icons/TableIcons.js +69 -0
- package/lib/components/icons/TextAlign.js +194 -0
- package/lib/components/icons/index.js +194 -0
- package/lib/components/image/ImageToolbar.js +16 -0
- package/lib/components/image/InsertImageHandler.js +16 -0
- package/lib/components/media/MediaDialog.js +16 -0
- package/lib/components/media/MediaToolbar.js +16 -0
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +94 -0
- package/lib/components/respArea/DragInTheBlank/choice.js +289 -0
- package/lib/components/respArea/DragInTheBlank.js +94 -0
- package/lib/components/respArea/ExplicitConstructedResponse.js +120 -0
- package/lib/components/respArea/InlineDropdown.js +126 -0
- package/lib/components/respArea/ToolbarIcon.js +105 -0
- package/lib/components/respArea/choice.js +2 -0
- package/lib/extensions/component.js +5 -5
- package/lib/extensions/custom-toolbar-wrapper.js +2 -4
- package/lib/extensions/extended-table.js +30 -0
- package/lib/extensions/index.js +52 -0
- package/lib/extensions/media.js +5 -5
- package/lib/extensions/responseArea.js +7 -7
- package/lib/index.js +16 -1481
- package/lib/plugins/index.js +8 -80
- package/lib/styles/editorContainerStyles.js +200 -0
- package/lib/utils/size.js +34 -0
- package/package.json +1 -1
- package/src/components/CharacterPicker.jsx +185 -0
- package/src/components/EditableHtml.jsx +306 -0
- package/src/components/MenuBar.jsx +630 -0
- package/src/components/TiptapContainer.jsx +96 -0
- package/src/components/characters/characterUtils.js +127 -0
- package/src/components/image/ImageToolbar.jsx +1 -0
- package/src/components/image/InsertImageHandler.js +1 -0
- package/src/components/media/MediaDialog.js +1 -0
- package/src/components/media/MediaToolbar.jsx +1 -0
- package/src/{plugins/respArea/drag-in-the-blank → components/respArea/DragInTheBlank}/choice.jsx +1 -1
- package/src/{plugins/respArea/inline-dropdown/index.jsx → components/respArea/InlineDropdown.jsx} +1 -1
- package/src/components/respArea/ToolbarIcon.jsx +68 -0
- package/src/extensions/component.jsx +2 -2
- package/src/extensions/custom-toolbar-wrapper.jsx +6 -7
- package/src/extensions/extended-table.js +27 -0
- package/src/extensions/index.js +76 -0
- package/src/extensions/media.js +10 -4
- package/src/extensions/responseArea.js +7 -7
- package/src/index.jsx +3 -1440
- package/src/styles/editorContainerStyles.js +203 -0
- package/src/utils/size.js +32 -0
- package/src/__tests__/editor.test.jsx +0 -363
- package/src/__tests__/serialization.test.js +0 -291
- package/src/block-tags.js +0 -17
- package/src/editor.jsx +0 -1197
- package/src/extensions/characters.js +0 -46
- package/src/old-index.jsx +0 -162
- package/src/parse-html.js +0 -8
- package/src/plugins/README.md +0 -27
- package/src/plugins/characters/index.jsx +0 -284
- package/src/plugins/characters/utils.js +0 -447
- package/src/plugins/css/index.jsx +0 -340
- package/src/plugins/customPlugin/index.jsx +0 -85
- package/src/plugins/html/icons/index.jsx +0 -19
- package/src/plugins/html/index.jsx +0 -72
- package/src/plugins/image/__tests__/__snapshots__/component.test.jsx.snap +0 -51
- package/src/plugins/image/__tests__/__snapshots__/image-toolbar-logic.test.jsx.snap +0 -27
- package/src/plugins/image/__tests__/__snapshots__/image-toolbar.test.jsx.snap +0 -44
- package/src/plugins/image/__tests__/component.test.jsx +0 -41
- package/src/plugins/image/__tests__/image-toolbar-logic.test.jsx +0 -42
- package/src/plugins/image/__tests__/image-toolbar.test.jsx +0 -11
- package/src/plugins/image/__tests__/index.test.js +0 -95
- package/src/plugins/image/__tests__/insert-image-handler.test.js +0 -113
- package/src/plugins/image/__tests__/mock-change.js +0 -15
- package/src/plugins/image/alt-dialog.jsx +0 -82
- package/src/plugins/image/component.jsx +0 -343
- package/src/plugins/image/image-toolbar.jsx +0 -100
- package/src/plugins/image/index.jsx +0 -227
- package/src/plugins/image/insert-image-handler.js +0 -79
- package/src/plugins/index.jsx +0 -377
- package/src/plugins/list/__tests__/index.test.js +0 -54
- package/src/plugins/list/index.jsx +0 -305
- package/src/plugins/math/__tests__/__snapshots__/index.test.jsx.snap +0 -48
- package/src/plugins/math/__tests__/index.test.jsx +0 -245
- package/src/plugins/math/index.jsx +0 -379
- package/src/plugins/media/__tests__/index.test.js +0 -75
- package/src/plugins/media/index.jsx +0 -325
- package/src/plugins/media/media-dialog.js +0 -624
- package/src/plugins/media/media-toolbar.jsx +0 -56
- package/src/plugins/media/media-wrapper.jsx +0 -43
- package/src/plugins/rendering/index.js +0 -31
- package/src/plugins/respArea/index.jsx +0 -299
- package/src/plugins/respArea/math-templated/index.jsx +0 -104
- package/src/plugins/respArea/utils.jsx +0 -90
- package/src/plugins/table/CustomTablePlugin.js +0 -113
- package/src/plugins/table/__tests__/__snapshots__/table-toolbar.test.jsx.snap +0 -44
- package/src/plugins/table/__tests__/index.test.jsx +0 -401
- package/src/plugins/table/__tests__/table-toolbar.test.jsx +0 -42
- package/src/plugins/table/index.jsx +0 -427
- package/src/plugins/table/table-toolbar.jsx +0 -136
- package/src/plugins/textAlign/index.jsx +0 -23
- package/src/plugins/toolbar/__tests__/__snapshots__/default-toolbar.test.jsx.snap +0 -923
- package/src/plugins/toolbar/__tests__/__snapshots__/editor-and-toolbar.test.jsx.snap +0 -20
- package/src/plugins/toolbar/__tests__/__snapshots__/toolbar-buttons.test.jsx.snap +0 -36
- package/src/plugins/toolbar/__tests__/__snapshots__/toolbar.test.jsx.snap +0 -46
- package/src/plugins/toolbar/__tests__/default-toolbar.test.jsx +0 -94
- package/src/plugins/toolbar/__tests__/editor-and-toolbar.test.jsx +0 -37
- package/src/plugins/toolbar/__tests__/toolbar-buttons.test.jsx +0 -51
- package/src/plugins/toolbar/__tests__/toolbar.test.jsx +0 -106
- package/src/plugins/toolbar/default-toolbar.jsx +0 -206
- package/src/plugins/toolbar/editor-and-toolbar.jsx +0 -257
- package/src/plugins/toolbar/index.jsx +0 -23
- package/src/plugins/toolbar/toolbar-buttons.jsx +0 -138
- package/src/plugins/toolbar/toolbar.jsx +0 -338
- package/src/plugins/utils.js +0 -31
- package/src/serialization.jsx +0 -621
- /package/src/{plugins → components}/characters/custom-popper.js +0 -0
- /package/src/{plugins/toolbar → components/common}/done-button.jsx +0 -0
- /package/src/{plugins/css/icons/index.jsx → components/icons/CssIcon.jsx} +0 -0
- /package/src/{plugins/respArea/icons/index.jsx → components/icons/RespArea.jsx} +0 -0
- /package/src/{plugins/table/icons/index.jsx → components/icons/TableIcons.jsx} +0 -0
- /package/src/{plugins/textAlign/icons/index.jsx → components/icons/TextAlign.jsx} +0 -0
- /package/src/{plugins/respArea/drag-in-the-blank/index.jsx → components/respArea/DragInTheBlank/DragInTheBlank.jsx} +0 -0
- /package/src/{plugins/respArea/explicit-constructed-response/index.jsx → components/respArea/ExplicitConstructedResponse.jsx} +0 -0
package/src/editor.jsx
DELETED
|
@@ -1,1197 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Editor as SlateEditor, findNode, getEventRange, getEventTransfer } from 'slate-react';
|
|
3
|
-
import SlateTypes from 'slate-prop-types';
|
|
4
|
-
import { Value, Block, Inline } from 'slate';
|
|
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
|
-
import debug from 'debug';
|
|
11
|
-
import { withStyles } from '@material-ui/core/styles';
|
|
12
|
-
|
|
13
|
-
import { color } from '@pie-lib/render-ui';
|
|
14
|
-
import AlertDialog from '../../config-ui/src/alert-dialog';
|
|
15
|
-
import { PreviewPrompt } from '@pie-lib/render-ui';
|
|
16
|
-
|
|
17
|
-
import { getBase64, htmlToValue } from './serialization';
|
|
18
|
-
import InsertImageHandler from './plugins/image/insert-image-handler';
|
|
19
|
-
import * as serialization from './serialization';
|
|
20
|
-
import { buildPlugins, ALL_PLUGINS, DEFAULT_PLUGINS } from './plugins';
|
|
21
|
-
|
|
22
|
-
export { ALL_PLUGINS, DEFAULT_PLUGINS, serialization };
|
|
23
|
-
|
|
24
|
-
const log = debug('editable-html:editor');
|
|
25
|
-
|
|
26
|
-
const defaultToolbarOpts = {
|
|
27
|
-
position: 'bottom',
|
|
28
|
-
alignment: 'left',
|
|
29
|
-
alwaysVisible: false,
|
|
30
|
-
showDone: true,
|
|
31
|
-
doneOn: 'blur',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const defaultResponseAreaProps = {
|
|
35
|
-
options: {},
|
|
36
|
-
respAreaToolbar: () => {},
|
|
37
|
-
onHandleAreaChange: () => {},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const defaultLanguageCharactersProps = [];
|
|
41
|
-
|
|
42
|
-
const createToolbarOpts = (toolbarOpts, error, isHtmlMode) => {
|
|
43
|
-
return {
|
|
44
|
-
...defaultToolbarOpts,
|
|
45
|
-
...toolbarOpts,
|
|
46
|
-
error,
|
|
47
|
-
isHtmlMode,
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The maximum number of characters the editor can support
|
|
53
|
-
* @type {number}
|
|
54
|
-
*/
|
|
55
|
-
const MAX_CHARACTERS_LIMIT = 1000000;
|
|
56
|
-
|
|
57
|
-
export class Editor extends React.Component {
|
|
58
|
-
static propTypes = {
|
|
59
|
-
autoFocus: PropTypes.bool,
|
|
60
|
-
editorRef: PropTypes.func.isRequired,
|
|
61
|
-
error: PropTypes.any,
|
|
62
|
-
onRef: PropTypes.func.isRequired,
|
|
63
|
-
onChange: PropTypes.func.isRequired,
|
|
64
|
-
onFocus: PropTypes.func,
|
|
65
|
-
onBlur: PropTypes.func,
|
|
66
|
-
onKeyDown: PropTypes.func,
|
|
67
|
-
focus: PropTypes.func.isRequired,
|
|
68
|
-
value: SlateTypes.value.isRequired,
|
|
69
|
-
imageSupport: PropTypes.object,
|
|
70
|
-
mathMlOptions: PropTypes.shape({
|
|
71
|
-
mmlOutput: PropTypes.bool,
|
|
72
|
-
mmlEditing: PropTypes.bool,
|
|
73
|
-
}),
|
|
74
|
-
disableImageAlignmentButtons: PropTypes.bool,
|
|
75
|
-
uploadSoundSupport: PropTypes.shape({
|
|
76
|
-
add: PropTypes.func,
|
|
77
|
-
delete: PropTypes.func,
|
|
78
|
-
}),
|
|
79
|
-
charactersLimit: PropTypes.number,
|
|
80
|
-
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
81
|
-
minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
82
|
-
maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
83
|
-
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
84
|
-
minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
85
|
-
maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
86
|
-
classes: PropTypes.object.isRequired,
|
|
87
|
-
highlightShape: PropTypes.bool,
|
|
88
|
-
disabled: PropTypes.bool,
|
|
89
|
-
spellCheck: PropTypes.bool,
|
|
90
|
-
nonEmpty: PropTypes.bool,
|
|
91
|
-
disableScrollbar: PropTypes.bool,
|
|
92
|
-
disableUnderline: PropTypes.bool,
|
|
93
|
-
autoWidthToolbar: PropTypes.bool,
|
|
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
|
-
// ),
|
|
104
|
-
placeholder: PropTypes.string,
|
|
105
|
-
isEditor: PropTypes.bool,
|
|
106
|
-
responseAreaProps: PropTypes.shape({
|
|
107
|
-
type: PropTypes.oneOf([
|
|
108
|
-
'explicit-constructed-response',
|
|
109
|
-
'inline-dropdown',
|
|
110
|
-
'drag-in-the-blank',
|
|
111
|
-
'math-templated',
|
|
112
|
-
]),
|
|
113
|
-
options: PropTypes.object,
|
|
114
|
-
respAreaToolbar: PropTypes.func,
|
|
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,
|
|
122
|
-
}),
|
|
123
|
-
languageCharactersProps: PropTypes.arrayOf(
|
|
124
|
-
PropTypes.shape({
|
|
125
|
-
language: PropTypes.string,
|
|
126
|
-
characterIcon: PropTypes.string,
|
|
127
|
-
characters: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
|
128
|
-
}),
|
|
129
|
-
),
|
|
130
|
-
runSerializationOnMarkup: PropTypes.func,
|
|
131
|
-
toolbarOpts: PropTypes.shape({
|
|
132
|
-
position: PropTypes.oneOf(['bottom', 'top']),
|
|
133
|
-
alignment: PropTypes.oneOf(['left', 'right']),
|
|
134
|
-
alwaysVisible: PropTypes.bool,
|
|
135
|
-
showDone: PropTypes.bool,
|
|
136
|
-
doneOn: PropTypes.string,
|
|
137
|
-
minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
138
|
-
}),
|
|
139
|
-
activePlugins: PropTypes.arrayOf((values) => {
|
|
140
|
-
const allValid = values.every((v) => ALL_PLUGINS.includes(v));
|
|
141
|
-
|
|
142
|
-
return !allValid && new Error(`Invalid values: ${values}, values must be one of [${ALL_PLUGINS.join(',')}]`);
|
|
143
|
-
}),
|
|
144
|
-
className: PropTypes.string,
|
|
145
|
-
maxImageWidth: PropTypes.number,
|
|
146
|
-
maxImageHeight: PropTypes.number,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
static defaultProps = {
|
|
150
|
-
disableUnderline: true,
|
|
151
|
-
onFocus: () => {},
|
|
152
|
-
onBlur: () => {},
|
|
153
|
-
onKeyDown: () => {},
|
|
154
|
-
runSerializationOnMarkup: () => {},
|
|
155
|
-
mathMlOptions: {
|
|
156
|
-
mmlOutput: false,
|
|
157
|
-
mmlEditing: false,
|
|
158
|
-
},
|
|
159
|
-
toolbarOpts: defaultToolbarOpts,
|
|
160
|
-
responseAreaProps: defaultResponseAreaProps,
|
|
161
|
-
languageCharactersProps: defaultLanguageCharactersProps,
|
|
162
|
-
extraCSSRules: null,
|
|
163
|
-
isEditor: false,
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
constructor(props) {
|
|
167
|
-
super(props);
|
|
168
|
-
this.state = {
|
|
169
|
-
value: props.value,
|
|
170
|
-
toolbarOpts: createToolbarOpts(props.toolbarOpts, props.error),
|
|
171
|
-
pendingImages: [],
|
|
172
|
-
isHtmlMode: false,
|
|
173
|
-
isEditedInHtmlMode: false,
|
|
174
|
-
focusToolbar: false,
|
|
175
|
-
dialog: {
|
|
176
|
-
open: false,
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
this.keyPadCharacterRef = React.createRef();
|
|
181
|
-
this.doneButtonRef = React.createRef();
|
|
182
|
-
this.keypadInteractionDetected = false;
|
|
183
|
-
|
|
184
|
-
this.toggleHtmlMode = this.toggleHtmlMode.bind(this);
|
|
185
|
-
this.handleToolbarFocus = this.handleToolbarFocus.bind(this);
|
|
186
|
-
this.handleToolbarBlur = this.handleToolbarBlur.bind(this);
|
|
187
|
-
|
|
188
|
-
this.onResize = debounce(() => {
|
|
189
|
-
if (!this.state.isHtmlMode && props.onChange) {
|
|
190
|
-
props.onChange(this.state.value, true);
|
|
191
|
-
}
|
|
192
|
-
}, 50);
|
|
193
|
-
|
|
194
|
-
this.handlePlugins(this.props);
|
|
195
|
-
}
|
|
196
|
-
|
|
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) => {
|
|
226
|
-
this.setState(
|
|
227
|
-
{
|
|
228
|
-
dialog: {
|
|
229
|
-
open,
|
|
230
|
-
...extraDialogProps,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
callback,
|
|
234
|
-
);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
toggleHtmlMode = () => {
|
|
238
|
-
this.setState(
|
|
239
|
-
(prevState) => ({
|
|
240
|
-
isHtmlMode: !prevState.isHtmlMode,
|
|
241
|
-
isEditedInHtmlMode: false,
|
|
242
|
-
}),
|
|
243
|
-
() => {
|
|
244
|
-
const { error } = this.props;
|
|
245
|
-
const { toolbarOpts } = this.state;
|
|
246
|
-
const newToolbarOpts = createToolbarOpts(toolbarOpts, error, this.state.isHtmlMode);
|
|
247
|
-
this.setState({
|
|
248
|
-
toolbarOpts: newToolbarOpts,
|
|
249
|
-
});
|
|
250
|
-
},
|
|
251
|
-
);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
handlePlugins = (props) => {
|
|
255
|
-
const normalizedResponseAreaProps = {
|
|
256
|
-
...defaultResponseAreaProps,
|
|
257
|
-
...props.responseAreaProps,
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const htmlPluginOpts = {
|
|
261
|
-
currentValue: this.props.value,
|
|
262
|
-
isHtmlMode: this.state.isHtmlMode,
|
|
263
|
-
isEditedInHtmlMode: this.state.isEditedInHtmlMode,
|
|
264
|
-
toggleHtmlMode: this.toggleHtmlMode,
|
|
265
|
-
handleAlertDialog: this.handleDialog,
|
|
266
|
-
};
|
|
267
|
-
let { customPlugins } = props.pluginProps || {};
|
|
268
|
-
customPlugins = customPlugins || [];
|
|
269
|
-
|
|
270
|
-
this.plugins = buildPlugins(props.activePlugins, customPlugins, {
|
|
271
|
-
math: {
|
|
272
|
-
onClick: this.onMathClick,
|
|
273
|
-
onFocus: this.onPluginFocus,
|
|
274
|
-
onBlur: this.onPluginBlur,
|
|
275
|
-
...props.mathMlOptions,
|
|
276
|
-
},
|
|
277
|
-
textAlign: {
|
|
278
|
-
getValue: () => this.state.value,
|
|
279
|
-
onChange: this.onChange,
|
|
280
|
-
},
|
|
281
|
-
html: htmlPluginOpts,
|
|
282
|
-
extraCSSRules: props.extraCSSRules || {},
|
|
283
|
-
image: {
|
|
284
|
-
disableImageAlignmentButtons: props.disableImageAlignmentButtons,
|
|
285
|
-
onDelete:
|
|
286
|
-
props.imageSupport &&
|
|
287
|
-
props.imageSupport.delete &&
|
|
288
|
-
((node, done) => {
|
|
289
|
-
const src = node.data.get('src');
|
|
290
|
-
|
|
291
|
-
props.imageSupport.delete(src, (e) => {
|
|
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));
|
|
300
|
-
});
|
|
301
|
-
}),
|
|
302
|
-
insertImageRequested:
|
|
303
|
-
props.imageSupport &&
|
|
304
|
-
((addedImage, getHandler) => {
|
|
305
|
-
const { pendingImages } = this.state;
|
|
306
|
-
const onFinish = (result) => {
|
|
307
|
-
let cb;
|
|
308
|
-
|
|
309
|
-
if (this.state.scheduled && result) {
|
|
310
|
-
// finish editing only on success
|
|
311
|
-
cb = this.onEditingDone.bind(this);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const newPendingImages = this.state.pendingImages.filter((img) => img.key !== addedImage.key);
|
|
315
|
-
const newState = {
|
|
316
|
-
pendingImages: newPendingImages,
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
if (newPendingImages.length === 0) {
|
|
320
|
-
newState.scheduled = false;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
this.setState(newState, cb);
|
|
324
|
-
};
|
|
325
|
-
const callback = () => {
|
|
326
|
-
/**
|
|
327
|
-
* The handler is the object through which the outer context
|
|
328
|
-
* communicates file upload events like: fileChosen, cancel, progress
|
|
329
|
-
*/
|
|
330
|
-
const handler = getHandler(onFinish, () => this.state.value);
|
|
331
|
-
props.imageSupport.add(handler);
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
this.setState(
|
|
335
|
-
{
|
|
336
|
-
pendingImages: [...pendingImages, addedImage],
|
|
337
|
-
},
|
|
338
|
-
callback,
|
|
339
|
-
);
|
|
340
|
-
}),
|
|
341
|
-
onFocus: this.onPluginFocus,
|
|
342
|
-
onBlur: this.onPluginBlur,
|
|
343
|
-
maxImageWidth: props.maxImageWidth,
|
|
344
|
-
maxImageHeight: props.maxImageHeight,
|
|
345
|
-
},
|
|
346
|
-
toolbar: {
|
|
347
|
-
/**
|
|
348
|
-
* To minimize converting html -> state -> html
|
|
349
|
-
* We only emit markup once 'done' is clicked.
|
|
350
|
-
*/
|
|
351
|
-
disableScrollbar: !!props.disableScrollbar,
|
|
352
|
-
disableUnderline: props.disableUnderline,
|
|
353
|
-
autoWidth: props.autoWidthToolbar,
|
|
354
|
-
onDone: () => {
|
|
355
|
-
const { nonEmpty } = props;
|
|
356
|
-
|
|
357
|
-
log('[onDone]');
|
|
358
|
-
this.setState({ toolbarInFocus: false, focusedNode: null, focusToolbar: false });
|
|
359
|
-
this.editor.blur();
|
|
360
|
-
|
|
361
|
-
if (nonEmpty && this.state.value.startText?.text?.length === 0) {
|
|
362
|
-
this.resetValue(true).then(() => {
|
|
363
|
-
this.onEditingDone();
|
|
364
|
-
});
|
|
365
|
-
} else {
|
|
366
|
-
this.onEditingDone();
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
table: {
|
|
371
|
-
onFocus: () => {
|
|
372
|
-
log('[table:onFocus]...');
|
|
373
|
-
this.onPluginFocus();
|
|
374
|
-
},
|
|
375
|
-
onBlur: () => {
|
|
376
|
-
log('[table:onBlur]...');
|
|
377
|
-
this.onPluginBlur();
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
responseArea: {
|
|
381
|
-
type: normalizedResponseAreaProps.type,
|
|
382
|
-
options: normalizedResponseAreaProps.options,
|
|
383
|
-
maxResponseAreas: normalizedResponseAreaProps.maxResponseAreas,
|
|
384
|
-
respAreaToolbar: normalizedResponseAreaProps.respAreaToolbar,
|
|
385
|
-
onHandleAreaChange: normalizedResponseAreaProps.onHandleAreaChange,
|
|
386
|
-
error: normalizedResponseAreaProps.error,
|
|
387
|
-
onFocus: () => {
|
|
388
|
-
log('[table:onFocus]...');
|
|
389
|
-
this.onPluginFocus();
|
|
390
|
-
},
|
|
391
|
-
onBlur: () => {
|
|
392
|
-
log('[table:onBlur]...');
|
|
393
|
-
this.onPluginBlur();
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
languageCharacters: props.languageCharactersProps,
|
|
397
|
-
keyPadCharacterRef: this.keyPadCharacterRef,
|
|
398
|
-
setKeypadInteraction: this.setKeypadInteraction,
|
|
399
|
-
media: {
|
|
400
|
-
focus: this.focus,
|
|
401
|
-
createChange: () => this.state.value.change(),
|
|
402
|
-
onChange: this.onChange,
|
|
403
|
-
uploadSoundSupport: props.uploadSoundSupport,
|
|
404
|
-
},
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
if (props.mathMlOptions.mmlOutput || props.mathMlOptions.mmlEditing) {
|
|
408
|
-
this.props.runSerializationOnMarkup();
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
componentDidMount() {
|
|
413
|
-
// onRef is needed to get the ref of the component because we export it using withStyles
|
|
414
|
-
this.props.onRef(this);
|
|
415
|
-
|
|
416
|
-
window.addEventListener('resize', this.onResize);
|
|
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
|
-
|
|
428
|
-
if (this.editor && this.props.autoFocus) {
|
|
429
|
-
Promise.resolve().then(() => {
|
|
430
|
-
if (this.editor) {
|
|
431
|
-
const editorDOM = document.querySelector(`[data-key="${this.editor.value.document.key}"]`);
|
|
432
|
-
|
|
433
|
-
this.editor.focus();
|
|
434
|
-
|
|
435
|
-
if (editorDOM) {
|
|
436
|
-
editorDOM.focus();
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
444
|
-
const { isHtmlMode, toolbarOpts } = this.state;
|
|
445
|
-
const newToolbarOpts = createToolbarOpts(nextProps.toolbarOpts, nextProps.error, isHtmlMode);
|
|
446
|
-
|
|
447
|
-
if (!isEqual(newToolbarOpts, toolbarOpts)) {
|
|
448
|
-
this.setState({
|
|
449
|
-
toolbarOpts: newToolbarOpts,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const differentCharacterProps = !isEqual(nextProps.languageCharactersProps, this.props.languageCharactersProps);
|
|
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);
|
|
458
|
-
|
|
459
|
-
if (differentCharacterProps || differentMathMlProps || differentImageMaxDimensionsProps) {
|
|
460
|
-
this.handlePlugins(nextProps);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (!nextProps.value?.document?.equals(this.props.value?.document)) {
|
|
464
|
-
this.setState({
|
|
465
|
-
focus: false,
|
|
466
|
-
value: nextProps.value,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
componentDidUpdate(prevProps, prevState) {
|
|
472
|
-
// The cursor is on a zero width element and when that is placed near void elements, it is not visible
|
|
473
|
-
// so we increase the width to at least 2px in order for the user to see it
|
|
474
|
-
|
|
475
|
-
// Trigger plugins and finish editing if:
|
|
476
|
-
// 1. The 'isHtmlMode' state has been toggled.
|
|
477
|
-
// 2. We're currently in 'isHtmlMode' and the editor value has been modified.
|
|
478
|
-
if (
|
|
479
|
-
this.state.isHtmlMode !== prevState.isHtmlMode ||
|
|
480
|
-
(this.state.isHtmlMode && !prevState.isEditedInHtmlMode && this.state.isEditedInHtmlMode)
|
|
481
|
-
) {
|
|
482
|
-
this.handlePlugins(this.props);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const zeroWidthEls = document.querySelectorAll('[data-slate-zero-width="z"]');
|
|
486
|
-
|
|
487
|
-
Array.from(zeroWidthEls).forEach((el) => {
|
|
488
|
-
el.style.minWidth = '2px';
|
|
489
|
-
el.style.display = 'inline-block';
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
onPluginBlur = (e) => {
|
|
494
|
-
log('[onPluginBlur]', e && e.relatedTarget);
|
|
495
|
-
const target = e && e.relatedTarget;
|
|
496
|
-
|
|
497
|
-
const node = target ? findNode(target, this.state.value) : null;
|
|
498
|
-
log('[onPluginBlur] node: ', node);
|
|
499
|
-
this.setState({ focusedNode: node }, () => {
|
|
500
|
-
this.resetValue();
|
|
501
|
-
});
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
onPluginFocus = (e) => {
|
|
505
|
-
log('[onPluginFocus]', e && e.target);
|
|
506
|
-
const target = e && e.target;
|
|
507
|
-
if (target) {
|
|
508
|
-
const node = findNode(target, this.state.value);
|
|
509
|
-
log('[onPluginFocus] node: ', node);
|
|
510
|
-
|
|
511
|
-
const stashedValue = this.state.stashedValue || this.state.value;
|
|
512
|
-
this.setState({ focusedNode: node, stashedValue });
|
|
513
|
-
} else {
|
|
514
|
-
this.setState({ focusedNode: null });
|
|
515
|
-
}
|
|
516
|
-
this.stashValue();
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
onMathClick = (node) => {
|
|
520
|
-
this.editor.change((c) => c.collapseToStartOf(node));
|
|
521
|
-
this.setState({ selectedNode: node });
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
onEditingDone = () => {
|
|
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);
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (pendingImages.length) {
|
|
540
|
-
// schedule image processing
|
|
541
|
-
this.setState({ scheduled: true });
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Finalizing editing
|
|
546
|
-
log('[onEditingDone]');
|
|
547
|
-
this.setState({ pendingImages: [], stashedValue: null, focusedNode: null });
|
|
548
|
-
log('[onEditingDone] value: ', this.state.value);
|
|
549
|
-
this.props.onChange(this.state.value, true);
|
|
550
|
-
};
|
|
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
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Remove onResize event listener
|
|
609
|
-
*/
|
|
610
|
-
componentWillUnmount() {
|
|
611
|
-
window.removeEventListener('resize', this.onResize);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Allowing time for onChange to take effect if it is called
|
|
615
|
-
handleBlur = (resolve) => {
|
|
616
|
-
const { nonEmpty } = this.props;
|
|
617
|
-
const {
|
|
618
|
-
toolbarOpts: { doneOn },
|
|
619
|
-
} = this.state;
|
|
620
|
-
|
|
621
|
-
this.setState({ toolbarInFocus: false, focusedNode: null });
|
|
622
|
-
|
|
623
|
-
if (this.editor) {
|
|
624
|
-
this.editor.blur();
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (doneOn === 'blur') {
|
|
628
|
-
if (nonEmpty && this.state.value.startText?.text?.length === 0) {
|
|
629
|
-
this.resetValue(true).then(() => {
|
|
630
|
-
this.onEditingDone();
|
|
631
|
-
resolve();
|
|
632
|
-
});
|
|
633
|
-
} else {
|
|
634
|
-
this.onEditingDone();
|
|
635
|
-
resolve();
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
onBlur = (event) => {
|
|
641
|
-
log('[onBlur]');
|
|
642
|
-
const relatedTarget = event.relatedTarget;
|
|
643
|
-
const toolbarElement = this.toolbarRef && relatedTarget?.closest(`[class*="${this.toolbarRef.className}"]`);
|
|
644
|
-
|
|
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;
|
|
659
|
-
|
|
660
|
-
log('[onBlur] node: ', node);
|
|
661
|
-
|
|
662
|
-
return new Promise((resolve) => {
|
|
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
|
-
|
|
671
|
-
this.props.onBlur(event);
|
|
672
|
-
});
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
handleDomBlur = (e) => {
|
|
676
|
-
const editorDOM = document.querySelector(`[data-key="${this.state.value.document.key}"]`);
|
|
677
|
-
|
|
678
|
-
setTimeout(() => {
|
|
679
|
-
const { value: stateValue } = this.state;
|
|
680
|
-
|
|
681
|
-
if (!this.wrapperRef) {
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const editorElement = !editorDOM || document.activeElement.closest(`[class*="${editorDOM.className}"]`);
|
|
686
|
-
const toolbarElement =
|
|
687
|
-
!this.toolbarRef || document.activeElement.closest(`[class*="${this.toolbarRef.className}"]`);
|
|
688
|
-
const isInCurrentComponent = this.wrapperRef.contains(editorElement) || this.wrapperRef.contains(toolbarElement);
|
|
689
|
-
|
|
690
|
-
if (!isInCurrentComponent) {
|
|
691
|
-
editorDOM.removeEventListener('blur', this.handleDomBlur);
|
|
692
|
-
|
|
693
|
-
if (stateValue.isFocused) {
|
|
694
|
-
this.onBlur(e);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}, 50);
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
/*
|
|
701
|
-
* Needs to be wrapped otherwise it causes issues because of race conditions
|
|
702
|
-
* Known issue for slatejs. See: https://github.com/ianstormtaylor/slate/issues/2097
|
|
703
|
-
* Using timeout I wasn't able to test this
|
|
704
|
-
*
|
|
705
|
-
* Note: The use of promises has been causing issues with MathQuill
|
|
706
|
-
* */
|
|
707
|
-
onFocus = (event, change) =>
|
|
708
|
-
new Promise((resolve) => {
|
|
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);
|
|
712
|
-
|
|
713
|
-
log('[onFocus]', document.activeElement);
|
|
714
|
-
|
|
715
|
-
if (this.keypadInteractionDetected && this.__TEMPORARY_CHANGE_DATA) {
|
|
716
|
-
this.__TEMPORARY_CHANGE_DATA = null;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* This is a temporary hack - @see changeData below for some more information.
|
|
721
|
-
*/
|
|
722
|
-
if (this.__TEMPORARY_CHANGE_DATA) {
|
|
723
|
-
const { key, data } = this.__TEMPORARY_CHANGE_DATA;
|
|
724
|
-
const domEl = document.querySelector(`[data-key="${key}"]`);
|
|
725
|
-
|
|
726
|
-
if (domEl) {
|
|
727
|
-
let change = this.state.value.change().setNodeByKey(key, { data });
|
|
728
|
-
this.setState({ value: change.value }, () => {
|
|
729
|
-
this.__TEMPORARY_CHANGE_DATA = null;
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* This is needed just in case the browser decides to make the editor
|
|
736
|
-
* lose focus without triggering the onBlur event (can happen in a few cases).
|
|
737
|
-
* This will also trigger onBlur if the user clicks outside of the page when the editor
|
|
738
|
-
* is focused.
|
|
739
|
-
*/
|
|
740
|
-
if (editorDOM === document.activeElement) {
|
|
741
|
-
editorDOM.removeEventListener('blur', this.handleDomBlur);
|
|
742
|
-
editorDOM.addEventListener('blur', this.handleDomBlur);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
this.stashValue();
|
|
746
|
-
this.props.onFocus();
|
|
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
|
-
|
|
755
|
-
resolve();
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
stashValue = () => {
|
|
759
|
-
log('[stashValue]');
|
|
760
|
-
|
|
761
|
-
if (!this.state.stashedValue) {
|
|
762
|
-
this.setState({ stashedValue: this.state.value });
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* Reset the value if the user didn't click done.
|
|
768
|
-
*/
|
|
769
|
-
resetValue = (force) => {
|
|
770
|
-
const { value, focusedNode } = this.state;
|
|
771
|
-
|
|
772
|
-
const stopReset = this.plugins.reduce((s, p) => {
|
|
773
|
-
return s || (p.stopReset && p.stopReset(this.state.value));
|
|
774
|
-
}, false);
|
|
775
|
-
|
|
776
|
-
log('[resetValue]', value.isFocused, focusedNode, 'stopReset: ', stopReset);
|
|
777
|
-
if ((this.state.stashedValue && !value.isFocused && !focusedNode && !stopReset) || force) {
|
|
778
|
-
log('[resetValue] resetting...');
|
|
779
|
-
log('stashed', this.state.stashedValue.document.toObject());
|
|
780
|
-
log('current', this.state.value.document.toObject());
|
|
781
|
-
|
|
782
|
-
const newValue = Value.fromJSON(this.state.stashedValue.toJSON());
|
|
783
|
-
|
|
784
|
-
log('newValue: ', newValue.document);
|
|
785
|
-
return new Promise((resolve) => {
|
|
786
|
-
setTimeout(() => {
|
|
787
|
-
this.setState({ value: newValue, stashedValue: null }, () => {
|
|
788
|
-
log('value now: ', this.state.value.document.toJSON());
|
|
789
|
-
resolve();
|
|
790
|
-
});
|
|
791
|
-
}, 50);
|
|
792
|
-
});
|
|
793
|
-
} else {
|
|
794
|
-
return Promise.resolve({});
|
|
795
|
-
}
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
onChange = (change, done) => {
|
|
799
|
-
log('[onChange]');
|
|
800
|
-
window.me = this;
|
|
801
|
-
|
|
802
|
-
const { value } = change;
|
|
803
|
-
const { charactersLimit } = this.props;
|
|
804
|
-
let limit = charactersLimit;
|
|
805
|
-
if (!limit || limit > MAX_CHARACTERS_LIMIT) {
|
|
806
|
-
limit = MAX_CHARACTERS_LIMIT;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (value && value.document && value.document.text && value.document.text.length > limit) {
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Mark the editor as edited when in HTML mode and its content has changed.
|
|
814
|
-
// This status will later be used to decide whether to prompt a warning to the user when exiting HTML mode.
|
|
815
|
-
const isEditedInHtmlMode = !this.state.isHtmlMode
|
|
816
|
-
? false
|
|
817
|
-
: this.state.value.document.text !== value.document.text
|
|
818
|
-
? true
|
|
819
|
-
: this.state.isEditedInHtmlMode;
|
|
820
|
-
|
|
821
|
-
if (isEditedInHtmlMode != this.state.isEditedInHtmlMode) {
|
|
822
|
-
this.handlePlugins(this.props);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
this.setState({ value, isEditedInHtmlMode }, () => {
|
|
826
|
-
log('[onChange], call done()');
|
|
827
|
-
|
|
828
|
-
if (done) {
|
|
829
|
-
done();
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
getFocusedValue = () => {
|
|
835
|
-
if (this.state.value.isFocused) {
|
|
836
|
-
return this.state.value;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return this.state.preBlurValue;
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
valueToSize = (v) => {
|
|
843
|
-
if (!v) {
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
const calcRegex = /^calc\((.*)\)$/;
|
|
847
|
-
|
|
848
|
-
if (typeof v === 'string') {
|
|
849
|
-
if (v.endsWith('%')) {
|
|
850
|
-
return undefined;
|
|
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
|
-
) {
|
|
859
|
-
return v;
|
|
860
|
-
} else {
|
|
861
|
-
const value = parseInt(v, 10);
|
|
862
|
-
return isNaN(value) ? value : `${value}px`;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
if (typeof v === 'number') {
|
|
866
|
-
return `${v}px`;
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
buildSizeStyle() {
|
|
871
|
-
const { minWidth, width, maxWidth, minHeight, height, maxHeight } = this.props;
|
|
872
|
-
|
|
873
|
-
return {
|
|
874
|
-
width: this.valueToSize(width),
|
|
875
|
-
minWidth: this.valueToSize(minWidth),
|
|
876
|
-
maxWidth: this.valueToSize(maxWidth),
|
|
877
|
-
height: this.valueToSize(height),
|
|
878
|
-
minHeight: this.valueToSize(minHeight),
|
|
879
|
-
maxHeight: this.valueToSize(maxHeight),
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
validateNode = (node) => {
|
|
884
|
-
if (node.object !== 'block') return;
|
|
885
|
-
|
|
886
|
-
const last = node.nodes.last();
|
|
887
|
-
if (!last) return;
|
|
888
|
-
|
|
889
|
-
if (last.type !== 'image') return;
|
|
890
|
-
|
|
891
|
-
log('[validateNode] last is image..');
|
|
892
|
-
|
|
893
|
-
const parent = last.getParent(last.key);
|
|
894
|
-
const p = Block.getParent(last.key);
|
|
895
|
-
log('[validateNode] parent:', parent, p);
|
|
896
|
-
|
|
897
|
-
return undefined;
|
|
898
|
-
};
|
|
899
|
-
|
|
900
|
-
changeData = (key, data) => {
|
|
901
|
-
log('[changeData]. .. ', key, data);
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* HACK ALERT: We should be calling setState here and storing the change data:
|
|
905
|
-
*
|
|
906
|
-
* <code>this.setState({changeData: { key, data}})</code>
|
|
907
|
-
* However this is causing issues with the Mathquill instance. The 'input' event stops firing on the element and no
|
|
908
|
-
* more changes get through. The issues seem to be related to the promises in onBlur/onFocus. But removing these
|
|
909
|
-
* brings it's own problems. A major clean up is planned for this component so I've decided to temporarily settle
|
|
910
|
-
* on this hack rather than spend more time on this.
|
|
911
|
-
*/
|
|
912
|
-
|
|
913
|
-
// Uncomment this line to see the bug described above.
|
|
914
|
-
// this.setState({changeData: {key, data}})
|
|
915
|
-
|
|
916
|
-
this.__TEMPORARY_CHANGE_DATA = { key, data };
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
focus = (pos, node) => {
|
|
920
|
-
const position = pos || 'end';
|
|
921
|
-
|
|
922
|
-
this.props.focus(position, node);
|
|
923
|
-
};
|
|
924
|
-
|
|
925
|
-
onDropPaste = async (event, change, dropContext) => {
|
|
926
|
-
const editor = change.editor;
|
|
927
|
-
const transfer = getEventTransfer(event);
|
|
928
|
-
const file = transfer.files && transfer.files[0];
|
|
929
|
-
|
|
930
|
-
const type = transfer.type;
|
|
931
|
-
const fragment = transfer.fragment;
|
|
932
|
-
const text = transfer.text;
|
|
933
|
-
|
|
934
|
-
if (file && (file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png')) {
|
|
935
|
-
if (!this.props.imageSupport) {
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
try {
|
|
939
|
-
log('[onDropPaste]');
|
|
940
|
-
const src = await getBase64(file);
|
|
941
|
-
const inline = Inline.create({
|
|
942
|
-
type: 'image',
|
|
943
|
-
isVoid: true,
|
|
944
|
-
data: {
|
|
945
|
-
loading: false,
|
|
946
|
-
src,
|
|
947
|
-
},
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
if (dropContext) {
|
|
951
|
-
this.focus();
|
|
952
|
-
} else {
|
|
953
|
-
const range = getEventRange(event, editor);
|
|
954
|
-
if (range) {
|
|
955
|
-
change.select(range);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const ch = change.insertInline(inline);
|
|
960
|
-
this.onChange(ch);
|
|
961
|
-
const handler = new InsertImageHandler(
|
|
962
|
-
inline,
|
|
963
|
-
() => {},
|
|
964
|
-
() => this.state.value,
|
|
965
|
-
this.onChange,
|
|
966
|
-
true,
|
|
967
|
-
);
|
|
968
|
-
handler.fileChosen(file);
|
|
969
|
-
this.props.imageSupport.add(handler);
|
|
970
|
-
} catch (err) {
|
|
971
|
-
log('[onDropPaste] error: ', err);
|
|
972
|
-
}
|
|
973
|
-
} else if (type === 'fragment') {
|
|
974
|
-
change.insertFragment(fragment);
|
|
975
|
-
} else if (type === 'text' || type === 'html') {
|
|
976
|
-
if (!text) {
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
const {
|
|
980
|
-
value: { document, selection, startBlock },
|
|
981
|
-
} = change;
|
|
982
|
-
|
|
983
|
-
if (startBlock.isVoid) {
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const defaultBlock = startBlock;
|
|
988
|
-
const defaultMarks = document.getInsertMarksAtRange(selection);
|
|
989
|
-
const frag = Plain.deserialize(text, {
|
|
990
|
-
defaultBlock,
|
|
991
|
-
defaultMarks,
|
|
992
|
-
}).document;
|
|
993
|
-
change.insertFragment(frag);
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
renderPlaceholder = (props) => {
|
|
998
|
-
const { editor } = props;
|
|
999
|
-
const { document } = editor.value;
|
|
1000
|
-
|
|
1001
|
-
if (!editor.props.placeholder || document.text !== '' || document.nodes.size !== 1 || !document.isEmpty) {
|
|
1002
|
-
return false;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
return (
|
|
1006
|
-
<span
|
|
1007
|
-
contentEditable={false}
|
|
1008
|
-
style={{
|
|
1009
|
-
display: 'inline-block',
|
|
1010
|
-
width: 'fit-content', // for centering the placeholder if text-align is set to center
|
|
1011
|
-
maxWidth: '100%',
|
|
1012
|
-
whiteSpace: 'nowrap',
|
|
1013
|
-
opacity: '0.33',
|
|
1014
|
-
pointerEvents: 'none',
|
|
1015
|
-
userSelect: 'none',
|
|
1016
|
-
}}
|
|
1017
|
-
>
|
|
1018
|
-
{editor.props.placeholder}
|
|
1019
|
-
</span>
|
|
1020
|
-
);
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
render() {
|
|
1024
|
-
const {
|
|
1025
|
-
disabled,
|
|
1026
|
-
spellCheck,
|
|
1027
|
-
highlightShape,
|
|
1028
|
-
classes,
|
|
1029
|
-
className,
|
|
1030
|
-
isEditor,
|
|
1031
|
-
placeholder,
|
|
1032
|
-
pluginProps,
|
|
1033
|
-
onKeyDown,
|
|
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 || {};
|
|
1040
|
-
|
|
1041
|
-
const { value, focusedNode, toolbarOpts, dialog, scheduled } = this.state;
|
|
1042
|
-
|
|
1043
|
-
log('[render] value: ', value);
|
|
1044
|
-
const sizeStyle = this.buildSizeStyle();
|
|
1045
|
-
const names = classNames(
|
|
1046
|
-
{
|
|
1047
|
-
[classes.withBg]: highlightShape,
|
|
1048
|
-
[classes.toolbarOnTop]: toolbarOpts.alwaysVisible && toolbarOpts.position === 'top',
|
|
1049
|
-
[classes.scheduled]: scheduled,
|
|
1050
|
-
},
|
|
1051
|
-
className,
|
|
1052
|
-
);
|
|
1053
|
-
|
|
1054
|
-
return (
|
|
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
|
-
>
|
|
1061
|
-
{scheduled && <div className={classes.uploading}>Uploading image and then saving...</div>}
|
|
1062
|
-
<SlateEditor
|
|
1063
|
-
plugins={this.plugins}
|
|
1064
|
-
innerRef={(r) => {
|
|
1065
|
-
if (r) {
|
|
1066
|
-
this.slateEditor = r;
|
|
1067
|
-
}
|
|
1068
|
-
}}
|
|
1069
|
-
ref={(r) => (this.editor = r && this.props.editorRef(r))}
|
|
1070
|
-
toolbarRef={(r) => {
|
|
1071
|
-
if (r) {
|
|
1072
|
-
this.toolbarRef = r;
|
|
1073
|
-
}
|
|
1074
|
-
}}
|
|
1075
|
-
doneButtonRef={this.doneButtonRef}
|
|
1076
|
-
value={value}
|
|
1077
|
-
focusToolbar={this.state.focusToolbar}
|
|
1078
|
-
onToolbarFocus={this.handleToolbarFocus}
|
|
1079
|
-
onToolbarBlur={this.handleToolbarBlur}
|
|
1080
|
-
focus={this.focus}
|
|
1081
|
-
onKeyDown={onKeyDown}
|
|
1082
|
-
onChange={this.onChange}
|
|
1083
|
-
getFocusedValue={this.getFocusedValue}
|
|
1084
|
-
onBlur={this.onBlur}
|
|
1085
|
-
onDrop={(event, editor) => this.onDropPaste(event, editor, true)}
|
|
1086
|
-
onPaste={(event, editor) => this.onDropPaste(event, editor)}
|
|
1087
|
-
onFocus={this.onFocus}
|
|
1088
|
-
onEditingDone={this.onEditingDone}
|
|
1089
|
-
focusedNode={focusedNode}
|
|
1090
|
-
normalize={this.normalize}
|
|
1091
|
-
readOnly={disabled}
|
|
1092
|
-
spellCheck={spellCheck}
|
|
1093
|
-
autoCorrect={spellCheck}
|
|
1094
|
-
className={classNames(
|
|
1095
|
-
{
|
|
1096
|
-
[classes.noPadding]: toolbarOpts?.noPadding,
|
|
1097
|
-
[classes.showParagraph]: showParagraphs && !showParagraphs.disabled,
|
|
1098
|
-
[classes.separateParagraph]: separateParagraphs && !separateParagraphs.disabled,
|
|
1099
|
-
},
|
|
1100
|
-
classes.slateEditor,
|
|
1101
|
-
)}
|
|
1102
|
-
style={{
|
|
1103
|
-
minHeight: sizeStyle.minHeight,
|
|
1104
|
-
height: sizeStyle.height,
|
|
1105
|
-
maxHeight: sizeStyle.maxHeight,
|
|
1106
|
-
}}
|
|
1107
|
-
pluginProps={otherPluginProps}
|
|
1108
|
-
toolbarOpts={toolbarOpts}
|
|
1109
|
-
placeholder={placeholder}
|
|
1110
|
-
renderPlaceholder={this.renderPlaceholder}
|
|
1111
|
-
onDataChange={this.changeData}
|
|
1112
|
-
/>
|
|
1113
|
-
<AlertDialog
|
|
1114
|
-
open={dialog.open}
|
|
1115
|
-
title={dialog.title}
|
|
1116
|
-
text={dialog.text}
|
|
1117
|
-
onClose={dialog.onClose}
|
|
1118
|
-
onConfirm={dialog.onConfirm}
|
|
1119
|
-
onConfirmText={dialog.onConfirmText}
|
|
1120
|
-
onCloseText={dialog.onCloseText}
|
|
1121
|
-
/>
|
|
1122
|
-
</div>
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// TODO color - hardcoded gray background and keypad colors will need to change too
|
|
1128
|
-
const styles = {
|
|
1129
|
-
withBg: {
|
|
1130
|
-
backgroundColor: 'rgba(0,0,0,0.06)',
|
|
1131
|
-
},
|
|
1132
|
-
scheduled: {
|
|
1133
|
-
opacity: 0.5,
|
|
1134
|
-
pointerEvents: 'none',
|
|
1135
|
-
position: 'relative',
|
|
1136
|
-
},
|
|
1137
|
-
uploading: {
|
|
1138
|
-
position: 'absolute',
|
|
1139
|
-
top: '50%',
|
|
1140
|
-
left: '50%',
|
|
1141
|
-
transform: 'translate(-50%, -50%)',
|
|
1142
|
-
},
|
|
1143
|
-
slateEditor: {
|
|
1144
|
-
fontFamily: 'Roboto, sans-serif',
|
|
1145
|
-
|
|
1146
|
-
'& table': {
|
|
1147
|
-
tableLayout: 'fixed',
|
|
1148
|
-
width: '100%',
|
|
1149
|
-
borderCollapse: 'collapse',
|
|
1150
|
-
color: color.text(),
|
|
1151
|
-
backgroundColor: color.background(),
|
|
1152
|
-
},
|
|
1153
|
-
'& table:not([border="1"]) tr': {
|
|
1154
|
-
borderTop: '1px solid #dfe2e5',
|
|
1155
|
-
// TODO perhaps secondary color for background, for now disable
|
|
1156
|
-
// '&:nth-child(2n)': {
|
|
1157
|
-
// backgroundColor: '#f6f8fa'
|
|
1158
|
-
// }
|
|
1159
|
-
},
|
|
1160
|
-
'& td, th': {
|
|
1161
|
-
padding: '.6em 1em',
|
|
1162
|
-
textAlign: 'center',
|
|
1163
|
-
},
|
|
1164
|
-
'& table:not([border="1"]) td, th': {
|
|
1165
|
-
border: '1px solid #dfe2e5',
|
|
1166
|
-
},
|
|
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
|
-
},
|
|
1183
|
-
toolbarOnTop: {
|
|
1184
|
-
marginTop: '45px',
|
|
1185
|
-
},
|
|
1186
|
-
noPadding: {
|
|
1187
|
-
padding: '0 !important',
|
|
1188
|
-
},
|
|
1189
|
-
previewText: {
|
|
1190
|
-
marginBottom: '36px',
|
|
1191
|
-
marginTop: '6px',
|
|
1192
|
-
padding: '20px',
|
|
1193
|
-
backgroundColor: 'rgba(0,0,0,0.06)',
|
|
1194
|
-
},
|
|
1195
|
-
};
|
|
1196
|
-
|
|
1197
|
-
export default withStyles(styles)(Editor);
|