@pie-lib/editable-html-tip-tap 2.1.2 → 2.1.4
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 +15 -0
- package/lib/components/EditableHtml.js +3 -3
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +6 -3
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +52 -8
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/components/respArea/inlineDropdownUtils.js +67 -0
- package/lib/components/respArea/inlineDropdownUtils.js.map +1 -0
- package/lib/extensions/math.js +24 -25
- package/lib/extensions/math.js.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/EditableHtml.test.jsx +2 -2
- package/src/components/EditableHtml.jsx +3 -3
- package/src/components/MenuBar.jsx +6 -1
- package/src/components/__tests__/InlineDropdown.test.jsx +8 -0
- package/src/components/__tests__/MenuBar.test.jsx +1 -0
- package/src/components/respArea/InlineDropdown.jsx +72 -19
- package/src/components/respArea/inlineDropdownUtils.js +79 -0
- package/src/extensions/__tests__/math.test.js +184 -40
- package/src/extensions/math.js +21 -22
- package/src/index.jsx +2 -2
|
@@ -86,6 +86,10 @@ function MenuBar({
|
|
|
86
86
|
|
|
87
87
|
let currentNode;
|
|
88
88
|
|
|
89
|
+
if (!ctx.editor?.commandManager) {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
89
93
|
if (selection instanceof NodeSelection) {
|
|
90
94
|
currentNode = selection.node; // the selected node
|
|
91
95
|
}
|
|
@@ -143,7 +147,8 @@ function MenuBar({
|
|
|
143
147
|
[classes.toolbarWithNoDone]: !hasDoneButton,
|
|
144
148
|
[classes.toolbarTop]: toolbarOpts.position === 'top',
|
|
145
149
|
[classes.toolbarRight]: toolbarOpts.alignment === 'right',
|
|
146
|
-
[classes.focused]:
|
|
150
|
+
[classes.focused]:
|
|
151
|
+
toolbarOpts.alwaysVisible || (editorState.isFocused && !editor._toolbarOpened && !editorState.hideDefaultToolbar),
|
|
147
152
|
[classes.autoWidth]: autoWidth,
|
|
148
153
|
[classes.fullWidth]: !autoWidth,
|
|
149
154
|
[classes.hidden]: toolbarOpts.isHidden === true,
|
|
@@ -17,8 +17,15 @@ jest.mock('react-dom', () => ({
|
|
|
17
17
|
|
|
18
18
|
describe('InlineDropdown', () => {
|
|
19
19
|
const buildMockEditor = (overrides = {}) => {
|
|
20
|
+
const mockNodeAt = jest.fn((pos) => (pos === 5 ? { nodeSize: 1 } : null));
|
|
21
|
+
const mockDoc = {
|
|
22
|
+
descendants: jest.fn(),
|
|
23
|
+
nodeAt: mockNodeAt,
|
|
24
|
+
};
|
|
20
25
|
const mockTr = {
|
|
21
26
|
delete: jest.fn(),
|
|
27
|
+
doc: { nodeAt: mockNodeAt },
|
|
28
|
+
setSelection: jest.fn(),
|
|
22
29
|
};
|
|
23
30
|
return {
|
|
24
31
|
state: {
|
|
@@ -27,6 +34,7 @@ describe('InlineDropdown', () => {
|
|
|
27
34
|
to: 1,
|
|
28
35
|
},
|
|
29
36
|
tr: mockTr,
|
|
37
|
+
doc: mockDoc,
|
|
30
38
|
},
|
|
31
39
|
view: {
|
|
32
40
|
coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { NodeViewWrapper } from '@tiptap/react';
|
|
4
|
+
import { NodeSelection } from 'prosemirror-state';
|
|
4
5
|
import { Chevron } from '../icons/RespArea';
|
|
5
6
|
import ReactDOM from 'react-dom';
|
|
6
7
|
import CustomToolbarWrapper from '../../extensions/custom-toolbar-wrapper';
|
|
@@ -9,15 +10,74 @@ const InlineDropdown = (props) => {
|
|
|
9
10
|
const { editor, node, getPos, options, selected } = props;
|
|
10
11
|
const { attrs: attributes } = node;
|
|
11
12
|
const { value, error } = attributes;
|
|
12
|
-
// TODO: Investigate
|
|
13
|
-
// Needed because items with values inside have different positioning for some reason
|
|
14
13
|
const html = value || '<div> </div>';
|
|
15
14
|
const pos = getPos();
|
|
16
15
|
const toolbarRef = useRef(null);
|
|
17
16
|
const toolbarEditor = useRef(null);
|
|
17
|
+
const pendingCloseRequest = useRef(false);
|
|
18
|
+
|
|
19
|
+
const isHeld = () =>
|
|
20
|
+
editor._holdInlineDropdownToolbarIndex != null &&
|
|
21
|
+
String(editor._holdInlineDropdownToolbarIndex) === String(node.attrs.index);
|
|
22
|
+
|
|
18
23
|
const [showToolbar, setShowToolbar] = useState(false);
|
|
19
24
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
20
|
-
|
|
25
|
+
|
|
26
|
+
const closeToolbar = () => {
|
|
27
|
+
if (isHeld()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setShowToolbar(false);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const InlineDropdownToolbar = options.respAreaToolbar([node, pos], editor, closeToolbar);
|
|
35
|
+
|
|
36
|
+
const reselectNode = () => {
|
|
37
|
+
const { tr } = editor.state;
|
|
38
|
+
const nodeAtPos = tr.doc.nodeAt(pos);
|
|
39
|
+
|
|
40
|
+
if (!nodeAtPos) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { selection } = tr;
|
|
45
|
+
|
|
46
|
+
if (selection.from === pos && selection.to === pos + nodeAtPos.nodeSize) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
tr.setSelection(NodeSelection.create(tr.doc, pos));
|
|
51
|
+
editor.view.dispatch(tr);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const requestClose = () => {
|
|
55
|
+
if (pendingCloseRequest.current) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.onToolbarCloseRequest) {
|
|
60
|
+
pendingCloseRequest.current = true;
|
|
61
|
+
|
|
62
|
+
options.onToolbarCloseRequest(
|
|
63
|
+
[node, pos],
|
|
64
|
+
editor,
|
|
65
|
+
() => {
|
|
66
|
+
pendingCloseRequest.current = false;
|
|
67
|
+
delete editor._holdInlineDropdownToolbarIndex;
|
|
68
|
+
closeToolbar();
|
|
69
|
+
},
|
|
70
|
+
() => {
|
|
71
|
+
pendingCloseRequest.current = false;
|
|
72
|
+
delete editor._holdInlineDropdownToolbarIndex;
|
|
73
|
+
setShowToolbar(true);
|
|
74
|
+
setTimeout(reselectNode, 0);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
closeToolbar();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
21
81
|
|
|
22
82
|
useEffect(() => {
|
|
23
83
|
const { selection } = editor.state;
|
|
@@ -25,10 +85,10 @@ const InlineDropdown = (props) => {
|
|
|
25
85
|
|
|
26
86
|
if (selected) {
|
|
27
87
|
if (onlyThisNodeSelected) {
|
|
28
|
-
setShowToolbar(
|
|
88
|
+
setShowToolbar(true);
|
|
29
89
|
}
|
|
30
|
-
} else {
|
|
31
|
-
|
|
90
|
+
} else if (showToolbar) {
|
|
91
|
+
requestClose();
|
|
32
92
|
}
|
|
33
93
|
}, [editor, node, selected]);
|
|
34
94
|
|
|
@@ -47,13 +107,14 @@ const InlineDropdown = (props) => {
|
|
|
47
107
|
const insideSomeEditor = event.target.closest('[data-toolbar-for]');
|
|
48
108
|
|
|
49
109
|
if (
|
|
50
|
-
|
|
110
|
+
!event.target.closest('[data-inline-dropdown-toolbar]') &&
|
|
111
|
+
(!insideSomeEditor || insideSomeEditor.dataset.toolbarFor !== toolbarEditor.current?.instanceId) &&
|
|
51
112
|
!editor._toolbarOpened &&
|
|
52
113
|
toolbarRef.current &&
|
|
53
114
|
!toolbarRef.current.contains(event.target) &&
|
|
54
115
|
!event.target.closest('[data-inline-node]')
|
|
55
116
|
) {
|
|
56
|
-
|
|
117
|
+
requestClose();
|
|
57
118
|
}
|
|
58
119
|
};
|
|
59
120
|
|
|
@@ -105,19 +166,10 @@ const InlineDropdown = (props) => {
|
|
|
105
166
|
display: 'inline-block',
|
|
106
167
|
verticalAlign: 'middle',
|
|
107
168
|
}}
|
|
108
|
-
dangerouslySetInnerHTML={{
|
|
109
|
-
__html: html,
|
|
110
|
-
}}
|
|
169
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
111
170
|
/>
|
|
112
171
|
</div>
|
|
113
|
-
<Chevron
|
|
114
|
-
direction="down"
|
|
115
|
-
style={{
|
|
116
|
-
position: 'absolute',
|
|
117
|
-
top: '5px',
|
|
118
|
-
right: '5px',
|
|
119
|
-
}}
|
|
120
|
-
/>
|
|
172
|
+
<Chevron direction="down" style={{ position: 'absolute', top: '5px', right: '5px' }} />
|
|
121
173
|
</div>
|
|
122
174
|
{showToolbar && (
|
|
123
175
|
<React.Fragment>
|
|
@@ -145,6 +197,7 @@ const InlineDropdown = (props) => {
|
|
|
145
197
|
// Prevent the debounced onBlur/onDone from firing into the
|
|
146
198
|
// now-deleted node's stale position
|
|
147
199
|
editor._toolbarOpened = false;
|
|
200
|
+
delete editor._holdInlineDropdownToolbarIndex;
|
|
148
201
|
editor.view.dispatch(tr);
|
|
149
202
|
setShowToolbar(false);
|
|
150
203
|
editor.commands.focus();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { NodeSelection } from 'prosemirror-state';
|
|
2
|
+
|
|
3
|
+
export const HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX = '_holdInlineDropdownToolbarIndex';
|
|
4
|
+
|
|
5
|
+
export const findInlineDropdownPos = (editor, index) => {
|
|
6
|
+
let foundPos = null;
|
|
7
|
+
|
|
8
|
+
editor.state.doc.descendants((n, p) => {
|
|
9
|
+
if (n.type?.name === 'inline_dropdown' && String(n.attrs?.index) === String(index)) {
|
|
10
|
+
foundPos = p;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return true;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return foundPos;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const holdInlineDropdownToolbar = (editor, index) => {
|
|
21
|
+
editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX] = index;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const releaseInlineDropdownToolbarHold = (editor) => {
|
|
25
|
+
delete editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const isInlineDropdownToolbarHeld = (editor, index) =>
|
|
29
|
+
editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX] != null &&
|
|
30
|
+
String(editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX]) === String(index);
|
|
31
|
+
|
|
32
|
+
export const selectInlineDropdownNode = (editor, index, fallbackPos) => {
|
|
33
|
+
const pos = findInlineDropdownPos(editor, index) ?? fallbackPos;
|
|
34
|
+
|
|
35
|
+
if (pos == null) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { tr } = editor.state;
|
|
40
|
+
const nodeAtPos = tr.doc.nodeAt(pos);
|
|
41
|
+
|
|
42
|
+
if (!nodeAtPos) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { selection } = tr;
|
|
47
|
+
|
|
48
|
+
if (selection.from === pos && selection.to === pos + nodeAtPos.nodeSize) {
|
|
49
|
+
return pos;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
tr.setSelection(NodeSelection.create(tr.doc, pos));
|
|
53
|
+
editor.view.dispatch(tr);
|
|
54
|
+
|
|
55
|
+
return pos;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const deleteInlineDropdownByIndex = (editor, index, fallbackPos) => {
|
|
59
|
+
const pos = findInlineDropdownPos(editor, index) ?? fallbackPos;
|
|
60
|
+
|
|
61
|
+
if (pos == null) {
|
|
62
|
+
releaseInlineDropdownToolbarHold(editor);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { tr } = editor.state;
|
|
67
|
+
const nodeAtPos = tr.doc.nodeAt(pos);
|
|
68
|
+
|
|
69
|
+
if (!nodeAtPos) {
|
|
70
|
+
releaseInlineDropdownToolbarHold(editor);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
tr.delete(pos, pos + nodeAtPos.nodeSize);
|
|
75
|
+
editor.view.dispatch(tr);
|
|
76
|
+
releaseInlineDropdownToolbarHold(editor);
|
|
77
|
+
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, waitFor, fireEvent } from '@testing-library/react';
|
|
3
|
-
import { MathNode, MathNodeView } from '../math';
|
|
3
|
+
import { EnsureTextAfterMathPlugin, MathNode, MathNodeView, ZeroWidthSpaceHandlingPlugin } from '../math';
|
|
4
4
|
|
|
5
5
|
jest.mock('@tiptap/react', () => ({
|
|
6
6
|
NodeViewWrapper: ({ children, ...props }) => (
|
|
@@ -156,6 +156,151 @@ describe('MathNode', () => {
|
|
|
156
156
|
expect(result).toBeDefined();
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
describe('addProseMirrorPlugins', () => {
|
|
161
|
+
it('registers ensure-text-after-math and zero-width-space plugins', () => {
|
|
162
|
+
const plugins = MathNode.addProseMirrorPlugins();
|
|
163
|
+
|
|
164
|
+
expect(plugins).toHaveLength(2);
|
|
165
|
+
expect(plugins[0].appendTransaction).toBeDefined();
|
|
166
|
+
expect(plugins[1].props.handleKeyDown).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('EnsureTextAfterMathPlugin', () => {
|
|
172
|
+
it('inserts a zero-width space after a math node when no text follows', () => {
|
|
173
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
174
|
+
const textNode = { type: { name: 'text' } };
|
|
175
|
+
const mathNode = { type: { name: 'math' }, nodeSize: 3 };
|
|
176
|
+
const tr = { insert: jest.fn() };
|
|
177
|
+
|
|
178
|
+
const newState = {
|
|
179
|
+
schema: { text: jest.fn((value) => ({ type: textNode.type, text: value })) },
|
|
180
|
+
tr,
|
|
181
|
+
doc: {
|
|
182
|
+
descendants: (cb) => cb(mathNode, 5),
|
|
183
|
+
nodeAt: jest.fn(() => null),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const result = plugin.appendTransaction([{ docChanged: true }], {}, newState);
|
|
188
|
+
|
|
189
|
+
expect(tr.insert).toHaveBeenCalledWith(8, expect.anything());
|
|
190
|
+
expect(result).toBe(tr);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('does not insert when text already follows the math node', () => {
|
|
194
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
195
|
+
const tr = { insert: jest.fn() };
|
|
196
|
+
const mathNode = { type: { name: 'math' }, nodeSize: 3 };
|
|
197
|
+
|
|
198
|
+
const newState = {
|
|
199
|
+
schema: { text: jest.fn() },
|
|
200
|
+
tr,
|
|
201
|
+
doc: {
|
|
202
|
+
descendants: (cb) => cb(mathNode, 5),
|
|
203
|
+
nodeAt: jest.fn(() => ({ type: { name: 'text' } })),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = plugin.appendTransaction([{ docChanged: true }], {}, newState);
|
|
208
|
+
|
|
209
|
+
expect(tr.insert).not.toHaveBeenCalled();
|
|
210
|
+
expect(result).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('returns null when the document did not change', () => {
|
|
214
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
215
|
+
|
|
216
|
+
const result = plugin.appendTransaction([{ docChanged: false }], {}, {});
|
|
217
|
+
expect(result).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('ZeroWidthSpaceHandlingPlugin', () => {
|
|
222
|
+
const createDefaultDoc = () => ({
|
|
223
|
+
textBetween: jest.fn(() => '\u200b'),
|
|
224
|
+
resolve: jest.fn(() => ({
|
|
225
|
+
nodeAfter: null,
|
|
226
|
+
nodeBefore: null,
|
|
227
|
+
})),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const createView = ({ state: stateOverrides = {} } = {}) => {
|
|
231
|
+
const dispatch = jest.fn();
|
|
232
|
+
const tr = {
|
|
233
|
+
delete: jest.fn().mockReturnThis(),
|
|
234
|
+
setSelection: jest.fn().mockReturnThis(),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
state: {
|
|
239
|
+
selection: { from: 2, empty: true },
|
|
240
|
+
doc: createDefaultDoc(),
|
|
241
|
+
tr,
|
|
242
|
+
...stateOverrides,
|
|
243
|
+
doc: { ...createDefaultDoc(), ...stateOverrides.doc },
|
|
244
|
+
},
|
|
245
|
+
dispatch,
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
it('deletes math and zero-width space on Backspace', () => {
|
|
250
|
+
const view = createView();
|
|
251
|
+
const event = { key: 'Backspace' };
|
|
252
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, event);
|
|
253
|
+
|
|
254
|
+
expect(handled).toBe(true);
|
|
255
|
+
expect(view.state.tr.delete).toHaveBeenCalledWith(0, 2);
|
|
256
|
+
expect(view.dispatch).toHaveBeenCalledWith(view.state.tr);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('selects the math node on ArrowLeft before a zero-width space', () => {
|
|
260
|
+
const mathNode = { nodeSize: 3 };
|
|
261
|
+
const view = createView({
|
|
262
|
+
state: {
|
|
263
|
+
doc: {
|
|
264
|
+
resolve: jest
|
|
265
|
+
.fn()
|
|
266
|
+
.mockReturnValueOnce({ nodeAfter: mathNode, nodeBefore: null })
|
|
267
|
+
.mockReturnValueOnce({ pos: 4 }),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const { NodeSelection } = require('prosemirror-state');
|
|
272
|
+
|
|
273
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'ArrowLeft' });
|
|
274
|
+
|
|
275
|
+
expect(handled).toBe(true);
|
|
276
|
+
expect(NodeSelection.create).toHaveBeenCalledWith(view.state.doc, 4);
|
|
277
|
+
expect(view.dispatch).toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('moves the text cursor before the zero-width space when no inline node precedes it', () => {
|
|
281
|
+
const view = createView();
|
|
282
|
+
const { TextSelection } = require('prosemirror-state');
|
|
283
|
+
|
|
284
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'ArrowLeft' });
|
|
285
|
+
|
|
286
|
+
expect(handled).toBe(true);
|
|
287
|
+
expect(TextSelection.create).toHaveBeenCalledWith(view.state.doc, 0);
|
|
288
|
+
expect(view.dispatch).toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('returns false for unrelated keys', () => {
|
|
292
|
+
const view = createView({
|
|
293
|
+
state: {
|
|
294
|
+
doc: {
|
|
295
|
+
textBetween: jest.fn(() => 'a'),
|
|
296
|
+
resolve: jest.fn(),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'Enter' });
|
|
302
|
+
expect(handled).toBe(false);
|
|
303
|
+
});
|
|
159
304
|
});
|
|
160
305
|
|
|
161
306
|
describe('MathNodeView', () => {
|
|
@@ -244,37 +389,30 @@ describe('MathNodeView', () => {
|
|
|
244
389
|
});
|
|
245
390
|
|
|
246
391
|
describe('toolbar positioning', () => {
|
|
247
|
-
it('uses
|
|
248
|
-
const
|
|
249
|
-
containerEl.getBoundingClientRect = jest.fn(() => ({ top: -50, left: 20, width: 600, height: 400 }));
|
|
250
|
-
|
|
251
|
-
const editor = {
|
|
252
|
-
...defaultProps.editor,
|
|
253
|
-
_tiptapContainerEl: containerEl,
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
392
|
+
it('uses a fixed top offset and horizontal position from coordsAtPos', async () => {
|
|
393
|
+
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
257
394
|
await waitFor(() => {
|
|
258
395
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
259
396
|
expect(toolbar).toBeInTheDocument();
|
|
260
|
-
|
|
261
|
-
expect(toolbar.style.top).toBe('190px');
|
|
397
|
+
expect(toolbar.style.top).toBe('40px');
|
|
262
398
|
expect(toolbar.style.left).toBe('50px');
|
|
263
399
|
});
|
|
264
400
|
});
|
|
265
401
|
|
|
266
|
-
it('
|
|
402
|
+
it('keeps the fixed top offset when the editor container is scrolled', async () => {
|
|
403
|
+
const containerEl = document.createElement('div');
|
|
404
|
+
containerEl.getBoundingClientRect = jest.fn(() => ({ top: -200, left: 0, width: 600, height: 400 }));
|
|
405
|
+
|
|
267
406
|
const editor = {
|
|
268
407
|
...defaultProps.editor,
|
|
269
|
-
_tiptapContainerEl:
|
|
408
|
+
_tiptapContainerEl: containerEl,
|
|
270
409
|
};
|
|
271
410
|
|
|
272
411
|
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
273
412
|
await waitFor(() => {
|
|
274
413
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
275
414
|
expect(toolbar).toBeInTheDocument();
|
|
276
|
-
|
|
277
|
-
expect(toolbar.style.top).toBe('140px');
|
|
415
|
+
expect(toolbar.style.top).toBe('40px');
|
|
278
416
|
expect(toolbar.style.left).toBe('50px');
|
|
279
417
|
});
|
|
280
418
|
});
|
|
@@ -288,7 +426,7 @@ describe('MathNodeView', () => {
|
|
|
288
426
|
});
|
|
289
427
|
});
|
|
290
428
|
|
|
291
|
-
it('
|
|
429
|
+
it('updates horizontal position from coordsAtPos when selection changes', async () => {
|
|
292
430
|
const editor = {
|
|
293
431
|
...defaultProps.editor,
|
|
294
432
|
view: {
|
|
@@ -302,30 +440,11 @@ describe('MathNodeView', () => {
|
|
|
302
440
|
await waitFor(() => {
|
|
303
441
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
304
442
|
expect(toolbar).toBeInTheDocument();
|
|
305
|
-
|
|
306
|
-
expect(toolbar.style.top).toBe('240px');
|
|
443
|
+
expect(toolbar.style.top).toBe('40px');
|
|
307
444
|
expect(toolbar.style.left).toBe('150px');
|
|
308
445
|
});
|
|
309
446
|
});
|
|
310
447
|
|
|
311
|
-
it('accounts for negative container top (scrolled container)', async () => {
|
|
312
|
-
const containerEl = document.createElement('div');
|
|
313
|
-
containerEl.getBoundingClientRect = jest.fn(() => ({ top: -200, left: 0, width: 600, height: 400 }));
|
|
314
|
-
|
|
315
|
-
const editor = {
|
|
316
|
-
...defaultProps.editor,
|
|
317
|
-
_tiptapContainerEl: containerEl,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
321
|
-
await waitFor(() => {
|
|
322
|
-
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
323
|
-
expect(toolbar).toBeInTheDocument();
|
|
324
|
-
// top = 100 + Math.abs(-200) + 40 = 340
|
|
325
|
-
expect(toolbar.style.top).toBe('340px');
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
448
|
it('portals toolbar into _tiptapContainerEl when available', async () => {
|
|
330
449
|
const containerEl = document.createElement('div');
|
|
331
450
|
containerEl.getBoundingClientRect = jest.fn(() => ({ top: 0, left: 0, width: 600, height: 400 }));
|
|
@@ -410,8 +529,13 @@ describe('MathNodeView', () => {
|
|
|
410
529
|
});
|
|
411
530
|
});
|
|
412
531
|
|
|
413
|
-
it('closes toolbar on outside click', async () => {
|
|
414
|
-
const
|
|
532
|
+
it('closes toolbar on outside click and runs handleDone', async () => {
|
|
533
|
+
const updateAttributes = jest.fn();
|
|
534
|
+
const editor = createMockEditor();
|
|
535
|
+
const { TextSelection } = require('prosemirror-state');
|
|
536
|
+
const { queryByTestId } = render(
|
|
537
|
+
<MathNodeView {...defaultProps} editor={editor} updateAttributes={updateAttributes} selected={true} />,
|
|
538
|
+
);
|
|
415
539
|
|
|
416
540
|
await waitFor(() => {
|
|
417
541
|
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
@@ -421,6 +545,26 @@ describe('MathNodeView', () => {
|
|
|
421
545
|
|
|
422
546
|
await waitFor(() => {
|
|
423
547
|
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
548
|
+
expect(updateAttributes).toHaveBeenCalledWith({ latex: 'x^2' });
|
|
549
|
+
expect(TextSelection.create).toHaveBeenCalledWith(editor.state.doc, 1);
|
|
550
|
+
expect(editor.state.tr.setSelection).toHaveBeenCalled();
|
|
551
|
+
expect(editor.view.dispatch).toHaveBeenCalledWith(editor.state.tr);
|
|
552
|
+
expect(editor.commands.focus).toHaveBeenCalled();
|
|
553
|
+
expect(editor._toolbarOpened).toBe(false);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('does not close toolbar when clicking the math node preview', async () => {
|
|
558
|
+
const { getByTestId, queryByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
559
|
+
|
|
560
|
+
await waitFor(() => {
|
|
561
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
fireEvent.click(getByTestId('math-preview'));
|
|
565
|
+
|
|
566
|
+
await waitFor(() => {
|
|
567
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
424
568
|
});
|
|
425
569
|
});
|
|
426
570
|
|
package/src/extensions/math.js
CHANGED
|
@@ -195,6 +195,25 @@ export const MathNodeView = (props) => {
|
|
|
195
195
|
|
|
196
196
|
const latex = node.attrs.latex || '';
|
|
197
197
|
|
|
198
|
+
const handleChange = (newLatex) => {
|
|
199
|
+
updateAttributes({ latex: newLatex });
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleDone = (newLatex) => {
|
|
203
|
+
updateAttributes({ latex: newLatex });
|
|
204
|
+
setShowToolbar(false);
|
|
205
|
+
|
|
206
|
+
editor._toolbarOpened = false;
|
|
207
|
+
|
|
208
|
+
const { selection, tr, doc } = editor.state;
|
|
209
|
+
const sel = TextSelection.create(doc, selection.from + 1);
|
|
210
|
+
|
|
211
|
+
// Build a fresh transaction from the current state and set the selection
|
|
212
|
+
tr.setSelection(sel);
|
|
213
|
+
editor.view.dispatch(tr);
|
|
214
|
+
editor.commands.focus();
|
|
215
|
+
};
|
|
216
|
+
|
|
198
217
|
useEffect(() => {
|
|
199
218
|
if (selected) {
|
|
200
219
|
setShowToolbar(true);
|
|
@@ -207,12 +226,10 @@ export const MathNodeView = (props) => {
|
|
|
207
226
|
|
|
208
227
|
useEffect(() => {
|
|
209
228
|
// Calculate position relative to selection
|
|
210
|
-
const container = editor?._tiptapContainerEl || document.body;
|
|
211
|
-
const bodyRect = container.getBoundingClientRect();
|
|
212
229
|
const { from } = editor.state.selection;
|
|
213
230
|
const start = editor.view.coordsAtPos(from);
|
|
214
231
|
setPosition({
|
|
215
|
-
top:
|
|
232
|
+
top: 40, // shift above
|
|
216
233
|
left: start.left,
|
|
217
234
|
});
|
|
218
235
|
|
|
@@ -247,6 +264,7 @@ export const MathNodeView = (props) => {
|
|
|
247
264
|
!clickedMathNode
|
|
248
265
|
) {
|
|
249
266
|
setShowToolbar(false);
|
|
267
|
+
handleDone(node.attrs.latex);
|
|
250
268
|
}
|
|
251
269
|
};
|
|
252
270
|
|
|
@@ -261,25 +279,6 @@ export const MathNodeView = (props) => {
|
|
|
261
279
|
return () => document.removeEventListener('click', handleClickOutside);
|
|
262
280
|
}, [editor, showToolbar]);
|
|
263
281
|
|
|
264
|
-
const handleChange = (newLatex) => {
|
|
265
|
-
updateAttributes({ latex: newLatex });
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const handleDone = (newLatex) => {
|
|
269
|
-
updateAttributes({ latex: newLatex });
|
|
270
|
-
setShowToolbar(false);
|
|
271
|
-
|
|
272
|
-
editor._toolbarOpened = false;
|
|
273
|
-
|
|
274
|
-
const { selection, tr, doc } = editor.state;
|
|
275
|
-
const sel = TextSelection.create(doc, selection.from + 1);
|
|
276
|
-
|
|
277
|
-
// Build a fresh transaction from the current state and set the selection
|
|
278
|
-
tr.setSelection(sel);
|
|
279
|
-
editor.view.dispatch(tr);
|
|
280
|
-
editor.commands.focus();
|
|
281
|
-
};
|
|
282
|
-
|
|
283
282
|
return (
|
|
284
283
|
<NodeViewWrapper
|
|
285
284
|
className="math-node"
|
package/src/index.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import StyledEditor, { EditableHtml } from './components/EditableHtml';
|
|
2
2
|
import { ALL_PLUGINS, DEFAULT_PLUGINS } from './extensions';
|
|
3
|
-
|
|
4
|
-
export { EditableHtml, ALL_PLUGINS, DEFAULT_PLUGINS };
|
|
3
|
+
import { deleteInlineDropdownByIndex } from './components/respArea/inlineDropdownUtils';
|
|
4
|
+
export { EditableHtml, ALL_PLUGINS, DEFAULT_PLUGINS, deleteInlineDropdownByIndex };
|
|
5
5
|
export default StyledEditor;
|