@pie-lib/editable-html-tip-tap 1.2.0-next.8 → 2.0.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 +178 -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/components/respArea/MathTemplated.js +130 -0
- package/lib/components/respArea/MathTemplated.js.map +1 -0
- 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 +22 -13
- 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 +93 -7
- package/src/__tests__/index.test.jsx +11 -3
- package/src/components/CharacterPicker.jsx +1 -0
- package/src/components/EditableHtml.jsx +93 -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/components/respArea/MathTemplated.jsx +124 -0
- package/src/components/respArea/__tests__/MathTemplated.test.jsx +210 -0
- 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 +15 -12
- 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} />
|
|
@@ -5,6 +5,7 @@ import { Node, ReactNodeViewRenderer } from '@tiptap/react';
|
|
|
5
5
|
import ExplicitConstructedResponse from '../components/respArea/ExplicitConstructedResponse';
|
|
6
6
|
import DragInTheBlank from '../components/respArea/DragInTheBlank/DragInTheBlank';
|
|
7
7
|
import InlineDropdown from '../components/respArea/InlineDropdown';
|
|
8
|
+
import MathTemplated from '../components/respArea/MathTemplated';
|
|
8
9
|
|
|
9
10
|
const lastIndexMap = {};
|
|
10
11
|
|
|
@@ -141,10 +142,12 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
// --- Slate: indexing logic (kept identical) ---
|
|
144
|
-
if (lastIndexMap[typeName] === undefined)
|
|
145
|
+
if (lastIndexMap[typeName] === undefined) {
|
|
146
|
+
lastIndexMap[typeName] = 0;
|
|
147
|
+
}
|
|
145
148
|
|
|
146
149
|
const prevIndex = lastIndexMap[typeName];
|
|
147
|
-
const newIndex = prevIndex
|
|
150
|
+
const newIndex = prevIndex + 1;
|
|
148
151
|
|
|
149
152
|
// Slate increments map even if newIndex === 0
|
|
150
153
|
lastIndexMap[typeName] += 1;
|
|
@@ -155,7 +158,9 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
155
158
|
index: newIndex,
|
|
156
159
|
});
|
|
157
160
|
|
|
158
|
-
if (!newInline)
|
|
161
|
+
if (!newInline) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
159
164
|
|
|
160
165
|
// --- Insert logic ---
|
|
161
166
|
const { selection } = state;
|
|
@@ -181,20 +186,18 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
181
186
|
if (usedPos == null) {
|
|
182
187
|
usedPos = tryInsertAt(tr.doc.content.size);
|
|
183
188
|
}
|
|
184
|
-
|
|
189
|
+
|
|
190
|
+
if (usedPos == null) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
185
193
|
|
|
186
194
|
// Optionally select the node you just inserted (like your original command)
|
|
187
195
|
// tr.setSelection(NodeSelection.create(tr.doc, usedPos))
|
|
188
196
|
|
|
189
197
|
// --- Cursor move behavior for certain types (Slate: moveFocusTo next text) ---
|
|
190
|
-
if (
|
|
191
|
-
['math_templated', 'inline_dropdown', 'drag_in_the_blank', 'explicit_constructed_response'].includes(
|
|
192
|
-
typeName,
|
|
193
|
-
)
|
|
194
|
-
) {
|
|
198
|
+
if (['math_templated', 'inline_dropdown', 'explicit_constructed_response'].includes(typeName)) {
|
|
195
199
|
tr.setSelection(NodeSelection.create(tr.doc, usedPos));
|
|
196
200
|
} else {
|
|
197
|
-
// Default: put cursor after inserted node
|
|
198
201
|
const after = usedPos + newInline.nodeSize;
|
|
199
202
|
tr.setSelection(selectionAfterPos(tr.doc, after));
|
|
200
203
|
}
|
|
@@ -213,7 +216,7 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
213
216
|
const node = selection.$from.nodeAfter;
|
|
214
217
|
const nodePos = selection.from;
|
|
215
218
|
|
|
216
|
-
tr.setNodeMarkup(nodePos, undefined, { ...node
|
|
219
|
+
tr.setNodeMarkup(nodePos, undefined, { ...node?.attrs, updated: `${Date.now()}` });
|
|
217
220
|
tr.setSelection(NodeSelection.create(tr.doc, nodePos));
|
|
218
221
|
|
|
219
222
|
if (dispatch) {
|
|
@@ -305,7 +308,7 @@ export const MathTemplatedNode = Node.create({
|
|
|
305
308
|
];
|
|
306
309
|
},
|
|
307
310
|
addNodeView() {
|
|
308
|
-
return ReactNodeViewRenderer(() => <
|
|
311
|
+
return ReactNodeViewRenderer((props) => <MathTemplated {...{ ...props, options: this.options }} />);
|
|
309
312
|
},
|
|
310
313
|
});
|
|
311
314
|
|
|
@@ -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
|