mirror of https://github.com/fantasticit/think.git
tiptap: enhance mind
parent
59b2965b79
commit
337c3d172a
|
@ -0,0 +1,14 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconStructure: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
|
||||
<path d="M896 645.888V608C896 519.808 824.256 448 736 448H512V384h32c52.928 0 96-43.072 96-96v-128C640 107.072 596.928 64 544 64h-128C363.072 64 320 107.072 320 160v128C320 340.928 363.072 384 416 384H448v64H224A160.192 160.192 0 0 0 64 608v37.888c-37.184 13.248-64 48.448-64 90.112v128c0 52.928 43.072 96 96 96h64c52.928 0 96-43.072 96-96v-128c0-52.928-43.072-96-96-96H128v-32C128 555.072 171.072 512 224 512H448v128h-32c-52.928 0-96 43.072-96 96v128c0 52.928 43.072 96 96 96h128c52.928 0 96-43.072 96-96v-128c0-52.928-43.072-96-96-96H512V512h224c52.928 0 96 43.072 96 96v32h-32c-52.928 0-96 43.072-96 96v128c0 52.928 43.072 96 96 96h64c52.928 0 96-43.072 96-96v-128c0-41.664-26.816-76.864-64-90.112zM384 288v-128a32 32 0 0 1 32-32h128a32 32 0 0 1 32 32v128a32 32 0 0 1-32 32h-128a32 32 0 0 1-32-32z m-192 448v128a32 32 0 0 1-32 32h-64a32 32 0 0 1-32-32v-128a32 32 0 0 1 32-32h64a32 32 0 0 1 32 32z m384 0v128a32 32 0 0 1-32 32h-128a32 32 0 0 1-32-32v-128a32 32 0 0 1 32-32h128a32 32 0 0 1 32 32z m320 128c0 17.6-14.4 32-32 32h-64a32.064 32.064 0 0 1-32-32v-128c0-17.6 14.4-32 32-32h64c17.6 0 32 14.4 32 32v128z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -51,3 +51,4 @@ export * from './IconGlobe';
|
|||
export * from './IconCountdown';
|
||||
export * from './IconDrawBoard';
|
||||
export * from './IconCallout';
|
||||
export * from './IconStructure';
|
||||
|
|
|
@ -6,7 +6,7 @@ import { getDatasetAttribute } from '../utils/dataset';
|
|||
const DEFAULT_MIND_DATA = {
|
||||
root: { data: { text: '中心节点' }, children: [] },
|
||||
template: 'default',
|
||||
theme: 'fresh-blue',
|
||||
theme: 'classic',
|
||||
version: '1.4.43',
|
||||
};
|
||||
|
||||
|
@ -39,6 +39,18 @@ export const Mind = Node.create({
|
|||
default: DEFAULT_MIND_DATA,
|
||||
parseHTML: getDatasetAttribute('data', true),
|
||||
},
|
||||
template: {
|
||||
default: 'default',
|
||||
parseHTML: getDatasetAttribute('template'),
|
||||
},
|
||||
theme: {
|
||||
default: 'classic',
|
||||
parseHTML: getDatasetAttribute('theme'),
|
||||
},
|
||||
zoom: {
|
||||
default: 100,
|
||||
parseHTML: getDatasetAttribute('zoom'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
.sectionWrap {
|
||||
margin-top: 16px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
width: 168px;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
width: 80px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
padding: 0 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgb(28 31 35 / 8%);
|
||||
|
||||
&.active {
|
||||
border: 1px solid rgb(0 101 255);
|
||||
}
|
||||
|
||||
&:nth-of-type(2n) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&:nth-of-type(n + 3) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,63 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Space, Button, List, Popover, Typography } from '@douyinfe/semi-ui';
|
||||
import cls from 'classnames';
|
||||
import { Space, Button, List, Popover, Typography, RadioGroup, Radio } from '@douyinfe/semi-ui';
|
||||
import { IconEdit, IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { IconDocument } from 'components/icons';
|
||||
import { useWikiTocs } from 'data/wiki';
|
||||
import { IconStructure, IconDrawBoard, IconZoomIn, IconZoomOut } from 'components/icons';
|
||||
import { BubbleMenu } from '../../views/bubble-menu';
|
||||
import { Mind } from '../../extensions/mind';
|
||||
import { Divider } from '../../divider';
|
||||
import { clamp } from '../../utils/clamp';
|
||||
import { TEMPLATES, THEMES, MAX_ZOOM, MIN_ZOOM, ZOOM_STEP } from './constant';
|
||||
import styles from './bubble.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const MindBubbleMenu = ({ editor }) => {
|
||||
const { template, theme, zoom } = editor.getAttributes(Mind.name);
|
||||
|
||||
const setZoom = useCallback(
|
||||
(type: 'minus' | 'plus') => {
|
||||
return () => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Mind.name, {
|
||||
zoom: clamp(type === 'minus' ? parseInt(zoom) - ZOOM_STEP : parseInt(zoom) + ZOOM_STEP, MIN_ZOOM, MAX_ZOOM),
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
};
|
||||
},
|
||||
[editor, zoom]
|
||||
);
|
||||
|
||||
const setTemplate = useCallback(
|
||||
(template) => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Mind.name, {
|
||||
template,
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const setTheme = useCallback(
|
||||
(theme) => {
|
||||
editor
|
||||
.chain()
|
||||
.updateAttributes(Mind.name, {
|
||||
theme,
|
||||
})
|
||||
.focus()
|
||||
.run();
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
||||
|
||||
return (
|
||||
|
@ -24,6 +69,82 @@ export const MindBubbleMenu = ({ editor }) => {
|
|||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||
>
|
||||
<Space>
|
||||
<Tooltip content="缩小">
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
disabled={+zoom <= MIN_ZOOM}
|
||||
icon={<IconZoomOut />}
|
||||
onClick={setZoom('minus')}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text style={{ width: 20, textAlign: 'center' }}>{zoom}</Text>
|
||||
<Tooltip content="放大">
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
disabled={+zoom >= MAX_ZOOM}
|
||||
icon={<IconZoomIn />}
|
||||
onClick={setZoom('plus')}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Popover
|
||||
zIndex={10000}
|
||||
spacing={10}
|
||||
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
||||
content={
|
||||
<section className={styles.sectionWrap}>
|
||||
<Text type="secondary">布局</Text>
|
||||
<div>
|
||||
<ul>
|
||||
{TEMPLATES.map((item) => {
|
||||
return (
|
||||
<li
|
||||
className={cls(template === item.value && styles.active)}
|
||||
onClick={() => setTemplate(item.value)}
|
||||
>
|
||||
{item.label}
|
||||
</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' }}
|
||||
content={
|
||||
<section className={styles.sectionWrap}>
|
||||
<Text type="secondary">主题</Text>
|
||||
<div>
|
||||
<ul>
|
||||
{THEMES.map((item) => {
|
||||
return (
|
||||
<li
|
||||
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>
|
||||
<Divider />
|
||||
<Tooltip content="删除节点" hideOnClick>
|
||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
export const TEMPLATES = [
|
||||
{
|
||||
label: '经典',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: '文件夹',
|
||||
value: 'filetree',
|
||||
},
|
||||
{
|
||||
label: '鱼骨图',
|
||||
value: 'fish-bone',
|
||||
},
|
||||
{
|
||||
label: '靠右',
|
||||
value: 'right',
|
||||
},
|
||||
{
|
||||
label: '组织',
|
||||
value: 'structure',
|
||||
},
|
||||
{
|
||||
label: '天盘',
|
||||
value: 'tianpan',
|
||||
},
|
||||
];
|
||||
|
||||
export const THEMES = [
|
||||
{
|
||||
label: '经典',
|
||||
value: 'classic',
|
||||
style: {
|
||||
color: 'rgb(68, 51, 0)',
|
||||
background: ' rgb(233, 223, 152)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '紧凑',
|
||||
value: 'classic-compact',
|
||||
style: {
|
||||
color: 'rgb(68, 51, 0)',
|
||||
background: ' rgb(233, 223, 152)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '清新红',
|
||||
value: 'fresh-red',
|
||||
style: {
|
||||
color: 'white',
|
||||
background: ' rgb(191, 115, 115)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '泥土黄',
|
||||
value: 'fresh-soil',
|
||||
style: {
|
||||
color: 'white',
|
||||
background: 'rgb(191, 147, 115)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '文艺绿',
|
||||
value: 'fresh-green',
|
||||
style: {
|
||||
color: 'white',
|
||||
background: 'rgb(115, 191, 118)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '天空蓝',
|
||||
value: 'fresh-blue',
|
||||
style: {
|
||||
color: 'white',
|
||||
background: 'rgb(115, 161, 191)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '浪漫紫',
|
||||
value: 'fresh-purple',
|
||||
style: {
|
||||
color: 'white',
|
||||
background: 'rgb(123, 115, 191)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '胭脂粉',
|
||||
value: 'fresh-pink',
|
||||
style: {
|
||||
color: 'white',
|
||||
background: 'rgb(191, 115, 148)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '冷光',
|
||||
value: 'snow',
|
||||
style: {
|
||||
color: '#fff',
|
||||
background: 'rgb(164, 197, 192)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '鱼骨图',
|
||||
value: 'fish',
|
||||
style: {
|
||||
color: '#fff',
|
||||
background: 'rgb(58, 65, 68)',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const MIN_ZOOM = 10;
|
||||
export const MAX_ZOOM = 200;
|
||||
export const ZOOM_STEP = 15;
|
|
@ -3,11 +3,32 @@
|
|||
max-width: 100%;
|
||||
overflow: visible;
|
||||
line-height: 0;
|
||||
outline: none;
|
||||
|
||||
.renderWrap {
|
||||
border: 1px solid var(--node-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&::after {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mindHandlerWrap {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 1000;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--semi-color-bg-2);
|
||||
border: 1px solid var(--node-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
opacity: 0;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
&.isActive {
|
||||
.mindHandlerWrap {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { NodeViewWrapper } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { Spin, Button } from '@douyinfe/semi-ui';
|
||||
import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
|
||||
import { Resizeable } from 'components/resizeable';
|
||||
import deepEqual from 'deep-equal';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { MIN_ZOOM, MAX_ZOOM, ZOOM_STEP } from '../../menus/mind/constant';
|
||||
import { clamp } from '../../utils/clamp';
|
||||
import { Mind } from '../../extensions/mind';
|
||||
import { loadKityMinder } from './kityminder';
|
||||
import styles from './index.module.scss';
|
||||
|
@ -15,7 +17,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
const $mind = useRef<any>();
|
||||
const isMindActive = editor.isActive(Mind.name);
|
||||
const isEditable = editor.isEditable;
|
||||
const { data, width, height = 100 } = node.attrs;
|
||||
const { data, template, theme, zoom, width, height = 100 } = node.attrs;
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
|
||||
const onResize = useCallback(
|
||||
|
@ -25,6 +27,23 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
[updateAttributes]
|
||||
);
|
||||
|
||||
const setZoom = useCallback(
|
||||
(type: 'minus' | 'plus') => {
|
||||
return () => {
|
||||
const minder = $mind.current;
|
||||
if (!minder) return;
|
||||
const currentZoom = minder.getZoomValue();
|
||||
const nextZoom = clamp(
|
||||
type === 'minus' ? currentZoom - ZOOM_STEP : currentZoom + ZOOM_STEP,
|
||||
MIN_ZOOM,
|
||||
MAX_ZOOM
|
||||
);
|
||||
minder.execCommand('zoom', nextZoom);
|
||||
};
|
||||
},
|
||||
[editor, zoom]
|
||||
);
|
||||
|
||||
const saveData = useCallback(() => {
|
||||
const minder = $mind.current;
|
||||
if (!minder) return;
|
||||
|
@ -38,12 +57,15 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
};
|
||||
loadKityMinder().then((Editor) => {
|
||||
toggleLoading(false);
|
||||
|
||||
try {
|
||||
const minder = new Editor($container.current).minder;
|
||||
minder.importJson(data);
|
||||
$mind.current = minder;
|
||||
minder.on('contentChange', onChange);
|
||||
// @ts-ignore
|
||||
window.minder = minder;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
@ -63,13 +85,42 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
minder.importData(data);
|
||||
}, [data]);
|
||||
|
||||
// 布局
|
||||
useEffect(() => {
|
||||
const minder = $mind.current;
|
||||
if (!minder) return;
|
||||
minder.execCommand('template', template);
|
||||
}, [template]);
|
||||
|
||||
// 主题
|
||||
useEffect(() => {
|
||||
const minder = $mind.current;
|
||||
if (!minder) return;
|
||||
minder.execCommand('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
// 缩放
|
||||
useEffect(() => {
|
||||
const minder = $mind.current;
|
||||
if (!minder) return;
|
||||
minder.execCommand('zoom', parseInt(zoom));
|
||||
}, [zoom]);
|
||||
|
||||
// 启用/禁用
|
||||
useEffect(() => {
|
||||
const minder = $mind.current;
|
||||
if (!minder) return;
|
||||
|
||||
if (isEditable) {
|
||||
minder.enable();
|
||||
} else {
|
||||
minder.disable();
|
||||
}
|
||||
}, [isEditable]);
|
||||
|
||||
const content = (
|
||||
const content = loading ? (
|
||||
<Spin spinning={loading} style={{ width: '100%', height: '100%' }}></Spin>
|
||||
) : (
|
||||
<div
|
||||
ref={$container}
|
||||
className={cls(styles.renderWrap, 'render-wrapper')}
|
||||
|
@ -87,6 +138,25 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
|||
) : (
|
||||
<div style={{ display: 'inline-block', width, height, maxWidth: '100%' }}>{content}</div>
|
||||
)}
|
||||
|
||||
{!isEditable && (
|
||||
<div className={styles.mindHandlerWrap}>
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconMinus style={{ fontSize: 14 }} />}
|
||||
onClick={setZoom('minus')}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconPlus style={{ fontSize: 14 }} />}
|
||||
onClick={setZoom('plus')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,9 @@ define(function (require, exports, module) {
|
|||
Minder.registerInitHook(function () {
|
||||
this.on('beforemousedown', function (e) {
|
||||
this.focus();
|
||||
// e.preventDefault();
|
||||
// FIXME:如果遇到事件触发问题,需要检查这里
|
||||
if (e.kityEvent.targetShape.__KityClassName === 'Paper') return;
|
||||
e.preventDefault();
|
||||
});
|
||||
this.on('paperrender', function () {
|
||||
this.focus();
|
||||
|
|
Loading…
Reference in New Issue