diff --git a/packages/editable-html-tip-tap/src/components/__tests__/InlineDropdown.test.jsx b/packages/editable-html-tip-tap/src/components/__tests__/InlineDropdown.test.jsx
index 206716814..0a4345875 100644
--- a/packages/editable-html-tip-tap/src/components/__tests__/InlineDropdown.test.jsx
+++ b/packages/editable-html-tip-tap/src/components/__tests__/InlineDropdown.test.jsx
@@ -17,8 +17,15 @@ jest.mock('react-dom', () => ({
describe('InlineDropdown', () => {
const buildMockEditor = (overrides = {}) => {
+ const mockNodeAt = jest.fn((pos) => (pos === 5 ? { nodeSize: 1 } : null));
+ const mockDoc = {
+ descendants: jest.fn(),
+ nodeAt: mockNodeAt,
+ };
const mockTr = {
delete: jest.fn(),
+ doc: { nodeAt: mockNodeAt },
+ setSelection: jest.fn(),
};
return {
state: {
@@ -27,6 +34,7 @@ describe('InlineDropdown', () => {
to: 1,
},
tr: mockTr,
+ doc: mockDoc,
},
view: {
coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
diff --git a/packages/editable-html-tip-tap/src/components/respArea/InlineDropdown.jsx b/packages/editable-html-tip-tap/src/components/respArea/InlineDropdown.jsx
index f1ba32087..0dd26838f 100644
--- a/packages/editable-html-tip-tap/src/components/respArea/InlineDropdown.jsx
+++ b/packages/editable-html-tip-tap/src/components/respArea/InlineDropdown.jsx
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { NodeViewWrapper } from '@tiptap/react';
+import { NodeSelection } from 'prosemirror-state';
import { Chevron } from '../icons/RespArea';
import ReactDOM from 'react-dom';
import CustomToolbarWrapper from '../../extensions/custom-toolbar-wrapper';
@@ -9,15 +10,74 @@ const InlineDropdown = (props) => {
const { editor, node, getPos, options, selected } = props;
const { attrs: attributes } = node;
const { value, error } = attributes;
- // TODO: Investigate
- // Needed because items with values inside have different positioning for some reason
const html = value || '
 
';
const pos = getPos();
const toolbarRef = useRef(null);
const toolbarEditor = useRef(null);
+ const pendingCloseRequest = useRef(false);
+
+ const isHeld = () =>
+ editor._holdInlineDropdownToolbarIndex != null &&
+ String(editor._holdInlineDropdownToolbarIndex) === String(node.attrs.index);
+
const [showToolbar, setShowToolbar] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
- const InlineDropdownToolbar = options.respAreaToolbar([node, pos], editor, () => {});
+
+ const closeToolbar = () => {
+ if (isHeld()) {
+ return;
+ }
+
+ setShowToolbar(false);
+ };
+
+ const InlineDropdownToolbar = options.respAreaToolbar([node, pos], editor, closeToolbar);
+
+ const reselectNode = () => {
+ const { tr } = editor.state;
+ const nodeAtPos = tr.doc.nodeAt(pos);
+
+ if (!nodeAtPos) {
+ return;
+ }
+
+ const { selection } = tr;
+
+ if (selection.from === pos && selection.to === pos + nodeAtPos.nodeSize) {
+ return;
+ }
+
+ tr.setSelection(NodeSelection.create(tr.doc, pos));
+ editor.view.dispatch(tr);
+ };
+
+ const requestClose = () => {
+ if (pendingCloseRequest.current) {
+ return;
+ }
+
+ if (options.onToolbarCloseRequest) {
+ pendingCloseRequest.current = true;
+
+ options.onToolbarCloseRequest(
+ [node, pos],
+ editor,
+ () => {
+ pendingCloseRequest.current = false;
+ delete editor._holdInlineDropdownToolbarIndex;
+ closeToolbar();
+ },
+ () => {
+ pendingCloseRequest.current = false;
+ delete editor._holdInlineDropdownToolbarIndex;
+ setShowToolbar(true);
+ setTimeout(reselectNode, 0);
+ },
+ );
+ } else {
+ closeToolbar();
+ }
+ };
useEffect(() => {
const { selection } = editor.state;
@@ -25,10 +85,10 @@ const InlineDropdown = (props) => {
if (selected) {
if (onlyThisNodeSelected) {
- setShowToolbar(selected);
+ setShowToolbar(true);
}
- } else {
- setShowToolbar(selected);
+ } else if (showToolbar) {
+ requestClose();
}
}, [editor, node, selected]);
@@ -47,13 +107,14 @@ const InlineDropdown = (props) => {
const insideSomeEditor = event.target.closest('[data-toolbar-for]');
if (
- (!insideSomeEditor || insideSomeEditor.dataset.toolbarFor !== toolbarEditor.current.instanceId) &&
+ !event.target.closest('[data-inline-dropdown-toolbar]') &&
+ (!insideSomeEditor || insideSomeEditor.dataset.toolbarFor !== toolbarEditor.current?.instanceId) &&
!editor._toolbarOpened &&
toolbarRef.current &&
!toolbarRef.current.contains(event.target) &&
!event.target.closest('[data-inline-node]')
) {
- setShowToolbar(false);
+ requestClose();
}
};
@@ -105,19 +166,10 @@ const InlineDropdown = (props) => {
display: 'inline-block',
verticalAlign: 'middle',
}}
- dangerouslySetInnerHTML={{
- __html: html,
- }}
+ dangerouslySetInnerHTML={{ __html: html }}
/>
-
+
{showToolbar && (
@@ -145,6 +197,7 @@ const InlineDropdown = (props) => {
// Prevent the debounced onBlur/onDone from firing into the
// now-deleted node's stale position
editor._toolbarOpened = false;
+ delete editor._holdInlineDropdownToolbarIndex;
editor.view.dispatch(tr);
setShowToolbar(false);
editor.commands.focus();
diff --git a/packages/editable-html-tip-tap/src/components/respArea/inlineDropdownUtils.js b/packages/editable-html-tip-tap/src/components/respArea/inlineDropdownUtils.js
new file mode 100644
index 000000000..a0b8fbccf
--- /dev/null
+++ b/packages/editable-html-tip-tap/src/components/respArea/inlineDropdownUtils.js
@@ -0,0 +1,79 @@
+import { NodeSelection } from 'prosemirror-state';
+
+export const HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX = '_holdInlineDropdownToolbarIndex';
+
+export const findInlineDropdownPos = (editor, index) => {
+ let foundPos = null;
+
+ editor.state.doc.descendants((n, p) => {
+ if (n.type?.name === 'inline_dropdown' && String(n.attrs?.index) === String(index)) {
+ foundPos = p;
+ return false;
+ }
+
+ return true;
+ });
+
+ return foundPos;
+};
+
+export const holdInlineDropdownToolbar = (editor, index) => {
+ editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX] = index;
+};
+
+export const releaseInlineDropdownToolbarHold = (editor) => {
+ delete editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX];
+};
+
+export const isInlineDropdownToolbarHeld = (editor, index) =>
+ editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX] != null &&
+ String(editor[HOLD_INLINE_DROPDOWN_TOOLBAR_INDEX]) === String(index);
+
+export const selectInlineDropdownNode = (editor, index, fallbackPos) => {
+ const pos = findInlineDropdownPos(editor, index) ?? fallbackPos;
+
+ if (pos == null) {
+ return null;
+ }
+
+ const { tr } = editor.state;
+ const nodeAtPos = tr.doc.nodeAt(pos);
+
+ if (!nodeAtPos) {
+ return null;
+ }
+
+ const { selection } = tr;
+
+ if (selection.from === pos && selection.to === pos + nodeAtPos.nodeSize) {
+ return pos;
+ }
+
+ tr.setSelection(NodeSelection.create(tr.doc, pos));
+ editor.view.dispatch(tr);
+
+ return pos;
+};
+
+export const deleteInlineDropdownByIndex = (editor, index, fallbackPos) => {
+ const pos = findInlineDropdownPos(editor, index) ?? fallbackPos;
+
+ if (pos == null) {
+ releaseInlineDropdownToolbarHold(editor);
+ return false;
+ }
+
+ const { tr } = editor.state;
+ const nodeAtPos = tr.doc.nodeAt(pos);
+
+ if (!nodeAtPos) {
+ releaseInlineDropdownToolbarHold(editor);
+ return false;
+ }
+
+ tr.delete(pos, pos + nodeAtPos.nodeSize);
+ editor.view.dispatch(tr);
+ releaseInlineDropdownToolbarHold(editor);
+
+ return true;
+};
\ No newline at end of file
diff --git a/packages/editable-html-tip-tap/src/index.jsx b/packages/editable-html-tip-tap/src/index.jsx
index 7f51abfed..9f0018d4c 100644
--- a/packages/editable-html-tip-tap/src/index.jsx
+++ b/packages/editable-html-tip-tap/src/index.jsx
@@ -1,5 +1,5 @@
import StyledEditor, { EditableHtml } from './components/EditableHtml';
import { ALL_PLUGINS, DEFAULT_PLUGINS } from './extensions';
-
-export { EditableHtml, ALL_PLUGINS, DEFAULT_PLUGINS };
+import { deleteInlineDropdownByIndex } from './components/respArea/inlineDropdownUtils';
+export { EditableHtml, ALL_PLUGINS, DEFAULT_PLUGINS, deleteInlineDropdownByIndex };
export default StyledEditor;