tiptap: refactor mind

pull/29/head
fantasticit 2022-04-26 14:05:16 +08:00
parent ad7b9db5d4
commit 59b2965b79
141 changed files with 17897 additions and 570 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 选择器的数量

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

@ -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: 'fresh-blue',
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 {

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,34 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { Space, Button, List, Popover, Typography } from '@douyinfe/semi-ui';
import { IconEdit, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip';
import { DataRender } from 'components/data-render';
import { IconDocument } from 'components/icons';
import { useWikiTocs } from 'data/wiki';
import { BubbleMenu } from '../../views/bubble-menu';
import { Mind } from '../../extensions/mind';
import { Divider } from '../../divider';
const { Text } = Typography;
export const MindBubbleMenu = ({ 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>
<Divider />
<Tooltip content="删除节点" hideOnClick>
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
</Tooltip>
</Space>
</BubbleMenu>
);
};

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

@ -5,113 +5,9 @@
line-height: 0; line-height: 0;
outline: none; 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; &::after {
min-height: 50px; background-color: transparent !important;
overflow: hidden;
border: 1px solid var(--node-border-color);
border-radius: var(--border-radius);
outline: none;
> input {
position: absolute;
top: 0;
left: 0;
z-index: 10000;
width: 100%;
height: 100%;
background-color: transparent;
}
.mindHandlerWrap {
position: absolute;
right: 20px;
bottom: 20px;
z-index: 1000;
padding: 4px 8px;
background-color: var(--semi-color-bg-2);
border: 1px solid var(--node-border-color);
border-radius: var(--border-radius);
opacity: 0;
box-shadow: var(--box-shadow);
}
}
&.isActive {
.jsmindWrap {
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,13 +1,13 @@
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, useRef } from 'react';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconMinus, IconPlus } from '@douyinfe/semi-icons'; import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import deepEqual from 'deep-equal'; import deepEqual from 'deep-equal';
import { useToggle } from 'hooks/use-toggle';
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';
export const MindWrapper = ({ editor, node, updateAttributes }) => { export const MindWrapper = ({ editor, node, updateAttributes }) => {
@ -16,87 +16,57 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
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, width, height = 100 } = node.attrs;
const [loading, toggleLoading] = useToggle(true);
const zoomIn = useCallback(() => { const onResize = useCallback(
const jm = $mind.current; (size) => {
if (!jm) return;
jm.view.zoomIn();
}, []);
const zoomOut = useCallback(() => {
const jm = $mind.current;
if (!jm) return;
jm.view.zoomOut();
}, []);
const syncData = useCallback(() => {
const jm = $mind.current;
if (!jm) return;
const data = jm.get_data();
try {
updateAttributes({ data });
} catch (e) {}
}, []);
useEffect(() => {
if (!data) return;
if (!data.meta) return;
const onChange = (_, data) => {
if (data.node) {
syncData();
}
};
setTimeout(() => {
if ($mind.current) {
const jm = $mind.current;
const currentData = jm.get_data();
const isEqual = deepEqual(currentData, data);
if (!isEqual) {
jm.show(data);
}
return;
}
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 }); updateAttributes({ width: size.width, height: size.height });
setTimeout(() => { },
jm.view.show(true); [updateAttributes]
jm.view.showAddHandlerDOMNode(); );
}, 100);
};
const saveData = useCallback(() => {
const minder = $mind.current;
if (!minder) return;
updateAttributes({ data: minder.exportJson() });
}, [updateAttributes]);
// 初始化
useEffect(() => { useEffect(() => {
const jm = $mind.current; const onChange = () => {
if (!jm) return; saveData();
};
loadKityMinder().then((Editor) => {
toggleLoading(false);
const minder = new Editor($container.current).minder;
minder.importJson(data);
$mind.current = minder;
minder.on('contentChange', onChange);
// @ts-ignore
window.minder = minder;
});
if (isEditable) { return () => {
jm.enable_edit(); if ($mind.current) {
} else { $mind.current.off('contentChange', onChange);
jm.disable_edit();
} }
};
}, [toggleLoading]);
// 数据同步渲染
useEffect(() => {
const minder = $mind.current;
if (!minder) return;
const currentData = minder.exportJson();
const isEqual = deepEqual(currentData, data);
if (isEqual) return;
minder.importData(data);
}, [data]);
// 启用/禁用
useEffect(() => {
const minder = $mind.current;
if (!minder) return;
}, [isEditable]); }, [isEditable]);
const content = ( const content = (
@ -105,26 +75,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
className={cls(styles.renderWrap, 'render-wrapper')} className={cls(styles.renderWrap, 'render-wrapper')}
tabIndex={0} tabIndex={0}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
> ></div>
{!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 (

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,9 @@
export const loadKityMinder = async (): Promise<any> => {
await import('kity');
await import('./kity-core/kityminder');
await import('./kity-editor/expose-editor');
const Editor = (window as any).kityminder.Editor;
return 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,41 @@
define(function (require, exports, module) {
var kity = require('./kity');
var Minder = require('./minder');
Minder.registerInitHook(function () {
this.on('beforemousedown', function (e) {
this.focus();
// 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,151 @@
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,92 @@
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 || 800);
},
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);
}
};
})());
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);
minder.execCommand('camera');
},
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]
}
};
});
});

View File

