mirror of https://github.com/fantasticit/think.git
tiptap: fix mind toolbar
This commit is contained in:
parent
a9eefd3c37
commit
2ad45a880e
|
@ -1,4 +1,4 @@
|
||||||
export const Divider = () => {
|
export const Divider = ({ vertical = false }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -7,6 +7,7 @@ export const Divider = () => {
|
||||||
height: 24,
|
height: 24,
|
||||||
margin: '0 6px',
|
margin: '0 6px',
|
||||||
backgroundColor: 'var(--semi-color-border)',
|
backgroundColor: 'var(--semi-color-border)',
|
||||||
|
transform: `rotate(${vertical ? 90 : 0}deg)`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,74 +1,11 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import cls from 'classnames';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import { Space, Button, Popover, Typography } from '@douyinfe/semi-ui';
|
import { IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { IconAlignCenter, IconDelete } from '@douyinfe/semi-icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconStructure, IconDrawBoard, IconZoomIn, IconZoomOut } from 'components/icons';
|
|
||||||
import { BubbleMenu } from '../../views/bubble-menu';
|
import { BubbleMenu } from '../../views/bubble-menu';
|
||||||
import { Mind } from '../../extensions/mind';
|
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 }) => {
|
export const MindBubbleMenu = ({ editor }) => {
|
||||||
const { template, theme, zoom, callCenterCount } = 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 setCenter = useCallback(() => {
|
|
||||||
const nextValue = Number.isNaN(callCenterCount) ? 1 : Number(callCenterCount) + 1;
|
|
||||||
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Mind.name, {
|
|
||||||
callCenterCount: nextValue,
|
|
||||||
})
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}, [editor, callCenterCount]);
|
|
||||||
|
|
||||||
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]);
|
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -80,90 +17,6 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<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>
|
|
||||||
|
|
||||||
<Tooltip content="居中">
|
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconAlignCenter />} onClick={setCenter} />
|
|
||||||
</Tooltip>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<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
|
|
||||||
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' }}
|
|
||||||
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>
|
|
||||||
<Divider />
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -13,21 +13,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mindHandlerWrap {
|
.toolbarWrap {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
top: 50%;
|
||||||
bottom: 20px;
|
left: 0;
|
||||||
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;
|
opacity: 0;
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.isActive {
|
&.isActive {
|
||||||
.mindHandlerWrap {
|
.toolbarWrap {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { NodeViewWrapper } from '@tiptap/react';
|
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, Button, Typography } from '@douyinfe/semi-ui';
|
import { Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
import { IconMinus, IconPlus, IconAlignCenter } from '@douyinfe/semi-icons';
|
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
import { Resizeable } from 'components/resizeable';
|
import { Resizeable } from 'components/resizeable';
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { MIN_ZOOM, MAX_ZOOM, ZOOM_STEP } from '../../menus/mind/constant';
|
|
||||||
import { clamp } from '../../utils/clamp';
|
import { clamp } from '../../utils/clamp';
|
||||||
import { Mind } from '../../extensions/mind';
|
import { Mind } from '../../extensions/mind';
|
||||||
import { loadKityMinder } from './kityminder';
|
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;
|
||||||
|
@ -20,7 +19,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const $mind = useRef<any>();
|
const $mind = useRef<any>();
|
||||||
const isMindActive = editor.isActive(Mind.name);
|
const isMindActive = editor.isActive(Mind.name);
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const { data, template, theme, zoom, callCenterCount, width, height } = node.attrs;
|
const { data, template, theme, zoom, 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);
|
||||||
|
|
||||||
|
@ -55,12 +54,6 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
[updateAttributes]
|
[updateAttributes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setCenter = useCallback(() => {
|
|
||||||
const minder = $mind.current;
|
|
||||||
if (!minder) return;
|
|
||||||
minder.execCommand('camera');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setZoom = useCallback(
|
const setZoom = useCallback(
|
||||||
(type: 'minus' | 'plus') => {
|
(type: 'minus' | 'plus') => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -73,16 +66,45 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
MAX_ZOOM
|
MAX_ZOOM
|
||||||
);
|
);
|
||||||
minder.execCommand('zoom', nextZoom);
|
minder.execCommand('zoom', nextZoom);
|
||||||
|
isEditable && updateAttributes({ zoom: nextZoom });
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[editor, zoom]
|
[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 saveData = useCallback(() => {
|
||||||
const minder = $mind.current;
|
const minder = $mind.current;
|
||||||
if (!minder) return;
|
if (!minder) return;
|
||||||
updateAttributes({ data: minder.exportJson() });
|
isEditable && updateAttributes({ data: minder.exportJson() });
|
||||||
}, [updateAttributes]);
|
}, [updateAttributes, isEditable]);
|
||||||
|
|
||||||
// 加载依赖
|
// 加载依赖
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -113,6 +135,8 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
|
|
||||||
if (!isEditable) {
|
if (!isEditable) {
|
||||||
minder.disable();
|
minder.disable();
|
||||||
|
} else {
|
||||||
|
minder.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
$mind.current = minder;
|
$mind.current = minder;
|
||||||
|
@ -125,6 +149,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
return () => {
|
return () => {
|
||||||
if ($mind.current) {
|
if ($mind.current) {
|
||||||
$mind.current.off('contentChange', onChange);
|
$mind.current.off('contentChange', onChange);
|
||||||
|
$mind.current.destroy();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [loading]);
|
}, [loading]);
|
||||||
|
@ -133,14 +158,33 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const minder = $mind.current;
|
const minder = $mind.current;
|
||||||
if (!minder) return;
|
if (!minder) return;
|
||||||
|
|
||||||
const currentData = minder.exportJson();
|
const currentData = minder.exportJson();
|
||||||
const isEqual = deepEqual(currentData, data);
|
const isEqual = deepEqual(currentData, data);
|
||||||
if (isEqual) return;
|
if (isEqual) return;
|
||||||
|
|
||||||
// TODO: 也许刷新更好些
|
// TODO: 也许刷新更好些
|
||||||
minder.importJson(data);
|
minder.importJson(data);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// 启用/禁用
|
||||||
|
useEffect(() => {
|
||||||
|
const minder = $mind.current;
|
||||||
|
if (!minder) return;
|
||||||
|
|
||||||
|
if (!isEditable) {
|
||||||
|
minder.disable();
|
||||||
|
} else {
|
||||||
|
minder.enable();
|
||||||
|
}
|
||||||
|
}, [isEditable]);
|
||||||
|
|
||||||
|
// 缩放
|
||||||
|
useEffect(() => {
|
||||||
|
const minder = $mind.current;
|
||||||
|
if (!minder) return;
|
||||||
|
minder.execCommand('zoom', parseInt(zoom));
|
||||||
|
}, [zoom]);
|
||||||
|
|
||||||
// 布局
|
// 布局
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const minder = $mind.current;
|
const minder = $mind.current;
|
||||||
|
@ -155,30 +199,6 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
minder.execCommand('theme', theme);
|
minder.execCommand('theme', 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]);
|
|
||||||
|
|
||||||
// 居中
|
|
||||||
useEffect(() => {
|
|
||||||
setCenter();
|
|
||||||
}, [callCenterCount]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className={cls(styles.wrap, isMindActive && styles.isActive)}>
|
<NodeViewWrapper className={cls(styles.wrap, isMindActive && styles.isActive)}>
|
||||||
{isEditable ? (
|
{isEditable ? (
|
||||||
|
@ -188,38 +208,19 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'inline-block', width, height, maxWidth: '100%' }}>{content}</div>
|
<div style={{ display: 'inline-block', width, height, maxWidth: '100%' }}>{content}</div>
|
||||||
)}
|
)}
|
||||||
|
<div className={styles.toolbarWrap}>
|
||||||
{!isEditable && (
|
<Toolbar
|
||||||
<div className={styles.mindHandlerWrap}>
|
isEditable={isEditable}
|
||||||
<Tooltip content="缩小">
|
template={template}
|
||||||
<Button
|
theme={theme}
|
||||||
size="small"
|
zoom={zoom}
|
||||||
theme="borderless"
|
setZoomMinus={setZoom('minus')}
|
||||||
type="tertiary"
|
setZoomPlus={setZoom('plus')}
|
||||||
icon={<IconMinus style={{ fontSize: 14 }} />}
|
setCenter={setCenter}
|
||||||
onClick={setZoom('minus')}
|
setTemplate={setTemplate}
|
||||||
/>
|
setTheme={setTheme}
|
||||||
</Tooltip>
|
/>
|
||||||
<Tooltip content="放大">
|
</div>
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
theme="borderless"
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconPlus style={{ fontSize: 14 }} />}
|
|
||||||
onClick={setZoom('plus')}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="居中">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
theme="borderless"
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconAlignCenter style={{ fontSize: 14 }} />}
|
|
||||||
onClick={setCenter}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
export const loadKityMinder = async (): Promise<any> => {
|
export const loadKityMinder = async (): Promise<any> => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
if (window.kityminder) {
|
if (window.kityminder) {
|
||||||
if (window.kityminder.Editor) return;
|
if (window.kityminder.Editor) {
|
||||||
|
console.log('无需重复');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@ define(function (require, exports, module) {
|
||||||
|
|
||||||
Minder.registerInitHook(function () {
|
Minder.registerInitHook(function () {
|
||||||
this.on('beforemousedown', function (e) {
|
this.on('beforemousedown', function (e) {
|
||||||
this.focus();
|
|
||||||
// FIXME:如果遇到事件触发问题,需要检查这里
|
// FIXME:如果遇到事件触发问题,需要检查这里
|
||||||
if (e.kityEvent.targetShape.__KityClassName === 'Paper') return;
|
if (e.kityEvent.targetShape.__KityClassName === 'Paper') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
this.focus();
|
||||||
});
|
});
|
||||||
this.on('paperrender', function () {
|
this.on('paperrender', function () {
|
||||||
this.focus();
|
this.focus();
|
||||||
|
|
|
@ -7,57 +7,56 @@
|
||||||
* @copyright: Baidu FEX, 2014
|
* @copyright: Baidu FEX, 2014
|
||||||
*/
|
*/
|
||||||
|
|
||||||
define(function(require, exports, module) {
|
define(function (require, exports, module) {
|
||||||
var kity = require('./kity');
|
var kity = require('./kity');
|
||||||
var Minder = require('./minder');
|
var Minder = require('./minder');
|
||||||
var MinderEvent = require('./event');
|
var MinderEvent = require('./event');
|
||||||
|
|
||||||
Minder.registerInitHook(function(options) {
|
Minder.registerInitHook(function (options) {
|
||||||
if (options.readOnly) {
|
if (options.readOnly) {
|
||||||
this.setDisabled();
|
this.setDisabled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
kity.extendClass(Minder, {
|
||||||
|
disable: function () {
|
||||||
|
var me = this;
|
||||||
|
//禁用命令
|
||||||
|
me.bkqueryCommandState = me.queryCommandState;
|
||||||
|
me.bkqueryCommandValue = me.queryCommandValue;
|
||||||
|
me.queryCommandState = function (type) {
|
||||||
|
var cmd = this._getCommand(type);
|
||||||
|
if (cmd && cmd.enableReadOnly) {
|
||||||
|
return me.bkqueryCommandState.apply(me, arguments);
|
||||||
}
|
}
|
||||||
});
|
return -1;
|
||||||
|
};
|
||||||
kity.extendClass(Minder, {
|
me.queryCommandValue = function (type) {
|
||||||
|
var cmd = this._getCommand(type);
|
||||||
disable: function() {
|
if (cmd && cmd.enableReadOnly) {
|
||||||
var me = this;
|
return me.bkqueryCommandValue.apply(me, arguments);
|
||||||
//禁用命令
|
|
||||||
me.bkqueryCommandState = me.queryCommandState;
|
|
||||||
me.bkqueryCommandValue = me.queryCommandValue;
|
|
||||||
me.queryCommandState = function(type) {
|
|
||||||
var cmd = this._getCommand(type);
|
|
||||||
if (cmd && cmd.enableReadOnly) {
|
|
||||||
return me.bkqueryCommandState.apply(me, arguments);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
me.queryCommandValue = function(type) {
|
|
||||||
var cmd = this._getCommand(type);
|
|
||||||
if (cmd && cmd.enableReadOnly) {
|
|
||||||
return me.bkqueryCommandValue.apply(me, arguments);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
this.setStatus('readonly');
|
|
||||||
me._interactChange();
|
|
||||||
},
|
|
||||||
|
|
||||||
enable: function() {
|
|
||||||
var me = this;
|
|
||||||
|
|
||||||
if (me.bkqueryCommandState) {
|
|
||||||
me.queryCommandState = me.bkqueryCommandState;
|
|
||||||
delete me.bkqueryCommandState;
|
|
||||||
}
|
|
||||||
if (me.bkqueryCommandValue) {
|
|
||||||
me.queryCommandValue = me.bkqueryCommandValue;
|
|
||||||
delete me.bkqueryCommandValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStatus('normal');
|
|
||||||
|
|
||||||
me._interactChange();
|
|
||||||
}
|
}
|
||||||
});
|
return null;
|
||||||
});
|
};
|
||||||
|
this.setStatus('readonly');
|
||||||
|
me._interactChange();
|
||||||
|
},
|
||||||
|
|
||||||
|
enable: function () {
|
||||||
|
var me = this;
|
||||||
|
|
||||||
|
if (me.bkqueryCommandState) {
|
||||||
|
me.queryCommandState = me.bkqueryCommandState;
|
||||||
|
delete me.bkqueryCommandState;
|
||||||
|
}
|
||||||
|
if (me.bkqueryCommandValue) {
|
||||||
|
me.queryCommandValue = me.bkqueryCommandValue;
|
||||||
|
delete me.bkqueryCommandValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus('normal');
|
||||||
|
|
||||||
|
me._interactChange();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -222,22 +222,23 @@ define(function (require, exports, module) {
|
||||||
let dx = 0;
|
let dx = 0;
|
||||||
let dy = 0;
|
let dy = 0;
|
||||||
|
|
||||||
if (!focusNode || focusNode.type === 'root') {
|
// if (!focusNode || focusNode.type === 'root') {
|
||||||
// 默认居中
|
// // 默认居中
|
||||||
const parentNode = km.getPaper().node;
|
// const parentNode = km.getPaper().node;
|
||||||
const shapeNode = km.getRoot().rc.container.node;
|
// const shapeNode = km.getRoot().rc.container.node;
|
||||||
const { width: pw, height: ph, x: px, y: py } = parentNode.getBoundingClientRect();
|
// const { width: pw, height: ph, x: px, y: py } = parentNode.getBoundingClientRect();
|
||||||
const { width: sw, height: sh, x, y } = shapeNode.getBBox();
|
// const { width: sw, height: sh, x, y } = shapeNode.getBBox();
|
||||||
dx = pw / 2 - x - sw / 2;
|
// dx = pw / 2 - x - sw / 2;
|
||||||
dy = ph / 2 - y - sh / 2;
|
// dy = ph / 2 - y - sh / 2;
|
||||||
dragger.moveTo(new kity.Point(dx, dy), duration);
|
// dragger.moveTo(new kity.Point(dx, dy), duration);
|
||||||
} else {
|
// } else {
|
||||||
var viewport = km.getPaper().getViewPort();
|
focusNode = focusNode || km.getRoot();
|
||||||
var offset = focusNode.getRenderContainer().getRenderBox('view');
|
var viewport = km.getPaper().getViewPort();
|
||||||
dx = viewport.center.x - offset.x - offset.width / 2;
|
var offset = focusNode.getRenderContainer().getRenderBox('view');
|
||||||
dy = viewport.center.y - offset.y;
|
dx = viewport.center.x - offset.x - offset.width / 2;
|
||||||
dragger.move(new kity.Point(dx, dy), duration);
|
dy = viewport.center.y - offset.y;
|
||||||
}
|
dragger.move(new kity.Point(dx, dy), duration);
|
||||||
|
// }
|
||||||
|
|
||||||
this.setContentChanged(false);
|
this.setContentChanged(false);
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,139 +7,137 @@
|
||||||
* @copyright: Baidu FEX, 2014
|
* @copyright: Baidu FEX, 2014
|
||||||
*/
|
*/
|
||||||
|
|
||||||
define(function(require, exports, module) {
|
define(function (require, exports, module) {
|
||||||
var key = require('../tool/key');
|
var key = require('../tool/key');
|
||||||
|
|
||||||
function ReceiverRuntime() {
|
function ReceiverRuntime() {
|
||||||
var fsm = this.fsm;
|
var fsm = this.fsm;
|
||||||
var minder = this.minder;
|
var minder = this.minder;
|
||||||
var me = this;
|
var me = this;
|
||||||
|
|
||||||
// 接收事件的 div
|
// 接收事件的 div
|
||||||
var element = document.createElement('div');
|
var element = document.createElement('div');
|
||||||
element.contentEditable = true;
|
element.contentEditable = true;
|
||||||
/**
|
/**
|
||||||
* @Desc: 增加tabindex属性使得element的contenteditable不管是trur还是false都能有focus和blur事件
|
* @Desc: 增加tabindex属性使得element的contenteditable不管是trur还是false都能有focus和blur事件
|
||||||
* @Editor: Naixor
|
* @Editor: Naixor
|
||||||
* @Date: 2015.09.14
|
* @Date: 2015.09.14
|
||||||
*/
|
*/
|
||||||
element.setAttribute('tabindex', -1);
|
element.setAttribute('tabindex', -1);
|
||||||
element.classList.add('receiver');
|
element.classList.add('receiver');
|
||||||
element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent;
|
element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent;
|
||||||
element.addEventListener('compositionstart', dispatchKeyEvent);
|
element.addEventListener('compositionstart', dispatchKeyEvent);
|
||||||
// element.addEventListener('compositionend', dispatchKeyEvent);
|
// element.addEventListener('compositionend', dispatchKeyEvent);
|
||||||
this.container.appendChild(element);
|
this.container.appendChild(element);
|
||||||
|
|
||||||
// receiver 对象
|
// receiver 对象
|
||||||
var receiver = {
|
var receiver = {
|
||||||
element: element,
|
element: element,
|
||||||
selectAll: function() {
|
selectAll: function () {
|
||||||
// 保证有被选中的
|
// 保证有被选中的
|
||||||
if (!element.innerHTML) element.innerHTML = ' ';
|
if (!element.innerHTML) element.innerHTML = ' ';
|
||||||
var range = document.createRange();
|
var range = document.createRange();
|
||||||
var selection = window.getSelection();
|
var selection = window.getSelection();
|
||||||
range.selectNodeContents(element);
|
range.selectNodeContents(element);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
element.focus();
|
element.focus();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @Desc: 增加enable和disable方法用于解决热核态的输入法屏蔽问题
|
* @Desc: 增加enable和disable方法用于解决热核态的输入法屏蔽问题
|
||||||
* @Editor: Naixor
|
* @Editor: Naixor
|
||||||
* @Date: 2015.09.14
|
* @Date: 2015.09.14
|
||||||
*/
|
*/
|
||||||
enable: function() {
|
enable: function () {
|
||||||
element.setAttribute("contenteditable", true);
|
element.setAttribute('contenteditable', true);
|
||||||
},
|
},
|
||||||
disable: function() {
|
disable: function () {
|
||||||
element.setAttribute("contenteditable", false);
|
element.setAttribute('contenteditable', false);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @Desc: hack FF下div contenteditable的光标丢失BUG
|
* @Desc: hack FF下div contenteditable的光标丢失BUG
|
||||||
* @Editor: Naixor
|
* @Editor: Naixor
|
||||||
* @Date: 2015.10.15
|
* @Date: 2015.10.15
|
||||||
*/
|
*/
|
||||||
fixFFCaretDisappeared: function() {
|
fixFFCaretDisappeared: function () {
|
||||||
element.removeAttribute("contenteditable");
|
element.removeAttribute('contenteditable');
|
||||||
element.setAttribute("contenteditable", "true");
|
element.setAttribute('contenteditable', 'true');
|
||||||
element.blur();
|
element.blur();
|
||||||
element.focus();
|
element.focus();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 以此事件代替通过mouse事件来判断receiver丢失焦点的事件
|
* 以此事件代替通过mouse事件来判断receiver丢失焦点的事件
|
||||||
* @editor Naixor
|
* @editor Naixor
|
||||||
* @Date 2015-12-2
|
* @Date 2015-12-2
|
||||||
*/
|
*/
|
||||||
onblur: function (handler) {
|
onblur: function (handler) {
|
||||||
element.onblur = handler;
|
element.onblur = handler;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
receiver.selectAll();
|
receiver.selectAll();
|
||||||
|
|
||||||
minder.on('beforemousedown', receiver.selectAll);
|
minder.on('beforemousedown', receiver.selectAll);
|
||||||
minder.on('receiverfocus', receiver.selectAll);
|
minder.on('receiverfocus', receiver.selectAll);
|
||||||
minder.on('readonly', function() {
|
minder.on('readonly', function () {
|
||||||
// 屏蔽minder的事件接受,删除receiver和hotbox
|
// 屏蔽minder的事件接受,删除receiver和hotbox
|
||||||
minder.disable();
|
minder.disable();
|
||||||
editor.receiver.element.parentElement.removeChild(editor.receiver.element);
|
editor.receiver.element.parentElement.removeChild(editor.receiver.element);
|
||||||
editor.hotbox.$container.removeChild(editor.hotbox.$element);
|
editor.hotbox.$container.removeChild(editor.hotbox.$element);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 侦听器,接收到的事件会派发给所有侦听器
|
// 侦听器,接收到的事件会派发给所有侦听器
|
||||||
var listeners = [];
|
var listeners = [];
|
||||||
|
|
||||||
// 侦听指定状态下的事件,如果不传 state,侦听所有状态
|
// 侦听指定状态下的事件,如果不传 state,侦听所有状态
|
||||||
receiver.listen = function(state, listener) {
|
receiver.listen = function (state, listener) {
|
||||||
if (arguments.length == 1) {
|
if (arguments.length == 1) {
|
||||||
listener = state;
|
listener = state;
|
||||||
state = '*';
|
state = '*';
|
||||||
}
|
}
|
||||||
listener.notifyState = state;
|
listener.notifyState = state;
|
||||||
listeners.push(listener);
|
listeners.push(listener);
|
||||||
};
|
};
|
||||||
|
|
||||||
function dispatchKeyEvent(e) {
|
function dispatchKeyEvent(e) {
|
||||||
e.is = function(keyExpression) {
|
e.is = function (keyExpression) {
|
||||||
var subs = keyExpression.split('|');
|
var subs = keyExpression.split('|');
|
||||||
for (var i = 0; i < subs.length; i++) {
|
for (var i = 0; i < subs.length; i++) {
|
||||||
if (key.is(this, subs[i])) return true;
|
if (key.is(this, subs[i])) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
var listener, jumpState;
|
var listener, jumpState;
|
||||||
for (var i = 0; i < listeners.length; i++) {
|
for (var i = 0; i < listeners.length; i++) {
|
||||||
|
listener = listeners[i];
|
||||||
listener = listeners[i];
|
// 忽略不在侦听状态的侦听器
|
||||||
// 忽略不在侦听状态的侦听器
|
if (listener.notifyState != '*' && listener.notifyState != fsm.state()) {
|
||||||
if (listener.notifyState != '*' && listener.notifyState != fsm.state()) {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* 对于所有的侦听器,只允许一种处理方式:跳转状态。
|
|
||||||
* 如果侦听器确定要跳转,则返回要跳转的状态。
|
|
||||||
* 每个事件只允许一个侦听器进行状态跳转
|
|
||||||
* 跳转动作由侦听器自行完成(因为可能需要在跳转时传递 reason),返回跳转结果即可。
|
|
||||||
* 比如:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* receiver.listen('normal', function(e) {
|
|
||||||
* if (isSomeReasonForJumpState(e)) {
|
|
||||||
* return fsm.jump('newstate', e);
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
if (listener.call(null, e)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.receiver = receiver;
|
/**
|
||||||
|
*
|
||||||
|
* 对于所有的侦听器,只允许一种处理方式:跳转状态。
|
||||||
|
* 如果侦听器确定要跳转,则返回要跳转的状态。
|
||||||
|
* 每个事件只允许一个侦听器进行状态跳转
|
||||||
|
* 跳转动作由侦听器自行完成(因为可能需要在跳转时传递 reason),返回跳转结果即可。
|
||||||
|
* 比如:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* receiver.listen('normal', function(e) {
|
||||||
|
* if (isSomeReasonForJumpState(e)) {
|
||||||
|
* return fsm.jump('newstate', e);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
if (listener.call(null, e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return module.exports = ReceiverRuntime;
|
this.receiver = receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (module.exports = ReceiverRuntime);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
.wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 1em;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
padding: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
background-color: var(--semi-color-nav-bg);
|
||||||
|
border: 1px solid var(--semi-color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sectionWrap {
|
.sectionWrap {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
import cls from 'classnames';
|
||||||
|
import { Button, Popover, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { IconMinus, IconPlus, IconAlignCenter } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { IconStructure, IconDrawBoard, IconZoomIn, IconZoomOut } from 'components/icons';
|
||||||
|
import { Divider } from '../../../divider';
|
||||||
|
import { TEMPLATES, THEMES, MAX_ZOOM, MIN_ZOOM } from './constant';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isEditable: boolean;
|
||||||
|
zoom: number | string;
|
||||||
|
template: string;
|
||||||
|
theme: string;
|
||||||
|
setZoomMinus: () => void;
|
||||||
|
setZoomPlus: () => void;
|
||||||
|
setCenter: () => void;
|
||||||
|
setTemplate: (arg: string) => void;
|
||||||
|
setTheme: (arg: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toolbar: React.FC<IProps> = ({
|
||||||
|
isEditable,
|
||||||
|
template,
|
||||||
|
theme,
|
||||||
|
zoom,
|
||||||
|
setZoomMinus,
|
||||||
|
setZoomPlus,
|
||||||
|
setCenter,
|
||||||
|
setTemplate,
|
||||||
|
setTheme,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
{isEditable ? (
|
||||||
|
<>
|
||||||
|
<Tooltip content="缩小" position="right">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
disabled={+zoom <= MIN_ZOOM}
|
||||||
|
icon={<IconZoomOut />}
|
||||||
|
onClick={setZoomMinus}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Text style={{ width: 26, textAlign: 'center' }}>{zoom}</Text>
|
||||||
|
<Tooltip content="放大" position="right">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
disabled={+zoom >= MAX_ZOOM}
|
||||||
|
icon={<IconZoomIn />}
|
||||||
|
onClick={setZoomPlus}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="居中" position="right">
|
||||||
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconAlignCenter />} onClick={setCenter} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
position="right"
|
||||||
|
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
|
||||||
|
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
|
||||||
|
position="right"
|
||||||
|
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
|
||||||
|
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="缩小" position="right">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconMinus style={{ fontSize: 14 }} />}
|
||||||
|
onClick={setZoomMinus}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="放大" position="right">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconPlus style={{ fontSize: 14 }} />}
|
||||||
|
onClick={setZoomPlus}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="居中" position="right">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconAlignCenter style={{ fontSize: 14 }} />}
|
||||||
|
onClick={setCenter}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue