mirror of https://github.com/fantasticit/think.git
tiptap: improve mind
This commit is contained in:
parent
2f6713144e
commit
803612a5c5
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue