mirror of https://github.com/fantasticit/think.git
feat: add toolbar for mind
This commit is contained in:
parent
6550d7cb14
commit
65b7a1451a
|
@ -1,86 +1,35 @@
|
|||
import { IconHelpCircle, IconMinus, IconPlus } from '@douyinfe/semi-icons';
|
||||
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 { Modal, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
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 { Editor } from 'tiptap/editor';
|
||||
import { clamp } from 'tiptap/prose-utils';
|
||||
|
||||
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 { Toolbar } from './toolbar';
|
||||
|
||||
type IProps = { editor: Editor };
|
||||
|
||||
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 }) => {
|
||||
const $mind = useRef(null);
|
||||
const [initialData, setInitialData] = useState({ template: '', theme: '' });
|
||||
const [template, setTemplateState] = useState('');
|
||||
const [theme, setThemeState] = useState('');
|
||||
const [mind, setMind] = useState(null);
|
||||
const [initialData, setInitialData] = useState({});
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const setZoom = useCallback((type: 'minus' | 'plus') => {
|
||||
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(
|
||||
const renderMindEditor = useCallback(
|
||||
(div) => {
|
||||
if (!div) return;
|
||||
|
||||
if ($mind.current) {
|
||||
$mind.current.destroy();
|
||||
$mind.current = null;
|
||||
}
|
||||
|
||||
$mind.current = renderMind({
|
||||
const mindInstance = renderMind({
|
||||
container: div,
|
||||
data: initialData,
|
||||
isEditable: true,
|
||||
});
|
||||
|
||||
setMind(mindInstance);
|
||||
},
|
||||
[initialData]
|
||||
);
|
||||
|
@ -89,32 +38,23 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
|||
load()
|
||||
.catch(setError)
|
||||
.finally(() => toggleLoading(false));
|
||||
|
||||
return () => {
|
||||
if ($mind.current) {
|
||||
$mind.current.destroy();
|
||||
$mind.current = null;
|
||||
}
|
||||
};
|
||||
}, [toggleLoading]);
|
||||
|
||||
const save = useCallback(() => {
|
||||
if (!$mind.current) {
|
||||
if (!mind) {
|
||||
toggleVisible(false);
|
||||
return;
|
||||
}
|
||||
const data = $mind.current.exportJson();
|
||||
const data = mind.exportJson();
|
||||
editor.chain().focus().setMind({ data }).run();
|
||||
toggleVisible(false);
|
||||
}, [editor, toggleVisible]);
|
||||
}, [editor, toggleVisible, mind]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (data) => {
|
||||
toggleVisible(true);
|
||||
if (data) {
|
||||
setInitialData(data.data);
|
||||
setTemplateState(data.data.template);
|
||||
setThemeState(data.data.theme);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -151,116 +91,10 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
|||
</Spin>
|
||||
)}
|
||||
{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}>
|
||||
<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={<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>
|
||||
<Toolbar mind={mind} />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -1,61 +1,13 @@
|
|||
.toolbarWrap {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 2px 4px;
|
||||
width: 100%;
|
||||
padding: 6px 24px;
|
||||
overflow-x: auto;
|
||||
color: #fff;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: 3px;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
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