tiptap: fix mind toolbar

This commit is contained in:
fantasticit 2022-04-27 16:23:14 +08:00
parent a9eefd3c37
commit 2ad45a880e
12 changed files with 449 additions and 423 deletions

View File

@ -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>
); );

View File

@ -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>

View File

@ -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;
} }
} }

View File

@ -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>
); );
}; };

View File

@ -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;
}
} }
} }

View File

@ -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();

View File

@ -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();
},
});
});

View File

@ -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);
}, },

View File

@ -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 = '&nbsp;'; if (!element.innerHTML) element.innerHTML = '&nbsp;';
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);
}); });

View File

@ -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;

View File

@ -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>
);
};