@pie-lib/editable-html-tip-tap 2.1.6 → 2.1.8
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 +20 -0
- package/lib/components/EditableHtml.js +3 -1
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +5 -0
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/TiptapContainer.js +1 -0
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +1 -1
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/extensions/css.js +17 -7
- package/lib/extensions/css.js.map +1 -1
- package/lib/extensions/math.js +83 -14
- package/lib/extensions/math.js.map +1 -1
- package/package.json +3 -3
- package/src/components/EditableHtml.jsx +1 -1
- package/src/components/MenuBar.jsx +5 -0
- package/src/components/TiptapContainer.jsx +1 -0
- package/src/components/__tests__/InlineDropdown.test.jsx +79 -0
- package/src/components/respArea/InlineDropdown.jsx +1 -1
- package/src/extensions/__tests__/math.test.js +146 -9
- package/src/extensions/css.js +16 -3
- package/src/extensions/math.js +92 -15
|
@@ -185,6 +185,85 @@ describe('InlineDropdown', () => {
|
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
it('uses the current node when closing on outside click after the node prop changes', async () => {
|
|
189
|
+
const onToolbarCloseRequest = jest.fn((_tuple, _editor, onConfirm) => onConfirm());
|
|
190
|
+
const options = {
|
|
191
|
+
...mockOptions,
|
|
192
|
+
onToolbarCloseRequest,
|
|
193
|
+
};
|
|
194
|
+
const updatedNode = {
|
|
195
|
+
attrs: { index: '1', value: 'Updated Option', error: false },
|
|
196
|
+
nodeSize: 1,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const { queryByTestId, rerender } = render(<InlineDropdown {...defaultProps} options={options} selected={true} />);
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
rerender(<InlineDropdown {...defaultProps} options={options} node={updatedNode} selected={true} />);
|
|
206
|
+
|
|
207
|
+
fireEvent.mouseDown(document.body);
|
|
208
|
+
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
expect(onToolbarCloseRequest).toHaveBeenCalledWith(
|
|
211
|
+
[updatedNode, 5],
|
|
212
|
+
expect.anything(),
|
|
213
|
+
expect.any(Function),
|
|
214
|
+
expect.any(Function),
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('respects hold state for the current node after the node prop changes', async () => {
|
|
220
|
+
const updatedNode = {
|
|
221
|
+
attrs: { index: '1', value: 'Updated Option', error: false },
|
|
222
|
+
nodeSize: 1,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
mockEditor._holdInlineDropdownToolbarIndex = '1';
|
|
226
|
+
|
|
227
|
+
const { queryByTestId, rerender } = render(<InlineDropdown {...defaultProps} selected={true} />);
|
|
228
|
+
|
|
229
|
+
await waitFor(() => {
|
|
230
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
rerender(<InlineDropdown {...defaultProps} node={updatedNode} selected={true} />);
|
|
234
|
+
|
|
235
|
+
fireEvent.mouseDown(document.body);
|
|
236
|
+
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('rebinds the outside-click listener when the node prop changes while the toolbar is open', async () => {
|
|
243
|
+
const addSpy = jest.spyOn(document, 'addEventListener');
|
|
244
|
+
const removeSpy = jest.spyOn(document, 'removeEventListener');
|
|
245
|
+
const updatedNode = { ...mockNode, attrs: { ...mockNode.attrs, value: 'Updated Option' } };
|
|
246
|
+
|
|
247
|
+
const { rerender } = render(<InlineDropdown {...defaultProps} selected={true} />);
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(addSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const removeCallsBeforeRerender = removeSpy.mock.calls.filter(([type]) => type === 'mousedown').length;
|
|
254
|
+
|
|
255
|
+
rerender(<InlineDropdown {...defaultProps} node={updatedNode} selected={true} />);
|
|
256
|
+
|
|
257
|
+
await waitFor(() => {
|
|
258
|
+
const removeCallsAfterRerender = removeSpy.mock.calls.filter(([type]) => type === 'mousedown').length;
|
|
259
|
+
expect(removeCallsAfterRerender).toBeGreaterThan(removeCallsBeforeRerender);
|
|
260
|
+
expect(addSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
addSpy.mockRestore();
|
|
264
|
+
removeSpy.mockRestore();
|
|
265
|
+
});
|
|
266
|
+
|
|
188
267
|
it('has correct border styling', () => {
|
|
189
268
|
const { container } = render(<InlineDropdown {...defaultProps} />);
|
|
190
269
|
const dropdownDiv = container.querySelector('div[style*="border"]');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, waitFor, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { render, waitFor, fireEvent, act } from '@testing-library/react';
|
|
3
3
|
import { EnsureTextAfterMathPlugin, MathNode, MathNodeView, ZeroWidthSpaceHandlingPlugin } from '../math';
|
|
4
4
|
import * as toolbarUtils from '../../utils/toolbar';
|
|
5
5
|
|
|
@@ -390,6 +390,7 @@ describe('MathNodeView', () => {
|
|
|
390
390
|
selection: {
|
|
391
391
|
from: 0,
|
|
392
392
|
to: 1,
|
|
393
|
+
node: { type: { name: 'math' } },
|
|
393
394
|
},
|
|
394
395
|
tr: {
|
|
395
396
|
setSelection: jest.fn().mockReturnThis(),
|
|
@@ -467,30 +468,31 @@ describe('MathNodeView', () => {
|
|
|
467
468
|
});
|
|
468
469
|
|
|
469
470
|
describe('toolbar positioning', () => {
|
|
470
|
-
it('positions relative to
|
|
471
|
+
it('positions relative to portal container using coordsAtPos', async () => {
|
|
471
472
|
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
472
473
|
await waitFor(() => {
|
|
473
474
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
474
475
|
expect(toolbar).toBeInTheDocument();
|
|
475
|
-
expect(toolbar.style.top).toBe('
|
|
476
|
+
expect(toolbar.style.top).toBe('100px');
|
|
476
477
|
expect(toolbar.style.left).toBe('50px');
|
|
477
478
|
});
|
|
478
479
|
});
|
|
479
480
|
|
|
480
|
-
it('
|
|
481
|
-
const
|
|
481
|
+
it('offsets position by portal container getBoundingClientRect', async () => {
|
|
482
|
+
const containerEl = document.createElement('div');
|
|
483
|
+
containerEl.getBoundingClientRect = jest.fn(() => ({ top: 100, left: 50, width: 600, height: 400 }));
|
|
482
484
|
|
|
483
485
|
const editor = {
|
|
484
486
|
...defaultProps.editor,
|
|
485
|
-
|
|
487
|
+
_tiptapContainerEl: containerEl,
|
|
486
488
|
};
|
|
487
489
|
|
|
488
490
|
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
489
491
|
await waitFor(() => {
|
|
490
492
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
491
493
|
expect(toolbar).toBeInTheDocument();
|
|
492
|
-
expect(toolbar.style.top).toBe('
|
|
493
|
-
expect(toolbar.style.left).toBe('
|
|
494
|
+
expect(toolbar.style.top).toBe('0px');
|
|
495
|
+
expect(toolbar.style.left).toBe('0px');
|
|
494
496
|
});
|
|
495
497
|
});
|
|
496
498
|
|
|
@@ -503,6 +505,15 @@ describe('MathNodeView', () => {
|
|
|
503
505
|
});
|
|
504
506
|
});
|
|
505
507
|
|
|
508
|
+
it('renders above other editor overlays with a high z-index', async () => {
|
|
509
|
+
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
510
|
+
await waitFor(() => {
|
|
511
|
+
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
512
|
+
expect(toolbar).toBeInTheDocument();
|
|
513
|
+
expect(toolbar.style.zIndex).toBe('1000');
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
506
517
|
it('updates position from coordsAtPos when selection changes', async () => {
|
|
507
518
|
const editor = {
|
|
508
519
|
...defaultProps.editor,
|
|
@@ -517,11 +528,64 @@ describe('MathNodeView', () => {
|
|
|
517
528
|
await waitFor(() => {
|
|
518
529
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
519
530
|
expect(toolbar).toBeInTheDocument();
|
|
520
|
-
expect(toolbar.style.top).toBe('
|
|
531
|
+
expect(toolbar.style.top).toBe('200px');
|
|
521
532
|
expect(toolbar.style.left).toBe('150px');
|
|
522
533
|
});
|
|
523
534
|
});
|
|
524
535
|
|
|
536
|
+
it('clamps toolbar position to viewport margins', async () => {
|
|
537
|
+
const originalInnerHeight = window.innerHeight;
|
|
538
|
+
const originalInnerWidth = window.innerWidth;
|
|
539
|
+
|
|
540
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, writable: true, value: 200 });
|
|
541
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: 300 });
|
|
542
|
+
|
|
543
|
+
const editor = {
|
|
544
|
+
...defaultProps.editor,
|
|
545
|
+
view: {
|
|
546
|
+
...defaultProps.editor.view,
|
|
547
|
+
coordsAtPos: jest.fn(() => ({ top: 190, left: 280, bottom: 195 })),
|
|
548
|
+
dispatch: jest.fn(),
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
553
|
+
|
|
554
|
+
let toolbar;
|
|
555
|
+
await waitFor(() => {
|
|
556
|
+
toolbar = container.querySelector('[data-toolbar-for]');
|
|
557
|
+
expect(toolbar).toBeInTheDocument();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
Object.defineProperty(toolbar, 'offsetHeight', { configurable: true, value: 100 });
|
|
561
|
+
Object.defineProperty(toolbar, 'offsetWidth', { configurable: true, value: 150 });
|
|
562
|
+
|
|
563
|
+
await act(async () => {
|
|
564
|
+
window.dispatchEvent(new Event('resize'));
|
|
565
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await waitFor(() => {
|
|
569
|
+
expect(parseInt(toolbar.style.top, 10)).toBeLessThanOrEqual(200 - 100 - 8);
|
|
570
|
+
expect(parseInt(toolbar.style.left, 10)).toBeLessThanOrEqual(300 - 150 - 8);
|
|
571
|
+
expect(parseInt(toolbar.style.top, 10)).toBeGreaterThanOrEqual(8);
|
|
572
|
+
expect(parseInt(toolbar.style.left, 10)).toBeGreaterThanOrEqual(8);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, writable: true, value: originalInnerHeight });
|
|
576
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: originalInnerWidth });
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('attaches scroll and resize listeners while toolbar is open', async () => {
|
|
580
|
+
const addSpy = jest.spyOn(window, 'addEventListener');
|
|
581
|
+
render(<MathNodeView {...defaultProps} selected={true} />);
|
|
582
|
+
await waitFor(() => {
|
|
583
|
+
expect(addSpy).toHaveBeenCalledWith('scroll', expect.any(Function), true);
|
|
584
|
+
expect(addSpy).toHaveBeenCalledWith('resize', expect.any(Function));
|
|
585
|
+
});
|
|
586
|
+
addSpy.mockRestore();
|
|
587
|
+
});
|
|
588
|
+
|
|
525
589
|
it('portals toolbar into _tiptapContainerEl when available', async () => {
|
|
526
590
|
const containerEl = document.createElement('div');
|
|
527
591
|
containerEl.getBoundingClientRect = jest.fn(() => ({ top: 0, left: 0, width: 600, height: 400 }));
|
|
@@ -648,6 +712,31 @@ describe('MathNodeView', () => {
|
|
|
648
712
|
});
|
|
649
713
|
});
|
|
650
714
|
|
|
715
|
+
it('re-registers click listener when node changes', async () => {
|
|
716
|
+
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
|
|
717
|
+
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
|
|
718
|
+
const nodeA = { attrs: { latex: 'x^2' } };
|
|
719
|
+
const nodeB = { attrs: { latex: 'y^2' } };
|
|
720
|
+
|
|
721
|
+
const { rerender } = render(<MathNodeView {...defaultProps} node={nodeA} selected={true} />);
|
|
722
|
+
|
|
723
|
+
await waitFor(() => {
|
|
724
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const initialCallCount = addEventListenerSpy.mock.calls.length;
|
|
728
|
+
|
|
729
|
+
rerender(<MathNodeView {...defaultProps} node={nodeB} selected={true} />);
|
|
730
|
+
|
|
731
|
+
await waitFor(() => {
|
|
732
|
+
expect(removeEventListenerSpy).toHaveBeenCalled();
|
|
733
|
+
expect(addEventListenerSpy.mock.calls.length).toBeGreaterThan(initialCallCount);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
addEventListenerSpy.mockRestore();
|
|
737
|
+
removeEventListenerSpy.mockRestore();
|
|
738
|
+
});
|
|
739
|
+
|
|
651
740
|
it('does not close toolbar when clicking the math node preview', async () => {
|
|
652
741
|
const { getByTestId, queryByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
653
742
|
|
|
@@ -724,6 +813,54 @@ describe('MathNodeView', () => {
|
|
|
724
813
|
});
|
|
725
814
|
});
|
|
726
815
|
|
|
816
|
+
describe('selection-based toolbar guard', () => {
|
|
817
|
+
it('opens toolbar when selected transitions to true and the editor has a NodeSelection on math', async () => {
|
|
818
|
+
const { queryByTestId, rerender } = render(<MathNodeView {...defaultProps} selected={false} />);
|
|
819
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
820
|
+
|
|
821
|
+
rerender(<MathNodeView {...defaultProps} selected={true} />);
|
|
822
|
+
await waitFor(() => {
|
|
823
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('does not open toolbar when selected briefly becomes true but editor selection has no node (Cmd+A / drag case)', async () => {
|
|
828
|
+
const editor = {
|
|
829
|
+
...defaultProps.editor,
|
|
830
|
+
state: {
|
|
831
|
+
...defaultProps.editor.state,
|
|
832
|
+
selection: { from: 0, to: 100 }, // no .node — TextSelection / AllSelection shape
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const { queryByTestId, rerender } = render(
|
|
837
|
+
<MathNodeView {...defaultProps} editor={editor} selected={false} />,
|
|
838
|
+
);
|
|
839
|
+
rerender(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
840
|
+
|
|
841
|
+
await act(async () => {});
|
|
842
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('does not open toolbar when selected briefly becomes true but NodeSelection targets a non-math node', async () => {
|
|
846
|
+
const editor = {
|
|
847
|
+
...defaultProps.editor,
|
|
848
|
+
state: {
|
|
849
|
+
...defaultProps.editor.state,
|
|
850
|
+
selection: { from: 0, to: 1, node: { type: { name: 'image' } } },
|
|
851
|
+
},
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const { queryByTestId, rerender } = render(
|
|
855
|
+
<MathNodeView {...defaultProps} editor={editor} selected={false} />,
|
|
856
|
+
);
|
|
857
|
+
rerender(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
858
|
+
|
|
859
|
+
await act(async () => {});
|
|
860
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
727
864
|
it('does not close toolbar when clicking equation editor dropdown', async () => {
|
|
728
865
|
const { queryByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
729
866
|
|
package/src/extensions/css.js
CHANGED
|
@@ -169,14 +169,27 @@ export const CSSMark = Mark.create({
|
|
|
169
169
|
},
|
|
170
170
|
|
|
171
171
|
parseHTML() {
|
|
172
|
-
// Any span with a class that matches one of allowed classes
|
|
172
|
+
// Any span with a class that matches one of allowed classes any span that carries a class attribute
|
|
173
|
+
// so that pre-existing spans are preserved when loading content
|
|
173
174
|
return [
|
|
174
175
|
{
|
|
175
176
|
tag: 'span[class]',
|
|
176
177
|
getAttrs: (el) => {
|
|
177
178
|
const cls = el.getAttribute('class') || '';
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
|
|
180
|
+
if (!cls) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const allowedClasses = (this.options && this.options.classes) || [];
|
|
185
|
+
|
|
186
|
+
if (allowedClasses.length > 0) {
|
|
187
|
+
const match = this.options.classes.find((name) => cls.includes(name));
|
|
188
|
+
|
|
189
|
+
return match ? { class: match } : false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { class: cls };
|
|
180
193
|
},
|
|
181
194
|
},
|
|
182
195
|
];
|
package/src/extensions/math.js
CHANGED
|
@@ -23,7 +23,9 @@ export const EnsureTextAfterMathPlugin = (mathNodeName) =>
|
|
|
23
23
|
key: ensureTextAfterMathPluginKey,
|
|
24
24
|
appendTransaction: (transactions, oldState, newState) => {
|
|
25
25
|
// Only act when the doc actually changed
|
|
26
|
-
if (!transactions.some((tr) => tr.docChanged))
|
|
26
|
+
if (!transactions.some((tr) => tr.docChanged)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
27
29
|
|
|
28
30
|
const tr = newState.tr;
|
|
29
31
|
let changed = false;
|
|
@@ -202,6 +204,7 @@ export const MathNodeView = (props) => {
|
|
|
202
204
|
const { node, updateAttributes, editor, selected, options } = props;
|
|
203
205
|
const [showToolbar, setShowToolbar] = useState(selected);
|
|
204
206
|
const toolbarRef = useRef(null);
|
|
207
|
+
const nodeRef = useRef(null);
|
|
205
208
|
const timestamp = useRef(Date.now());
|
|
206
209
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
207
210
|
const { math: mathOptions = {} } = options || {};
|
|
@@ -232,28 +235,102 @@ export const MathNodeView = (props) => {
|
|
|
232
235
|
editor.commands.focus();
|
|
233
236
|
};
|
|
234
237
|
|
|
238
|
+
// Only open the toolbar when this node is *explicitly* selected
|
|
239
|
+
// via a NodeSelection — not when it's merely included in a broader
|
|
240
|
+
// TextSelection or AllSelection (e.g. click-drag across math, or Cmd+A).
|
|
235
241
|
useEffect(() => {
|
|
236
|
-
if (selected)
|
|
242
|
+
if (!selected) return;
|
|
243
|
+
|
|
244
|
+
const { selection } = editor.state;
|
|
245
|
+
const isNodeSelected = selection.node?.type?.name === 'math';
|
|
246
|
+
|
|
247
|
+
if (isNodeSelected) {
|
|
237
248
|
setShowToolbar(true);
|
|
238
249
|
}
|
|
239
|
-
}, [selected]);
|
|
250
|
+
}, [selected, editor]);
|
|
240
251
|
|
|
241
252
|
useEffect(() => {
|
|
242
253
|
setToolbarOpened(editor, selected || showToolbar);
|
|
243
254
|
}, [editor, showToolbar, selected]);
|
|
244
255
|
|
|
245
256
|
useEffect(() => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
if (!editor || !showToolbar) {
|
|
258
|
+
setPosition({ top: 0, left: 0 });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Clamp in viewport coordinates, then convert to portal-container-relative values
|
|
263
|
+
// for position: absolute (toolbar is portaled into _tiptapContainerEl or document.body).
|
|
264
|
+
const updatePosition = () => {
|
|
265
|
+
if (!toolbarRef.current) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { from } = editor.state.selection;
|
|
270
|
+
const start = editor.view.coordsAtPos(from);
|
|
271
|
+
const nodeRect = nodeRef.current?.getBoundingClientRect?.();
|
|
272
|
+
|
|
273
|
+
// Anchor to the math node element when available; fall back to selection coords.
|
|
274
|
+
const anchorTop = nodeRect?.height ? nodeRect.top : start.top;
|
|
275
|
+
const anchorLeft = nodeRect?.width ? nodeRect.left : start.left;
|
|
276
|
+
const anchorBottom = nodeRect?.height ? nodeRect.bottom : (start.bottom ?? start.top);
|
|
277
|
+
|
|
278
|
+
const toolbarHeight = toolbarRef.current.offsetHeight;
|
|
279
|
+
const toolbarWidth = toolbarRef.current.offsetWidth;
|
|
280
|
+
|
|
281
|
+
const gap = 0;
|
|
282
|
+
const spaceBelow = window.innerHeight - (anchorBottom + gap);
|
|
283
|
+
|
|
284
|
+
// Place the toolbar's top-left corner directly below the anchor; flip above when needed.
|
|
285
|
+
let top = spaceBelow >= toolbarHeight ? anchorBottom + gap : anchorTop - toolbarHeight - gap;
|
|
286
|
+
let left = anchorLeft;
|
|
287
|
+
|
|
288
|
+
const margin = 8;
|
|
289
|
+
top = Math.max(margin, Math.min(top, window.innerHeight - toolbarHeight - margin));
|
|
290
|
+
left = Math.max(margin, Math.min(left, window.innerWidth - toolbarWidth - margin));
|
|
291
|
+
|
|
292
|
+
const portalEl = editor._tiptapContainerEl || document.body;
|
|
293
|
+
const containerRect = portalEl.getBoundingClientRect();
|
|
294
|
+
|
|
295
|
+
setPosition({
|
|
296
|
+
top: top - containerRect.top,
|
|
297
|
+
left: left - containerRect.left,
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
updatePosition();
|
|
302
|
+
|
|
303
|
+
let frame = null;
|
|
304
|
+
const scheduleUpdate = () => {
|
|
305
|
+
if (frame !== null) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
frame = requestAnimationFrame(() => {
|
|
310
|
+
frame = null;
|
|
311
|
+
updatePosition();
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
frame = requestAnimationFrame(() => {
|
|
316
|
+
frame = null;
|
|
317
|
+
updatePosition();
|
|
255
318
|
});
|
|
256
319
|
|
|
320
|
+
window.addEventListener('scroll', scheduleUpdate, true);
|
|
321
|
+
window.addEventListener('resize', scheduleUpdate);
|
|
322
|
+
|
|
323
|
+
return () => {
|
|
324
|
+
if (frame !== null) {
|
|
325
|
+
cancelAnimationFrame(frame);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
window.removeEventListener('scroll', scheduleUpdate, true);
|
|
329
|
+
window.removeEventListener('resize', scheduleUpdate);
|
|
330
|
+
};
|
|
331
|
+
}, [editor, showToolbar]);
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
257
334
|
const handleClickOutside = (event) => {
|
|
258
335
|
const target = event?.target;
|
|
259
336
|
|
|
@@ -298,7 +375,7 @@ export const MathNodeView = (props) => {
|
|
|
298
375
|
}
|
|
299
376
|
|
|
300
377
|
return () => document.removeEventListener('click', handleClickOutside);
|
|
301
|
-
}, [editor, showToolbar]);
|
|
378
|
+
}, [editor, showToolbar, node]);
|
|
302
379
|
|
|
303
380
|
return (
|
|
304
381
|
<NodeViewWrapper
|
|
@@ -310,7 +387,7 @@ export const MathNodeView = (props) => {
|
|
|
310
387
|
}}
|
|
311
388
|
data-selected={selected}
|
|
312
389
|
>
|
|
313
|
-
<div onClick={() => setShowToolbar(true)} contentEditable={false}>
|
|
390
|
+
<div ref={nodeRef} onClick={() => setShowToolbar(true)} contentEditable={false}>
|
|
314
391
|
<MathPreview latex={latex} />
|
|
315
392
|
</div>
|
|
316
393
|
{showToolbar &&
|
|
@@ -322,7 +399,7 @@ export const MathNodeView = (props) => {
|
|
|
322
399
|
position: 'absolute',
|
|
323
400
|
top: `${position.top}px`,
|
|
324
401
|
left: `${position.left}px`,
|
|
325
|
-
zIndex:
|
|
402
|
+
zIndex: 1000,
|
|
326
403
|
background: 'var(--editable-html-toolbar-bg, #efefef)',
|
|
327
404
|
boxShadow:
|
|
328
405
|
'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)',
|