@ -0,0 +1,156 @@
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('PriorityModule', function() {
var minder = this;
// Designed by Akikonata
// [MASK, BACK]
var PRIORITY_COLORS = [null, ['#FF1200', '#840023'], // 1 - red
['#0074FF', '#01467F'], // 2 - blue
['#00AF00', '#006300'], // 3 - green
['#FF962E', '#B25000'], // 4 - orange
['#A464FF', '#4720C4'], // 5 - purple
['#A3A3A3', '#515151'], // 6,7,8,9 - gray
['#A3A3A3', '#515151'],
['#A3A3A3', '#515151'],
['#A3A3A3', '#515151'],
]; // hue from 1 to 5
// jscs:disable maximumLineLength
var BACK_PATH = 'M0,13c0,3.866,3.134,7,7,7h6c3.866,0,7-3.134,7-7V7H0V13z';
var MASK_PATH = 'M20,10c0,3.866-3.134,7-7,7H7c-3.866,0-7-3.134-7-7V7c0-3.866,3.134-7,7-7h6c3.866,0,7,3.134,7,7V10z';
var PRIORITY_DATA = 'priority';
// 优先级图标的图形
var PriorityIcon = kity.createClass('PriorityIcon', {
base: kity.Group,
constructor: function() {
this.callBase();
this.setSize(20);
this.create();
this.setId(utils.uuid('node_priority'));
},
setSize: function(size) {
this.width = this.height = size;
},
create: function() {
var white, back, mask, number; // 4 layer
white = new kity.Path().setPathData(MASK_PATH).fill('white');
back = new kity.Path().setPathData(BACK_PATH).setTranslate(0.5, 0.5);
mask = new kity.Path().setPathData(MASK_PATH).setOpacity(0.8).setTranslate(0.5, 0.5);
number = new kity.Text()
.setX(this.width / 2 - 0.5).setY(this.height / 2)
.setTextAnchor('middle')
.setVerticalAlign('middle')
.setFontItalic(true)
.setFontSize(12)
.fill('white');
this.addShapes([back, mask, number]);
this.mask = mask;
this.back = back;
this.number = number;
},
setValue: function(value) {
var back = this.back,
mask = this.mask,
number = this.number;
var color = PRIORITY_COLORS[value];
if (color) {
back.fill(color[1]);
mask.fill(color[0]);
}
number.setContent(value);
}
});
/**
* @command Priority
* @description 设置节点的优先级信息
* @param {number} value 要设置的优先级添加一个优先级小图标
* 取值为 0 移除优先级信息
* 取值为 1 - 9 设置优先级超过 9 的优先级不渲染
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var PriorityCommand = kity.createClass('SetPriorityCommand', {
base: Command,
execute: function(km, value) {
var nodes = km.getSelectedNodes();
for (var i = 0; i < nodes.length; i++) {
nodes[i].setData(PRIORITY_DATA, value || null).render();
}
km.layout();
},
queryValue: function(km) {
var nodes = km.getSelectedNodes();
var val;
for (var i = 0; i < nodes.length; i++) {
val = nodes[i].getData(PRIORITY_DATA);
if (val) break;
}
return val || null;
},
queryState: function(km) {
return km.getSelectedNodes().length ? 0 : -1;
}
});
return {
'commands': {
'priority': PriorityCommand
},
'renderers': {
left: kity.createClass('PriorityRenderer', {
base: Renderer,
create: function(node) {
return new PriorityIcon();
},
shouldRender: function(node) {
return node.getData(PRIORITY_DATA);
},
update: function(icon, node, box) {
var data = node.getData(PRIORITY_DATA);
var spaceLeft = node.getStyle('space-left'),
x, y;
icon.setValue(data);
x = box.left - icon.width - spaceLeft;
y = -icon.height / 2;
icon.setTranslate(x, y);
return new kity.Box({
x: x,
y: y,
width: icon.width,
height: icon.height
});
}
})
}
};
});
});

View File

@ -0,0 +1,160 @@
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('ProgressModule', function() {
var minder = this;
var PROGRESS_DATA = 'progress';
// Designed by Akikonata
var BG_COLOR = '#FFED83';
var PIE_COLOR = '#43BC00';
var SHADOW_PATH = 'M10,3c4.418,0,8,3.582,8,8h1c0-5.523-3.477-10-9-10S1,5.477,1,11h1C2,6.582,5.582,3,10,3z';
var SHADOW_COLOR = '#8E8E8E';
// jscs:disable maximumLineLength
var FRAME_PATH = 'M10,0C4.477,0,0,4.477,0,10c0,5.523,4.477,10,10,10s10-4.477,10-10C20,4.477,15.523,0,10,0zM10,18c-4.418,0-8-3.582-8-8s3.582-8,8-8s8,3.582,8,8S14.418,18,10,18z';
var FRAME_GRAD = new kity.LinearGradient().pipe(function(g) {
g.setStartPosition(0, 0);
g.setEndPosition(0, 1);
g.addStop(0, '#fff');
g.addStop(1, '#ccc');
});
var CHECK_PATH = 'M15.812,7.896l-6.75,6.75l-4.5-4.5L6.25,8.459l2.812,2.803l5.062-5.053L15.812,7.896z';
var CHECK_COLOR = '#EEE';
minder.getPaper().addResource(FRAME_GRAD);
// 进度图标的图形
var ProgressIcon = kity.createClass('ProgressIcon', {
base: kity.Group,
constructor: function(value) {
this.callBase();
this.setSize(20);
this.create();
this.setValue(value);
this.setId(utils.uuid('node_progress'));
this.translate(0.5, 0.5);
},
setSize: function(size) {
this.width = this.height = size;
},
create: function() {
var bg, pie, shadow, frame, check;
bg = new kity.Circle(9)
.fill(BG_COLOR);
pie = new kity.Pie(9, 0)
.fill(PIE_COLOR);
shadow = new kity.Path()
.setPathData(SHADOW_PATH)
.setTranslate(-10, -10)
.fill(SHADOW_COLOR);
frame = new kity.Path()
.setTranslate(-10, -10)
.setPathData(FRAME_PATH)
.fill(FRAME_GRAD);
check = new kity.Path()
.setTranslate(-10, -10)
.setPathData(CHECK_PATH)
.fill(CHECK_COLOR);
this.addShapes([bg, pie, shadow, check, frame]);
this.pie = pie;
this.check = check;
},
setValue: function(value) {
this.pie.setAngle(-360 * (value - 1) / 8);
this.check.setVisible(value == 9);
}
});
/**
* @command Progress
* @description 设置节点的进度信息添加一个进度小图标
* @param {number} value 要设置的进度
* 取值为 0 移除进度信息
* 取值为 1 表示未开始
* 取值为 2 表示完成 1/8
* 取值为 3 表示完成 2/8
* 取值为 4 表示完成 3/8
* 其余类推取值为 9 表示全部完成
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
var ProgressCommand = kity.createClass('ProgressCommand', {
base: Command,
execute: function(km, value) {
var nodes = km.getSelectedNodes();
for (var i = 0; i < nodes.length; i++) {
nodes[i].setData(PROGRESS_DATA, value || null).render();
}
km.layout();
},
queryValue: function(km) {
var nodes = km.getSelectedNodes();
var val;
for (var i = 0; i < nodes.length; i++) {
val = nodes[i].getData(PROGRESS_DATA);
if (val) break;
}
return val || null;
},
queryState: function(km) {
return km.getSelectedNodes().length ? 0 : -1;
}
});
return {
'commands': {
'progress': ProgressCommand
},
'renderers': {
left: kity.createClass('ProgressRenderer', {
base: Renderer,
create: function(node) {
return new ProgressIcon();
},
shouldRender: function(node) {
return node.getData(PROGRESS_DATA);
},
update: function(icon, node, box) {
var data = node.getData(PROGRESS_DATA);
var spaceLeft = node.getStyle('space-left');
var x, y;
icon.setValue(data);
x = box.left - icon.width - spaceLeft;
y = -icon.height / 2;
icon.setTranslate(x + icon.width / 2, y + icon.height / 2);
return new kity.Box(x, y, icon.width, icon.height);
}
})
}
};
});
});

View File

@ -0,0 +1,314 @@
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('Resource', function() {
// String Hash
// https://github.com/drostie/sha3-js/edit/master/blake32.min.js
var blake32=(function(){var k,g,r,l,m,o,p,q,t,w,x;x=4*(1<<30);k=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];m=[0x243F6A88,0x85A308D3,0x13198A2E,0x03707344,0xA4093822,0x299F31D0,0x082EFA98,0xEC4E6C89,0x452821E6,0x38D01377,0xBE5466CF,0x34E90C6C,0xC0AC29B7,0xC97C50DD,0x3F84D5B5,0xB5470917];w=function(i){if(i<0){i+=x}return("00000000"+i.toString(16)).slice(-8)};o=[[16,50,84,118,152,186,220,254],[174,132,249,109,193,32,123,53],[139,12,37,223,234,99,23,73],[151,19,205,235,98,165,4,143],[9,117,66,250,30,203,134,211],[194,166,176,56,212,87,239,145],[92,241,222,164,112,54,41,184],[189,231,28,147,5,79,104,162],[246,158,59,128,44,125,65,90],[42,72,103,81,191,233,195,13]];p=function(a,b,n){var s=q[a]^q[b];q[a]=(s>>>n)|(s<<(32-n))};g=function(i,a,b,c,d){var u=l+o[r][i]%16,v=l+(o[r][i]>>4);a%=4;b=4+b%4;c=8+c%4;d=12+d%4;q[a]+=q[b]+(t[u]^m[v%16]);p(d,a,16);q[c]+=q[d];p(b,c,12);q[a]+=q[b]+(t[v]^m[u%16]);p(d,a,8);q[c]+=q[d];p(b,c,7)};return function(a,b){if(!(b instanceof Array&&b.length===4)){b=[0,0,0,0]}var c,d,e,L,f,h,j,i;d=k.slice(0);c=m.slice(0,8);for(r=0;r<4;r+=1){c[r]^=b[r]}e=a.length*16;f=(e%512>446||e%512===0)?0:e;if(e%512===432){a+="\u8001"}else{a+="\u8000";while(a.length%32!==27){a+="\u0000"}a+="\u0001"}t=[];for(i=0;i<a.length;i+=2){t.push(a.charCodeAt(i)*65536+a.charCodeAt(i+1))}t.push(0);t.push(e);h=t.length-16;j=0;for(l=0;l<t.length;l+=16){j+=512;L=(l===h)?f:Math.min(e,j);q=d.concat(c);q[12]^=L;q[13]^=L;for(r=0;r<10;r+=1){for(i=0;i<8;i+=1){if(i<4){g(i,i,i,i,i)}else{g(i,i,i+1,i+2,i+3)}}}for(i=0;i<8;i+=1){d[i]^=b[i%4]^q[i]^q[i+8]}}return d.map(w).join("")}}());
/**
* 自动使用的颜色序列
*/
var RESOURCE_COLOR_SERIES = [51, 303, 75, 200, 157, 0, 26, 254].map(function(h) {
return kity.Color.createHSL(h, 100, 85);
});
/**
* Minder 上拓展一些关于资源的支持接口
*/
kity.extendClass(Minder, {
/**
* 获取字符串的哈希值
*
* @param {String} str
* @return {Number} hashCode
*/
getHashCode: function(str) {
str = blake32(str);
var hash = 1315423911, i, ch;
for (i = str.length - 1; i >= 0; i--) {
ch = str.charCodeAt(i);
hash ^= ((hash << 5) + ch + (hash >> 2));
}
return (hash & 0x7FFFFFFF);
},
/**
* 获取脑图中某个资源对应的颜色
*
* 如果存在同名资源则返回已经分配给该资源的颜色否则分配给该资源一个颜色并且返回
*
* 如果资源数超过颜色序列数量返回哈希颜色
*
* @param {String} resource 资源名称
* @return {Color}
*/
getResourceColor: function(resource) {
var colorMapping = this._getResourceColorIndexMapping();
var nextIndex;
if (!Object.prototype.hasOwnProperty.call(colorMapping, resource)) {
// 找不到找下个可用索引
nextIndex = this._getNextResourceColorIndex();
colorMapping[resource] = nextIndex;
}
// 资源过多,找不到可用索引颜色,统一返回哈希函数得到的颜色
return RESOURCE_COLOR_SERIES[colorMapping[resource]] || kity.Color.createHSL(Math.floor(this.getHashCode(resource) / 0x7FFFFFFF * 359), 100, 85);
},
/**
* 获得已使用的资源的列表
*
* @return {Array}
*/
getUsedResource: function() {
var mapping = this._getResourceColorIndexMapping();
var used = [],
resource;
for (resource in mapping) {
if (Object.prototype.hasOwnProperty.call(mapping, resource)) {
used.push(resource);
}
}
return used;
},
/**
* 获取脑图下一个可用的资源颜色索引
*
* @return {int}
*/
_getNextResourceColorIndex: function() {
// 获取现有颜色映射
// resource => color_index
var colorMapping = this._getResourceColorIndexMapping();
var resource, used, i;
used = [];
// 抽取已经使用的值到 used 数组
for (resource in colorMapping) {
if (Object.prototype.hasOwnProperty.call(colorMapping, resource)) {
used.push(colorMapping[resource]);
}
}
// 枚举所有的可用值,如果还没被使用,返回
for (i = 0; i < RESOURCE_COLOR_SERIES.length; i++) {
if (!~used.indexOf(i)) return i;
}
// 没有可用的颜色了
return -1;
},
// 获取现有颜色映射
// resource => color_index
_getResourceColorIndexMapping: function() {
return this._resourceColorMapping || (this._resourceColorMapping = {});
}
});
/**
* @class 设置资源的命令
*
* @example
*
* // 设置选中节点资源为 "张三"
* minder.execCommand('resource', ['张三']);
*
* // 添加资源 "李四" 到选中节点
* var resource = minder.queryCommandValue();
* resource.push('李四');
* minder.execCommand('resource', resource);
*
* // 清除选中节点的资源
* minder.execCommand('resource', null);
*/
var ResourceCommand = kity.createClass('ResourceCommand', {
base: Command,
execute: function(minder, resource) {
var nodes = minder.getSelectedNodes();
if (typeof(resource) == 'string') {
resource = [resource];
}
nodes.forEach(function(node) {
node.setData('resource', resource).render();
});
minder.layout(200);
},
queryValue: function(minder) {
var nodes = minder.getSelectedNodes();
var resource = [];
nodes.forEach(function(node) {
var nodeResource = node.getData('resource');
if (!nodeResource) return;
nodeResource.forEach(function(name) {
if (!~resource.indexOf(name)) {
resource.push(name);
}
});
});
return resource;
},
queryState: function(km) {
return km.getSelectedNode() ? 0 : -1;
}
});
/**
* @class 资源的覆盖图形
*
* 该类为一个资源以指定的颜色渲染一个动态的覆盖图形
*/
var ResourceOverlay = kity.createClass('ResourceOverlay', {
base: kity.Group,
constructor: function() {
this.callBase();
var text, rect;
rect = this.rect = new kity.Rect().setRadius(4);
text = this.text = new kity.Text()
.setFontSize(12)
.setVerticalAlign('middle');
this.addShapes([rect, text]);
},
setValue: function(resourceName, color) {
var paddingX = 8,
paddingY = 4,
borderRadius = 4;
var text, box, rect;
text = this.text;
if (resourceName == this.lastResourceName) {
box = this.lastBox;
} else {
text.setContent(resourceName);
box = text.getBoundaryBox();
this.lastResourceName = resourceName;
this.lastBox = box;
}
text.setX(paddingX).fill(color.dec('l', 70));
rect = this.rect;
rect.setPosition(0, box.y - paddingY);
this.width = Math.round(box.width + paddingX * 2);
this.height = Math.round(box.height + paddingY * 2);
rect.setSize(this.width, this.height);
rect.fill(color);
}
});
/**
* @class 资源渲染器
*/
var ResourceRenderer = kity.createClass('ResourceRenderer', {
base: Renderer,
create: function(node) {
this.overlays = [];
return new kity.Group();
},
shouldRender: function(node) {
return node.getData('resource') && node.getData('resource').length;
},
update: function(container, node, box) {
var spaceRight = node.getStyle('space-right');
var overlays = this.overlays;
/* resource null bug
* @Author zhangbobell
* @date 2016-01-15
*/
var resource = node.getData("resource").filter(function(ele) {
return ele !== null;
});
if (resource.length === 0) {
return;
}
var minder = node.getMinder();
var i, overlay, x;
x = 0;
for (i = 0; i < resource.length; i++) {
x += spaceRight;
overlay = overlays[i];
if (!overlay) {
overlay = new ResourceOverlay();
overlays.push(overlay);
container.addShape(overlay);
}
overlay.setVisible(true);
overlay.setValue(resource[i], minder.getResourceColor(resource[i]));
overlay.setTranslate(x, -1);
x += overlay.width;
}
while ((overlay = overlays[i++])) overlay.setVisible(false);
container.setTranslate(box.right, 0);
return new kity.Box({
x: box.right,
y: Math.round(-overlays[0].height / 2),
width: x,
height: overlays[0].height
});
}
});
return {
commands: {
'resource': ResourceCommand
},
renderers: {
right: ResourceRenderer
}
};
});
});

View File

@ -0,0 +1,184 @@
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('Select', function() {
var minder = this;
var rc = minder.getRenderContainer();
// 在实例上渲染框选矩形、计算框选范围的对象
var marqueeActivator = (function() {
// 记录选区的开始位置mousedown的位置
var startPosition = null;
// 选区的图形
var marqueeShape = new kity.Path();
// 标记是否已经启动框选状态
// 并不是 mousedown 发生之后就启动框选状态而是检测到移动了一定的距离MARQUEE_MODE_THRESHOLD之后
var marqueeMode = false;
var MARQUEE_MODE_THRESHOLD = 10;
return {
selectStart: function(e) {
// 只接受左键
if (e.originEvent.button || e.originEvent.altKey) return;
// 清理不正确状态
if (startPosition) {
return this.selectEnd();
}
startPosition = e.getPosition(rc).round();
},
selectMove: function(e) {
if (minder.getStatus() == 'textedit') {
return;
}
if (!startPosition) return;
var p1 = startPosition,
p2 = e.getPosition(rc);
// 检测是否要进入选区模式
if (!marqueeMode) {
// 距离没达到阈值,退出
if (kity.Vector.fromPoints(p1, p2).length() < MARQUEE_MODE_THRESHOLD) {
return;
}
// 已经达到阈值,记录下来并且重置选区形状
marqueeMode = true;
rc.addShape(marqueeShape);
marqueeShape
.fill(minder.getStyle('marquee-background'))
.stroke(minder.getStyle('marquee-stroke')).setOpacity(0.8).getDrawer().clear();
}
var marquee = new kity.Box(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y),
selectedNodes = [];
// 使其犀利
marquee.left = Math.round(marquee.left);
marquee.top = Math.round(marquee.top);
marquee.right = Math.round(marquee.right);
marquee.bottom = Math.round(marquee.bottom);
// 选区形状更新
marqueeShape.getDrawer().pipe(function() {
this.clear();
this.moveTo(marquee.left, marquee.top);
this.lineTo(marquee.right, marquee.top);
this.lineTo(marquee.right, marquee.bottom);
this.lineTo(marquee.left, marquee.bottom);
this.close();
});
// 计算选中范围
minder.getRoot().traverse(function(node) {
var renderBox = node.getLayoutBox();
if (!renderBox.intersect(marquee).isEmpty()) {
selectedNodes.push(node);
}
});
// 应用选中范围
minder.select(selectedNodes, true);
// 清除多余的东西
window.getSelection().removeAllRanges();
},
selectEnd: function(e) {
if (startPosition) {
startPosition = null;
}
if (marqueeMode) {
marqueeShape.fadeOut(200, 'ease', 0, function() {
if (marqueeShape.remove) marqueeShape.remove();
});
marqueeMode = false;
}
}
};
})();
var lastDownNode = null,
lastDownPosition = null;
return {
'init': function() {
window.addEventListener('mouseup', function() {
marqueeActivator.selectEnd();
});
},
'events': {
'mousedown': function(e) {
var downNode = e.getTargetNode();
// 没有点中节点:
// 清除选中状态,并且标记选区开始位置
if (!downNode) {
this.removeAllSelectedNodes();
marqueeActivator.selectStart(e);
this.setStatus('normal');
}
// 点中了节点,并且按了 shift 键:
// 被点中的节点切换选中状态
else if (e.isShortcutKey('Ctrl')) {
this.toggleSelect(downNode);
}
// 点中的节点没有被选择:
// 单选点中的节点
else if (!downNode.isSelected()) {
this.select(downNode, true);
}
// 点中的节点被选中了,并且不是单选:
// 完成整个点击之后需要使其变为单选。
// 不能马上变为单选,因为可能是需要拖动选中的多个节点
else if (!this.isSingleSelect()) {
lastDownNode = downNode;
lastDownPosition = e.getPosition();
}
},
'mousemove': marqueeActivator.selectMove,
'mouseup': function(e) {
var upNode = e.getTargetNode();
// 如果 mouseup 发生在 lastDownNode 外,是无需理会的
if (upNode && upNode == lastDownNode) {
var upPosition = e.getPosition();
var movement = kity.Vector.fromPoints(lastDownPosition, upPosition);
if (movement.length() < 1) this.select(lastDownNode, true);
lastDownNode = null;
}
// 清理一下选择状态
marqueeActivator.selectEnd(e);
},
//全选操作
'normal.keydown': function(e) {
if (e.isShortcutKey('ctrl+a')) {
var selectedNodes = [];
this.getRoot().traverse(function(node) {
selectedNodes.push(node);
});
this.select(selectedNodes, true);
e.preventDefault();
}
}
}
};
});
});

