The provided code defines a custom floating menu component called CustomFloatingMenu
in React. This component renders a menu with toggle buttons for bullet lists, ordered lists, articles, and images. When users interact with the menu, it triggers various actions such as toggling between different list types, opening a dialog to insert a document, or uploading and inserting an image. The component aims to provide a user-friendly interface for editing and enhancing content within an editor.
import React, { useState } from 'react';
import { isNodeEmpty } from '@tiptap/react';
import API from '@common/crud';
import { Menu, ToggleButtonGroup, ToggleButton } from '@mui/material';
import ArticleIcon from '@mui/icons-material/Article';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
import ImageIcon from '@mui/icons-material/Image';
import InsertDocumentDialog from '@forethought/pages/document/menus/insert-document-dialog';
const CustomFloatingMenu = ({ anchorEl, onClose, editor, node, pos: nodeStartPos }) => {
const [openDialog, setOpenDialog] = useState(false);
const nodeSize = node.content.size;
const emptyNodeStartPos = nodeStartPos + 2; // https://tinyurl.com/emptysize
const endOfNodeContent = nodeStartPos + nodeSize;
/**
* @param {string} listType - 'toggleBulletList' or 'toggleOrderedList'
*/
const handleToggleEmptyNode = listType => {
editor.chain().focus(emptyNodeStartPos)[listType]().run();
};
const handleTogglePopulatedNode = listType => {
editor.chain().focus(endOfNodeContent).run();
editor.chain().enter().run();
editor
.chain()
.focus(endOfNodeContent + 4) // Account for the enter command
[listType]()
.run();
};
const handleToggleList = listType => {
if (isNodeEmpty(node)) {
handleToggleEmptyNode(listType);
} else {
handleTogglePopulatedNode(listType);
}
onClose();
};
/**
* @param {string} documentId
*/
const handleDocumentInsert = documentId => {
onClose();
// Insert the document view node at the determined position
const nextNodeStart = nodeStartPos + nodeSize + 2;
const documentInsertPos = isNodeEmpty(node) ? emptyNodeStartPos : nextNodeStart;
const documentViewNode = `<document-view data-guid="${documentId}"/>`;
const insertOptions = {
updateSelection: true,
parseOptions: {
preserveWhitespace: 'full'
}
};
editor.commands.insertContentAt(documentInsertPos, documentViewNode, insertOptions);
/**
* Checks if a given position is out of range based on the size of the document content.
*
* @param {number} pos - The position to check.
*/
const isOutOfRange = pos => {
return pos > editor.view.state.doc.content.size;
};
const isFirstNodeEmpty = nodeStartPos === 0 && isNodeEmpty(node);
const resolvedPos = isFirstNodeEmpty
? isOutOfRange(emptyNodeStartPos)
? documentInsertPos
: emptyNodeStartPos
: isOutOfRange(documentInsertPos + 2)
? documentInsertPos
: documentInsertPos + 2;
const {
state: { schema, tr },
view
} = editor;
// Insert node into the document
const emptyNode = schema.nodes.paragraph.create();
const transaction = tr.insert(resolvedPos, emptyNode);
view.dispatch(transaction);
// Set the selection to the newly inserted node
const selection = view.state.selection.constructor.near(
view.state.doc.resolve(resolvedPos)
);
view.dispatch(view.state.tr.setSelection(selection));
setOpenDialog(false);
};
const handleArticleDialog = () => {
setOpenDialog(true);
};
const handleImage = async e => {
onClose();
const file = e.target.files[0];
const { url } = await API().createFile('/FrameworkAPI/files2', file, 'upload');
editor.commands.insertContent(`<img src="${url}" />`).run();
};
return (
<Menu
sx={{
'& .MuiList-root': {
p: 0
}
}}
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => onClose()}
>
<InsertDocumentDialog
open={openDialog}
onClose={() => setOpenDialog(false)}
onInsert={handleDocumentInsert}
/>
<ToggleButtonGroup
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.7)',
backdropFilter: 'blur(5px)'
}}
>
<ToggleButton
value="bulletList"
onClick={() => handleToggleList('toggleBulletList')}
>
<FormatListBulletedIcon fontSize="small" />
</ToggleButton>
<ToggleButton
value="orderedList"
onClick={() => handleToggleList('toggleOrderedList')}
>
<FormatListNumberedIcon fontSize="small" />
</ToggleButton>
<ToggleButton size="small" value="standard" onClick={() => handleArticleDialog()}>
<ArticleIcon fontSize="small" />
</ToggleButton>
<ToggleButton size="small" value="image" component="label">
<ImageIcon fontSize="small" />
<input type="file" hidden onChange={handleImage} />
</ToggleButton>
</ToggleButtonGroup>
</Menu>
);
};
export default CustomFloatingMenu;
I provided the entire component for reference but I really want to focus on the handleDocumentInsert
. When called, it determines the appropriate position within the existing content to incorporate the document. It creates a specific view node representing the document and inserts it at the determined position. Additionally, it takes care of setting the selection to the newly inserted document, ensuring a smooth transition for further editing. It adds an empty paragraph node and then sets the selection there. Overall, the function facilitates the seamless insertion of documents, enhancing the editing experience within the context of the application.
I’m very new to ProseMirror and Tip Tap in general and I’m just wondering if I’m on the right track here. This essentially works but is it good practice? Any suggestions would be great or directions to working sandboxes would help. I want to lay a good foundation for my work. Thanks for all the help!