@pie-lib/editable-html-tip-tap 2.1.2-next.3 → 2.1.3

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.1.2-next.3+0f77a8069",
6
+ "version": "2.1.3",
7
7
  "description": "",
8
8
  "license": "ISC",
9
9
  "main": "lib/index.js",
@@ -16,28 +16,28 @@
16
16
  "@dnd-kit/utilities": "3.2.2",
17
17
  "@mui/icons-material": "^7.3.4",
18
18
  "@mui/material": "^7.3.4",
19
- "@pie-lib/drag": "^4.0.2",
19
+ "@pie-lib/drag": "^4.0.3",
20
20
  "@pie-lib/math-input": "^8.1.0",
21
21
  "@pie-lib/math-rendering": "^5.0.2",
22
- "@pie-lib/math-toolbar": "^3.0.2",
23
- "@pie-lib/render-ui": "^6.1.0",
24
- "@tiptap/core": "3.0.9",
25
- "@tiptap/extension-character-count": "3.0.9",
26
- "@tiptap/extension-color": "3.0.9",
27
- "@tiptap/extension-image": "3.0.9",
28
- "@tiptap/extension-list-item": "3.0.9",
22
+ "@pie-lib/math-toolbar": "^3.0.3",
23
+ "@pie-lib/render-ui": "^6.1.1",
24
+ "@tiptap/core": "3.20.0",
25
+ "@tiptap/extension-character-count": "3.20.0",
26
+ "@tiptap/extension-color": "3.20.0",
27
+ "@tiptap/extension-image": "3.20.0",
28
+ "@tiptap/extension-list-item": "3.20.0",
29
29
  "@tiptap/extension-placeholder": "3.20.0",
30
- "@tiptap/extension-subscript": "3.0.9",
31
- "@tiptap/extension-superscript": "3.0.9",
32
- "@tiptap/extension-table": "3.0.9",
33
- "@tiptap/extension-table-cell": "3.0.9",
34
- "@tiptap/extension-table-header": "3.0.9",
35
- "@tiptap/extension-table-row": "3.0.9",
36
- "@tiptap/extension-text-align": "3.0.9",
37
- "@tiptap/extension-text-style": "3.0.9",
38
- "@tiptap/pm": "3.0.9",
39
- "@tiptap/react": "3.0.9",
40
- "@tiptap/starter-kit": "3.0.9",
30
+ "@tiptap/extension-subscript": "3.20.0",
31
+ "@tiptap/extension-superscript": "3.20.0",
32
+ "@tiptap/extension-table": "3.20.0",
33
+ "@tiptap/extension-table-cell": "3.20.0",
34
+ "@tiptap/extension-table-header": "3.20.0",
35
+ "@tiptap/extension-table-row": "3.20.0",
36
+ "@tiptap/extension-text-align": "3.20.0",
37
+ "@tiptap/extension-text-style": "3.20.0",
38
+ "@tiptap/pm": "3.20.0",
39
+ "@tiptap/react": "3.20.0",
40
+ "@tiptap/starter-kit": "3.20.0",
41
41
  "change-case": "^3.0.2",
42
42
  "classnames": "^2.2.6",
43
43
  "debug": "^4.1.1",
@@ -59,6 +59,6 @@
59
59
  "peerDependencies": {
60
60
  "react": "^18.2.0"
61
61
  },
62
- "gitHead": "0f77a806995c7b7f717af1dc18d0f3f6a138d1db",
62
+ "gitHead": "381e012542b7849f83aa84b08e4b6664cedff245",
63
63
  "scripts": {}
64
64
  }