View File

@ -0,0 +1,114 @@
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('StyleModule', function() {
var styleNames = ['font-size', 'font-family', 'font-weight', 'font-style', 'background', 'color'];
var styleClipBoard = null;
function hasStyle(node) {
var data = node.getData();
for (var i = 0; i < styleNames.length; i++) {
if (styleNames[i] in data) return true;
}
}
return {
'commands': {
/**
* @command CopyStyle
* @description 拷贝选中节点的当前样式包括字体字号粗体斜体背景色字体色
* @state
* 0: 当前有选中的节点
* -1: 当前没有选中的节点
*/
'copystyle': kity.createClass('CopyStyleCommand', {
base: Command,
execute: function(minder) {
var node = minder.getSelectedNode();
var nodeData = node.getData();
styleClipBoard = {};
styleNames.forEach(function(name) {
if (name in nodeData) styleClipBoard[name] = nodeData[name];
else {
styleClipBoard[name] = null;
delete styleClipBoard[name];
}
});
return styleClipBoard;
},
queryState: function(minder) {
var nodes = minder.getSelectedNodes();
if (nodes.length !== 1) return -1;
return hasStyle(nodes[0]) ? 0 : -1;
}
}),
/**
* @command PasteStyle
* @description 粘贴已拷贝的样式到选中的节点上包括字体字号粗体斜体背景色字体色
* @state
* 0: 当前有选中的节点并且已经有复制的样式
* -1: 当前没有选中的节点或者没有复制的样式
*/
'pastestyle': kity.createClass('PastStyleCommand', {
base: Command,
execute: function(minder) {
minder.getSelectedNodes().forEach(function(node) {
for (var name in styleClipBoard) {
if (styleClipBoard.hasOwnProperty(name))
node.setData(name, styleClipBoard[name]);
}
});
minder.renderNodeBatch(minder.getSelectedNodes());
minder.layout(300);
return styleClipBoard;
},
queryState: function(minder) {
return (styleClipBoard && minder.getSelectedNodes().length) ? 0 : -1;
}
}),
/**
* @command ClearStyle
* @description 移除选中节点的样式包括字体字号粗体斜体背景色字体色
* @state
* 0: 当前有选中的节点并且至少有一个设置了至少一种样式
* -1: 其它情况
*/
'clearstyle': kity.createClass('ClearStyleCommand', {
base: Command,
execute: function(minder) {
minder.getSelectedNodes().forEach(function(node) {
styleNames.forEach(function(name) {
node.setData(name);
});
});
minder.renderNodeBatch(minder.getSelectedNodes());
minder.layout(300);
return styleClipBoard;
},
queryState: function(minder) {
var nodes = minder.getSelectedNodes();
if (!nodes.length) return -1;
for (var i = 0; i < nodes.length; i++) {
if (hasStyle(nodes[i])) return 0;
}
return -1;
}
})
}
};
});
});

View File

@ -0,0 +1,288 @@
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');
/**
* 针对不同系统不同浏览器不同字体做居中兼容性处理
* 暂时未增加Linux的处理
*/
var FONT_ADJUST = {
'safari': {
'微软雅黑,Microsoft YaHei': -0.17,
'楷体,楷体_GB2312,SimKai': -0.1,
'隶书, SimLi': -0.1,
'comic sans ms': -0.23,
'impact,chicago': -0.15,
'times new roman': -0.1,
'arial black,avant garde': -0.17,
'default': 0
},
'ie': {
10: {
'微软雅黑,Microsoft YaHei': -0.17,
'comic sans ms': -0.17,
'impact,chicago': -0.08,
'times new roman': 0.04,
'arial black,avant garde': -0.17,
'default': -0.15
},
11: {
'微软雅黑,Microsoft YaHei': -0.17,
'arial,helvetica,sans-serif': -0.17,
'comic sans ms': -0.17,
'impact,chicago': -0.08,
'times new roman': 0.04,
'sans-serif': -0.16,
'arial black,avant garde': -0.17,
'default': -0.15
}
},
'edge': {
'微软雅黑,Microsoft YaHei': -0.15,
'arial,helvetica,sans-serif': -0.17,
'comic sans ms': -0.17,
'impact,chicago': -0.08,
'sans-serif': -0.16,
'arial black,avant garde': -0.17,
'default': -0.15
},
'sg': {
'微软雅黑,Microsoft YaHei': -0.15,
'arial,helvetica,sans-serif': -0.05,
'comic sans ms': -0.22,
'impact,chicago': -0.16,
'times new roman': -0.03,
'arial black,avant garde': -0.22,
'default': -0.15
},
'chrome': {
'Mac': {
'andale mono': -0.05,
'comic sans ms': -0.3,
'impact,chicago': -0.13,
'times new roman': -0.1,
'arial black,avant garde': -0.17,
'default': 0
},
'Win': {
'微软雅黑,Microsoft YaHei': -0.15,
'arial,helvetica,sans-serif': -0.02,
'arial black,avant garde': -0.2,
'comic sans ms': -0.2,
'impact,chicago': -0.12,
'times new roman': -0.02,
'default': -0.15
},
'Lux': {
'andale mono': -0.05,
'comic sans ms': -0.3,
'impact,chicago': -0.13,
'times new roman': -0.1,
'arial black,avant garde': -0.17,
'default': 0
}
},
'firefox': {
'Mac': {
'微软雅黑,Microsoft YaHei': -0.2,
'宋体,SimSun': 0.05,
'comic sans ms': -0.2,
'impact,chicago': -0.15,
'arial black,avant garde': -0.17,
'times new roman': -0.1,
'default': 0.05
},
'Win': {
'微软雅黑,Microsoft YaHei': -0.16,
'andale mono': -0.17,
'arial,helvetica,sans-serif': -0.17,
'comic sans ms': -0.22,
'impact,chicago': -0.23,
'times new roman': -0.22,
'sans-serif': -0.22,
'arial black,avant garde': -0.17,
'default': -0.16
},
'Lux': {
"宋体,SimSun": -0.2,
"微软雅黑,Microsoft YaHei": -0.2,
"黑体, SimHei": -0.2,
"隶书, SimLi": -0.2,
"楷体,楷体_GB2312,SimKai": -0.2,
"andale mono": -0.2,
"arial,helvetica,sans-serif": -0.2,
"comic sans ms": -0.2,
"impact,chicago": -0.2,
"times new roman": -0.2,
"sans-serif": -0.2,
"arial black,avant garde": -0.2,
"default": -0.16
}
},
};
var TextRenderer = kity.createClass('TextRenderer', {
base: Renderer,
create: function() {
return new kity.Group().setId(utils.uuid('node_text'));
},
update: function(textGroup, node) {
function getDataOrStyle(name) {
return node.getData(name) || node.getStyle(name);
}
var nodeText = node.getText();
var textArr = nodeText ? nodeText.split('\n') : [' '];
var lineHeight = node.getStyle('line-height');
var fontSize = getDataOrStyle('font-size');
var fontFamily = getDataOrStyle('font-family') || 'default';
var height = (lineHeight * fontSize) * textArr.length - (lineHeight - 1) * fontSize;
var yStart = -height / 2;
var Browser = kity.Browser;
var adjust;
if (Browser.chrome || Browser.opera || Browser.bd ||Browser.lb === "chrome") {
adjust = FONT_ADJUST['chrome'][Browser.platform][fontFamily];
} else if (Browser.gecko) {
adjust = FONT_ADJUST['firefox'][Browser.platform][fontFamily];
} else if (Browser.sg) {
adjust = FONT_ADJUST['sg'][fontFamily];
} else if (Browser.safari) {
adjust = FONT_ADJUST['safari'][fontFamily];
} else if (Browser.ie) {
adjust = FONT_ADJUST['ie'][Browser.version][fontFamily];
} else if (Browser.edge) {
adjust = FONT_ADJUST['edge'][fontFamily];
} else if (Browser.lb) {
// 猎豹浏览器的ie内核兼容性模式下
adjust = 0.9;
}
textGroup.setTranslate(0, (adjust || 0) * fontSize);
var rBox = new kity.Box(),
r = Math.round;
this.setTextStyle(node, textGroup);
var textLength = textArr.length;
var textGroupLength = textGroup.getItems().length;
var i, ci, textShape, text;
if (textLength < textGroupLength) {
for (i = textLength, ci; ci = textGroup.getItem(i);) {
textGroup.removeItem(i);
}
} else if (textLength > textGroupLength) {
var growth = textLength - textGroupLength;
while (growth--) {
textShape = new kity.Text()
.setAttr('text-rendering', 'inherit');
if (kity.Browser.ie || kity.Browser.edge) {
textShape.setVerticalAlign('top');
} else {
textShape.setAttr('dominant-baseline', 'text-before-edge');
}
textGroup.addItem(textShape);
}
}
for (i = 0, text, textShape;
(text = textArr[i], textShape = textGroup.getItem(i)); i++) {
textShape.setContent(text);
if (kity.Browser.ie || kity.Browser.edge) {
textShape.fixPosition();
}
}
this.setTextStyle(node, textGroup);
var textHash = node.getText() +
['font-size', 'font-name', 'font-weight', 'font-style'].map(getDataOrStyle).join('/');
if (node._currentTextHash == textHash && node._currentTextGroupBox) return node._currentTextGroupBox;
node._currentTextHash = textHash;
return function() {
textGroup.eachItem(function(i, textShape) {
var y = yStart + i * fontSize * lineHeight;
textShape.setY(y);
var bbox = textShape.getBoundaryBox();
rBox = rBox.merge(new kity.Box(0, y, bbox.height && bbox.width || 1, fontSize));
});
var nBox = new kity.Box(r(rBox.x), r(rBox.y), r(rBox.width), r(rBox.height));
node._currentTextGroupBox = nBox;
return nBox;
};
},
setTextStyle: function(node, text) {
var hooks = TextRenderer._styleHooks;
hooks.forEach(function(hook) {
hook(node, text);
});
}
});
var TextCommand = kity.createClass({
base: Command,
execute: function(minder, text) {
var node = minder.getSelectedNode();
if (node) {
node.setText(text);
node.render();
minder.layout();
}
},
queryState: function(minder) {
return minder.getSelectedNodes().length == 1 ? 0 : -1;
},
queryValue: function(minder) {
var node = minder.getSelectedNode();
return node ? node.getText() : null;
}
});
utils.extend(TextRenderer, {
_styleHooks: [],
registerStyleHook: function(fn) {
TextRenderer._styleHooks.push(fn);
}
});
kity.extendClass(MinderNode, {
getTextGroup: function() {
return this.getRenderer('TextRenderer').getRenderShape();
}
});
Module.register('text', {
'commands': {
'text': TextCommand
},
'renderers': {
center: TextRenderer
}
});
module.exports = TextRenderer;
});

View File

@ -0,0 +1,397 @@
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 ViewDragger = kity.createClass('ViewDragger', {
constructor: function(minder) {
this._minder = minder;
this._enabled = false;
this._bind();
var me = this;
this._minder.getViewDragger = function() {
return me;
};
this.setEnabled(false);
},
isEnabled: function() {
return this._enabled;
},
setEnabled: function(value) {
var paper = this._minder.getPaper();
paper.setStyle('cursor', value ? 'pointer' : 'default');
paper.setStyle('cursor', value ? '-webkit-grab' : 'default');
this._enabled = value;
},
timeline: function() {
return this._moveTimeline;
},
move: function(offset, duration) {
var minder = this._minder;
var targetPosition = this.getMovement().offset(offset);
this.moveTo(targetPosition, duration);
},
moveTo: function(position, duration) {
if (duration) {
var dragger = this;
if (this._moveTimeline) this._moveTimeline.stop();
this._moveTimeline = this._minder.getRenderContainer().animate(new kity.Animator(
this.getMovement(),
position,
function(target, value) {
dragger.moveTo(value);
}
), duration, 'easeOutCubic').timeline();
this._moveTimeline.on('finish', function() {
dragger._moveTimeline = null;
});
return this;
}
this._minder.getRenderContainer().setTranslate(position.round());
this._minder.fire('viewchange');
},
getMovement: function() {
var translate = this._minder.getRenderContainer().transform.translate;
return translate ? translate[0] : new kity.Point();
},
getView: function() {
var minder = this._minder;
var c = minder._lastClientSize || {
width: minder.getRenderTarget().clientWidth,
height: minder.getRenderTarget().clientHeight
};
var m = this.getMovement();
var box = new kity.Box(0, 0, c.width, c.height);
var viewMatrix = minder.getPaper().getViewPortMatrix();
return viewMatrix.inverse().translate(-m.x, -m.y).transformBox(box);
},
_bind: function() {
var dragger = this,
isTempDrag = false,
lastPosition = null,
currentPosition = null;
function dragEnd(e) {
if (!lastPosition) return;
lastPosition = null;
e.stopPropagation();
// 临时拖动需要还原状态
if (isTempDrag) {
dragger.setEnabled(false);
isTempDrag = false;
if (dragger._minder.getStatus() == 'hand')
dragger._minder.rollbackStatus();
}
var paper = dragger._minder.getPaper();
paper.setStyle('cursor', dragger._minder.getStatus() == 'hand' ? '-webkit-grab' : 'default');
dragger._minder.fire('viewchanged');
}
this._minder.on('normal.mousedown normal.touchstart ' +
'inputready.mousedown inputready.touchstart ' +
'readonly.mousedown readonly.touchstart',
function(e) {
if (e.originEvent.button == 2) {
e.originEvent.preventDefault(); // 阻止中键拉动
}
// 点击未选中的根节点临时开启
if (e.getTargetNode() == this.getRoot() || e.originEvent.button == 2 || e.originEvent.altKey) {
lastPosition = e.getPosition('view');
isTempDrag = true;
}
})
.on('normal.mousemove normal.touchmove ' +
'readonly.mousemove readonly.touchmove ' +
'inputready.mousemove inputready.touchmove',
function(e) {
if (e.type == 'touchmove') {
e.preventDefault(); // 阻止浏览器的后退事件
}
if (!isTempDrag) return;
var offset = kity.Vector.fromPoints(lastPosition, e.getPosition('view'));
if (offset.length() > 10) {
this.setStatus('hand', true);
var paper = dragger._minder.getPaper();
paper.setStyle('cursor', '-webkit-grabbing');
}
})
.on('hand.beforemousedown hand.beforetouchstart', function(e) {
// 已经被用户打开拖放模式
if (dragger.isEnabled()) {
lastPosition = e.getPosition('view');
e.stopPropagation();
var paper = dragger._minder.getPaper();
paper.setStyle('cursor', '-webkit-grabbing');
}
})
.on('hand.beforemousemove hand.beforetouchmove', function(e) {
if (lastPosition) {
currentPosition = e.getPosition('view');
// 当前偏移加上历史偏移
var offset = kity.Vector.fromPoints(lastPosition, currentPosition);
dragger.move(offset);
e.stopPropagation();
e.preventDefault();
e.originEvent.preventDefault();
lastPosition = currentPosition;
}
})
.on('mouseup touchend', dragEnd);
window.addEventListener('mouseup', dragEnd);
this._minder.on('contextmenu', function(e) {
e.preventDefault();
});
}
});
Module.register('View', function() {
var km = this;
/**
* @command Hand
* @description 切换抓手状态抓手状态下鼠标拖动将拖动视野而不是创建选区
* @state
* 0: 当前不是抓手状态
* 1: 当前是抓手状态
*/
var ToggleHandCommand = kity.createClass('ToggleHandCommand', {
base: Command,
execute: function(minder) {
if (minder.getStatus() != 'hand') {
minder.setStatus('hand', true);
} else {
minder.rollbackStatus();
}
this.setContentChanged(false);
},
queryState: function(minder) {
return minder.getStatus() == 'hand' ? 1 : 0;
},
enableReadOnly: true
});
/**
* @command Camera
* @description 设置当前视野的中心位置到某个节点上
* @param {kityminder.MinderNode} focusNode 要定位的节点
* @param {number} duration 设置视野移动的动画时长单位 ms设置为 0 不使用动画
* @state
* 0: 始终可用
*/
var CameraCommand = kity.createClass('CameraCommand', {
base: Command,
execute: function(km, focusNode) {
focusNode = focusNode || km.getRoot();
var viewport = km.getPaper().getViewPort();
var offset = focusNode.getRenderContainer().getRenderBox('view');
var dx = viewport.center.x - offset.x - offset.width / 2,
dy = viewport.center.y - offset.y;
var dragger = km._viewDragger;
var duration = km.getOption('viewAnimationDuration');
dragger.move(new kity.Point(dx, dy), duration);
this.setContentChanged(false);
},
enableReadOnly: true
});
/**
* @command Move
* @description 指定方向移动当前视野
* @param {string} dir 移动方向
* 取值为 'left'视野向左移动一半
* 取值为 'right'视野向右移动一半
* 取值为 'up'视野向上移动一半
* 取值为 'down'视野向下移动一半
* @param {number} duration 视野移动的动画时长单位 ms设置为 0 不使用动画
* @state
* 0: 始终可用
*/
var MoveCommand = kity.createClass('MoveCommand', {
base: Command,
execute: function(km, dir) {
var dragger = km._viewDragger;
var size = km._lastClientSize;
var duration = km.getOption('viewAnimationDuration');
switch (dir) {
case 'up':
dragger.move(new kity.Point(0, size.height / 2), duration);
break;
case 'down':
dragger.move(new kity.Point(0, -size.height / 2), duration);
break;
case 'left':
dragger.move(new kity.Point(size.width / 2, 0), duration);
break;
case 'right':
dragger.move(new kity.Point(-size.width / 2, 0), duration);
break;
}
},
enableReadOnly: true
});
return {
init: function() {
this._viewDragger = new ViewDragger(this);
},
commands: {
'hand': ToggleHandCommand,
'camera': CameraCommand,
'move': MoveCommand
},
events: {
statuschange: function(e) {
this._viewDragger.setEnabled(e.currentStatus == 'hand');
},
mousewheel: function(e) {
var dx, dy;
e = e.originEvent;
if (e.ctrlKey || e.shiftKey) return;
if ('wheelDeltaX' in e) {
dx = e.wheelDeltaX || 0;
dy = e.wheelDeltaY || 0;
} else {
dx = 0;
dy = e.wheelDelta;
}
this._viewDragger.move({
x: dx / 2.5,
y: dy / 2.5
});
var me = this;
clearTimeout(this._mousewheeltimer);
this._mousewheeltimer = setTimeout(function() {
me.fire('viewchanged');
}, 100);
e.preventDefault();
},
'normal.dblclick readonly.dblclick': function(e) {
if (e.kityEvent.targetShape instanceof kity.Paper) {
this.execCommand('camera', this.getRoot(), 800);
}
},
'paperrender finishInitHook': function() {
if (!this.getRenderTarget()) {
return;
}
this.execCommand('camera', null, 0);
this._lastClientSize = {
width: this.getRenderTarget().clientWidth,
height: this.getRenderTarget().clientHeight
};
},
resize: function(e) {
var a = {
width: this.getRenderTarget().clientWidth,
height: this.getRenderTarget().clientHeight
},
b = this._lastClientSize;
this._viewDragger.move(
new kity.Point((a.width - b.width) / 2 | 0, (a.height - b.height) / 2 | 0));
this._lastClientSize = a;
},
'selectionchange layoutallfinish': function(e) {
var selected = this.getSelectedNode();
var minder = this;
/*
* Added by zhangbobell 2015.9.9
* windows 10 edge 浏览器在全部动画停止后优先级图标不显示 text
* 因此再次触发一次 render 事件让浏览器重绘
* */
if (kity.Browser.edge) {
this.fire('paperrender');
}
if (!selected) return;
var dragger = this._viewDragger;
var timeline = dragger.timeline();
/*
* Added by zhangbobell 2015.09.25
* 如果之前有动画那么就先暂时返回等之前动画结束之后再次执行本函数
* 以防止 view 动画变动了位置导致本函数执行的时候位置计算不对
*
* fixed bug : 初始化的时候中心节点位置不固定有的时候在左上角有的时候在中心
* */
if (timeline){
timeline.on('finish', function() {
minder.fire('selectionchange');
});
return;
}
var view = dragger.getView();
var focus = selected.getLayoutBox();
var space = 50;
var dx = 0, dy = 0;
if (focus.right > view.right) {
dx += view.right - focus.right - space;
}
else if (focus.left < view.left) {
dx += view.left - focus.left + space;
}
if (focus.bottom > view.bottom) {
dy += view.bottom - focus.bottom - space;
}
if (focus.top < view.top) {
dy += view.top - focus.top + space;
}
if (dx || dy) dragger.move(new kity.Point(dx, dy), 100);
}
}
};
});
});

