tiptap: improve mind toolbar

This commit is contained in:
fantasticit 2022-08-19 17:38:51 +08:00
parent 2f97380e63
commit 3b68238249
12 changed files with 481 additions and 204 deletions

View File

@ -82,7 +82,7 @@ define(function (require, exports, module) {
mask.fill(color[0]); mask.fill(color[0]);
} }
number.setContent(value); number.setContent('P' + value);
}, },
}); });

View File

@ -1,3 +1,36 @@
const getProgressTitle = (index) => {
switch (index) {
case 0:
return '移除进度';
case 1:
return '未开始';
case 9:
return '全部完成';
default:
return '完成' + (index - 1) + '/8';
}
};
export const PROGRESSES = Array.from({ length: 10 }, (_, i) => {
return {
text: getProgressTitle(i),
value: i,
};
});
export const PRIORITIES = [
{
text: '移除优先级',
value: 0,
},
...Array.from({ length: 9 }, (_, i) => {
return {
text: `P${i + 1}`,
value: i + 1,
};
}),
];
export const TEMPLATES = [ export const TEMPLATES = [
{ {
label: '经典', label: '经典',

View File

@ -65,6 +65,12 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
}; };
}, [editor, toggleVisible]); }, [editor, toggleVisible]);
useEffect(() => {
if (!visible && mind) {
mind.destroy();
}
}, [visible, mind]);
return ( return (
<Modal <Modal
centered centered
@ -75,6 +81,7 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
onOk={save} onOk={save}
okText="保存" okText="保存"
cancelText="退出" cancelText="退出"
motion={false}
> >
<div <div
style={{ style={{

View File

@ -0,0 +1,39 @@
import { IconMark } from '@douyinfe/semi-icons';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { ColorPicker } from 'components/color-picker';
export const BgColor = ({ bgColor, selectedNode, setBackgroundColor }) => {
return (
<ColorPicker
onSetColor={(color) => {
setBackgroundColor(color);
}}
>
<Tooltip content="背景色" zIndex={10000}>
<Button
disabled={!selectedNode}
type="tertiary"
theme={bgColor ? 'light' : 'borderless'}
icon={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IconMark />
<span
style={{
width: 12,
height: 2,
backgroundColor: bgColor,
}}
></span>
</div>
}
/>
</Tooltip>
</ColorPicker>
);
};

View File

@ -0,0 +1,39 @@
import { IconFont } from '@douyinfe/semi-icons';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { ColorPicker } from 'components/color-picker';
export const FontColor = ({ selectedNode, setFontColor, textColor }) => {
return (
<ColorPicker
onSetColor={(color) => {
setFontColor(color);
}}
>
<Tooltip content="文本色" zIndex={10000}>
<Button
disabled={!selectedNode}
type="tertiary"
theme={textColor ? 'light' : 'borderless'}
icon={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IconFont />
<span
style={{
width: 12,
height: 2,
backgroundColor: textColor,
}}
></span>
</div>
}
/>
</Tooltip>
</ColorPicker>
);
};

View File

@ -0,0 +1,33 @@
import { IconHelpCircle } from '@douyinfe/semi-icons';
import { Button, Descriptions, Popover } from '@douyinfe/semi-ui';
import styles from './index.module.scss';
const HELP_MESSAGE = [
{ key: '新增同级节点', value: 'Enter 键' },
{ key: '新增子节点', value: 'Tab 键' },
{ key: '编辑节点文字', value: '双击节点' },
{ key: '编辑节点菜单', value: '在节点右键' },
];
const HELP_MESSAGE_STYLE = {
width: '200px',
};
export const Help = () => {
return (
<Popover
zIndex={10000}
spacing={10}
style={{ padding: 12, overflow: 'hidden' }}
position="bottomLeft"
content={
<section className={styles.sectionWrap}>
<Descriptions data={HELP_MESSAGE} style={HELP_MESSAGE_STYLE} />
</section>
}
>
<Button size="small" theme="borderless" type="tertiary" icon={<IconHelpCircle />} />
</Popover>
);
};

View File

@ -1,4 +1,4 @@
import { IconFile } from '@douyinfe/semi-icons'; import { IconImage } from '@douyinfe/semi-icons';
import { Button, Dropdown, Form, Tooltip } from '@douyinfe/semi-ui'; import { Button, Dropdown, Form, Tooltip } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { Upload } from 'components/upload'; import { Upload } from 'components/upload';
@ -48,7 +48,7 @@ export const Image = ({ disabled, image, setImage }) => {
> >
<span style={{ display: 'inline-block' }}> <span style={{ display: 'inline-block' }}>
<Tooltip content="设置图片" zIndex={10000}> <Tooltip content="设置图片" zIndex={10000}>
<Button disabled={disabled} type="tertiary" theme={image ? 'light' : 'borderless'} icon={<IconFile />} /> <Button disabled={disabled} type="tertiary" theme={image ? 'light' : 'borderless'} icon={<IconImage />} />
</Tooltip> </Tooltip>
</span> </span>
</Dropdown> </Dropdown>

View File

@ -1,108 +1,188 @@
import { IconBold, IconFont, IconHelpCircle, IconMark } from '@douyinfe/semi-icons'; import { IconBold, IconRedo, IconUndo } from '@douyinfe/semi-icons';
import { Button, Descriptions, Popover, Space, Tooltip, Typography } from '@douyinfe/semi-ui'; import { Button, Space, Tooltip } from '@douyinfe/semi-ui';
import cls from 'classnames'; import { Divider } from 'components/divider';
import { ColorPicker } from 'components/color-picker'; import { IconMindCenter } from 'components/icons';
import { IconDrawBoard, IconMindCenter, IconStructure } from 'components/icons';
import { IconZoomIn, IconZoomOut } from 'components/icons'; import { IconZoomIn, IconZoomOut } from 'components/icons';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { clamp } from 'tiptap/prose-utils';
import { MAX_ZOOM, MIN_ZOOM, TEMPLATES, THEMES, ZOOM_STEP } from '../constant'; import { BgColor } from './bgcolor';
import { FontColor } from './font-color';
import { Help } from './help';
import { Image } from './image'; import { Image } from './image';
import styles from './index.module.scss';
import { Link } from './link'; import { Link } from './link';
import { Priority } from './priority';
const { Text } = Typography; import { Progress } from './progress';
import { Template } from './template';
const HELP_MESSAGE = [ import { Theme } from './theme';
{ key: '新增同级节点', value: 'Enter 键' },
{ key: '新增子节点', value: 'Tab 键' },
{ key: '编辑节点文字', value: '双击节点' },
{ key: '编辑节点菜单', value: '在节点右键' },
];
const HELP_MESSAGE_STYLE = {
width: '200px',
};
export const Toolbar = ({ mind }) => { export const Toolbar = ({ mind }) => {
const [template, setTemplateState] = useState(''); const [node, setNode] = useState(null); // 当前选择节点
const [theme, setThemeState] = useState('');
const [node, setNode] = useState(null); const [hasUndo, toggleHasUndo] = useToggle(false);
const [hasRedo, toggleHasRedo] = useToggle(false);
const [isBold, toggleIsBold] = useToggle(false); const [isBold, toggleIsBold] = useToggle(false);
const [textColor, setTextColor] = useState(''); const [textColor, setTextColor] = useState('');
const [bgColor, setBgColor] = useState(''); const [bgColor, setBgColor] = useState('');
const [link, setLink] = useState(''); const [link, setLink] = useState('');
const [image, setImage] = useState(''); const [image, setImage] = useState('');
const setTemplate = useCallback( const [template, setTemplateState] = useState('');
(template) => { const [theme, setThemeState] = useState('');
mind.execCommand('template', template);
},
[mind]
);
const setTheme = useCallback( /**
(theme) => { *
mind.execCommand('theme', theme); */
}, const undo = useCallback(() => {
[mind]
);
const setZoom = useCallback(
(type: 'minus' | 'plus') => {
return () => {
if (!mind) return; if (!mind) return;
const currentZoom = mind.getZoomValue(); if (mind.editor.history.hasUndo()) {
const nextZoom = clamp( mind.editor.history.undo();
type === 'minus' ? currentZoom - ZOOM_STEP : currentZoom + ZOOM_STEP, }
MIN_ZOOM,
MAX_ZOOM
);
mind.zoom(nextZoom);
};
},
[mind]
);
const setCenter = useCallback(() => {
if (!mind) return;
mind.execCommand('camera');
}, [mind]); }, [mind]);
/**
*
*/
const redo = useCallback(() => {
if (!mind) return;
if (mind.editor.history.hasRedo()) {
mind.editor.history.redo();
}
}, [mind]);
/**
*
*/
const toggleBold = useCallback(() => { const toggleBold = useCallback(() => {
if (!mind) return;
mind.execCommand('Bold'); mind.execCommand('Bold');
}, [mind]); }, [mind]);
/**
*
*/
const setFontColor = useCallback( const setFontColor = useCallback(
(color) => { (color) => {
if (!mind) return;
mind.execCommand('ForeColor', color); mind.execCommand('ForeColor', color);
}, },
[mind] [mind]
); );
/**
*
*/
const setBackgroundColor = useCallback( const setBackgroundColor = useCallback(
(color) => { (color) => {
if (!mind) return;
mind.execCommand('Background', color); mind.execCommand('Background', color);
}, },
[mind] [mind]
); );
/**
*
*/
const setHyperLink = useCallback( const setHyperLink = useCallback(
(url) => { (url) => {
if (!mind) return;
mind.execCommand('HyperLink', url); mind.execCommand('HyperLink', url);
}, },
[mind] [mind]
); );
/**
*
*/
const insertImage = useCallback( const insertImage = useCallback(
(url) => { (url) => {
if (!mind) return;
mind.execCommand('Image', url); mind.execCommand('Image', url);
}, },
[mind] [mind]
); );
/**
*
*/
const setProgress = useCallback(
(value) => () => {
if (!mind) return;
const node = mind.getSelectedNode();
if (!node) return;
mind.execCommand('progress', value);
},
[mind]
);
/**
*
*/
const setPriority = useCallback(
(value) => () => {
if (!mind) return;
const node = mind.getSelectedNode();
if (!node) return;
mind.execCommand('priority', value);
},
[mind]
);
/**
*
*/
const setTemplate = useCallback(
(template) => {
if (!mind) return;
mind.execCommand('template', template);
},
[mind]
);
/**
*
*/
const setTheme = useCallback(
(theme) => {
if (!mind) return;
mind.execCommand('theme', theme);
},
[mind]
);
/**
*
*/
const setZoom = useCallback(
(type: 'minus' | 'plus') => {
return () => {
if (!mind) return;
mind.execCommand(type === 'minus' ? 'zoomOut' : 'zoomIn');
};
},
[mind]
);
/**
*
*/
const setCenter = useCallback(() => {
if (!mind) return;
mind.execCommand('camera', mind.getRoot(), 600);
}, [mind]);
useEffect(() => { useEffect(() => {
if (!mind) return; if (!mind) return;
@ -126,6 +206,9 @@ export const Toolbar = ({ mind }) => {
setNode(null); setNode(null);
} }
toggleHasUndo(mind.editor.history.hasUndo());
toggleHasRedo(mind.editor.history.hasRedo());
setTemplateState(mind.queryCommandValue('Template')); setTemplateState(mind.queryCommandValue('Template'));
setThemeState(mind.queryCommandValue('Theme')); setThemeState(mind.queryCommandValue('Theme'));
toggleIsBold(isBold); toggleIsBold(isBold);
@ -140,68 +223,53 @@ export const Toolbar = ({ mind }) => {
return () => { return () => {
mind.off('interactchange', handler); mind.off('interactchange', handler);
}; };
}, [mind, toggleIsBold, setBackgroundColor]); }, [mind, toggleHasUndo, toggleHasRedo, toggleIsBold, setBackgroundColor]);
return ( return (
<Space> <Space>
<Popover <Tooltip content="撤销">
zIndex={10000} <Button
spacing={10} onClick={undo}
style={{ padding: '0 12px 12px', overflow: 'hidden' }} icon={<IconUndo />}
position="bottomLeft" disabled={!hasUndo}
content={ theme={hasUndo ? 'light' : 'borderless'}
<section className={styles.sectionWrap}> type="tertiary"
<Text type="secondary"></Text> />
<div> </Tooltip>
<ul>
{TEMPLATES.map((item) => {
return (
<li
key={item.label}
className={cls(template === item.value && styles.active)}
onClick={() => setTemplate(item.value)}
>
<Text>{item.label}</Text>
</li>
);
})}
</ul>
</div>
</section>
}
>
<Button icon={<IconStructure />} type="tertiary" theme="borderless" size="small" />
</Popover>
<Popover <Tooltip content="重做">
zIndex={10000} <Button
spacing={10} onClick={redo}
style={{ padding: '0 12px 12px', overflow: 'hidden' }} icon={<IconRedo />}
position="bottomLeft" disabled={!hasRedo}
content={ theme={hasRedo ? 'light' : 'borderless'}
<section className={styles.sectionWrap}> type="tertiary"
<Text type="secondary"></Text> />
<div> </Tooltip>
<ul>
{THEMES.map((item) => { <Divider />
return (
<li <Tooltip content="加粗" zIndex={10000}>
key={item.label} <Button
className={cls(theme === item.value && styles.active)} disabled={!node}
style={item.style || {}} theme={isBold ? 'light' : 'borderless'}
onClick={() => setTheme(item.value)} type="tertiary"
> onClick={toggleBold}
{item.label} icon={<IconBold />}
</li> />
); </Tooltip>
})}
</ul> <FontColor selectedNode={node} textColor={textColor} setFontColor={setFontColor} />
</div> <BgColor selectedNode={node} bgColor={bgColor} setBackgroundColor={setBackgroundColor} />
</section> <Link disabled={!node} link={link} setLink={setHyperLink} />
} <Image disabled={!node} image={image} setImage={insertImage} />
>
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" /> <Divider />
</Popover>
<Progress selectedNode={node} setProgress={setProgress} />
<Priority selectedNode={node} setPriority={setPriority} />
<Divider />
<Tooltip content="居中"> <Tooltip content="居中">
<Button <Button
@ -233,97 +301,14 @@ export const Toolbar = ({ mind }) => {
/> />
</Tooltip> </Tooltip>
<Tooltip content="加粗" zIndex={10000}> <Divider />
<Button
disabled={!node}
type="tertiary"
theme={isBold ? 'light' : 'borderless'}
onClick={toggleBold}
icon={<IconBold />}
/>
</Tooltip>
<ColorPicker <Template template={template} setTemplate={setTemplate} />
onSetColor={(color) => { <Theme theme={theme} setTheme={setTheme} />
setFontColor(color);
}}
>
<Tooltip content="文本色" zIndex={10000}>
<Button
disabled={!node}
type="tertiary"
theme={textColor ? 'light' : 'borderless'}
icon={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IconFont />
<span
style={{
width: 12,
height: 2,
backgroundColor: textColor,
}}
></span>
</div>
}
/>
</Tooltip>
</ColorPicker>
<ColorPicker <Divider />
onSetColor={(color) => {
setBackgroundColor(color);
}}
>
<Tooltip content="背景色" zIndex={10000}>
<Button
disabled={!node}
type="tertiary"
theme={bgColor ? 'light' : 'borderless'}
icon={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IconMark />
<span
style={{
width: 12,
height: 2,
backgroundColor: bgColor,
}}
></span>
</div>
}
/>
</Tooltip>
</ColorPicker>
<Link disabled={!node} link={link} setLink={setHyperLink} /> <Help />
<Image disabled={!node} image={image} setImage={insertImage} />
<Popover
zIndex={10000}
spacing={10}
style={{ padding: 12, overflow: 'hidden' }}
position="bottomLeft"
content={
<section className={styles.sectionWrap}>
<Descriptions data={HELP_MESSAGE} style={HELP_MESSAGE_STYLE} />
</section>
}
>
<Button size="small" theme="borderless" type="tertiary" icon={<IconHelpCircle />} />
</Popover>
</Space> </Space>
); );
}; };

View File

@ -0,0 +1,29 @@
import { IconClock } from '@douyinfe/semi-icons';
import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
import { PRIORITIES } from '../constant';
export const Priority = ({ selectedNode, setPriority }) => {
return (
<Dropdown
trigger="click"
render={
<Dropdown.Menu>
{PRIORITIES.map((item) => {
return (
<Dropdown.Item key={item.value} onClick={setPriority(item.value)}>
{item.text}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
}
>
<span>
<Tooltip content="优先级" zIndex={10000}>
<Button size="small" theme="borderless" type="tertiary" disabled={!selectedNode} icon={<IconClock />} />
</Tooltip>
</span>
</Dropdown>
);
};

View File

@ -0,0 +1,29 @@
import { IconDuration } from '@douyinfe/semi-icons';
import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
import { PROGRESSES } from '../constant';
export const Progress = ({ selectedNode, setProgress }) => {
return (
<Dropdown
trigger="click"
render={
<Dropdown.Menu>
{PROGRESSES.map((progress) => {
return (
<Dropdown.Item key={progress.value} onClick={setProgress(progress.value)}>
{progress.text}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
}
>
<span>
<Tooltip content="进度" zIndex={10000}>
<Button size="small" theme="borderless" type="tertiary" disabled={!selectedNode} icon={<IconDuration />} />
</Tooltip>
</span>
</Dropdown>
);
};

View File

@ -0,0 +1,41 @@
import { Button, Popover, Typography } from '@douyinfe/semi-ui';
import cls from 'classnames';
import { IconStructure } from 'components/icons';
import { TEMPLATES } from '../constant';
import styles from './index.module.scss';
const { Text } = Typography;
export const Template = ({ template, setTemplate }) => {
return (
<Popover
zIndex={10000}
spacing={10}
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
position="bottomLeft"
content={
<section className={styles.sectionWrap}>
<Text type="secondary"></Text>
<div>
<ul>
{TEMPLATES.map((item) => {
return (
<li
key={item.label}
className={cls(template === item.value && styles.active)}
onClick={() => setTemplate(item.value)}
>
<Text>{item.label}</Text>
</li>
);
})}
</ul>
</div>
</section>
}
>
<Button icon={<IconStructure />} type="tertiary" theme="borderless" size="small" />
</Popover>
);
};

View File

@ -0,0 +1,42 @@
import { Button, Popover, Typography } from '@douyinfe/semi-ui';
import cls from 'classnames';
import { IconDrawBoard } from 'components/icons';
import { THEMES } from '../constant';
import styles from './index.module.scss';
const { Text } = Typography;
export const Theme = ({ theme, setTheme }) => {
return (
<Popover
zIndex={10000}
spacing={10}
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
position="bottomLeft"
content={
<section className={styles.sectionWrap}>
<Text type="secondary"></Text>
<div>
<ul>
{THEMES.map((item) => {
return (
<li
key={item.label}
className={cls(theme === item.value && styles.active)}
style={item.style || {}}
onClick={() => setTheme(item.value)}
>
{item.label}
</li>
);
})}
</ul>
</div>
</section>
}
>
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" />
</Popover>
);
};