@pie-lib/editable-html-tip-tap 2.1.4 → 2.1.6
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 +17 -0
- package/lib/components/CharacterPicker.js +41 -16
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/MenuBar.js +5 -4
- package/lib/components/MenuBar.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 +4 -6
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/extensions/math.js +51 -37
- package/lib/extensions/math.js.map +1 -1
- package/lib/utils/toolbar.js +19 -0
- package/lib/utils/toolbar.js.map +1 -0
- package/package.json +6 -6
- package/src/components/CharacterPicker.jsx +46 -16
- package/src/components/MenuBar.jsx +5 -3
- package/src/components/__tests__/CharacterPicker.test.jsx +10 -1
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +6 -1
- package/src/components/__tests__/InlineDropdown.test.jsx +31 -8
- package/src/components/common/toolbar-buttons.jsx +5 -0
- package/src/components/respArea/ExplicitConstructedResponse.jsx +2 -1
- package/src/components/respArea/InlineDropdown.jsx +4 -3
- package/src/extensions/__tests__/math.test.js +220 -64
- package/src/extensions/math.js +60 -39
- package/src/utils/__tests__/toolbar.test.js +43 -0
- package/src/utils/toolbar.js +15 -0
|
@@ -78,25 +78,52 @@ export function CharacterPicker({ editor, opts, onClose }) {
|
|
|
78
78
|
useEffect(() => {
|
|
79
79
|
if (!editor) return;
|
|
80
80
|
|
|
81
|
-
// Calculate position relative to selection
|
|
82
81
|
const editorDOM = editor.options.element;
|
|
83
|
-
const
|
|
84
|
-
const bodyRect = document.body.getBoundingClientRect();
|
|
85
|
-
const { from } = editor.state.selection;
|
|
86
|
-
const start = editor.view.coordsAtPos(from);
|
|
82
|
+
const editorViewDom = editor.view.dom;
|
|
87
83
|
|
|
88
|
-
|
|
84
|
+
// Position is computed in viewport coordinates (the dialog uses position: fixed),
|
|
85
|
+
// so coordsAtPos / getBoundingClientRect values can be used directly without
|
|
86
|
+
// adding scroll offsets. The dialog is then clamped to the viewport so it does
|
|
87
|
+
// not get cut off by fixed page headers/footers.
|
|
88
|
+
const updatePosition = () => {
|
|
89
|
+
if (!containerRef.current) return;
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const editorRect = editorDOM.getBoundingClientRect();
|
|
92
|
+
const { from } = editor.state.selection;
|
|
93
|
+
const start = editor.view.coordsAtPos(from);
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
left: start.left,
|
|
97
|
-
});
|
|
95
|
+
const dialogHeight = containerRef.current.offsetHeight;
|
|
96
|
+
const dialogWidth = containerRef.current.offsetWidth;
|
|
98
97
|
|
|
99
|
-
|
|
98
|
+
// prefer below the editor; flip above when there isn't room below.
|
|
99
|
+
const spaceBelow = window.innerHeight - (editorRect.bottom + 60);
|
|
100
|
+
let top =
|
|
101
|
+
spaceBelow >= dialogHeight || editorRect.top < dialogHeight + 80
|
|
102
|
+
? editorRect.bottom + 60
|
|
103
|
+
: editorRect.top - dialogHeight - 20;
|
|
104
|
+
|
|
105
|
+
let left = start.left;
|
|
106
|
+
|
|
107
|
+
const margin = 8;
|
|
108
|
+
top = Math.max(margin, Math.min(top, window.innerHeight - dialogHeight - margin));
|
|
109
|
+
left = Math.max(margin, Math.min(left, window.innerWidth - dialogWidth - margin));
|
|
110
|
+
|
|
111
|
+
setPosition({ top, left });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
updatePosition();
|
|
115
|
+
|
|
116
|
+
let frame = null;
|
|
117
|
+
const scheduleUpdate = () => {
|
|
118
|
+
if (frame !== null) return;
|
|
119
|
+
frame = requestAnimationFrame(() => {
|
|
120
|
+
frame = null;
|
|
121
|
+
updatePosition();
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
window.addEventListener('scroll', scheduleUpdate, true);
|
|
126
|
+
window.addEventListener('resize', scheduleUpdate);
|
|
100
127
|
|
|
101
128
|
const handleClickOutside = (e) => {
|
|
102
129
|
if (containerRef.current && !containerRef.current.contains(e.target) && !editorViewDom.contains(e.target)) {
|
|
@@ -110,6 +137,9 @@ export function CharacterPicker({ editor, opts, onClose }) {
|
|
|
110
137
|
|
|
111
138
|
return () => {
|
|
112
139
|
clearTimeout(timeoutId);
|
|
140
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
141
|
+
window.removeEventListener('scroll', scheduleUpdate, true);
|
|
142
|
+
window.removeEventListener('resize', scheduleUpdate);
|
|
113
143
|
document.removeEventListener('click', handleClickOutside);
|
|
114
144
|
};
|
|
115
145
|
}, [editor]);
|
|
@@ -131,11 +161,11 @@ export function CharacterPicker({ editor, opts, onClose }) {
|
|
|
131
161
|
data-toolbar-for={editor.instanceId}
|
|
132
162
|
style={{
|
|
133
163
|
visibility: position.top === 0 && position.left === 0 ? 'hidden' : 'initial',
|
|
134
|
-
position: '
|
|
164
|
+
position: 'fixed',
|
|
135
165
|
top: `${position.top}px`,
|
|
136
166
|
left: `${position.left}px`,
|
|
137
167
|
maxWidth: '500px',
|
|
138
|
-
zIndex:
|
|
168
|
+
zIndex: 1000,
|
|
139
169
|
}}
|
|
140
170
|
>
|
|
141
171
|
<div>
|
|
@@ -107,6 +107,7 @@ function MenuBar({
|
|
|
107
107
|
hideDefaultToolbar,
|
|
108
108
|
hasTextSelectionInTable,
|
|
109
109
|
isFocused: ctx.editor?.isFocused,
|
|
110
|
+
toolbarOpened: ctx.editor?._toolbarOpened ?? false,
|
|
110
111
|
isBold: ctx.editor.isActive('bold') ?? false,
|
|
111
112
|
canBold: ctx.editor.can().chain().toggleBold().run() ?? false,
|
|
112
113
|
isTable: ctx.editor.isActive('table') ?? false,
|
|
@@ -148,7 +149,8 @@ function MenuBar({
|
|
|
148
149
|
[classes.toolbarTop]: toolbarOpts.position === 'top',
|
|
149
150
|
[classes.toolbarRight]: toolbarOpts.alignment === 'right',
|
|
150
151
|
[classes.focused]:
|
|
151
|
-
toolbarOpts.alwaysVisible ||
|
|
152
|
+
toolbarOpts.alwaysVisible ||
|
|
153
|
+
(editorState.isFocused && !editorState.toolbarOpened && !editorState.hideDefaultToolbar),
|
|
152
154
|
[classes.autoWidth]: autoWidth,
|
|
153
155
|
[classes.fullWidth]: !autoWidth,
|
|
154
156
|
[classes.hidden]: toolbarOpts.isHidden === true,
|
|
@@ -436,7 +438,7 @@ const StyledMenuBar = (props) => {
|
|
|
436
438
|
const classes = {
|
|
437
439
|
defaultToolbar: 'defaultToolbar',
|
|
438
440
|
buttonsContainer: 'buttonsContainer',
|
|
439
|
-
button: '
|
|
441
|
+
button: 'toolbarButton',
|
|
440
442
|
active: 'active',
|
|
441
443
|
disabled: 'disabled',
|
|
442
444
|
isActive: 'isActive',
|
|
@@ -471,7 +473,7 @@ const StyledMenuBarRoot = styled('div')(({ theme }) => ({
|
|
|
471
473
|
display: 'flex',
|
|
472
474
|
width: '100%',
|
|
473
475
|
},
|
|
474
|
-
'& .
|
|
476
|
+
'& .toolbarButton': {
|
|
475
477
|
color: 'grey',
|
|
476
478
|
display: 'inline-flex',
|
|
477
479
|
padding: '2px',
|
|
@@ -234,7 +234,16 @@ describe('CharacterPicker', () => {
|
|
|
234
234
|
};
|
|
235
235
|
const { container } = render(<CharacterPicker editor={mockEditor} opts={opts} onClose={jest.fn()} />);
|
|
236
236
|
const dialog = container.querySelector('.insert-character-dialog');
|
|
237
|
-
expect(dialog).toHaveStyle({ position: '
|
|
237
|
+
expect(dialog).toHaveStyle({ position: 'fixed' });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('renders above other editor overlays with a high z-index', () => {
|
|
241
|
+
const opts = {
|
|
242
|
+
characters: [['á']],
|
|
243
|
+
};
|
|
244
|
+
const { container } = render(<CharacterPicker editor={mockEditor} opts={opts} onClose={jest.fn()} />);
|
|
245
|
+
const dialog = container.querySelector('.insert-character-dialog');
|
|
246
|
+
expect(dialog).toHaveStyle({ zIndex: '1000' });
|
|
238
247
|
});
|
|
239
248
|
|
|
240
249
|
it('adds data-toolbar-for attribute with editor instanceId', () => {
|
|
@@ -19,6 +19,9 @@ describe('ExplicitConstructedResponse', () => {
|
|
|
19
19
|
const buildMockEditor = (overrides = {}) => {
|
|
20
20
|
const mockTr = {
|
|
21
21
|
delete: jest.fn(),
|
|
22
|
+
setMeta: jest.fn(function setMeta() {
|
|
23
|
+
return mockTr;
|
|
24
|
+
}),
|
|
22
25
|
};
|
|
23
26
|
return {
|
|
24
27
|
state: {
|
|
@@ -197,7 +200,9 @@ describe('ExplicitConstructedResponse', () => {
|
|
|
197
200
|
const { findByLabelText } = render(<ExplicitConstructedResponse {...defaultProps} selected />);
|
|
198
201
|
fireEvent.mouseDown(await findByLabelText('Delete'));
|
|
199
202
|
expect(mockEditor.state.tr.delete).toHaveBeenCalledWith(5, 6);
|
|
200
|
-
expect(mockEditor.view.dispatch).
|
|
203
|
+
expect(mockEditor.view.dispatch).toHaveBeenCalledTimes(2);
|
|
204
|
+
expect(mockEditor.view.dispatch).toHaveBeenNthCalledWith(1, mockEditor.state.tr);
|
|
205
|
+
expect(mockEditor.view.dispatch).toHaveBeenNthCalledWith(2, mockEditor.state.tr);
|
|
201
206
|
expect(mockEditor._toolbarOpened).toBe(false);
|
|
202
207
|
expect(mockEditor.commands.focus).toHaveBeenCalled();
|
|
203
208
|
});
|
|
@@ -10,9 +10,11 @@ jest.mock('@tiptap/react', () => ({
|
|
|
10
10
|
),
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
+
const mockCreatePortal = jest.fn((node) => node);
|
|
14
|
+
|
|
13
15
|
jest.mock('react-dom', () => ({
|
|
14
16
|
...jest.requireActual('react-dom'),
|
|
15
|
-
createPortal: (
|
|
17
|
+
createPortal: (...args) => mockCreatePortal(...args),
|
|
16
18
|
}));
|
|
17
19
|
|
|
18
20
|
describe('InlineDropdown', () => {
|
|
@@ -26,6 +28,9 @@ describe('InlineDropdown', () => {
|
|
|
26
28
|
delete: jest.fn(),
|
|
27
29
|
doc: { nodeAt: mockNodeAt },
|
|
28
30
|
setSelection: jest.fn(),
|
|
31
|
+
setMeta: jest.fn(function setMeta() {
|
|
32
|
+
return mockTr;
|
|
33
|
+
}),
|
|
29
34
|
};
|
|
30
35
|
return {
|
|
31
36
|
state: {
|
|
@@ -74,6 +79,7 @@ describe('InlineDropdown', () => {
|
|
|
74
79
|
|
|
75
80
|
beforeEach(() => {
|
|
76
81
|
jest.clearAllMocks();
|
|
82
|
+
mockCreatePortal.mockClear();
|
|
77
83
|
mockEditor = buildMockEditor();
|
|
78
84
|
defaultProps.editor = mockEditor;
|
|
79
85
|
Object.defineProperty(document.body, 'getBoundingClientRect', {
|
|
@@ -208,14 +214,29 @@ describe('InlineDropdown', () => {
|
|
|
208
214
|
expect(textContainer).toHaveStyle({ textOverflow: 'ellipsis', whiteSpace: 'nowrap' });
|
|
209
215
|
});
|
|
210
216
|
|
|
211
|
-
it('
|
|
212
|
-
const
|
|
217
|
+
it('portals toolbar into editor container when _tiptapContainerEl is set', async () => {
|
|
218
|
+
const containerEl = document.createElement('div');
|
|
219
|
+
const editor = buildMockEditor({ _tiptapContainerEl: containerEl });
|
|
220
|
+
|
|
221
|
+
render(<InlineDropdown {...defaultProps} editor={editor} selected={true} />);
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(mockCreatePortal).toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(mockCreatePortal.mock.calls[0][1]).toBe(containerEl);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('portals toolbar into document.body when _tiptapContainerEl is missing', async () => {
|
|
231
|
+
const editor = buildMockEditor({ _tiptapContainerEl: undefined });
|
|
232
|
+
|
|
233
|
+
render(<InlineDropdown {...defaultProps} editor={editor} selected={true} />);
|
|
234
|
+
|
|
213
235
|
await waitFor(() => {
|
|
214
|
-
|
|
215
|
-
if (toolbarContainer) {
|
|
216
|
-
expect(toolbarContainer).toHaveStyle({ zIndex: '1' });
|
|
217
|
-
}
|
|
236
|
+
expect(mockCreatePortal).toHaveBeenCalled();
|
|
218
237
|
});
|
|
238
|
+
|
|
239
|
+
expect(mockCreatePortal.mock.calls[0][1]).toBe(document.body);
|
|
219
240
|
});
|
|
220
241
|
|
|
221
242
|
it('passes editorCallback to InlineDropdownToolbar', async () => {
|
|
@@ -381,7 +402,9 @@ describe('InlineDropdown', () => {
|
|
|
381
402
|
const { findByLabelText } = render(<InlineDropdown {...defaultProps} selected />);
|
|
382
403
|
fireEvent.mouseDown(await findByLabelText('Delete'));
|
|
383
404
|
expect(mockEditor.state.tr.delete).toHaveBeenCalledWith(5, 6);
|
|
384
|
-
expect(mockEditor.view.dispatch).
|
|
405
|
+
expect(mockEditor.view.dispatch).toHaveBeenCalledTimes(2);
|
|
406
|
+
expect(mockEditor.view.dispatch).toHaveBeenNthCalledWith(1, mockEditor.state.tr);
|
|
407
|
+
expect(mockEditor.view.dispatch).toHaveBeenNthCalledWith(2, mockEditor.state.tr);
|
|
385
408
|
expect(mockEditor._toolbarOpened).toBe(false);
|
|
386
409
|
expect(mockEditor.commands.focus).toHaveBeenCalled();
|
|
387
410
|
});
|
|
@@ -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();
|
|
@@ -5,6 +5,7 @@ import { NodeSelection } from 'prosemirror-state';
|
|
|
5
5
|
import { Chevron } from '../icons/RespArea';
|
|
6
6
|
import ReactDOM from 'react-dom';
|
|
7
7
|
import CustomToolbarWrapper from '../../extensions/custom-toolbar-wrapper';
|
|
8
|
+
import { setToolbarOpened } from '../../utils/toolbar';
|
|
8
9
|
|
|
9
10
|
const InlineDropdown = (props) => {
|
|
10
11
|
const { editor, node, getPos, options, selected } = props;
|
|
@@ -174,14 +175,14 @@ const InlineDropdown = (props) => {
|
|
|
174
175
|
{showToolbar && (
|
|
175
176
|
<React.Fragment>
|
|
176
177
|
{ReactDOM.createPortal(
|
|
177
|
-
<div ref={toolbarRef}
|
|
178
|
+
<div ref={toolbarRef}>
|
|
178
179
|
<InlineDropdownToolbar
|
|
179
180
|
editorCallback={(instance) => {
|
|
180
181
|
toolbarEditor.current = instance;
|
|
181
182
|
}}
|
|
182
183
|
/>
|
|
183
184
|
</div>,
|
|
184
|
-
document.body,
|
|
185
|
+
editor?._tiptapContainerEl || document.body,
|
|
185
186
|
)}
|
|
186
187
|
|
|
187
188
|
{editor._tiptapContainerEl &&
|
|
@@ -196,7 +197,7 @@ const InlineDropdown = (props) => {
|
|
|
196
197
|
tr.delete(pos, pos + node.nodeSize);
|
|
197
198
|
// Prevent the debounced onBlur/onDone from firing into the
|
|
198
199
|
// now-deleted node's stale position
|
|
199
|
-
editor
|
|
200
|
+
setToolbarOpened(editor, false);
|
|
200
201
|
delete editor._holdInlineDropdownToolbarIndex;
|
|
201
202
|
editor.view.dispatch(tr);
|
|
202
203
|
setShowToolbar(false);
|