tiptap: improve mind

This commit is contained in:
fantasticit 2022-04-30 22:03:28 +08:00
parent 2f6713144e
commit 803612a5c5
4 changed files with 38 additions and 146 deletions

View File

@ -19,3 +19,4 @@ export * from './text';
export * from './type'; export * from './type';
export * from './upload'; export * from './upload';
export * from './url'; export * from './url';
export * from './uuid';

View File

@ -41,6 +41,7 @@ export function isInCustomNode(state: EditorState, nodeName: string): boolean {
return true; return true;
} }
} }
return false;
} }
export function isInCodeBlock(state: EditorState): boolean { export function isInCodeBlock(state: EditorState): boolean {

View File

@ -15,4 +15,4 @@
@import './selection.scss'; @import './selection.scss';
@import './table.scss'; @import './table.scss';
@import './title.scss'; @import './title.scss';
@import './kityminder.scss'; @import './mind/index.scss';

View File

@ -2,14 +2,10 @@ import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Spin, Typography } from '@douyinfe/semi-ui'; import { Spin, Typography } from '@douyinfe/semi-ui';
import deepEqual from 'deep-equal';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { clamp, getEditorContainerDOMSize } from 'tiptap/prose-utils'; import { getEditorContainerDOMSize, uuid } from 'tiptap/prose-utils';
import { Mind } from 'tiptap/extensions/mind'; import { Mind } from 'tiptap/extensions/mind';
import { loadKityMinder } from './kityminder';
import { Toolbar } from './toolbar';
import { MIN_ZOOM, MAX_ZOOM, ZOOM_STEP } from './toolbar/constant';
import styles from './index.module.scss'; import styles from './index.module.scss';
const { Text } = Typography; const { Text } = Typography;
@ -17,12 +13,13 @@ const { Text } = Typography;
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%' }; const INHERIT_SIZE_STYLE = { width: '100%', height: '100%' };
export const MindWrapper = ({ editor, node, updateAttributes }) => { export const MindWrapper = ({ editor, node, updateAttributes }) => {
const $container = useRef(); const $container = useRef<HTMLDivElement>();
const $mind = useRef<any>(); const $mind = useRef(null);
const containerId = useRef(`js-mind-container-${uuid()}`);
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const isActive = editor.isActive(Mind.name); const isActive = editor.isActive(Mind.name);
const { width: maxWidth } = getEditorContainerDOMSize(editor); const { width: maxWidth } = getEditorContainerDOMSize(editor);
const { data, template, theme, zoom, width, height } = node.attrs; const { data, width, height } = node.attrs;
const [loading, toggleLoading] = useToggle(true); const [loading, toggleLoading] = useToggle(true);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
@ -42,9 +39,10 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
return ( return (
<div <div
ref={$container} ref={$container}
id={containerId.current}
className={cls(styles.renderWrap, 'render-wrapper')} className={cls(styles.renderWrap, 'render-wrapper')}
tabIndex={0} tabIndex={0}
style={INHERIT_SIZE_STYLE} style={{ ...INHERIT_SIZE_STYLE, overflow: 'hidden' }}
></div> ></div>
); );
}, [loading, error, width, height]); }, [loading, error, width, height]);
@ -52,68 +50,19 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
const onResize = useCallback( const onResize = useCallback(
(size) => { (size) => {
updateAttributes({ width: size.width, height: size.height }); updateAttributes({ width: size.width, height: size.height });
setCenter(); setTimeout(() => {
$mind.current && $mind.current.toCenter();
});
}, },
[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);
isEditable && updateAttributes({ zoom: nextZoom });
};
},
[editor, zoom, isEditable, updateAttributes]
);
const setCenter = useCallback(() => {
const minder = $mind.current;
if (!minder) return;
minder.execCommand('camera');
}, []);
// 布局
const setTemplate = useCallback(
(template) => {
const minder = $mind.current;
if (!minder) return;
minder.execCommand('template', template);
isEditable && updateAttributes({ template });
},
[updateAttributes, isEditable]
);
// 主题
const setTheme = useCallback(
(theme) => {
const minder = $mind.current;
if (!minder) return;
minder.execCommand('theme', theme);
isEditable && updateAttributes({ theme });
},
[updateAttributes, isEditable]
);
const saveData = useCallback(() => {
const minder = $mind.current;
if (!minder) return;
isEditable && updateAttributes({ data: minder.exportJson() });
}, [updateAttributes, isEditable]);
// 加载依赖 // 加载依赖
useEffect(() => { useEffect(() => {
loadKityMinder() import('./mind-elixir')
.then(() => { .then((module) => {
toggleLoading(false); toggleLoading(false);
window.MindElixir = module.default;
}) })
.catch((e) => { .catch((e) => {
setError(e); setError(e);
@ -125,104 +74,45 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
if (loading || !$container.current) return; if (loading || !$container.current) return;
const onChange = () => { const onChange = () => {
saveData(); updateAttributes({ data: mind.getAllData() });
}; };
let mind = null;
try { try {
const Editor = window.kityminder.Editor; mind = new window.MindElixir({
const minder = new Editor($container.current).minder; el: `#${containerId.current}`,
minder.importJson(data); direction: window.MindElixir.SIDE,
minder.execCommand('template', template); data: JSON.parse(JSON.stringify(data)),
minder.execCommand('theme', theme); editable: editor.isEditable,
minder.execCommand('zoom', parseInt(zoom)); draggable: editor.isEditable,
contextMenu: editor.isEditable,
if (!isEditable) { toolBar: true,
minder.preventEdit = true; keypress: editor.isEditable,
} else { nodeMenu: true,
minder.preventEdit = false; locale: 'zh_CN',
minder.enable(); });
} mind.shouldPreventDefault = () => editor.isActive('mind');
mind.init();
$mind.current = minder; mind.bus.addListener('operation', onChange);
minder.on('contentChange', onChange); $mind.current = mind;
toggleLoading(false); toggleLoading(false);
} catch (e) { } catch (e) {
setError(e); setError(e);
} }
return () => { return () => {
if ($mind.current) { if (mind) {
$mind.current.off('contentChange', onChange); mind.bus.removeListener('operation', onChange);
$mind.current.destroy();
} }
}; };
}, [loading]); }, [loading, editor, updateAttributes]);
// 数据同步渲染
useEffect(() => {
const minder = $mind.current;
if (!minder) return;
const currentData = minder.exportJson();
const isEqual = deepEqual(currentData, data);
if (isEqual) return;
// TODO: 也许刷新更好些
minder.importJson(data);
}, [data]);
// 启用/禁用
useEffect(() => {
const minder = $mind.current;
if (!minder) return;
if (!isEditable) {
minder.preventEdit = true;
} else {
minder.preventEdit = false;
minder.enable();
}
}, [isEditable]);
// 缩放
useEffect(() => {
const minder = $mind.current;
if (!minder) return;
minder.execCommand('zoom', parseInt(zoom));
}, [zoom]);
// 布局
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]);
return ( return (
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}> <NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}> <Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
{content} {content}
</Resizeable> </Resizeable>
<div className={styles.toolbarWrap}>
<Toolbar
isEditable={isEditable}
maxHeight={height * 0.8}
template={template}
theme={theme}
zoom={zoom}
setZoomMinus={setZoom('minus')}
setZoomPlus={setZoom('plus')}
setCenter={setCenter}
setTemplate={setTemplate}
setTheme={setTheme}
/>
</div>
</NodeViewWrapper> </NodeViewWrapper>
); );
}; };