tiptap: improve mind operations

This commit is contained in:
fantasticit 2022-05-01 13:06:16 +08:00
parent a7d3ff0005
commit c1a4e4b35e
18 changed files with 303 additions and 379 deletions

View File

@ -4,7 +4,7 @@ export const Divider = ({ vertical = false }) => {
style={{ style={{
display: 'inline-block', display: 'inline-block',
width: 1, width: 1,
height: 24, height: 18,
margin: '0 6px', margin: '0 6px',
backgroundColor: 'var(--semi-color-border)', backgroundColor: 'var(--semi-color-border)',
transform: `rotate(${vertical ? 90 : 0}deg)`, transform: `rotate(${vertical ? 90 : 0}deg)`,

View File

@ -2,7 +2,7 @@
.toolbar { .toolbar {
position: absolute; position: absolute;
display: flex; display: flex;
padding: 8px; padding: 4px 8px;
overflow-x: auto; overflow-x: auto;
color: #fff; color: #fff;
background-color: var(--semi-color-nav-bg); background-color: var(--semi-color-nav-bg);

View File

@ -85,11 +85,11 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
direction: window.MindElixir.SIDE, direction: window.MindElixir.SIDE,
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
editable: editor.isEditable, editable: editor.isEditable,
draggable: editor.isEditable,
contextMenu: editor.isEditable, contextMenu: editor.isEditable,
toolBar: true,
keypress: editor.isEditable, keypress: editor.isEditable,
nodeMenu: true, nodeMenu: editor.isEditable,
toolBar: true,
draggable: false, // 需要修复
locale: 'zh_CN', locale: 'zh_CN',
}); });
mind.shouldPreventDefault = () => editor.isActive('mind'); mind.shouldPreventDefault = () => editor.isActive('mind');
@ -103,7 +103,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
return () => { return () => {
if (mind) { if (mind) {
mind.bus.removeListener('operation', onChange); mind.destroy();
} }
}; };
}, [loading, editor, updateAttributes]); }, [loading, editor, updateAttributes]);

View File

@ -112,6 +112,11 @@ export const createLink = function (from, to, isInitPaint, obj) {
if (!isInitPaint) { if (!isInitPaint) {
this.showLinkController(p2x, p2y, p3x, p3y, newLinkObj, fromData, toData); this.showLinkController(p2x, p2y, p3x, p3y, newLinkObj, fromData, toData);
} }
this.bus.fire('operation', {
name: 'createLink',
linkObj: newLinkObj,
});
}; };
export const removeLink = function (linkSvg) { export const removeLink = function (linkSvg) {
@ -129,6 +134,10 @@ export const removeLink = function (linkSvg) {
delete this.linkData[id]; delete this.linkData[id];
link.remove(); link.remove();
link = null; link = null;
this.bus.fire('operation', {
name: 'removeLink',
});
}; };
export const selectLink = function (targetElement) { export const selectLink = function (targetElement) {
this.currentLink = targetElement; this.currentLink = targetElement;
@ -222,6 +231,11 @@ export const showLinkController = function (p2x, p2y, p3x, p3y, linkObj, fromDat
this.line1.setAttribute('y2', p2y); this.line1.setAttribute('y2', p2y);
linkObj.delta1.x = p2x - fromData.cx; linkObj.delta1.x = p2x - fromData.cx;
linkObj.delta1.y = p2y - fromData.cy; linkObj.delta1.y = p2y - fromData.cy;
this.bus.fire('operation', {
name: 'updateLink',
linkObj,
});
}); });
this.helper2.init(this.map, (deltaX, deltaY) => { this.helper2.init(this.map, (deltaX, deltaY) => {
@ -246,5 +260,10 @@ export const showLinkController = function (p2x, p2y, p3x, p3y, linkObj, fromDat
this.line2.setAttribute('y2', p4y); this.line2.setAttribute('y2', p4y);
linkObj.delta2.x = p3x - toData.cx; linkObj.delta2.x = p3x - toData.cx;
linkObj.delta2.y = p3y - toData.cy; linkObj.delta2.y = p3y - toData.cy;
this.bus.fire('operation', {
name: 'updateLink',
linkObj,
});
}); });
}; };

View File

