Merge pull request #29 from fantasticit/feat/kity-minder

This commit is contained in:
fantasticit 2022-04-26 19:25:20 +08:00 committed by GitHub
commit 1859be133e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
152 changed files with 18403 additions and 3747 deletions

View File

@ -16,7 +16,7 @@ module.exports = {
'at-rule-name-case': 'lower', // 指定@规则名的大小写 'at-rule-name-case': 'lower', // 指定@规则名的大小写
'length-zero-no-unit': true, // 禁止零长度的单位(可自动修复) 'length-zero-no-unit': true, // 禁止零长度的单位(可自动修复)
'shorthand-property-no-redundant-values': true, // 简写属性 'shorthand-property-no-redundant-values': true, // 简写属性
'number-leading-zero': 'never', // 小数不带0 'number-leading-zero': 'always', // 小数不带0
'declaration-block-no-duplicate-properties': true, // 禁止声明快重复属性 'declaration-block-no-duplicate-properties': true, // 禁止声明快重复属性
'no-descending-specificity': true, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器。 'no-descending-specificity': true, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器。
'selector-max-id': 3, // 限制一个选择器中 ID 选择器的数量 'selector-max-id': 3, // 限制一个选择器中 ID 选择器的数量

3
packages/client/global.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
interface Window {
kityminder: any;
}

View File

@ -61,6 +61,7 @@
"dompurify": "^2.3.5", "dompurify": "^2.3.5",
"interactjs": "^1.10.11", "interactjs": "^1.10.11",
"katex": "^0.15.2", "katex": "^0.15.2",
"kity": "^2.0.4",
"lib0": "^0.2.47", "lib0": "^0.2.47",
"lowlight": "^2.5.0", "lowlight": "^2.5.0",
"markdown-it": "^12.3.2", "markdown-it": "^12.3.2",

View File

@ -0,0 +1,14 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconStructure: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<path d="M896 645.888V608C896 519.808 824.256 448 736 448H512V384h32c52.928 0 96-43.072 96-96v-128C640 107.072 596.928 64 544 64h-128C363.072 64 320 107.072 320 160v128C320 340.928 363.072 384 416 384H448v64H224A160.192 160.192 0 0 0 64 608v37.888c-37.184 13.248-64 48.448-64 90.112v128c0 52.928 43.072 96 96 96h64c52.928 0 96-43.072 96-96v-128c0-52.928-43.072-96-96-96H128v-32C128 555.072 171.072 512 224 512H448v128h-32c-52.928 0-96 43.072-96 96v128c0 52.928 43.072 96 96 96h128c52.928 0 96-43.072 96-96v-128c0-52.928-43.072-96-96-96H512V512h224c52.928 0 96 43.072 96 96v32h-32c-52.928 0-96 43.072-96 96v128c0 52.928 43.072 96 96 96h64c52.928 0 96-43.072 96-96v-128c0-41.664-26.816-76.864-64-90.112zM384 288v-128a32 32 0 0 1 32-32h128a32 32 0 0 1 32 32v128a32 32 0 0 1-32 32h-128a32 32 0 0 1-32-32z m-192 448v128a32 32 0 0 1-32 32h-64a32 32 0 0 1-32-32v-128a32 32 0 0 1 32-32h64a32 32 0 0 1 32 32z m384 0v128a32 32 0 0 1-32 32h-128a32 32 0 0 1-32-32v-128a32 32 0 0 1 32-32h128a32 32 0 0 1 32 32z m320 128c0 17.6-14.4 32-32 32h-64a32.064 32.064 0 0 1-32-32v-128c0-17.6 14.4-32 32-32h64c17.6 0 32 14.4 32 32v128z"></path>
</svg>
}
/>
);
};

View File

@ -51,3 +51,4 @@ export * from './IconGlobe';
export * from './IconCountdown'; export * from './IconCountdown';
export * from './IconDrawBoard'; export * from './IconDrawBoard';
export * from './IconCallout'; export * from './IconCallout';
export * from './IconStructure';

View File

@ -7,14 +7,15 @@ import styles from './style.module.scss';
interface IProps { interface IProps {
width: number; width: number;
height: number; height: number;
onChange: (arg: { width: number; height: number }) => void; onChange?: (arg: { width: number; height: number }) => void;
onChangeEnd?: (arg: { width: number; height: number }) => void;
className?: string; className?: string;
} }
const MIN_WIDTH = 50; const MIN_WIDTH = 50;
const MIN_HEIGHT = 50; const MIN_HEIGHT = 50;
export const Resizeable: React.FC<IProps> = ({ width, height, className, onChange, children }) => { export const Resizeable: React.FC<IProps> = ({ width, height, className, onChange, onChangeEnd, children }) => {
const $container = useRef<HTMLDivElement>(null); const $container = useRef<HTMLDivElement>(null);
const $topLeft = useRef<HTMLDivElement>(null); const $topLeft = useRef<HTMLDivElement>(null);
const $topRight = useRef<HTMLDivElement>(null); const $topRight = useRef<HTMLDivElement>(null);
@ -52,6 +53,10 @@ export const Resizeable: React.FC<IProps> = ({ width, height, className, onChang
Object.assign(event.target.dataset, { x, y }); Object.assign(event.target.dataset, { x, y });
onChange && onChange({ width, height }); onChange && onChange({ width, height });
}, },
end: function (event) {
let { width, height } = event.rect;
onChangeEnd && onChangeEnd({ width, height });
},
}, },
}); });
}, []); }, []);

View File

@ -36,7 +36,7 @@
border-radius: 0 0 var(--border-radius) var(--border-radius); border-radius: 0 0 var(--border-radius) var(--border-radius);
opacity: 0; opacity: 0;
justify-content: space-around; justify-content: space-around;
transition: all ease-in-out .2s; transition: all ease-in-out 0.2s;
button { button {
width: 40%; width: 40%;

View File

@ -103,5 +103,5 @@
} }
.docListTitle { .docListTitle {
margin: 12px .5rem; margin: 12px 0.5rem;
} }

View File

