@pie-lib/editable-html-tip-tap 2.1.1 → 2.1.2-next.102
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 +51 -20
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +37 -24
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +15 -6
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/TiptapContainer.js +9 -1
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/common/toolbar-buttons.js +5 -0
- package/lib/components/common/toolbar-buttons.js.map +1 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js +2 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +74 -16
- 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/css.js +17 -7
- package/lib/extensions/css.js.map +1 -1
- package/lib/extensions/image-component.js +9 -6
- package/lib/extensions/image-component.js.map +1 -1
- package/lib/extensions/math.js +179 -71
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/responseArea.js +7 -1
- package/lib/extensions/responseArea.js.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/toolbar.js +19 -0
- package/lib/utils/toolbar.js.map +1 -0
- package/package.json +18 -18
- package/src/__tests__/EditableHtml.test.jsx +116 -2
- package/src/components/CharacterPicker.jsx +56 -19
- package/src/components/EditableHtml.jsx +43 -23
- package/src/components/MenuBar.jsx +15 -3
- package/src/components/TiptapContainer.jsx +18 -2
- package/src/components/__tests__/CharacterPicker.test.jsx +57 -6
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +6 -1
- package/src/components/__tests__/InlineDropdown.test.jsx +185 -8
- package/src/components/__tests__/MenuBar.test.jsx +1 -0
- package/src/components/common/toolbar-buttons.jsx +5 -0
- package/src/components/respArea/ExplicitConstructedResponse.jsx +2 -1
- package/src/components/respArea/InlineDropdown.jsx +97 -24
- package/src/components/respArea/inlineDropdownUtils.js +79 -0
- package/src/extensions/__tests__/image-component.test.jsx +1 -0
- package/src/extensions/__tests__/math.test.js +481 -42
- package/src/extensions/__tests__/responseArea.test.js +57 -3
- package/src/extensions/css.js +16 -3
- package/src/extensions/image-component.jsx +8 -6
- package/src/extensions/math.js +188 -69
- package/src/extensions/responseArea.js +7 -1
- package/src/index.jsx +2 -2
- package/src/utils/__tests__/toolbar.test.js +43 -0
- package/src/utils/toolbar.js +15 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { fireEvent, render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { renderMath } from '@pie-lib/math-rendering';
|
|
3
4
|
import InlineDropdown from '../respArea/InlineDropdown';
|
|
4
5
|
|
|
6
|
+
jest.mock('@pie-lib/math-rendering', () => ({
|
|
7
|
+
renderMath: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
5
10
|
jest.mock('@tiptap/react', () => ({
|
|
6
11
|
NodeViewWrapper: ({ children, ...props }) => (
|
|
7
12
|
<div data-testid="node-view-wrapper" {...props}>
|
|
@@ -10,15 +15,27 @@ jest.mock('@tiptap/react', () => ({
|
|
|
10
15
|
),
|
|
11
16
|
}));
|
|
12
17
|
|
|
18
|
+
const mockCreatePortal = jest.fn((node) => node);
|
|
19
|
+
|
|
13
20
|
jest.mock('react-dom', () => ({
|
|
14
21
|
...jest.requireActual('react-dom'),
|
|
15
|
-
createPortal: (
|
|
22
|
+
createPortal: (...args) => mockCreatePortal(...args),
|
|
16
23
|
}));
|
|
17
24
|
|
|
18
25
|
describe('InlineDropdown', () => {
|
|
19
26
|
const buildMockEditor = (overrides = {}) => {
|
|
27
|
+
const mockNodeAt = jest.fn((pos) => (pos === 5 ? { nodeSize: 1 } : null));
|
|
28
|
+
const mockDoc = {
|
|
29
|
+
descendants: jest.fn(),
|
|
30
|
+
nodeAt: mockNodeAt,
|
|
31
|
+
};
|
|
20
32
|
const mockTr = {
|
|
21
33
|
delete: jest.fn(),
|
|
34
|
+
doc: { nodeAt: mockNodeAt },
|
|
35
|
+
setSelection: jest.fn(),
|
|
36
|
+
setMeta: jest.fn(function setMeta() {
|
|
37
|
+
return mockTr;
|
|
38
|
+
}),
|
|
22
39
|
};
|
|
23
40
|
return {
|
|
24
41
|
state: {
|
|
@@ -27,6 +44,7 @@ describe('InlineDropdown', () => {
|
|
|
27
44
|
to: 1,
|
|
28
45
|
},
|
|
29
46
|
tr: mockTr,
|
|
47
|
+
doc: mockDoc,
|
|
30
48
|
},
|
|
31
49
|
view: {
|
|
32
50
|
coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
|
|
@@ -66,6 +84,7 @@ describe('InlineDropdown', () => {
|
|
|
66
84
|
|
|
67
85
|
beforeEach(() => {
|
|
68
86
|
jest.clearAllMocks();
|
|
87
|
+
mockCreatePortal.mockClear();
|
|
69
88
|
mockEditor = buildMockEditor();
|
|
70
89
|
defaultProps.editor = mockEditor;
|
|
71
90
|
Object.defineProperty(document.body, 'getBoundingClientRect', {
|
|
@@ -90,6 +109,32 @@ describe('InlineDropdown', () => {
|
|
|
90
109
|
expect(valueDiv).toBeInTheDocument();
|
|
91
110
|
});
|
|
92
111
|
|
|
112
|
+
it('renders math inside the value control on mount', () => {
|
|
113
|
+
const { container } = render(<InlineDropdown {...defaultProps} />);
|
|
114
|
+
const valueDiv = container.querySelector('div[style*="border"]');
|
|
115
|
+
|
|
116
|
+
expect(renderMath).toHaveBeenCalledWith(valueDiv);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('re-renders math when the value changes', () => {
|
|
120
|
+
const updatedNode = {
|
|
121
|
+
...mockNode,
|
|
122
|
+
attrs: {
|
|
123
|
+
...mockNode.attrs,
|
|
124
|
+
value: '<span>Updated math</span>',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const { container, rerender } = render(<InlineDropdown {...defaultProps} />);
|
|
129
|
+
|
|
130
|
+
renderMath.mockClear();
|
|
131
|
+
|
|
132
|
+
rerender(<InlineDropdown {...defaultProps} node={updatedNode} />);
|
|
133
|
+
|
|
134
|
+
const valueDiv = container.querySelector('div[style*="border"]');
|
|
135
|
+
expect(renderMath).toHaveBeenCalledWith(valueDiv);
|
|
136
|
+
});
|
|
137
|
+
|
|
93
138
|
it('uses 2px horizontal margin on the value control and no horizontal margin on the wrapper', () => {
|
|
94
139
|
const { container, getByTestId } = render(<InlineDropdown {...defaultProps} />);
|
|
95
140
|
const valueDiv = container.querySelector('div[style*="border"]');
|
|
@@ -171,6 +216,107 @@ describe('InlineDropdown', () => {
|
|
|
171
216
|
});
|
|
172
217
|
});
|
|
173
218
|
|
|
219
|
+
it('calls close callback when toolbar closes on outside click', async () => {
|
|
220
|
+
const onInlineDropdownToolbarClose = jest.fn();
|
|
221
|
+
const options = {
|
|
222
|
+
...mockOptions,
|
|
223
|
+
onInlineDropdownToolbarClose,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const { queryByTestId } = render(<InlineDropdown {...defaultProps} options={options} selected={true} />);
|
|
227
|
+
|
|
228
|
+
await waitFor(() => {
|
|
229
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
fireEvent.mouseDown(document.body);
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(queryByTestId('inline-dropdown-toolbar')).not.toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(onInlineDropdownToolbarClose).toHaveBeenCalledWith(mockEditor);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('uses the current node when closing on outside click after the node prop changes', async () => {
|
|
242
|
+
const onToolbarCloseRequest = jest.fn((_tuple, _editor, onConfirm) => onConfirm());
|
|
243
|
+
const options = {
|
|
244
|
+
...mockOptions,
|
|
245
|
+
onToolbarCloseRequest,
|
|
246
|
+
};
|
|
247
|
+
const updatedNode = {
|
|
248
|
+
attrs: { index: '1', value: 'Updated Option', error: false },
|
|
249
|
+
nodeSize: 1,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const { queryByTestId, rerender } = render(<InlineDropdown {...defaultProps} options={options} selected={true} />);
|
|
253
|
+
|
|
254
|
+
await waitFor(() => {
|
|
255
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
rerender(<InlineDropdown {...defaultProps} options={options} node={updatedNode} selected={true} />);
|
|
259
|
+
|
|
260
|
+
fireEvent.mouseDown(document.body);
|
|
261
|
+
|
|
262
|
+
await waitFor(() => {
|
|
263
|
+
expect(onToolbarCloseRequest).toHaveBeenCalledWith(
|
|
264
|
+
[updatedNode, 5],
|
|
265
|
+
expect.anything(),
|
|
266
|
+
expect.any(Function),
|
|
267
|
+
expect.any(Function),
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('respects hold state for the current node after the node prop changes', async () => {
|
|
273
|
+
const updatedNode = {
|
|
274
|
+
attrs: { index: '1', value: 'Updated Option', error: false },
|
|
275
|
+
nodeSize: 1,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
mockEditor._holdInlineDropdownToolbarIndex = '1';
|
|
279
|
+
|
|
280
|
+
const { queryByTestId, rerender } = render(<InlineDropdown {...defaultProps} selected={true} />);
|
|
281
|
+
|
|
282
|
+
await waitFor(() => {
|
|
283
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
rerender(<InlineDropdown {...defaultProps} node={updatedNode} selected={true} />);
|
|
287
|
+
|
|
288
|
+
fireEvent.mouseDown(document.body);
|
|
289
|
+
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('rebinds the outside-click listener when the node prop changes while the toolbar is open', async () => {
|
|
296
|
+
const addSpy = jest.spyOn(document, 'addEventListener');
|
|
297
|
+
const removeSpy = jest.spyOn(document, 'removeEventListener');
|
|
298
|
+
const updatedNode = { ...mockNode, attrs: { ...mockNode.attrs, value: 'Updated Option' } };
|
|
299
|
+
|
|
300
|
+
const { rerender } = render(<InlineDropdown {...defaultProps} selected={true} />);
|
|
301
|
+
|
|
302
|
+
await waitFor(() => {
|
|
303
|
+
expect(addSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const removeCallsBeforeRerender = removeSpy.mock.calls.filter(([type]) => type === 'mousedown').length;
|
|
307
|
+
|
|
308
|
+
rerender(<InlineDropdown {...defaultProps} node={updatedNode} selected={true} />);
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
const removeCallsAfterRerender = removeSpy.mock.calls.filter(([type]) => type === 'mousedown').length;
|
|
312
|
+
expect(removeCallsAfterRerender).toBeGreaterThan(removeCallsBeforeRerender);
|
|
313
|
+
expect(addSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
addSpy.mockRestore();
|
|
317
|
+
removeSpy.mockRestore();
|
|
318
|
+
});
|
|
319
|
+
|
|
174
320
|
it('has correct border styling', () => {
|
|
175
321
|
const { container } = render(<InlineDropdown {...defaultProps} />);
|
|
176
322
|
const dropdownDiv = container.querySelector('div[style*="border"]');
|
|
@@ -200,14 +346,29 @@ describe('InlineDropdown', () => {
|
|
|
200
346
|
expect(textContainer).toHaveStyle({ textOverflow: 'ellipsis', whiteSpace: 'nowrap' });
|
|
201
347
|
});
|
|
202
348
|
|
|
203
|
-
it('
|
|
204
|
-
const
|
|
349
|
+
it('portals toolbar into editor container when _tiptapContainerEl is set', async () => {
|
|
350
|
+
const containerEl = document.createElement('div');
|
|
351
|
+
const editor = buildMockEditor({ _tiptapContainerEl: containerEl });
|
|
352
|
+
|
|
353
|
+
render(<InlineDropdown {...defaultProps} editor={editor} selected={true} />);
|
|
354
|
+
|
|
355
|
+
await waitFor(() => {
|
|
356
|
+
expect(mockCreatePortal).toHaveBeenCalled();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(mockCreatePortal.mock.calls[0][1]).toBe(containerEl);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('portals toolbar into document.body when _tiptapContainerEl is missing', async () => {
|
|
363
|
+
const editor = buildMockEditor({ _tiptapContainerEl: undefined });
|
|
364
|
+
|
|
365
|
+
render(<InlineDropdown {...defaultProps} editor={editor} selected={true} />);
|
|
366
|
+
|
|
205
367
|
await waitFor(() => {
|
|
206
|
-
|
|
207
|
-
if (toolbarContainer) {
|
|
208
|
-
expect(toolbarContainer).toHaveStyle({ zIndex: '1' });
|
|
209
|
-
}
|
|
368
|
+
expect(mockCreatePortal).toHaveBeenCalled();
|
|
210
369
|
});
|
|
370
|
+
|
|
371
|
+
expect(mockCreatePortal.mock.calls[0][1]).toBe(document.body);
|
|
211
372
|
});
|
|
212
373
|
|
|
213
374
|
it('passes editorCallback to InlineDropdownToolbar', async () => {
|
|
@@ -356,6 +517,20 @@ describe('InlineDropdown', () => {
|
|
|
356
517
|
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
357
518
|
});
|
|
358
519
|
|
|
520
|
+
it('does not close toolbar when clicking the page scrollbar (documentElement)', async () => {
|
|
521
|
+
const { queryByTestId } = render(<InlineDropdown {...defaultProps} selected={true} />);
|
|
522
|
+
|
|
523
|
+
await waitFor(() => {
|
|
524
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
fireEvent.mouseDown(document.documentElement);
|
|
528
|
+
|
|
529
|
+
await waitFor(() => {
|
|
530
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
359
534
|
it('renders delete control on portaled custom toolbar when container el is set', async () => {
|
|
360
535
|
const { findByLabelText } = render(<InlineDropdown {...defaultProps} selected />);
|
|
361
536
|
expect(await findByLabelText('Delete')).toBeInTheDocument();
|
|
@@ -373,7 +548,9 @@ describe('InlineDropdown', () => {
|
|
|
373
548
|
const { findByLabelText } = render(<InlineDropdown {...defaultProps} selected />);
|
|
374
549
|
fireEvent.mouseDown(await findByLabelText('Delete'));
|
|
375
550
|
expect(mockEditor.state.tr.delete).toHaveBeenCalledWith(5, 6);
|
|
376
|
-
expect(mockEditor.view.dispatch).
|
|
551
|
+
expect(mockEditor.view.dispatch).toHaveBeenCalledTimes(2);
|
|
552
|
+
expect(mockEditor.view.dispatch).toHaveBeenNthCalledWith(1, mockEditor.state.tr);
|
|
553
|
+
expect(mockEditor.view.dispatch).toHaveBeenNthCalledWith(2, mockEditor.state.tr);
|
|
377
554
|
expect(mockEditor._toolbarOpened).toBe(false);
|
|
378
555
|
expect(mockEditor.commands.focus).toHaveBeenCalled();
|
|
379
556
|
});
|
|
@@ -12,6 +12,11 @@ const StyledButton = styled('button', {
|
|
|
12
12
|
background: 'none',
|
|
13
13
|
border: 'none',
|
|
14
14
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
15
|
+
// previously we had implicit 24×24 icon rendering for mui svg icons, but now we need to explicitly set the size to 24×24 to match the previous behavior
|
|
16
|
+
'& svg': {
|
|
17
|
+
width: '24px',
|
|
18
|
+
height: '24px',
|
|
19
|
+
},
|
|
15
20
|
'&:hover': {
|
|
16
21
|
color: disabled ? 'grey' : 'black',
|
|
17
22
|
},
|
|
@@ -3,6 +3,7 @@ import { NodeViewWrapper } from '@tiptap/react';
|
|
|
3
3
|
import ReactDOM from 'react-dom';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import CustomToolbarWrapper from '../../extensions/custom-toolbar-wrapper';
|
|
6
|
+
import { setToolbarOpened } from '../../utils/toolbar';
|
|
6
7
|
|
|
7
8
|
const ExplicitConstructedResponse = (props) => {
|
|
8
9
|
const { editor, node, getPos, options, selected } = props;
|
|
@@ -110,7 +111,7 @@ const ExplicitConstructedResponse = (props) => {
|
|
|
110
111
|
tr.delete(pos, pos + node.nodeSize);
|
|
111
112
|
// Prevent the debounced onBlur/onDone from firing into the
|
|
112
113
|
// now-deleted node's stale position
|
|
113
|
-
editor
|
|
114
|
+
setToolbarOpened(editor, false);
|
|
114
115
|
editor.view.dispatch(tr);
|
|
115
116
|
setShowToolbar(false);
|
|
116
117
|
editor.commands.focus();
|
|
@@ -1,23 +1,87 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import { renderMath } from '@pie-lib/math-rendering';
|
|
2
4
|
import PropTypes from 'prop-types';
|
|
3
5
|
import { NodeViewWrapper } from '@tiptap/react';
|
|
6
|
+
import { NodeSelection } from 'prosemirror-state';
|
|
4
7
|
import { Chevron } from '../icons/RespArea';
|
|
5
|
-
import ReactDOM from 'react-dom';
|
|
6
8
|
import CustomToolbarWrapper from '../../extensions/custom-toolbar-wrapper';
|
|
9
|
+
import { setToolbarOpened } from '../../utils/toolbar';
|
|
7
10
|
|
|
8
11
|
const InlineDropdown = (props) => {
|
|
9
12
|
const { editor, node, getPos, options, selected } = props;
|
|
10
13
|
const { attrs: attributes } = node;
|
|
11
14
|
const { value, error } = attributes;
|
|
12
|
-
// TODO: Investigate
|
|
13
|
-
// Needed because items with values inside have different positioning for some reason
|
|
14
15
|
const html = value || '<div> </div>';
|
|
15
16
|
const pos = getPos();
|
|
16
17
|
const toolbarRef = useRef(null);
|
|
17
18
|
const toolbarEditor = useRef(null);
|
|
19
|
+
const pendingCloseRequest = useRef(false);
|
|
20
|
+
const elementRef = useRef(null);
|
|
21
|
+
|
|
22
|
+
const isHeld = () =>
|
|
23
|
+
editor._holdInlineDropdownToolbarIndex != null &&
|
|
24
|
+
String(editor._holdInlineDropdownToolbarIndex) === String(node.attrs.index);
|
|
25
|
+
|
|
18
26
|
const [showToolbar, setShowToolbar] = useState(false);
|
|
19
27
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
20
|
-
|
|
28
|
+
|
|
29
|
+
const closeToolbar = () => {
|
|
30
|
+
if (isHeld()) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setShowToolbar(false);
|
|
35
|
+
options.onInlineDropdownToolbarClose?.(editor);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const InlineDropdownToolbar = options.respAreaToolbar([node, pos], editor, closeToolbar);
|
|
39
|
+
|
|
40
|
+
const reselectNode = () => {
|
|
41
|
+
const { tr } = editor.state;
|
|
42
|
+
const nodeAtPos = tr.doc.nodeAt(pos);
|
|
43
|
+
|
|
44
|
+
if (!nodeAtPos) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { selection } = tr;
|
|
49
|
+
|
|
50
|
+
if (selection.from === pos && selection.to === pos + nodeAtPos.nodeSize) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
tr.setSelection(NodeSelection.create(tr.doc, pos));
|
|
55
|
+
editor.view.dispatch(tr);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const requestClose = () => {
|
|
59
|
+
if (pendingCloseRequest.current) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.onToolbarCloseRequest) {
|
|
64
|
+
pendingCloseRequest.current = true;
|
|
65
|
+
|
|
66
|
+
options.onToolbarCloseRequest(
|
|
67
|
+
[node, pos],
|
|
68
|
+
editor,
|
|
69
|
+
() => {
|
|
70
|
+
pendingCloseRequest.current = false;
|
|
71
|
+
delete editor._holdInlineDropdownToolbarIndex;
|
|
72
|
+
closeToolbar();
|
|
73
|
+
},
|
|
74
|
+
() => {
|
|
75
|
+
pendingCloseRequest.current = false;
|
|
76
|
+
delete editor._holdInlineDropdownToolbarIndex;
|
|
77
|
+
setShowToolbar(true);
|
|
78
|
+
setTimeout(reselectNode, 0);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
closeToolbar();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
21
85
|
|
|
22
86
|
useEffect(() => {
|
|
23
87
|
const { selection } = editor.state;
|
|
@@ -25,13 +89,24 @@ const InlineDropdown = (props) => {
|
|
|
25
89
|
|
|
26
90
|
if (selected) {
|
|
27
91
|
if (onlyThisNodeSelected) {
|
|
28
|
-
setShowToolbar(
|
|
92
|
+
setShowToolbar(true);
|
|
29
93
|
}
|
|
30
|
-
} else {
|
|
31
|
-
|
|
94
|
+
} else if (showToolbar) {
|
|
95
|
+
requestClose();
|
|
32
96
|
}
|
|
33
97
|
}, [editor, node, selected]);
|
|
34
98
|
|
|
99
|
+
const isScrollbarClicked = (event) =>
|
|
100
|
+
event.clientX > document.documentElement.clientWidth ||
|
|
101
|
+
event.clientY > document.documentElement.clientHeight ||
|
|
102
|
+
event.target === document.documentElement;
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (elementRef.current && typeof renderMath === 'function') {
|
|
106
|
+
renderMath(elementRef.current);
|
|
107
|
+
}
|
|
108
|
+
}, [value]);
|
|
109
|
+
|
|
35
110
|
useEffect(() => {
|
|
36
111
|
// Calculate position relative to selection
|
|
37
112
|
const bodyRect = document.body.getBoundingClientRect();
|
|
@@ -44,16 +119,21 @@ const InlineDropdown = (props) => {
|
|
|
44
119
|
});
|
|
45
120
|
|
|
46
121
|
const handleClickOutside = (event) => {
|
|
122
|
+
if (isScrollbarClicked(event)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
47
126
|
const insideSomeEditor = event.target.closest('[data-toolbar-for]');
|
|
48
127
|
|
|
49
128
|
if (
|
|
50
|
-
|
|
129
|
+
!event.target.closest('[data-inline-dropdown-toolbar]') &&
|
|
130
|
+
(!insideSomeEditor || insideSomeEditor.dataset.toolbarFor !== toolbarEditor.current?.instanceId) &&
|
|
51
131
|
!editor._toolbarOpened &&
|
|
52
132
|
toolbarRef.current &&
|
|
53
133
|
!toolbarRef.current.contains(event.target) &&
|
|
54
134
|
!event.target.closest('[data-inline-node]')
|
|
55
135
|
) {
|
|
56
|
-
|
|
136
|
+
requestClose();
|
|
57
137
|
}
|
|
58
138
|
};
|
|
59
139
|
|
|
@@ -64,7 +144,7 @@ const InlineDropdown = (props) => {
|
|
|
64
144
|
}
|
|
65
145
|
|
|
66
146
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
67
|
-
}, [showToolbar]);
|
|
147
|
+
}, [showToolbar, node]);
|
|
68
148
|
|
|
69
149
|
return (
|
|
70
150
|
<NodeViewWrapper
|
|
@@ -77,6 +157,7 @@ const InlineDropdown = (props) => {
|
|
|
77
157
|
}}
|
|
78
158
|
>
|
|
79
159
|
<div
|
|
160
|
+
ref={elementRef}
|
|
80
161
|
style={{
|
|
81
162
|
display: 'inline-flex',
|
|
82
163
|
minWidth: '178px',
|
|
@@ -105,31 +186,22 @@ const InlineDropdown = (props) => {
|
|
|
105
186
|
display: 'inline-block',
|
|
106
187
|
verticalAlign: 'middle',
|
|
107
188
|
}}
|
|
108
|
-
dangerouslySetInnerHTML={{
|
|
109
|
-
__html: html,
|
|
110
|
-
}}
|
|
189
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
111
190
|
/>
|
|
112
191
|
</div>
|
|
113
|
-
<Chevron
|
|
114
|
-
direction="down"
|
|
115
|
-
style={{
|
|
116
|
-
position: 'absolute',
|
|
117
|
-
top: '5px',
|
|
118
|
-
right: '5px',
|
|
119
|
-
}}
|
|
120
|
-
/>
|
|
192
|
+
<Chevron direction="down" style={{ position: 'absolute', top: '5px', right: '5px' }} />
|
|
121
193
|
</div>
|
|
122
194
|
{showToolbar && (
|
|
123
195
|
<React.Fragment>
|
|
124
196
|
{ReactDOM.createPortal(
|
|
125
|
-
<div ref={toolbarRef}
|
|
197
|
+
<div ref={toolbarRef}>
|
|
126
198
|
<InlineDropdownToolbar
|
|
127
199
|
editorCallback={(instance) => {
|
|
128
200
|
toolbarEditor.current = instance;
|
|
129
201
|
}}
|
|
130
202
|
/>
|
|
131
203
|
</div>,
|
|
132
|
-
document.body,
|
|
204
|
+
editor?._tiptapContainerEl || document.body,
|
|
133
205
|
)}
|
|
134
206
|
|
|
135
207
|
{editor._tiptapContainerEl &&
|
|
@@ -144,7 +216,8 @@ const InlineDropdown = (props) => {
|
|
|
144
216
|
tr.delete(pos, pos + node.nodeSize);
|
|
145
217
|
// Prevent the debounced onBlur/onDone from firing into the
|
|
146
218
|
// now-deleted node's stale position
|
|
147
|
-
editor
|
|
219
|
+
setToolbarOpened(editor, false);
|
|
220
|
+
delete editor._holdInlineDropdownToolbarIndex;
|
|
148
221
|
editor.view.dispatch(tr);
|
|
149
222
|
setShowToolbar(false);
|
|
150
223
|
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
|
+
};
|