import { Block, Plugin } from '@chamaeleon/core';
import CloseIcon from '@mui/icons-material/Close';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import capitalize from '@mui/material/utils/capitalize';
import ReactDOM from 'react-dom';
// Our plugin will have two commands for opening
// and closing Drawer so that typescript knows about them,
// we use this code to extend the basic commands with our own
declare module '@chamaeleon/core' {
interface Commands<ReturnType> {
'block-settings': {
openBlockSettings: (target: Block['id']) => ReturnType;
closeBlockSettings: () => ReturnType;
};
}
}
// Plugins can have their own state, in our plugin
// we will have the open state of Drawer and
// the current Block['id'] for which settings are displayed
type State = {
opened: boolean;
target?: Block['id'];
};
export function BlockSettings(): Plugin<State> {
return {
name: 'block-settings',
state: {
// Here you need to set initial state
init() {
return {
opened: false,
};
},
// The apply method is a more advanced method for changing state,
// it is called on every change in the state of the editor,
// it has the ability to access transactions that are applied to the state.
// Now we simply return the current state of the plugin without modifying it
apply(_, value) {
return value;
},
},
apply(editor, { addCommands, addView, setState, usePluginState }) {
// "addCommands" are used to expand editor commands
addCommands({
// A command is a function that returns a function
// (...yourArgs) => (context) => void
// The first function contains your custom arguments,
// and the second contains the context,
// which includes the editor, commands, transaction, etc.
openBlockSettings(target) {
return () => {
// "setState" is a method for setting the state of the plugin
setState({
opened: true,
target,
});
};
},
closeBlockSettings() {
return () => {
setState((prev) => ({
...prev,
opened: false,
}));
};
},
});
// addView allows you to add any components, you can use
// the react portal to control the rendering location
addView({
component: () => {
// api "usePluginState" is similar to React.useState
const [{ opened, target }] = usePluginState();
return ReactDOM.createPortal(
<Drawer
anchor="right"
open={opened}
onClose={editor.commands.closeBlockSettings}
sx={{
'.MuiPaper-root': {
maxWidth: 400,
width: '100%',
},
}}
>
{target && <DrawerBody target={target} editor={editor} />}
</Drawer>,
document.body,
);
},
});
},
};
}
function DrawerBody({
target,
editor,
}: {
target: Block['id'];
editor: Editor;
}) {
const targetBlock = editor.state.getBlock(target);
return (
<Stack spacing={4} p={3}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Typography variant="h5">
{capitalize(editor.state.getBlock(target).type.name)} settings
</Typography>
<IconButton
aria-label="close block settings"
onClick={editor.commands.closeBlockSettings}
>
<CloseIcon />
</IconButton>
</Stack>
{/* "editor.view.pluginPropsViews" contains
an array of all Views from all plugins,
this is the same object that is passed to "addPropsView" */}
{editor.view.pluginPropsViews.map(
({ id, view: { filter, component: Component } }) => {
// get targetBlock from state
const targetBlock = editor.state.getBlock(target);
// determine whether the component needs
// to be rendered for this block
if (!filter(targetBlock)) return null;
// And we render it!
return <Component key={id} editor={editor} block={targetBlock} />;
},
)}
</Stack>
);
}