@ -1,5 +1,5 @@
.navItemWrap { .navItemWrap {
padding: 0 .5rem; padding: 0 0.5rem;
.navItem { .navItem {
display: flex; display: flex;

View File

@ -1,6 +1,8 @@
@import '~@douyinfe/semi-ui/dist/css/semi.min.css'; @import '~@douyinfe/semi-ui/dist/css/semi.min.css';
@import './var.scss'; @import './var.scss';
@import './reset.scss'; @import './reset.scss';
@import './kityminder.editor.css';
@import './hotbox.css';
.container { .container {
margin-right: auto; margin-right: auto;
@ -27,7 +29,7 @@
.Resizer { .Resizer {
z-index: 1; z-index: 1;
opacity: .2; opacity: 0.2;
box-sizing: border-box; box-sizing: border-box;
background-clip: padding; background-clip: padding;
} }

View File

@ -0,0 +1,191 @@
/* stylelint-disable */
.hotbox {
position: absolute;
top: 0;
left: 0;
overflow: visible;
font-family: Arial, 'Hiragino Sans GB', 'Microsoft YaHei', 'WenQuanYi Micro Hei', sans-serif;
}
.hotbox .state {
position: absolute;
display: none;
overflow: visible;
}
.hotbox .state .center .button,
.hotbox .state .ring .button {
position: absolute;
width: 70px;
height: 70px;
margin-top: -35px;
margin-left: -35px;
border-radius: 100%;
box-shadow: 0 0 30px rgb(0 0 0 / 30%);
}
.hotbox .state .center .label,
.hotbox .state .ring .label,
.hotbox .state .center .key,
.hotbox .state .ring .key {
display: block;
line-height: 1.4em;
text-align: center;
vertical-align: middle;
}
.hotbox .state .center .label,
.hotbox .state .ring .label {
margin-top: 17px;
font-size: 16px;
font-weight: normal;
line-height: 1em;
color: black;
}
.hotbox .state .center .key,
.hotbox .state .ring .key {
font-size: 12px;
color: #999;
}
.hotbox .state .ring-shape {
position: absolute;
top: -25px;
left: -25px;
border: 25px solid rgb(0 0 0 / 30%);
border-radius: 100%;
box-sizing: content-box;
}
.hotbox .state .top,
.hotbox .state .bottom {
position: absolute;
white-space: nowrap;
}
.hotbox .state .top .button,
.hotbox .state .bottom .button {
position: relative;
display: inline-block;
padding: 8px 15px;
margin: 0 10px;
border-radius: 15px;
box-shadow: 0 0 30px rgb(0 0 0 / 30%);
}
.hotbox .state .top .button .label,
.hotbox .state .bottom .button .label {
font-size: 14px;
line-height: 14px;
line-height: 1em;
color: black;
vertical-align: middle;
}
.hotbox .state .top .button .key,
.hotbox .state .bottom .button .key {
margin-left: 3px;
font-size: 12px;
line-height: 12px;
color: #999;
vertical-align: middle;
}
.hotbox .state .top .button .key::before,
.hotbox .state .bottom .button .key::before {
content: '(';
}
.hotbox .state .top .button .key::after,
.hotbox .state .bottom .button .key::after {
content: ')';
}
.hotbox .state .button {
overflow: hidden;
cursor: default;
background: #f9f9f9;
}
.hotbox .state .button .key,
.hotbox .state .button .label {
opacity: 0.3;
}
.hotbox .state .button.enabled {
background: white;
}
.hotbox .state .button.enabled .key,
.hotbox .state .button.enabled .label {
opacity: 1;
}
.hotbox .state .button.enabled:hover {
background: #e87372;
}
.hotbox .state .button.enabled:hover .label {
color: white;
}
.hotbox .state .button.enabled:hover .key {
color: #fadfdf;
}
.hotbox .state .button.enabled.selected {
animation: selected 0.1s ease;
background: #e45d5c;
}
.hotbox .state .button.enabled.selected .label {
color: white;
}
.hotbox .state .button.enabled.selected .key {
color: #fadfdf;
}
.hotbox .state .button.enabled.pressed,
.hotbox .state .button.enabled:active {
background: #ff974d;
}
.hotbox .state .button.enabled.pressed .label,
.hotbox .state .button.enabled:active .label {
color: white;
}
.hotbox .state .button.enabled.pressed .key,
.hotbox .state .button.enabled:active .key {
color: #fff0e6;
}
.hotbox .state.active {
display: block;
}
@keyframes selected {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.hotbox-key-receiver {
position: absolute;
top: -999999px;
left: -999999px;
width: 20px;
height: 20px;
margin: 0;
outline: none;
}

File diff suppressed because it is too large Load Diff

View File

@ -81,8 +81,8 @@ select {
*, *,
*::before, *::before,
*::after { *::after {
transition-duration: .01ms !important; transition-duration: 0.01ms !important;
animation-duration: .01ms !important; animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important; animation-iteration-count: 1 !important;
scroll-behavior: auto !important; scroll-behavior: auto !important;
} }
@ -95,8 +95,8 @@ select {
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: transparent; background-color: transparent;
border-radius: .5rem; border-radius: 0.5rem;
transition: all .2s; transition: all 0.2s;
} }
*:hover { *:hover {

View File

@ -4,13 +4,10 @@ import { MindWrapper } from '../wrappers/mind';
import { getDatasetAttribute } from '../utils/dataset'; import { getDatasetAttribute } from '../utils/dataset';
const DEFAULT_MIND_DATA = { const DEFAULT_MIND_DATA = {
meta: { root: { data: { text: '中心节点' }, children: [] },
name: 'jsMind', template: 'default',
author: 'think', theme: 'classic',
version: '0.2', version: '1.4.43',
},
format: 'node_tree',
data: { id: 'root', topic: '中心节点', children: [] },
}; };
declare module '@tiptap/core' { declare module '@tiptap/core' {
@ -23,11 +20,10 @@ declare module '@tiptap/core' {
export const Mind = Node.create({ export const Mind = Node.create({
name: 'mind', name: 'mind',
content: '',
marks: '',
group: 'block', group: 'block',
selectable: true, selectable: true,
atom: true, atom: true,
inline: false,
addAttributes() { addAttributes() {
return { return {
@ -43,6 +39,22 @@ export const Mind = Node.create({
default: DEFAULT_MIND_DATA, default: DEFAULT_MIND_DATA,
parseHTML: getDatasetAttribute('data', true), parseHTML: getDatasetAttribute('data', true),
}, },
template: {
default: 'default',
parseHTML: getDatasetAttribute('template'),
},
theme: {
default: 'classic',
parseHTML: getDatasetAttribute('theme'),
},
zoom: {
default: 100,
parseHTML: getDatasetAttribute('zoom'),
},
callCenterCount: {
default: 0,
parseHTML: (element) => Number(getDatasetAttribute('callcentercount')(element)),
},
}; };
}, },

View File

@ -38,6 +38,7 @@ import { DocumentReference } from './menus/document-reference';
import { Image } from './menus/image'; import { Image } from './menus/image';
import { Iframe } from './menus/iframe'; import { Iframe } from './menus/iframe';
import { Table } from './menus/table'; import { Table } from './menus/table';
import { Mind } from './menus/mind';
export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => { export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
if (!editor) { if (!editor) {
@ -94,6 +95,7 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
<Image editor={editor} /> <Image editor={editor} />
<Iframe editor={editor} /> <Iframe editor={editor} />
<Table editor={editor} /> <Table editor={editor} />
<Mind editor={editor} />
</Space> </Space>
</div> </div>
); );

View File

@ -38,7 +38,7 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
editor={editor} editor={editor}
pluginKey="countdonw-bubble-menu" pluginKey="document-reference-bubble-menu"
shouldShow={() => editor.isActive(DocumentReference.name)} shouldShow={() => editor.isActive(DocumentReference.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
> >

View File

@ -0,0 +1,42 @@
.sectionWrap {
margin-top: 16px;
> div {
display: flex;
width: 168px;
margin-top: 8px;
flex-wrap: wrap;
ul {
display: flex;
padding: 0;
margin: 0;
list-style: none;
flex-wrap: wrap;
li {
width: 80px;
height: 30px;
padding: 0 5px;
font-size: 12px;
line-height: 30px;
text-align: center;
text-decoration: none;
cursor: pointer;
border: 1px solid rgb(28 31 35 / 8%);
&.active {
border: 1px solid rgb(0 101 255);
}
&:nth-of-type(2n) {
margin-left: 8px;
}
&:nth-of-type(n + 3) {
margin-top: 8px;
}
}
}
}
}

View File

@ -0,0 +1,173 @@
import { useCallback } from 'react';
import cls from 'classnames';
import { Space, Button, Popover, Typography } from '@douyinfe/semi-ui';
import { IconAlignCenter, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { IconStructure, IconDrawBoard, IconZoomIn, IconZoomOut } from 'components/icons';
import { BubbleMenu } from '../../views/bubble-menu';
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 }) => {
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]);
return (
<BubbleMenu
className={'bubble-menu'}
editor={editor}
pluginKey="mind-bubble-menu"
shouldShow={() => editor.isActive(Mind.name)}
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
>
<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)}
>
{item.label}
</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>
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Space>
</BubbleMenu>
);
};

View File

@ -0,0 +1,113 @@
export const TEMPLATES = [
{
label: '经典',
value: 'default',
},
{
label: '文件夹',
value: 'filetree',
},
{
label: '鱼骨图',
value: 'fish-bone',
},
{
label: '靠右',
value: 'right',
},
{
label: '组织',
value: 'structure',
},
{
label: '天盘',
value: 'tianpan',
},
];
export const THEMES = [
{
label: '经典',
value: 'classic',
style: {
color: 'rgb(68, 51, 0)',
background: ' rgb(233, 223, 152)',
},
},
{
label: '紧凑',
value: 'classic-compact',
style: {
color: 'rgb(68, 51, 0)',
background: ' rgb(233, 223, 152)',
},
},
{
label: '清新红',
value: 'fresh-red',
style: {
color: 'white',
background: ' rgb(191, 115, 115)',
},
},
{
label: '泥土黄',
value: 'fresh-soil',
style: {
color: 'white',
background: 'rgb(191, 147, 115)',
},
},
{
label: '文艺绿',
value: 'fresh-green',
style: {
color: 'white',
background: 'rgb(115, 191, 118)',
},
},
{
label: '天空蓝',
value: 'fresh-blue',
style: {
color: 'white',
background: 'rgb(115, 161, 191)',
},
},
{
label: '浪漫紫',
value: 'fresh-purple',
style: {
color: 'white',
background: 'rgb(123, 115, 191)',
},
},
{
label: '胭脂粉',
value: 'fresh-pink',
style: {
color: 'white',
background: 'rgb(191, 115, 148)',
},
},
{
label: '冷光',
value: 'snow',
style: {
color: '#fff',
background: 'rgb(164, 197, 192)',
},
},
{
label: '鱼骨图',
value: 'fish',
style: {
color: '#fff',
background: 'rgb(58, 65, 68)',
},
},
];
export const MIN_ZOOM = 10;
export const MAX_ZOOM = 200;
export const ZOOM_STEP = 15;

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Editor } from '@tiptap/core';
import { MindBubbleMenu } from './bubble';
export const Mind: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) {
return null;
}
return (
<>
<MindBubbleMenu editor={editor} />
</>
);
};

View File

@ -12,7 +12,7 @@
Arial, Arial,
sans-serif; sans-serif;
line-height: 1.74; line-height: 1.74;
letter-spacing: .008em; letter-spacing: 0.008em;
color: var(--semi-color-text-0); color: var(--semi-color-text-0);
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
@ -34,19 +34,19 @@
} }
p { p {
margin-top: .75rem; margin-top: 0.75rem;
margin-bottom: 0; margin-bottom: 0;
font-size: 1em; font-size: 1em;
font-weight: normal; font-weight: normal;
line-height: 1.714; line-height: 1.714;
letter-spacing: -.005em; letter-spacing: -0.005em;
} }
blockquote { blockquote {
box-sizing: border-box; box-sizing: border-box;
padding-left: 10px; padding-left: 10px;
border-left: 2px solid var(--semi-color-fill-2); border-left: 2px solid var(--semi-color-fill-2);
margin: .75rem 0; margin: 0.75rem 0;
} }
hr { hr {
@ -84,7 +84,7 @@
body[theme-mode='dark'] { body[theme-mode='dark'] {
.ProseMirror { .ProseMirror {
img { img {
opacity: .75; opacity: 0.75;
} }
} }
} }

View File

@ -2,7 +2,7 @@
code { code {
padding: 4px; padding: 4px;
font-family: Consolas, Menlo, Courier, monospace; font-family: Consolas, Menlo, Courier, monospace;
font-size: .875rem; font-size: 0.875rem;
line-height: 1.3; line-height: 1.3;
cursor: text; cursor: text;
background-color: var(--semi-color-fill-1); background-color: var(--semi-color-fill-1);
@ -25,7 +25,7 @@
padding: 0; padding: 0;
margin: 8px; margin: 8px;
overflow: auto; overflow: auto;
font-size: .875rem; font-size: 0.875rem;
line-height: 1.5rem; line-height: 1.5rem;
color: inherit; color: inherit;
white-space: pre; white-space: pre;

View File

@ -12,7 +12,7 @@
position: absolute; position: absolute;
top: -1.4em; top: -1.4em;
left: -1px; left: -1px;
padding: .1rem .3rem; padding: 0.1rem 0.3rem;
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;

View File

@ -5,8 +5,8 @@
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1em; height: 1em;
margin-right: .1em; margin-right: 0.1em;
margin-bottom: .15em; margin-bottom: 0.15em;
vertical-align: middle; vertical-align: middle;
background-color: var(--color); background-color: var(--color);
border: 1px solid rgb(128 128 128 / 30%); border: 1px solid rgb(128 128 128 / 30%);

View File

@ -8,10 +8,10 @@
@import './list.scss'; @import './list.scss';
@import './mention.scss'; @import './mention.scss';
@import './menu.scss'; @import './menu.scss';
@import './mind.scss';
@import './node.scss'; @import './node.scss';
@import './placeholder.scss'; @import './placeholder.scss';
@import './search.scss'; @import './search.scss';
@import './selection.scss'; @import './selection.scss';
@import './table.scss'; @import './table.scss';
@import './title.scss'; @import './title.scss';
@import './kityminder.scss';

File diff suppressed because it is too large Load Diff

View File

@ -1,334 +0,0 @@
/* stylelint-disable */
/* important section */
.jsmind-inner {
position: relative;
width: 100%;
height: 100%;
overflow: auto;
} /* box-shadow:0 0 2px #000; */
.jsmind-inner {
user-select: none;
user-select: none;
user-select: none;
user-select: none;
user-select: none;
user-select: none;
}
/* z-index:1 */
canvas {
position: absolute;
z-index: 1;
}
/* z-index:2 */
jmnodes {
position: absolute;
z-index: 2;
background-color: rgb(0 0 0 / 0%);
} /* background color is necessary */
jmnode {
position: absolute;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
jmexpander {
position: absolute;
display: block;
width: 11px;
height: 11px;
overflow: hidden;
font-size: 12px;
line-height: 12px;
text-align: center;
cursor: pointer;
border-radius: 6px;
border-width: 1px;
border-style: solid;
}
/* default theme */
jmnode {
padding: 10px;
font: 16px/1.125 Verdana, Arial, Helvetica, sans-serif;
color: #333;
background-color: #fff;
border-radius: 5px;
box-shadow: 1px 1px 1px #666;
}
jmnode:hover {
color: #333;
background-color: #ebebeb;
box-shadow: 2px 2px 8px #000;
}
jmnode.selected {
color: #fff;
background-color: #11f;
box-shadow: 2px 2px 8px #000;
}
jmnode.root {
font-size: 24px;
}
jmexpander {
border-color: gray;
}
jmexpander:hover {
border-color: #000;
}
@media screen and (max-device-width: 1024px) {
jmnode {
padding: 5px;
font-size: 14px;
border-radius: 3px;
}
jmnode.root {
font-size: 21px;
}
}
/* primary theme */
jmnodes.theme-primary jmnode {
border-color: #357ebd;
color: #fff;
background-color: #428bca;
}
jmnodes.theme-primary jmnode:hover {
border-color: #285e8e;
background-color: #3276b1;
}
jmnodes.theme-primary jmnode.selected {
color: #fff;
background-color: #f1c40f;
}
/* warning theme */
jmnodes.theme-warning jmnode {
border-color: #eea236;
color: #fff;
background-color: #f0ad4e;
}
jmnodes.theme-warning jmnode:hover {
border-color: #d58512;
background-color: #ed9c28;
}
jmnodes.theme-warning jmnode.selected {
color: #fff;
background-color: #11f;
}
/* danger theme */
jmnodes.theme-danger jmnode {
border-color: #d43f3a;
color: #fff;
background-color: #d9534f;
}
jmnodes.theme-danger jmnode:hover {
border-color: #ac2925;
background-color: #d2322d;
}
jmnodes.theme-danger jmnode.selected {
color: #fff;
background-color: #11f;
}
/* success theme */
jmnodes.theme-success jmnode {
border-color: #4cae4c;
color: #fff;
background-color: #5cb85c;
}
jmnodes.theme-success jmnode:hover {
border-color: #398439;
background-color: #47a447;
}
jmnodes.theme-success jmnode.selected {
color: #fff;
background-color: #11f;
}
/* info theme */
jmnodes.theme-info jmnode {
border-color: #46b8da;
color: #fff;
background-color: #5dc0de;
}
jmnodes.theme-info jmnode:hover {
border-color: #269abc;
background-color: #39b3d7;
}
jmnodes.theme-info jmnode.selected {
color: #fff;
background-color: #11f;
}
/* greensea theme */
jmnodes.theme-greensea jmnode {
color: #fff;
background-color: #1abc9c;
}
jmnodes.theme-greensea jmnode:hover {
background-color: #16a085;
}
jmnodes.theme-greensea jmnode.selected {
color: #fff;
background-color: #11f;
}
/* nephrite theme */
jmnodes.theme-nephrite jmnode {
color: #fff;
background-color: #2ecc71;
}
jmnodes.theme-nephrite jmnode:hover {
background-color: #27ae60;
}
jmnodes.theme-nephrite jmnode.selected {
color: #fff;
background-color: #11f;
}
/* belizehole theme */
jmnodes.theme-belizehole jmnode {
color: #fff;
background-color: #3498db;
}
jmnodes.theme-belizehole jmnode:hover {
background-color: #2980b9;
}
jmnodes.theme-belizehole jmnode.selected {
color: #fff;
background-color: #11f;
}
/* wisteria theme */
jmnodes.theme-wisteria jmnode {
color: #fff;
background-color: #9b59b6;
}
jmnodes.theme-wisteria jmnode:hover {
background-color: #8e44ad;
}
jmnodes.theme-wisteria jmnode.selected {
color: #fff;
background-color: #11f;
}
/* asphalt theme */
jmnodes.theme-asphalt jmnode {
color: #fff;
background-color: #34495e;
}
jmnodes.theme-asphalt jmnode:hover {
background-color: #2c3e50;
}
jmnodes.theme-asphalt jmnode.selected {
color: #fff;
background-color: #11f;
}
/* orange theme */
jmnodes.theme-orange jmnode {
color: #fff;
background-color: #f1c40f;
}
jmnodes.theme-orange jmnode:hover {
background-color: #f39c12;
}
jmnodes.theme-orange jmnode.selected {
color: #fff;
background-color: #11f;
}
/* pumpkin theme */
jmnodes.theme-pumpkin jmnode {
color: #fff;
background-color: #e67e22;
}
jmnodes.theme-pumpkin jmnode:hover {
background-color: #d35400;
}
jmnodes.theme-pumpkin jmnode.selected {
color: #fff;
background-color: #11f;
}
/* pomegranate theme */
jmnodes.theme-pomegranate jmnode {
color: #fff;
background-color: #e74c3c;
}
jmnodes.theme-pomegranate jmnode:hover {
background-color: #c0392b;
}
jmnodes.theme-pomegranate jmnode.selected {
color: #fff;
background-color: #11f;
}
/* clouds theme */
jmnodes.theme-clouds jmnode {
color: #333;
background-color: #ecf0f1;
}
jmnodes.theme-clouds jmnode:hover {
background-color: #bdc3c7;
}
jmnodes.theme-clouds jmnode.selected {
color: #fff;
background-color: #11f;
}
/* asbestos theme */
jmnodes.theme-asbestos jmnode {
color: #fff;
background-color: #95a5a6;
}
jmnodes.theme-asbestos jmnode:hover {
background-color: #7f8c8d;
}
jmnodes.theme-asbestos jmnode.selected {
color: #fff;
background-color: #11f;
}

View File

@ -6,7 +6,7 @@
table { table {
width: 100%; width: 100%;
margin: .75em 0 0; margin: 0.75em 0 0;
overflow: hidden; overflow: hidden;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed; table-layout: fixed;

View File

@ -1,5 +1,5 @@
.wrap { .wrap {
margin-top: .75em; margin-top: 0.75em;
line-height: 0; line-height: 0;
.innerWrap { .innerWrap {
@ -29,7 +29,7 @@
} }
p { p {
margin-top: .25em; margin-top: 0.25em;
} }
p:first-child { p:first-child {

View File

@ -1,6 +1,6 @@
.wrap { .wrap {
position: relative; position: relative;
margin-top: .75em; margin-top: 0.75em;
.handleWrap { .handleWrap {
display: flex; display: flex;

View File

@ -1,6 +1,6 @@
.wrap { .wrap {
padding: 12px; padding: 12px;
margin-top: .75em; margin-top: 0.75em;
border: 1px solid var(--node-border-color); border: 1px solid var(--node-border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);

View File

@ -1,5 +1,5 @@
.wrap { .wrap {
margin-top: .75em; margin-top: 0.75em;
border-radius: var(--border-radius); border-radius: var(--border-radius);
.itemWrap { .itemWrap {

View File

@ -17,6 +17,7 @@
.innerWrap { .innerWrap {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%;
overflow: hidden; overflow: hidden;
border-radius: var(--border-radius); border-radius: var(--border-radius);
flex: 1; flex: 1;

View File

@ -29,7 +29,7 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
)} )}
</NodeViewContent> </NodeViewContent>
), ),
[url] [url, width, height]
); );
if (!isEditable && !url) { if (!isEditable && !url) {
@ -39,8 +39,8 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
return ( return (
<NodeViewWrapper> <NodeViewWrapper>
{isEditable ? ( {isEditable ? (
<Resizeable height={height} width={width} onChange={onResize}> <Resizeable height={height} width={width} onChangeEnd={onResize}>
{content} <div style={{ width, height, maxWidth: '100%' }}>{content}</div>
</Resizeable> </Resizeable>
) : ( ) : (
<div style={{ width, height, maxWidth: '100%' }}>{content}</div> <div style={{ width, height, maxWidth: '100%' }}>{content}</div>

View File

@ -80,7 +80,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
if (isEditable) { if (isEditable) {
return ( return (
<Resizeable className={cls('render-wrapper')} width={width} height={height} onChange={onResize}> <Resizeable className={cls('render-wrapper')} width={width} height={height} onChangeEnd={onResize}>
{img} {img}
</Resizeable> </Resizeable>
); );
@ -91,7 +91,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
{img} {img}
</div> </div>
); );
}, [error, src, isEditable]); }, [error, src, isEditable, width, height]);
return ( return (
<NodeViewWrapper as="div" style={{ textAlign, fontSize: 0, maxWidth: '100%' }}> <NodeViewWrapper as="div" style={{ textAlign, fontSize: 0, maxWidth: '100%' }}>

View File

@ -3,115 +3,32 @@
max-width: 100%; max-width: 100%;
overflow: visible; overflow: visible;
line-height: 0; line-height: 0;
outline: none;
:global {
/* stylelint-disable-next-line selector-type-no-unknown */
jmnodes {
/* stylelint-disable-next-line selector-type-no-unknown */
jmnode {
color: var(--semi-color-text-1);
background-color: var(--semi-color-fill-2);
outline: 2px solid transparent;
box-shadow: none;
&:first-of-type {
color: #fff;
background-color: var(--semi-color-link);
}
&.selected {
outline: 2px solid var(--semi-color-link);
}
input {
width: 100px !important;
height: 100%;
background-color: #fff;
border: 0;
outline: none;
}
}
/* stylelint-disable-next-line selector-type-no-unknown */
jmexpander {
display: flex;
justify-content: center;
align-items: center;
width: 12px;
height: 12px;
border-color: var(--node-border-color);
}
}
}
.jsmindWrap {
position: absolute;
top: 10px;
left: 50%;
z-index: 200;
max-width: 100%;
padding-bottom: 10px;
opacity: 0;
transform: translate(-50%, 0);
.jsmindInnerWrap {
padding: 6px;
background-color: var(--semi-color-bg-2);
border: 1px solid var(--node-border-color);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
}
}
.renderWrap { .renderWrap {
position: relative;
min-height: 50px;
overflow: hidden;
border: 1px solid var(--node-border-color); border: 1px solid var(--node-border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
outline: none;
> input { &::after {
position: absolute; background-color: transparent !important;
top: 0;
left: 0;
z-index: 10000;
width: 100%;
height: 100%;
background-color: transparent;
} }
}
.mindHandlerWrap { .mindHandlerWrap {
position: absolute; position: absolute;
right: 20px; right: 20px;
bottom: 20px; bottom: 20px;
z-index: 1000; z-index: 1000;
padding: 4px 8px; padding: 4px 8px;
background-color: var(--semi-color-bg-2); background-color: var(--semi-color-bg-2);
border: 1px solid var(--node-border-color); border: 1px solid var(--node-border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
opacity: 0; opacity: 0;
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
}
} }
&.isActive { &.isActive {
.jsmindWrap { .mindHandlerWrap {
opacity: 1; opacity: 1;
} }
.renderWrap {
.mindHandlerWrap {
opacity: 1;
}
}
}
.handlerWrap {
display: flex;
justify-content: center;
padding: 10px;
border-top: 1px solid var(--node-border-color);
} }
} }

View File

@ -1,141 +1,221 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames'; import cls from 'classnames';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button } from '@douyinfe/semi-ui'; import { Spin, Button, Typography } from '@douyinfe/semi-ui';
import { IconMinus, IconPlus } from '@douyinfe/semi-icons'; import { IconMinus, IconPlus, IconAlignCenter } from '@douyinfe/semi-icons';
import { Resizeable } from 'components/resizeable';
import deepEqual from 'deep-equal'; import deepEqual from 'deep-equal';
import { Resizeable } from 'components/resizeable';
import { Tooltip } from 'components/tooltip';
import { useToggle } from 'hooks/use-toggle';
import { MIN_ZOOM, MAX_ZOOM, ZOOM_STEP } from '../../menus/mind/constant';
import { clamp } from '../../utils/clamp';
import { Mind } from '../../extensions/mind'; import { Mind } from '../../extensions/mind';
// @ts-ignore import { loadKityMinder } from './kityminder';
import jsMind from './jsmind.jsx';
import styles from './index.module.scss'; import styles from './index.module.scss';
const { Text } = Typography;
export const MindWrapper = ({ editor, node, updateAttributes }) => { export const MindWrapper = ({ editor, node, updateAttributes }) => {
const $container = useRef(); const $container = useRef();
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, width, height = 100 } = node.attrs; const { data, template, theme, zoom, callCenterCount, width, height } = node.attrs;
const [loading, toggleLoading] = useToggle(true);
const [error, setError] = useState<Error | null>(null);
const zoomIn = useCallback(() => { const content = useMemo(() => {
const jm = $mind.current; if (error) {
if (!jm) return; return (
<div style={{ width: '100%', height: '100%' }}>
<Text>{error.message || error}</Text>
</div>
);
}
jm.view.zoomIn(); if (loading) {
return <Spin spinning={loading} style={{ width: '100%', height: '100%' }}></Spin>;
}
return (
<div
ref={$container}
className={cls(styles.renderWrap, 'render-wrapper')}
tabIndex={0}
style={{ width: '100%', height: '100%' }}
></div>
);
}, [loading, error]);
const onResize = useCallback(
(size) => {
updateAttributes({ width: size.width, height: size.height });
setCenter();
},
[updateAttributes]
);
const setCenter = useCallback(() => {
const minder = $mind.current;
if (!minder) return;
minder.execCommand('camera');
}, []); }, []);
const zoomOut = useCallback(() => { const setZoom = useCallback(
const jm = $mind.current; (type: 'minus' | 'plus') => {
if (!jm) return; return () => {
const minder = $mind.current;
if (!minder) return;
const currentZoom = minder.getZoomValue();
const nextZoom = clamp(
type === 'minus' ? currentZoom - ZOOM_STEP : currentZoom + ZOOM_STEP,
MIN_ZOOM,
MAX_ZOOM
);
minder.execCommand('zoom', nextZoom);
};
},
[editor, zoom]
);
jm.view.zoomOut(); const saveData = useCallback(() => {
}, []); const minder = $mind.current;
if (!minder) return;
const syncData = useCallback(() => { updateAttributes({ data: minder.exportJson() });
const jm = $mind.current; }, [updateAttributes]);
if (!jm) return;
const data = jm.get_data();
try {
updateAttributes({ data });
} catch (e) {}
}, []);
// 加载依赖
useEffect(() => { useEffect(() => {
if (!data) return; loadKityMinder()
if (!data.meta) return; .then(() => {
toggleLoading(false);
})
.catch((e) => {
setError(e);
});
}, []);
const onChange = (_, data) => { // 初始化渲染
if (data.node) { useEffect(() => {
syncData(); if (loading || !$container.current) return;
}
const onChange = () => {
saveData();
}; };
setTimeout(() => { try {
const Editor = window.kityminder.Editor;
const minder = new Editor($container.current).minder;
minder.importJson(data);
minder.execCommand('template', template);
minder.execCommand('theme', theme);
minder.execCommand('zoom', parseInt(zoom));
$mind.current = minder;
minder.on('contentChange', onChange);
toggleLoading(false);
} catch (e) {
setError(e);
}
return () => {
if ($mind.current) { if ($mind.current) {
const jm = $mind.current; $mind.current.off('contentChange', onChange);
const currentData = jm.get_data();
const isEqual = deepEqual(currentData, data);
if (!isEqual) {
jm.show(data);
}
return;
} }
};
}, [loading]);
const options = { // 数据同步渲染
container: $container.current,
editable: isEditable,
view: {
hmargin: 100,
vmargin: 50,
line_width: window.devicePixelRatio,
line_color: '#e5e9ef',
},
};
const jm = new jsMind(options);
jm.show(data);
$mind.current = jm;
jm.add_event_listener(onChange);
}, 0);
}, [data, isEditable]);
const onResize = (size) => {
const jm = $mind.current;
if (!jm) return;
updateAttributes({ width: size.width, height: size.height });
setTimeout(() => {
jm.view.show(true);
jm.view.showAddHandlerDOMNode();
}, 100);
};
useEffect(() => { useEffect(() => {
const jm = $mind.current; const minder = $mind.current;
if (!jm) return; if (!minder) return;
const currentData = minder.exportJson();
const isEqual = deepEqual(currentData, data);
if (isEqual) return;
// TODO: 也许刷新更好些
minder.importJson(data);
}, [data]);
// 布局
useEffect(() => {
const minder = $mind.current;
if (!minder) return;
minder.execCommand('template', template);
}, [template]);
// 主题
useEffect(() => {
const minder = $mind.current;
if (!minder) return;
minder.execCommand('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) { if (isEditable) {
jm.enable_edit(); minder.enable();
} else { } else {
jm.disable_edit(); minder.disable();
} }
}, [isEditable]); }, [isEditable]);
const content = ( // 居中
<div useEffect(() => {
ref={$container} setCenter();
className={cls(styles.renderWrap, 'render-wrapper')} }, [callCenterCount]);
tabIndex={0}
style={{ width: '100%', height: '100%' }}
>
{!isEditable && (
<div className={styles.mindHandlerWrap}>
<Button
size="small"
theme="borderless"
type="tertiary"
icon={<IconMinus style={{ fontSize: 14 }} />}
onClick={zoomOut}
/>
<Button
size="small"
theme="borderless"
type="tertiary"
icon={<IconPlus style={{ fontSize: 14 }} />}
onClick={zoomIn}
/>
</div>
)}
</div>
);
return ( return (
<NodeViewWrapper className={cls(styles.wrap, isMindActive && styles.isActive)}> <NodeViewWrapper className={cls(styles.wrap, isMindActive && styles.isActive)}>
{isEditable ? ( {isEditable ? (
<Resizeable width={width} height={height} onChange={onResize}> <Resizeable width={width} height={height} onChangeEnd={onResize}>
{content} {content}
</Resizeable> </Resizeable>
) : ( ) : (
<div style={{ display: 'inline-block', width, height, maxWidth: '100%' }}>{content}</div> <div style={{ display: 'inline-block', width, height, maxWidth: '100%' }}>{content}</div>
)} )}
{!isEditable && (
<div className={styles.mindHandlerWrap}>
<Tooltip content="缩小">
<Button
size="small"
theme="borderless"
type="tertiary"
icon={<IconMinus style={{ fontSize: 14 }} />}
onClick={setZoom('minus')}
/>
</Tooltip>
<Tooltip content="放大">
<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>
); );
}; };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
define('expose', function(require, exports, module) {
module.exports = window.HotBox = require('./hotbox');
});

View File

@ -0,0 +1,636 @@
define(function(require, exports, module) {
var key = require('./key');
var KeyControl = require('./keycontrol');
/**** Dom Utils ****/
function createElement(name) {
return document.createElement(name);
}
function setElementAttribute(element, name, value) {
element.setAttribute(name, value);
}
function getElementAttribute(element, name) {
return element.getAttribute(name);
}
function addElementClass(element, name) {
element.classList.add(name);
}
function removeElementClass(element, name) {
element.classList.remove(name);
}
function appendChild(parent, child) {
parent.appendChild(child);
}
/*******************/
var IDLE = HotBox.STATE_IDLE = 'idle';
var div = 'div';
/**
* Simple Formatter
*/
function format(template, args) {
if (typeof(args) != 'object') {
args = [].slice.apply(arguments, 1);
}
return String(template).replace(/\{(\w+)\}/g, function(match, name) {
return args[name] || match;
});
}
/**
* Hot Box Class
*/
function HotBox($container) {
if (typeof($container) == 'string') {
$container = document.querySelector($container);
}
if (!$container || !($container instanceof HTMLElement)) {
throw new Error('No container or not invalid container for hot box');
}
// 创建 HotBox Dom 解构
var $hotBox = createElement(div);
addElementClass($hotBox, 'hotbox');
appendChild($container, $hotBox);
// 保存 Dom 解构和父容器
this.$element = $hotBox;
this.$container = $container;
// 标示是否是输入法状态
this.isIME = false;
/**
* @Desc: 增加一个browser用于判断浏览器类型方便解决兼容性问题
* @Editor: Naixor
* @Date: 2015.09.14
*/
this.browser = {
sg: /se[\s\S]+metasr/.test(navigator.userAgent.toLowerCase())
};
/*
* added by zhangbobell
* 2015.09.22
* 增加父状态机以解决在父 FSM 下状态控制的问题最好的解决办法是增加一个函数队列
* 将其中的函数一起执行//TODO
* */
this._parentFSM = {};
// 记录位置
this.position = {};
// 已定义的状态string => HotBoxState
var _states = {};
// 主状态HotBoxState
var _mainState = null;
// 当前状态HotBoxState
var _currentState = IDLE;
// 当前状态堆栈
var _stateStack = [];
// 实例引用
var _this = this;
var _controler;
/**
* Controller: {
* constructor(hotbox: HotBox),
* active: () => void
* }
*/
function _control(Controller) {
if (_controler) {
_controler.active();
return;
}
Controller = Controller || KeyControl;
_controler = new Controller(_this);
_controler.active();
$hotBox.onmousedown = function(e) {
e.stopPropagation();
e.preventDefault();
};
return _this;
}
function _dispatchKey(e) {
var type = e.type.toLowerCase();
e.keyHash = key.hash(e);
e.isKey = function(keyExpression) {
if (!keyExpression) return false;
var expressions = keyExpression.split(/\s*\|\s*/);
while(expressions.length) {
if (e.keyHash == key.hash(expressions.shift())) return true;
}
return false;
};
e[type] = true;
// Boot: keyup and activeKey pressed on IDLE, active main state.
if (e.keyup && _this.activeKey && e.isKey(_this.activeKey) && _currentState == IDLE && _mainState) {
_activeState('main', {
x: $container.clientWidth / 2,
y: $container.clientHeight / 2
});
return;
}
var handleState = _currentState == IDLE ? _mainState : _currentState;
if (handleState) {
var handleResult = handleState.handleKeyEvent(e);
if (typeof(_this.onkeyevent) == 'function') {
e.handleResult = handleResult;
_this.onkeyevent(e, handleResult);
}
return handleResult;
}
return null;
}
function _addState(name) {
if (!name) return _currentState;
if (name == IDLE) {
throw new Error('Can not define or use the `idle` state.');
}
_states[name] = _states[name] || new HotBoxState(this, name);
if (name == 'main') {
_mainState = _states[name];
}
return _states[name];
}
function _activeState(name, position) {
_this.position = position;
// 回到 IDLE
if (name == IDLE) {
if (_currentState != IDLE) {
_stateStack.shift().deactive();
_stateStack = [];
}
_currentState = IDLE;
}
// 回退一个状态
else if (name == 'back') {
if (_currentState != IDLE) {
_currentState.deactive();
_stateStack.shift();
_currentState = _stateStack[0];
if (_currentState) {
_currentState.active();
} else {
_currentState = 'idle';
}
}
}
// 切换到具体状态
else {
if (_currentState != IDLE) {
_currentState.deactive();
}
var newState = _states[name];
_stateStack.unshift(newState);
if (typeof(_this.position) == 'function') {
position = _this.position(position);
}
newState.active(position);
_currentState = newState;
}
}
function setParentFSM(fsm) {
_this._parentFSM = fsm;
}
function getParentFSM() {
return _this._parentFSM;
}
this.control = _control;
this.state = _addState;
this.active = _activeState;
this.dispatch = _dispatchKey;
this.setParentFSM = setParentFSM;
this.getParentFSM = getParentFSM;
this.activeKey = 'space';
this.actionKey = 'space';
}
/**
* 表示热盒某个状态包含这些状态需要的 Dom 对象
*/
function HotBoxState(hotBox, stateName) {
var BUTTON_SELECTED_CLASS = 'selected';
var BUTTON_PRESSED_CLASS = 'pressed';
var STATE_ACTIVE_CLASS = 'active';
// 状态容器
var $state = createElement(div);
// 四种可见的按钮容器
var $center = createElement(div);
var $ring = createElement(div);
var $ringShape = createElement('div');
var $top = createElement(div);
var $bottom = createElement(div);
// 添加 CSS 类
addElementClass($state, 'state');
addElementClass($state, stateName);
addElementClass($center, 'center');
addElementClass($ring, 'ring');
addElementClass($ringShape, 'ring-shape');
addElementClass($top, 'top');
addElementClass($bottom, 'bottom');
// 摆放容器
appendChild(hotBox.$element, $state);
appendChild($state, $ringShape);
appendChild($state, $center);
appendChild($state, $ring);
appendChild($state, $top);
appendChild($state, $bottom);
// 记住状态名称
this.name = stateName;
// 五种按钮:中心,圆环,上栏,下栏,幕后
var buttons = {
center: null,
ring: [],
top: [],
bottom: [],
behind: []
};
var allButtons = [];
var selectedButton = null;
var pressedButton = null;
var stateActived = false;
// 布局,添加按钮后,标记需要布局
var needLayout = true;
function layout() {
var radius = buttons.ring.length * 15;
layoutRing(radius);
layoutTop(radius);
layoutBottom(radius);
indexPosition();
needLayout = false;
function layoutRing(radius) {
var ring = buttons.ring;
var step = 2 * Math.PI / ring.length;
if (buttons.center) {
buttons.center.indexedPosition = [0, 0];
}
$ringShape.style.marginLeft = $ringShape.style.marginTop = -radius + 'px';
$ringShape.style.width = $ringShape.style.height = (radius + radius) + 'px';
var $button, angle, x, y;
for (var i = 0; i < ring.length; i++) {
$button = ring[i].$button;
angle = step * i - Math.PI / 2;
x = radius * Math.cos(angle);
y = radius * Math.sin(angle);
ring[i].indexedPosition = [x, y];
$button.style.left = x + 'px';
$button.style.top = y + 'px';
}
}
function layoutTop(radius) {
var xOffset = -$top.clientWidth / 2;
var yOffset = -radius * 2 - $top.clientHeight / 2;
$top.style.marginLeft = xOffset + 'px';
$top.style.marginTop = yOffset + 'px';
buttons.top.forEach(function(topButton) {
var $button = topButton.$button;
topButton.indexedPosition = [xOffset + $button.offsetLeft + $button.clientWidth / 2, yOffset];
});
}
function layoutBottom(radius) {
var xOffset = -$bottom.clientWidth / 2;
var yOffset = radius * 2 - $bottom.clientHeight / 2;
$bottom.style.marginLeft = xOffset + 'px';
$bottom.style.marginTop = yOffset + 'px';
buttons.bottom.forEach(function(bottomButton) {
var $button = bottomButton.$button;
bottomButton.indexedPosition = [xOffset + $button.offsetLeft + $button.clientWidth / 2, yOffset];
});
}
function indexPosition() {
var positionedButtons = allButtons.filter(function(button) {
return button.indexedPosition;
});
positionedButtons.forEach(findNeightbour);
function findNeightbour(button) {
var neighbor = {};
var coef = 0;
var minCoef = {};
var homePosition = button.indexedPosition;
var candidatePosition, dx, dy, ds;
var possible, dir;
var abs = Math.abs;
positionedButtons.forEach(function(candidate) {
if (button == candidate) return;
candidatePosition = candidate.indexedPosition;
possible = [];
dx = candidatePosition[0] - homePosition[0];
dy = candidatePosition[1] - homePosition[1];
ds = Math.sqrt(dx * dx + dy * dy);
if (abs(dx) > 2) {
possible.push(dx > 0 ? 'right' : 'left');
possible.push(ds + abs(dy)); // coef for right/left neighbor
}
if (abs(dy) > 2) {
possible.push(dy > 0 ? 'down' : 'up');
possible.push(ds + abs(dx)); // coef for up/down neighbor
}
while (possible.length) {
dir = possible.shift();
coef = possible.shift();
if (!neighbor[dir] || coef < minCoef[dir]) {
neighbor[dir] = candidate;
minCoef[dir] = coef;
}
}
});
button.neighbor = neighbor;
}
}
}
function alwaysEnable() {
return true;
}
// 为状态创建按钮
function createButton(option) {
var $button = createElement(div);
addElementClass($button, 'button');
var render = option.render || defaultButtonRender;
$button.innerHTML = render(format, option);
switch (option.position) {
case 'center': appendChild($center, $button); break;
case 'ring': appendChild($ring, $button); break;
case 'top': appendChild($top, $button); break;
case 'bottom': appendChild($bottom, $button); break;
}
return {
action: option.action,
enable: option.enable || alwaysEnable,
beforeShow: option.beforeShow,
key: option.key,
next: option.next,
label: option.label,
data: option.data || null,
$button: $button
};
}
// 默认按钮渲染
function defaultButtonRender(format, option) {
return format('<span class="label">{label}</span><span class="key">{key}</span>', {
label: option.label,
key: option.key && option.key.split('|')[0]
});
}
// 为当前状态添加按钮
this.button = function(option) {
var button = createButton(option);
if (option.position == 'center') {
buttons.center = button;
} else if (buttons[option.position]) {
buttons[option.position].push(button);
}
allButtons.push(button);
needLayout = true;
};
function activeState(position) {
position = position || {
x: hotBox.$container.clientWidth / 2,
y: hotBox.$container.clientHeight / 2
};
if (position) {
$state.style.left = position.x + 'px';
$state.style.top = position.y + 'px';
}
allButtons.forEach(function(button) {
var $button = button.$button;
if ($button) {
$button.classList[button.enable() ? 'add' : 'remove']('enabled');
}
if (button.beforeShow) {
button.beforeShow();
}
});
addElementClass($state, STATE_ACTIVE_CLASS);
if (needLayout) {
layout();
}
if (!selectedButton) {
select(buttons.center || buttons.ring[0] || buttons.top[0] || buttons.bottom[0]);
}
stateActived = true;
}
function deactiveState() {
removeElementClass($state, STATE_ACTIVE_CLASS);
select(null);
stateActived = false;
}
// 激活当前状态
this.active = activeState;
// 反激活当前状态
this.deactive = deactiveState;
function press(button) {
if (pressedButton && pressedButton.$button) {
removeElementClass(pressedButton.$button, BUTTON_PRESSED_CLASS);
}
pressedButton = button;
if (pressedButton && pressedButton.$button) {
addElementClass(pressedButton.$button, BUTTON_PRESSED_CLASS);
}
}
function select(button) {
if (selectedButton && selectedButton.$button) {
if (selectedButton.$button) {
removeElementClass(selectedButton.$button, BUTTON_SELECTED_CLASS);
}
}
selectedButton = button;
if (selectedButton && selectedButton.$button) {
addElementClass(selectedButton.$button, BUTTON_SELECTED_CLASS);
}
}
$state.onmouseup = function(e) {
if (e.button) return;
var target = e.target;
while (target && target != $state) {
if (target.classList.contains('button')) {
allButtons.forEach(function(button) {
if (button.$button == target) {
execute(button);
}
});
}
target = target.parentNode;
}
};
this.handleKeyEvent = function(e) {
var handleResult = null;
/**
* @Desc: 搜狗浏览器下esc只触发keyup因此做兼容性处理
* @Editor: Naixor
* @Date: 2015.09.14
*/
if (hotBox.browser.sg) {
if (e.isKey('esc')) {
if (pressedButton) { // 若存在已经按下的按钮,则取消操作
if (!e.isKey(pressedButton.key)) { // the button is not esc
press(null);
}
} else {
hotBox.active('back', hotBox.position);
}
return 'back';
};
};
if (e.keydown || (hotBox.isIME && e.keyup)) {
allButtons.forEach(function(button) {
if (button.enable() && e.isKey(button.key)) {
if (stateActived || hotBox.hintDeactiveMainState) {
select(button);
press(button);
handleResult = 'buttonpress';
// 如果是 keyup 事件触发的,因为没有后续的按键事件,所以就直接执行
if(e.keyup) {
execute(button);
handleResult = 'execute';
return handleResult;
}
} else {
execute(button);
handleResult = 'execute';
}
e.preventDefault();
e.stopPropagation();
if (!stateActived && hotBox.hintDeactiveMainState) {
hotBox.active(stateName, hotBox.position);
}
}
});
if (stateActived) {
if (e.isKey('esc')) {
if (pressedButton) { // 若存在已经按下的按钮,则取消操作
if (!e.isKey(pressedButton.key)) { // the button is not esc
press(null);
}
} else {
hotBox.active('back', hotBox.position);
}
return 'back';
}
['up', 'down', 'left', 'right'].forEach(function(dir) {
if (!e.isKey(dir)) return;
if (!selectedButton) {
select(buttons.center || buttons.ring[0] || buttons.top[0] || buttons.bottom[0]);
return;
}
var neighbor = selectedButton.neighbor[dir];
while (neighbor && !neighbor.enable()) {
neighbor = neighbor.neighbor[dir];
}
if (neighbor) {
select(neighbor);
}
handleResult = 'navigate';
});
// 若是由 keyup 触发的,则直接执行选中的按钮
if (e.isKey('space') && e.keyup) {
execute(selectedButton);
e.preventDefault();
e.stopPropagation();
handleResult = 'execute';
} else if (e.isKey('space') && selectedButton) {
press(selectedButton);
handleResult = 'buttonpress';
} else if (pressedButton && pressedButton != selectedButton) {
press(null);
handleResult = 'selectcancel';
}
}
}
else if (e.keyup && (stateActived || !hotBox.hintDeactiveMainState)) {
if (pressedButton) {
if (e.isKey('space') && selectedButton == pressedButton || e.isKey(pressedButton.key)) {
execute(pressedButton);
e.preventDefault();
e.stopPropagation();
handleResult = 'execute';
}
}
}
/*
* Add by zhangbobell 2015.09.06
* 增加了下面这一个判断因为 safari 下开启输入法后所有的 keydown keycode 都为 229
* 只能以 keyup keycode 进行判断
* */
hotBox.isIME = (e.keyCode == 229 && e.keydown);
return handleResult;
};
function execute(button) {
if (button) {
if (!button.enable || button.enable()) {
if (button.action) button.action(button);
hotBox.active(button.next || IDLE, hotBox.position);
}
press(null);
select(null);
}
}
}
module.exports = HotBox;
});

View File

@ -0,0 +1,59 @@
define(function(require, exports, module) {
var keymap = require('./keymap');
var CTRL_MASK = 0x1000;
var ALT_MASK = 0x2000;
var SHIFT_MASK = 0x4000;
function hash(unknown) {
if (typeof(unknown) == 'string') {
return hashKeyExpression(unknown);
}
return hashKeyEvent(unknown);
}
function is(a, b) {
return a && b && hash(a) == hash(b);
}
exports.hash = hash;
exports.is = is;
function hashKeyEvent(keyEvent) {
var hashCode = 0;
if (keyEvent.ctrlKey || keyEvent.metaKey) {
hashCode |= CTRL_MASK;
}
if (keyEvent.altKey) {
hashCode |= ALT_MASK;
}
if (keyEvent.shiftKey) {
hashCode |= SHIFT_MASK;
}
// Shift, Control, Alt KeyCode ignored.
if ([16, 17, 18, 91].indexOf(keyEvent.keyCode) == -1) {
hashCode |= keyEvent.keyCode;
}
return hashCode;
}
function hashKeyExpression(keyExpression) {
var hashCode = 0;
keyExpression.toLowerCase().split(/\s*\+\s*/).forEach(function(name) {
switch(name) {
case 'ctrl':
case 'cmd':
hashCode |= CTRL_MASK;
break;
case 'alt':
hashCode |= ALT_MASK;
break;
case 'shift':
hashCode |= SHIFT_MASK;
break;
default:
hashCode |= keymap[name];
}
});
return hashCode;
}
});

View File

@ -0,0 +1,68 @@
define(function(require, exports, module) {
var key = require('./key');
var FOCUS_CLASS = 'hotbox-focus';
var RECEIVER_CLASS = 'hotbox-key-receiver';
function KeyControl(hotbox) {
var _this = this;
var _receiver;
var _actived = true;
var _receiverIsSelfCreated = false;
var $container = hotbox.$container;
_createReceiver();
_bindReceiver();
_bindContainer();
_active();
function _createReceiver() {
_receiver = document.createElement('input');
_receiver.classList.add(RECEIVER_CLASS);
$container.appendChild(_receiver);
_receiverIsSelfCreated = true;
}
function _bindReceiver() {
_receiver.onkeyup = _handle;
_receiver.onkeypress = _handle;
_receiver.onkeydown = _handle;
_receiver.onfocus = _active;
_receiver.onblur = _deactive;
if (_receiverIsSelfCreated) {
_receiver.oninput = function(e) { _receiver.value = null; };
}
}
function _bindContainer() {
$container.onmousedown = function(e) {
_active();
e.preventDefault();
};
}
function _handle(keyEvent) {
if (!_actived) return;
hotbox.dispatch(keyEvent);
}
function _active() {
_receiver.select();
_receiver.focus();
_actived = true;
$container.classList.add(FOCUS_CLASS);
}
function _deactive() {
_receiver.blur();
_actived = false;
$container.classList.remove(FOCUS_CLASS);
}
this.handle = _handle;
this.active = _active;
this.deactive = _deactive;
}
module.exports = KeyControl;
});

View File

@ -0,0 +1,82 @@
define(function(require, exports, module) {
var keymap = {
'Shift': 16,
'Control': 17,
'Alt': 18,
'CapsLock': 20,
'BackSpace': 8,
'Tab': 9,
'Enter': 13,
'Esc': 27,
'Space': 32,
'PageUp': 33,
'PageDown': 34,
'End': 35,
'Home': 36,
'Insert': 45,
'Left': 37,
'Up': 38,
'Right': 39,
'Down': 40,
'Direction': {
37: 1,
38: 1,
39: 1,
40: 1
},
'Delete': 46,
'NumLock': 144,
'Cmd': 91,
'CmdFF': 224,
'F1': 112,
'F2': 113,
'F3': 114,
'F4': 115,
'F5': 116,
'F6': 117,
'F7': 118,
'F8': 119,
'F9': 120,
'F10': 121,
'F11': 122,
'F12': 123,
'`': 192,
'=': 187,
'-': 189,
'/': 191,
'.': 190
};
// 小写适配
for (var key in keymap) {
if (keymap.hasOwnProperty(key)) {
keymap[key.toLowerCase()] = keymap[key];
}
}
var aKeyCode = 65;
var aCharCode = 'a'.charCodeAt(0);
// letters
'abcdefghijklmnopqrstuvwxyz'.split('').forEach(function(letter) {
keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode);
});
// numbers
var n = 9;
do {
keymap[n.toString()] = n + 48;
} while (n--);
module.exports = keymap;
});

View File

@ -0,0 +1,11 @@
export const loadKityMinder = async (): Promise<any> => {
if (typeof window !== 'undefined') {
if (window.kityminder) {
if (window.kityminder.Editor) return;
}
}
await import('kity');
await import('./kity-core/kityminder');
await import('./kity-editor/expose-editor');
};

View File

@ -0,0 +1,49 @@
/**
* @fileOverview
*
* 圆弧连线
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var connect = require('../core/connect');
var connectMarker = new kity.Marker().pipe(function() {
var r = 7;
var dot = new kity.Circle(r - 1);
this.addShape(dot);
this.setRef(r - 1, 0).setViewBox(-r, -r, r + r, r + r).setWidth(r).setHeight(r);
this.dot = dot;
this.node.setAttribute('markerUnits', 'userSpaceOnUse');
});
connect.register('arc', function(node, parent, connection, width, color) {
var box = node.getLayoutBox(),
pBox = parent.getLayoutBox();
var start, end, vector;
var abs = Math.abs;
var pathData = [];
var side = box.x > pBox.x ? 'right' : 'left';
node.getMinder().getPaper().addResource(connectMarker);
start = new kity.Point(pBox.cx, pBox.cy);
end = side == 'left' ?
new kity.Point(box.right + 2, box.cy) :
new kity.Point(box.left - 2, box.cy);
vector = kity.Vector.fromPoints(start, end);
pathData.push('M', start);
pathData.push('A', abs(vector.x), abs(vector.y), 0, 0, (vector.x * vector.y > 0 ? 0 : 1), end);
connection.setMarker(connectMarker);
connectMarker.dot.fill(color);
connection.setPathData(pathData);
});
});

View File

@ -0,0 +1,85 @@
/**
*
* 圆弧连线
*
* @author: along
* @copyright: bpd729@163.com , 2015
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var connect = require('../core/connect');
var connectMarker = new kity.Marker().pipe(function () {
var r = 7;
var dot = new kity.Circle(r - 1);
this.addShape(dot);
this.setRef(r - 1, 0).setViewBox(-r, -r, r + r, r + r).setWidth(r).setHeight(r);
this.dot = dot;
this.node.setAttribute('markerUnits', 'userSpaceOnUse');
});
/**
* 天盘图连线除了连接当前节点和前一个节点外, 还需要渲染当前节点和后一个节点的连接, 防止样式上的断线
* 这是天盘图与其余的模板不同的地方
*/
connect.register('arc_tp', function (node, parent, connection, width, color) {
var end_box = node.getLayoutBox(),
start_box = parent.getLayoutBox();
var index = node.getIndex();
var nextNode = parent.getChildren()[index + 1];
if (node.getIndex() > 0) {
start_box = parent.getChildren()[index - 1].getLayoutBox();
}
var start, end, vector;
var abs = Math.abs;
var pathData = [];
var side = end_box.x > start_box.x ? 'right' : 'left';
node.getMinder().getPaper().addResource(connectMarker);
start = new kity.Point(start_box.cx, start_box.cy);
end = new kity.Point(end_box.cx, end_box.cy);
var jl = Math.sqrt(Math.pow((start.x - end.x), 2) + Math.pow((start.y - end.y), 2)); //两圆中心点距离
jl = node.getIndex() == 0 ? jl * 0.4 : jl;
vector = kity.Vector.fromPoints(start, end);
pathData.push('M', start);
pathData.push('A', jl, jl, 0, 0, 1, end);
connection.setMarker(connectMarker);
connectMarker.dot.fill(color);
connection.setPathData(pathData);
// 设置下一个的节点的连接线
if (nextNode && nextNode.getConnection()) {
var nextConnection = nextNode.getConnection();
var next_end_box = nextNode.getLayoutBox();
var next_end = new kity.Point(next_end_box.cx, next_end_box.cy);
var jl2 = Math.sqrt(Math.pow((end.x - next_end.x), 2) + Math.pow((end.y - next_end.y), 2)); //两圆中心点距离
pathData = [];
pathData.push('M', end);
pathData.push('A', jl2, jl2, 0, 0, 1, next_end);
nextConnection.setMarker(connectMarker);
connectMarker.dot.fill(color);
nextConnection.setPathData(pathData);
}
});
});

View File

@ -0,0 +1,42 @@
/**
* @fileOverview
*
* 提供折线相连的方法
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var connect = require('../core/connect');
connect.register('bezier', function(node, parent, connection) {
// 连线起点和终点
var po = parent.getLayoutVertexOut(),
pi = node.getLayoutVertexIn();
// 连线矢量和方向
var v = parent.getLayoutVectorOut().normalize();
var r = Math.round;
var abs = Math.abs;
var pathData = [];
pathData.push('M', r(po.x), r(po.y));
if (abs(v.x) > abs(v.y)) {
// x - direction
var hx = (pi.x + po.x) / 2;
pathData.push('C', hx, po.y, hx, pi.y, pi.x, pi.y);
} else {
// y - direction
var hy = (pi.y + po.y) / 2;
pathData.push('C', po.x, hy, pi.x, hy, pi.x, pi.y);
}
connection.setMarker(null);
connection.setPathData(pathData);
});
});

View File

@ -0,0 +1,33 @@
/**
* @fileOverview
*
* 鱼骨头主干连线
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var connect = require('../core/connect');
connect.register('fish-bone-master', function(node, parent, connection) {
var pout = parent.getLayoutVertexOut(),
pin = node.getLayoutVertexIn();
var abs = Math.abs;
var dy = abs(pout.y - pin.y),
dx = abs(pout.x - pin.x);
var pathData = [];
pathData.push('M', pout.x, pout.y);
pathData.push('h', dx - dy);
pathData.push('L', pin.x, pin.y);
connection.setMarker(null);
connection.setPathData(pathData);
});
});

View File

@ -0,0 +1,34 @@
/**
* @fileOverview
*
* "L" 连线
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var connect = require('../core/connect');
connect.register('l', function(node, parent, connection) {
var po = parent.getLayoutVertexOut();
var pi = node.getLayoutVertexIn();
var vo = parent.getLayoutVectorOut();
var pathData = [];
var r = Math.round,
abs = Math.abs;
pathData.push('M', po.round());
if (abs(vo.x) > abs(vo.y)) {
pathData.push('H', r(pi.x));
} else {
pathData.push('V', pi.y);
}
pathData.push('L', pi);
connection.setPathData(pathData);
});
});

View File

@ -0,0 +1,63 @@
/**
* @fileOverview
*
* 提供折线相连的方法
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var connect = require('../core/connect');
connect.register('poly', function(node, parent, connection, width) {
// 连线起点和终点
var po = parent.getLayoutVertexOut(),
pi = node.getLayoutVertexIn();
// 连线矢量和方向
var v = parent.getLayoutVectorOut().normalize();
var r = Math.round;
var abs = Math.abs;
var pathData = [];
pathData.push('M', r(po.x), r(po.y));
switch (true) {
case abs(v.x) > abs(v.y) && v.x < 0:
// left
pathData.push('h', -parent.getStyle('margin-left'));
pathData.push('v', pi.y - po.y);
pathData.push('H', pi.x);
break;
case abs(v.x) > abs(v.y) && v.x >= 0:
// right
pathData.push('h', parent.getStyle('margin-right'));
pathData.push('v', pi.y - po.y);
pathData.push('H', pi.x);
break;
case abs(v.x) <= abs(v.y) && v.y < 0:
// top
pathData.push('v', -parent.getStyle('margin-top'));
pathData.push('h', pi.x - po.x);
pathData.push('V', pi.y);
break;
case abs(v.x) <= abs(v.y) && v.y >= 0:
// bottom
pathData.push('v', parent.getStyle('margin-bottom'));
pathData.push('h', pi.x - po.x);
pathData.push('V', pi.y);
break;
}
connection.setMarker(null);
connection.setPathData(pathData);
});
});

View File

@ -0,0 +1,50 @@
/**
* @fileOverview
*
* 下划线连线
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var connect = require('../core/connect');
connect.register('under', function(node, parent, connection, width, color) {
var box = node.getLayoutBox(),
pBox = parent.getLayoutBox();
var start, end, vector;
var abs = Math.abs;
var pathData = [];
var side = box.x > pBox.x ? 'right' : 'left';
var radius = node.getStyle('connect-radius');
var underY = box.bottom + 3;
var startY = parent.getType() == 'sub' ? pBox.bottom + 3 : pBox.cy;
var p1, p2, p3, mx;
if (side == 'right') {
p1 = new kity.Point(pBox.right, startY);
p2 = new kity.Point(box.left - 10, underY);
p3 = new kity.Point(box.right, underY);
} else {
p1 = new kity.Point(pBox.left, startY);
p2 = new kity.Point(box.right + 10, underY);
p3 = new kity.Point(box.left, underY);
}
mx = (p1.x + p2.x) / 2;
pathData.push('M', p1);
pathData.push('C', mx, p1.y, mx, p2.y, p2);
pathData.push('L', p3);
connection.setMarker(null);
connection.setPathData(pathData);
});
});

View File

@ -0,0 +1,34 @@
/**
* @fileOverview
*
* 调试工具 kity.Box 提供一个可视化的渲染
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var Minder = require('./minder');
if (location.href.indexOf('boxv') != -1) {
var vrect;
Object.defineProperty(kity.Box.prototype, 'visualization', {
get: function() {
if (!vrect) return null;
return vrect.setBox(this);
}
});
Minder.registerInitHook(function() {
this.on('paperrender', function() {
vrect = new kity.Rect();
vrect.fill('rgba(200, 200, 200, .5)');
vrect.stroke('orange');
this.getRenderContainer().addShape(vrect);
});
});
}
});

View File

@ -0,0 +1,43 @@
/**
* @fileOverview
*
* 动画控制
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var Minder = require('./minder');
var animateDefaultOptions = {
enableAnimation: true,
layoutAnimationDuration: 300,
viewAnimationDuration: 100,
zoomAnimationDuration: 300
};
var resoredAnimationOptions = {};
Minder.registerInitHook(function() {
this.setDefaultOptions(animateDefaultOptions);
if (!this.getOption('enableAnimation')) {
this.disableAnimation();
}
});
Minder.prototype.enableAnimation = function() {
for (var name in animateDefaultOptions) {
if (animateDefaultOptions.hasOwnProperty(name)) {
this.setOption(resoredAnimationOptions[name]);
}
}
};
Minder.prototype.disableAnimation = function() {
for (var name in animateDefaultOptions) {
if (animateDefaultOptions.hasOwnProperty(name)) {
resoredAnimationOptions[name] = this.getOption(name);
this.setOption(name, 0);
}
}
};
});

View File

@ -0,0 +1,169 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
var MinderNode = require('./node');
var MinderEvent = require('./event');
var COMMAND_STATE_NORMAL = 0;
var COMMAND_STATE_DISABLED = -1;
var COMMAND_STATE_ACTIVED = 1;
/**
* 表示一个命令包含命令的查询及执行
*/
var Command = kity.createClass('Command', {
constructor: function() {
this._isContentChange = true;
this._isSelectionChange = false;
},
execute: function(minder, args) {
throw new Error('Not Implement: Command.execute()');
},
setContentChanged: function(val) {
this._isContentChange = !!val;
},
isContentChanged: function() {
return this._isContentChange;
},
setSelectionChanged: function(val) {
this._isSelectionChange = !!val;
},
isSelectionChanged: function() {
return this._isContentChange;
},
queryState: function(km) {
return COMMAND_STATE_NORMAL;
},
queryValue: function(km) {
return 0;
},
isNeedUndo: function() {
return true;
}
});
Command.STATE_NORMAL = COMMAND_STATE_NORMAL;
Command.STATE_ACTIVE = COMMAND_STATE_ACTIVED;
Command.STATE_DISABLED = COMMAND_STATE_DISABLED;
kity.extendClass(Minder, {
_getCommand: function(name) {
return this._commands[name.toLowerCase()];
},
_queryCommand: function(name, type, args) {
var cmd = this._getCommand(name);
if (cmd) {
var queryCmd = cmd['query' + type];
if (queryCmd)
return queryCmd.apply(cmd, [this].concat(args));
}
return 0;
},
/**
* @method queryCommandState()
* @for Minder
* @description 查询指定命令的状态
*
* @grammar queryCommandName(name) => {number}
*
* @param {string} name 要查询的命令名称
*
* @return {number}
* -1: 命令不存在或命令当前不可用
* 0: 命令可用
* 1: 命令当前可用并且已经执行过
*/
queryCommandState: function(name) {
return this._queryCommand(name, 'State', [].slice.call(arguments, 1));
},
/**
* @method queryCommandValue()
* @for Minder
* @description 查询指定命令当前的执行值
*
* @grammar queryCommandValue(name) => {any}
*
* @param {string} name 要查询的命令名称
*
* @return {any}
* 如果命令不存在返回 undefined
* 不同命令具有不同返回值具体请查看 [Command](command) 章节
*/
queryCommandValue: function(name) {
return this._queryCommand(name, 'Value', [].slice.call(arguments, 1));
},
/**
* @method execCommand()
* @for Minder
* @description 执行指定的命令
*
* @grammar execCommand(name, args...)
*
* @param {string} name 要执行的命令名称
* @param {argument} args 要传递给命令的其它参数
*/
execCommand: function(name) {
if (!name) return null;
name = name.toLowerCase();
var cmdArgs = [].slice.call(arguments, 1),
cmd, stoped, result, eventParams;
var me = this;
cmd = this._getCommand(name);
eventParams = {
command: cmd,
commandName: name.toLowerCase(),
commandArgs: cmdArgs
};
if (!cmd || !~this.queryCommandState(name)) {
return false;
}
if (!this._hasEnterExecCommand) {
this._hasEnterExecCommand = true;
stoped = this._fire(new MinderEvent('beforeExecCommand', eventParams, true));
if (!stoped) {
this._fire(new MinderEvent('preExecCommand', eventParams, false));
result = cmd.execute.apply(cmd, [me].concat(cmdArgs));
this._fire(new MinderEvent('execCommand', eventParams, false));
if (cmd.isContentChanged()) {
this._firePharse(new MinderEvent('contentchange'));
}
this._interactChange();
}
this._hasEnterExecCommand = false;
} else {
result = cmd.execute.apply(cmd, [me].concat(cmdArgs));
if (!this._hasEnterExecCommand) {
this._interactChange();
}
}
return result === undefined ? null : result;
}
});
module.exports = Command;
});

View File

@ -0,0 +1,92 @@
define(function(require, exports, module) {
var utils = require('./utils');
function compatibility(json) {
var version = json.version || (json.root ? '1.4.0' : '1.1.3');
switch (version) {
case '1.1.3':
c_113_120(json);
/* falls through */
case '1.2.0':
case '1.2.1':
c_120_130(json);
/* falls through */
case '1.3.0':
case '1.3.1':
case '1.3.2':
case '1.3.3':
case '1.3.4':
case '1.3.5':
/* falls through */
c_130_140(json);
}
return json;
}
function traverse(node, fn) {
fn(node);
if (node.children) node.children.forEach(function(child) {
traverse(child, fn);
});
}
/* 脑图数据升级 */
function c_120_130(json) {
traverse(json, function(node) {
var data = node.data;
delete data.layout_bottom_offset;
delete data.layout_default_offset;
delete data.layout_filetree_offset;
});
}
/**
* 脑图数据升级
* v1.1.3 => v1.2.0
* */
function c_113_120(json) {
// 原本的布局风格
var ocs = json.data.currentstyle;
delete json.data.currentstyle;
// 为 1.2 选择模板,同时保留老版本文件的皮肤
if (ocs == 'bottom') {
json.template = 'structure';
json.theme = 'snow';
} else if (ocs == 'default') {
json.template = 'default';
json.theme = 'classic';
}
traverse(json, function(node) {
var data = node.data;
// 升级优先级、进度图标
if ('PriorityIcon' in data) {
data.priority = data.PriorityIcon;
delete data.PriorityIcon;
}
if ('ProgressIcon' in data) {
data.progress = 1 + ((data.ProgressIcon - 1) << 1);
delete data.ProgressIcon;
}
// 删除过时属性
delete data.point;
delete data.layout;
});
}
function c_130_140(json) {
json.root = {
data: json.data,
children: json.children
};
delete json.data;
delete json.children;
}
return compatibility;
});

View File

@ -0,0 +1,126 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Module = require('./module');
var Minder = require('./minder');
var MinderNode = require('./node');
// 连线提供方
var _connectProviders = {};
function register(name, provider) {
_connectProviders[name] = provider;
}
register('default', function(node, parent, connection) {
connection.setPathData([
'M', parent.getLayoutVertexOut(),
'L', node.getLayoutVertexIn()
]);
});
kity.extendClass(MinderNode, {
/**
* @private
* @method getConnect()
* @for MinderNode
* @description 获取当前节点的连线类型
*
* @grammar getConnect() => {string}
*/
getConnect: function() {
return this.data.connect || 'default';
},
getConnectProvider: function() {
return _connectProviders[this.getConnect()] || _connectProviders['default'];
},
/**
* @private
* @method getConnection()
* @for MinderNode
* @description 获取当前节点的连线对象
*
* @grammar getConnection() => {kity.Path}
*/
getConnection: function() {
return this._connection || null;
}
});
kity.extendClass(Minder, {
getConnectContainer: function() {
return this._connectContainer;
},
createConnect: function(node) {
if (node.isRoot()) return;
var connection = new kity.Path();
node._connection = connection;
this._connectContainer.addShape(connection);
this.updateConnect(node);
},
removeConnect: function(node) {
var me = this;
node.traverse(function(node) {
me._connectContainer.removeShape(node._connection);
node._connection = null;
});
},
updateConnect: function(node) {
var connection = node._connection;
var parent = node.parent;
if (!parent || !connection) return;
if (parent.isCollapsed()) {
connection.setVisible(false);
return;
}
connection.setVisible(true);
var provider = node.getConnectProvider();
var strokeColor = node.getStyle('connect-color') || 'white',
strokeWidth = node.getStyle('connect-width') || 2;
connection.stroke(strokeColor, strokeWidth);
provider(node, parent, connection, strokeWidth, strokeColor);
if (strokeWidth % 2 === 0) {
connection.setTranslate(0.5, 0.5);
} else {
connection.setTranslate(0, 0);
}
}
});
Module.register('Connect', {
init: function() {
this._connectContainer = new kity.Group().setId(utils.uuid('minder_connect_group'));
this.getRenderContainer().prependShape(this._connectContainer);
},
events: {
'nodeattach': function(e) {
this.createConnect(e.node);
},
'nodedetach': function(e) {
this.removeConnect(e.node);
},
'layoutapply layoutfinish noderender': function(e) {
this.updateConnect(e.node);
}
}
});
exports.register = register;
});

View File

@ -0,0 +1,362 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
var MinderNode = require('./node');
var MinderEvent = require('./event');
var compatibility = require('./compatibility');
var Promise = require('./promise');
var protocols = {};
function registerProtocol(name, protocol) {
protocols[name] = protocol;
for (var pname in protocols) {
if (protocols.hasOwnProperty(pname)) {
protocols[pname] = protocols[pname];
protocols[pname].name = pname;
}
}
}
function getRegisterProtocol(name) {
return name === undefined ? protocols : (protocols[name] || null);
}
exports.registerProtocol = registerProtocol;
exports.getRegisterProtocol = getRegisterProtocol;
// 导入导出
kity.extendClass(Minder, {
// 自动导入
setup: function(target) {
if (typeof target == 'string') {
target = document.querySelector(target);
}
if (!target) return;
var protocol = target.getAttribute('minder-data-type');
if (protocol in protocols) {
var data = target.textContent;
target.textContent = null;
this.renderTo(target);
this.importData(protocol, data);
}
return this;
},
/**
* @method exportJson()
* @for Minder
* @description
* 导出当前脑图数据为 JSON 对象导出的数据格式请参考 [Data](data) 章节
* @grammar exportJson() => {plain}
*/
exportJson: function() {
/* 导出 node 上整棵树的数据为 JSON */
function exportNode(node) {
var exported = {};
exported.data = node.getData();
var childNodes = node.getChildren();
exported.children = [];
for (var i = 0; i < childNodes.length; i++) {
exported.children.push(exportNode(childNodes[i]));
}
return exported;
}
var json = {
root: exportNode(this.getRoot())
};
json.template = this.getTemplate();
json.theme = this.getTheme();
json.version = Minder.version;
return JSON.parse(JSON.stringify(json));
},
/**
* function Text2Children(MinderNode, String)
* @param {MinderNode} node 要导入数据的节点
* @param {String} text 导入的text数据
* @Desc: 用于批量插入子节点并不会修改被插入的父节点
* @Editor: Naixor
* @Date: 2015.9.21
* @example: 用于批量导入如下类型的节点
* 234
* 3456346 asadf
* 12312414
* wereww
* 12314
* 1231412
* 13123
*/
Text2Children: function (node, text) {
if (!(node instanceof kityminder.Node)) {
return;
// throw new Error('Json2Children::node is not a kityminder.Node type!');
};
var children = [],
jsonMap = {},
level = 0;
var LINE_SPLITTER = /\r|\n|\r\n/,
TAB_REGEXP = /^(\t|\x20{4})/;
var lines = text.split(LINE_SPLITTER),
line = '', jsonNode, i = 0;
var minder = this;
function isEmpty(line) {
return line === "" && !/\S/.test(line);
}
function getNode(line) {
return {
data: {
text: line.replace(/^(\t|\x20{4})+/, '').replace(/(\t|\x20{4})+$/, '')
},
children: []
}
}
function getLevel(text) {
var level = 0;
while (TAB_REGEXP.test(text)) {
text = text.replace(TAB_REGEXP, '');
level++;
}
return level;
}
function addChild(parent, node) {
parent.children.push(node);
}
function importChildren(node, children) {
for (var i = 0, l = children.length; i < l; i++) {
var childNode = minder.createNode(null, node);
childNode.setData('text', children[i].data.text || '');
importChildren(childNode, children[i].children);
}
}
while ((line = lines[i++]) !== undefined) {
line = line.replace(/&nbsp;/g, '');
if (isEmpty(line)) continue;
level = getLevel(line);
jsonNode = getNode(line);
if (level === 0) {
jsonMap = {};
children.push(jsonNode);
jsonMap[0] = children[children.length-1];
} else {
if (!jsonMap[level-1]) {
throw new Error('Invalid local format');
};
addChild(jsonMap[level-1], jsonNode);
jsonMap[level] = jsonNode;
}
}
importChildren(node, children);
minder.refresh();
},
/**
* @method exportNode(MinderNode)
* @param {MinderNode} node 当前要被导出的节点
* @return {Object} 返回只含有data和children的Object
* @Editor: Naixor
* @Date: 2015.9.22
*/
exportNode: function (node) {
var exported = {};
exported.data = node.getData();
var childNodes = node.getChildren();
exported.children = [];
for (var i = 0; i < childNodes.length; i++) {
exported.children.push(this.exportNode(childNodes[i]));
}
return exported;
},
/**
* @method importNode()
* @description 根据纯json {data, children}数据转换成为脑图节点
* @Editor: Naixor
* @Date: 2015.9.20
*/
importNode: function(node, json) {
var data = json.data;
node.data = {};
for (var field in data) {
node.setData(field, data[field]);
}
var childrenTreeData = json.children || [];
for (var i = 0; i < childrenTreeData.length; i++) {
var childNode = this.createNode(null, node);
this.importNode(childNode, childrenTreeData[i]);
}
return node;
},
/**
* @method importJson()
* @for Minder
* @description 导入脑图数据数据为 JSON 对象具体的数据字段形式请参考 [Data](data) 章节
*
* @grammar importJson(json) => {this}
*
* @param {plain} json 要导入的数据
*/
importJson: function(json) {
if (!json) return;
/**
* @event preimport
* @for Minder
* @when 导入数据之前
*/
this._fire(new MinderEvent('preimport', null, false));
// 删除当前所有节点
while (this._root.getChildren().length) {
this.removeNode(this._root.getChildren()[0]);
}
json = compatibility(json);
this.importNode(this._root, json.root);
this.setTemplate(json.template || 'default');
this.setTheme(json.theme || null);
this.refresh();
/**
* @event import,contentchange,interactchange
* @for Minder
* @when 导入数据之后
*/
this.fire('import');
this._firePharse({
type: 'contentchange'
});
this._interactChange();
return this;
},
/**
* @method exportData()
* @for Minder
* @description 使用指定使用的数据协议导入脑图数据
*
* @grammar exportData(protocol) => Promise<data>
*
* @param {string} protocol 指定的数据协议默认内置五种数据协议 `json``text``markdown``svg` `png`
*/
exportData: function(protocolName, option) {
var json, protocol;
json = this.exportJson();
// 指定了协议进行导出,需要检测协议是否支持
if (protocolName) {
protocol = protocols[protocolName];
if (!protocol || !protocol.encode) {
return Promise.reject(new Error('Not supported protocol:' + protocolName));
}
}
// 导出前抛个事件
this._fire(new MinderEvent('beforeexport', {
json: json,
protocolName: protocolName,
protocol: protocol
}));
return Promise.resolve(protocol.encode(json, this, option));
},
/**
* @method importData()
* @for Minder
* @description 使用指定的数据协议导入脑图数据覆盖当前实例的脑图
*
* @grammar importData(protocol, callback) => Promise<json>
*
* @param {string} protocol 指定的用于解析数据的数据协议默认内置三种数据协议 `json``text` `markdown` 的支持
* @param {any} data 要导入的数据
*/
importData: function(protocolName, data, option) {
var json, protocol;
var minder = this;
// 指定了协议进行导入,需要检测协议是否支持
if (protocolName) {
protocol = protocols[protocolName];
if (!protocol || !protocol.decode) {
return Promise.reject(new Error('Not supported protocol:' + protocolName));
}
}
var params = {
local: data,
protocolName: protocolName,
protocol: protocol
};
// 导入前抛事件
this._fire(new MinderEvent('beforeimport', params));
return Promise.resolve(protocol.decode(data, this, option)).then(function(json) {
minder.importJson(json);
return json;
});
},
/**
* @method decodeData()
* @for Minder
* @description 使用指定的数据协议解析为脑图数据 importData 的区别在于不覆盖当前实例的脑图
*
* @grammar decodeData(protocol, callback) => Promise<json>
*
* @param {string} protocol 指定的用于解析数据的数据协议默认内置三种数据协议 `json``text` `markdown` 的支持
* @param {any} data 要导入的数据
*/
decodeData: function(protocolName, data, option) {
var json, protocol;
var minder = this;
// 指定了协议进行导入,需要检测协议是否支持
if (protocolName) {
protocol = protocols[protocolName];
if (!protocol || !protocol.decode) {
return Promise.reject(new Error('Not supported protocol:' + protocolName));
}
}
var params = {
local: data,
protocolName: protocolName,
protocol: protocol
};
// 导入前抛事件
this._fire(new MinderEvent('beforeimport', params));
return Promise.resolve(protocol.decode(data, this, option))
}
});
});

View File

@ -0,0 +1,263 @@
define(function (require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
/**
* @class MinderEvent
* @description 表示一个脑图中发生的事件
*/
var MinderEvent = kity.createClass('MindEvent', {
constructor: function (type, params, canstop) {
params = params || {};
if (params.getType && params.getType() == 'ShapeEvent') {
/**
* @property kityEvent
* @for MinderEvent
* @description 如果事件是从一个 kity 的事件派生的会有 kityEvent 属性指向原来的 kity 事件
* @type {KityEvent}
*/
this.kityEvent = params;
/**
* @property originEvent
* @for MinderEvent
* @description 如果事件是从原声 Dom 事件派生的 clickmousemove 会有 originEvent 指向原来的 Dom 事件
* @type {DomEvent}
*/
this.originEvent = params.originEvent;
} else if (params.target && params.preventDefault) {
this.originEvent = params;
} else {
kity.Utils.extend(this, params);
}
/**
* @property type
* @for MinderEvent
* @description 事件的类型 `click``contentchange`
* @type {string}
*/
this.type = type;
this._canstop = canstop || false;
},
/**
* @method getPosition()
* @for MinderEvent
* @description 如果事件是从一个 kity 事件派生的会有 `getPosition()` 获取事件发生的坐标
*
* @grammar getPosition(refer) => {kity.Point}
*
* @param {string|kity.Shape} refer
* 参照的坐标系
* `"screen"` - 以浏览器屏幕为参照坐标系
* `"minder"` - 默认以脑图画布为参照坐标系
* `{kity.Shape}` - 指定以某个 kity 图形为参照坐标系
*/
getPosition: function (refer) {
if (!this.kityEvent) return;
if (!refer || refer == 'minder') {
return this.kityEvent.getPosition(this.minder.getRenderContainer());
}
return this.kityEvent.getPosition.call(this.kityEvent, refer);
},
/**
* @method getTargetNode()
* @for MinderEvent
* @description 当发生的事件是鼠标事件时获取事件位置命中的脑图节点
*
* @grammar getTargetNode() => {MinderNode}
*/
getTargetNode: function () {
var findShape = this.kityEvent && this.kityEvent.targetShape;
if (!findShape) return null;
while (!findShape.minderNode && findShape.container) {
findShape = findShape.container;
}
var node = findShape.minderNode;
if (node && findShape.getOpacity() < 1) return null;
return node || null;
},
/**
* @method stopPropagation()
* @for MinderEvent
* @description 当发生的事件是鼠标事件时获取事件位置命中的脑图节点
*
* @grammar getTargetNode() => {MinderNode}
*/
stopPropagation: function () {
this._stoped = true;
},
stopPropagationImmediately: function () {
this._immediatelyStoped = true;
this._stoped = true;
},
shouldStopPropagation: function () {
return this._canstop && this._stoped;
},
shouldStopPropagationImmediately: function () {
return this._canstop && this._immediatelyStoped;
},
preventDefault: function () {
this.originEvent.preventDefault();
},
isRightMB: function () {
var isRightMB = false;
if (!this.originEvent) {
return false;
}
if ('which' in this.originEvent) isRightMB = this.originEvent.which == 3;
else if ('button' in this.originEvent) isRightMB = this.originEvent.button == 2;
return isRightMB;
},
getKeyCode: function () {
var evt = this.originEvent;
return evt.keyCode || evt.which;
},
});
Minder.registerInitHook(function (option) {
this._initEvents();
});
kity.extendClass(Minder, {
_initEvents: function () {
this._eventCallbacks = {};
},
_resetEvents: function () {
this._initEvents();
this._bindEvents();
},
_bindEvents: function () {
/* jscs:disable maximumLineLength */
this._paper.on(
'click dblclick mousedown contextmenu mouseup mousemove mouseover mousewheel DOMMouseScroll touchstart touchmove touchend dragenter dragleave drop',
this._firePharse.bind(this)
);
if (window) {
window.addEventListener('resize', this._firePharse.bind(this));
}
},
/**
* @method dispatchKeyEvent
* @description 派发键盘相关事件到脑图实例上让实例的模块处理
* @grammar dispatchKeyEvent(e) => {this}
* @param {Event} e 原生的 Dom 事件对象
*/
dispatchKeyEvent: function (e) {
this._firePharse(e);
},
_firePharse: function (e) {
var beforeEvent, preEvent, executeEvent;
if (e.type == 'DOMMouseScroll') {
e.type = 'mousewheel';
e.wheelDelta = e.originEvent.wheelDelta = e.originEvent.detail * -10;
e.wheelDeltaX = e.originEvent.mozMovementX;
e.wheelDeltaY = e.originEvent.mozMovementY;
}
beforeEvent = new MinderEvent('before' + e.type, e, true);
if (this._fire(beforeEvent)) {
return;
}
preEvent = new MinderEvent('pre' + e.type, e, true);
executeEvent = new MinderEvent(e.type, e, true);
if (this._fire(preEvent) || this._fire(executeEvent)) this._fire(new MinderEvent('after' + e.type, e, false));
},
_interactChange: function (e) {
var me = this;
if (me._interactScheduled) return;
setTimeout(function () {
me._fire(new MinderEvent('interactchange'));
me._interactScheduled = false;
}, 100);
me._interactScheduled = true;
},
_listen: function (type, callback) {
var callbacks = this._eventCallbacks[type] || (this._eventCallbacks[type] = []);
callbacks.push(callback);
},
_fire: function (e) {
/**
* @property minder
* @description 产生事件的 Minder 对象
* @for MinderShape
* @type {Minder}
*/
e.minder = this;
var status = this.getStatus();
var callbacks = this._eventCallbacks[e.type.toLowerCase()] || [];
if (status) {
callbacks = callbacks.concat(this._eventCallbacks[status + '.' + e.type.toLowerCase()] || []);
}
if (callbacks.length === 0) {
return;
}
var lastStatus = this.getStatus();
for (var i = 0; i < callbacks.length; i++) {
callbacks[i].call(this, e);
/* this.getStatus() != lastStatus ||*/
if (e.shouldStopPropagationImmediately()) {
break;
}
}
return e.shouldStopPropagation();
},
on: function (name, callback) {
var km = this;
name.split(/\s+/).forEach(function (n) {
km._listen(n.toLowerCase(), callback);
});
return this;
},
off: function (name, callback) {
var types = name.split(/\s+/);
var i, j, callbacks, removeIndex;
for (i = 0; i < types.length; i++) {
callbacks = this._eventCallbacks[types[i].toLowerCase()];
if (callbacks) {
removeIndex = null;
for (j = 0; j < callbacks.length; j++) {
if (callbacks[j] == callback) {
removeIndex = j;
}
}
if (removeIndex !== null) {
callbacks.splice(removeIndex, 1);
}
}
}
},
fire: function (type, params) {
var e = new MinderEvent(type, params);
this._fire(e);
return this;
},
});
module.exports = MinderEvent;
});

View File

@ -0,0 +1,43 @@
define(function (require, exports, module) {
var kity = require('./kity');
var Minder = require('./minder');
Minder.registerInitHook(function () {
this.on('beforemousedown', function (e) {
this.focus();
// FIXME如果遇到事件触发问题需要检查这里
if (e.kityEvent.targetShape.__KityClassName === 'Paper') return;
e.preventDefault();
});
this.on('paperrender', function () {
this.focus();
});
});
kity.extendClass(Minder, {
focus: function () {
if (!this.isFocused()) {
var renderTarget = this._renderTarget;
renderTarget.classList.add('focus');
this.renderNodeBatch(this.getSelectedNodes());
}
this.fire('focus');
return this;
},
blur: function () {
if (this.isFocused()) {
var renderTarget = this._renderTarget;
renderTarget.classList.remove('focus');
this.renderNodeBatch(this.getSelectedNodes());
}
this.fire('blur');
return this;
},
isFocused: function () {
var renderTarget = this._renderTarget;
return renderTarget && renderTarget.classList.contains('focus');
},
});
});

View File

@ -0,0 +1,128 @@
define(function(require, exports, module) {
var keymap = {
'Backspace': 8,
'Tab': 9,
'Enter': 13,
'Shift': 16,
'Control': 17,
'Alt': 18,
'CapsLock': 20,
'Esc': 27,
'Spacebar': 32,
'PageUp': 33,
'PageDown': 34,
'End': 35,
'Home': 36,
'Insert': 45,
'Left': 37,
'Up': 38,
'Right': 39,
'Down': 40,
'direction': {
37: 1,
38: 1,
39: 1,
40: 1
},
'Del': 46,
'NumLock': 144,
'Cmd': 91,
'CmdFF': 224,
'F1': 112,
'F2': 113,
'F3': 114,
'F4': 115,
'F5': 116,
'F6': 117,
'F7': 118,
'F8': 119,
'F9': 120,
'F10': 121,
'F11': 122,
'F12': 123,
'`': 192,
'=': 187,
'-': 189,
'/': 191,
'.': 190,
controlKeys: {
16: 1,
17: 1,
18: 1,
20: 1,
91: 1,
224: 1
},
'notContentChange': {
13: 1,
9: 1,
33: 1,
34: 1,
35: 1,
36: 1,
16: 1,
17: 1,
18: 1,
20: 1,
91: 1,
//上下左右
37: 1,
38: 1,
39: 1,
40: 1,
113: 1,
114: 1,
115: 1,
144: 1,
27: 1
},
'isSelectedNodeKey': {
//上下左右
37: 1,
38: 1,
39: 1,
40: 1,
13: 1,
9: 1
}
};
// 小写适配
for (var key in keymap) {
if (keymap.hasOwnProperty(key)) {
keymap[key.toLowerCase()] = keymap[key];
}
}
var aKeyCode = 65;
var aCharCode = 'a'.charCodeAt(0);
// letters
'abcdefghijklmnopqrstuvwxyz'.split('').forEach(function(letter) {
keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode);
});
// numbers
var n = 9;
do {
keymap[n.toString()] = n + 48;
} while (--n);
module.exports = keymap;
});

View File

@ -0,0 +1,66 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
function listen(element, type, handler) {
type.split(' ').forEach(function(name) {
element.addEventListener(name, handler, false);
});
}
Minder.registerInitHook(function(option) {
this.setDefaultOptions({
enableKeyReceiver: true
});
if (this.getOption('enableKeyReceiver')) {
this.on('paperrender', function() {
this._initKeyReceiver();
});
}
});
kity.extendClass(Minder, {
_initKeyReceiver: function() {
if (this._keyReceiver) return;
var receiver = this._keyReceiver = document.createElement('input');
receiver.classList.add('km-receiver');
var renderTarget = this._renderTarget;
renderTarget.appendChild(receiver);
var minder = this;
listen(receiver, 'keydown keyup keypress copy paste blur focus input', function(e) {
switch (e.type) {
case 'blur':
minder.blur();
break;
case 'focus':
minder.focus();
break;
case 'input':
receiver.value = null;
break;
}
minder._firePharse(e);
e.preventDefault();
});
this.on('focus', function() {
receiver.select();
receiver.focus();
});
this.on('blur', function() {
receiver.blur();
});
if (this.isFocused()) {
receiver.select();
receiver.focus();
}
}
});
});

View File

@ -0,0 +1,11 @@
/**
* @fileOverview
*
* Kity 引入
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
module.exports = window.kity;
});

View File

@ -0,0 +1,523 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
var MinderNode = require('./node');
var MinderEvent = require('./event');
var Command = require('./command');
var _layouts = {};
var _defaultLayout;
function register(name, layout) {
_layouts[name] = layout;
_defaultLayout = _defaultLayout || name;
}
/**
* @class Layout 布局基类具体布局需要从该类派生
*/
var Layout = kity.createClass('Layout', {
/**
* @abstract
*
* 子类需要实现的布局算法该算法输入一个节点排布该节点的子节点相对父节点的变换
*
* @param {MinderNode} node 需要布局的节点
*
* @example
*
* doLayout: function(node) {
* var children = node.getChildren();
* // layout calculation
* children[i].setLayoutTransform(new kity.Matrix().translate(x, y));
* }
*/
doLayout: function(parent, children) {
throw new Error('Not Implement: Layout.doLayout()');
},
/**
* 对齐指定的节点
*
* @param {Array<MinderNode>} nodes 要对齐的节点
* @param {string} border 对齐边界允许取值 left, right, top, bottom
*
*/
align: function(nodes, border, offset) {
var me = this;
offset = offset || 0;
nodes.forEach(function(node) {
var tbox = me.getTreeBox([node]);
var matrix = node.getLayoutTransform();
switch (border) {
case 'left':
return matrix.translate(offset - tbox.left, 0);
case 'right':
return matrix.translate(offset - tbox.right, 0);
case 'top':
return matrix.translate(0, offset - tbox.top);
case 'bottom':
return matrix.translate(0, offset - tbox.bottom);
}
});
},
stack: function(nodes, axis, distance) {
var me = this;
var position = 0;
distance = distance || function(node, next, axis) {
return node.getStyle({
x: 'margin-right',
y: 'margin-bottom'
}[axis]) + next.getStyle({
x: 'margin-left',
y: 'margin-top'
}[axis]);
};
nodes.forEach(function(node, index, nodes) {
var tbox = me.getTreeBox([node]);
var size = {
x: tbox.width,
y: tbox.height
}[axis];
var offset = {
x: tbox.left,
y: tbox.top
}[axis];
var matrix = node.getLayoutTransform();
if (axis == 'x') {
matrix.translate(position - offset, 0);
} else {
matrix.translate(0, position - offset);
}
position += size;
if (nodes[index + 1])
position += distance(node, nodes[index + 1], axis);
});
return position;
},
move: function(nodes, dx, dy) {
nodes.forEach(function(node) {
node.getLayoutTransform().translate(dx, dy);
});
},
/**
* 工具方法获取给点的节点所占的布局区域
*
* @param {MinderNode[]} nodes 需要计算的节点
*
* @return {Box} 计算结果
*/
getBranchBox: function(nodes) {
var box = new kity.Box();
var i, node, matrix, contentBox;
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
matrix = node.getLayoutTransform();
contentBox = node.getContentBox();
box = box.merge(matrix.transformBox(contentBox));
}
return box;
},
/**
* 工具方法计算给定的节点的子树所占的布局区域
*
* @param {MinderNode} nodes 需要计算的节点
*
* @return {Box} 计算的结果
*/
getTreeBox: function(nodes) {
var i, node, matrix, treeBox;
var box = new kity.Box();
if (!(nodes instanceof Array)) nodes = [nodes];
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
matrix = node.getLayoutTransform();
treeBox = node.getContentBox();
if (node.isExpanded() && node.children.length) {
treeBox = treeBox.merge(this.getTreeBox(node.children));
}
box = box.merge(matrix.transformBox(treeBox));
}
return box;
},
getOrderHint: function(node) {
return [];
}
});
Layout.register = register;
Minder.registerInitHook(function(options) {
this.refresh();
});
/**
* 布局支持池子管理
*/
utils.extend(Minder, {
getLayoutList: function() {
return _layouts;
},
getLayoutInstance: function(name) {
var LayoutClass = _layouts[name];
if (!LayoutClass) throw new Error('Missing Layout: ' + name);
var layout = new LayoutClass();
return layout;
}
});
/**
* MinderNode 上的布局支持
*/
kity.extendClass(MinderNode, {
/**
* 获得当前节点的布局名称
*
* @return {String}
*/
getLayout: function() {
var layout = this.getData('layout');
layout = layout || (this.isRoot() ? _defaultLayout : this.parent.getLayout());
return layout;
},
setLayout: function(name) {
if (name) {
if (name == 'inherit') {
this.setData('layout');
} else {
this.setData('layout', name);
}
}
return this;
},
layout: function(name) {
this.setLayout(name).getMinder().layout();
return this;
},
getLayoutInstance: function() {
return Minder.getLayoutInstance(this.getLayout());
},
getOrderHint: function(refer) {
return this.parent.getLayoutInstance().getOrderHint(this);
},
/**
* 获取当前节点相对于父节点的布局变换
*/
getLayoutTransform: function() {
return this._layoutTransform || new kity.Matrix();
},
/**
* 第一轮布局计算后获得的全局布局位置
*
* @return {[type]} [description]
*/
getGlobalLayoutTransformPreview: function() {
var pMatrix = this.parent ? this.parent.getLayoutTransform() : new kity.Matrix();
var matrix = this.getLayoutTransform();
var offset = this.getLayoutOffset();
if (offset) {
matrix = matrix.clone().translate(offset.x, offset.y);
}
return pMatrix.merge(matrix);
},
getLayoutPointPreview: function() {
return this.getGlobalLayoutTransformPreview().transformPoint(new kity.Point());
},
/**
* 获取节点相对于全局的布局变换
*/
getGlobalLayoutTransform: function() {
if (this._globalLayoutTransform) {
return this._globalLayoutTransform;
} else if (this.parent) {
return this.parent.getGlobalLayoutTransform();
} else {
return new kity.Matrix();
}
},
/**
* 设置当前节点相对于父节点的布局变换
*/
setLayoutTransform: function(matrix) {
this._layoutTransform = matrix;
return this;
},
/**
* 设置当前节点相对于全局的布局变换冗余优化
*/
setGlobalLayoutTransform: function(matrix) {
this.getRenderContainer().setMatrix(this._globalLayoutTransform = matrix);
return this;
},
setVertexIn: function(p) {
this._vertexIn = p;
},
setVertexOut: function(p) {
this._vertexOut = p;
},
getVertexIn: function() {
return this._vertexIn || new kity.Point();
},
getVertexOut: function() {
return this._vertexOut || new kity.Point();
},
getLayoutVertexIn: function() {
return this.getGlobalLayoutTransform().transformPoint(this.getVertexIn());
},
getLayoutVertexOut: function() {
return this.getGlobalLayoutTransform().transformPoint(this.getVertexOut());
},
setLayoutVectorIn: function(v) {
this._layoutVectorIn = v;
return this;
},
setLayoutVectorOut: function(v) {
this._layoutVectorOut = v;
return this;
},
getLayoutVectorIn: function() {
return this._layoutVectorIn || new kity.Vector();
},
getLayoutVectorOut: function() {
return this._layoutVectorOut || new kity.Vector();
},
getLayoutBox: function() {
var matrix = this.getGlobalLayoutTransform();
return matrix.transformBox(this.getContentBox());
},
getLayoutPoint: function() {
var matrix = this.getGlobalLayoutTransform();
return matrix.transformPoint(new kity.Point());
},
getLayoutOffset: function() {
if (!this.parent) return new kity.Point();
// 影响当前节点位置的是父节点的布局
var data = this.getData('layout_' + this.parent.getLayout() + '_offset');
if (data) return new kity.Point(data.x, data.y);
return new kity.Point();
},
setLayoutOffset: function(p) {
if (!this.parent) return this;
this.setData('layout_' + this.parent.getLayout() + '_offset', p ? {
x: p.x,
y: p.y
} : undefined);
return this;
},
hasLayoutOffset: function() {
return !!this.getData('layout_' + this.parent.getLayout() + '_offset');
},
resetLayoutOffset: function() {
return this.setLayoutOffset(null);
},
getLayoutRoot: function() {
if (this.isLayoutRoot()) {
return this;
}
return this.parent.getLayoutRoot();
},
isLayoutRoot: function() {
return this.getData('layout') || this.isRoot();
}
});
/**
* Minder 上的布局支持
*/
kity.extendClass(Minder, {
layout: function() {
var duration = this.getOption('layoutAnimationDuration');
this.getRoot().traverse(function(node) {
// clear last results
node.setLayoutTransform(null);
});
function layoutNode(node, round) {
// layout all children first
// 剪枝:收起的节点无需计算
if (node.isExpanded() || true) {
node.children.forEach(function(child) {
layoutNode(child, round);
});
}
var layout = node.getLayoutInstance();
// var childrenInFlow = node.getChildren().filter(function(child) {
// return !child.hasLayoutOffset();
// });
layout.doLayout(node, node.getChildren(), round);
}
// 第一轮布局
layoutNode(this.getRoot(), 1);
// 第二轮布局
layoutNode(this.getRoot(), 2);
var minder = this;
this.applyLayoutResult(this.getRoot(), duration, function() {
/**
* 当节点>200, 不使用动画时, 此处逻辑变为同步逻辑, 外部minder.on事件无法
* 被提前录入, 因此增加setTimeout
* @author Naixor
*/
setTimeout(function () {
minder.fire('layoutallfinish');
}, 0);
});
return this.fire('layout');
},
refresh: function() {
this.getRoot().renderTree();
this.layout().fire('contentchange')._interactChange();
return this;
},
applyLayoutResult: function(root, duration, callback) {
root = root || this.getRoot();
var me = this;
var complex = root.getComplex();
function consume() {
if (!--complex) {
if (callback) {
callback();
}
}
}
// 节点复杂度大于 100关闭动画
if (complex > 200) duration = 0;
function applyMatrix(node, matrix) {
node.setGlobalLayoutTransform(matrix);
me.fire('layoutapply', {
node: node,
matrix: matrix
});
}
function apply(node, pMatrix) {
var matrix = node.getLayoutTransform().merge(pMatrix.clone());
var lastMatrix = node.getGlobalLayoutTransform() || new kity.Matrix();
var offset = node.getLayoutOffset();
matrix.translate(offset.x, offset.y);
matrix.m.e = Math.round(matrix.m.e);
matrix.m.f = Math.round(matrix.m.f);
// 如果当前有动画,停止动画
if (node._layoutTimeline) {
node._layoutTimeline.stop();
node._layoutTimeline = null;
}
// 如果要求以动画形式来更新,创建动画
if (duration) {
node._layoutTimeline = new kity.Animator(lastMatrix, matrix, applyMatrix)
.start(node, duration, 'ease')
.on('finish', function() {
//可能性能低的时候会丢帧,手动添加一帧
setTimeout(function() {
applyMatrix(node, matrix);
me.fire('layoutfinish', {
node: node,
matrix: matrix
});
consume();
}, 150);
});
}
// 否则直接更新
else {
applyMatrix(node, matrix);
me.fire('layoutfinish', {
node: node,
matrix: matrix
});
consume();
}
for (var i = 0; i < node.children.length; i++) {
apply(node.children[i], matrix);
}
}
apply(root, root.parent ? root.parent.getGlobalLayoutTransform() : new kity.Matrix());
return this;
}
});
module.exports = Layout;
});

View File

@ -0,0 +1,40 @@
/**
* @fileOverview
*
* KityMinder 暴露在 window 上的唯一变量
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var _initHooks = [];
var Minder = kity.createClass('Minder', {
constructor: function(options) {
this._options = utils.extend({}, options);
var initHooks = _initHooks.slice();
var initHook;
while (initHooks.length) {
initHook = initHooks.shift();
if (typeof(initHook) == 'function') {
initHook.call(this, this._options);
}
}
this.fire('finishInitHook');
}
});
Minder.version = '1.4.43';
Minder.registerInitHook = function(hook) {
_initHooks.push(hook);
};
module.exports = Minder;
});

View File

@ -0,0 +1,149 @@
define(function (require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
/* 已注册的模块 */
var _modules = {};
exports.register = function (name, module) {
_modules[name] = module;
};
/* 模块初始化 */
Minder.registerInitHook(function () {
this._initModules();
});
// 模块声明周期维护
kity.extendClass(Minder, {
_initModules: function () {
var modulesPool = _modules;
var modulesToLoad = this._options.modules || utils.keys(modulesPool);
this._commands = {};
this._query = {};
this._modules = {};
this._rendererClasses = {};
var i, name, type, module, moduleDeals, dealCommands, dealEvents, dealRenderers;
var me = this;
for (i = 0; i < modulesToLoad.length; i++) {
name = modulesToLoad[i];
if (!modulesPool[name]) continue;
// 执行模块初始化,抛出后续处理对象
if (typeof modulesPool[name] == 'function') {
moduleDeals = modulesPool[name].call(me);
} else {
moduleDeals = modulesPool[name];
}
this._modules[name] = moduleDeals;
if (!moduleDeals) continue;
if (moduleDeals.defaultOptions) {
me.setDefaultOptions(moduleDeals.defaultOptions);
}
if (moduleDeals.init) {
moduleDeals.init.call(me, this._options);
}
/**
* @Desc: 判断是否支持原生clipboard事件如果支持则对pager添加其监听
* @Editor: Naixor
* @Date: 2015.9.20
*/
/**
* 由于当前脑图解构问题clipboard暂时全权交由玩不托管
* @Editor: Naixor
* @Date: 2015.9.24
*/
// if (name === 'ClipboardModule' && this.supportClipboardEvent && !kity.Browser.gecko) {
// var on = function () {
// var clipBoardReceiver = this.clipBoardReceiver || document;
// if (document.addEventListener) {
// clipBoardReceiver.addEventListener.apply(this, arguments);
// } else {
// arguments[0] = 'on' + arguments[0];
// clipBoardReceiver.attachEvent.apply(this, arguments);
// }
// }
// for (var command in moduleDeals.clipBoardEvents) {
// on(command, moduleDeals.clipBoardEvents[command]);
// }
// };
// command加入命令池子
dealCommands = moduleDeals.commands;
for (name in dealCommands) {
this._commands[name.toLowerCase()] = new dealCommands[name]();
}
// 绑定事件
dealEvents = moduleDeals.events;
if (dealEvents) {
for (type in dealEvents) {
me.on(type, dealEvents[type]);
}
}
// 渲染器
dealRenderers = moduleDeals.renderers;
if (dealRenderers) {
for (type in dealRenderers) {
this._rendererClasses[type] = this._rendererClasses[type] || [];
if (utils.isArray(dealRenderers[type])) {
this._rendererClasses[type] = this._rendererClasses[type].concat(dealRenderers[type]);
} else {
this._rendererClasses[type].push(dealRenderers[type]);
}
}
}
//添加模块的快捷键
if (moduleDeals.commandShortcutKeys) {
this.addCommandShortcutKeys(moduleDeals.commandShortcutKeys);
}
}
},
_garbage: function () {
// this.clearSelect();
while (this._root.getChildren().length) {
this._root.removeChild(0);
}
},
destroy: function () {
var modules = this._modules;
this._resetEvents();
this._garbage();
for (var key in modules) {
if (!modules[key].destroy) continue;
modules[key].destroy.call(this);
}
},
reset: function () {
var modules = this._modules;
this._garbage();
for (var key in modules) {
if (!modules[key].reset) continue;
modules[key].reset.call(this);
}
},
});
});

View File

@ -0,0 +1,407 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
/**
* @class MinderNode
*
* 表示一个脑图节点
*/
var MinderNode = kity.createClass('MinderNode', {
/**
* 创建一个游离的脑图节点
*
* @param {String|Object} textOrData
* 节点的初始数据或文本
*/
constructor: function(textOrData) {
// 指针
this.parent = null;
this.root = this;
this.children = [];
// 数据
this.data = {
id: utils.guid(),
created: +new Date()
};
// 绘图容器
this.initContainers();
if (utils.isString(textOrData)) {
this.setText(textOrData);
} else if (utils.isObject(textOrData)) {
utils.extend(this.data, textOrData);
}
},
initContainers: function() {
this.rc = new kity.Group().setId(utils.uuid('minder_node'));
this.rc.minderNode = this;
},
/**
* 判断节点是否根节点
*/
isRoot: function() {
return this.root === this;
},
/**
* 判断节点是否叶子
*/
isLeaf: function() {
return this.children.length === 0;
},
/**
* 获取节点的根节点
*/
getRoot: function() {
return this.root || this;
},
/**
* 获得节点的父节点
*/
getParent: function() {
return this.parent;
},
getSiblings: function() {
var children = this.parent.children;
var siblings = [];
var self = this;
children.forEach(function(child) {
if (child != self) siblings.push(child);
});
return siblings;
},
/**
* 获得节点的深度
*/
getLevel: function() {
var level = 0,
ancestor = this.parent;
while (ancestor) {
level++;
ancestor = ancestor.parent;
}
return level;
},
/**
* 获得节点的复杂度即子树中节点的数量
*/
getComplex: function() {
var complex = 0;
this.traverse(function() {
complex++;
});
return complex;
},
/**
* 获得节点的类型root|main|sub
*/
getType: function(type) {
this.type = ['root', 'main', 'sub'][Math.min(this.getLevel(), 2)];
return this.type;
},
/**
* 判断当前节点是否被测试节点的祖先
* @param {MinderNode} test 被测试的节点
*/
isAncestorOf: function(test) {
var ancestor = test.parent;
while (ancestor) {
if (ancestor == this) return true;
ancestor = ancestor.parent;
}
return false;
},
getData: function(key) {
return key ? this.data[key] : this.data;
},
setData: function(key, value) {
if (typeof key == 'object') {
var data = key;
for (key in data) if (data.hasOwnProperty(key)) {
this.data[key] = data[key];
}
}
else {
this.data[key] = value;
}
return this;
},
/**
* 设置节点的文本数据
* @param {String} text 文本数据
*/
setText: function(text) {
return this.data.text = text;
},
/**
* 获取节点的文本数据
* @return {String}
*/
getText: function() {
return this.data.text || null;
},
/**
* 先序遍历当前节点树
* @param {Function} fn 遍历函数
*/
preTraverse: function(fn, excludeThis) {
var children = this.getChildren();
if (!excludeThis) fn(this);
for (var i = 0; i < children.length; i++) {
children[i].preTraverse(fn);
}
},
/**
* 后序遍历当前节点树
* @param {Function} fn 遍历函数
*/
postTraverse: function(fn, excludeThis) {
var children = this.getChildren();
for (var i = 0; i < children.length; i++) {
children[i].postTraverse(fn);
}
if (!excludeThis) fn(this);
},
traverse: function(fn, excludeThis) {
return this.postTraverse(fn, excludeThis);
},
getChildren: function() {
return this.children;
},
getIndex: function() {
return this.parent ? this.parent.children.indexOf(this) : -1;
},
insertChild: function(node, index) {
if (index === undefined) {
index = this.children.length;
}
if (node.parent) {
node.parent.removeChild(node);
}
node.parent = this;
node.root = this.root;
this.children.splice(index, 0, node);
},
appendChild: function(node) {
return this.insertChild(node);
},
prependChild: function(node) {
return this.insertChild(node, 0);
},
removeChild: function(elem) {
var index = elem,
removed;
if (elem instanceof MinderNode) {
index = this.children.indexOf(elem);
}
if (index >= 0) {
removed = this.children.splice(index, 1)[0];
removed.parent = null;
removed.root = removed;
}
},
clearChildren: function() {
this.children = [];
},
getChild: function(index) {
return this.children[index];
},
getRenderContainer: function() {
return this.rc;
},
getCommonAncestor: function(node) {
return MinderNode.getCommonAncestor(this, node);
},
contains: function(node) {
return this == node || this.isAncestorOf(node);
},
clone: function() {
var cloned = new MinderNode();
cloned.data = utils.clone(this.data);
this.children.forEach(function(child) {
cloned.appendChild(child.clone());
});
return cloned;
},
compareTo: function(node) {
if (!utils.comparePlainObject(this.data, node.data)) return false;
if (!utils.comparePlainObject(this.temp, node.temp)) return false;
if (this.children.length != node.children.length) return false;
var i = 0;
while (this.children[i]) {
if (!this.children[i].compareTo(node.children[i])) return false;
i++;
}
return true;
},
getMinder: function() {
return this.getRoot().minder;
}
});
MinderNode.getCommonAncestor = function(nodeA, nodeB) {
if (nodeA instanceof Array) {
return MinderNode.getCommonAncestor.apply(this, nodeA);
}
switch (arguments.length) {
case 1:
return nodeA.parent || nodeA;
case 2:
if (nodeA.isAncestorOf(nodeB)) {
return nodeA;
}
if (nodeB.isAncestorOf(nodeA)) {
return nodeB;
}
var ancestor = nodeA.parent;
while (ancestor && !ancestor.isAncestorOf(nodeB)) {
ancestor = ancestor.parent;
}
return ancestor;
default:
return Array.prototype.reduce.call(arguments,
function(prev, current) {
return MinderNode.getCommonAncestor(prev, current);
},
nodeA
);
}
};
kity.extendClass(Minder, {
getRoot: function() {
return this._root;
},
setRoot: function(root) {
this._root = root;
root.minder = this;
},
getAllNode: function() {
var nodes = [];
this.getRoot().traverse(function(node) {
nodes.push(node);
});
return nodes;
},
getNodeById: function(id) {
return this.getNodesById([id])[0];
},
getNodesById: function(ids) {
var nodes = this.getAllNode();
var result = [];
nodes.forEach(function(node) {
if (ids.indexOf(node.getData('id')) != -1) {
result.push(node);
}
});
return result;
},
createNode: function(textOrData, parent, index) {
var node = new MinderNode(textOrData);
this.fire('nodecreate', {
node: node,
parent: parent,
index: index
});
this.appendNode(node, parent, index);
return node;
},
appendNode: function(node, parent, index) {
if (parent) parent.insertChild(node, index);
this.attachNode(node);
return this;
},
removeNode: function(node) {
if (node.parent) {
node.parent.removeChild(node);
this.detachNode(node);
this.fire('noderemove', {
node: node
});
}
},
attachNode: function(node) {
var rc = this.getRenderContainer();
node.traverse(function(current) {
current.attached = true;
rc.addShape(current.getRenderContainer());
});
rc.addShape(node.getRenderContainer());
this.fire('nodeattach', {
node: node
});
},
detachNode: function(node) {
var rc = this.getRenderContainer();
node.traverse(function(current) {
current.attached = false;
rc.removeShape(current.getRenderContainer());
});
this.fire('nodedetach', {
node: node
});
},
getMinderTitle: function() {
return this.getRoot().getText();
}
});
module.exports = MinderNode;
});

View File

@ -0,0 +1,34 @@
/**
* @fileOverview
*
* 提供脑图选项支持
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
Minder.registerInitHook(function(options) {
this._defaultOptions = {};
});
kity.extendClass(Minder, {
setDefaultOptions: function(options) {
utils.extend(this._defaultOptions, options);
return this;
},
getOption: function(key) {
if (key) {
return key in this._options ? this._options[key] : this._defaultOptions[key];
} else {
return utils.extend({}, this._defaultOptions, this._options);
}
},
setOption: function(key, value) {
this._options[key] = value;
}
});
});

View File

@ -0,0 +1,76 @@
/**
* @fileOverview
*
* 初始化渲染容器
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
Minder.registerInitHook(function() {
this._initPaper();
});
kity.extendClass(Minder, {
_initPaper: function() {
this._paper = new kity.Paper();
this._paper._minder = this;
this._paper.getNode().ondragstart = function(e) {
e.preventDefault();
};
this._paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)');
this._addRenderContainer();
this.setRoot(this.createNode());
if (this._options.renderTo) {
this.renderTo(this._options.renderTo);
}
},
_addRenderContainer: function() {
this._rc = new kity.Group().setId(utils.uuid('minder'));
this._paper.addShape(this._rc);
},
renderTo: function(target) {
if (typeof(target) == 'string') {
target = document.querySelector(target);
}
if (target) {
if (target.tagName.toLowerCase() == 'script') {
var newTarget = document.createElement('div');
newTarget.id = target.id;
newTarget.class = target.class;
target.parentNode.insertBefore(newTarget, target);
target.parentNode.removeChild(target);
target = newTarget;
}
target.classList.add('km-view');
this._paper.renderTo(this._renderTarget = target);
this._bindEvents();
this.fire('paperrender');
}
return this;
},
getRenderContainer: function() {
return this._rc;
},
getPaper: function() {
return this._paper;
},
getRenderTarget: function() {
return this._renderTarget;
},
});
});

View File

@ -0,0 +1,110 @@
/**
* @fileOverview
*
* 打补丁
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var Minder = require('./minder');
function insertNode(minder, info, parent, index) {
parent = minder.createNode(info.data, parent, index);
info.children.forEach(function(childInfo, index) {
insertNode(minder, childInfo, parent, index);
});
return parent;
}
function applyPatch(minder, patch) {
// patch.op - 操作,包括 remove, add, replace
// patch.path - 路径,如 '/root/children/1/data'
// patch.value - 数据,如 { text: "思路" }
var path = patch.path.split('/');
path.shift();
var changed = path.shift();
var node;
if (changed == 'root') {
var dataIndex = path.indexOf('data');
if (dataIndex > -1) {
changed = 'data';
var dataPath = path.splice(dataIndex + 1);
patch.dataPath = dataPath;
} else {
changed = 'node';
}
node = minder.getRoot();
var segment, index;
while (segment = path.shift()) {
if (segment == 'children') continue;
if (typeof index != 'undefined') node = node.getChild(index);
index = +segment;
}
patch.index = index;
patch.node = node;
}
var express = patch.express = [changed, patch.op].join('.');
switch (express) {
case 'theme.replace':
minder.useTheme(patch.value);
break;
case 'template.replace':
minder.useTemplate(patch.value);
break;
case 'node.add':
insertNode(minder, patch.value, patch.node, patch.index).renderTree();
minder.layout();
break;
case 'node.remove':
minder.removeNode(patch.node.getChild(patch.index));
minder.layout();
break;
case 'data.add':
case 'data.replace':
case 'data.remove':
var data = patch.node.data;
var field;
path = patch.dataPath.slice();
while (data && path.length > 1) {
field = path.shift();
if (field in data) {
data = data[field];
} else if (patch.op != 'remove') {
data = data[field] = {};
}
}
if (data) {
field = path.shift();
data[field] = patch.value;
}
if (field == 'expandState') {
node.renderTree();
} else {
node.render();
}
minder.layout();
}
minder.fire('patch', { 'patch' : patch } );
}
kity.extendClass(Minder, {
applyPatches: function(patches) {
for (var i = 0; i < patches.length; i++) {
applyPatch(this, patches[i]);
}
this.fire('contentchange');
return this;
}
});
});

View File

@ -0,0 +1,214 @@
define(function(require, exports, module) {
/*!
** Thenable -- Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
** Copyright (c) 2013-2014 Ralf S. Engelschall <http://engelschall.com>
** Licensed under The MIT License <http://opensource.org/licenses/MIT>
** Source-Code distributed on <http://github.com/rse/thenable>
*/
/* promise states [Promises/A+ 2.1] */
var STATE_PENDING = 0; /* [Promises/A+ 2.1.1] */
var STATE_FULFILLED = 1; /* [Promises/A+ 2.1.2] */
var STATE_REJECTED = 2; /* [Promises/A+ 2.1.3] */
/* promise object constructor */
var Promise = function(executor) {
/* optionally support non-constructor/plain-function call */
if (!(this instanceof Promise))
return new Promise(executor);
/* initialize object */
this.id = 'Thenable/1.0.7';
this.state = STATE_PENDING; /* initial state */
this.fulfillValue = undefined; /* initial value */ /* [Promises/A+ 1.3, 2.1.2.2] */
this.rejectReason = undefined; /* initial reason */ /* [Promises/A+ 1.5, 2.1.3.2] */
this.onFulfilled = []; /* initial handlers */
this.onRejected = []; /* initial handlers */
/* support optional executor function */
if (typeof executor === 'function')
executor.call(this, this.fulfill.bind(this), this.reject.bind(this));
};
/* Promise API methods */
Promise.prototype = {
/* promise resolving methods */
fulfill: function(value) { return deliver(this, STATE_FULFILLED, 'fulfillValue', value); },
reject: function(value) { return deliver(this, STATE_REJECTED, 'rejectReason', value); },
/* 'The then Method' [Promises/A+ 1.1, 1.2, 2.2] */
then: function(onFulfilled, onRejected) {
var curr = this;
var next = new Promise(); /* [Promises/A+ 2.2.7] */
curr.onFulfilled.push(
resolver(onFulfilled, next, 'fulfill')); /* [Promises/A+ 2.2.2/2.2.6] */
curr.onRejected.push(
resolver(onRejected, next, 'reject')); /* [Promises/A+ 2.2.3/2.2.6] */
execute(curr);
return next; /* [Promises/A+ 2.2.7, 3.3] */
}
};
Promise.all = function (arr) {
return new Promise(function(resolve, reject) {
var len = arr.length,
i = 0,
res = 0,
results = [];
if (len === 0) {
resolve(results);
}
while (i < len) {
arr[i].then(
function (result) {
results.push(result);
if (++res === len) {
resolve(results);
}
},
function (val) {
reject(val);
}
);
i++;
}
});
};
/* deliver an action */
var deliver = function(curr, state, name, value) {
if (curr.state === STATE_PENDING) {
curr.state = state; /* [Promises/A+ 2.1.2.1, 2.1.3.1] */
curr[name] = value; /* [Promises/A+ 2.1.2.2, 2.1.3.2] */
execute(curr);
}
return curr;
};
/* execute all handlers */
var execute = function(curr) {
if (curr.state === STATE_FULFILLED)
execute_handlers(curr, 'onFulfilled', curr.fulfillValue);
else if (curr.state === STATE_REJECTED)
execute_handlers(curr, 'onRejected', curr.rejectReason);
};
/* execute particular set of handlers */
var execute_handlers = function(curr, name, value) {
/* global process: true */
/* global setImmediate: true */
/* global setTimeout: true */
/* short-circuit processing */
if (curr[name].length === 0)
return;
/* iterate over all handlers, exactly once */
var handlers = curr[name];
curr[name] = []; /* [Promises/A+ 2.2.2.3, 2.2.3.3] */
var func = function() {
for (var i = 0; i < handlers.length; i++)
handlers[i](value); /* [Promises/A+ 2.2.5] */
};
/* execute procedure asynchronously */ /* [Promises/A+ 2.2.4, 3.1] */
if (typeof process === 'object' && typeof process.nextTick === 'function')
process.nextTick(func);
else if (typeof setImmediate === 'function')
setImmediate(func);
else
setTimeout(func, 0);
};
/* generate a resolver function */
var resolver = function(cb, next, method) {
return function(value) {
if (typeof cb !== 'function') /* [Promises/A+ 2.2.1, 2.2.7.3, 2.2.7.4] */
next[method].call(next, value); /* [Promises/A+ 2.2.7.3, 2.2.7.4] */
else {
var result;
try {
if (value instanceof Promise) {
result = value.then(cb);
}
else result = cb(value);
} /* [Promises/A+ 2.2.2.1, 2.2.3.1, 2.2.5, 3.2] */
catch (e) {
next.reject(e); /* [Promises/A+ 2.2.7.2] */
return;
}
resolve(next, result); /* [Promises/A+ 2.2.7.1] */
}
};
};
/* 'Promise Resolution Procedure' */ /* [Promises/A+ 2.3] */
var resolve = function(promise, x) {
/* sanity check arguments */ /* [Promises/A+ 2.3.1] */
if (promise === x) {
promise.reject(new TypeError('cannot resolve promise with itself'));
return;
}
/* surgically check for a 'then' method
(mainly to just call the 'getter' of 'then' only once) */
var then;
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
try { then = x.then; } /* [Promises/A+ 2.3.3.1, 3.5] */
catch (e) {
promise.reject(e); /* [Promises/A+ 2.3.3.2] */
return;
}
}
/* handle own Thenables [Promises/A+ 2.3.2]
and similar 'thenables' [Promises/A+ 2.3.3] */
if (typeof then === 'function') {
var resolved = false;
try {
/* call retrieved 'then' method */ /* [Promises/A+ 2.3.3.3] */
then.call(x,
/* resolvePromise */ /* [Promises/A+ 2.3.3.3.1] */
function(y) {
if (resolved) return; resolved = true; /* [Promises/A+ 2.3.3.3.3] */
if (y === x) /* [Promises/A+ 3.6] */
promise.reject(new TypeError('circular thenable chain'));
else
resolve(promise, y);
},
/* rejectPromise */ /* [Promises/A+ 2.3.3.3.2] */
function(r) {
if (resolved) return; resolved = true; /* [Promises/A+ 2.3.3.3.3] */
promise.reject(r);
}
);
}
catch (e) {
if (!resolved) /* [Promises/A+ 2.3.3.3.3] */
promise.reject(e); /* [Promises/A+ 2.3.3.3.4] */
}
return;
}
/* handle other values */
promise.fulfill(x); /* [Promises/A+ 2.3.4, 2.3.3.4] */
};
Promise.resolve = function(value) {
return new Promise(function(resolve) {
resolve(value);
});
};
Promise.reject = function(reason) {
return new Promise(function(resolve, reject) {
reject(reason);
});
};
/* export API */
module.exports = Promise;
});

View File

@ -0,0 +1,63 @@
/**
* @fileOverview
*
* 只读模式支持
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var Minder = require('./minder');
var MinderEvent = require('./event');
Minder.registerInitHook(function(options) {
if (options.readOnly) {
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;
};
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();
}
});
});

View File

@ -0,0 +1,261 @@
define(function(require, exports, module) {
var kity = require('./kity');
var Minder = require('./minder');
var MinderNode = require('./node');
var Renderer = kity.createClass('Renderer', {
constructor: function(node) {
this.node = node;
},
create: function(node) {
throw new Error('Not implement: Renderer.create()');
},
shouldRender: function(node) {
return true;
},
watchChange: function(data) {
var changed;
if (this.watchingData === undefined) {
changed = true;
} else if (this.watchingData != data) {
changed = true;
} else {
changed = false;
}
this.watchingData = data;
},
shouldDraw: function(node) {
return true;
},
update: function(shape, node, box) {
if (this.shouldDraw()) this.draw(shape, node);
return this.place(shape, node, box);
},
draw: function(shape, node) {
throw new Error('Not implement: Renderer.draw()');
},
place: function(shape, node, box) {
throw new Error('Not implement: Renderer.place()');
},
getRenderShape: function() {
return this._renderShape || null;
},
setRenderShape: function(shape) {
this._renderShape = shape;
}
});
function createMinderExtension() {
function createRendererForNode(node, registered) {
var renderers = [];
['center', 'left', 'right', 'top', 'bottom', 'outline', 'outside'].forEach(function(section) {
var before = 'before' + section;
var after = 'after' + section;
if (registered[before]) {
renderers = renderers.concat(registered[before]);
}
if (registered[section]) {
renderers = renderers.concat(registered[section]);
}
if (registered[after]) {
renderers = renderers.concat(registered[after]);
}
});
node._renderers = renderers.map(function(Renderer) {
return new Renderer(node);
});
}
return {
renderNodeBatch: function(nodes) {
var rendererClasses = this._rendererClasses;
var lastBoxes = [];
var rendererCount = 0;
var i, j, renderer, node;
if (!nodes.length) return;
for (j = 0; j < nodes.length; j++) {
node = nodes[j];
if (!node._renderers) {
createRendererForNode(node, rendererClasses);
}
node._contentBox = new kity.Box();
this.fire('beforerender', {
node: node
});
}
// 所有节点渲染器数量是一致的
rendererCount = nodes[0]._renderers.length;
for (i = 0; i < rendererCount; i++) {
// 获取延迟盒子数据
for (j = 0; j < nodes.length; j++) {
if (typeof(lastBoxes[j]) == 'function') {
lastBoxes[j] = lastBoxes[j]();
}
if (!(lastBoxes[j] instanceof kity.Box)) {
lastBoxes[j] = new kity.Box(lastBoxes[j]);
}
}
for (j = 0; j < nodes.length; j++) {
node = nodes[j];
renderer = node._renderers[i];
// 合并盒子
if (lastBoxes[j]) {
node._contentBox = node._contentBox.merge(lastBoxes[j]);
renderer.contentBox = lastBoxes[j];
}
// 判断当前上下文是否应该渲染
if (renderer.shouldRender(node)) {
// 应该渲染,但是渲染图形没创建过,需要创建
if (!renderer.getRenderShape()) {
renderer.setRenderShape(renderer.create(node));
if (renderer.bringToBack) {
node.getRenderContainer().prependShape(renderer.getRenderShape());
} else {
node.getRenderContainer().appendShape(renderer.getRenderShape());
}
}
// 强制让渲染图形显示
renderer.getRenderShape().setVisible(true);
// 更新渲染图形
lastBoxes[j] = renderer.update(renderer.getRenderShape(), node, node._contentBox);
}
// 如果不应该渲染,但是渲染图形创建过了,需要隐藏起来
else if (renderer.getRenderShape()) {
renderer.getRenderShape().setVisible(false);
lastBoxes[j] = null;
}
}
}
for (j = 0; j < nodes.length; j++) {
this.fire('noderender', {
node: nodes[j]
});
}
},
renderNode: function(node) {
var rendererClasses = this._rendererClasses;
var i, latestBox, renderer;
if (!node._renderers) {
createRendererForNode(node, rendererClasses);
}
this.fire('beforerender', {
node: node
});
node._contentBox = new kity.Box();
node._renderers.forEach(function(renderer) {
// 判断当前上下文是否应该渲染
if (renderer.shouldRender(node)) {
// 应该渲染,但是渲染图形没创建过,需要创建
if (!renderer.getRenderShape()) {
renderer.setRenderShape(renderer.create(node));
if (renderer.bringToBack) {
node.getRenderContainer().prependShape(renderer.getRenderShape());
} else {
node.getRenderContainer().appendShape(renderer.getRenderShape());
}
}
// 强制让渲染图形显示
renderer.getRenderShape().setVisible(true);
// 更新渲染图形
latestBox = renderer.update(renderer.getRenderShape(), node, node._contentBox);
if (typeof(latestBox) == 'function') latestBox = latestBox();
// 合并渲染区域
if (latestBox) {
node._contentBox = node._contentBox.merge(latestBox);
renderer.contentBox = latestBox;
}
}
// 如果不应该渲染,但是渲染图形创建过了,需要隐藏起来
else if (renderer.getRenderShape()) {
renderer.getRenderShape().setVisible(false);
}
});
this.fire('noderender', {
node: node
});
}
};
}
kity.extendClass(Minder, createMinderExtension());
kity.extendClass(MinderNode, {
render: function() {
if (!this.attached) return;
this.getMinder().renderNode(this);
return this;
},
renderTree: function() {
if (!this.attached) return;
var list = [];
this.traverse(function(node) {
list.push(node);
});
this.getMinder().renderNodeBatch(list);
return this;
},
getRenderer: function(type) {
var rs = this._renderers;
if (!rs) return null;
for (var i = 0; i < rs.length; i++) {
if (rs[i].getType() == type) return rs[i];
}
return null;
},
getContentBox: function() {
//if (!this._contentBox) this.render();
return this.parent && this.parent.isCollapsed() ? new kity.Box() : (this._contentBox || new kity.Box());
},
getRenderBox: function(rendererType, refer) {
var renderer = rendererType && this.getRenderer(rendererType);
var contentBox = renderer ? renderer.contentBox : this.getContentBox();
var ctm = kity.Matrix.getCTM(this.getRenderContainer(), refer || 'paper');
return ctm.transformBox(contentBox);
}
});
module.exports = Renderer;
});

View File

@ -0,0 +1,146 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
var MinderNode = require('./node');
Minder.registerInitHook(function() {
this._initSelection();
});
// 选区管理
kity.extendClass(Minder, {
_initSelection: function() {
this._selectedNodes = [];
},
renderChangedSelection: function(last) {
var current = this.getSelectedNodes();
var changed = [];
current.forEach(function(node) {
if (last.indexOf(node) == -1) {
changed.push(node);
}
});
last.forEach(function(node) {
if (current.indexOf(node) == -1) {
changed.push(node);
}
});
if (changed.length) {
this._interactChange();
this.fire('selectionchange');
}
while (changed.length) {
changed.shift().render();
}
},
getSelectedNodes: function() {
//不能克隆返回会对当前选区操作从而影响querycommand
return this._selectedNodes;
},
getSelectedNode: function() {
return this.getSelectedNodes()[0] || null;
},
removeAllSelectedNodes: function() {
var me = this;
var last = this._selectedNodes.splice(0);
this._selectedNodes = [];
this.renderChangedSelection(last);
return this.fire('selectionclear');
},
removeSelectedNodes: function(nodes) {
var me = this;
var last = this._selectedNodes.slice(0);
nodes = utils.isArray(nodes) ? nodes : [nodes];
nodes.forEach(function(node) {
var index;
if ((index = me._selectedNodes.indexOf(node)) === -1) return;
me._selectedNodes.splice(index, 1);
});
this.renderChangedSelection(last);
return this;
},
select: function(nodes, isSingleSelect) {
var lastSelect = this.getSelectedNodes().slice(0);
if (isSingleSelect) {
this._selectedNodes = [];
}
var me = this;
nodes = utils.isArray(nodes) ? nodes : [nodes];
nodes.forEach(function(node) {
if (me._selectedNodes.indexOf(node) !== -1) return;
me._selectedNodes.unshift(node);
});
this.renderChangedSelection(lastSelect);
return this;
},
selectById: function(ids, isSingleSelect) {
ids = utils.isArray(ids) ? ids : [ids];
var nodes = this.getNodesById(ids);
return this.select(nodes, isSingleSelect);
},
//当前选区中的节点在给定的节点范围内的保留选中状态,
//没在给定范围的取消选中,给定范围中的但没在当前选中范围的也做选中效果
toggleSelect: function(node) {
if (utils.isArray(node)) {
node.forEach(this.toggleSelect.bind(this));
} else {
if (node.isSelected()) this.removeSelectedNodes(node);
else this.select(node);
}
return this;
},
isSingleSelect: function() {
return this._selectedNodes.length == 1;
},
getSelectedAncestors: function(includeRoot) {
var nodes = this.getSelectedNodes().slice(0),
ancestors = [],
judge;
// 根节点不参与计算
var rootIndex = nodes.indexOf(this.getRoot());
if (~rootIndex && !includeRoot) {
nodes.splice(rootIndex, 1);
}
// 判断 nodes 列表中是否存在 judge 的祖先
function hasAncestor(nodes, judge) {
for (var i = nodes.length - 1; i >= 0; --i) {
if (nodes[i].isAncestorOf(judge)) return true;
}
return false;
}
// 按照拓扑排序
nodes.sort(function(node1, node2) {
return node1.getLevel() - node2.getLevel();
});
// 因为是拓扑有序的,所以只需往上查找
while ((judge = nodes.pop())) {
if (!hasAncestor(nodes, judge)) {
ancestors.push(judge);
}
}
return ancestors;
}
});
kity.extendClass(MinderNode, {
isSelected: function() {
var minder = this.getMinder();
return minder && minder.getSelectedNodes().indexOf(this) != -1;
}
});
});

View File

@ -0,0 +1,154 @@
/**
* @fileOverview
*
* 添加快捷键支持
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var keymap = require('./keymap');
var Minder = require('./minder');
var MinderEvent = require('./event');
/**
* 计算包含 meta 键的 keycode
*
* @param {String|KeyEvent} unknown
*/
function getMetaKeyCode(unknown) {
var CTRL_MASK = 0x1000;
var ALT_MASK = 0x2000;
var SHIFT_MASK = 0x4000;
var metaKeyCode = 0;
if (typeof(unknown) == 'string') {
// unknown as string
unknown.toLowerCase().split(/\+\s*/).forEach(function(name) {
switch(name) {
case 'ctrl':
case 'cmd':
metaKeyCode |= CTRL_MASK;
break;
case 'alt':
metaKeyCode |= ALT_MASK;
break;
case 'shift':
metaKeyCode |= SHIFT_MASK;
break;
default:
metaKeyCode |= keymap[name];
}
});
} else {
// unknown as key event
if (unknown.ctrlKey || unknown.metaKey) {
metaKeyCode |= CTRL_MASK;
}
if (unknown.altKey) {
metaKeyCode |= ALT_MASK;
}
if (unknown.shiftKey) {
metaKeyCode |= SHIFT_MASK;
}
metaKeyCode |= unknown.keyCode;
}
return metaKeyCode;
}
kity.extendClass(MinderEvent, {
isShortcutKey: function(keyCombine) {
var keyEvent = this.originEvent;
if (!keyEvent) return false;
return getMetaKeyCode(keyCombine) == getMetaKeyCode(keyEvent);
}
});
Minder.registerInitHook(function() {
this._initShortcutKey();
});
kity.extendClass(Minder, {
_initShortcutKey: function() {
this._bindShortcutKeys();
},
_bindShortcutKeys: function() {
var map = this._shortcutKeys = {};
var has = 'hasOwnProperty';
this.on('keydown', function(e) {
for (var keys in map) {
if (!map[has](keys)) continue;
if (e.isShortcutKey(keys)) {
var fn = map[keys];
if (fn.__statusCondition && fn.__statusCondition != this.getStatus()) return;
fn();
e.preventDefault();
}
}
});
},
addShortcut: function(keys, fn) {
var binds = this._shortcutKeys;
keys.split(/\|\s*/).forEach(function(combine) {
var parts = combine.split('::');
var status;
if (parts.length > 1) {
combine = parts[1];
status = parts[0];
fn.__statusCondition = status;
}
binds[combine] = fn;
});
},
addCommandShortcutKeys: function(cmd, keys) {
var binds = this._commandShortcutKeys || (this._commandShortcutKeys = {});
var obj = {},
km = this;
if (keys) {
obj[cmd] = keys;
} else {
obj = cmd;
}
var minder = this;
utils.each(obj, function(keys, command) {
binds[command] = keys;
minder.addShortcut(keys, function execCommandByShortcut() {
/**
* 之前判断有问题 === 0 改为 !== -1
* @editor Naixor
* @Date 2015-12-2
*/
if (minder.queryCommandState(command) !== -1) {
minder.execCommand(command);
}
});
});
},
getCommandShortcutKey: function(cmd) {
var binds = this._commandShortcutKeys;
return binds && binds[cmd] || null;
},
/**
* @Desc: 添加一个判断是否支持原生Clipboard的变量用于对ctrl + v和ctrl + c的处理
* @Editor: Naixor
* @Date: 2015.9.20
*/
supportClipboardEvent: (function(window) {
return !!window.ClipboardEvent;
})(window)
});
});

View File

@ -0,0 +1,60 @@
/**
* @fileOverview
*
* 状态切换控制
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('./kity');
var Minder = require('./minder');
var sf = ~window.location.href.indexOf('status');
var tf = ~window.location.href.indexOf('trace');
Minder.registerInitHook(function() {
this._initStatus();
});
kity.extendClass(Minder, {
_initStatus: function() {
this._status = 'normal';
this._rollbackStatus = 'normal';
},
setStatus: function(status, force) {
// 在 readonly 模式下,只有 force 为 true 才能切换回来
if (this._status == 'readonly' && !force) return this;
if (status != this._status) {
this._rollbackStatus = this._status;
this._status = status;
this.fire('statuschange', {
lastStatus: this._rollbackStatus,
currentStatus: this._status
});
if (sf) {
/* global console: true */
console.log(window.event.type, this._rollbackStatus, '->', this._status);
if (tf) {
console.trace();
}
}
}
return this;
},
rollbackStatus: function() {
this.setStatus(this._rollbackStatus);
},
getRollbackStatus:function(){
return this._rollbackStatus;
},
getStatus: function() {
return this._status;
}
});
});

