@pie-lib/editable-html-tip-tap 1.2.0-next.9 → 2.0.1
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 +176 -0
- package/lib/components/CharacterPicker.js +1 -0
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +84 -43
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +74 -43
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/TiptapContainer.js +9 -8
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/icons/TextAlign.js +2 -2
- package/lib/components/icons/TextAlign.js.map +1 -1
- package/lib/components/image/InsertImageHandler.js +10 -13
- package/lib/components/image/InsertImageHandler.js.map +1 -1
- package/lib/components/media/MediaDialog.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +6 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/choice.js +15 -7
- package/lib/components/respArea/DragInTheBlank/choice.js.map +1 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js +29 -11
- package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +35 -6
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/extensions/custom-toolbar-wrapper.js +3 -2
- package/lib/extensions/custom-toolbar-wrapper.js.map +1 -1
- package/lib/extensions/div-node.js +83 -0
- package/lib/extensions/div-node.js.map +1 -0
- package/lib/extensions/ensure-empty-root-div.js +48 -0
- package/lib/extensions/ensure-empty-root-div.js.map +1 -0
- package/lib/extensions/ensure-list-item-content-is-div.js +64 -0
- package/lib/extensions/ensure-list-item-content-is-div.js.map +1 -0
- package/lib/extensions/extended-list-item.js +15 -0
- package/lib/extensions/extended-list-item.js.map +1 -0
- package/lib/extensions/extended-table-cell.js +22 -0
- package/lib/extensions/extended-table-cell.js.map +1 -0
- package/lib/extensions/extended-table.js +50 -1
- package/lib/extensions/extended-table.js.map +1 -1
- package/lib/extensions/image-component.js +102 -51
- package/lib/extensions/image-component.js.map +1 -1
- package/lib/extensions/image.js +51 -2
- package/lib/extensions/image.js.map +1 -1
- package/lib/extensions/math.js +50 -9
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/media.js +3 -1
- package/lib/extensions/media.js.map +1 -1
- package/lib/extensions/responseArea.js +12 -7
- package/lib/extensions/responseArea.js.map +1 -1
- package/lib/styles/editorContainerStyles.js +5 -4
- package/lib/styles/editorContainerStyles.js.map +1 -1
- package/lib/utils/helper.js +17 -0
- package/lib/utils/helper.js.map +1 -0
- package/package.json +8 -8
- package/src/__tests__/EditableHtml.test.jsx +90 -7
- package/src/__tests__/index.test.jsx +11 -3
- package/src/components/CharacterPicker.jsx +1 -0
- package/src/components/EditableHtml.jsx +91 -41
- package/src/components/MenuBar.jsx +57 -24
- package/src/components/TiptapContainer.jsx +10 -8
- package/src/components/__tests__/CharacterPicker.test.jsx +22 -0
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +55 -12
- package/src/components/__tests__/InlineDropdown.test.jsx +203 -10
- package/src/components/__tests__/InsertImageHandler.test.js +28 -21
- package/src/components/__tests__/MenuBar.test.jsx +32 -0
- package/src/components/icons/TextAlign.jsx +1 -1
- package/src/components/image/InsertImageHandler.js +9 -13
- package/src/components/respArea/DragInTheBlank/DragInTheBlank.jsx +6 -1
- package/src/components/respArea/DragInTheBlank/choice.jsx +32 -4
- package/src/components/respArea/ExplicitConstructedResponse.jsx +33 -10
- package/src/components/respArea/InlineDropdown.jsx +45 -10
- package/src/extensions/__tests__/divNode.test.js +87 -0
- package/src/extensions/__tests__/ensure-empty-root-div.test.js +57 -0
- package/src/extensions/__tests__/ensure-list-item-content-is-div.test.js +44 -0
- package/src/extensions/__tests__/extended-list-item.test.js +13 -0
- package/src/extensions/__tests__/extended-table-cell.test.js +22 -0
- package/src/extensions/__tests__/extended-table.test.js +98 -1
- package/src/extensions/__tests__/image-component.test.jsx +105 -9
- package/src/extensions/__tests__/image.test.js +109 -8
- package/src/extensions/__tests__/math.test.js +348 -0
- package/src/extensions/__tests__/media-node-view.test.jsx +10 -8
- package/src/extensions/__tests__/responseArea.test.js +291 -0
- package/src/extensions/custom-toolbar-wrapper.jsx +2 -2
- package/src/extensions/div-node.js +86 -0
- package/src/extensions/ensure-empty-root-div.js +47 -0
- package/src/extensions/ensure-list-item-content-is-div.js +62 -0
- package/src/extensions/extended-list-item.js +10 -0
- package/src/extensions/extended-table-cell.js +19 -0
- package/src/extensions/extended-table.js +37 -1
- package/src/extensions/image-component.jsx +114 -69
- package/src/extensions/image.js +56 -1
- package/src/extensions/math.js +62 -10
- package/src/extensions/media.js +1 -1
- package/src/extensions/responseArea.js +13 -11
- package/src/styles/editorContainerStyles.js +5 -4
- package/src/utils/helper.js +17 -0
- /package/src/components/media/{MediaDialog.js → MediaDialog.jsx} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import isEqual from 'lodash-es/isEqual';
|
|
4
4
|
import debug from 'debug';
|
|
5
5
|
import LinearProgress from '@mui/material/LinearProgress';
|
|
6
6
|
import { styled } from '@mui/material/styles';
|
|
7
7
|
import { NodeViewWrapper } from '@tiptap/react';
|
|
8
|
+
import ReactDOM from 'react-dom';
|
|
8
9
|
import InsertImageHandler from '../components/image/InsertImageHandler';
|
|
9
10
|
import ImageToolbar from '../components/image/ImageToolbar';
|
|
10
11
|
import CustomToolbarWrapper from './custom-toolbar-wrapper';
|
|
@@ -26,9 +27,8 @@ const StyledProgress = styled(LinearProgress, {
|
|
|
26
27
|
|
|
27
28
|
const StyledRoot = styled('div', {
|
|
28
29
|
shouldForwardProp: (prop) => !['active', 'loading', 'pendingDelete'].includes(prop),
|
|
29
|
-
})(({
|
|
30
|
+
})(({ loading, pendingDelete }) => ({
|
|
30
31
|
position: 'relative',
|
|
31
|
-
border: active ? `solid 1px ${theme.palette.primary.main}` : `solid 1px ${theme.palette.common.white}`,
|
|
32
32
|
display: 'flex',
|
|
33
33
|
transition: 'opacity 200ms linear',
|
|
34
34
|
...(loading && {
|
|
@@ -49,6 +49,12 @@ const StyledImageContainer = styled('div')(({ theme }) => ({
|
|
|
49
49
|
},
|
|
50
50
|
}));
|
|
51
51
|
|
|
52
|
+
const StyledImage = styled('img', {
|
|
53
|
+
shouldForwardProp: (prop) => prop !== 'active',
|
|
54
|
+
})(({ theme, active }) => ({
|
|
55
|
+
border: active ? `solid 1px ${theme.palette.primary.main}` : 'solid 1px transparent',
|
|
56
|
+
}));
|
|
57
|
+
|
|
52
58
|
const StyledResize = styled('div')(({ theme }) => ({
|
|
53
59
|
backgroundColor: theme.palette.primary.main,
|
|
54
60
|
cursor: 'col-resize',
|
|
@@ -68,15 +74,18 @@ function ImageComponent(props) {
|
|
|
68
74
|
editor,
|
|
69
75
|
attributes,
|
|
70
76
|
onFocus,
|
|
77
|
+
getPos,
|
|
71
78
|
selected,
|
|
72
79
|
options,
|
|
73
80
|
maxImageWidth = 700,
|
|
74
81
|
maxImageHeight = 900,
|
|
75
|
-
latex,
|
|
76
|
-
handleChange,
|
|
77
|
-
handleDone,
|
|
78
82
|
} = props;
|
|
79
83
|
const { alt } = node.attrs;
|
|
84
|
+
const pos = getPos();
|
|
85
|
+
|
|
86
|
+
const selFrom = editor.state.selection.from;
|
|
87
|
+
const selTo = editor.state.selection.to;
|
|
88
|
+
const onlyThisNodeSelected = useMemo(() => selFrom + node.nodeSize === selTo, [selFrom, selTo, node.nodeSize]);
|
|
80
89
|
|
|
81
90
|
const [showToolbar, setShowToolbar] = useState(false);
|
|
82
91
|
|
|
@@ -90,39 +99,66 @@ function ImageComponent(props) {
|
|
|
90
99
|
return parseInt(floored.toFixed(0) * 25, 10);
|
|
91
100
|
}, []);
|
|
92
101
|
|
|
102
|
+
const findNodePos = useCallback(() => {
|
|
103
|
+
const key = latestNodeRef.current.attrs.nodeKey;
|
|
104
|
+
let found = null;
|
|
105
|
+
editor.state.doc.descendants((n, pos) => {
|
|
106
|
+
if (found !== null) return false;
|
|
107
|
+
if (n.type.name === 'imageUploadNode' && n.attrs.nodeKey === key) {
|
|
108
|
+
found = pos;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return found;
|
|
113
|
+
}, [editor]);
|
|
114
|
+
|
|
115
|
+
// dispatch an attribute update targeted precisely at this node by nodeKey.
|
|
116
|
+
const updateThisNode = useCallback(
|
|
117
|
+
(newAttrs) => {
|
|
118
|
+
const nodePos = findNodePos();
|
|
119
|
+
if (nodePos === null) return;
|
|
120
|
+
const currentNode = editor.state.doc.nodeAt(nodePos);
|
|
121
|
+
if (!currentNode) return;
|
|
122
|
+
editor.view.dispatch(editor.state.tr.setNodeMarkup(nodePos, undefined, { ...currentNode.attrs, ...newAttrs }));
|
|
123
|
+
},
|
|
124
|
+
[editor, findNodePos],
|
|
125
|
+
);
|
|
126
|
+
|
|
93
127
|
const applySizeData = useCallback(() => {
|
|
94
128
|
if (!node.attrs.width || !imgRef.current) return;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
if (!isEqual(update, node.attrs)) {
|
|
102
|
-
editor.commands.updateAttributes('imageUploadNode', update);
|
|
103
|
-
}
|
|
104
|
-
}, [editor, node.attrs, getPercentFromWidth]);
|
|
129
|
+
const resizePercent = getPercentFromWidth(node.attrs.width);
|
|
130
|
+
if (node.attrs.resizePercent === resizePercent) return;
|
|
131
|
+
updateThisNode({ resizePercent });
|
|
132
|
+
}, [node.attrs.width, node.attrs.resizePercent, getPercentFromWidth, updateThisNode]);
|
|
105
133
|
|
|
106
134
|
// keep ref in sync with latest node
|
|
107
135
|
useEffect(() => {
|
|
108
|
-
latestNodeRef.current = node;
|
|
109
|
-
}, [node]);
|
|
136
|
+
latestNodeRef.current = { ...node, pos };
|
|
137
|
+
}, [node, pos]);
|
|
110
138
|
|
|
111
139
|
useEffect(() => {
|
|
112
|
-
const { selection } = editor.state;
|
|
113
|
-
const onlyThisNodeSelected = selection.from + node.nodeSize === selection.to;
|
|
114
|
-
|
|
115
140
|
if (selected) {
|
|
116
141
|
if (onlyThisNodeSelected) {
|
|
142
|
+
// Only open the upload UI for a fresh placeholder. Remounting after tab switch
|
|
143
|
+
// would otherwise call insertImageRequested again and reopen the file modal.
|
|
144
|
+
const hasImageSrc = String(node.attrs?.src ?? '').trim();
|
|
145
|
+
|
|
146
|
+
if (!hasImageSrc && options.imageHandling?.insertImageRequested) {
|
|
147
|
+
options.imageHandling.insertImageRequested(
|
|
148
|
+
editor,
|
|
149
|
+
[node, pos],
|
|
150
|
+
(finish) => new InsertImageHandler(editor, [node, pos], finish),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
117
154
|
setShowToolbar(selected);
|
|
118
155
|
}
|
|
119
156
|
} else {
|
|
120
157
|
setShowToolbar(selected);
|
|
121
158
|
}
|
|
122
|
-
}, [
|
|
159
|
+
}, [onlyThisNodeSelected, selected]);
|
|
123
160
|
|
|
124
161
|
useEffect(() => {
|
|
125
|
-
options.imageHandling.insertImageRequested(node, (finish) => new InsertImageHandler(editor, node, finish));
|
|
126
162
|
applySizeData();
|
|
127
163
|
|
|
128
164
|
const resizeHandle = resizeRef.current;
|
|
@@ -133,8 +169,6 @@ function ImageComponent(props) {
|
|
|
133
169
|
if (resizeHandle) {
|
|
134
170
|
resizeHandle.removeEventListener('mousedown', initResize, false);
|
|
135
171
|
}
|
|
136
|
-
|
|
137
|
-
options.imageHandling.onDelete(latestNodeRef.current);
|
|
138
172
|
};
|
|
139
173
|
}, []);
|
|
140
174
|
|
|
@@ -154,11 +188,11 @@ function ImageComponent(props) {
|
|
|
154
188
|
box.style.height = `${h}px`;
|
|
155
189
|
|
|
156
190
|
const update = { width: w, height: h };
|
|
157
|
-
if (!isEqual(update, node.attrs)) {
|
|
158
|
-
|
|
191
|
+
if (!isEqual(update, { width: node.attrs.width, height: node.attrs.height })) {
|
|
192
|
+
updateThisNode(update);
|
|
159
193
|
}
|
|
160
194
|
}
|
|
161
|
-
}, [
|
|
195
|
+
}, [node.attrs.width, node.attrs.height, maxImageWidth, maxImageHeight, updateThisNode]);
|
|
162
196
|
|
|
163
197
|
const updateAspect = (initial, next, keepAspect = true, resizeType) => {
|
|
164
198
|
if (keepAspect) {
|
|
@@ -186,20 +220,17 @@ function ImageComponent(props) {
|
|
|
186
220
|
box.style.width = `${next.width}px`;
|
|
187
221
|
box.style.height = `${next.height}px`;
|
|
188
222
|
|
|
189
|
-
|
|
190
|
-
if (!isEqual(update, node.attrs)) {
|
|
191
|
-
editor.commands.updateAttributes('imageUploadNode', update);
|
|
192
|
-
}
|
|
223
|
+
updateThisNode({ width: next.width, height: next.height });
|
|
193
224
|
}
|
|
194
225
|
},
|
|
195
|
-
[editor,
|
|
226
|
+
[editor, updateThisNode],
|
|
196
227
|
);
|
|
197
228
|
|
|
198
229
|
const onChange = useCallback(
|
|
199
230
|
(newValues) => {
|
|
200
|
-
|
|
231
|
+
updateThisNode(newValues);
|
|
201
232
|
},
|
|
202
|
-
[editor],
|
|
233
|
+
[editor, updateThisNode],
|
|
203
234
|
);
|
|
204
235
|
|
|
205
236
|
const stopResize = useCallback(() => {
|
|
@@ -224,16 +255,17 @@ function ImageComponent(props) {
|
|
|
224
255
|
<NodeViewWrapper>
|
|
225
256
|
<StyledRoot
|
|
226
257
|
onFocus={onFocus}
|
|
227
|
-
active={selected}
|
|
228
258
|
loading={!node.attrs.loaded}
|
|
229
259
|
pendingDelete={node.attrs.deleteStatus === 'pending'}
|
|
230
260
|
style={{ justifyContent: flexAlign }}
|
|
231
261
|
>
|
|
232
262
|
<StyledProgress mode="determinate" value={node.attrs.percent || 0} hideProgress={node.attrs.loaded} />
|
|
233
263
|
|
|
234
|
-
<StyledImageContainer>
|
|
235
|
-
<
|
|
264
|
+
<StyledImageContainer onDragStart={(e) => e.preventDefault()}>
|
|
265
|
+
<StyledImage
|
|
236
266
|
{...attributes}
|
|
267
|
+
active={selected && node.attrs.loaded}
|
|
268
|
+
draggable={false}
|
|
237
269
|
ref={imgRef}
|
|
238
270
|
src={node.attrs.src}
|
|
239
271
|
style={style}
|
|
@@ -244,39 +276,52 @@ function ImageComponent(props) {
|
|
|
244
276
|
</StyledImageContainer>
|
|
245
277
|
</StyledRoot>
|
|
246
278
|
|
|
247
|
-
{showToolbar &&
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
width: '100%',
|
|
259
|
-
}}
|
|
260
|
-
>
|
|
261
|
-
<CustomToolbarWrapper
|
|
262
|
-
showDone
|
|
263
|
-
{...options}
|
|
264
|
-
onDone={() => {
|
|
265
|
-
setShowToolbar(false);
|
|
266
|
-
props.imageHandling?.onDone();
|
|
267
|
-
props.editor.commands.focus('end');
|
|
279
|
+
{showToolbar &&
|
|
280
|
+
editor._tiptapContainerEl &&
|
|
281
|
+
ReactDOM.createPortal(
|
|
282
|
+
<div
|
|
283
|
+
ref={toolbarRef}
|
|
284
|
+
style={{
|
|
285
|
+
zIndex: 20,
|
|
286
|
+
background: 'var(--editable-html-toolbar-bg, #efefef)',
|
|
287
|
+
boxShadow:
|
|
288
|
+
'0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
|
|
289
|
+
width: '100%',
|
|
268
290
|
}}
|
|
269
291
|
>
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
<CustomToolbarWrapper
|
|
293
|
+
showDone
|
|
294
|
+
deletable
|
|
295
|
+
toolbarOpts={options.toolbarOpts || {}}
|
|
296
|
+
onDelete={() => {
|
|
297
|
+
const nodePos = findNodePos();
|
|
298
|
+
if (nodePos === null) return;
|
|
299
|
+
|
|
300
|
+
options.imageHandling?.onDelete?.(latestNodeRef.current);
|
|
301
|
+
|
|
302
|
+
editor.view.dispatch(
|
|
303
|
+
editor.state.tr.delete(nodePos, nodePos + editor.state.doc.nodeAt(nodePos).nodeSize),
|
|
304
|
+
);
|
|
305
|
+
setShowToolbar(false);
|
|
306
|
+
editor.commands.focus();
|
|
307
|
+
}}
|
|
308
|
+
onDone={() => {
|
|
309
|
+
setShowToolbar(false);
|
|
310
|
+
options.imageHandling?.onDone?.(editor);
|
|
311
|
+
editor.commands.focus('end');
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<ImageToolbar
|
|
315
|
+
disableImageAlignmentButtons={options.imageHandling?.disableImageAlignmentButtons}
|
|
316
|
+
alt={node.attrs.alt}
|
|
317
|
+
imageLoaded={node.attrs.loaded}
|
|
318
|
+
alignment={node.attrs.alignment || 'left'}
|
|
319
|
+
onChange={onChange}
|
|
320
|
+
/>
|
|
321
|
+
</CustomToolbarWrapper>
|
|
322
|
+
</div>,
|
|
323
|
+
editor._tiptapContainerEl,
|
|
324
|
+
)}
|
|
280
325
|
</NodeViewWrapper>
|
|
281
326
|
);
|
|
282
327
|
}
|
package/src/extensions/image.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mergeAttributes, Node } from '@tiptap/core';
|
|
2
2
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
|
3
|
+
import { Plugin } from '@tiptap/pm/state';
|
|
3
4
|
import React from 'react';
|
|
4
5
|
import ImageComponent from './image-component';
|
|
6
|
+
import { node } from 'prop-types';
|
|
5
7
|
|
|
6
8
|
export const ImageUploadNode = Node.create({
|
|
7
9
|
name: 'imageUploadNode',
|
|
@@ -13,6 +15,7 @@ export const ImageUploadNode = Node.create({
|
|
|
13
15
|
|
|
14
16
|
addAttributes() {
|
|
15
17
|
return {
|
|
18
|
+
nodeKey: { default: null },
|
|
16
19
|
loaded: { default: false },
|
|
17
20
|
deleteStatus: { default: null },
|
|
18
21
|
alignment: { default: null },
|
|
@@ -27,7 +30,7 @@ export const ImageUploadNode = Node.create({
|
|
|
27
30
|
parseHTML() {
|
|
28
31
|
return [
|
|
29
32
|
{
|
|
30
|
-
tag: '
|
|
33
|
+
tag: 'img[data-type="image-upload-node"]',
|
|
31
34
|
},
|
|
32
35
|
];
|
|
33
36
|
},
|
|
@@ -47,8 +50,60 @@ export const ImageUploadNode = Node.create({
|
|
|
47
50
|
({ commands }) => {
|
|
48
51
|
return commands.insertContent({
|
|
49
52
|
type: this.name,
|
|
53
|
+
// adding a unique nodeKey attribute to help identify this node instance later due to issues with multiple images
|
|
54
|
+
attrs: { nodeKey: `img-${Date.now()}-${Math.random().toString(36).slice(2)}` },
|
|
50
55
|
});
|
|
51
56
|
},
|
|
52
57
|
};
|
|
53
58
|
},
|
|
59
|
+
|
|
60
|
+
addProseMirrorPlugins() {
|
|
61
|
+
const editor = this.editor;
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
new Plugin({
|
|
65
|
+
props: {
|
|
66
|
+
handlePaste(view, event) {
|
|
67
|
+
const items = Array.from(event.clipboardData?.items || []);
|
|
68
|
+
|
|
69
|
+
const imageItem = items.find((item) => item.kind === 'file' && item.type.startsWith('image/'));
|
|
70
|
+
|
|
71
|
+
if (!imageItem) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const file = imageItem.getAsFile();
|
|
76
|
+
|
|
77
|
+
if (!file) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Example 1: insert as base64 immediately
|
|
82
|
+
const reader = new FileReader();
|
|
83
|
+
|
|
84
|
+
reader.onload = () => {
|
|
85
|
+
const src = reader.result;
|
|
86
|
+
|
|
87
|
+
if (typeof src !== 'string') {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
editor.commands.insertContent({
|
|
92
|
+
type: 'imageUploadNode',
|
|
93
|
+
attrs: {
|
|
94
|
+
src,
|
|
95
|
+
loaded: true,
|
|
96
|
+
nodeKey: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
reader.readAsDataURL(file);
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
];
|
|
108
|
+
},
|
|
54
109
|
});
|
package/src/extensions/math.js
CHANGED
|
@@ -8,6 +8,15 @@ import { wrapMath } from '@pie-lib/math-rendering';
|
|
|
8
8
|
|
|
9
9
|
const ensureTextAfterMathPluginKey = new PluginKey('ensureTextAfterMath');
|
|
10
10
|
|
|
11
|
+
const generateAdditionalKeys = (keyData = []) => {
|
|
12
|
+
return keyData.map((key) => ({
|
|
13
|
+
name: key,
|
|
14
|
+
latex: key,
|
|
15
|
+
write: key,
|
|
16
|
+
label: key,
|
|
17
|
+
}));
|
|
18
|
+
};
|
|
19
|
+
|
|
11
20
|
export const EnsureTextAfterMathPlugin = (mathNodeName) =>
|
|
12
21
|
new Plugin({
|
|
13
22
|
key: ensureTextAfterMathPluginKey,
|
|
@@ -175,6 +184,14 @@ export const MathNodeView = (props) => {
|
|
|
175
184
|
const [showToolbar, setShowToolbar] = useState(selected);
|
|
176
185
|
const toolbarRef = useRef(null);
|
|
177
186
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
187
|
+
const { math: mathOptions = {} } = options || {};
|
|
188
|
+
const {
|
|
189
|
+
keypadMode,
|
|
190
|
+
controlledKeypadMode = true,
|
|
191
|
+
customKeys = [],
|
|
192
|
+
keyPadCharacterRef,
|
|
193
|
+
setKeypadInteraction,
|
|
194
|
+
} = mathOptions;
|
|
178
195
|
|
|
179
196
|
const latex = node.attrs.latex || '';
|
|
180
197
|
|
|
@@ -199,22 +216,48 @@ export const MathNodeView = (props) => {
|
|
|
199
216
|
});
|
|
200
217
|
|
|
201
218
|
const handleClickOutside = (event) => {
|
|
219
|
+
const target = event?.target;
|
|
220
|
+
|
|
221
|
+
// MUI's `Select` renders its dropdown options in a portal attached to `document.body`.
|
|
222
|
+
// Those clicks should not dismiss the math toolbar.
|
|
223
|
+
const equationEditorListboxes =
|
|
224
|
+
document.querySelectorAll?.(
|
|
225
|
+
'[id^="equation-editor-select"][id*="listbox"], [aria-labelledby="equation-editor-label"][role="listbox"]',
|
|
226
|
+
) || [];
|
|
227
|
+
|
|
228
|
+
const equationEditorPopoverOpen = equationEditorListboxes.length > 0;
|
|
229
|
+
const clickedEquationEditorSelect =
|
|
230
|
+
!!(target?.id && target.id.includes('equation-editor-select')) ||
|
|
231
|
+
!!target?.closest?.('[id*="equation-editor-select"]');
|
|
232
|
+
|
|
233
|
+
// If the click originated from the math node preview itself (the element
|
|
234
|
+
// that opens the toolbar), ignore it here — the node's own onClick handler
|
|
235
|
+
// will keep/re-open the toolbar. Without this guard, closing and then
|
|
236
|
+
// immediately clicking the math node would fire this listener in the same
|
|
237
|
+
// event cycle and close the toolbar before it could open.
|
|
238
|
+
const clickedMathNode = !!target?.closest?.('.math-node');
|
|
239
|
+
|
|
202
240
|
if (
|
|
203
241
|
toolbarRef.current &&
|
|
204
|
-
!toolbarRef.current.contains(
|
|
205
|
-
!
|
|
242
|
+
!toolbarRef.current.contains(target) &&
|
|
243
|
+
!target?.closest?.('[data-inline-node]') &&
|
|
244
|
+
!equationEditorPopoverOpen &&
|
|
245
|
+
!clickedEquationEditorSelect &&
|
|
246
|
+
!clickedMathNode
|
|
206
247
|
) {
|
|
207
248
|
setShowToolbar(false);
|
|
208
249
|
}
|
|
209
250
|
};
|
|
210
251
|
|
|
211
252
|
if (showToolbar) {
|
|
212
|
-
|
|
253
|
+
// Use `click` (not `mousedown`) so interacting with browser UI like the scrollbar
|
|
254
|
+
// doesn't automatically dismiss the math toolbar.
|
|
255
|
+
document.addEventListener('click', handleClickOutside);
|
|
213
256
|
} else {
|
|
214
|
-
document.removeEventListener('
|
|
257
|
+
document.removeEventListener('click', handleClickOutside);
|
|
215
258
|
}
|
|
216
259
|
|
|
217
|
-
return () => document.removeEventListener('
|
|
260
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
218
261
|
}, [editor, showToolbar]);
|
|
219
262
|
|
|
220
263
|
const handleChange = (newLatex) => {
|
|
@@ -253,19 +296,28 @@ export const MathNodeView = (props) => {
|
|
|
253
296
|
ReactDOM.createPortal(
|
|
254
297
|
<div
|
|
255
298
|
ref={toolbarRef}
|
|
299
|
+
data-toolbar-for={editor.instanceId}
|
|
256
300
|
style={{
|
|
257
|
-
|
|
258
|
-
top: `${position.top}px`,
|
|
259
|
-
left: `${position.left}px`,
|
|
301
|
+
marginTop: '6px',
|
|
260
302
|
zIndex: 20,
|
|
261
303
|
background: 'var(--editable-html-toolbar-bg, #efefef)',
|
|
262
304
|
boxShadow:
|
|
263
305
|
'0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
|
|
264
306
|
}}
|
|
265
307
|
>
|
|
266
|
-
<MathToolbar
|
|
308
|
+
<MathToolbar
|
|
309
|
+
latex={latex}
|
|
310
|
+
autoFocus
|
|
311
|
+
onChange={handleChange}
|
|
312
|
+
onDone={handleDone}
|
|
313
|
+
keypadMode={keypadMode}
|
|
314
|
+
controlledKeypadMode={controlledKeypadMode}
|
|
315
|
+
additionalKeys={generateAdditionalKeys(customKeys)}
|
|
316
|
+
keyPadCharacterRef={keyPadCharacterRef}
|
|
317
|
+
setKeypadInteraction={setKeypadInteraction}
|
|
318
|
+
/>
|
|
267
319
|
</div>,
|
|
268
|
-
document.body,
|
|
320
|
+
editor?._tiptapContainerEl || document.body,
|
|
269
321
|
)}
|
|
270
322
|
</NodeViewWrapper>
|
|
271
323
|
);
|
package/src/extensions/media.js
CHANGED
|
@@ -179,7 +179,7 @@ export default function MediaNodeView({ editor, node, updateAttributes, deleteNo
|
|
|
179
179
|
<source type="audio/mp3" src={src} />
|
|
180
180
|
</audio>
|
|
181
181
|
) : (
|
|
182
|
-
<iframe src={src} allowFullScreen frameBorder="0" />
|
|
182
|
+
<iframe src={src} allowFullScreen frameBorder="0" width={width} height={height} />
|
|
183
183
|
)}
|
|
184
184
|
|
|
185
185
|
<MediaToolbar onEdit={handleEdit} onRemove={deleteNode} />
|
|
@@ -142,10 +142,12 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// --- Slate: indexing logic (kept identical) ---
|
|
145
|
-
if (lastIndexMap[typeName] === undefined)
|
|
145
|
+
if (lastIndexMap[typeName] === undefined) {
|
|
146
|
+
lastIndexMap[typeName] = 0;
|
|
147
|
+
}
|
|
146
148
|
|
|
147
149
|
const prevIndex = lastIndexMap[typeName];
|
|
148
|
-
const newIndex = prevIndex
|
|
150
|
+
const newIndex = prevIndex + 1;
|
|
149
151
|
|
|
150
152
|
// Slate increments map even if newIndex === 0
|
|
151
153
|
lastIndexMap[typeName] += 1;
|
|
@@ -156,7 +158,9 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
156
158
|
index: newIndex,
|
|
157
159
|
});
|
|
158
160
|
|
|
159
|
-
if (!newInline)
|
|
161
|
+
if (!newInline) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
160
164
|
|
|
161
165
|
// --- Insert logic ---
|
|
162
166
|
const { selection } = state;
|
|
@@ -182,20 +186,18 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
182
186
|
if (usedPos == null) {
|
|
183
187
|
usedPos = tryInsertAt(tr.doc.content.size);
|
|
184
188
|
}
|
|
185
|
-
|
|
189
|
+
|
|
190
|
+
if (usedPos == null) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
186
193
|
|
|
187
194
|
// Optionally select the node you just inserted (like your original command)
|
|
188
195
|
// tr.setSelection(NodeSelection.create(tr.doc, usedPos))
|
|
189
196
|
|
|
190
197
|
// --- Cursor move behavior for certain types (Slate: moveFocusTo next text) ---
|
|
191
|
-
if (
|
|
192
|
-
['math_templated', 'inline_dropdown', 'drag_in_the_blank', 'explicit_constructed_response'].includes(
|
|
193
|
-
typeName,
|
|
194
|
-
)
|
|
195
|
-
) {
|
|
198
|
+
if (['math_templated', 'inline_dropdown', 'explicit_constructed_response'].includes(typeName)) {
|
|
196
199
|
tr.setSelection(NodeSelection.create(tr.doc, usedPos));
|
|
197
200
|
} else {
|
|
198
|
-
// Default: put cursor after inserted node
|
|
199
201
|
const after = usedPos + newInline.nodeSize;
|
|
200
202
|
tr.setSelection(selectionAfterPos(tr.doc, after));
|
|
201
203
|
}
|
|
@@ -214,7 +216,7 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
214
216
|
const node = selection.$from.nodeAfter;
|
|
215
217
|
const nodePos = selection.from;
|
|
216
218
|
|
|
217
|
-
tr.setNodeMarkup(nodePos, undefined, { ...node
|
|
219
|
+
tr.setNodeMarkup(nodePos, undefined, { ...node?.attrs, updated: `${Date.now()}` });
|
|
218
220
|
tr.setSelection(NodeSelection.create(tr.doc, nodePos));
|
|
219
221
|
|
|
220
222
|
if (dispatch) {
|
|
@@ -68,7 +68,7 @@ const styles = (theme) => ({
|
|
|
68
68
|
background: 'var(--black)',
|
|
69
69
|
borderRadius: '0.5rem',
|
|
70
70
|
color: 'var(--white)',
|
|
71
|
-
fontFamily:
|
|
71
|
+
fontFamily: '\'JetBrainsMono\', monospace',
|
|
72
72
|
margin: '1.5rem 0',
|
|
73
73
|
padding: '0.75rem 1rem',
|
|
74
74
|
|
|
@@ -81,9 +81,10 @@ const styles = (theme) => ({
|
|
|
81
81
|
},
|
|
82
82
|
|
|
83
83
|
'& blockquote': {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
background: '#f9f9f9',
|
|
85
|
+
borderLeft: '5px solid #ccc',
|
|
86
|
+
margin: '1.5em 10px',
|
|
87
|
+
padding: '.5em 10px',
|
|
87
88
|
},
|
|
88
89
|
|
|
89
90
|
'& hr': {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const escapeHtml = (str) =>
|
|
2
|
+
String(str)
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
|
|
9
|
+
export const normalizeInitialMarkup = (markup) => {
|
|
10
|
+
const trimmed = String(markup ?? '').trim();
|
|
11
|
+
if (!trimmed) return '<div></div>';
|
|
12
|
+
|
|
13
|
+
const looksLikeHtml = /<[^>]+>/.test(trimmed);
|
|
14
|
+
if (looksLikeHtml) return trimmed;
|
|
15
|
+
|
|
16
|
+
return `<div>${escapeHtml(trimmed)}</div>`;
|
|
17
|
+
};
|
|
File without changes
|