Skip to content

Commit

Permalink
feat(editor): use slatejs instead of draftjs
Browse files Browse the repository at this point in the history
Change editor library to slatejs.

BREAKING CHANGE: changing editor library will break previously saved notes
  • Loading branch information
jbcl-io committed Mar 30, 2021
1 parent 65c8d12 commit ad1e563
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 137 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
"@slater-notes/core": "slater-notes/core",
"@types/async": "3.2.5",
"@types/bluebird": "3.5.33",
"@types/draft-js": "0.10.45",
"@types/lodash": "4.14.168",
"@types/moment": "2.13.0",
"@types/node": "14.14.22",
Expand All @@ -72,7 +71,6 @@
"async": "3.2.0",
"axios": "0.21.1",
"bluebird": "3.7.2",
"draft-js": "0.11.7",
"easy-peasy": "4.0.1",
"formik": "2.2.6",
"lodash": "4.17.21",
Expand All @@ -82,6 +80,9 @@
"react-dom": "17.0.1",
"react-feather": "2.0.9",
"react-scripts": "4.0.1",
"slate": "0.59.0",
"slate-history": "0.59.0",
"slate-react": "0.59.0",
"styled-components": "5.2.1",
"typescript": "4.1.3",
"workerize-loader": "1.3.0",
Expand Down
20 changes: 6 additions & 14 deletions src/components/Buttons/DefaultIconButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconButton, makeStyles, Tooltip } from '@material-ui/core';
import { IconButton, Tooltip } from '@material-ui/core';
import React from 'react';
import { Icon } from 'react-feather';

Expand All @@ -9,36 +9,28 @@ interface Props {
size?: string | number;
color?: 'default' | 'inherit' | 'primary' | 'secondary';
fill?: string;
strokeWidth?: number;
disabled?: boolean;
onClick?: () => void;
onClick?: (event: React.MouseEvent) => void;
onMouseDown?: (event: React.MouseEvent) => void;
}

const DefaultIconButton = (props: Props) => {
const classes = useStyles();

const fillProps = props.fill ? { fill: props.fill } : undefined;

return (
<Tooltip title={props.label || ''}>
<IconButton
className={classes.iconButton}
color={props.color}
disabled={props.disabled}
style={props.style}
onClick={props.onClick}
onMouseDown={props.onMouseDown}
>
<props.icon size={props.size || 16} {...fillProps} />
<props.icon size={props.size || 16} {...fillProps} strokeWidth={props.strokeWidth} />
</IconButton>
</Tooltip>
);
};

const useStyles = makeStyles((theme) => ({
iconButton: {
'&:hover': {
color: `${theme.palette.text.primary}`,
},
},
}));

export default DefaultIconButton;
62 changes: 62 additions & 0 deletions src/components/Editor/HoveringToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { makeStyles, Paper, Popper } from '@material-ui/core';
import React from 'react';
import { Editor, Range } from 'slate';
import { useSlate } from 'slate-react';
import Toolbar from './Toolbar';

const HoveringToolbar = () => {
const classes = useStyles();
const editor = useSlate();

const [isOpen, setIsOpen] = React.useState(false);
const [anchor, setAnchor] = React.useState<Anchor | null>(null);

React.useEffect(() => {
const { selection } = editor;

if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') {
setIsOpen(false);
return;
}

const domSelection = globalThis.getSelection();
const domRange = domSelection?.getRangeAt(0);
const rect = domRange?.getBoundingClientRect();

if (rect) {
setAnchor({
clientWidth: rect.width,
clientHeight: rect.height,
getBoundingClientRect: () => rect,
});
setIsOpen(true);
}
}, [editor.selection]);

return (
<Popper anchorEl={anchor} open={isOpen} className={classes.popper} placement='top' keepMounted>
<Paper className={classes.paper} elevation={3}>
<Toolbar />
</Paper>
</Popper>
);
};