View File

@ -0,0 +1,101 @@
define(function (require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
var Command = require('./command');
var MinderNode = require('./node');
var Module = require('./module');
var _templates = {};
function register(name, supports) {
_templates[name] = supports;
}
exports.register = register;
utils.extend(Minder, {
getTemplateList: function () {
return _templates;
},
});
kity.extendClass(
Minder,
(function () {
var originGetTheme = Minder.prototype.getTheme;
return {
useTemplate: function (name, duration) {
this.setTemplate(name);
this.refresh(duration || 500);
},
getTemplate: function () {
return this._template || 'default';
},
setTemplate: function (name) {
this._template = name || null;
},
getTemplateSupport: function (method) {
var supports = _templates[this.getTemplate()];
return supports && supports[method];
},
getTheme: function (node) {
var support = this.getTemplateSupport('getTheme') || originGetTheme;
return support.call(this, node);
},
};
})()
);
kity.extendClass(
MinderNode,
(function () {
var originGetLayout = MinderNode.prototype.getLayout;
var originGetConnect = MinderNode.prototype.getConnect;
return {
getLayout: function () {
var support = this.getMinder().getTemplateSupport('getLayout') || originGetLayout;
return support.call(this, this);
},
getConnect: function () {
var support = this.getMinder().getTemplateSupport('getConnect') || originGetConnect;
return support.call(this, this);
},
};
})()
);
let timer = null;
Module.register('TemplateModule', {
/**
* @command Template
* @description 设置当前脑图的模板
* @param {string} name 模板名称
* 允许使用的模板可以使用 `kityminder.Minder.getTemplateList()` 查询
* @state
* 0: 始终可用
* @return 返回当前的模板名称
*/
commands: {
template: kity.createClass('TemplateCommand', {
base: Command,
execute: function (minder, name) {
minder.useTemplate(name);
clearTimeout(timer);
timer = setTimeout(() => {
minder.execCommand('camera');
}, 550);
},
queryValue: function (minder) {
return minder.getTemplate() || 'default';
},
}),
},
});
});

