@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.
@@ -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]: toolbarOpts.alwaysVisible || (editorState.isFocused && !editor._toolbarOpened && !editorState.hideDefaultToolbar),
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 })),
@@ -20,6 +20,7 @@ jest.mock('@tiptap/react', () => ({
20
20
  })),
21
21
  })),
22
22
  getAttributes: jest.fn(() => ({ border: '1' })),
23
+ commandManager: {},
23
24
  isFocused: true,
24
25
  state: {
25
26
  selection: {},
@@ -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>&nbsp</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
- const InlineDropdownToolbar = options.respAreaToolbar([node, pos], editor, () => {});
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(selected);
88
+ setShowToolbar(true);
29
89
  }
30
- } else {
31
- setShowToolbar(selected);
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
- (!insideSomeEditor || insideSomeEditor.dataset.toolbarFor !== toolbarEditor.current.instanceId) &&
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
- setShowToolbar(false);
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 _tiptapContainerEl for position calculation when available', async () => {
248
- const containerEl = document.createElement('div');
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
- // top = coordsAtPos.top (100) + Math.abs(containerEl.top (-50)) + 40 = 190
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('falls back to document.body when _tiptapContainerEl is not set', async () => {
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: undefined,
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
- // top = coordsAtPos.top (100) + Math.abs(document.body.top (0)) + 40 = 140
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('recalculates position when toolbar reopens', async () => {
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
- // top = 200 + Math.abs(0) + 40 = 240
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 { queryByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
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
 
@@ -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: start.top + Math.abs(bodyRect.top) + 40, // shift above
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;