Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -27,6 +34,7 @@ describe('InlineDropdown', () => {
to: 1,
},
tr: mockTr,
doc: mockDoc,
},
view: {
coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,26 +10,85 @@ 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 || '<div>&nbsp</div>';
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;
const onlyThisNodeSelected = selection.from + node.nodeSize === selection.to;

if (selected) {
if (onlyThisNodeSelected) {
setShowToolbar(selected);
setShowToolbar(true);
}
} else {
setShowToolbar(selected);
} else if (showToolbar) {
requestClose();
}
}, [editor, node, selected]);

Expand All @@ -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();
}
};

Expand Down Expand Up @@ -105,19 +166,10 @@ const InlineDropdown = (props) => {
display: 'inline-block',
verticalAlign: 'middle',
}}
dangerouslySetInnerHTML={{
__html: html,
}}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
<Chevron
direction="down"
style={{
position: 'absolute',
top: '5px',
right: '5px',
}}
/>
<Chevron direction="down" style={{ position: 'absolute', top: '5px', right: '5px' }} />
</div>
{showToolbar && (
<React.Fragment>
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
4 changes: 2 additions & 2 deletions packages/editable-html-tip-tap/src/index.jsx
Original file line number Diff line number Diff line change
@@ -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;
Loading