View File

@ -0,0 +1,200 @@
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('Zoom', function() {
var me = this;
var timeline;
function setTextRendering() {
var value = me._zoomValue >= 100 ? 'optimize-speed' : 'geometricPrecision';
me.getRenderContainer().setAttr('text-rendering', value);
}
function fixPaperCTM(paper) {
var node = paper.shapeNode;
var ctm = node.getCTM();
var matrix = new kity.Matrix(ctm.a, ctm.b, ctm.c, ctm.d, (ctm.e | 0) + 0.5, (ctm.f | 0) + 0.5);
node.setAttribute('transform', 'matrix(' + matrix.toString() + ')');
}
kity.extendClass(Minder, {
zoom: function(value) {
var paper = this.getPaper();
var viewport = paper.getViewPort();
viewport.zoom = value / 100;
viewport.center = {
x: viewport.center.x,
y: viewport.center.y
};
paper.setViewPort(viewport);
if (value == 100) fixPaperCTM(paper);
},
getZoomValue: function() {
return this._zoomValue;
}
});
function zoomMinder(minder, value) {
var paper = minder.getPaper();
var viewport = paper.getViewPort();
if (!value) return;
setTextRendering();
var duration = minder.getOption('zoomAnimationDuration');
if (minder.getRoot().getComplex() > 200 || !duration) {
minder._zoomValue = value;
minder.zoom(value);
minder.fire('viewchange');
} else {
var animator = new kity.Animator({
beginValue: minder._zoomValue,
finishValue: value,
setter: function(target, value) {
target.zoom(value);
}
});
minder._zoomValue = value;
if (timeline) {
timeline.pause();
}
timeline = animator.start(minder, duration, 'easeInOutSine');
timeline.on('finish', function() {
minder.fire('viewchange');
});
}
minder.fire('zoom', {
zoom: value
});
}
/**
* @command Zoom
* @description 缩放当前的视野到一定的比例百分比
* @param {number} value 设置的比例取值 100 则为原尺寸
* @state
* 0: 始终可用
*/
var ZoomCommand = kity.createClass('Zoom', {
base: Command,
execute: zoomMinder,
queryValue: function(minder) {
return minder._zoomValue;
}
});
/**
* @command ZoomIn
* @description 放大当前的视野到下一个比例等级百分比
* @shortcut =
* @state
* 0: 如果当前脑图的配置中还有下一个比例等级
* -1: 其它情况
*/
var ZoomInCommand = kity.createClass('ZoomInCommand', {
base: Command,
execute: function(minder) {
zoomMinder(minder, this.nextValue(minder));
},
queryState: function(minder) {
return +!this.nextValue(minder);
},
nextValue: function(minder) {
var stack = minder.getOption('zoom'),
i;
for (i = 0; i < stack.length; i++) {
if (stack[i] > minder._zoomValue) return stack[i];
}
return 0;
},
enableReadOnly: true
});
/**
* @command ZoomOut
* @description 缩小当前的视野到上一个比例等级百分比
* @shortcut -
* @state
* 0: 如果当前脑图的配置中还有上一个比例等级
* -1: 其它情况
*/
var ZoomOutCommand = kity.createClass('ZoomOutCommand', {
base: Command,
execute: function(minder) {
zoomMinder(minder, this.nextValue(minder));
},
queryState: function(minder) {
return +!this.nextValue(minder);
},
nextValue: function(minder) {
var stack = minder.getOption('zoom'),
i;
for (i = stack.length - 1; i >= 0; i--) {
if (stack[i] < minder._zoomValue) return stack[i];
}
return 0;
},
enableReadOnly: true
});
return {
init: function() {
this._zoomValue = 100;
this.setDefaultOptions({
zoom: [10, 20, 50, 100, 200]
});
setTextRendering();
},
commands: {
'zoomin': ZoomInCommand,
'zoomout': ZoomOutCommand,
'zoom': ZoomCommand
},
events: {
'normal.mousewheel readonly.mousewheel': function(e) {
if (!e.originEvent.ctrlKey && !e.originEvent.metaKey) return;
var delta = e.originEvent.wheelDelta;
var me = this;
if (!kity.Browser.mac) {
delta = -delta;
}
// 稀释
if (Math.abs(delta) > 100) {
clearTimeout(this._wheelZoomTimeout);
} else {
return;
}
this._wheelZoomTimeout = setTimeout(function() {
var value;
var lastValue = me.getPaper()._zoom || 1;
if (delta < 0) {
me.execCommand('zoomin');
} else if (delta > 0) {
me.execCommand('zoomout');
}
}, 100);
e.originEvent.preventDefault();
}
},
commandShortcutKeys: {
'zoomin': 'ctrl+=',
'zoomout': 'ctrl+-'
}
};
});
});