View File

@ -0,0 +1,175 @@
define(function(require, exports, module) {
var kity = require('./kity');
var utils = require('./utils');
var Minder = require('./minder');
var MinderNode = require('./node');
var Module = require('./module');
var Command = require('./command');
var cssLikeValueMatcher = {
left: function(value) {
return 3 in value && value[3] ||
1 in value && value[1] ||
value[0];
},
right: function(value) {
return 1 in value && value[1] || value[0];
},
top: function(value) {
return value[0];
},
bottom: function(value) {
return 2 in value && value[2] || value[0];
}
};
var _themes = {};
/**
* 注册一个主题
*
* @param {String} name 主题的名称
* @param {Plain} theme 主题的样式描述
*
* @example
* Minder.registerTheme('default', {
* 'root-color': 'red',
* 'root-stroke': 'none',
* 'root-padding': [10, 20]
* });
*/
function register(name, theme) {
_themes[name] = theme;
}
exports.register = register;
utils.extend(Minder, {
getThemeList: function() {
return _themes;
}
});
kity.extendClass(Minder, {
/**
* 切换脑图实例上的主题
* @param {String} name 要使用的主题的名称
*/
useTheme: function(name) {
this.setTheme(name);
this.refresh(800);
return true;
},
setTheme: function(name) {
if (name && !_themes[name]) throw new Error('Theme ' + name + ' not exists!');
var lastTheme = this._theme;
this._theme = name || null;
var container = this.getRenderTarget();
if (container) {
container.classList.remove('km-theme-' + lastTheme);
if (name) {
container.classList.add('km-theme-' + name);
}
container.style.background = this.getStyle('background');
}
this.fire('themechange', {
theme: name
});
return this;
},
/**
* 获取脑图实例上的当前主题
* @return {[type]} [description]
*/
getTheme: function(node) {
return this._theme || this.getOption('defaultTheme') || 'fresh-blue';
},
getThemeItems: function(node) {
var theme = this.getTheme(node);
return _themes[this.getTheme(node)];
},
/**
* 获得脑图实例上的样式
* @param {String} item 样式名称
*/
getStyle: function(item, node) {
var items = this.getThemeItems(node);
var segment, dir, selector, value, matcher;
if (item in items) return items[item];
// 尝试匹配 CSS 数组形式的值
// 比如 item 为 'pading-left'
// theme 里有 {'padding': [10, 20]} 的定义,则可以返回 20
segment = item.split('-');
if (segment.length < 2) return null;
dir = segment.pop();
item = segment.join('-');
if (item in items) {
value = items[item];
if (utils.isArray(value) && (matcher = cssLikeValueMatcher[dir])) {
return matcher(value);
}
if (!isNaN(value)) return value;
}
return null;
},
/**
* 获取指定节点的样式
* @param {String} name 样式名称可以不加节点类型的前缀
*/
getNodeStyle: function(node, name) {
var value = this.getStyle(node.getType() + '-' + name, node);
return value !== null ? value : this.getStyle(name, node);
}
});
kity.extendClass(MinderNode, {
getStyle: function(name) {
return this.getMinder().getNodeStyle(this, name);
}
});
Module.register('Theme', {
defaultOptions: {
defaultTheme: 'fresh-blue'
},
commands: {
/**
* @command Theme
* @description 设置当前脑图的主题
* @param {string} name 主题名称
* 允许使用的主题可以使用 `kityminder.Minder.getThemeList()` 查询
* @state
* 0: 始终可用
* @return 返回当前的主题名称
*/
'theme': kity.createClass('ThemeCommand', {
base: Command,
execute: function(km, name) {
return km.useTheme(name);
},
queryValue: function(km) {
return km.getTheme() || 'default';
}
})
}
});
Minder.registerInitHook(function() {
this.setTheme();
});
});