const useStyles = makeStyles((theme) => ({
popper: {
top: '-8px !important',
},
paper: {
padding: `${theme.spacing(0.5)}px ${theme.spacing(1.5)}px`,
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
}));

interface Anchor {
clientWidth: number;
clientHeight: number;
getBoundingClientRect: () => DOMRect;
}

export default HoveringToolbar;
29 changes: 29 additions & 0 deletions src/components/Editor/RenderElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { makeStyles } from '@material-ui/core';
import React from 'react';
import { RenderElementProps } from 'slate-react';

const TextElement = (props: RenderElementProps) => {
const classes = useStyles();

return (
<div className={classes.textElement} {...props.attributes}>
{props.children}
</div>
);
};

const useStyles = makeStyles((theme) => ({
textElement: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
}));

const RenderElement = (props: RenderElementProps) => {
switch (props.element.type) {
default:
return <TextElement {...props} />;
}
};

export default RenderElement;
35 changes: 35 additions & 0 deletions src/components/Editor/RenderLeaf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { makeStyles } from '@material-ui/core';
import { RenderLeafProps } from 'slate-react';

const RenderLeaf = (props: RenderLeafProps) => {
const classes = useStyles();

const classNames: string[] = [];

if (props.leaf.bold) classNames.push(classes.isBold);
if (props.leaf.italic) classNames.push(classes.isItalic);
if (props.leaf.underline) classNames.push(classes.isUnderline);

return (
<span className={classNames.join(' ')} {...props.attributes}>
{props.children}
</span>
);
};

const useStyles = makeStyles({
isBold: {
fontWeight: 'bold',
},

isItalic: {
fontStyle: 'italic',
},

isUnderline: {
textDecoration: 'underline',
},
});

export default RenderLeaf;
77 changes: 77 additions & 0 deletions src/components/Editor/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { makeStyles } from '@material-ui/core';
import React from 'react';
import { Bold, Italic, Slash, Underline } from 'react-feather';
import { useSlate } from 'slate-react';
import DefaultIconButton from '../Buttons/DefaultIconButton';
import {
isBoldMarkActive,
toggleBoldMark,
isItalicMarkActive,
toggleItalicMark,
isUnderlineMarkActive,
toggleUnderlineMark,
clearMarks,
} from './editorCommands';

const Toolbar = () => {
const editor = useSlate();
const classes = useStyles();

const iconFontSize = 16;
const iconStrokeWidth = 2.5;
const defaultIconColor = 'inherit';
const activeIconColor = 'secondary';

return (
<div className={classes.container}>
<div>
<DefaultIconButton
label='Bold'
icon={Bold}
color={isBoldMarkActive(editor) ? activeIconColor : defaultIconColor}
strokeWidth={iconStrokeWidth}
size={iconFontSize}
onMouseDown={() => toggleBoldMark(editor)}
/>
</div>
<div>
<DefaultIconButton
label='Italic'
icon={Italic}
color={isItalicMarkActive(editor) ? activeIconColor : defaultIconColor}
strokeWidth={iconStrokeWidth}
size={iconFontSize}
onMouseDown={() => toggleItalicMark(editor)}
/>
</div>
<div>
<DefaultIconButton
label='Underline'
icon={Underline}
color={isUnderlineMarkActive(editor) ? activeIconColor : defaultIconColor}
strokeWidth={iconStrokeWidth}
size={iconFontSize}
onMouseDown={() => toggleUnderlineMark(editor)}
/>
</div>
<div>
<DefaultIconButton
label='Clear Formatting'
icon={Slash}
color={defaultIconColor}
strokeWidth={iconStrokeWidth}
size={iconFontSize}
onMouseDown={() => clearMarks(editor)}
/>
</div>
</div>
);
};

const useStyles = makeStyles((theme) => ({
container: {
display: 'flex',
},
}));

export default Toolbar;
60 changes: 60 additions & 0 deletions src/components/Editor/editorCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Editor, Transforms, Text } from 'slate';

export const isBoldMarkActive = (editor: Editor) => {
const [match] = Editor.nodes(editor, {
match: (n) => n.bold === true,
universal: true,
}) as any;

return !!match;
};

export const isItalicMarkActive = (editor: Editor) => {
const [match] = Editor.nodes(editor, {
match: (n) => n.italic === true,
universal: true,
}) as any;

return !!match;
};

export const isUnderlineMarkActive = (editor: Editor) => {
const [match] = Editor.nodes(editor, {
match: (n) => n.underline === true,
universal: true,
}) as any;

return !!match;
};

export const toggleBoldMark = (editor: Editor) => {
Transforms.setNodes(
editor,
{ bold: !isBoldMarkActive(editor) },
{ match: (n) => Text.isText(n), split: true },
);
};

export const toggleItalicMark = (editor: Editor) => {
Transforms.setNodes(
editor,
{ italic: !isItalicMarkActive(editor) },
{ match: (n) => Text.isText(n), split: true },
);
};

export const toggleUnderlineMark = (editor: Editor) => {
Transforms.setNodes(
editor,
{ underline: !isUnderlineMarkActive(editor) },
{ match: (n) => Text.isText(n), split: true },
);
};

export const clearMarks = (editor: Editor) => {
Transforms.setNodes(
editor,
{ bold: false, italic: false, underline: false },
{ match: (n) => Text.isText(n) },
);
};
Loading

0 comments on commit ad1e563

Please sign in to comment.