View File

@ -0,0 +1,18 @@
define(function(require, exports, module) {
var data = require('../core/data');
data.registerProtocol('json', module.exports = {
fileDescription: 'KityMinder 格式',
fileExtension: '.km',
dataType: 'text',
mineType: 'application/json',
encode: function(json) {
return JSON.stringify(json);
},
decode: function(local) {
return JSON.parse(local);
}
});
});

View File

@ -0,0 +1,158 @@
define(function(require, exports, module) {
var data = require('../core/data');
var LINE_ENDING_SPLITER = /\r\n|\r|\n/;
var EMPTY_LINE = '';
var NOTE_MARK_START = '<!--Note-->';
var NOTE_MARK_CLOSE = '<!--/Note-->';
function encode(json) {
return _build(json, 1).join('\n');
}
function _build(node, level) {
var lines = [];
level = level || 1;
var sharps = _generateHeaderSharp(level);
lines.push(sharps + ' ' + node.data.text);
lines.push(EMPTY_LINE);
var note = node.data.note;
if (note) {
var hasSharp = /^#/.test(note);
if (hasSharp) {
lines.push(NOTE_MARK_START);
note = note.replace(/^#+/gm, function($0) {
return sharps + $0;
});
}
lines.push(note);
if (hasSharp) {
lines.push(NOTE_MARK_CLOSE);
}
lines.push(EMPTY_LINE);
}
if (node.children) node.children.forEach(function(child) {
lines = lines.concat(_build(child, level + 1));
});
return lines;
}
function _generateHeaderSharp(level) {
var sharps = '';
while (level--) sharps += '#';
return sharps;
}
function decode(markdown) {
var json,
parentMap = {},
lines, line, lineInfo, level, node, parent, noteProgress, codeBlock;
// 一级标题转换 `{title}\n===` => `# {title}`
markdown = markdown.replace(/^(.+)\n={3,}/, function($0, $1) {
return '# ' + $1;
});
lines = markdown.split(LINE_ENDING_SPLITER);
// 按行分析
for (var i = 0; i < lines.length; i++) {
line = lines[i];
lineInfo = _resolveLine(line);
// 备注标记处理
if (lineInfo.noteClose) {
noteProgress = false;
continue;
} else if (lineInfo.noteStart) {
noteProgress = true;
continue;
}
// 代码块处理
codeBlock = lineInfo.codeBlock ? !codeBlock : codeBlock;
// 备注条件:备注标签中,非标题定义,或标题越位
if (noteProgress || codeBlock || !lineInfo.level || lineInfo.level > level + 1) {
if (node) _pushNote(node, line);
continue;
}
// 标题处理
level = lineInfo.level;
node = _initNode(lineInfo.content, parentMap[level - 1]);
parentMap[level] = node;
}
_cleanUp(parentMap[1]);
return parentMap[1];
}
function _initNode(text, parent) {
var node = {
data: {
text: text,
note: ''
}
};
if (parent) {
if (parent.children) parent.children.push(node);
else parent.children = [node];
}
return node;
}
function _pushNote(node, line) {
node.data.note += line + '\n';
}
function _isEmpty(line) {
return !/\S/.test(line);
}
function _resolveLine(line) {
var match = /^(#+)?\s*(.*)$/.exec(line);
return {
level: match[1] && match[1].length || null,
content: match[2],
noteStart: line == NOTE_MARK_START,
noteClose: line == NOTE_MARK_CLOSE,
codeBlock: /^\s*```/.test(line)
};
}
function _cleanUp(node) {
if (!/\S/.test(node.data.note)) {
node.data.note = null;
delete node.data.note;
} else {
var notes = node.data.note.split('\n');
while (notes.length && !/\S/.test(notes[0])) notes.shift();
while (notes.length && !/\S/.test(notes[notes.length - 1])) notes.pop();
node.data.note = notes.join('\n');
}
if (node.children) node.children.forEach(_cleanUp);
}
data.registerProtocol('markdown', module.exports = {
fileDescription: 'Markdown/GFM 格式',
fileExtension: '.md',
mineType: 'text/markdown',
dataType: 'text',
encode: function(json) {
return encode(json.root);
},
decode: function(markdown) {
return decode(markdown);
}
});
});

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