@ -20,80 +20,4 @@ const cn = {
export default { export default {
cn, cn,
zh_CN: cn, zh_CN: cn,
zh_TW: {
addChild: '插入子節點',
addParent: '插入父節點',
addSibling: '插入同級節點',
removeNode: '刪除節點',
focus: '專注',
cancelFocus: '取消專注',
moveUp: '上移',
moveDown: '下移',
link: '連接',
clickTips: '請點擊目標節點',
font: '文字',
background: '背景',
tag: '標簽',
icon: '圖標',
tagsSeparate: '多個標簽半角逗號分隔',
iconsSeparate: '多個圖標半角逗號分隔',
},
en: {
addChild: 'Add child',
addParent: 'Add parent',
addSibling: 'Add sibling',
removeNode: 'Remove node',
focus: 'Focus Mode',
cancelFocus: 'Cancel Focus Mode',
moveUp: 'Move up',
moveDown: 'Move down',
link: 'Link',
clickTips: 'Please click the target node',
font: 'Font',
background: 'Background',
tag: 'Tag',
icon: 'Icon',
tagsSeparate: 'Separate tags by comma',
iconsSeparate: 'Separate icons by comma',
},
ja: {
addChild: '子ノードを追加する',
addParent: '親ノードを追加します',
addSibling: '兄弟ノードを追加する',
removeNode: 'ノードを削除',
focus: '集中',
cancelFocus: '集中解除',
moveUp: '上へ移動',
moveDown: '下へ移動',
link: 'コネクト',
clickTips: 'ターゲットノードをクリックしてください',
font: 'フォント',
background: 'バックグラウンド',
tag: 'タグ',
icon: 'アイコン',
tagsSeparate: '複数タグはカンマ区切り',
iconsSeparate: '複数アイコンはカンマ区切り',
},
pt: {
addChild: 'Adicionar item filho',
addParent: 'Adicionar item pai',
addSibling: 'Adicionar item irmao',
removeNode: 'Remover item',
focus: 'Modo Foco',
cancelFocus: 'Cancelar Modo Foco',
moveUp: 'Mover para cima',
moveDown: 'Mover para baixo',
link: 'Link',
clickTips: 'Favor clicar no item alvo',
font: 'Fonte',
background: 'Cor de fundo',
tag: 'Tag',
icon: 'Icone',
tagsSeparate: 'Separe tags por virgula',
iconsSeparate: 'Separe icones por virgula',
},
}; };

View File

@ -48,6 +48,7 @@ import {
updateNodeStyle, updateNodeStyle,
updateNodeTags, updateNodeTags,
updateNodeIcons, updateNodeIcons,
updateNodeHyperLink,
processPrimaryNode, processPrimaryNode,
setNodeTopic, setNodeTopic,
moveNodeBefore, moveNodeBefore,
@ -294,6 +295,7 @@ MindElixir.prototype = {
updateNodeStyle, updateNodeStyle,
updateNodeTags, updateNodeTags,
updateNodeIcons, updateNodeIcons,
updateNodeHyperLink,
processPrimaryNode, processPrimaryNode,
setNodeTopic, setNodeTopic,
@ -400,6 +402,11 @@ MindElixir.prototype = {
this.linkDiv(); this.linkDiv();
if (!this.overflowHidden) initMouseEvent(this); if (!this.overflowHidden) initMouseEvent(this);
}, },
destroy: function () {
this.bus.destroy();
this.mindElixirBox.removeChild(this.container);
},
}; };
MindElixir.LEFT = LEFT; MindElixir.LEFT = LEFT;

View File