View File

@ -0,0 +1,65 @@
define(function(require, exports) {
var kity = require('./kity');
var uuidMap = {};
exports.extend = kity.Utils.extend.bind(kity.Utils);
exports.each = kity.Utils.each.bind(kity.Utils);
exports.uuid = function(group) {
uuidMap[group] = uuidMap[group] ? uuidMap[group] + 1 : 1;
return group + uuidMap[group];
};
exports.guid = function() {
return (+new Date() * 1e6 + Math.floor(Math.random() * 1e6)).toString(36);
};
exports.trim = function(str) {
return str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, '');
};
exports.keys = function(plain) {
var keys = [];
for (var key in plain) {
if (plain.hasOwnProperty(key)) {
keys.push(key);
}
}
return keys;
};
exports.clone = function(source) {
return JSON.parse(JSON.stringify(source));
};
exports.comparePlainObject = function(a, b) {
return JSON.stringify(a) == JSON.stringify(b);
};
exports.encodeHtml = function(str, reg) {
return str ? str.replace(reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp);)?/g, function(a, b) {
if (b) {
return a;
} else {
return {
'<': '&lt;',
'&': '&amp;',
'"': '&quot;',
'>': '&gt;',
'\'': '&#39;'
}[a];
}
}) : '';
};
exports.clearWhiteSpace = function(str) {
return str.replace(/[\u200b\t\r\n]/g, '');
};
exports.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object'], function(v) {
var toString = Object.prototype.toString;
exports['is' + v] = function(obj) {
return toString.apply(obj) == '[object ' + v + ']';
};
});
});

View File

@ -0,0 +1,3 @@
define('expose-kityminder', function(require, exports, module) {
module.exports = window.kityminder = require('./kityminder');
});

View File

@ -0,0 +1,106 @@
/**
* @fileOverview
*
* 默认导出全部模块
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function (require, exports, module) {
var kityminder = {
version: require('./core/minder').version,
};
// 核心导出,大写的部分导出类,小写的部分简单 require 一下
// 这里顺序是有讲究的,调整前先弄清楚依赖关系。
require('./core/utils');
kityminder.Minder = require('./core/minder');
kityminder.Command = require('./core/command');
kityminder.Node = require('./core/node');
require('./core/option');
require('./core/animate');
kityminder.Event = require('./core/event');
kityminder.data = require('./core/data');
require('./core/compatibility');
kityminder.KeyMap = require('./core/keymap');
require('./core/shortcut');
require('./core/status');
require('./core/paper');
require('./core/select');
require('./core/focus');
require('./core/keyreceiver');
kityminder.Module = require('./core/module');
require('./core/readonly');
kityminder.Render = require('./core/render');
kityminder.Connect = require('./core/connect');
kityminder.Layout = require('./core/layout');
kityminder.Theme = require('./core/theme');
kityminder.Template = require('./core/template');
kityminder.Promise = require('./core/promise');
require('./core/_boxv');
require('./core/patch');
// 模块依赖
require('./module/arrange');
require('./module/basestyle');
require('./module/clipboard');
require('./module/dragtree');
require('./module/expand');
require('./module/font');
require('./module/hyperlink');
require('./module/image');
require('./module/image-viewer');
require('./module/keynav');
require('./module/layout');
require('./module/node');
require('./module/note');
require('./module/outline');
require('./module/priority');
require('./module/progress');
require('./module/resource');
require('./module/select');
require('./module/style');
require('./module/text');
require('./module/view');
require('./module/zoom');
require('./protocol/json');
require('./protocol/text');
require('./protocol/markdown');
require('./protocol/svg');
require('./protocol/png');
require('./layout/mind');
require('./layout/btree');
require('./layout/filetree');
require('./layout/fish-bone-master');
require('./layout/fish-bone-slave');
require('./layout/tianpan');
require('./theme/default');
require('./theme/snow');
require('./theme/fresh');
require('./theme/fish');
require('./theme/snow');
require('./theme/wire');
require('./theme/tianpan');
require('./connect/arc');
require('./connect/arc_tp');
require('./connect/bezier');
require('./connect/fish-bone-master');
require('./connect/l');
require('./connect/poly');
require('./connect/under');
require('./template/default');
require('./template/structure');
require('./template/filetree');
require('./template/right');
require('./template/fish-bone');
require('./template/tianpan');
window.kityminder = kityminder;
module.exports = kityminder;
});

View File

@ -0,0 +1,143 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var Layout = require('../core/layout');
['left', 'right', 'top', 'bottom'].forEach(registerLayoutForDirection);
function registerLayoutForDirection(name) {
var axis = (name == 'left' || name == 'right') ? 'x' : 'y';
var dir = (name == 'left' || name == 'top') ? -1 : 1;
var oppsite = {
'left': 'right',
'right': 'left',
'top': 'bottom',
'bottom': 'top',
'x': 'y',
'y': 'x'
};
function getOrderHint(node) {
var hint = [];
var box = node.getLayoutBox();
var offset = 5;
if (axis == 'x') {
hint.push({
type: 'up',
node: node,
area: new kity.Box({
x: box.x,
y: box.top - node.getStyle('margin-top') - offset,
width: box.width,
height: node.getStyle('margin-top')
}),
path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
});
hint.push({
type: 'down',
node: node,
area: new kity.Box({
x: box.x,
y: box.bottom + offset,
width: box.width,
height: node.getStyle('margin-bottom')
}),
path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
});
} else {
hint.push({
type: 'up',
node: node,
area: new kity.Box({
x: box.left - node.getStyle('margin-left') - offset,
y: box.top,
width: node.getStyle('margin-left'),
height: box.height
}),
path: ['M', box.left - offset, box.top, 'L', box.left - offset, box.bottom]
});
hint.push({
type: 'down',
node: node,
area: new kity.Box({
x: box.right + offset,
y: box.top,
width: node.getStyle('margin-right'),
height: box.height
}),
path: ['M', box.right + offset, box.top, 'L', box.right + offset, box.bottom]
});
}
return hint;
}
Layout.register(name, kity.createClass({
base: Layout,
doLayout: function(parent, children) {
var pbox = parent.getContentBox();
if (axis == 'x') {
parent.setVertexOut(new kity.Point(pbox[name], pbox.cy));
parent.setLayoutVectorOut(new kity.Vector(dir, 0));
} else {
parent.setVertexOut(new kity.Point(pbox.cx, pbox[name]));
parent.setLayoutVectorOut(new kity.Vector(0, dir));
}
if (!children.length) {
return false;
}
children.forEach(function(child) {
var cbox = child.getContentBox();
child.setLayoutTransform(new kity.Matrix());
if (axis == 'x') {
child.setVertexIn(new kity.Point(cbox[oppsite[name]], cbox.cy));
child.setLayoutVectorIn(new kity.Vector(dir, 0));
} else {
child.setVertexIn(new kity.Point(cbox.cx, cbox[oppsite[name]]));
child.setLayoutVectorIn(new kity.Vector(0, dir));
}
});
this.align(children, oppsite[name]);
this.stack(children, oppsite[axis]);
var bbox = this.getBranchBox(children);
var xAdjust = 0, yAdjust = 0;
if (axis == 'x') {
xAdjust = pbox[name];
xAdjust += dir * parent.getStyle('margin-' + name);
xAdjust += dir * children[0].getStyle('margin-' + oppsite[name]);
yAdjust = pbox.bottom;
yAdjust -= pbox.height / 2;
yAdjust -= bbox.height / 2;
yAdjust -= bbox.y;
} else {
xAdjust = pbox.right;
xAdjust -= pbox.width / 2;
xAdjust -= bbox.width / 2;
xAdjust -= bbox.x;
yAdjust = pbox[name];
yAdjust += dir * parent.getStyle('margin-' + name);
yAdjust += dir * children[0].getStyle('margin-' + oppsite[name]);
}
this.move(children, xAdjust, yAdjust);
},
getOrderHint: getOrderHint
}));
}
});

View File

@ -0,0 +1,89 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var Layout = require('../core/layout');
[-1, 1].forEach(registerLayoutForDir);
function registerLayoutForDir(dir) {
var name = 'filetree-' + (dir > 0 ? 'down' : 'up');
Layout.register(name, kity.createClass({
base: Layout,
doLayout: function(parent, children, round) {
var pBox = parent.getContentBox();
var indent = 20;
parent.setVertexOut(new kity.Point(pBox.left + indent, dir > 0 ? pBox.bottom : pBox.top));
parent.setLayoutVectorOut(new kity.Vector(0, dir));
if (!children.length) return;
children.forEach(function(child) {
var cbox = child.getContentBox();
child.setLayoutTransform(new kity.Matrix());
child.setVertexIn(new kity.Point(cbox.left, cbox.cy));
child.setLayoutVectorIn(new kity.Vector(1, 0));
});
this.align(children, 'left');
this.stack(children, 'y');
var xAdjust = 0;
xAdjust += pBox.left;
xAdjust += indent;
xAdjust += children[0].getStyle('margin-left');
var yAdjust = 0;
if (dir > 0) {
yAdjust += pBox.bottom;
yAdjust += parent.getStyle('margin-bottom');
yAdjust += children[0].getStyle('margin-top');
} else {
yAdjust -= this.getTreeBox(children).bottom;
yAdjust += pBox.top;
yAdjust -= parent.getStyle('margin-top');
yAdjust -= children[0].getStyle('margin-bottom');
}
this.move(children, xAdjust, yAdjust);
},
getOrderHint: function(node) {
var hint = [];
var box = node.getLayoutBox();
var offset = node.getLevel() > 1 ? 3 : 5;
hint.push({
type: 'up',
node: node,
area: new kity.Box({
x: box.x,
y: box.top - node.getStyle('margin-top') - offset,
width: box.width,
height: node.getStyle('margin-top')
}),
path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
});
hint.push({
type: 'down',
node: node,
area: new kity.Box({
x: box.x,
y: box.bottom + offset,
width: box.width,
height: node.getStyle('margin-bottom')
}),
path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
});
return hint;
}
}));
}
});

View File

@ -0,0 +1,68 @@
/**
* @fileOverview
*
* 鱼骨图主骨架布局
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var Layout = require('../core/layout');
Layout.register('fish-bone-master', kity.createClass('FishBoneMasterLayout', {
base: Layout,
doLayout: function(parent, children, round) {
var upPart = [],
downPart = [];
var child = children[0];
var pBox = parent.getContentBox();
parent.setVertexOut(new kity.Point(pBox.right, pBox.cy));
parent.setLayoutVectorOut(new kity.Vector(1, 0));
if (!child) return;
var cBox = child.getContentBox();
var pMarginRight = parent.getStyle('margin-right');
var cMarginLeft = child.getStyle('margin-left');
var cMarginTop = child.getStyle('margin-top');
var cMarginBottom = child.getStyle('margin-bottom');
children.forEach(function(child, index) {
child.setLayoutTransform(new kity.Matrix());
var cBox = child.getContentBox();
if (index % 2) {
downPart.push(child);
child.setVertexIn(new kity.Point(cBox.left, cBox.top));
child.setLayoutVectorIn(new kity.Vector(1, 1));
}
else {
upPart.push(child);
child.setVertexIn(new kity.Point(cBox.left, cBox.bottom));
child.setLayoutVectorIn(new kity.Vector(1, -1));
}
});
this.stack(upPart, 'x');
this.stack(downPart, 'x');
this.align(upPart, 'bottom');
this.align(downPart, 'top');
var xAdjust = pBox.right + pMarginRight + cMarginLeft;
var yAdjustUp = pBox.cy - cMarginBottom - parent.getStyle('margin-top');
var yAdjustDown = pBox.cy + cMarginTop + parent.getStyle('margin-bottom');
this.move(upPart, xAdjust, yAdjustUp);
this.move(downPart, xAdjust + cMarginLeft, yAdjustDown);
}
}));
});

View File

@ -0,0 +1,72 @@
/**
* @fileOverview
*
*
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var Layout = require('../core/layout');
Layout.register('fish-bone-slave', kity.createClass('FishBoneSlaveLayout', {
base: Layout,
doLayout: function (parent, children, round) {
var layout = this;
var abs = Math.abs;
var GOLD_CUT = 1 - 0.618;
var pBox = parent.getContentBox();
var vi = parent.getLayoutVectorIn();
parent.setLayoutVectorOut(vi);
var goldX = pBox.left + pBox.width * GOLD_CUT;
var pout = new kity.Point(goldX, vi.y > 0 ? pBox.bottom : pBox.top);
parent.setVertexOut(pout);
var child = children[0];
if (!child) return;
var cBox = child.getContentBox();
children.forEach(function(child, index) {
child.setLayoutTransform(new kity.Matrix());
child.setLayoutVectorIn(new kity.Vector(1, 0));
child.setVertexIn(new kity.Point(cBox.left, cBox.cy));
});
this.stack(children, 'y');
this.align(children, 'left');
var xAdjust = 0, yAdjust = 0;
xAdjust += pout.x;
if (parent.getLayoutVectorOut().y < 0) {
yAdjust -= this.getTreeBox(children).bottom;
yAdjust += parent.getContentBox().top;
yAdjust -= parent.getStyle('margin-top');
yAdjust -= child.getStyle('margin-bottom');
} else {
yAdjust += parent.getContentBox().bottom;
yAdjust += parent.getStyle('margin-bottom');
yAdjust += child.getStyle('margin-top');
}
this.move(children, xAdjust, yAdjust);
if (round == 2) {
children.forEach(function(child) {
var m = child.getLayoutTransform();
var cbox = child.getContentBox();
var pin = m.transformPoint(new kity.Point(cbox.left, 0));
layout.move([child], abs(pin.y - pout.y), 0);
});
}
}
}));
});

View File

@ -0,0 +1,62 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var Layout = require('../core/layout');
var Minder = require('../core/minder');
Layout.register('mind', kity.createClass({
base: Layout,
doLayout: function(node, children) {
var layout = this;
var half = Math.ceil(node.children.length / 2);
var right = [];
var left = [];
children.forEach(function(child) {
if (child.getIndex() < half) right.push(child);
else left.push(child);
});
var leftLayout = Minder.getLayoutInstance('left');
var rightLayout = Minder.getLayoutInstance('right');
leftLayout.doLayout(node, left);
rightLayout.doLayout(node, right);
var box = node.getContentBox();
node.setVertexOut(new kity.Point(box.cx, box.cy));
node.setLayoutVectorOut(new kity.Vector(0, 0));
},
getOrderHint: function(node) {
var hint = [];
var box = node.getLayoutBox();
var offset = 5;
hint.push({
type: 'up',
node: node,
area: new kity.Box({
x: box.x,
y: box.top - node.getStyle('margin-top') - offset,
width: box.width,
height: node.getStyle('margin-top')
}),
path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
});
hint.push({
type: 'down',
node: node,
area: new kity.Box({
x: box.x,
y: box.bottom + offset,
width: box.width,
height: node.getStyle('margin-bottom')
}),
path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
});
return hint;
}
}));
});

View File

@ -0,0 +1,76 @@
/**
* @fileOverview
*
* 天盘模板
*
* @author: along
* @copyright: bpd729@163.com, 2015
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var Layout = require('../core/layout');
var Minder = require('../core/minder');
Layout.register('tianpan', kity.createClass({
base: Layout,
doLayout: function (parent, children) {
if (children.length == 0) return;
var layout = this;
var pbox = parent.getContentBox();
var x, y,box;
var _theta = 5;
var _r = Math.max(pbox.width, 50);
children.forEach(function (child, index) {
child.setLayoutTransform(new kity.Matrix());
box = layout.getTreeBox(child);
_r = Math.max(Math.max(box.width, box.height), _r);
})
_r = _r / 1.5 / Math.PI;
children.forEach(function (child, index) {
x = _r * (Math.cos(_theta) + Math.sin(_theta) * _theta);
y = _r * (Math.sin(_theta) - Math.cos(_theta) * _theta);
_theta += (0.9 - index * 0.02);
child.setLayoutVectorIn(new kity.Vector(1, 0));
child.setVertexIn(new kity.Point(pbox.cx, pbox.cy));
child.setLayoutTransform(new kity.Matrix());
layout.move([child], x, y);
});
},
getOrderHint: function (node) {
var hint = [];
var box = node.getLayoutBox();
var offset = 5;
hint.push({
type: 'up',
node: node,
area: {
x: box.x,
y: box.top - node.getStyle('margin-top') - offset,
width: box.width,
height: node.getStyle('margin-top')
},
path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
});
hint.push({
type: 'down',
node: node,
area: {
x: box.x,
y: box.bottom + offset,
width: box.width,
height: node.getStyle('margin-bottom')
},
path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
});
return hint;
}
}));
});

View File

@ -0,0 +1,156 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
kity.extendClass(MinderNode, {
arrange: function(index) {
var parent = this.parent;
if (!parent) return;
var sibling = parent.children;
if (index < 0 || index >= sibling.length) return;
sibling.splice(this.getIndex(), 1);
sibling.splice(index, 0, this);
return this;
}
});
function asc(nodeA, nodeB) {
return nodeA.getIndex() - nodeB.getIndex();
}
function desc(nodeA, nodeB) {
return -asc(nodeA, nodeB);
}
function canArrange(km) {
var selected = km.getSelectedNode();
return selected && selected.parent && selected.parent.children.length > 1;
}
/**
* @command ArrangeUp
* @description 向上调整选中节点的位置
* @shortcut Alt + Up
* @state
* 0: 当前选中了具有相同父亲的节点
* -1: 其它情况
*/
var ArrangeUpCommand = kity.createClass('ArrangeUpCommand', {
base: Command,
execute: function(km) {
var nodes = km.getSelectedNodes();
nodes.sort(asc);
var lastIndexes = nodes.map(function(node) {
return node.getIndex();
});
nodes.forEach(function(node, index) {
node.arrange(lastIndexes[index] - 1);
});
km.layout(300);
},
queryState: function(km) {
var selected = km.getSelectedNode();
return selected ? 0 : -1;
}
});
/**
* @command ArrangeDown
* @description 向下调整选中节点的位置
* @shortcut Alt + Down
* @state
* 0: 当前选中了具有相同父亲的节点
* -1: 其它情况
*/
var ArrangeDownCommand = kity.createClass('ArrangeUpCommand', {
base: Command,
execute: function(km) {
var nodes = km.getSelectedNodes();
nodes.sort(desc);
var lastIndexes = nodes.map(function(node) {
return node.getIndex();
});
nodes.forEach(function(node, index) {
node.arrange(lastIndexes[index] + 1);
});
km.layout(300);
},
queryState: function(km) {
var selected = km.getSelectedNode();
return selected ? 0 : -1;
}
});
/**
* @command Arrange
* @description 调整选中节点的位置
* @param {number} index 调整后节点的新位置
* @state
* 0: 当前选中了具有相同父亲的节点
* -1: 其它情况
*/
var ArrangeCommand = kity.createClass('ArrangeCommand', {
base: Command,
execute: function(km, index) {
var nodes = km.getSelectedNodes().slice();
if (!nodes.length) return;
var ancestor = MinderNode.getCommonAncestor(nodes);
if (ancestor != nodes[0].parent) return;
var indexed = nodes.map(function(node) {
return {
index: node.getIndex(),
node: node
};
});
var asc = Math.min.apply(Math, indexed.map(function(one) { return one.index; })) >= index;
indexed.sort(function(a, b) {
return asc ? (b.index - a.index) : (a.index - b.index);
});
indexed.forEach(function(one) {
one.node.arrange(index);
});
km.layout(300);
},
queryState: function(km) {
var selected = km.getSelectedNode();
return selected ? 0 : -1;
}
});
Module.register('ArrangeModule', {
commands: {
'arrangeup': ArrangeUpCommand,
'arrangedown': ArrangeDownCommand,
'arrange': ArrangeCommand
},
contextmenu: [{
command: 'arrangeup'
}, {
command: 'arrangedown'
}, {
divider: true
}],
commandShortcutKeys: {
'arrangeup': 'normal::alt+Up',
'arrangedown': 'normal::alt+Down'
}
});
});

View File

@ -0,0 +1,126 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var TextRenderer = require('./text');
Module.register('basestylemodule', function() {
var km = this;
function getNodeDataOrStyle(node, name) {
return node.getData(name) || node.getStyle(name);
}
TextRenderer.registerStyleHook(function(node, textGroup) {
var fontWeight = getNodeDataOrStyle(node,'font-weight');
var fontStyle = getNodeDataOrStyle(node, 'font-style');
var styleHash = [fontWeight, fontStyle].join('/');
textGroup.eachItem(function(index,item) {
item.setFont({
'weight': fontWeight,
'style': fontStyle
});
});
});
return {
'commands': {
/**
* @command Bold
* @description 加粗选中的节点
* @shortcut Ctrl + B
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* 1: 当前已选中的节点已加粗
*/
'bold': kity.createClass('boldCommand', {
base: Command,
execute: function(km) {
var nodes = km.getSelectedNodes();
if (this.queryState('bold') == 1) {
nodes.forEach(function(n) {
n.setData('font-weight').render();
});
} else {
nodes.forEach(function(n) {
n.setData('font-weight', 'bold').render();
});
}
km.layout();
},
queryState: function() {
var nodes = km.getSelectedNodes(),
result = 0;
if (nodes.length === 0) {
return -1;
}
nodes.forEach(function(n) {
if (n && n.getData('font-weight')) {
result = 1;
return false;
}
});
return result;
}
}),
/**
* @command Italic
* @description 加斜选中的节点
* @shortcut Ctrl + I
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* 1: 当前已选中的节点已加斜
*/
'italic': kity.createClass('italicCommand', {
base: Command,
execute: function(km) {
var nodes = km.getSelectedNodes();
if (this.queryState('italic') == 1) {
nodes.forEach(function(n) {
n.setData('font-style').render();
});
} else {
nodes.forEach(function(n) {
n.setData('font-style', 'italic').render();
});
}
km.layout();
},
queryState: function() {
var nodes = km.getSelectedNodes(),
result = 0;
if (nodes.length === 0) {
return -1;
}
nodes.forEach(function(n) {
if (n && n.getData('font-style')) {
result = 1;
return false;
}
});
return result;
}
})
},
commandShortcutKeys: {
'bold': 'ctrl+b', //bold
'italic': 'ctrl+i' //italic
}
};
});
});

View File

@ -0,0 +1,172 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
Module.register('ClipboardModule', function() {
var km = this,
_clipboardNodes = [],
_selectedNodes = [];
function appendChildNode(parent, child) {
_selectedNodes.push(child);
km.appendNode(child, parent);
child.render();
child.setLayoutOffset(null);
var children = child.children.map(function(node) {
return node.clone();
});
/*
* fixed bug: Modified on 2015.08.05
* 原因粘贴递归 append 时没有清空原来父节点的子节点而父节点被复制的时候是连同子节点一起复制过来的
* 解决办法增加了下面这一行代码
* by: @zhangbobell zhangbobell@163.com
*/
child.clearChildren();
for (var i = 0, ci;
(ci = children[i]); i++) {
appendChildNode(child, ci);
}
}
function sendToClipboard(nodes) {
if (!nodes.length) return;
nodes.sort(function(a, b) {
return a.getIndex() - b.getIndex();
});
_clipboardNodes = nodes.map(function(node) {
return node.clone();
});
}
/**
* @command Copy
* @description 复制当前选中的节点
* @shortcut Ctrl + C
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var CopyCommand = kity.createClass('CopyCommand', {
base: Command,
execute: function(km) {
sendToClipboard(km.getSelectedAncestors(true));
this.setContentChanged(false);
}
});
/**
* @command Cut
* @description 剪切当前选中的节点
* @shortcut Ctrl + X
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var CutCommand = kity.createClass('CutCommand', {
base: Command,
execute: function(km) {
var ancestors = km.getSelectedAncestors();
if (ancestors.length === 0) return;
sendToClipboard(ancestors);
km.select(MinderNode.getCommonAncestor(ancestors), true);
ancestors.slice().forEach(function(node) {
km.removeNode(node);
});
km.layout(300);
}
});
/**
* @command Paste
* @description 粘贴已复制的节点到每一个当前选中的节点上
* @shortcut Ctrl + V
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var PasteCommand = kity.createClass('PasteCommand', {
base: Command,
execute: function(km) {
if (_clipboardNodes.length) {
var nodes = km.getSelectedNodes();
if (!nodes.length) return;
for (var i = 0, ni; ni = _clipboardNodes[i]; i++) {
for (var j = 0, node; node = nodes[j]; j++) {
appendChildNode(node, ni.clone());
}
}
km.select(_selectedNodes, true);
_selectedNodes = [];
km.layout(300);
}
},
queryState: function(km) {
return km.getSelectedNode() ? 0 : -1;
}
});
/**
* @Desc: 若支持原生clipboadr事件则基于原生扩展否则使用km的基础事件只处理节点的粘贴复制
* @Editor: Naixor
* @Date: 2015.9.20
*/
if (km.supportClipboardEvent && !kity.Browser.gecko) {
var Copy = function (e) {
this.fire('beforeCopy', e);
}
var Cut = function (e) {
this.fire('beforeCut', e);
}
var Paste = function (e) {
this.fire('beforePaste', e);
}
return {
'commands': {
'copy': CopyCommand,
'cut': CutCommand,
'paste': PasteCommand
},
'clipBoardEvents': {
'copy': Copy.bind(km),
'cut': Cut.bind(km),
'paste': Paste.bind(km)
},
sendToClipboard: sendToClipboard
};
} else {
return {
'commands': {
'copy': CopyCommand,
'cut': CutCommand,
'paste': PasteCommand
},
'commandShortcutKeys': {
'copy': 'normal::ctrl+c|',
'cut': 'normal::ctrl+x',
'paste': 'normal::ctrl+v'
},
sendToClipboard: sendToClipboard
};
}
});
});

View File

@ -0,0 +1,405 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
// 矩形的变形动画定义
var MoveToParentCommand = kity.createClass('MoveToParentCommand', {
base: Command,
execute: function(minder, nodes, parent) {
var node;
for (var i = 0; i < nodes.length; i++) {
node = nodes[i];
if (node.parent) {
node.parent.removeChild(node);
parent.appendChild(node);
node.render();
}
}
parent.expand();
minder.select(nodes, true);
}
});
var DropHinter = kity.createClass('DropHinter', {
base: kity.Group,
constructor: function() {
this.callBase();
this.rect = new kity.Rect();
this.addShape(this.rect);
},
render: function(target) {
this.setVisible(!!target);
if (target) {
this.rect
.setBox(target.getLayoutBox())
.setRadius(target.getStyle('radius') || 0)
.stroke(
target.getStyle('drop-hint-color') || 'yellow',
target.getStyle('drop-hint-width') || 2
);
this.bringTop();
}
}
});
var OrderHinter = kity.createClass('OrderHinter', {
base: kity.Group,
constructor: function() {
this.callBase();
this.area = new kity.Rect();
this.path = new kity.Path();
this.addShapes([this.area, this.path]);
},
render: function(hint) {
this.setVisible(!!hint);
if (hint) {
this.area.setBox(hint.area);
this.area.fill(hint.node.getStyle('order-hint-area-color') || 'rgba(0, 255, 0, .5)');
this.path.setPathData(hint.path);
this.path.stroke(
hint.node.getStyle('order-hint-path-color') || '#0f0',
hint.node.getStyle('order-hint-path-width') || 1);
}
}
});
// 对拖动对象的一个替代盒子,控制整个拖放的逻辑,包括:
// 1. 从节点列表计算出拖动部分
// 2. 计算可以 drop 的节点,产生 drop 交互提示
var TreeDragger = kity.createClass('TreeDragger', {
constructor: function(minder) {
this._minder = minder;
this._dropHinter = new DropHinter();
this._orderHinter = new OrderHinter();
minder.getRenderContainer().addShapes([this._dropHinter, this._orderHinter]);
},
dragStart: function(position) {
// 只记录开始位置,不马上开启拖放模式
// 这个位置同时是拖放范围收缩时的焦点位置(中心)
this._startPosition = position;
},
dragMove: function(position) {
// 启动拖放模式需要最小的移动距离
var DRAG_MOVE_THRESHOLD = 10;
if (!this._startPosition) return;
var movement = kity.Vector.fromPoints(this._dragPosition || this._startPosition, position);
var minder = this._minder;
this._dragPosition = position;
if (!this._dragMode) {
// 判断拖放模式是否该启动
if (kity.Vector.fromPoints(this._dragPosition, this._startPosition).length() < DRAG_MOVE_THRESHOLD) {
return;
}
if (!this._enterDragMode()) {
return;
}
}
for (var i = 0; i < this._dragSources.length; i++) {
this._dragSources[i].setLayoutOffset(this._dragSources[i].getLayoutOffset().offset(movement));
minder.applyLayoutResult(this._dragSources[i]);
}
if (!this._dropTest()) {
this._orderTest();
} else {
this._renderOrderHint(this._orderSucceedHint = null);
}
},
dragEnd: function() {
this._startPosition = null;
this._dragPosition = null;
if (!this._dragMode) {
return;
}
this._fadeDragSources(1);
if (this._dropSucceedTarget) {
this._dragSources.forEach(function(source) {
source.setLayoutOffset(null);
});
this._minder.layout(-1);
this._minder.execCommand('movetoparent', this._dragSources, this._dropSucceedTarget);
} else if (this._orderSucceedHint) {
var hint = this._orderSucceedHint;
var index = hint.node.getIndex();
var sourceIndexes = this._dragSources.map(function(source) {
// 顺便干掉布局偏移
source.setLayoutOffset(null);
return source.getIndex();
});
var maxIndex = Math.max.apply(Math, sourceIndexes);
var minIndex = Math.min.apply(Math, sourceIndexes);
if (index < minIndex && hint.type == 'down') index++;
if (index > maxIndex && hint.type == 'up') index--;
hint.node.setLayoutOffset(null);
this._minder.execCommand('arrange', index);
this._renderOrderHint(null);
} else {
this._minder.fire('savescene');
}
this._minder.layout(300);
this._leaveDragMode();
this._minder.fire('contentchange');
},
// 进入拖放模式:
// 1. 计算拖放源和允许的拖放目标
// 2. 标记已启动
_enterDragMode: function() {
this._calcDragSources();
if (!this._dragSources.length) {
this._startPosition = null;
return false;
}
this._fadeDragSources(0.5);
this._calcDropTargets();
this._calcOrderHints();
this._dragMode = true;
this._minder.setStatus('dragtree');
return true;
},
// 从选中的节点计算拖放源
// 并不是所有选中的节点都作为拖放源,如果选中节点中存在 A 和 B
// 并且 A 是 B 的祖先,则 B 不作为拖放源
//
// 计算过程:
// 1. 将节点按照树高排序,排序后只可能是前面节点是后面节点的祖先
// 2. 从后往前枚举排序的结果,如果发现枚举目标之前存在其祖先,
// 则排除枚举目标作为拖放源,否则加入拖放源
_calcDragSources: function() {
this._dragSources = this._minder.getSelectedAncestors();
},
_fadeDragSources: function(opacity) {
var minder = this._minder;
this._dragSources.forEach(function(source) {
source.getRenderContainer().setOpacity(opacity, 200);
source.traverse(function(node) {
if (opacity < 1) {
minder.detachNode(node);
} else {
minder.attachNode(node);
}
}, true);
});
},
// 计算拖放目标可以释放的节点列表(释放意味着成为其子树),存在这条限制规则:
// - 不能拖放到拖放目标的子树上(允许拖放到自身,因为多选的情况下可以把其它节点加入)
//
// 1. 加入当前节点(初始为根节点)到允许列表
// 2. 对于当前节点的每一个子节点:
// (1) 如果是拖放目标的其中一个节点,忽略(整棵子树被剪枝)
// (2) 如果不是拖放目标之一,以当前子节点为当前节点,回到 1 计算
// 3. 返回允许列表
//
_calcDropTargets: function() {
function findAvailableParents(nodes, root) {
var availables = [],
i;
availables.push(root);
root.getChildren().forEach(function(test) {
for (i = 0; i < nodes.length; i++) {
if (nodes[i] == test) return;
}
availables = availables.concat(findAvailableParents(nodes, test));
});
return availables;
}
this._dropTargets = findAvailableParents(this._dragSources, this._minder.getRoot());
this._dropTargetBoxes = this._dropTargets.map(function(source) {
return source.getLayoutBox();
});
},
_calcOrderHints: function() {
var sources = this._dragSources;
var ancestor = MinderNode.getCommonAncestor(sources);
// 只有一个元素选中,公共祖先是其父
if (ancestor == sources[0]) ancestor = sources[0].parent;
if (sources.length === 0 || ancestor != sources[0].parent) {
this._orderHints = [];
return;
}
var siblings = ancestor.children;
this._orderHints = siblings.reduce(function(hint, sibling) {
if (sources.indexOf(sibling) == -1) {
hint = hint.concat(sibling.getOrderHint());
}
return hint;
}, []);
},
_leaveDragMode: function() {
this._dragMode = false;
this._dropSucceedTarget = null;
this._orderSucceedHint = null;
this._renderDropHint(null);
this._renderOrderHint(null);
this._minder.rollbackStatus();
},
_drawForDragMode: function() {
this._text.setContent(this._dragSources.length + ' items');
this._text.setPosition(this._startPosition.x, this._startPosition.y + 5);
this._minder.getRenderContainer().addShape(this);
},
/**
* 通过 judge 函数判断 targetBox sourceBox 的位置交叉关系
* @param targets -- 目标节点
* @param targetBoxMapper -- 目标节点与对应 Box 的映射关系
* @param judge -- 判断函数
* @returns {*}
* @private
*/
_boxTest: function(targets, targetBoxMapper, judge) {
var sourceBoxes = this._dragSources.map(function(source) {
return source.getLayoutBox();
});
var i, j, target, sourceBox, targetBox;
judge = judge || function(intersectBox, sourceBox, targetBox) {
return intersectBox && !intersectBox.isEmpty();
};
for (i = 0; i < targets.length; i++) {
target = targets[i];
targetBox = targetBoxMapper.call(this, target, i);
for (j = 0; j < sourceBoxes.length; j++) {
sourceBox = sourceBoxes[j];
var intersectBox = sourceBox.intersect(targetBox);
if (judge(intersectBox, sourceBox, targetBox)) {
return target;
}
}
}
return null;
},
_dropTest: function() {
this._dropSucceedTarget = this._boxTest(this._dropTargets, function(target, i) {
return this._dropTargetBoxes[i];
}, function(intersectBox, sourceBox, targetBox) {
function area(box) {
return box.width * box.height;
}
if (!intersectBox) return false;
/*
* Added by zhangbobell, 2015.9.8
*
* 增加了下面一行判断修复了循环比较中 targetBox 为折叠节点时intersetBox 面积为 0
* targetBox width height 均为 0
* 此时造成了满足以下的第二个条件而返回 true
* */
if (!area(intersectBox)) return false;
// 面积判断,交叉面积大于其中的一半
if (area(intersectBox) > 0.5 * Math.min(area(sourceBox), area(targetBox))) return true;
// 有一个边完全重合的情况,也认为两个是交叉的
if (intersectBox.width + 1 >= Math.min(sourceBox.width, targetBox.width)) return true;
if (intersectBox.height + 1 >= Math.min(sourceBox.height, targetBox.height)) return true;
return false;
});
this._renderDropHint(this._dropSucceedTarget);
return !!this._dropSucceedTarget;
},
_orderTest: function() {
this._orderSucceedHint = this._boxTest(this._orderHints, function(hint) {
return hint.area;
});
this._renderOrderHint(this._orderSucceedHint);
return !!this._orderSucceedHint;
},
_renderDropHint: function(target) {
this._dropHinter.render(target);
},
_renderOrderHint: function(hint) {
this._orderHinter.render(hint);
},
preventDragMove: function() {
this._startPosition = null;
}
});
Module.register('DragTree', function() {
var dragger;
return {
init: function() {
dragger = new TreeDragger(this);
window.addEventListener('mouseup', function() {
dragger.dragEnd();
});
},
events: {
'normal.mousedown inputready.mousedown': function(e) {
// 单选中根节点也不触发拖拽
if (e.originEvent.button) return;
if (e.getTargetNode() && e.getTargetNode() != this.getRoot()) {
dragger.dragStart(e.getPosition());
}
},
'normal.mousemove dragtree.mousemove': function(e) {
dragger.dragMove(e.getPosition());
},
'normal.mouseup dragtree.beforemouseup': function(e) {
dragger.dragEnd();
//e.stopPropagation();
e.preventDefault();
},
'statuschange': function(e) {
if (e.lastStatus == 'textedit' && e.currentStatus == 'normal') {
dragger.preventDragMove();
}
}
},
commands: {
'movetoparent': MoveToParentCommand
}
};
});
});

View File

@ -0,0 +1,293 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var keymap = require('../core/keymap');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var Renderer = require('../core/render');
Module.register('Expand', function() {
var minder = this;
var EXPAND_STATE_DATA = 'expandState',
STATE_EXPAND = 'expand',
STATE_COLLAPSE = 'collapse';
// 将展开的操作和状态读取接口拓展到 MinderNode 上
kity.extendClass(MinderNode, {
/**
* 展开节点
* @param {Policy} policy 展开的策略默认为 KEEP_STATE
*/
expand: function() {
this.setData(EXPAND_STATE_DATA, STATE_EXPAND);
return this;
},
/**
* 收起节点
*/
collapse: function() {
this.setData(EXPAND_STATE_DATA, STATE_COLLAPSE);
return this;
},
/**
* 判断节点当前的状态是否为展开
*/
isExpanded: function() {
var expanded = this.getData(EXPAND_STATE_DATA) !== STATE_COLLAPSE;
return expanded && (this.isRoot() || this.parent.isExpanded());
},
/**
* 判断节点当前的状态是否为收起
*/
isCollapsed: function() {
return !this.isExpanded();
}
});
/**
* @command Expand
* @description 展开当前选中的节点保证其可见
* @param {bool} justParents 是否只展开到父亲
* * `false` - 默认保证选中的节点以及其子树可见
* * `true` - 只保证选中的节点可见不展开其子树
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var ExpandCommand = kity.createClass('ExpandCommand', {
base: Command,
execute: function(km, justParents) {
var node = km.getSelectedNode();
if (!node) return;
if (justParents) {
node = node.parent;
}
while (node.parent) {
node.expand();
node = node.parent;
}
node.renderTree();
km.layout(100);
},
queryState: function(km) {
var node = km.getSelectedNode();
return node && !node.isRoot() && !node.isExpanded() ? 0 : -1;
}
});
/**
* @command ExpandToLevel
* @description 展开脑图到指定的层级
* @param {number} level 指定展开到的层级最少值为 1
* @state
* 0: 一直可用
*/
var ExpandToLevelCommand = kity.createClass('ExpandToLevelCommand', {
base: Command,
execute: function(km, level) {
km.getRoot().traverse(function(node) {
if (node.getLevel() < level) node.expand();
if (node.getLevel() == level && !node.isLeaf()) node.collapse();
});
km.refresh(100);
},
enableReadOnly: true
});
/**
* @command Collapse
* @description 收起当前节点的子树
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var CollapseCommand = kity.createClass('CollapseCommand', {
base: Command,
execute: function(km) {
var node = km.getSelectedNode();
if (!node) return;
node.collapse();
node.renderTree();
km.layout();
},
queryState: function(km) {
var node = km.getSelectedNode();
return node && !node.isRoot() && node.isExpanded() ? 0 : -1;
}
});
var Expander = kity.createClass('Expander', {
base: kity.Group,
constructor: function(node) {
this.callBase();
this.radius = 6;
this.outline = new kity.Circle(this.radius).stroke('gray').fill('white');
this.sign = new kity.Path().stroke('gray');
this.addShapes([this.outline, this.sign]);
this.initEvent(node);
this.setId(utils.uuid('node_expander'));
this.setStyle('cursor', 'pointer');
},
initEvent: function(node) {
this.on('mousedown', function(e) {
minder.select([node], true);
if (node.isExpanded()) {
node.collapse();
} else {
node.expand();
}
node.renderTree().getMinder().layout(100);
node.getMinder().fire('contentchange');
e.stopPropagation();
e.preventDefault();
});
this.on('dblclick click mouseup', function(e) {
e.stopPropagation();
e.preventDefault();
});
},
setState: function(state) {
if (state == 'hide') {
this.setVisible(false);
return;
}
this.setVisible(true);
var pathData = ['M', 1.5 - this.radius, 0, 'L', this.radius - 1.5, 0];
if (state == STATE_COLLAPSE) {
pathData.push(['M', 0, 1.5 - this.radius, 'L', 0, this.radius - 1.5]);
}
this.sign.setPathData(pathData);
}
});
var ExpanderRenderer = kity.createClass('ExpanderRenderer', {
base: Renderer,
create: function(node) {
if (node.isRoot()) return;
this.expander = new Expander(node);
node.getRenderContainer().prependShape(this.expander);
node.expanderRenderer = this;
this.node = node;
return this.expander;
},
shouldRender: function(node) {
return !node.isRoot();
},
update: function(expander, node, box) {
if (!node.parent) return;
var visible = node.parent.isExpanded();
expander.setState(visible && node.children.length ? node.getData(EXPAND_STATE_DATA) : 'hide');
var vector = node.getLayoutVectorIn().normalize(expander.radius + node.getStyle('stroke-width'));
var position = node.getVertexIn().offset(vector.reverse());
this.expander.setTranslate(position);
}
});
return {
commands: {
'expand': ExpandCommand,
'expandtolevel': ExpandToLevelCommand,
'collapse': CollapseCommand
},
events: {
'layoutapply': function(e) {
var r = e.node.getRenderer('ExpanderRenderer');
if (r.getRenderShape()) {
r.update(r.getRenderShape(), e.node);
}
},
'beforerender': function(e) {
var node = e.node;
var visible = !node.parent || node.parent.isExpanded();
var minder = this;
node.getRenderContainer().setVisible(visible);
if (!visible) e.stopPropagation();
},
'normal.keydown': function(e) {
if (this.getStatus() == 'textedit') return;
if (e.originEvent.keyCode == keymap['/']) {
var node = this.getSelectedNode();
if (!node || node == this.getRoot()) return;
var expanded = node.isExpanded();
this.getSelectedNodes().forEach(function(node) {
if (expanded) node.collapse();
else node.expand();
node.renderTree();
});
this.layout(100);
this.fire('contentchange');
e.preventDefault();
e.stopPropagationImmediately();
}
if (e.isShortcutKey('Alt+`')) {
this.execCommand('expandtolevel', 9999);
}
for (var i = 1; i < 6; i++) {
if (e.isShortcutKey('Alt+' + i)) {
this.execCommand('expandtolevel', i);
}
}
}
},
renderers: {
outside: ExpanderRenderer
},
contextmenu: [{
command: 'expandtoleaf',
query: function() {
return !minder.getSelectedNode();
},
fn: function(minder) {
minder.execCommand('expandtolevel', 9999);
}
}, {
command: 'expandtolevel1',
query: function() {
return !minder.getSelectedNode();
},
fn: function(minder) {
minder.execCommand('expandtolevel', 1);
}
}, {
command: 'expandtolevel2',
query: function() {
return !minder.getSelectedNode();
},
fn: function(minder) {
minder.execCommand('expandtolevel', 2);
}
},{
command: 'expandtolevel3',
query: function() {
return !minder.getSelectedNode();
},
fn: function(minder) {
minder.execCommand('expandtolevel', 3);
}
}, {
divider: true
}]
};
});
});

View File

@ -0,0 +1,159 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var TextRenderer = require('./text');
function getNodeDataOrStyle(node, name) {
return node.getData(name) || node.getStyle(name);
}
TextRenderer.registerStyleHook(function(node, textGroup) {
var dataColor = node.getData('color');
var selectedColor = node.getStyle('selected-color');
var styleColor = node.getStyle('color');
var foreColor = dataColor || (node.isSelected() && selectedColor ? selectedColor : styleColor);
var fontFamily = getNodeDataOrStyle(node, 'font-family');
var fontSize = getNodeDataOrStyle(node, 'font-size');
textGroup.fill(foreColor);
textGroup.eachItem(function(index, item) {
item.setFont({
'family': fontFamily,
'size': fontSize
});
});
});
Module.register('fontmodule', {
'commands': {
/**
* @command ForeColor
* @description 设置选中节点的字体颜色
* @param {string} color 表示颜色的字符串
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* @return 如果只有一个节点选中返回已选中节点的字体颜色否则返回 'mixed'
*/
'forecolor': kity.createClass('fontcolorCommand', {
base: Command,
execute: function(km, color) {
var nodes = km.getSelectedNodes();
nodes.forEach(function(n) {
n.setData('color', color);
n.render();
});
},
queryState: function(km) {
return km.getSelectedNodes().length === 0 ? -1 : 0;
},
queryValue: function(km) {
if (km.getSelectedNodes().length == 1) {
return km.getSelectedNodes()[0].getData('color');
}
return 'mixed';
}
}),
/**
* @command Background
* @description 设置选中节点的背景颜色
* @param {string} color 表示颜色的字符串
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* @return 如果只有一个节点选中返回已选中节点的背景颜色否则返回 'mixed'
*/
'background': kity.createClass('backgroudCommand', {
base: Command,
execute: function(km, color) {
var nodes = km.getSelectedNodes();
nodes.forEach(function(n) {
n.setData('background', color);
n.render();
});
},
queryState: function(km) {
return km.getSelectedNodes().length === 0 ? -1 : 0;
},
queryValue: function(km) {
if (km.getSelectedNodes().length == 1) {
return km.getSelectedNodes()[0].getData('background');
}
return 'mixed';
}
}),
/**
* @command FontFamily
* @description 设置选中节点的字体
* @param {string} family 表示字体的字符串
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* @return 返回首个选中节点的字体
*/
'fontfamily': kity.createClass('fontfamilyCommand', {
base: Command,
execute: function(km, family) {
var nodes = km.getSelectedNodes();
nodes.forEach(function(n) {
n.setData('font-family', family);
n.render();
km.layout();
});
},
queryState: function(km) {
return km.getSelectedNodes().length === 0 ? -1 : 0;
},
queryValue: function(km) {
var node = km.getSelectedNode();
if (node) return node.getData('font-family');
return null;
}
}),
/**
* @command FontSize
* @description 设置选中节点的字体大小
* @param {number} size 字体大小px
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* @return 返回首个选中节点的字体大小
*/
'fontsize': kity.createClass('fontsizeCommand', {
base: Command,
execute: function(km, size) {
var nodes = km.getSelectedNodes();
nodes.forEach(function(n) {
n.setData('font-size', size);
n.render();
km.layout(300);
});
},
queryState: function(km) {
return km.getSelectedNodes().length === 0 ? -1 : 0;
},
queryValue: function(km) {
var node = km.getSelectedNode();
if (node) return node.getData('font-size');
return null;
}
})
}
});
});

