mirror of https://github.com/fantasticit/think.git
feat: add toolbar for mind
parent
6550d7cb14
commit
65b7a1451a
|
@ -1,86 +1,35 @@
|
||||||
import { IconHelpCircle, IconMinus, IconPlus } from '@douyinfe/semi-icons';
|
import { Modal, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
import { Button, Descriptions, Modal, Popover, Space, Spin, Typography } from '@douyinfe/semi-ui';
|
|
||||||
import cls from 'classnames';
|
|
||||||
import { IconDrawBoard, IconMindCenter, IconStructure } from 'components/icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { load, renderMind } from 'thirtypart/kityminder';
|
import { load, renderMind } from 'thirtypart/kityminder';
|
||||||
import { Editor } from 'tiptap/editor';
|
import { Editor } from 'tiptap/editor';
|
||||||
import { clamp } from 'tiptap/prose-utils';
|
|
||||||
|
|
||||||
import { cancelSubject, OPEN_MIND_SETTING_MODAL, subject } from '../_event';
|
import { cancelSubject, OPEN_MIND_SETTING_MODAL, subject } from '../_event';
|
||||||
import { MAX_ZOOM, MIN_ZOOM, TEMPLATES, THEMES, ZOOM_STEP } from './constant';
|
|
||||||
import styles from './style.module.scss';
|
import styles from './style.module.scss';
|
||||||
|
import { Toolbar } from './toolbar';
|
||||||
|
|
||||||
type IProps = { editor: Editor };
|
type IProps = { editor: Editor };
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const HELP_MESSAGE = [
|
|
||||||
{ key: '新增同级节点', value: 'Enter 键' },
|
|
||||||
{ key: '新增子节点', value: 'Tab 键' },
|
|
||||||
{ key: '编辑节点文字', value: '双击节点' },
|
|
||||||
{ key: '编辑节点菜单', value: '在节点右键' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const HELP_MESSAGE_STYLE = {
|
|
||||||
width: '200px',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
const $mind = useRef(null);
|
const [mind, setMind] = useState(null);
|
||||||
const [initialData, setInitialData] = useState({ template: '', theme: '' });
|
const [initialData, setInitialData] = useState({});
|
||||||
const [template, setTemplateState] = useState('');
|
|
||||||
const [theme, setThemeState] = useState('');
|
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
const [loading, toggleLoading] = useToggle(true);
|
const [loading, toggleLoading] = useToggle(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const setZoom = useCallback((type: 'minus' | 'plus') => {
|
const renderMindEditor = useCallback(
|
||||||
return () => {
|
|
||||||
const mind = $mind.current;
|
|
||||||
if (!mind) return;
|
|
||||||
const currentZoom = mind.getZoomValue();
|
|
||||||
const nextZoom = clamp(type === 'minus' ? currentZoom - ZOOM_STEP : currentZoom + ZOOM_STEP, MIN_ZOOM, MAX_ZOOM);
|
|
||||||
mind.zoom(nextZoom);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setCenter = useCallback(() => {
|
|
||||||
const mind = $mind.current;
|
|
||||||
if (!mind) return;
|
|
||||||
mind.execCommand('camera');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setTemplate = useCallback((template) => {
|
|
||||||
const mind = $mind.current;
|
|
||||||
if (!mind) return;
|
|
||||||
mind.execCommand('template', template);
|
|
||||||
setTemplateState(template);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setTheme = useCallback((theme) => {
|
|
||||||
const mind = $mind.current;
|
|
||||||
if (!mind) return;
|
|
||||||
mind.execCommand('theme', theme);
|
|
||||||
setThemeState(theme);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setMind = useCallback(
|
|
||||||
(div) => {
|
(div) => {
|
||||||
if (!div) return;
|
if (!div) return;
|
||||||
|
|
||||||
if ($mind.current) {
|
const mindInstance = renderMind({
|
||||||
$mind.current.destroy();
|
|
||||||
$mind.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$mind.current = renderMind({
|
|
||||||
container: div,
|
container: div,
|
||||||
data: initialData,
|
data: initialData,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setMind(mindInstance);
|
||||||
},
|
},
|
||||||
[initialData]
|
[initialData]
|
||||||
);
|
);
|
||||||
|
@ -89,32 +38,23 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
load()
|
load()
|
||||||
.catch(setError)
|
.catch(setError)
|
||||||
.finally(() => toggleLoading(false));
|
.finally(() => toggleLoading(false));
|
||||||
|
|
||||||
return () => {
|
|
||||||
if ($mind.current) {
|
|
||||||
$mind.current.destroy();
|
|
||||||
$mind.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [toggleLoading]);
|
}, [toggleLoading]);
|
||||||
|
|
||||||
const save = useCallback(() => {
|
const save = useCallback(() => {
|
||||||
if (!$mind.current) {
|
if (!mind) {
|
||||||
toggleVisible(false);
|
toggleVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = $mind.current.exportJson();
|
const data = mind.exportJson();
|
||||||
editor.chain().focus().setMind({ data }).run();
|
editor.chain().focus().setMind({ data }).run();
|
||||||
toggleVisible(false);
|
toggleVisible(false);
|
||||||
}, [editor, toggleVisible]);
|
}, [editor, toggleVisible, mind]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (data) => {
|
const handler = (data) => {
|
||||||
toggleVisible(true);
|
toggleVisible(true);
|
||||||
if (data) {
|
if (data) {
|
||||||
setInitialData(data.data);
|
setInitialData(data.data);
|
||||||
setTemplateState(data.data.template);
|
|
||||||
setThemeState(data.data.theme);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -151,116 +91,10 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
</Spin>
|
</Spin>
|
||||||
)}
|
)}
|
||||||
{error && <Text>{(error && error.message) || '未知错误'}</Text>}
|
{error && <Text>{(error && error.message) || '未知错误'}</Text>}
|
||||||
<div style={{ height: '100%', maxHeight: '100%', overflow: 'hidden' }} ref={setMind}></div>
|
<div style={{ height: '100%', maxHeight: '100%', overflow: 'hidden' }} ref={renderMindEditor}></div>
|
||||||
|
|
||||||
<div className={styles.toolbarWrap}>
|
<div className={styles.toolbarWrap}>
|
||||||
<Space>
|
<Toolbar mind={mind} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<Tooltip content="居中">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
theme="borderless"
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconMindCenter style={{ fontSize: '0.85em' }} />}
|
|
||||||
onClick={setCenter}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="缩小">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
theme="borderless"
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconMinus style={{ fontSize: '0.85em' }} />}
|
|
||||||
onClick={setZoom('minus')}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="放大">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
theme="borderless"
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconPlus style={{ fontSize: '0.85em' }} />}
|
|
||||||
onClick={setZoom('plus')}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
zIndex={10000}
|
|
||||||
spacing={10}
|
|
||||||
style={{ padding: '0 12px 12px', 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 style={{ fontSize: '0.85em' }} />}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,61 +1,13 @@
|
||||||
.toolbarWrap {
|
.toolbarWrap {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 24px;
|
top: 0;
|
||||||
bottom: 24px;
|
left: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
padding: 2px 4px;
|
width: 100%;
|
||||||
|
padding: 6px 24px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--semi-color-nav-bg);
|
background-color: var(--semi-color-nav-bg);
|
||||||
border: 1px solid var(--semi-color-border);
|
border-bottom: 1px solid var(--semi-color-border);
|
||||||
border-radius: 3px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionWrap {
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
width: 168px;
|
|
||||||
margin-top: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
list-style: none;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
li {
|
|
||||||
width: 80px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 30px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid rgb(28 31 35 / 8%);
|
|
||||||
|
|
||||||
* {
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border: 1px solid rgb(0 101 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-of-type(2n) {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-of-type(n + 3) {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { IconFile } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Dropdown, Form, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
|
import { Upload } from 'components/upload';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
export const Image = ({ disabled, image, setImage }) => {
|
||||||
|
const $form = useRef<FormApi>();
|
||||||
|
const [initialState, setInitialState] = useState({ image });
|
||||||
|
|
||||||
|
const setImageUrl = useCallback((url) => {
|
||||||
|
$form.current.setValue('image', url);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOk = useCallback(() => {
|
||||||
|
$form.current.validate().then((values) => {
|
||||||
|
setImage(values.image);
|
||||||
|
});
|
||||||
|
}, [setImage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInitialState({ image });
|
||||||
|
}, [image]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
stopPropagation
|
||||||
|
zIndex={10000}
|
||||||
|
trigger="click"
|
||||||
|
position={'bottomLeft'}
|
||||||
|
render={
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Form
|
||||||
|
initValues={initialState}
|
||||||
|
getFormApi={(formApi) => ($form.current = formApi)}
|
||||||
|
labelPosition="left"
|
||||||
|
onSubmit={handleOk}
|
||||||
|
layout="horizontal"
|
||||||
|
>
|
||||||
|
<Form.Input autofocus label="图片" field="image" placeholder="请输入图片地址" />
|
||||||
|
<Upload onOK={setImageUrl} />
|
||||||
|
<Button type="primary" htmlType="submit" style={{ marginLeft: 12 }}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span style={{ display: 'inline-block' }}>
|
||||||
|
<Tooltip content="设置图片" zIndex={10000}>
|
||||||
|
<Button disabled={disabled} type="tertiary" theme={image ? 'light' : 'borderless'} icon={<IconFile />} />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,46 @@
|
||||||
|
.sectionWrap {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
width: 168px;
|
||||||
|
margin-top: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 80px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid rgb(28 31 35 / 8%);
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 1px solid rgb(0 101 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(2n) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(n + 3) {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,329 @@
|
||||||
|
import { IconBold, IconFont, IconHelpCircle, IconMark } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Descriptions, Popover, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import cls from 'classnames';
|
||||||
|
import { IconDrawBoard, IconMindCenter, IconStructure } from 'components/icons';
|
||||||
|
import { IconZoomIn, IconZoomOut } from 'components/icons';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { ColorPicker } from 'tiptap/components/color-picker';
|
||||||
|
import { clamp } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
import { MAX_ZOOM, MIN_ZOOM, TEMPLATES, THEMES, ZOOM_STEP } from '../constant';
|
||||||
|
import { Image } from './image';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import { Link } from './link';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const HELP_MESSAGE = [
|
||||||
|
{ key: '新增同级节点', value: 'Enter 键' },
|
||||||
|
{ key: '新增子节点', value: 'Tab 键' },
|
||||||
|
{ key: '编辑节点文字', value: '双击节点' },
|
||||||
|
{ key: '编辑节点菜单', value: '在节点右键' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const HELP_MESSAGE_STYLE = {
|
||||||
|
width: '200px',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Toolbar = ({ mind }) => {
|
||||||
|
const [template, setTemplateState] = useState('');
|
||||||
|
const [theme, setThemeState] = useState('');
|
||||||
|
const [node, setNode] = useState(null);
|
||||||
|
const [isBold, toggleIsBold] = useToggle(false);
|
||||||
|
const [textColor, setTextColor] = useState('');
|
||||||
|
const [bgColor, setBgColor] = useState('');
|
||||||
|
const [link, setLink] = useState('');
|
||||||
|
const [image, setImage] = useState('');
|
||||||
|
|
||||||
|
const setTemplate = useCallback(
|
||||||
|
(template) => {
|
||||||
|
mind.execCommand('template', template);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setTheme = useCallback(
|
||||||
|
(theme) => {
|
||||||
|
mind.execCommand('theme', theme);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setZoom = useCallback(
|
||||||
|
(type: 'minus' | 'plus') => {
|
||||||
|
return () => {
|
||||||
|
if (!mind) return;
|
||||||
|
const currentZoom = mind.getZoomValue();
|
||||||
|
const nextZoom = clamp(
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const toggleBold = useCallback(() => {
|
||||||
|
mind.execCommand('Bold');
|
||||||
|
}, [mind]);
|
||||||
|
|
||||||
|
const setFontColor = useCallback(
|
||||||
|
(color) => {
|
||||||
|
mind.execCommand('ForeColor', color);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setBackgroundColor = useCallback(
|
||||||
|
(color) => {
|
||||||
|
mind.execCommand('Background', color);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setHyperLink = useCallback(
|
||||||
|
(url) => {
|
||||||
|
mind.execCommand('HyperLink', url);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
const insertImage = useCallback(
|
||||||
|
(url) => {
|
||||||
|
mind.execCommand('Image', url);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
|
const handler = () => {
|
||||||
|
const node = mind.getSelectedNode();
|
||||||
|
|
||||||
|
let isBold = false;
|
||||||
|
let textColor;
|
||||||
|
let bgColor;
|
||||||
|
let link;
|
||||||
|
let image;
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
isBold = mind.queryCommandState('Bold') === 1;
|
||||||
|
textColor = mind.queryCommandValue('ForeColor');
|
||||||
|
bgColor = mind.queryCommandValue('Background');
|
||||||
|
link = mind.queryCommandValue('HyperLink').url;
|
||||||
|
image = mind.queryCommandValue('Image').url;
|
||||||
|
setNode(node);
|
||||||
|
} else {
|
||||||
|
setNode(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplateState(mind.queryCommandValue('Template'));
|
||||||
|
setThemeState(mind.queryCommandValue('Theme'));
|
||||||
|
toggleIsBold(isBold);
|
||||||
|
setTextColor(textColor);
|
||||||
|
setBgColor(bgColor);
|
||||||
|
setLink(link);
|
||||||
|
setImage(image);
|
||||||
|
};
|
||||||
|
|
||||||
|
mind.on('interactchange', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mind.off('interactchange', handler);
|
||||||
|
};
|
||||||
|
}, [mind, toggleIsBold, setBackgroundColor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Tooltip content="居中">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconMindCenter style={{ fontSize: '0.85em' }} />}
|
||||||
|
onClick={setCenter}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="缩小">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconZoomOut style={{ fontSize: '0.85em' }} />}
|
||||||
|
onClick={setZoom('minus')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="放大">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconZoomIn style={{ fontSize: '0.85em' }} />}
|
||||||
|
onClick={setZoom('plus')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="加粗" zIndex={10000}>
|
||||||
|
<Button
|
||||||
|
disabled={!node}
|
||||||
|
type="tertiary"
|
||||||
|
theme={isBold ? 'light' : 'borderless'}
|
||||||
|
onClick={toggleBold}
|
||||||
|
icon={<IconBold />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<ColorPicker
|
||||||
|
onSetColor={(color) => {
|
||||||
|
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
|
||||||
|
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} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { IconLink } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Dropdown, Form, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
export const Link = ({ disabled, link, setLink }) => {
|
||||||
|
const $form = useRef<FormApi>();
|
||||||
|
const [initialState, setInitialState] = useState({ link });
|
||||||
|
|
||||||
|
const handleOk = useCallback(() => {
|
||||||
|
$form.current.validate().then((values) => {
|
||||||
|
setLink(values.link);
|
||||||
|
});
|
||||||
|
}, [setLink]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInitialState({ link });
|
||||||
|
}, [link]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
stopPropagation
|
||||||
|
zIndex={10000}
|
||||||
|
trigger="click"
|
||||||
|
position={'bottomLeft'}
|
||||||
|
render={
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Form
|
||||||
|
initValues={initialState}
|
||||||
|
getFormApi={(formApi) => ($form.current = formApi)}
|
||||||
|
labelPosition="left"
|
||||||
|
onSubmit={handleOk}
|
||||||
|
layout="horizontal"
|
||||||
|
>
|
||||||
|
<Form.Input autofocus label="链接" field="link" placeholder="请输入外链地址" />
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span style={{ display: 'inline-block' }}>
|
||||||
|
<Tooltip content="设置链接" zIndex={10000}>
|
||||||
|
<Button disabled={disabled} type="tertiary" theme={link ? 'light' : 'borderless'} icon={<IconLink />} />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue