@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/CHANGELOG.md +16 -0
- package/lib/components/CharacterPicker.js +10 -4
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +9 -8
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +6 -3
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +52 -8
- 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/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/EditableHtml.test.jsx +82 -2
- package/src/components/CharacterPicker.jsx +11 -4
- package/src/components/EditableHtml.jsx +10 -8
- package/src/components/MenuBar.jsx +6 -1
- package/src/components/__tests__/CharacterPicker.test.jsx +47 -5
- package/src/components/__tests__/InlineDropdown.test.jsx +8 -0
- package/src/components/__tests__/MenuBar.test.jsx +1 -0
- package/src/components/respArea/InlineDropdown.jsx +72 -19
- package/src/components/respArea/inlineDropdownUtils.js +79 -0
- package/src/index.jsx +2 -2
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "2.1.
|
|
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.
|
|
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.
|
|
23
|
-
"@pie-lib/render-ui": "^6.1.
|
|
24
|
-
"@tiptap/core": "3.0
|
|
25
|
-
"@tiptap/extension-character-count": "3.0
|
|
26
|
-
"@tiptap/extension-color": "3.0
|
|
27
|
-
"@tiptap/extension-image": "3.0
|
|
28
|
-
"@tiptap/extension-list-item": "3.0
|
|
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
|
|
31
|
-
"@tiptap/extension-superscript": "3.0
|
|
32
|
-
"@tiptap/extension-table": "3.0
|
|
33
|
-
"@tiptap/extension-table-cell": "3.0
|
|
34
|
-
"@tiptap/extension-table-header": "3.0
|
|
35
|
-
"@tiptap/extension-table-row": "3.0
|
|
36
|
-
"@tiptap/extension-text-align": "3.0
|
|
37
|
-
"@tiptap/extension-text-style": "3.0
|
|
38
|
-
"@tiptap/pm": "3.0
|
|
39
|
-
"@tiptap/react": "3.0
|
|
40
|
-
"@tiptap/starter-kit": "3.0
|
|
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": "
|
|
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('
|
|
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).
|
|
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) && !
|
|
96
|
-
|
|
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
|
|
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
|
|
291
|
+
if (transaction.isDone) {
|
|
292
292
|
props.onChange?.(editor.getHTML());
|
|
293
293
|
}
|
|
294
294
|
},
|
|
295
|
-
onBlur:
|
|
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
|
-
|
|
307
|
-
|
|
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?.(
|
|
313
|
+
props.onDone?.(html);
|
|
312
314
|
}
|
|
313
|
-
},
|
|
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
|
-
'& >
|
|
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]:
|
|
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
|
-
|
|
133
|
-
fireEvent.click(document.body);
|
|
134
|
-
}, 0);
|
|
137
|
+
expect(onClose).toHaveBeenCalled();
|
|
135
138
|
});
|
|
139
|
+
});
|
|
136
140
|
|
|
137
|
-
|
|
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 })),
|
|
@@ -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> </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
|
-
|
|
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(
|
|
88
|
+
setShowToolbar(true);
|
|
29
89
|
}
|
|
30
|
-
} else {
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|