@@ -336,6 +336,7 @@ describe('EditableHtml', () => {
336
336
  const editorConfig = useEditor.mock.calls[useEditor.mock.calls.length - 1][0];
337
337
  const blurEditor = {
338
338
  getHTML: jest.fn(() => '<p>changed</p>'),
339
+ schema: {},
339
340
  _insertingImage: true,
340
341
  _toolbarOpened: false,
341
342
  isActive: jest.fn(() => false),
@@ -350,6 +351,85 @@ describe('EditableHtml', () => {
350
351
  jest.useRealTimers();
351
352
  });
352
353
 
354
+ it('does not run blur onChange/onDone when editor has no schema', async () => {
355
+ jest.useFakeTimers();
356
+ const onChange = jest.fn();
357
+ const onDone = jest.fn();
358
+
359
+ render(
360
+ <EditableHtml
361
+ {...defaultProps}
362
+ markup="<p>Hello World</p>"
363
+ onChange={onChange}
364
+ onDone={onDone}
365
+ toolbarOpts={{ doneOn: 'blur' }}
366
+ />,
367
+ );
368
+
369
+ await waitFor(() => {
370
+ expect(useEditor).toHaveBeenCalled();
371
+ });
372
+
373
+ const editorConfig = useEditor.mock.calls[useEditor.mock.calls.length - 1][0];
374
+ const getHTML = jest.fn(() => '<p>changed</p>');
375
+ const blurEditor = {
376
+ getHTML,
377
+ schema: undefined,
378
+ _insertingImage: false,
379
+ _toolbarOpened: false,
380
+ isActive: jest.fn(() => false),
381
+ };
382
+
383
+ editorConfig.onBlur({ editor: blurEditor });
384
+ jest.advanceTimersByTime(200);
385
+
386
+ expect(getHTML).not.toHaveBeenCalled();
387
+ expect(onChange).not.toHaveBeenCalled();
388
+ expect(onDone).not.toHaveBeenCalled();
389
+
390
+ jest.useRealTimers();
391
+ });
392
+
393
+ it('calls getHTML once on blur and passes the same html to onChange and onDone', async () => {
394
+ jest.useFakeTimers();
395
+ const onChange = jest.fn();
396
+ const onDone = jest.fn();
397
+ const html = '<p>from editor</p>';
398
+
399
+ render(
400
+ <EditableHtml
401
+ {...defaultProps}
402
+ markup="<p>Hello World</p>"
403
+ onChange={onChange}
404
+ onDone={onDone}
405
+ toolbarOpts={{ doneOn: 'blur' }}
406
+ />,
407
+ );
408
+
409
+ await waitFor(() => {
410
+ expect(useEditor).toHaveBeenCalled();
411
+ });
412
+
413
+ const editorConfig = useEditor.mock.calls[useEditor.mock.calls.length - 1][0];
414
+ const getHTML = jest.fn(() => html);
415
+ const blurEditor = {
416
+ getHTML,
417
+ schema: {},
418
+ _insertingImage: false,
419
+ _toolbarOpened: false,
420
+ isActive: jest.fn(() => false),
421
+ };
422
+
423
+ editorConfig.onBlur({ editor: blurEditor });
424
+ jest.advanceTimersByTime(200);
425
+
426
+ expect(getHTML).toHaveBeenCalledTimes(1);
427
+ expect(onChange).toHaveBeenCalledWith(html);
428
+ expect(onDone).toHaveBeenCalledWith(html);
429
+
430
+ jest.useRealTimers();
431
+ });
432
+
353
433
  describe('onUpdate callback', () => {
354
434
  it('calls onChange when transaction.isDone is true', async () => {
355
435
  const onChange = jest.fn();
@@ -375,7 +455,7 @@ describe('EditableHtml', () => {
375
455
  expect(onChange).toHaveBeenCalledWith('<p>Updated content</p>');
376
456
  });
377
457
 
378
- it('calls onChange when markup differs from editor HTML', async () => {
458
+ it('does not call onChange when transaction.isDone is false even if markup differs from editor HTML', async () => {
379
459
  const onChange = jest.fn();
380
460
  const markup = '<p>Initial content</p>';
381
461
 
@@ -396,7 +476,7 @@ describe('EditableHtml', () => {
396
476
 
397
477
  editorConfig.onUpdate({ editor: mockEditor, transaction: mockTransaction });
398
478
 
399
- expect(onChange).toHaveBeenCalledWith('<p>Different content</p>');
479
+ expect(onChange).not.toHaveBeenCalled();
400
480
  });
401
481
 
402
482
  it('does not call onChange when transaction.isDone is false and markup matches editor HTML', async () => {
@@ -29,9 +29,12 @@ export function CharacterPicker({ editor, opts, onClose }) {
29
29
  }
30
30
 
31
31
  const containerRef = useRef(null);
32
+ const onCloseRef = useRef(onClose);
32
33
  const [position, setPosition] = useState({ top: 0, left: 0 });
33
34
  const [popover, setPopover] = useState(null);
34
35
 
36
+ onCloseRef.current = onClose;
37
+
35
38
  const configToUse = useMemo(() => {
36
39
  if (!opts) return spanishConfig;
37
40
 
@@ -69,6 +72,9 @@ export function CharacterPicker({ editor, opts, onClose }) {
69
72
  [],
70
73
  );
71
74
 
75
+ // Keep `onClose` out of the dependency array — parents often pass a new callback each
76
+ // render (e.g. after each keystroke), which would re-run this effect constantly. Use a
77
+ // ref so click-outside always calls the latest close handler.
72
78
  useEffect(() => {
73
79
  if (!editor) return;
74
80
 
@@ -86,14 +92,15 @@ export function CharacterPicker({ editor, opts, onClose }) {
86
92
  }
87
93
 
88
94
  setPosition({
89
- // top: start.top + Math.abs(bodyRect.top) - containerRef.current.offsetHeight - 10 + additionalTopOffset, // shift above
90
95
  top: top,
91
96
  left: start.left,
92
97
  });
93
98
 
99
+ const editorViewDom = editor.view.dom;
100
+
94
101
  const handleClickOutside = (e) => {
95
- if (containerRef.current && !containerRef.current.contains(e.target) && !editor.view.dom.contains(e.target)) {
96
- onClose();
102
+ if (containerRef.current && !containerRef.current.contains(e.target) && !editorViewDom.contains(e.target)) {
103
+ onCloseRef.current();
97
104
  }
98
105
  };
99
106
 
@@ -105,7 +112,7 @@ export function CharacterPicker({ editor, opts, onClose }) {
105
112
  clearTimeout(timeoutId);
106
113
  document.removeEventListener('click', handleClickOutside);
107
114
  };
108
- }, [editor, onClose]);
115
+ }, [editor]);
109
116
 
110
117
  const renderPopOver = (event, el) => setPopover({ anchorEl: event.currentTarget, el });
111
118
 
@@ -288,29 +288,31 @@ export const EditableHtml = (props) => {
288
288
  editable: !props.disabled,
289
289
  content: normalizeInitialMarkup(props.markup),
290
290
  onUpdate: ({ editor, transaction }) => {
291
- if (transaction.isDone || props.markup !== editor.getHTML()) {
291
+ if (transaction.isDone) {
292
292
  props.onChange?.(editor.getHTML());
293
293
  }
294
294
  },
295
- onBlur: debounce(({ editor }) => {
295
+ onBlur: ({ editor }) => {
296
296
  const otherToolbarOpened =
297
297
  editor._insertingImage ||
298
298
  editor._toolbarOpened ||
299
299
  editor.isActive('inline_dropdown') ||
300
300
  editor.isActive('explicit_constructed_response');
301
301
 
302
- if (otherToolbarOpened) {
302
+ if (otherToolbarOpened || !editor.schema) {
303
303
  return;
304
304
  }
305
305
 
306
- if (props.markup !== editor.getHTML()) {
307
- props.onChange?.(editor.getHTML());
306
+ const html = editor.getHTML();
307
+
308
+ if (props.markup !== html) {
309
+ props.onChange?.(html);
308
310
  }
309
311
 
310
312
  if (toolbarOptsToUse.doneOn === 'blur') {
311
- props.onDone?.(editor.getHTML());
313
+ props.onDone?.(html);
312
314
  }
313
- }, 200),
315
+ },
314
316
  },
315
317
  [props.charactersLimit],
316
318
  );
@@ -428,7 +430,7 @@ const StyledEditorContent = styled(EditorContent, {
428
430
  },
429
431
  }),
430
432
  ...(separateParagraph && {
431
- '& > div:has(+ div)': {
433
+ '& > p:has(+ p)': {
432
434
  marginBottom: '1em',
433
435
  },
434
436
  }),
@@ -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,
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { fireEvent, render, waitFor } from '@testing-library/react';
2
+ import { act, fireEvent, render, waitFor } from '@testing-library/react';
3
3
  import { CharacterIcon, CharacterPicker } from '../CharacterPicker';
4
4
 
5
5
  jest.mock('react-dom', () => ({
@@ -128,13 +128,55 @@ describe('CharacterPicker', () => {
128
128
  };
129
129
  render(<CharacterPicker editor={mockEditor} opts={opts} onClose={onClose} />);
130
130
 
131
+ await act(async () => {
132
+ await new Promise((r) => setTimeout(r, 0));
133
+ });
134
+ fireEvent.click(document.body);
135
+
131
136
  await waitFor(() => {
132
- setTimeout(() => {
133
- fireEvent.click(document.body);
134
- }, 0);
137
+ expect(onClose).toHaveBeenCalled();
135
138
  });
139
+ });
136
140
 
137
- expect(onClose).toHaveBeenCalled();
141
+ it('does not re-run positioning when only onClose reference changes', async () => {
142
+ const opts = {
143
+ characters: [['á', 'é']],
144
+ };
145
+ const getRect = mockEditor.options.element.getBoundingClientRect;
146
+ const { rerender } = render(<CharacterPicker editor={mockEditor} opts={opts} onClose={jest.fn()} />);
147
+
148
+ await act(async () => {
149
+ await new Promise((r) => setTimeout(r, 0));
150
+ });
151
+
152
+ const callsAfterMount = getRect.mock.calls.length;
153
+ expect(callsAfterMount).toBeGreaterThan(0);
154
+
155
+ rerender(<CharacterPicker editor={mockEditor} opts={opts} onClose={jest.fn()} />);
156
+
157
+ expect(getRect.mock.calls.length).toBe(callsAfterMount);
158
+ });
159
+
160
+ it('outside click invokes the latest onClose after rerender', async () => {
161
+ const opts = {
162
+ characters: [['á', 'é']],
163
+ };
164
+ const onCloseFirst = jest.fn();
165
+ const { rerender } = render(<CharacterPicker editor={mockEditor} opts={opts} onClose={onCloseFirst} />);
166
+
167
+ await act(async () => {
168
+ await new Promise((r) => setTimeout(r, 0));
169
+ });
170
+
171
+ const onCloseSecond = jest.fn();
172
+ rerender(<CharacterPicker editor={mockEditor} opts={opts} onClose={onCloseSecond} />);
173
+
174
+ fireEvent.click(document.body);
175
+
176
+ await waitFor(() => {
177
+ expect(onCloseSecond).toHaveBeenCalled();
178
+ });
179
+ expect(onCloseFirst).not.toHaveBeenCalled();
138
180
  });
139
181
 
140
182
  it('does not close when clicking inside picker', async () => {
@@ -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
+ };
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;