View File

@ -0,0 +1,127 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var Renderer = require('../core/render');
// jscs:disable maximumLineLength
var linkShapePath = 'M16.614,10.224h-1.278c-1.668,0-3.07-1.07-3.599-2.556h4.877c0.707,0,1.278-0.571,1.278-1.278V3.834 c0-0.707-0.571-1.278-1.278-1.278h-4.877C12.266,1.071,13.668,0,15.336,0h1.278c2.116,0,3.834,1.716,3.834,3.834V6.39 C20.448,8.508,18.73,10.224,16.614,10.224z M5.112,5.112c0-0.707,0.573-1.278,1.278-1.278h7.668c0.707,0,1.278,0.571,1.278,1.278 S14.765,6.39,14.058,6.39H6.39C5.685,6.39,5.112,5.819,5.112,5.112z M2.556,3.834V6.39c0,0.707,0.573,1.278,1.278,1.278h4.877 c-0.528,1.486-1.932,2.556-3.599,2.556H3.834C1.716,10.224,0,8.508,0,6.39V3.834C0,1.716,1.716,0,3.834,0h1.278 c1.667,0,3.071,1.071,3.599,2.556H3.834C3.129,2.556,2.556,3.127,2.556,3.834z';
Module.register('hyperlink',{
'commands': {
/**
* @command HyperLink
* @description 为选中的节点添加超链接
* @param {string} url 超链接的 URL设置为 null 移除
* @param {string} title 超链接的说明
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* @return 返回首个选中节点的超链接信息JSON 对象 `{url: url, title: title}`
*/
'hyperlink': kity.createClass('hyperlink', {
base: Command,
execute: function(km, url, title) {
var nodes = km.getSelectedNodes();
nodes.forEach(function(n) {
n.setData('hyperlink', url);
n.setData('hyperlinkTitle', url && title);
n.render();
});
km.layout();
},
queryState: function(km) {
var nodes = km.getSelectedNodes(),
result = 0;
if (nodes.length === 0) {
return -1;
}
nodes.forEach(function(n) {
if (n && n.getData('hyperlink')) {
result = 0;
return false;
}
});
return result;
},
queryValue: function(km) {
var node = km.getSelectedNode();
return {
url: node.getData('hyperlink'),
title: node.getData('hyperlinkTitle')
};
}
})
},
'renderers': {
right: kity.createClass('hyperlinkrender', {
base: Renderer,
create: function() {
var link = new kity.HyperLink();
var linkshape = new kity.Path();
var outline = new kity.Rect(24, 22, -2, -6, 4).fill('rgba(255, 255, 255, 0)');
linkshape.setPathData(linkShapePath).fill('#666');
link.addShape(outline);
link.addShape(linkshape);
link.setTarget('_blank');
link.setStyle('cursor', 'pointer');
link.on('mouseover', function() {
outline.fill('rgba(255, 255, 200, .8)');
}).on('mouseout', function() {
outline.fill('rgba(255, 255, 255, 0)');
});
return link;
},
shouldRender: function(node) {
return node.getData('hyperlink');
},
update: function(link, node, box) {
var href = node.getData('hyperlink');
link.setHref('#');
var allowed = ['^http:', '^https:', '^ftp:', '^mailto:'];
for (var i = 0; i < allowed.length; i++) {
var regex = new RegExp(allowed[i]);
if (regex.test(href)) {
link.setHref(href);
break;
}
}
var title = node.getData('hyperlinkTitle');
if (title) {
title = [title, '(', href, ')'].join('');
} else {
title = href;
}
link.node.setAttributeNS('http://www.w3.org/1999/xlink', 'title', title);
var spaceRight = node.getStyle('space-right');
link.setTranslate(box.right + spaceRight + 2, -5);
return new kity.Box({
x: box.right + spaceRight,
y: -11,
width: 24,
height: 22
});
}
})
}
});
});

View File

@ -0,0 +1,111 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var keymap = require('../core/keymap');
var Module = require('../core/module');
var Command = require('../core/command');
Module.register('ImageViewer', function() {
function createEl(name, classNames, children) {
var el = document.createElement(name);
addClass(el, classNames);
children && children.length && children.forEach(function (child) {
el.appendChild(child);
});
return el;
}
function on(el, event, handler) {
el.addEventListener(event, handler);
}
function addClass(el, classNames) {
classNames && classNames.split(' ').forEach(function (className) {
el.classList.add(className);
});
}
function removeClass(el, classNames) {
classNames && classNames.split(' ').forEach(function (className) {
el.classList.remove(className);
});
}
var ImageViewer = kity.createClass('ImageViewer', {
constructor: function () {
var btnClose = createEl('button', 'km-image-viewer-btn km-image-viewer-close');
var btnSource = createEl('button', 'km-image-viewer-btn km-image-viewer-source');
var image = this.image = createEl('img');
var toolbar = this.toolbar = createEl('div', 'km-image-viewer-toolbar', [btnSource, btnClose]);
var container = createEl('div', 'km-image-viewer-container', [image]);
var viewer = this.viewer = createEl('div', 'km-image-viewer', [toolbar, container]);
this.hotkeyHandler = this.hotkeyHandler.bind(this)
on(btnClose, 'click', this.close.bind(this));
on(btnSource, 'click', this.viewSource.bind(this));
on(image, 'click', this.zoomImage.bind(this));
on(viewer, 'contextmenu', this.toggleToolbar.bind(this));
on(document, 'keydown', this.hotkeyHandler);
},
dispose: function () {
this.close();
document.removeEventListener('remove', this.hotkeyHandler);
},
hotkeyHandler: function (e) {
if (!this.actived) {
return;
}
if (e.keyCode === keymap['esc']) {
this.close();
}
},
toggleToolbar: function (e) {
e && e.preventDefault();
this.toolbar.classList.toggle('hidden');
},
zoomImage: function (restore) {
var image = this.image;
if (typeof restore === 'boolean') {
restore && addClass(image, 'limited');
}
else {
image.classList.toggle('limited');
}
},
viewSource: function (src) {
window.open(this.image.src);
},
open: function (src) {
var input = document.querySelector('input');
if (input) {
input.focus();
input.blur();
}
this.image.src = src;
this.zoomImage(true);
document.body.appendChild(this.viewer);
this.actived = true;
},
close: function () {
this.image.src = '';
document.body.removeChild(this.viewer);
this.actived = false;
}
});
return {
init: function() {
this.viewer = new ImageViewer();
},
events: {
'normal.dblclick': function(e) {
var shape = e.kityEvent.targetShape
if (shape.__KityClassName === 'Image' && shape.url) {
this.viewer.open(shape.url);
}
}
}
};
});
});

View File

@ -0,0 +1,147 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var Renderer = require('../core/render');
Module.register('image', function() {
function loadImageSize(url, callback) {
var img = document.createElement('img');
img.onload = function() {
callback(img.width, img.height);
};
img.onerror = function() {
callback(null);
};
img.src = url;
}
function fitImageSize(width, height, maxWidth, maxHeight) {
var ratio = width / height,
fitRatio = maxWidth / maxHeight;
// 宽高比大于最大尺寸的宽高比,以宽度为标准适应
if (width > maxWidth && ratio > fitRatio) {
width = maxWidth;
height = width / ratio;
} else if (height > maxHeight) {
height = maxHeight;
width = height * ratio;
}
return {
width: width | 0,
height: height | 0
};
}
/**
* @command Image
* @description 为选中的节点添加图片
* @param {string} url 图片的 URL设置为 null 移除
* @param {string} title 图片的说明
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* @return 返回首个选中节点的图片信息JSON 对象 `{url: url, title: title}`
*/
var ImageCommand = kity.createClass('ImageCommand', {
base: Command,
execute: function(km, url, title) {
var nodes = km.getSelectedNodes();
loadImageSize(url, function(width, height) {
nodes.forEach(function(n) {
var size = fitImageSize(
width, height,
km.getOption('maxImageWidth'),
km.getOption('maxImageHeight'));
n.setData('image', url);
n.setData('imageTitle', url && title);
n.setData('imageSize', url && size);
n.render();
});
km.fire('saveScene');
km.layout(300);
});
},
queryState: function(km) {
var nodes = km.getSelectedNodes(),
result = 0;
if (nodes.length === 0) {
return -1;
}
nodes.forEach(function(n) {
if (n && n.getData('image')) {
result = 0;
return false;
}
});
return result;
},
queryValue: function(km) {
var node = km.getSelectedNode();
return {
url: node.getData('image'),
title: node.getData('imageTitle')
};
}
});
var ImageRenderer = kity.createClass('ImageRenderer', {
base: Renderer,
create: function(node) {
return new kity.Image(node.getData('image'));
},
shouldRender: function(node) {
return node.getData('image');
},
update: function(image, node, box) {
var url = node.getData('image');
var title = node.getData('imageTitle');
var size = node.getData('imageSize');
var spaceTop = node.getStyle('space-top');
if (!size) return;
if (title) {
image.node.setAttributeNS('http://www.w3.org/1999/xlink', 'title', title);
}
var x = box.cx - size.width / 2;
var y = box.y - size.height - spaceTop;
image
.setUrl(url)
.setX(x | 0)
.setY(y | 0)
.setWidth(size.width | 0)
.setHeight(size.height | 0);
return new kity.Box(x | 0, y | 0, size.width | 0, size.height | 0);
}
});
return {
'defaultOptions': {
'maxImageWidth': 200,
'maxImageHeight': 200
},
'commands': {
'image': ImageCommand
},
'renderers': {
'top': ImageRenderer
}
};
});
});

View File

@ -0,0 +1,171 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var keymap = require('../core/keymap');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var Renderer = require('../core/render');
Module.register('KeyboardModule', function() {
var min = Math.min,
max = Math.max,
abs = Math.abs,
sqrt = Math.sqrt,
exp = Math.exp;
function buildPositionNetwork(root) {
var pointIndexes = [],
p;
root.traverse(function(node) {
p = node.getLayoutBox();
// bugfix: 不应导航到收起的节点(判断其尺寸是否存在)
if (p.width && p.height) {
pointIndexes.push({
left: p.x,
top: p.y,
right: p.x + p.width,
bottom: p.y + p.height,
width: p.width,
height: p.height,
node: node
});
}
});
for (var i = 0; i < pointIndexes.length; i++) {
findClosestPointsFor(pointIndexes, i);
}
}
// 这是金泉的点子,赞!
// 求两个不相交矩形的最近距离
function getCoefedDistance(box1, box2) {
var xMin, xMax, yMin, yMax, xDist, yDist, dist, cx, cy;
xMin = min(box1.left, box2.left);
xMax = max(box1.right, box2.right);
yMin = min(box1.top, box2.top);
yMax = max(box1.bottom, box2.bottom);
xDist = xMax - xMin - box1.width - box2.width;
yDist = yMax - yMin - box1.height - box2.height;
if (xDist < 0) dist = yDist;
else if (yDist < 0) dist = xDist;
else dist = sqrt(xDist * xDist + yDist * yDist);
var node1 = box1.node;
var node2 = box2.node;
// sibling
if (node1.parent == node2.parent) {
dist /= 10;
}
// parent
if (node2.parent == node1) {
dist /= 5;
}
return dist;
}
function findClosestPointsFor(pointIndexes, iFind) {
var find = pointIndexes[iFind];
var most = {},
quad;
var current, dist;
for (var i = 0; i < pointIndexes.length; i++) {
if (i == iFind) continue;
current = pointIndexes[i];
dist = getCoefedDistance(current, find);
// left check
if (current.right < find.left) {
if (!most.left || dist < most.left.dist) {
most.left = {
dist: dist,
node: current.node
};
}
}
// right check
if (current.left > find.right) {
if (!most.right || dist < most.right.dist) {
most.right = {
dist: dist,
node: current.node
};
}
}
// top check
if (current.bottom < find.top) {
if (!most.top || dist < most.top.dist) {
most.top = {
dist: dist,
node: current.node
};
}
}
// bottom check
if (current.top > find.bottom) {
if (!most.down || dist < most.down.dist) {
most.down = {
dist: dist,
node: current.node
};
}
}
}
find.node._nearestNodes = {
right: most.right && most.right.node || null,
top: most.top && most.top.node || null,
left: most.left && most.left.node || null,
down: most.down && most.down.node || null
};
}
function navigateTo(km, direction) {
var referNode = km.getSelectedNode();
if (!referNode) {
km.select(km.getRoot());
buildPositionNetwork(km.getRoot());
return;
}
if (!referNode._nearestNodes) {
buildPositionNetwork(km.getRoot());
}
var nextNode = referNode._nearestNodes[direction];
if (nextNode) {
km.select(nextNode, true);
}
}
// 稀释用
var lastFrame;
return {
'events': {
'layoutallfinish': function() {
var root = this.getRoot();
buildPositionNetwork(root);
},
'normal.keydown readonly.keydown': function(e) {
var minder = this;
['left', 'right', 'up', 'down'].forEach(function(key) {
if (e.isShortcutKey(key)) {
navigateTo(minder, key == 'up' ? 'top' : key);
e.preventDefault();
}
});
}
}
};
});
});

View File

@ -0,0 +1,92 @@
/**
* @fileOverview
*
* 布局模块
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var Command = require('../core/command');
var Module = require('../core/module');
/**
* @command Layout
* @description 设置选中节点的布局
* 允许使用的布局可以使用 `kityminder.Minder.getLayoutList()` 查询
* @param {string} name 布局的名称设置为 null 则使用继承或默认的布局
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
* @return 返回首个选中节点的布局名称
*/
var LayoutCommand = kity.createClass('LayoutCommand', {
base: Command,
execute: function(minder, name) {
var nodes = minder.getSelectedNodes();
nodes.forEach(function(node) {
node.layout(name);
});
},
queryValue: function(minder) {
var node = minder.getSelectedNode();
if (node) {
return node.getData('layout');
}
},
queryState: function(minder) {
return minder.getSelectedNode() ? 0 : -1;
}
});
/**
* @command ResetLayout
* @description 重设选中节点的布局如果当前没有选中的节点重设整个脑图的布局
* @state
* 0: 始终可用
* @return 返回首个选中节点的布局名称
*/
var ResetLayoutCommand = kity.createClass('ResetLayoutCommand', {
base: Command,
execute: function(minder) {
var nodes = minder.getSelectedNodes();
if (!nodes.length) nodes = [minder.getRoot()];
nodes.forEach(function(node) {
node.traverse(function(child) {
child.resetLayoutOffset();
if (!child.isRoot()) {
child.setData('layout', null);
}
});
});
minder.layout(300);
},
enableReadOnly: true
});
Module.register('LayoutModule', {
commands: {
'layout': LayoutCommand,
'resetlayout': ResetLayoutCommand
},
contextmenu: [{
command: 'resetlayout'
}, {
divider: true
}],
commandShortcutKeys: {
'resetlayout': 'Ctrl+Shift+L'
}
});
});

View File

@ -0,0 +1,150 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var Renderer = require('../core/render');
/**
* @command AppendChildNode
* @description 添加子节点到选中的节点中
* @param {string|object} textOrData 要插入的节点的文本或数据
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var AppendChildCommand = kity.createClass('AppendChildCommand', {
base: Command,
execute: function(km, text) {
var parent = km.getSelectedNode();
if (!parent) {
return null;
}
var node = km.createNode(text, parent);
km.select(node, true);
if (parent.isExpanded()) {
node.render();
}
else {
parent.expand();
parent.renderTree();
}
km.layout(600);
},
queryState: function(km) {
var selectedNode = km.getSelectedNode();
return selectedNode ? 0 : -1;
}
});
/**
* @command AppendSiblingNode
* @description 添加选中的节点的兄弟节点
* @param {string|object} textOrData 要添加的节点的文本或数据
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var AppendSiblingCommand = kity.createClass('AppendSiblingCommand', {
base: Command,
execute: function(km, text) {
var sibling = km.getSelectedNode();
var parent = sibling.parent;
if (!parent) {
return km.execCommand('AppendChildNode', text);
}
var node = km.createNode(text, parent, sibling.getIndex() + 1);
node.setGlobalLayoutTransform(sibling.getGlobalLayoutTransform());
km.select(node, true);
node.render();
km.layout(600);
},
queryState: function(km) {
var selectedNode = km.getSelectedNode();
return selectedNode ? 0 : -1;
}
});
/**
* @command RemoveNode
* @description 移除选中的节点
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var RemoveNodeCommand = kity.createClass('RemoverNodeCommand', {
base: Command,
execute: function(km) {
var nodes = km.getSelectedNodes();
var ancestor = MinderNode.getCommonAncestor.apply(null, nodes);
var index = nodes[0].getIndex();
nodes.forEach(function(node) {
if (!node.isRoot()) km.removeNode(node);
});
if (nodes.length == 1) {
var selectBack = ancestor.children[index - 1] || ancestor.children[index];
km.select(selectBack || ancestor || km.getRoot(), true);
} else {
km.select(ancestor || km.getRoot(), true);
}
km.layout(600);
},
queryState: function(km) {
var selectedNode = km.getSelectedNode();
return selectedNode && !selectedNode.isRoot() ? 0 : -1;
}
});
var AppendParentCommand = kity.createClass('AppendParentCommand', {
base: Command,
execute: function(km, text) {
var nodes = km.getSelectedNodes();
nodes.sort(function(a, b) {
return a.getIndex() - b.getIndex();
});
var parent = nodes[0].parent;
var newParent = km.createNode(text, parent, nodes[0].getIndex());
nodes.forEach(function(node) {
newParent.appendChild(node);
});
newParent.setGlobalLayoutTransform(nodes[nodes.length >> 1].getGlobalLayoutTransform());
km.select(newParent, true);
km.layout(600);
},
queryState: function(km) {
var nodes = km.getSelectedNodes();
if (!nodes.length) return -1;
var parent = nodes[0].parent;
if (!parent) return -1;
for (var i = 1; i < nodes.length; i++) {
if (nodes[i].parent != parent) return -1;
}
return 0;
}
});
Module.register('NodeModule', function() {
return {
commands: {
'AppendChildNode': AppendChildCommand,
'AppendSiblingNode': AppendSiblingCommand,
'RemoveNode': RemoveNodeCommand,
'AppendParentNode': AppendParentCommand
},
'commandShortcutKeys': {
'appendsiblingnode': 'normal::Enter',
'appendchildnode': 'normal::Insert|Tab',
'appendparentnode': 'normal::Shift+Tab|normal::Shift+Insert',
'removenode': 'normal::Del|Backspace'
}
};
});
});

View File

@ -0,0 +1,116 @@
/**
* @fileOverview
*
* 支持节点详细信息HTML格式
*
* @author: techird
* @copyright: Baidu FEX, 2014
*/
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var Renderer = require('../core/render');
Module.register('NoteModule', function() {
var NOTE_PATH = 'M9,9H3V8h6L9,9L9,9z M9,7H3V6h6V7z M9,5H3V4h6V5z M8.5,11H2V2h8v7.5 M9,12l2-2V1H1v11';
/**
* @command Note
* @description 设置节点的备注信息
* @param {string} note 要设置的备注信息设置为 null 则移除备注信息
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var NoteCommand = kity.createClass('NoteCommand', {
base: Command,
execute: function(minder, note) {
var node = minder.getSelectedNode();
node.setData('note', note);
node.render();
node.getMinder().layout(300);
},
queryState: function(minder) {
return minder.getSelectedNodes().length === 1 ? 0 : -1;
},
queryValue: function(minder) {
var node = minder.getSelectedNode();
return node && node.getData('note');
}
});
var NoteIcon = kity.createClass('NoteIcon', {
base: kity.Group,
constructor: function() {
this.callBase();
this.width = 16;
this.height = 17;
this.rect = new kity.Rect(16, 17, 0.5, -8.5, 2).fill('transparent');
this.path = new kity.Path().setPathData(NOTE_PATH).setTranslate(2.5, -6.5);
this.addShapes([this.rect, this.path]);
this.on('mouseover', function() {
this.rect.fill('rgba(255, 255, 200, .8)');
}).on('mouseout', function() {
this.rect.fill('transparent');
});
this.setStyle('cursor', 'pointer');
}
});
var NoteIconRenderer = kity.createClass('NoteIconRenderer', {
base: Renderer,
create: function(node) {
var icon = new NoteIcon();
icon.on('mousedown', function(e) {
e.preventDefault();
node.getMinder().fire('editnoterequest');
});
icon.on('mouseover', function() {
node.getMinder().fire('shownoterequest', {node: node, icon: icon});
});
icon.on('mouseout', function() {
node.getMinder().fire('hidenoterequest', {node: node, icon: icon});
});
return icon;
},
shouldRender: function(node) {
return node.getData('note');
},
update: function(icon, node, box) {
var x = box.right + node.getStyle('space-left');
var y = box.cy;
icon.path.fill(node.getStyle('color'));
icon.setTranslate(x, y);
return new kity.Box(x, Math.round(y - icon.height / 2), icon.width, icon.height);
}
});
return {
renderers: {
right: NoteIconRenderer
},
commands: {
'note': NoteCommand
}
};
});
});

View File

@ -0,0 +1,165 @@
define(function(require, exports, module) {
var kity = require('../core/kity');
var utils = require('../core/utils');
var Minder = require('../core/minder');
var MinderNode = require('../core/node');
var Command = require('../core/command');
var Module = require('../core/module');
var Renderer = require('../core/render');
var OutlineRenderer = kity.createClass('OutlineRenderer', {
base: Renderer,
create: function(node) {
var outline = new kity.Rect()
.setId(utils.uuid('node_outline'));
this.bringToBack = true;
return outline;
},
update: function(outline, node, box) {
var shape = node.getStyle('shape');
var paddingLeft = node.getStyle('padding-left'),
paddingRight = node.getStyle('padding-right'),
paddingTop = node.getStyle('padding-top'),
paddingBottom = node.getStyle('padding-bottom');
var outlineBox = {
x: box.x - paddingLeft,
y: box.y - paddingTop,
width: box.width + paddingLeft + paddingRight,
height: box.height + paddingTop + paddingBottom
};
var radius = node.getStyle('radius');
// 天盘图圆形的情况
if (shape && shape == 'circle') {
var p = Math.pow;
var r = Math.round;
radius = r(Math.sqrt(p(outlineBox.width, 2) + p(outlineBox.height, 2)) / 2);
outlineBox.x = box.cx - radius;
outlineBox.y = box.cy - radius;
outlineBox.width = 2 * radius;
outlineBox.height = 2 * radius;
}
var prefix = node.isSelected() ? (node.getMinder().isFocused() ? 'selected-' : 'blur-selected-') : '';
outline
.setPosition(outlineBox.x, outlineBox.y)
.setSize(outlineBox.width, outlineBox.height)
.setRadius(radius)
.fill(node.getData('background') || node.getStyle(prefix + 'background') || node.getStyle('background'))
.stroke(node.getStyle(prefix + 'stroke' || node.getStyle('stroke')),
node.getStyle(prefix + 'stroke-width'));
return new kity.Box(outlineBox);
}
});
var ShadowRenderer = kity.createClass('ShadowRenderer', {
base: Renderer,
create: function(node) {
this.bringToBack = true;
return new kity.Rect();
},
shouldRender: function(node) {
return node.getStyle('shadow');
},
update: function(shadow, node, box) {
shadow.setPosition(box.x + 4, box.y + 5)
.fill(node.getStyle('shadow'));
var shape = node.getStyle('shape');
if(!shape){
shadow.setSize(box.width, box.height);
shadow.setRadius(node.getStyle('radius'));
}else if(shape=='circle'){
var width= Math.max(box.width,box.height);
shadow.setSize(width, width);
shadow.setRadius(width/2);
}
}
});
var marker = new kity.Marker();
marker.setWidth(10);
marker.setHeight(12);
marker.setRef(0, 0);
marker.setViewBox(-6, -4, 8, 10);
marker.addShape(new kity.Path().setPathData('M-5-3l5,3,-5,3').stroke('#33ffff'));
var wireframeOption = /wire/.test(window.location.href);
var WireframeRenderer = kity.createClass('WireframeRenderer', {
base: Renderer,
create: function() {
var wireframe = new kity.Group();
var oxy = this.oxy = new kity.Path()
.stroke('#f6f')
.setPathData('M0,-50L0,50M-50,0L50,0');
var box = this.wireframe = new kity.Rect()
.stroke('lightgreen');
var vectorIn = this.vectorIn = new kity.Path()
.stroke('#66ffff');
var vectorOut = this.vectorOut = new kity.Path()
.stroke('#66ffff');
vectorIn.setMarker(marker, 'end');
vectorOut.setMarker(marker, 'end');
return wireframe.addShapes([oxy, box, vectorIn, vectorOut]);
},
shouldRender: function() {
return wireframeOption;
},
update: function(created, node, box) {
this.wireframe
.setPosition(box.x, box.y)
.setSize(box.width, box.height);
var pin = node.getVertexIn();
var pout = node.getVertexOut();
var vin = node.getLayoutVectorIn().normalize(30);
var vout = node.getLayoutVectorOut().normalize(30);
this.vectorIn.setPathData(['M', pin.offset(vin.reverse()), 'L', pin]);
this.vectorOut.setPathData(['M', pout, 'l', vout]);
}
});
Module.register('OutlineModule', function() {
return {
events: (!wireframeOption ? null : {
'ready': function() {
this.getPaper().addResource(marker);
},
'layoutallfinish': function() {
this.getRoot().traverse(function(node) {
node.getRenderer('WireframeRenderer').update(null, node, node.getContentBox());
});
}
}),
renderers: {
outline: OutlineRenderer,
outside: [ShadowRenderer, WireframeRenderer]
}
};
});
});

Some files were not shown because too many files have changed in this diff Show More