@ -28,6 +28,7 @@ export const selectNode = function (targetElement, isNewNode, clickEvent) {
targetElement.classList.add('selected'); targetElement.classList.add('selected');
this.currentNode = targetElement; this.currentNode = targetElement;
if (isNewNode) { if (isNewNode) {
console.log('selectNewNode 1');
this.bus.fire('selectNewNode', targetElement.nodeObj, clickEvent); this.bus.fire('selectNewNode', targetElement.nodeObj, clickEvent);
} else { } else {
this.bus.fire('selectNode', targetElement.nodeObj, clickEvent); this.bus.fire('selectNode', targetElement.nodeObj, clickEvent);

View File

@ -1,6 +1,7 @@
import { dragMoveHelper } from './utils/index'; import { dragMoveHelper } from './utils/index';
export default function (mind) { export default function (mind) {
mind.map.addEventListener('click', (e) => { const onClick = (e) => {
// if (dragMoveHelper.afterMoving) return // if (dragMoveHelper.afterMoving) return
// e.preventDefault() // can cause a tag don't work // e.preventDefault() // can cause a tag don't work
if (e.target.nodeName === 'EPD') { if (e.target.nodeName === 'EPD') {
@ -17,36 +18,52 @@ export default function (mind) {
mind.unselectNode(); mind.unselectNode();
mind.hideLinkController(); mind.hideLinkController();
} }
}); };
mind.map.addEventListener('dblclick', (e) => { const onDbClick = (e) => {
e.preventDefault(); e.preventDefault();
if (!mind.editable) return; if (!mind.editable) return;
if (e.target.parentElement.nodeName === 'T' || e.target.parentElement.nodeName === 'ROOT') { if (e.target.parentElement.nodeName === 'T' || e.target.parentElement.nodeName === 'ROOT') {
mind.beginEdit(e.target); mind.beginEdit(e.target);
} }
}); };
/** const onMouseMove = (e) => {
* drag and move
*/
mind.map.addEventListener('mousemove', (e) => {
// click trigger mousemove in windows chrome // click trigger mousemove in windows chrome
// the 'true' is a string // the 'true' is a string
if (e.target.contentEditable !== 'true') { if (e.target.contentEditable !== 'true') {
dragMoveHelper.onMove(e, mind.container); dragMoveHelper.onMove(e, mind.container);
} }
}); };
mind.map.addEventListener('mousedown', (e) => {
const onMouseDown = (e) => {
if (e.target.contentEditable !== 'true') { if (e.target.contentEditable !== 'true') {
dragMoveHelper.afterMoving = false; dragMoveHelper.afterMoving = false;
dragMoveHelper.mousedown = true; dragMoveHelper.mousedown = true;
} }
}); };
mind.map.addEventListener('mouseleave', (e) => {
const onMouseLeave = (e) => {
dragMoveHelper.clear(); dragMoveHelper.clear();
}); };
mind.map.addEventListener('mouseup', (e) => {
const onMouseUp = (e) => {
dragMoveHelper.clear(); dragMoveHelper.clear();
};
mind.map.addEventListener('click', onClick);
mind.map.addEventListener('dblclick', onDbClick);
mind.map.addEventListener('mousemove', onMouseMove);
mind.map.addEventListener('mousedown', onMouseDown);
mind.map.addEventListener('mouseleave', onMouseLeave);
mind.map.addEventListener('mouseup', onMouseUp);
mind.bus.addListener('destroy', () => {
mind.map.removeEventListener('click', onClick);
mind.map.removeEventListener('dblclick', onDbClick);
mind.map.removeEventListener('mousemove', onMouseMove);
mind.map.removeEventListener('mousedown', onMouseDown);
mind.map.removeEventListener('mouseleave', onMouseLeave);
mind.map.removeEventListener('mouseup', onMouseUp);
}); });
} }

View File

@ -73,17 +73,24 @@ export const updateNodeIcons = function (object, icons) {
}; };
export const updateNodeHyperLink = function (object, hyperLink) { export const updateNodeHyperLink = function (object, hyperLink) {
if (!hyperLink) return;
const oldVal = object.hyperLink;
object.hyperLink = hyperLink;
const nodeEle = findEle(object.id); const nodeEle = findEle(object.id);
shapeTpc(nodeEle, object);
this.linkDiv(); if (!hyperLink) {
this.bus.fire('operation', { const linkEle = nodeEle.querySelector('a.hyper-link') as HTMLElement;
name: 'editHyperLink', if (linkEle) {
obj: object, linkEle.parentNode.removeChild(linkEle);
origin: oldVal, }
}); } else {
const oldVal = object.hyperLink;
object.hyperLink = hyperLink;
shapeTpc(nodeEle, object);
this.linkDiv();
this.bus.fire('operation', {
name: 'editHyperLink',
obj: object,
origin: oldVal,
});
}
}; };
export const updateNodeSvgChart = function () { export const updateNodeSvgChart = function () {
@ -404,6 +411,7 @@ export const removeNode = function (el) {
if (t.tagName === 'ROOT') { if (t.tagName === 'ROOT') {
return; return;
} }
this.bus.fire('removeNode', nodeObj);
if (childrenLength === 0) { if (childrenLength === 0) {
// remove epd when children length === 0 // remove epd when children length === 0
const parentT = t.parentNode.parentNode.previousSibling; const parentT = t.parentNode.parentNode.previousSibling;

View File

@ -82,6 +82,7 @@ export default function (mind) {
}; };
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (mind.isInnerEditing) return; // 脑图内部操作
if (!mind.editable) return; if (!mind.editable) return;
if (mind.shouldPreventDefault && mind.shouldPreventDefault()) { if (mind.shouldPreventDefault && mind.shouldPreventDefault()) {

View File

@ -11,18 +11,13 @@ export default function (mind, option?) {
const add_child = createLi('cm-add_child', 'zijiedian'); const add_child = createLi('cm-add_child', 'zijiedian');
const add_sibling = createLi('cm-add_sibling', 'tongjijiedian-'); const add_sibling = createLi('cm-add_sibling', 'tongjijiedian-');
const remove_child = createLi('cm-remove_child', 'shanchu2'); const remove_child = createLi('cm-remove_child', 'shanchu2');
// let focus = createLi('cm-fucus', i18n[locale].focus, '')
// let unfocus = createLi('cm-unfucus', i18n[locale].cancelFocus, '')
const up = createLi('cm-up', 'rising'); const up = createLi('cm-up', 'rising');
const down = createLi('cm-down', 'falling'); const down = createLi('cm-down', 'falling');
const edit = createLi('cm-edit', 'edit'); const edit = createLi('cm-edit', 'edit');
// let link = createLi('cm-down', i18n[locale].link, '')
const menuUl = document.createElement('ul'); const menuUl = document.createElement('ul');
menuUl.className = 'menu-list'; menuUl.className = 'menu-list';
// if (!option || option.link) {
// menuUl.appendChild(link)
// }
if (option && option.extend) { if (option && option.extend) {
for (let i = 0; i < option.extend.length; i++) { for (let i = 0; i < option.extend.length; i++) {
const item = option.extend[i]; const item = option.extend[i];
@ -37,10 +32,6 @@ export default function (mind, option?) {
menuContainer.appendChild(add_child); menuContainer.appendChild(add_child);
menuContainer.appendChild(add_sibling); menuContainer.appendChild(add_sibling);
menuContainer.appendChild(remove_child); menuContainer.appendChild(remove_child);
// if (!option || option.focus) {
// menuContainer.appendChild(focus)
// menuContainer.appendChild(unfocus)
// }
menuContainer.appendChild(up); menuContainer.appendChild(up);
menuContainer.appendChild(down); menuContainer.appendChild(down);
menuContainer.appendChild(edit); menuContainer.appendChild(edit);
@ -48,39 +39,6 @@ export default function (mind, option?) {
mind.container.append(menuContainer); mind.container.append(menuContainer);
let isRoot = true; let isRoot = true;
// mind.container.onclick = function (e) {
// e.preventDefault()
// // console.log(e.pageY, e.screenY, e.clientY)
// let target = e.target
// if (target.tagName === 'TPC') {
// if (target.parentElement.tagName === 'ROOT') {
// isRoot = true
// } else {
// isRoot = false
// }
// // if (isRoot) {
// // focus.className = 'disabled'
// // up.className = 'disabled'
// // down.className = 'disabled'
// // add_sibling.className = 'disabled'
// // remove_child.className = 'disabled'
// // } else {
// // focus.className = ''
// // up.className = ''
// // down.className = ''
// // add_sibling.className = ''
// // remove_child.className = ''
// // }
// mind.selectNode(target)
// menuContainer.hidden = false
// let height = menuUl.offsetHeight
// let width = menuUl.offsetWidth
// let rect = target.getBoundingClientRect()
// // menuUl.style.top = rect.top - 10 - height + 'px'
// // menuUl.style.left = rect.left - (width - rect.width) / 2 + 'px'
// // menuUl.style.left = e.clientX + 'px'
// }
// }
mind.bus.addListener('unselectNode', function () { mind.bus.addListener('unselectNode', function () {
menuContainer.hidden = true; menuContainer.hidden = true;
@ -108,15 +66,6 @@ export default function (mind, option?) {
if (isRoot) return; if (isRoot) return;
mind.removeNode(); mind.removeNode();
}; };
// focus.onclick = e => {
// if (isRoot) return
// mind.focusNode(mind.currentNode)
// menuContainer.hidden = true
// }
// unfocus.onclick = e => {
// mind.cancelFocus()
// menuContainer.hidden = true
// }
up.onclick = (e) => { up.onclick = (e) => {
if (isRoot) return; if (isRoot) return;
mind.moveUpNode(); mind.moveUpNode();
@ -128,24 +77,4 @@ export default function (mind, option?) {
edit.onclick = (e) => { edit.onclick = (e) => {
mind.beginEdit(); mind.beginEdit();
}; };
// link.onclick = e => {
// let from = mind.currentNode
// mind.map.addEventListener(
// 'click',
// e => {
// e.preventDefault()
// if (
// e.target.parentElement.nodeName === 'T' ||
// e.target.parentElement.nodeName === 'ROOT'
// ) {
// mind.createLink(from, mind.currentNode)
// } else {
// console.log('取消连接')
// }
// },
// {
// once: true,
// }
// )
// }
} }

View File

@ -1,8 +1,8 @@
import { dragMoveHelper, throttle } from '../utils/index'; import { dragMoveHelper, throttle } from '../utils/index';
import { findEle as E, Topic, Group } from '../utils/dom'; import { findEle as E, Topic, Group } from '../utils/dom';
// https://html.spec.whatwg.org/multipage/dnd.html#drag-and-drop-processing-model
const $d = document; const $d = document;
const insertPreview = function (el, insertLocation) { const insertPreview = function (el, insertLocation) {
if (!insertLocation) { if (!insertLocation) {
clearPreview(el); clearPreview(el);
@ -39,13 +39,13 @@ export default function (mind) {
let meet: Element; let meet: Element;
const threshold = 12; const threshold = 12;
mind.map.addEventListener('dragstart', function (e) { const onDragStart = function (e) {
dragged = e.target; dragged = e.target;
(dragged.parentNode.parentNode as Group).style.opacity = '0.5'; (dragged.parentNode.parentNode as Group).style.opacity = '0.5';
dragMoveHelper.clear(); dragMoveHelper.clear();
}); };
mind.map.addEventListener('dragend', async function (e: DragEvent) { const onDragEnd = async function (e: DragEvent) {
e.preventDefault(); e.preventDefault();
(e.target as HTMLElement).style.opacity = ''; (e.target as HTMLElement).style.opacity = '';
clearPreview(meet); clearPreview(meet);
@ -65,38 +65,43 @@ export default function (mind) {
} }
(dragged.parentNode.parentNode as Group).style.opacity = '1'; (dragged.parentNode.parentNode as Group).style.opacity = '1';
dragged = null; dragged = null;
}); };
mind.map.addEventListener( const onDragOver = throttle(function (e: DragEvent) {
'dragover', clearPreview(meet);
throttle(function (e: DragEvent) { const topMeet = $d.elementFromPoint(e.clientX, e.clientY - threshold);
// console.log('drag', e) if (canPreview(topMeet, dragged)) {
clearPreview(meet); meet = topMeet;
// minus threshold infer that postion of the cursor is above topic const y = topMeet.getBoundingClientRect().y;
const topMeet = $d.elementFromPoint(e.clientX, e.clientY - threshold); if (e.clientY > y + topMeet.clientHeight) {
if (canPreview(topMeet, dragged)) { insertLocation = 'after';
meet = topMeet; } else if (e.clientY > y + topMeet.clientHeight / 2) {
const y = topMeet.getBoundingClientRect().y; insertLocation = 'in';
if (e.clientY > y + topMeet.clientHeight) { }
insertLocation = 'after'; } else {
} else if (e.clientY > y + topMeet.clientHeight / 2) { const bottomMeet = $d.elementFromPoint(e.clientX, e.clientY + threshold);
if (canPreview(bottomMeet, dragged)) {
meet = bottomMeet;
const y = bottomMeet.getBoundingClientRect().y;
if (e.clientY < y) {
insertLocation = 'before';
} else if (e.clientY < y + bottomMeet.clientHeight / 2) {
insertLocation = 'in'; insertLocation = 'in';
} }
} else { } else {
const bottomMeet = $d.elementFromPoint(e.clientX, e.clientY + threshold); insertLocation = meet = null;
if (canPreview(bottomMeet, dragged)) {
meet = bottomMeet;
const y = bottomMeet.getBoundingClientRect().y;
if (e.clientY < y) {
insertLocation = 'before';
} else if (e.clientY < y + bottomMeet.clientHeight / 2) {
insertLocation = 'in';
}
} else {
insertLocation = meet = null;
}
} }
if (meet) insertPreview(meet, insertLocation); }
}, 200) if (meet) insertPreview(meet, insertLocation);
); }, 200);
mind.map.addEventListener('dragstart', onDragStart);
mind.map.addEventListener('dragend', onDragEnd);
mind.map.addEventListener('dragover', onDragOver);
mind.bus.addListener('destroy', () => {
mind.map.removeEventListener('dragstart', onDragStart);
mind.map.removeEventListener('dragend', onDragEnd);
mind.map.removeEventListener('dragover', onDragOver);
});
} }

View File

@ -1,11 +1,93 @@
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Button, Tooltip, Space } from '@douyinfe/semi-ui'; import { useCallback, useEffect, useRef, useState } from 'react';
import { IconBold, IconFont, IconMark } from '@douyinfe/semi-icons'; import { Button, Tooltip, Space, Dropdown, Form } from '@douyinfe/semi-ui';
import { IconBold, IconFont, IconMark, IconLink } from '@douyinfe/semi-icons';
import { ColorPicker } from 'tiptap/menus/_components/color-picker'; import { ColorPicker } from 'tiptap/menus/_components/color-picker';
import { findEle } from '../utils/dom'; import { findEle } from '../utils/dom';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
export const Link = ({ mind, link, setLink }) => {
const $form = useRef<FormApi>();
const $input = useRef<HTMLInputElement>();
const [initialState, setInitialState] = useState({ link });
const handleOk = useCallback(() => {
$form.current.validate().then((values) => {
setLink(values.link);
});
}, [setLink]);
useEffect(() => {
setInitialState({ link });
}, [link]);
return (
<Dropdown
stopPropagation
zIndex={10000}
trigger="click"
position={'bottomLeft'}
render={
<div style={{ padding: 12 }}>
<Form
initValues={initialState}
getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left"
onSubmit={handleOk}
layout="horizontal"
>
<Form.Input
autofocus
label="链接"
field="link"
placeholder="请输入外链地址"
onFocus={() => (mind.isInnerEditing = true)}
onBlur={() => (mind.isInnerEditing = false)}
/>
<Button type="primary" htmlType="submit">
</Button>
</Form>
</div>
}
>
<span style={{ display: 'inline-block' }}>
<Tooltip content="设置链接" zIndex={10000}>
<Button type="tertiary" theme="borderless" size="small" icon={<IconLink style={{ fontSize: '0.85em' }} />} />
</Tooltip>
</span>
</Dropdown>
);
};
const Toolbar = ({ mind, toggleBold, setFontColor, setBackgroundColor, setLink }) => {
const [textColor, setTextColor] = useState('');
const [bgColor, setBgColor] = useState('');
const [hyperLink, setHyperLink] = useState('');
useEffect(() => {
const listener = function (nodeObj, clickEvent) {
if (!clickEvent) return;
if (nodeObj.style) {
setTextColor(nodeObj.style.color);
setBgColor(nodeObj.style.background);
} else {
setTextColor('');
setBgColor('');
}
setHyperLink(nodeObj.hyperLink);
};
mind.bus.addListener('selectNode', listener);
return () => {
mind.bus.removeListener('selectNode', listener);
};
}, []);
const Toolbar = ({ toggleBold, setFontColor, setBackgroundColor }) => {
return ( return (
<Space spacing={4}> <Space spacing={4}>
<Tooltip content="加粗" zIndex={10000}> <Tooltip content="加粗" zIndex={10000}>
@ -24,7 +106,29 @@ const Toolbar = ({ toggleBold, setFontColor, setBackgroundColor }) => {
}} }}
> >
<Tooltip content="文本色" zIndex={10000}> <Tooltip content="文本色" zIndex={10000}>
<Button type="tertiary" theme="borderless" size="small" icon={<IconFont style={{ fontSize: '0.85em' }} />} /> <Button
type="tertiary"
theme="borderless"
size="small"
icon={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IconFont style={{ fontSize: '0.85em' }} />
<span
style={{
width: 12,
height: 2,
backgroundColor: textColor,
}}
></span>
</div>
}
/>
</Tooltip> </Tooltip>
</ColorPicker> </ColorPicker>
@ -37,6 +141,8 @@ const Toolbar = ({ toggleBold, setFontColor, setBackgroundColor }) => {
<Button type="tertiary" theme="borderless" size="small" icon={<IconMark />} /> <Button type="tertiary" theme="borderless" size="small" icon={<IconMark />} />
</Tooltip> </Tooltip>
</ColorPicker> </ColorPicker>
<Link mind={mind} link={hyperLink} setLink={setLink} />
</Space> </Space>
); );
}; };
@ -81,8 +187,19 @@ export default function (mind) {
mind.updateNodeIcons(mind.currentNode.nodeObj, newIcons); mind.updateNodeIcons(mind.currentNode.nodeObj, newIcons);
} }
function setLink(link: string) {
if (!mind.currentNode) return;
mind.updateNodeHyperLink(mind.currentNode.nodeObj, link);
}
ReactDOM.render( ReactDOM.render(
<Toolbar toggleBold={toggleBold} setFontColor={setFontColor} setBackgroundColor={setBackgroundColor} />, <Toolbar
mind={mind}
toggleBold={toggleBold}
setFontColor={setFontColor}
setBackgroundColor={setBackgroundColor}
setLink={setLink}
/>,
menuContainer menuContainer
); );
@ -94,20 +211,27 @@ export default function (mind) {
trigger: 'manual', trigger: 'manual',
placement: 'top', placement: 'top',
hideOnClick: 'toggle', hideOnClick: 'toggle',
offset: [-42, 40], offset: [-45, 45],
appendTo: mind.container, appendTo: mind.container,
}) as any; }) as any;
mind.bus.addListener('selectNode', function (nodeObj, clickEvent) { const onSelectNode = function (nodeObj) {
if (!clickEvent) return;
const element = findEle(nodeObj.id, mind) as HTMLElement; const element = findEle(nodeObj.id, mind) as HTMLElement;
toolbarInstance.setProps({ toolbarInstance.setProps({
getReferenceClientRect: () => element.getBoundingClientRect(), getReferenceClientRect: () => element.getBoundingClientRect(),
}); });
toolbarInstance.show(); toolbarInstance.show();
}); };
mind.bus.addListener('selectNode', onSelectNode);
mind.bus.addListener('selectNewNode', onSelectNode);
mind.bus.addListener('unselectNode', function () { mind.bus.addListener('unselectNode', function () {
toolbarInstance.hide(); toolbarInstance.hide();
}); });
mind.bus.addListener('removeNode', function (nodeObj) {
const isRemoveCurrentNode = mind.currentNode && mind.currentNode.nodeObj.id === nodeObj.id;
if (isRemoveCurrentNode) {
toolbarInstance.hide();
}
});
} }

View File

@ -9,46 +9,11 @@ import {
IconZoomOut, IconZoomOut,
IconZoomIn, IconZoomIn,
} from 'components/icons'; } from 'components/icons';
import { Divider } from 'tiptap/divider';
function Direction({ mind }) {
return (
<Space spacing={4}>
<Button
type="tertiary"
theme="borderless"
size="small"
onClick={() => {
mind.initLeft();
}}
icon={<IconMindLeft style={{ fontSize: '0.85em' }} />}
/>
<Button
type="tertiary"
theme="borderless"
size="small"
onClick={() => {
mind.initRight();
}}
icon={<IconMindRight style={{ fontSize: '0.85em' }} />}
/>
<Button
type="tertiary"
theme="borderless"
size="small"
onClick={() => {
mind.initSide();
}}
icon={<IconMindSide style={{ fontSize: '0.85em' }} />}
/>
</Space>
);
}
function Operation({ mind }) { function Operation({ mind }) {
return ( return (
<Space spacing={4}> <Space spacing={2}>
<Button <Button
type="tertiary" type="tertiary"
theme="borderless" theme="borderless"
@ -90,22 +55,45 @@ function Operation({ mind }) {
}} }}
icon={<IconZoomIn style={{ fontSize: '0.85em' }} />} icon={<IconZoomIn style={{ fontSize: '0.85em' }} />}
/> />
<Divider />
<Button
type="tertiary"
theme="borderless"
size="small"
onClick={() => {
mind.initLeft();
}}
icon={<IconMindLeft style={{ fontSize: '0.85em' }} />}
/>
<Button
type="tertiary"
theme="borderless"
size="small"
onClick={() => {
mind.initRight();
}}
icon={<IconMindRight style={{ fontSize: '0.85em' }} />}
/>
<Button
type="tertiary"
theme="borderless"
size="small"
onClick={() => {
mind.initSide();
}}
icon={<IconMindSide style={{ fontSize: '0.85em' }} />}
/>
</Space> </Space>
); );
} }
export default function (mind) { export default function (mind) {
{ const div = document.createElement('div');
const div = document.createElement('div'); div.className = 'toolbar rb';
div.className = 'toolbar lt'; ReactDOM.render(<Operation mind={mind} />, div);
ReactDOM.render(<Direction mind={mind} />, div); mind.container.append(div);
mind.container.append(div);
}
{
const div = document.createElement('div');
div.className = 'toolbar rb';
ReactDOM.render(<Operation mind={mind} />, div);
mind.container.append(div);
}
} }

View File

@ -1,8 +1,8 @@
import { LEFT, RIGHT, SIDE } from '../const'; import { LEFT, RIGHT, SIDE } from '../const';
import { NodeObj } from '../index'; import { NodeObj } from '../index';
import { encodeHTML } from '../utils/index'; import { encodeHTML } from '../utils/index';
export type Top = HTMLElement;
export type Top = HTMLElement;
export type Group = HTMLElement; export type Group = HTMLElement;
export interface Topic extends HTMLElement { export interface Topic extends HTMLElement {

View File

@ -1,103 +0,0 @@
// WIP
export function pieChart(data) {
const cx = 45 // circle center
const cy = 45
const r = 35 // radius
const lx = 105 // description position
const ly = 15
const svgns = 'http://www.w3.org/2000/svg'
const colors = [
'#004c6d',
'#346888',
'#5886a5',
'#7aa6c2',
'#9dc6e0',
'#c1e7ff',
]
// create <svg /> with specific width and height
const chart = document.createElementNS(svgns, 'svg:svg')
chart.setAttribute('width', 160)
chart.setAttribute('height', 90)
// chart.setAttribute("viewBox", "0 0 " + width + " " + height);
chart.setAttribute('style', 'display:block;')
let total = 0
for (let i = 0; i < data.length; i++) {
total += data[i].value
}
const angles = []
for (let i = 0; i < data.length; i++) {
angles[i] = (data[i].value / total) * Math.PI * 2
}
let starttangle = 0
for (let i = 0; i < data.length; i++) {
const endangle = starttangle + angles[i]
const x1 = cx + r * Math.sin(starttangle)
const y1 = cy - r * Math.cos(starttangle)
const x2 = cx + r * Math.sin(endangle)
const y2 = cy - r * Math.cos(endangle)
const path = document.createElementNS(svgns, 'path')
const d = `M ${cx},${cy} L ${x1},${y1} A ${r},${r} 0 0 1 ${x2},${y2} Z`
path.setAttribute('d', d)
path.setAttribute('fill', colors[i])
chart.appendChild(path)
starttangle = endangle
// description
const icon = document.createElementNS(svgns, 'rect')
icon.setAttribute('x', lx)
icon.setAttribute('y', ly + 15 * i)
icon.setAttribute('width', 10)
icon.setAttribute('height', 10)
icon.setAttribute('fill', colors[i])
chart.appendChild(icon)
const label = document.createElementNS(svgns, 'text')
label.setAttribute('x', lx + 50)
label.setAttribute('y', ly + 15 * i + 10)
label.setAttribute('font-family', 'sans-serif')
label.setAttribute('font-size', '14')
label.appendChild(document.createTextNode(data[i].label))
chart.appendChild(label)
}
return chart
}
// svg style
// background-color: #fff;
// padding: 5px;
// border-radius: 5px;
// border: 1px solid #ccc;
// example
if (nodeObj.pie) {
const pieContainer = $d.createElement('div')
pieContainer.className = 'pie'
const pieData = [
{
value: 12,
label: 'a',
},
{
value: 15,
label: 'b',
},
{
value: 42,
label: 'c',
},
{
value: 33,
label: 'd',
},
]
pieContainer.appendChild(pieChart(pieData))
tpc.appendChild(pieContainer)
}

View File

@ -30,4 +30,8 @@ Bus.prototype = {
} }
} }
}, },
destroy: function () {
this.fire('destroy');
this.handlers = {};
},
}; };

View File

@ -5,7 +5,7 @@ export const createMainPath = function (d: string) {
path.setAttribute('d', d); path.setAttribute('d', d);
path.setAttribute('stroke', '#4f83fd'); path.setAttribute('stroke', '#4f83fd');
path.setAttribute('fill', 'none'); path.setAttribute('fill', 'none');
path.setAttribute('stroke-width', '2'); path.setAttribute('stroke-width', '3');
return path; return path;
}; };
@ -21,7 +21,7 @@ export const createLine = function (x1: number, y1: number, x2: number, y2: numb
line.setAttribute('y1', y1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('x2', x2);
line.setAttribute('y2', y2); line.setAttribute('y2', y2);
line.setAttribute('stroke', '#f00'); line.setAttribute('stroke', '#6ec4c4');
line.setAttribute('fill', 'none'); line.setAttribute('fill', 'none');
line.setAttribute('stroke-width', '2'); line.setAttribute('stroke-width', '2');
return line; return line;
@ -33,7 +33,7 @@ export const createPath = function (d: string) {
path.setAttribute('stroke', '#6e80db'); path.setAttribute('stroke', '#6e80db');
path.setAttribute('fill', 'none'); path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'square'); path.setAttribute('stroke-linecap', 'square');
path.setAttribute('stroke-width', '1'); path.setAttribute('stroke-width', '2');
path.setAttribute('transform', 'translate(0.5,-0.5)'); path.setAttribute('transform', 'translate(0.5,-0.5)');
// adding translate(0.5,-0.5) can fix render error on windows, but i still dunno why // adding translate(0.5,-0.5) can fix render error on windows, but i still dunno why
return path; return path;
@ -47,12 +47,12 @@ export const createSvgGroup = function (d: string, arrowd: string): CustomSvg {
const path = $d.createElementNS(svgNS, 'path'); const path = $d.createElementNS(svgNS, 'path');
const arrow = $d.createElementNS(svgNS, 'path'); const arrow = $d.createElementNS(svgNS, 'path');
arrow.setAttribute('d', arrowd); arrow.setAttribute('d', arrowd);
arrow.setAttribute('stroke', 'rgb(235, 95, 82)'); arrow.setAttribute('stroke', '#6ec4c4');
arrow.setAttribute('fill', 'none'); arrow.setAttribute('fill', 'none');
arrow.setAttribute('stroke-linecap', 'cap'); arrow.setAttribute('stroke-linecap', 'cap');
arrow.setAttribute('stroke-width', '2'); arrow.setAttribute('stroke-width', '2');
path.setAttribute('d', d); path.setAttribute('d', d);
path.setAttribute('stroke', 'rgb(235, 95, 82)'); path.setAttribute('stroke', '#6ec4c4');
path.setAttribute('fill', 'none'); path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'cap'); path.setAttribute('stroke-linecap', 'cap');
path.setAttribute('stroke-width', '2'); path.setAttribute('stroke-width', '2');