feat: 完善图片显示和性能优化

- 添加图片数据库字段支持
This commit is contained in:
lixinran 2025-10-09 16:02:23 +08:00
parent 3b39f86f83
commit c95bbd649b
128 changed files with 940 additions and 29140 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -165,7 +165,7 @@ MindMap/
│ ├── test-*.html # 功能测试文件
│ ├── package.json # 前端依赖
│ └── vite.config.js # Vite配置
├── mind-elixir-core-master/ # MindElixir完整源码
../mind-elixir-core-master/ # MindElixir完整源码(平级目录)
│ ├── src/ # TypeScript源码
│ ├── tests/ # 测试套件
│ ├── dist/ # 编译后文件
@ -410,7 +410,7 @@ MIT License - 详见 [LICENSE](LICENSE) 文件
- ✅ **MindElixir增强**: 集成了自定义的Markdown和表格渲染功能
### 文件结构优化
- **保留**: `mind-elixir-core-master/` - 完整的源码和文档
- **保留**: `../mind-elixir-core-master/` - 完整的源码和文档(平级目录)
- **保留**: `frontend/src/lib/mind-elixir/` - 项目中使用的增强版本
- **保留**: 核心测试文件 - 用于功能验证和问题调试
- **删除**: 重复的调试文件和过时的测试文件

Binary file not shown.

View File

@ -77,7 +77,7 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
model=model,
messages=messages,
temperature=0.7,
max_tokens=4000, # 减少token限制提高响应速度
max_tokens=8000, # 增加token限制确保完整内容生成
stream=stream
)
except Exception as e:

View File

@ -0,0 +1,33 @@
# Generated by Django 4.2.7 on 2025-10-09 07:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mindmap', '0002_rename_mindmapnode_node'),
]
operations = [
migrations.AddField(
model_name='node',
name='image_fit',
field=models.CharField(blank=True, default='contain', max_length=20),
),
migrations.AddField(
model_name='node',
name='image_height',
field=models.IntegerField(blank=True, default=80, null=True),
),
migrations.AddField(
model_name='node',
name='image_url',
field=models.URLField(blank=True, max_length=1000, null=True),
),
migrations.AddField(
model_name='node',
name='image_width',
field=models.IntegerField(blank=True, default=120, null=True),
),
]

View File

@ -28,6 +28,13 @@ class Node(models.Model):
depth = models.IntegerField(default=0) # depth
title = models.CharField(max_length=500, blank=True, default='') # title
desc = models.TextField(blank=True, default='') # des
# 图片相关字段
image_url = models.URLField(max_length=1000, blank=True, null=True) # 图片URL
image_width = models.IntegerField(default=120, null=True, blank=True) # 图片宽度
image_height = models.IntegerField(default=80, null=True, blank=True) # 图片高度
image_fit = models.CharField(max_length=20, default='contain', blank=True) # 图片适配方式
created_at = models.DateTimeField(auto_now_add=True) # createDate
updated_at = models.DateTimeField(auto_now=True) # updateDate
deleted = models.BooleanField(default=False) # delete

View File

@ -39,7 +39,7 @@ def convert_to_mindelixir_format(mindmap, nodes):
# 创建节点映射
node_map = {}
for node in nodes:
node_map[str(node.id)] = {
node_data = {
"id": str(node.id),
"topic": node.title or "无标题",
"data": {
@ -50,6 +50,17 @@ def convert_to_mindelixir_format(mindmap, nodes):
"mindmapId": mindmap.id,
"mindmap_id": mindmap.id
}
# 添加图片信息
if node.image_url:
node_data["image"] = {
"url": node.image_url,
"width": node.image_width or 120,
"height": node.image_height or 80,
"fit": node.image_fit or "contain"
}
node_map[str(node.id)] = node_data
# 构建树形结构
root_nodes = []
@ -159,6 +170,11 @@ def create_mindmap(request):
children_count=0,
depth=0,
deleted=False,
# 添加图片信息
image_url=mindmap_data.get('image', {}).get('url') if mindmap_data.get('image') else None,
image_width=mindmap_data.get('image', {}).get('width') if mindmap_data.get('image') else None,
image_height=mindmap_data.get('image', {}).get('height') if mindmap_data.get('image') else None,
image_fit=mindmap_data.get('image', {}).get('fit') if mindmap_data.get('image') else 'contain',
)
# 递归创建所有子节点
@ -180,6 +196,11 @@ def create_mindmap(request):
children_count=0,
depth=0,
deleted=False,
# 图片字段默认为空
image_url=None,
image_width=None,
image_height=None,
image_fit='contain',
)
# 返回完整数据
@ -205,6 +226,11 @@ def create_nodes_recursively(nodes_data, mindmap, parent_id):
children_count=len(node_data.get('children', [])),
depth=1, # 可以根据实际层级计算
deleted=False,
# 添加图片信息
image_url=node_data.get('image', {}).get('url') if node_data.get('image') else None,
image_width=node_data.get('image', {}).get('width') if node_data.get('image') else None,
image_height=node_data.get('image', {}).get('height') if node_data.get('image') else None,
image_fit=node_data.get('image', {}).get('fit') if node_data.get('image') else 'contain',
)
# 递归创建子节点

BIN
frontend/.DS_Store vendored

Binary file not shown.

File diff suppressed because one or more lines are too long

606
frontend/dist/assets/index-7802977e.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,8 +23,8 @@
flex-direction: column;
}
</style>
<script type="module" crossorigin src="/assets/index-a09f7810.js"></script>
<link rel="stylesheet" href="/assets/index-5b39da23.css">
<script type="module" crossorigin src="/assets/index-7802977e.js"></script>
<link rel="stylesheet" href="/assets/index-3cdbb183.css">
</head>
<body>
<div id="app"></div>

View File

@ -292,7 +292,27 @@ const generateMarkdownFromFile = async () => {
markdownContent.value = '';
// AI APIMarkdown
const systemPrompt = '你是一个专业的文档分析专家。请分析上传的文档内容生成结构化的Markdown格式思维导图。要求1. 提取主要主题和关键概念 2. 组织成层次分明的结构 3. 使用清晰的标题和子标题 4. 保持内容的逻辑性和完整性';
const systemPrompt = `你是一个专业的文档分析专家。请分析上传的文档内容生成结构化的Markdown格式思维导图。要求
1. 提取主要主题和关键概念
2. 组织成层次分明的结构
3. 使用清晰的标题和子标题
4. 保持内容的逻辑性和完整性
5. 对于长文档请确保完整处理所有内容不要截断
6. **重要如果原文档中包含图片请按以下方式处理
- 识别图片在文档中的位置和上下文
- 根据图片内容生成准确的描述文字
- 在相应位置插入图片占位符![图片描述](图片路径)
- 图片描述要准确反映图片内容图片路径可以是相对路径或占位符
- 确保图片占位符放在逻辑上合适的位置**
7. **重要如果原文档中包含表格请完整保留表格结构
- 保持表格的Markdown格式
- 确保所有表格行都被包含
- 不要省略任何表格内容**
8. **重要确保内容完整性
- 不要截断任何内容
- 保持原文的完整性
- 所有重要信息都要包含在思维导图中**
9. 输出格式直接输出Markdown内容不要添加任何说明文字或代码块标记`;
const userPrompt = `请分析以下文档内容并生成结构化Markdown\n\n${fileContent}`;
// 使APIAPI
@ -464,6 +484,55 @@ const generateMarkdown = async () => {
}
};
// AI
const cleanAIResponse = (content) => {
if (!content) return content;
let cleaned = content
// AI -
.replace(/^好的[,]?作为.*?专家[,]?我已.*?分析.*?内容.*?生成了以下.*?思维导图[:]\s*/i, '')
.replace(/^好的[,]?作为.*?专业的.*?专家[,]?我已.*?分析.*?内容.*?生成了以下.*?思维导图[:]\s*/i, '')
.replace(/^以下是.*?结构化的.*?思维导图[:]\s*/i, '')
.replace(/^以下是.*?Markdown.*?思维导图[:]\s*/i, '')
.replace(/^以下是.*?Markdown.*?格式.*?思维导图[:]\s*/i, '')
//
.replace(/^```(?:markdown)?\s*/gm, '')
.replace(/```\s*$/gm, '')
// -
.replace(/^「」`markdown\s*/gm, '')
.replace(/^「」`\s*/gm, '')
.replace(/^\s*「」`markdown\s*/gm, '')
.replace(/^\s*「」`\s*/gm, '')
// AI
.replace(/^作为.*?专家[,]?我已.*?分析.*?内容.*?生成了以下.*?思维导图[:]\s*/i, '')
.replace(/^我已.*?分析.*?内容.*?生成了以下.*?思维导图[:]\s*/i, '')
.replace(/^根据.*?文档内容.*?生成了以下.*?思维导图[:]\s*/i, '')
.replace(/^基于.*?文档.*?生成了以下.*?思维导图[:]\s*/i, '')
//
.replace(/^以下是.*?分析.*?结果[:]\s*/i, '')
.replace(/^以下是.*?整理.*?结果[:]\s*/i, '')
.replace(/^以下是.*?结构化.*?内容[:]\s*/i, '')
//
.replace(/\n{3,}/g, '\n\n')
//
.trim();
return cleaned;
};
//
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// AI APIMarkdown
// AI API
const callAIStreamAPI = async (systemPrompt, userPrompt) => {
@ -497,7 +566,14 @@ Level 4 标题用 #####
保持段落和换行不变
输出格式 输出必须是纯Markdown格式的文本不得包含任何额外说明JSON或非Markdown元素确保输出与示例风格一致`;
**重要如果原文档中包含图片请按以下方式处理
1. 识别图片在文档中的位置和上下文
2. 根据图片内容生成准确的描述文字
3. 在相应位置插入图片占位符![图片描述](图片路径)
4. 图片描述要准确反映图片内容图片路径可以是相对路径或占位符
5. 确保图片占位符放在逻辑上合适的位置**
输出格式 输出必须是纯Markdown格式的文本不得包含任何额外说明JSON或非Markdown元素确保输出与示例风格一致直接输出Markdown内容不要添加任何说明文字`;
const finalSystemPrompt = systemPrompt || defaultSystemPrompt;
const finalUserPrompt = userPrompt || `请将以下内容转换为结构化的Markdown格式`;
@ -548,28 +624,57 @@ Level 4 标题用 #####
// Markdown
markdownContent.value += data.content;
// JSON
try {
const tempJSON = markdownToJSON(markdownContent.value);
convertedJSON.value = JSON.stringify(tempJSON, null, 2);
// 🎯
window.dispatchEvent(new CustomEvent('realtime-mindmap-update', {
detail: {
data: tempJSON,
title: tempJSON.topic || 'AI生成中...',
source: 'ai-streaming',
chunkCount: chunkCount
}
}));
} catch (e) {
//
console.warn('⚠️ 实时转换JSON失败:', e);
console.warn('⚠️ 当前Markdown内容:', markdownContent.value);
// 5chunk
if (chunkCount % 5 === 0) {
try {
// AI
const cleanedContent = cleanAIResponse(markdownContent.value);
const tempJSON = markdownToJSON(cleanedContent);
convertedJSON.value = JSON.stringify(tempJSON, null, 2);
// 🎯
window.dispatchEvent(new CustomEvent('realtime-mindmap-update', {
detail: {
data: tempJSON,
title: tempJSON.topic || 'AI生成中...',
source: 'ai-streaming',
chunkCount: chunkCount
}
}));
} catch (e) {
//
console.warn('⚠️ 实时转换JSON失败:', e);
}
}
} else if (data.type === 'end') {
showNotification('AI内容生成完成', 'success');
//
try {
const finalCleanedContent = cleanAIResponse(markdownContent.value);
console.log('🎯 最终内容长度:', finalCleanedContent.length);
console.log('🎯 最终内容预览:', finalCleanedContent.substring(0, 500) + '...');
//
const allImages = extractImageFromContent(finalCleanedContent);
console.log(`🖼️ 最终统计:共发现 ${allImages.length} 张图片`);
const finalJSON = markdownToJSON(finalCleanedContent);
convertedJSON.value = JSON.stringify(finalJSON, null, 2);
//
window.dispatchEvent(new CustomEvent('realtime-mindmap-update', {
detail: {
data: finalJSON,
title: finalJSON.topic || 'AI生成完成',
source: 'ai-final',
chunkCount: chunkCount
}
}));
} catch (e) {
console.error('⚠️ 最终处理失败:', e);
}
} else if (data.type === 'error') {
throw new Error(data.content);
}
@ -616,7 +721,14 @@ Level 4 标题用 #####
保持段落和换行不变
输出格式 输出必须是纯Markdown格式的文本不得包含任何额外说明JSON或非Markdown元素确保输出与示例风格一致`;
**重要如果原文档中包含图片请按以下方式处理
1. 识别图片在文档中的位置和上下文
2. 根据图片内容生成准确的描述文字
3. 在相应位置插入图片占位符![图片描述](图片路径)
4. 图片描述要准确反映图片内容图片路径可以是相对路径或占位符
5. 确保图片占位符放在逻辑上合适的位置**
输出格式 输出必须是纯Markdown格式的文本不得包含任何额外说明JSON或非Markdown元素确保输出与示例风格一致直接输出Markdown内容不要添加任何说明文字`;
// 使
const finalSystemPrompt = systemPrompt || defaultSystemPrompt;
@ -631,7 +743,7 @@ Level 4 标题用 #####
// -
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 1
const timeoutId = setTimeout(() => controller.abort(), 300000); // 5
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-markdown', {
method: 'POST',
@ -732,6 +844,8 @@ const formatMarkdownToText = (markdown) => {
}
}
// MindElixirimage
return markdown
//
.replace(/^### (.*$)/gim, '📋 $1') //
@ -759,9 +873,22 @@ const formatMarkdownToText = (markdown) => {
.replace(/`(.*?)`/g, '「$1」')
//
.replace(/\[([^\]]+)\]\([^)]+\)/g, '🔗 $1')
//
// MindElixirimage
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[图片: $1]')
// -
.replace(/\n\n/g, '\n')
.replace(/\n/g, '\n ');
.replace(/\n/g, '\n ')
//
.split('\n')
.map(line => {
//
if (line.length > 80) {
return line.substring(0, 77) + '...';
}
return line;
})
.join('\n')
.trim();
};
// MarkdownJSON
@ -773,7 +900,9 @@ const convertToJSON = async () => {
isConverting.value = true;
try {
const json = markdownToJSON(markdownContent.value);
// AI
const cleanedContent = cleanAIResponse(markdownContent.value);
const json = markdownToJSON(cleanedContent);
convertedJSON.value = JSON.stringify(json, null, 2);
} catch (error) {
console.error('转换失败:', error);
@ -795,6 +924,29 @@ const countNodes = (node) => {
return count;
};
//
const extractImageFromContent = (content) => {
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
const images = [];
let match;
while ((match = imageRegex.exec(content)) !== null) {
images.push({
alt: match[1] || '',
url: match[2],
fullMatch: match[0]
});
}
console.log(`🔍 从内容中提取到 ${images.length} 张图片:`, images);
return images;
};
// Markdown
const removeImageMarkdown = (content) => {
return content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '').trim();
};
// MarkdownJSON -
const markdownToJSON = (markdown) => {
const lines = markdown.split('\n');
@ -823,8 +975,13 @@ const markdownToJSON = (markdown) => {
const level = match[1].length;
const title = match[2].trim();
// Markdown
const cleanTitle = formatMarkdownToText(title);
//
const titleImages = extractImageFromContent(title);
const cleanTitle = removeImageMarkdown(formatMarkdownToText(title));
if (titleImages.length > 0) {
console.log(`🖼️ 在标题中发现 ${titleImages.length} 张图片: ${title}`);
}
//
const node = {
@ -835,6 +992,17 @@ const markdownToJSON = (markdown) => {
data: {},
};
// 使
if (titleImages.length > 0) {
node.image = {
url: titleImages[0].url,
width: 120,
height: 80,
fit: 'contain'
};
console.log(`🖼️ 为标题节点设置图片: ${titleImages[0].url}`);
}
//
if (level === 1 && !root) {
root = node;
@ -888,7 +1056,42 @@ const markdownToJSON = (markdown) => {
//
const processContentIntelligently = (content, parentNode, nodeCounter) => {
//
//
const images = extractImageFromContent(content);
if (images.length > 0) {
console.log(`🖼️ 在内容中发现 ${images.length} 张图片`);
//
images.forEach((image, index) => {
const imageNode = {
id: `node_${nodeCounter++}`,
topic: image.alt || `图片 ${index + 1}`,
children: [],
level: (parentNode.level || 0) + 1,
image: {
url: image.url,
width: 120,
height: 80,
fit: 'contain'
},
data: {}
};
parentNode.children.push(imageNode);
console.log(`🖼️ 创建图片节点: ${imageNode.topic} - ${image.url}`);
});
//
const contentWithoutImages = removeImageMarkdown(content);
if (contentWithoutImages.trim()) {
//
const processedContent = processContentIntelligently(contentWithoutImages, parentNode, nodeCounter);
nodeCounter = processedContent.nodeCounter;
}
return { nodeCounter };
}
//
if (hasTableContent(content)) {
console.log('🎯 检测到表格内容,创建表格节点');
const tableNode = {
@ -1005,6 +1208,7 @@ const processContentIntelligently = (content, parentNode, nodeCounter) => {
return { nodeCounter: currentNodeCounter };
};
//
const hasTableContent = (content) => {
if (!content || typeof content !== 'string') {

View File

@ -367,15 +367,11 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
maxScale: 5,
minScale: 0.1,
markdown: (text, nodeObj) => {
console.log('🔍 Mind Elixir markdown函数被调用:', text.substring(0, 100) + '...');
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#')) {
console.log('🎨 检测到markdown内容开始渲染:', text.substring(0, 100) + '...');
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
const result = smartRenderNodeContent(text);
console.log('🎨 渲染结果:', result.substring(0, 200) + '...');
return result;
}
console.log('🔍 内容不包含markdown语法返回原文本');
return text;
}
});
@ -422,8 +418,8 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
maxScale: 5,
minScale: 0.1,
markdown: (text, nodeObj) => {
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#')) {
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
return smartRenderNodeContent(text);
}
return text;
@ -484,8 +480,8 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
maxScale: 5,
minScale: 0.1,
markdown: (text, nodeObj) => {
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#')) {
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('![')) {
return smartRenderNodeContent(text);
}
return text;
@ -3494,15 +3490,11 @@ const updateMindMapRealtime = async (data, title) => {
maxScale: 5,
minScale: 0.1,
markdown: (text, nodeObj) => {
// console.log('🔍 Mind Elixir markdown:', text.substring(0, 100) + '...');
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('$')) {
console.log('🎨 实时更新 检测到markdown内容开始渲染:', text.substring(0, 100) + '...');
// markdown
if (text.includes('|') || text.includes('**') || text.includes('`') || text.includes('#') || text.includes('$') || text.includes('![')) {
const result = smartRenderNodeContent(text);
console.log('🎨 实时更新 渲染结果:', result.substring(0, 200) + '...');
return result;
}
console.log('🔍 实时更新 内容不包含markdown语法返回原文本');
return text;
}
});

View File

@ -3,6 +3,7 @@ import type { Topic, Wrapper, Parent, Children, Expander } from '../types/dom'
import type { MindElixirInstance, NodeObj } from '../types/index'
import { encodeHTML } from '../utils/index'
import { layoutChildren } from './layout'
// 移除imageProcessor引用使用MindElixir原生image属性
// DOM manipulation
const $d = document
@ -55,7 +56,8 @@ export const shapeTpc = function (this: MindElixirInstance, tpc: Topic, nodeObj:
if (this.markdown) {
textEl.innerHTML = this.markdown(nodeObj.topic, nodeObj)
} else {
textEl.textContent = nodeObj.topic
// 直接设置文本内容图片通过MindElixir原生image属性处理
textEl.innerHTML = nodeObj.topic || ''
}
tpc.appendChild(textEl)

View File

@ -14,12 +14,16 @@ import 'prismjs/components/prism-python';
import 'prismjs/components/prism-sql';
import 'katex/dist/katex.min.css';
// 自定义渲染器移除图片处理使用MindElixir原生image属性
const renderer = new marked.Renderer();
// 配置marked选项
marked.setOptions({
breaks: true,
gfm: true, // GitHub Flavored Markdown
tables: true, // 支持表格
sanitize: false, // 允许HTML用于数学公式等
renderer: renderer // 使用自定义渲染器
});
/**
@ -40,7 +44,9 @@ export const renderMarkdownToHTML = (markdown) => {
const html = marked.parse(processedMarkdown);
// 后处理HTML
return postprocessHTML(html);
const finalHTML = postprocessHTML(html);
return finalHTML;
} catch (error) {
console.error('Markdown渲染失败:', error);
return `<div class="markdown-error">渲染失败: ${error.message}</div>`;
@ -166,6 +172,11 @@ const addMarkdownStyles = (container) => {
.markdown-content {
max-width: 100%;
overflow: hidden;
word-wrap: break-word;
word-break: break-word;
line-height: 1.4;
text-align: left;
display: block;
}
.markdown-content h1,
@ -196,12 +207,15 @@ const addMarkdownStyles = (container) => {
.markdown-content ol {
margin: 2px 0;
padding-left: 16px;
list-style-position: inside;
}
.markdown-content li {
margin: 1px 0;
line-height: 1.3;
color: #666;
display: list-item;
text-align: left;
}
.markdown-content strong,

View File

@ -1,4 +0,0 @@
dist
src/__tests__
index.html
test.html

View File

@ -1,16 +0,0 @@
module.exports = {
env: {
browser: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
plugins: ['@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
},
rules: {
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
},
}

View File

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [ssshooter]
patreon: # Replace with a single Patreon username
open_collective: ssshooter
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,60 +0,0 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [created]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm i
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
- name: Checkout docs repository
uses: actions/checkout@v3
with:
repository: mind-elixir/docs
token: ${{ secrets.PAT }}
path: me-docs
- name: Generate API documentation
run: |
npm run doc
npm run doc:md
- name: Copy build results to docs repository
run: |
cp -r ./md/* ./me-docs/docs/api
cp -r ./md/* ./me-docs/i18n/ja/docusaurus-plugin-content-docs/current/api
cp -r ./md/* ./me-docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/api
- name: Push changes to docs repository
run: |
cd me-docs
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
if git diff-index --quiet HEAD; then
echo "No changes to commit"
else
git commit -m "Update API documentation"
git push
fi

View File

@ -1,32 +0,0 @@
.DS_Store
node_modules
dist
doc
.nyc_output
coverage
api
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.eslintcache
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/md

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run test

View File

@ -1,74 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variables file
.env
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity
src/
webpack.config.js
out/
doc/

View File

@ -1,9 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"arrowParens": "avoid",
"printWidth": 150,
"endOfLine": "auto"
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 DjZhou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,396 +0,0 @@
/**
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
/**
* Optionally specifies another JSON config file that this file extends from. This provides a way for
* standard settings to be shared across multiple projects.
*
* If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
* the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be
* resolved using NodeJS require().
*
* SUPPORTED TOKENS: none
* DEFAULT VALUE: ""
*/
// "extends": "./shared/api-extractor-base.json"
// "extends": "my-package/include/api-extractor-base.json"
/**
* Determines the "<projectFolder>" token that can be used with other config file settings. The project folder
* typically contains the tsconfig.json and package.json config files, but the path is user-defined.
*
* The path is resolved relative to the folder of the config file that contains the setting.
*
* The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing
* parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
* that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error
* will be reported.
*
* SUPPORTED TOKENS: <lookup>
* DEFAULT VALUE: "<lookup>"
*/
// "projectFolder": "..",
/**
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
* analyzes the symbols exported by this module.
*
* The file extension must be ".d.ts" and not ".ts".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
*/
"mainEntryPointFilePath": "<projectFolder>/dist/types/docs.d.ts",
/**
* A list of NPM package names whose exports should be treated as part of this package.
*
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
* imports library2. To avoid this, we can specify:
*
* "bundledPackages": [ "library2" ],
*
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
* local files for library1.
*/
"bundledPackages": [],
/**
* Specifies what type of newlines API Extractor should use when writing output files. By default, the output files
* will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead.
* To use the OS's default newline kind, specify "os".
*
* DEFAULT VALUE: "crlf"
*/
// "newlineKind": "crlf",
/**
* Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the
* .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests.
*
* DEFAULT VALUE: "false"
*/
// "testMode": false,
/**
* Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output
* files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify
* "preserve".
*
* DEFAULT VALUE: "by-name"
*/
// "enumMemberOrder": "by-name",
/**
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
*/
"compiler": {
/**
* Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* Note: This setting will be ignored if "overrideTsconfig" is used.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
*/
// "tsconfigFilePath": "<projectFolder>/tsconfig.json",
/**
* Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
* The object must conform to the TypeScript tsconfig schema:
*
* http://json.schemastore.org/tsconfig
*
* If omitted, then the tsconfig.json file will be read from the "projectFolder".
*
* DEFAULT VALUE: no overrideTsconfig section
*/
// "overrideTsconfig": {
// . . .
// }
/**
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
* and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
* dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
* for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
*
* DEFAULT VALUE: false
*/
// "skipLibCheck": true,
},
/**
* Configures how the API report file (*.api.md) will be generated.
*/
"apiReport": {
/**
* (REQUIRED) Whether to generate an API report.
*/
"enabled": true,
/**
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
* a full file path.
*
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
*
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
*/
// "reportFileName": "<unscopedPackageName>.api.md",
/**
* Specifies the folder where the API report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy,
* e.g. for an API review.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
"reportFolder": "<projectFolder>/api/",
/**
* Specifies the folder where the temporary report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* After the temporary file is written to disk, it is compared with the file in the "reportFolder".
* If they are different, a production build will fail.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
"reportTempFolder": "<projectFolder>/api/",
/**
* Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations
* flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to
* learn more.
*
* DEFAULT VALUE: "false"
*/
"includeForgottenExports": false
},
/**
* Configures how the doc model file (*.api.json) will be generated.
*/
"docModel": {
/**
* (REQUIRED) Whether to generate a doc model file.
*/
"enabled": true,
/**
* The output path for the doc model file. The file extension should be ".api.json".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
*/
"apiJsonFilePath": "<projectFolder>/api/<unscopedPackageName>.api.json"
/**
* Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations
* flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to
* learn more.
*
* DEFAULT VALUE: "false"
*/
// "includeForgottenExports": false,
/**
* The base URL where the project's source code can be viewed on a website such as GitHub or
* Azure DevOps. This URL path corresponds to the `<projectFolder>` path on disk.
*
* This URL is concatenated with the file paths serialized to the doc model to produce URL file paths to individual API items.
* For example, if the `projectFolderUrl` is "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor" and an API
* item's file path is "api/ExtractorConfig.ts", the full URL file path would be
* "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js".
*
* Can be omitted if you don't need source code links in your API documentation reference.
*
* SUPPORTED TOKENS: none
* DEFAULT VALUE: ""
*/
// "projectFolderUrl": "http://github.com/path/to/your/projectFolder"
},
/**
* Configures how the .d.ts rollup file will be generated.
*/
"dtsRollup": {
/**
* (REQUIRED) Whether to generate the .d.ts rollup file.
*/
"enabled": true
/**
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
* This file will include all declarations that are exported by the main entry point.
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
*/
// "untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release.
* This file will include only declarations that are marked as "@public", "@beta", or "@alpha".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "alphaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-alpha.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
* This file will include only declarations that are marked as "@public" or "@beta".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
* This file will include only declarations that are marked as "@public".
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts",
/**
* When a declaration is trimmed, by default it will be replaced by a code comment such as
* "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the
* declaration completely.
*
* DEFAULT VALUE: false
*/
// "omitTrimmingComments": true
},
/**
* Configures how the tsdoc-metadata.json file will be generated.
*/
"tsdocMetadata": {
/**
* Whether to generate the tsdoc-metadata.json file.
*
* DEFAULT VALUE: true
*/
// "enabled": true,
/**
* Specifies where the TSDoc metadata file should be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata",
* "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup
* falls back to "tsdoc-metadata.json" in the package folder.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<lookup>"
*/
// "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json"
},
/**
* Configures how API Extractor reports error and warning messages produced during analysis.
*
* There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages.
*/
"messages": {
/**
* Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
* the input .d.ts files.
*
* TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"compilerMessageReporting": {
/**
* Configures the default routing for messages that don't match an explicit rule in this table.
*/
"default": {
/**
* Specifies whether the message should be written to the the tool's output log. Note that
* the "addToApiReportFile" property may supersede this option.
*
* Possible values: "error", "warning", "none"
*
* Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail
* and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes
* the "--local" option), the warning is displayed but the build will not fail.
*
* DEFAULT VALUE: "warning"
*/
"logLevel": "warning"
/**
* When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md),
* then the message will be written inside that file; otherwise, the message is instead logged according to
* the "logLevel" option.
*
* DEFAULT VALUE: false
*/
// "addToApiReportFile": false
}
// "TS2551": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by API Extractor during its analysis.
*
* API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag"
*
* DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
*/
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
}
// "ae-extra-release-tag": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by the TSDoc parser when analyzing code comments.
*
* TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
}
// "tsdoc-link-tag-unescaped-text": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
}
}
}

View File

@ -1,47 +0,0 @@
import { fileURLToPath } from 'url'
import { build } from 'vite'
import strip from '@rollup/plugin-strip'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const buildList = [
{
name: 'MindElixir',
enrty: __dirname + './src/index.ts',
},
{
name: 'MindElixirLite',
enrty: __dirname + './src/index.ts',
mode: 'lite',
},
{
name: 'example',
enrty: __dirname + './src/exampleData/1.ts',
},
{
name: 'LayoutSsr',
enrty: __dirname + './src/utils/layout-ssr.ts',
},
]
for (let i = 0; i < buildList.length; i++) {
const info = buildList[i]
console.log(`\n\nBuilding ${info.name}...\n\n`)
await build({
build: {
emptyOutDir: i === 0,
lib: {
entry: info.enrty,
fileName: info.name,
name: info.name,
formats: ['iife', 'es'],
},
rollupOptions: {
plugins: [
strip({
include: ['**/*.ts', '**/*.js'],
}),
],
},
},
mode: info.mode,
})
}

View File

@ -1,107 +0,0 @@
export default {
parserPreset: 'conventional-changelog-conventionalcommits',
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 120],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [2, 'always', ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test']],
},
prompt: {
questions: {
type: {
description: "Select the type of change that you're committing",
enum: {
feat: {
description: 'A new feature',
title: 'Features',
emoji: '✨',
},
fix: {
description: 'A bug fix',
title: 'Bug Fixes',
emoji: '🐛',
},
docs: {
description: 'Documentation only changes',
title: 'Documentation',
emoji: '📚',
},
style: {
description: 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
title: 'Styles',
emoji: '💎',
},
refactor: {
description: 'A code change that neither fixes a bug nor adds a feature',
title: 'Code Refactoring',
emoji: '📦',
},
perf: {
description: 'A code change that improves performance',
title: 'Performance Improvements',
emoji: '🚀',
},
test: {
description: 'Adding missing tests or correcting existing tests',
title: 'Tests',
emoji: '🚨',
},
build: {
description: 'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',
title: 'Builds',
emoji: '🛠',
},
ci: {
description: 'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',
title: 'Continuous Integrations',
emoji: '⚙️',
},
chore: {
description: "Other changes that don't modify src or test files",
title: 'Chores',
emoji: '♻️',
},
revert: {
description: 'Reverts a previous commit',
title: 'Reverts',
emoji: '🗑',
},
},
},
scope: {
description: 'What is the scope of this change (e.g. component or file name)',
},
subject: {
description: 'Write a short, imperative tense description of the change',
},
body: {
description: 'Provide a longer description of the change',
},
isBreaking: {
description: 'Are there any breaking changes?',
},
breakingBody: {
description: 'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself',
},
breaking: {
description: 'Describe the breaking changes',
},
isIssueAffected: {
description: 'Does this change affect any open issues?',
},
issuesBody: {
description: 'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself',
},
issues: {
description: 'Add issue references (e.g. "fix #123", "re #123".)',
},
},
},
}

View File

@ -1,177 +0,0 @@
code[class*='language-'],
pre[class*='language-'] {
color: black;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*='language-'] {
position: relative;
margin: 0.5em 0;
overflow: visible;
padding: 0;
}
pre[class*='language-']>code {
position: relative;
}
code[class*='language'] {
border-radius: 3px;
background: #faf8f5;
max-height: inherit;
height: inherit;
padding: 1em;
display: block;
overflow: auto;
margin: 0;
}
/* Inline code */
:not(pre)>code[class*='language-'] {
display: inline;
position: relative;
color: #c92c2c;
padding: 0.15em;
white-space: normal;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #7d8b99;
}
.token.punctuation {
color: #5f6364;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.function-name,
.token.constant,
.token.symbol,
.token.deleted {
color: #c92c2c;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.function,
.token.builtin,
.token.inserted {
color: #2f9c0a;
}
.token.operator,
.token.entity,
.token.url,
.token.variable {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword,
.token.class-name {
color: #1990b8;
}
.token.regex,
.token.important {
color: #e90;
}
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.important {
font-weight: normal;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: 0.7;
}
@media screen and (max-width: 767px) {
pre[class*='language-']:before,
pre[class*='language-']:after {
bottom: 14px;
box-shadow: none;
}
}
/* Plugin styles */
.token.tab:not(:empty):before,
.token.cr:before,
.token.lf:before {
color: #e0d7d1;
}
/* Plugin styles: Line Numbers */
pre[class*='language-'].line-numbers.line-numbers {
padding-left: 0;
}
pre[class*='language-'].line-numbers.line-numbers code {
padding-left: 3.8em;
}
pre[class*='language-'].line-numbers.line-numbers .line-numbers-rows {
left: 0;
}
/* Plugin styles: Line Highlight */
pre[class*='language-'][data-line] {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
pre[data-line] code {
position: relative;
padding-left: 4em;
}
pre .line-highlight {
margin-top: 0;
}

View File

@ -1,40 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Mind Elixir</title>
<link rel="stylesheet" href="./index.css">
<style>
/* test tailwind compatibility */
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e5e7eb;
}
#map,
#map2 {
margin-top: 50px;
height: 500px;
width: 800px;
}
</style>
</head>
<body>
<div id="map"></div>
<!-- test transform compatibility -->
<div style="transform: translateX(30px) translateY(20px);">
<div id="map2"></div>
</div>
<div id="ssr"></div>
<script type="module" src="/src/dev.ts"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,108 +0,0 @@
{
"name": "mind-elixir",
"version": "5.1.1",
"type": "module",
"description": "Mind elixir is a free open source mind map core.",
"keywords": [
"mind-elixir",
"mindmap",
"dom",
"visualization"
],
"scripts": {
"prepare": "husky install",
"lint": "eslint --cache --max-warnings 0 \"src/**/*.{js,json,ts}\" --fix",
"dev": "vite",
"build": "node build.js && tsc",
"tsc": "tsc",
"preview": "vite preview",
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:clean": "rimraf .nyc_output coverage",
"test:coverage": "pnpm test:clean && pnpm test && pnpm nyc && npx http-server ./coverage",
"nyc": "nyc report --reporter=html",
"doc": "api-extractor run --local --verbose",
"doc:md": "api-documenter markdown --input-folder ./api --output-folder ./md",
"beta": "npm run build && npm publish --tag beta"
},
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/MindElixir.js",
"require": "./dist/MindElixir.js"
},
"./lite": {
"import": "./dist/MindElixirLite.iife.js"
},
"./example": {
"types": "./dist/types/exampleData/1.d.ts",
"import": "./dist/example.js",
"require": "./dist/example.js"
},
"./LayoutSsr": {
"types": "./dist/types/utils/LayoutSsr.d.ts",
"import": "./dist/LayoutSsr.js",
"require": "./dist/LayoutSsr.js"
},
"./readme.md": "./readme.md",
"./package.json": "./package.json",
"./style": "./dist/style.css",
"./style.css": "./dist/style.css"
},
"typesVersions": {
"*": {
"example": [
"./dist/types/exampleData/1.d.ts"
]
}
},
"main": "dist/MindElixir.js",
"types": "dist/types/index.d.ts",
"lint-staged": {
"src/**/*.{ts,js}": [
"eslint --cache --fix"
],
"src/**/*.{json,less}": [
"prettier --write"
]
},
"files": [
"package.json",
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/ssshooter/mind-elixir-core"
},
"homepage": "https://mind-elixir.com/",
"author": "ssshooter",
"license": "MIT",
"devDependencies": {
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@microsoft/api-documenter": "^7.25.21",
"@microsoft/api-extractor": "^7.47.11",
"@playwright/test": "^1.55.0",
"@rollup/plugin-strip": "^3.0.4",
"@types/node": "^20.14.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@viselect/vanilla": "3.9.0",
"@zumer/snapdom": "^1.3.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"katex": "^0.16.22",
"less": "^4.2.0",
"lint-staged": "^13.3.0",
"marked": "^16.2.0",
"nyc": "^17.1.0",
"prettier": "2.8.4",
"rimraf": "^6.0.1",
"simple-markdown-to-html": "^1.0.0",
"typescript": "^5.4.5",
"vite": "^4.5.3",
"vite-plugin-istanbul": "^7.1.0"
}
}

View File

@ -1,81 +0,0 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
timeout: 10000,
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'pnpm dev --port 23334',
url: 'http://127.0.0.1:23334',
reuseExistingServer: true,
env: {
VITE_COVERAGE: 'true'
},
},
})

File diff suppressed because it is too large Load Diff

View File

@ -1,432 +0,0 @@
<p align="center">
<a href="https://docs.mind-elixir.com" target="_blank" rel="noopener noreferrer">
<img width="150" src="https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png" alt="mindelixir logo2">
</a>
<h1 align="center">Mind Elixir</h1>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/mind-elixir">
<img src="https://img.shields.io/npm/v/mind-elixir" alt="version">
</a>
<a href="https://github.com/ssshooter/mind-elixir-core/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/mind-elixir" alt="license">
</a>
<a href="https://app.codacy.com/gh/ssshooter/mind-elixir-core?utm_source=github.com&utm_medium=referral&utm_content=ssshooter/mind-elixir-core&utm_campaign=Badge_Grade_Settings">
<img src="https://api.codacy.com/project/badge/Grade/09fadec5bf094886b30cea6aabf3a88b" alt="code quality">
</a>
<a href="https://bundlephobia.com/result?p=mind-elixir">
<img src="https://badgen.net/bundlephobia/dependency-count/mind-elixir" alt="dependency-count">
</a>
<a href="https://packagephobia.com/result?p=mind-elixir">
<img src="https://packagephobia.com/badge?p=mind-elixir" alt="package size">
</a>
</p>
[English](/readme.md) |
[中文](/readme/zh.md) |
[Español](/readme/es.md) |
[Français](/readme/fr.md) |
[Português](/readme/pt.md) |
[Русский](/readme/ru.md) |
[日本語](/readme/ja.md) |
[한국어](/readme/ko.md)
Mind elixir is a open source JavaScript mind map core. You can use it with any frontend framework you like.
## Features
### 🎨 **User Experience**
- **Fluent UX** - Smooth and intuitive interactions
- **Well designed** - Clean and modern interface
- **Mobile friendly** - Touch events for mobile devices
- **Efficient shortcuts** - Keyboard shortcuts for power users
### ⚡ **Performance & Architecture**
- **Lightweight** - Minimal bundle size
- **High performance** - Optimized for large mind maps
- **Framework agnostic** - Works with any frontend framework
- **Pluginable** - Extensible architecture
### 🛠️ **Core Features**
- **Interactive editing** - Built-in drag and drop / node edit capabilities
- **Bulk operations** - Multi-node selection and operations
- **Undo / Redo** - Complete operation history
- **Node connections & summarization** - Custom node linking and content summarization
### 📤 **Export & Customization**
- **Multiple export formats** - SVG / PNG / HTML export
- **Easy styling** - Customize mindmap with CSS variables
- **Theme support** - Built-in themes and custom styling
[v5 Breaking Changes](https://github.com/SSShooter/mind-elixir-core/wiki/Breaking-Change#500)
<details>
<summary>Table of Contents</summary>
- [Features](#features)
- [🎨 **User Experience**](#-user-experience)
- [⚡ **Performance \& Architecture**](#-performance--architecture)
- [🛠️ **Core Features**](#-core-features)
- [📤 **Export \& Customization**](#-export--customization)
- [Try now](#try-now)
- [Playground](#playground)
- [Documentation](#documentation)
- [Usage](#usage)
- [Install](#install)
- [NPM](#npm)
- [Script tag](#script-tag)
- [Init](#init)
- [Data Structure](#data-structure)
- [Event Handling](#event-handling)
- [Data Export And Import](#data-export-and-import)
- [Operation Guards](#operation-guards)
- [Export as a Image](#export-as-a-image)
- [Theme](#theme)
- [Shortcuts](#shortcuts)
- [Who's using](#whos-using)
- [Ecosystem](#ecosystem)
- [Development](#development)
- [Thanks](#thanks)
- [Contributors](#contributors)
</details>
## Try now
![mindelixir](https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/screenshot5.jpg)
https://mind-elixir.com/
### Playground
- Vanilla JS - https://codepen.io/ssshooter/pen/vEOqWjE
- React - https://codesandbox.io/p/devbox/mind-elixir-3-x-react-18-x-forked-f3mtcd
- Vue3 - https://codesandbox.io/p/sandbox/mind-elixir-3-x-vue3-lth484
## Documentation
https://docs.mind-elixir.com/
## Usage
### Install
#### NPM
```bash
npm i mind-elixir -S
```
```javascript
import MindElixir from 'mind-elixir'
import 'mind-elixir/style.css'
```
#### Script tag
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/MindElixir.js"></script>
```
And in your CSS file:
```css
@import 'https://cdn.jsdelivr.net/npm/mind-elixir/dist/style.css';
```
### Init
```html
<div id="map"></div>
<style>
#map {
height: 500px;
width: 100%;
}
</style>
```
```javascript
import MindElixir from 'mind-elixir'
import 'mind-elixir/style.css'
import example from 'mind-elixir/dist/example1'
let options = {
el: '#map', // or HTMLDivElement
direction: MindElixir.LEFT,
draggable: true, // default true
toolBar: true, // default true
nodeMenu: true, // default true
keypress: true, // default true
locale: 'en', // [zh_CN,zh_TW,en,ja,pt,ru] waiting for PRs
overflowHidden: false, // default false
mainLinkStyle: 2, // [1,2] default 1
mouseSelectionButton: 0, // 0 for left button, 2 for right button, default 0
contextMenu: {
focus: true,
link: true,
extend: [
{
name: 'Node edit',
onclick: () => {
alert('extend menu')
},
},
],
}, // default true
before: {
insertSibling(type, obj) {
return true
},
},
// Custom markdown parser (optional)
// markdown: (text) => customMarkdownParser(text), // provide your own markdown parser function
}
let mind = new MindElixir(options)
mind.install(plugin) // install your plugin
// create new map data
const data = MindElixir.new('new topic')
// or `example`
// or the data return from `.getData()`
mind.init(data)
// get a node
MindElixir.E('node-id')
```
### Data Structure
```javascript
// whole node data structure up to now
const nodeData = {
topic: 'node topic',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
expanded: true,
parent: null,
tags: ['Tag'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png', // required
// you need to query the height and width of the image and calculate the appropriate value to display the image
height: 90, // required
width: 90, // required
},
children: [
{
topic: 'child',
id: 'xxxx',
// ...
},
],
}
```
### Event Handling
```javascript
mind.bus.addListener('operation', operation => {
console.log(operation)
// return {
// name: action name,
// obj: target object
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: target
// name: moveNode
// obj: {from:target1,to:target2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
```
### Data Export And Import
```javascript
// data export
const data = mind.getData() // javascript object, see src/example.js
mind.getDataString() // stringify object
// data import
// initiate
let mind = new MindElixir(options)
mind.init(data)
// data update
mind.refresh(data)
```
### Markdown Support
Mind Elixir supports custom markdown parsing:
```javascript
// Disable markdown (default)
let mind = new MindElixir({
// markdown option omitted - no markdown processing
})
// Use custom markdown parser
let mind = new MindElixir({
markdown: (text) => {
// Your custom markdown implementation
return text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
},
})
// Use any markdown library (e.g., marked, markdown-it, etc.)
import { marked } from 'marked'
let mind = new MindElixir({
markdown: (text) => marked(text),
})
```
For detailed markdown configuration examples, see [docs/markdown-configuration.md](docs/markdown-configuration.md).
### Operation Guards
```javascript
let mind = new MindElixir({
// ...
before: {
async addChild(el, obj) {
await saveDataToDb()
return true
},
},
})
```
## Export as a Image
Install `@ssshooter/modern-screenshot`, then:
```typescript
import { domToPng } from '@ssshooter/modern-screenshot'
const download = async () => {
const dataUrl = await domToPng(mind.nodes, {
padding: 300,
quality: 1,
})
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataUrl
link.click()
}
```
## Theme
```javascript
const options = {
// ...
theme: {
name: 'Dark',
// main lines color palette
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
// overwrite css variables
cssVar: {
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--panel-color': '255, 255, 255',
'--panel-bgcolor': '45, 55, 72',
},
// all variables see /src/index.less
},
// ...
}
// ...
mind.changeTheme({
name: 'Latte',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
},
})
```
Be aware that Mind Elixir will not observe the change of `prefers-color-scheme`. Please change the theme **manually** when the scheme changes.
## Shortcuts
See [Shortcuts Guide](https://docs.mind-elixir.com/docs/guides/shortcuts) for detailed information.
## Who's using
- [Mind Elixir Desktop](https://desktop.mind-elixir.com/)
## Ecosystem
- [@mind-elixir/node-menu](https://github.com/ssshooter/node-menu)
- [@mind-elixir/node-menu-neo](https://github.com/ssshooter/node-menu-neo)
- [@mind-elixir/export-xmind](https://github.com/ssshooter/export-xmind)
- [@mind-elixir/export-html](https://github.com/ssshooter/export-html)
- [mind-elixir-react](https://github.com/ssshooter/mind-elixir-react)
PRs are welcome!
## Development
```
pnpm i
pnpm dev
```
Test generated files with `dev.dist.ts`:
```
pnpm build
pnpm link ./
```
Update docs:
```
# Install api-extractor
pnpm install -g @microsoft/api-extractor
# Maintain /src/docs.ts
# Generate docs
pnpm doc
pnpm doc:md
```
Use [DeepWiki](https://deepwiki.com/SSShooter/mind-elixir-core) to learn more about Mind Elixir
## Thanks
- [@viselect/vanilla](https://github.com/simonwep/selection/tree/master/packages/vanilla)
## Contributors
Thanks for your contributions to Mind Elixir! Your support and dedication make this project better.
<a href="https://github.com/SSShooter/mind-elixir-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=SSShooter/mind-elixir-core" />
</a>

File diff suppressed because it is too large Load Diff

View File

@ -1,556 +0,0 @@
import { generateUUID, getArrowPoints, getObjById, getOffsetLT, setAttributes } from './utils/index'
import LinkDragMoveHelper from './utils/LinkDragMoveHelper'
import { createSvgGroup, createSvgText, editSvgText, svgNS } from './utils/svg'
import type { CustomSvg, Topic } from './types/dom'
import type { MindElixirInstance, Uid } from './index'
const highlightColor = '#4dc4ff'
export interface Arrow {
id: string
/**
* label of arrow
*/
label: string
/**
* id of start node
*/
from: Uid
/**
* id of end node
*/
to: Uid
/**
* offset of control point from start point
*/
delta1: {
x: number
y: number
}
/**
* offset of control point from end point
*/
delta2: {
x: number
y: number
}
/**
* whether the arrow is bidirectional
*/
bidirectional?: boolean
/**
* style properties for the arrow
*/
style?: {
/**
* stroke color of the arrow
*/
stroke?: string
/**
* stroke width of the arrow
*/
strokeWidth?: string | number
/**
* stroke dash array for dashed lines
*/
strokeDasharray?: string
/**
* stroke line cap style
*/
strokeLinecap?: 'butt' | 'round' | 'square'
/**
* opacity of the arrow
*/
opacity?: string | number
/**
* color of the arrow label
*/
labelColor?: string
}
}
export type DivData = {
cx: number // center x
cy: number // center y
w: number // div width
h: number // div height
ctrlX: number // control point x
ctrlY: number // control point y
}
export type ArrowOptions = {
bidirectional?: boolean
style?: {
stroke?: string
strokeWidth?: string | number
strokeDasharray?: string
strokeLinecap?: 'butt' | 'round' | 'square'
opacity?: string | number
labelColor?: string
}
}
/**
* Calculate bezier curve midpoint position
*/
function calcBezierMidPoint(p1x: number, p1y: number, p2x: number, p2y: number, p3x: number, p3y: number, p4x: number, p4y: number) {
return {
x: p1x / 8 + (p2x * 3) / 8 + (p3x * 3) / 8 + p4x / 8,
y: p1y / 8 + (p2y * 3) / 8 + (p3y * 3) / 8 + p4y / 8,
}
}
/**
* Update arrow label position
*/
function updateArrowLabel(label: SVGTextElement, x: number, y: number) {
setAttributes(label, {
x: x + '',
y: y + '',
})
}
/**
* Update control line position
*/
function updateControlLine(line: SVGElement, x1: number, y1: number, x2: number, y2: number) {
setAttributes(line, {
x1: x1 + '',
y1: y1 + '',
x2: x2 + '',
y2: y2 + '',
})
}
/**
* Update arrow path and related elements
*/
function updateArrowPath(
arrow: CustomSvg,
p1x: number,
p1y: number,
p2x: number,
p2y: number,
p3x: number,
p3y: number,
p4x: number,
p4y: number,
linkItem: Arrow
) {
const mainPath = `M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`
// Update main path
arrow.line.setAttribute('d', mainPath)
// Apply styles to the main line if they exist
if (linkItem.style) {
const style = linkItem.style
if (style.stroke) arrow.line.setAttribute('stroke', style.stroke)
if (style.strokeWidth) arrow.line.setAttribute('stroke-width', String(style.strokeWidth))
if (style.strokeDasharray) arrow.line.setAttribute('stroke-dasharray', style.strokeDasharray)
if (style.strokeLinecap) arrow.line.setAttribute('stroke-linecap', style.strokeLinecap)
if (style.opacity !== undefined) arrow.line.setAttribute('opacity', String(style.opacity))
}
// Update hotzone for main path (find the first hotzone path which corresponds to the main line)
const hotzones = arrow.querySelectorAll('path[stroke="transparent"]')
if (hotzones.length > 0) {
hotzones[0].setAttribute('d', mainPath)
}
// Update arrow head
const arrowPoint = getArrowPoints(p3x, p3y, p4x, p4y)
if (arrowPoint) {
const arrowPath1 = `M ${arrowPoint.x1} ${arrowPoint.y1} L ${p4x} ${p4y} L ${arrowPoint.x2} ${arrowPoint.y2}`
arrow.arrow1.setAttribute('d', arrowPath1)
// Update hotzone for arrow1
if (hotzones.length > 1) {
hotzones[1].setAttribute('d', arrowPath1)
}
// Apply styles to arrow head
if (linkItem.style) {
const style = linkItem.style
if (style.stroke) arrow.arrow1.setAttribute('stroke', style.stroke)
if (style.strokeWidth) arrow.arrow1.setAttribute('stroke-width', String(style.strokeWidth))
if (style.strokeLinecap) arrow.arrow1.setAttribute('stroke-linecap', style.strokeLinecap)
if (style.opacity !== undefined) arrow.arrow1.setAttribute('opacity', String(style.opacity))
}
}
// Update start arrow if bidirectional
if (linkItem.bidirectional) {
const arrowPointStart = getArrowPoints(p2x, p2y, p1x, p1y)
if (arrowPointStart) {
const arrowPath2 = `M ${arrowPointStart.x1} ${arrowPointStart.y1} L ${p1x} ${p1y} L ${arrowPointStart.x2} ${arrowPointStart.y2}`
arrow.arrow2.setAttribute('d', arrowPath2)
// Update hotzone for arrow2
if (hotzones.length > 2) {
hotzones[2].setAttribute('d', arrowPath2)
}
// Apply styles to start arrow head
if (linkItem.style) {
const style = linkItem.style
if (style.stroke) arrow.arrow2.setAttribute('stroke', style.stroke)
if (style.strokeWidth) arrow.arrow2.setAttribute('stroke-width', String(style.strokeWidth))
if (style.strokeLinecap) arrow.arrow2.setAttribute('stroke-linecap', style.strokeLinecap)
if (style.opacity !== undefined) arrow.arrow2.setAttribute('opacity', String(style.opacity))
}
}
}
// Update label position and color
const { x: halfx, y: halfy } = calcBezierMidPoint(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)
updateArrowLabel(arrow.label, halfx, halfy)
// Apply label color if specified
if (linkItem.style?.labelColor) {
arrow.label.setAttribute('fill', linkItem.style.labelColor)
}
// Update highlight layer
updateArrowHighlight(arrow)
}
/**
* calc control point, center point and div size
*/
function calcCtrlP(mei: MindElixirInstance, tpc: Topic, delta: { x: number; y: number }) {
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
const w = tpc.offsetWidth
const h = tpc.offsetHeight
const cx = x + w / 2
const cy = y + h / 2
const ctrlX = cx + delta.x
const ctrlY = cy + delta.y
return {
w,
h,
cx,
cy,
ctrlX,
ctrlY,
}
}
/**
* calc start and end point using control point and div status
*/
function calcP(data: DivData) {
let x, y
const k = (data.cy - data.ctrlY) / (data.ctrlX - data.cx)
if (k > data.h / data.w || k < -data.h / data.w) {
if (data.cy - data.ctrlY < 0) {
x = data.cx - data.h / 2 / k
y = data.cy + data.h / 2
} else {
x = data.cx + data.h / 2 / k
y = data.cy - data.h / 2
}
} else {
if (data.cx - data.ctrlX < 0) {
x = data.cx + data.w / 2
y = data.cy - (data.w * k) / 2
} else {
x = data.cx - data.w / 2
y = data.cy + (data.w * k) / 2
}
}
return {
x,
y,
}
}
/**
* FYI
* p1: start point
* p2: control point of start point
* p3: control point of end point
* p4: end point
*/
const drawArrow = function (mei: MindElixirInstance, from: Topic, to: Topic, obj: Arrow, isInitPaint?: boolean) {
if (!from || !to) {
return // not expand
}
const fromData = calcCtrlP(mei, from, obj.delta1)
const toData = calcCtrlP(mei, to, obj.delta2)
const { x: p1x, y: p1y } = calcP(fromData)
const { ctrlX: p2x, ctrlY: p2y } = fromData
const { ctrlX: p3x, ctrlY: p3y } = toData
const { x: p4x, y: p4y } = calcP(toData)
const arrowT = getArrowPoints(p3x, p3y, p4x, p4y)
if (!arrowT) return
const toArrow = `M ${arrowT.x1} ${arrowT.y1} L ${p4x} ${p4y} L ${arrowT.x2} ${arrowT.y2}`
let fromArrow = ''
if (obj.bidirectional) {
const arrowF = getArrowPoints(p2x, p2y, p1x, p1y)
if (!arrowF) return
fromArrow = `M ${arrowF.x1} ${arrowF.y1} L ${p1x} ${p1y} L ${arrowF.x2} ${arrowF.y2}`
}
const newSvgGroup = createSvgGroup(`M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`, toArrow, fromArrow, obj.style)
// Use extracted common function to calculate midpoint
const { x: halfx, y: halfy } = calcBezierMidPoint(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)
const labelColor = obj.style?.labelColor
const label = createSvgText(obj.label, halfx, halfy, {
anchor: 'middle',
color: labelColor,
dataType: 'custom-link',
})
newSvgGroup.appendChild(label)
newSvgGroup.label = label
newSvgGroup.arrowObj = obj
newSvgGroup.dataset.linkid = obj.id
mei.linkSvgGroup.appendChild(newSvgGroup)
if (!isInitPaint) {
mei.arrows.push(obj)
mei.currentArrow = newSvgGroup
showLinkController(mei, obj, fromData, toData)
}
}
export const createArrow = function (this: MindElixirInstance, from: Topic, to: Topic, options: ArrowOptions = {}) {
const arrowObj = {
id: generateUUID(),
label: 'Custom Link',
from: from.nodeObj.id,
to: to.nodeObj.id,
delta1: {
x: from.offsetWidth / 2 + 100,
y: 0,
},
delta2: {
x: to.offsetWidth / 2 + 100,
y: 0,
},
...options,
}
drawArrow(this, from, to, arrowObj)
this.bus.fire('operation', {
name: 'createArrow',
obj: arrowObj,
})
}
export const createArrowFrom = function (this: MindElixirInstance, arrow: Omit<Arrow, 'id'>) {
hideLinkController(this)
const arrowObj = { ...arrow, id: generateUUID() }
drawArrow(this, this.findEle(arrowObj.from), this.findEle(arrowObj.to), arrowObj)
this.bus.fire('operation', {
name: 'createArrow',
obj: arrowObj,
})
}
export const removeArrow = function (this: MindElixirInstance, linkSvg?: CustomSvg) {
let link
if (linkSvg) {
link = linkSvg
} else {
link = this.currentArrow
}
if (!link) return
hideLinkController(this)
const id = link.arrowObj!.id
this.arrows = this.arrows.filter(arrow => arrow.id !== id)
link.remove()
this.bus.fire('operation', {
name: 'removeArrow',
obj: {
id,
},
})
}
export const selectArrow = function (this: MindElixirInstance, link: CustomSvg) {
this.currentArrow = link
const obj = link.arrowObj
const from = this.findEle(obj.from)
const to = this.findEle(obj.to)
const fromData = calcCtrlP(this, from, obj.delta1)
const toData = calcCtrlP(this, to, obj.delta2)
showLinkController(this, obj, fromData, toData)
}
export const unselectArrow = function (this: MindElixirInstance) {
hideLinkController(this)
this.currentArrow = null
}
/**
* Create a highlight path element with common attributes
*/
const createHighlightPath = function (d: string, highlightColor: string): SVGPathElement {
const path = document.createElementNS(svgNS, 'path')
setAttributes(path, {
d,
stroke: highlightColor,
fill: 'none',
'stroke-width': '6',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
})
return path
}
const addArrowHighlight = function (arrow: CustomSvg, highlightColor: string) {
const highlightGroup = document.createElementNS(svgNS, 'g')
highlightGroup.setAttribute('class', 'arrow-highlight')
highlightGroup.setAttribute('opacity', '0.45')
const highlightLine = createHighlightPath(arrow.line.getAttribute('d')!, highlightColor)
highlightGroup.appendChild(highlightLine)
const highlightArrow1 = createHighlightPath(arrow.arrow1.getAttribute('d')!, highlightColor)
highlightGroup.appendChild(highlightArrow1)
if (arrow.arrow2.getAttribute('d')) {
const highlightArrow2 = createHighlightPath(arrow.arrow2.getAttribute('d')!, highlightColor)
highlightGroup.appendChild(highlightArrow2)
}
arrow.insertBefore(highlightGroup, arrow.firstChild)
}
const removeArrowHighlight = function (arrow: CustomSvg) {
const highlightGroup = arrow.querySelector('.arrow-highlight')
if (highlightGroup) {
highlightGroup.remove()
}
}
const updateArrowHighlight = function (arrow: CustomSvg) {
const highlightGroup = arrow.querySelector('.arrow-highlight')
if (!highlightGroup) return
const highlightPaths = highlightGroup.querySelectorAll('path')
if (highlightPaths.length >= 1) {
highlightPaths[0].setAttribute('d', arrow.line.getAttribute('d')!)
}
if (highlightPaths.length >= 2) {
highlightPaths[1].setAttribute('d', arrow.arrow1.getAttribute('d')!)
}
if (highlightPaths.length >= 3 && arrow.arrow2.getAttribute('d')) {
highlightPaths[2].setAttribute('d', arrow.arrow2.getAttribute('d')!)
}
}
const hideLinkController = function (mei: MindElixirInstance) {
mei.helper1?.destroy!()
mei.helper2?.destroy!()
mei.linkController.style.display = 'none'
mei.P2.style.display = 'none'
mei.P3.style.display = 'none'
if (mei.currentArrow) {
removeArrowHighlight(mei.currentArrow)
}
}
const showLinkController = function (mei: MindElixirInstance, linkItem: Arrow, fromData: DivData, toData: DivData) {
const { linkController, P2, P3, line1, line2, nodes, map, currentArrow, bus } = mei
if (!currentArrow) return
linkController.style.display = 'initial'
P2.style.display = 'initial'
P3.style.display = 'initial'
nodes.appendChild(linkController)
nodes.appendChild(P2)
nodes.appendChild(P3)
addArrowHighlight(currentArrow, highlightColor)
// init points
let { x: p1x, y: p1y } = calcP(fromData)
let { ctrlX: p2x, ctrlY: p2y } = fromData
let { ctrlX: p3x, ctrlY: p3y } = toData
let { x: p4x, y: p4y } = calcP(toData)
P2.style.cssText = `top:${p2y}px;left:${p2x}px;`
P3.style.cssText = `top:${p3y}px;left:${p3x}px;`
updateControlLine(line1, p1x, p1y, p2x, p2y)
updateControlLine(line2, p3x, p3y, p4x, p4y)
mei.helper1 = LinkDragMoveHelper.create(P2)
mei.helper2 = LinkDragMoveHelper.create(P3)
mei.helper1.init(map, (deltaX, deltaY) => {
// recalc key points
p2x = p2x + deltaX / mei.scaleVal // scale should keep the latest value
p2y = p2y + deltaY / mei.scaleVal
const p1 = calcP({ ...fromData, ctrlX: p2x, ctrlY: p2y })
p1x = p1.x
p1y = p1.y
// update dom position
P2.style.top = p2y + 'px'
P2.style.left = p2x + 'px'
// Use extracted common function to update arrow
updateArrowPath(currentArrow, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, linkItem)
updateControlLine(line1, p1x, p1y, p2x, p2y)
linkItem.delta1.x = p2x - fromData.cx
linkItem.delta1.y = p2y - fromData.cy
bus.fire('updateArrowDelta', linkItem)
})
mei.helper2.init(map, (deltaX, deltaY) => {
p3x = p3x + deltaX / mei.scaleVal
p3y = p3y + deltaY / mei.scaleVal
const p4 = calcP({ ...toData, ctrlX: p3x, ctrlY: p3y })
p4x = p4.x
p4y = p4.y
P3.style.top = p3y + 'px'
P3.style.left = p3x + 'px'
// Use extracted common function to update arrow
updateArrowPath(currentArrow, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, linkItem)
updateControlLine(line2, p3x, p3y, p4x, p4y)
linkItem.delta2.x = p3x - toData.cx
linkItem.delta2.y = p3y - toData.cy
bus.fire('updateArrowDelta', linkItem)
})
}
export function renderArrow(this: MindElixirInstance) {
this.linkSvgGroup.innerHTML = ''
for (let i = 0; i < this.arrows.length; i++) {
const link = this.arrows[i]
try {
drawArrow(this, this.findEle(link.from), this.findEle(link.to), link, true)
} catch (e) {
console.warn('Node may not be expanded')
}
}
this.nodes.appendChild(this.linkSvgGroup)
}
export function editArrowLabel(this: MindElixirInstance, el: CustomSvg) {
hideLinkController(this)
console.time('editSummary')
if (!el) return
const textEl = el.label
editSvgText(this, textEl, el.arrowObj)
console.timeEnd('editSummary')
}
export function tidyArrow(this: MindElixirInstance) {
this.arrows = this.arrows.filter(arrow => {
return getObjById(arrow.from, this.nodeData) && getObjById(arrow.to, this.nodeData)
})
}

View File

@ -1,62 +0,0 @@
import type { Theme } from '.'
export const LEFT = 0
export const RIGHT = 1
export const SIDE = 2
export const DOWN = 3
export const THEME: Theme = {
name: 'Latte',
type: 'light',
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
cssVar: {
'--node-gap-x': '30px',
'--node-gap-y': '10px',
'--main-gap-x': '65px',
'--main-gap-y': '45px',
'--root-radius': '30px',
'--main-radius': '20px',
'--root-color': '#ffffff',
'--root-bgcolor': '#4c4f69',
'--root-border-color': 'rgba(0, 0, 0, 0)',
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--topic-padding': '3px',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
'--selected': '#4dc4ff',
'--accent-color': '#e64553',
'--panel-color': '#444446',
'--panel-bgcolor': '#ffffff',
'--panel-border-color': '#eaeaea',
'--map-padding': '50px',
},
}
export const DARK_THEME: Theme = {
name: 'Dark',
type: 'dark',
palette: ['#848FA0', '#748BE9', '#D2F9FE', '#4145A5', '#789AFA', '#706CF4', '#EF987F', '#775DD5', '#FCEECF', '#DA7FBC'],
cssVar: {
'--node-gap-x': '30px',
'--node-gap-y': '10px',
'--main-gap-x': '65px',
'--main-gap-y': '45px',
'--root-radius': '30px',
'--main-radius': '20px',
'--root-color': '#ffffff',
'--root-bgcolor': '#2d3748',
'--root-border-color': 'rgba(255, 255, 255, 0.1)',
'--main-color': '#ffffff',
'--main-bgcolor': '#4c4f69',
'--topic-padding': '3px',
'--color': '#cccccc',
'--bgcolor': '#252526',
'--selected': '#4dc4ff',
'--accent-color': '#789AFA',
'--panel-color': '#ffffff',
'--panel-bgcolor': '#2d3748',
'--panel-border-color': '#696969',
'--map-padding': '50px 80px',
},
}

View File

@ -1,68 +0,0 @@
import MindElixir from 'mind-elixir'
import example from 'mind-elixir/example'
import type { Options } from 'mind-elixir'
const E = MindElixir.E
const options: Options = {
el: '#map',
newTopicName: '子节点',
// direction: MindElixir.LEFT,
direction: MindElixir.RIGHT,
// data: MindElixir.new('new topic'),
locale: 'en',
draggable: true,
editable: true,
contextMenu: {
focus: true,
link: true,
extend: [
{
name: 'Node edit',
onclick: () => {
alert('extend menu')
},
},
],
},
toolBar: true,
keypress: true,
allowUndo: false,
before: {
moveDownNode() {
return false
},
insertSibling(el, obj) {
console.log('insertSibling', el, obj)
return true
},
async addChild(el, obj) {
return true
},
},
scaleSensitivity: 0.2,
}
const mind = new MindElixir(options)
mind.init(example)
function sleep() {
return new Promise<void>(res => {
setTimeout(() => res(), 1000)
})
}
console.log('test E function', E('bd4313fbac40284b'))
// let mind2 = new MindElixir({
// el: '#map2',
// direction: 2,
// data: MindElixir.example2,
// draggable: false,
// // overflowHidden: true,
// nodeMenu: true,
// })
// mind2.init()
mind.bus.addListener('operation', operation => {
console.log(operation)
})
mind.bus.addListener('selectNodes', nodes => {
console.log(nodes)
})

View File

@ -1,192 +0,0 @@
import type { MindElixirCtor } from './index'
import MindElixir from './index'
import example from './exampleData/1'
import example2 from './exampleData/2'
import example3 from './exampleData/3'
import type { Options, MindElixirInstance, NodeObj } from './types/index'
import type { Operation } from './utils/pubsub'
import 'katex/dist/katex.min.css'
import katex from 'katex'
import { layoutSSR, renderSSRHTML } from './utils/layout-ssr'
import { snapdom } from '@zumer/snapdom'
import type { Tokens } from 'marked'
import { marked } from 'marked'
import { md2html } from 'simple-markdown-to-html'
interface Window {
m?: MindElixirInstance
M: MindElixirCtor
E: typeof MindElixir.E
downloadPng: () => void
downloadSvg: () => void
destroy: () => void
testMarkdown: () => void
addMarkdownNode: () => void
}
declare let window: Window
const E = MindElixir.E
const options: Options = {
el: '#map',
newTopicName: '子节点',
locale: 'en',
// mouseSelectionButton: 2,
draggable: true,
editable: true,
markdown: (text: string, obj: NodeObj & { useMd?: boolean }) => {
if (!text) return ''
if (!obj.useMd) return text
try {
// Configure marked renderer to add target="_blank" to links and table classes
const renderer = {
link(token: Tokens.Link) {
const href = token.href || ''
const title = token.title ? ` title="${token.title}"` : ''
const text = token.text || ''
return `<a href="${href}"${title} target="_blank">${text}</a>`
},
table(token: Tokens.Table) {
const header = token.header.map(cell => `<th>${cell.text}</th>`).join('')
const rows = token.rows.map(row =>
`<tr>${row.map(cell => `<td>${cell.text}</td>`).join('')}</tr>`
).join('')
return `<table class="markdown-table"><thead><tr>${header}</tr></thead><tbody>${rows}</tbody></table>`
},
}
marked.use({ renderer, gfm: true })
let html = marked(text) as string
// let html = md2html(text)
// Process KaTeX math expressions
// Handle display math ($$...$$)
html = html.replace(/\$\$([^$]+)\$\$/g, (_, math) => {
return katex.renderToString(math.trim(), { displayMode: true })
})
// Handle inline math ($...$)
html = html.replace(/\$([^$]+)\$/g, (_, math) => {
return katex.renderToString(math.trim(), { displayMode: false })
})
return html.trim().replace(/\n/g, '')
} catch (error) {
return text
}
},
// To disable markdown, simply omit the markdown option or set it to undefined
// if you set contextMenu to false, you should handle contextmenu event by yourself, e.g. preventDefault
contextMenu: {
focus: true,
link: true,
extend: [
{
name: 'Node edit',
onclick: () => {
alert('extend menu')
},
},
],
},
toolBar: true,
keypress: {
e(e) {
if (!mind.currentNode) return
if (e.metaKey || e.ctrlKey) {
mind.expandNode(mind.currentNode)
}
},
f(e) {
if (!mind.currentNode) return
if (e.altKey) {
if (mind.isFocusMode) {
mind.cancelFocus()
} else {
mind.focusNode(mind.currentNode)
}
}
},
},
allowUndo: true,
before: {
insertSibling(el, obj) {
console.log('insertSibling', el, obj)
return true
},
async addChild(el, obj) {
console.log('addChild', el, obj)
// await sleep()
return true
},
},
// scaleMin:0.1
// alignment: 'nodes',
}
let mind = new MindElixir(options)
const data = MindElixir.new('new topic')
mind.init(example)
const m2 = new MindElixir({
el: '#map2',
selectionContainer: 'body', // use body to make selection usable when transform is not 0
direction: MindElixir.RIGHT,
theme: MindElixir.DARK_THEME,
// alignment: 'nodes',
})
m2.init(data)
function sleep() {
return new Promise<void>(res => {
setTimeout(() => res(), 1000)
})
}
// console.log('test E function', E('bd4313fbac40284b'))
mind.bus.addListener('operation', (operation: Operation) => {
console.log(operation)
// return {
// name: action name,
// obj: target object
// }
// name: [insertSibling|addChild|removeNode|beginEdit|finishEdit]
// obj: target
// name: moveNodeIn
// obj: {from:target1,to:target2}
})
mind.bus.addListener('selectNodes', nodes => {
console.log('selectNodes', nodes)
})
mind.bus.addListener('unselectNodes', nodes => {
console.log('unselectNodes', nodes)
})
mind.bus.addListener('expandNode', node => {
console.log('expandNode: ', node)
})
const dl2 = async () => {
const result = await snapdom(mind.nodes)
await result.download({ format: 'jpg', filename: 'my-capture' })
}
window.downloadPng = dl2
window.m = mind
// window.m2 = mind2
window.M = MindElixir
window.E = MindElixir.E
console.log('MindElixir Version', MindElixir.version)
window.destroy = () => {
mind.destroy()
// @ts-expect-error remove reference
mind = null
// @ts-expect-error remove reference
window.m = null
}
document.querySelector('#ssr')!.innerHTML = renderSSRHTML(layoutSSR(window.m.nodeData))

View File

@ -1,26 +0,0 @@
import type { Arrow } from './arrow'
import type methods from './methods'
import type { MindElixirMethods } from './methods'
import type { Summary, SummarySvgGroup } from './summary'
import type { MindElixirData, MindElixirInstance, NodeObj, NodeObjExport, Options, Theme } from './types'
import type { MainLineParams, SubLineParams } from './utils/generateBranch'
import type { Locale } from './i18n'
export {
methods,
Theme,
Options,
MindElixirMethods,
MindElixirInstance,
MindElixirData,
NodeObj,
NodeObjExport,
Summary,
SummarySvgGroup,
Arrow,
MainLineParams,
SubLineParams,
Locale,
}
export type * from './types/dom'
export type * from './utils/pubsub'

View File

@ -1,544 +0,0 @@
import type { MindElixirData } from '../index'
import { codeBlock, katexHTML, styledDiv } from './htmlText'
const aboutMindElixir: MindElixirData = {
nodeData: {
id: 'me-root',
topic: 'Mind Elixir',
tags: ['思维导图内核'],
children: [
{
topic: 'logo2',
id: '56dae51a90d350a8',
direction: 0,
expanded: true,
children: [
{
id: 'use-image',
topic: 'mind-elixir',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png',
height: 100,
width: 90,
fit: 'contain',
},
},
],
},
{
topic: '什么是 Mind Elixir',
id: 'bd4313fbac40284b',
direction: 0,
expanded: true,
children: [
{
topic: '一个思维导图内核',
id: 'beeb823afd6d2114',
},
{
topic: '免费',
id: 'c1f068377de9f3a0',
},
{
topic: '开源',
id: 'c1f06d38a09f23ca',
},
{
topic: '无框架依赖',
id: 'c1f06e4cbcf16463',
expanded: true,
children: [
{
topic: '无需 JavaScript 框架即可使用',
id: 'c1f06e4cbcf16464',
},
{
topic: '可插件化',
id: 'c1f06e4cbcf16465',
},
],
},
{
topic: '在你的项目中使用',
id: 'c1f1f11a7fbf7550',
children: [
{
topic: "import MindElixir from 'mind-elixir'",
id: 'c1f1e245b0a89f9b',
},
{
topic: 'new MindElixir({...}).init(data)',
id: 'c1f1ebc7072c8928',
},
],
},
{
topic: '核心特性',
id: 'c1f0723c07b408d7',
expanded: true,
children: [
{
topic: '流畅的用户体验',
id: 'c1f09612fd89920d',
},
{
topic: '精心设计',
id: 'c1f09612fd89920e',
},
{
topic: '移动端友好',
id: 'c1f09612fd89920f',
},
{
topic: '轻量级 & 高性能',
id: 'c1f09612fd899210',
},
],
},
],
},
{
topic: '高效快捷键',
id: 'bd1b66c4b56754d9',
direction: 0,
expanded: true,
children: [
{
topic: 'Tab - 创建子节点',
id: 'bd1b6892bcab126a',
},
{
topic: 'Enter - 创建同级节点',
id: 'bd1b6b632a434b27',
},
{
topic: 'F1 - 居中地图',
id: 'bd1b983085187c0a',
},
{
topic: 'F2 - 开始编辑',
id: 'bd1b983085187c0b',
},
{
topic: 'Ctrl + C/V - 复制/粘贴',
id: 'bd1b983085187c0c',
},
{
topic: 'Ctrl + +/- - 放大/缩小',
id: 'bd1b983085187c0d',
},
],
},
{
topic: '高级功能',
id: 'bd1b66c4b56754da',
direction: 0,
expanded: true,
children: [
{
topic: '支持批量操作',
id: 'bd1b6892bcab126b',
tags: ['新功能'],
},
{
topic: '撤销 / 重做',
id: 'bd1b6b632a434b28',
tags: ['新功能'],
},
{
topic: '节点总结',
id: 'bd1b983085187c0e',
},
{
topic: '使用 CSS 变量轻松样式化',
id: 'bd1b983085187c0f',
tags: ['新功能'],
},
],
},
{
topic: '专注模式',
id: 'bd1b9b94a9a7a913',
direction: 1,
expanded: true,
children: [
{
topic: '右键点击并选择专注模式',
id: 'bd1bb2ac4bbab458',
},
{
topic: '右键点击并选择取消专注模式',
id: 'bd1bb4b14d6697c3',
},
],
},
{
topic: '左侧菜单',
id: 'bd1b9d1816ede134',
direction: 0,
expanded: true,
children: [
{
topic: '节点分布',
id: 'bd1ba11e620c3c1a',
expanded: true,
children: [
{
topic: '左侧',
id: 'bd1c1cb51e6745d3',
},
{
topic: '右侧',
id: 'bd1c1e12fd603ff6',
},
{
topic: '左右两侧',
id: 'bd1c1f03def5c97b',
},
],
},
],
},
{
topic: '底部菜单',
id: 'bd1ba66996df4ba4',
direction: 1,
expanded: true,
children: [
{
topic: '全屏',
id: 'bd1ba81d9bc95a7e',
},
{
topic: '回到中心',
id: 'bd1babdd5c18a7a2',
},
{
topic: '放大',
id: 'bd1bae68e0ab186e',
},
{
topic: '缩小',
id: 'bd1bb06377439977',
},
],
},
{
topic: '连接',
id: 'bd1beff607711025',
direction: 0,
expanded: true,
children: [
{
topic: '右键点击并选择连接',
id: 'bd1bf320da90046a',
},
{
topic: '点击要连接的目标',
id: 'bd1bf6f94ff2e642',
},
{
topic: '使用控制点修改连接',
id: 'bd1c0c4a487bd036',
},
{
topic: '双向连接',
id: '4da8dbbc7b71be99',
},
{
topic: '也可用。',
id: '4da8ded27033a710',
},
],
},
{
topic: '节点样式',
id: 'bd1c217f9d0b20bd',
direction: 0,
expanded: true,
children: [
{
topic: '字体大小',
id: 'bd1c24420cd2c2f5',
style: {
fontSize: '32px',
color: '#3298db',
},
},
{
topic: '字体颜色',
id: 'bd1c2a59b9a2739c',
style: {
color: '#c0392c',
},
},
{
topic: '背景颜色',
id: 'bd1c2de33f057eb4',
style: {
color: '#bdc3c7',
background: '#2c3e50',
},
},
{
topic: '添加标签',
id: 'bd1cff58364436d0',
tags: ['已完成'],
},
{
topic: '添加图标',
id: 'bd1d0317f7e8a61a',
icons: ['😂'],
tags: ['www'],
},
{
topic: '加粗',
id: 'bd41fd4ca32322a4',
style: {
fontWeight: 'bold',
},
},
{
topic: '超链接',
id: 'bd41fd4ca32322a5',
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
},
],
},
{
topic: '可拖拽',
id: 'bd1f03fee1f63bc6',
direction: 1,
expanded: true,
children: [
{
topic: '将一个节点拖拽到另一个节点\n前者将成为后者的子节点',
id: 'bd1f07c598e729dc',
},
],
},
{
topic: '导出 & 导入',
id: 'beeb7586973430db',
direction: 1,
expanded: true,
children: [
{
topic: '导出为 SVG',
id: 'beeb7a6bec2d68e6',
},
{
topic: '导出为 PNG',
id: 'beeb7a6bec2d68e7',
tags: ['新功能'],
},
{
topic: '导出 JSON 数据',
id: 'beeb784cc189375f',
},
{
topic: '导出为 HTML',
id: 'beeb7a6bec2d68f5',
},
],
},
{
topic: '生态系统',
id: 'beeb7586973430dc',
direction: 1,
expanded: true,
children: [
{
topic: '@mind-elixir/node-menu',
id: 'beeb7586973430dd',
hyperLink: 'https://github.com/ssshooter/node-menu',
},
{
topic: '@mind-elixir/export-xmind',
id: 'beeb7586973430de',
hyperLink: 'https://github.com/ssshooter/export-xmind',
},
{
topic: 'mind-elixir-react',
id: 'beeb7586973430df',
hyperLink: 'https://github.com/ssshooter/mind-elixir-react',
},
],
},
{
topic: 'dangerouslySetInnerHTML',
id: 'c00a1cf60baa44f0',
style: {
background: '#f1c40e',
},
children: [
{
topic: 'Katex',
id: 'c00a2264f4532611',
children: [
{
topic: '',
id: 'c00a2264f4532612',
dangerouslySetInnerHTML:
'<div class="math math-display"><span class="katex-display"><span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.4em;vertical-align:-0.95em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size1">[</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.85em;"><span style="top:-3.01em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">x</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.35em;"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em;"></span><span class="arraycolsep" style="width:0.5em;"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.85em;"><span style="top:-3.01em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.35em;"><span></span></span></span></span></span></span></span><span class="mclose delimcenter" style="top:0em;"><span class="delimsizing size1">]</span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size3">[</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.45em;"><span style="top:-3.61em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">a</span></span></span><span style="top:-2.41em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">b</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.95em;"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em;"></span><span class="arraycolsep" style="width:0.5em;"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.45em;"><span style="top:-3.61em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">c</span></span></span><span style="top:-2.41em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.95em;"><span></span></span></span></span></span></span></span><span class="mclose delimcenter" style="top:0em;"><span class="delimsizing size3">]</span></span></span></span></span></span></span></div>',
},
],
},
{
topic: '代码块',
id: 'c00a2264fdaw32612',
children: [
{
topic: '',
id: 'c00a2264f4532613',
dangerouslySetInnerHTML:
'<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">let</span> message <span class="token operator">=</span> <span class="token string">"Hello world"</span>\n<span class="token function">alert</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span></code></pre>',
},
],
},
{
topic: '自定义 Div',
id: 'c00a2264f4532615',
children: [
{
topic: '',
id: 'c00a2264f4532614',
dangerouslySetInnerHTML:
'<div><style>.title{font-size:50px}</style><div class="title">标题</div><div style="color: red; font-size: 20px;">你好世界</div></div>',
},
],
},
],
direction: 1,
},
{
topic: '主题系统',
id: 'bd42dad21aaf6baf',
direction: 1,
expanded: true,
children: [
{
topic: '内置主题',
id: 'bd42e1d0163ebf05',
expanded: true,
children: [
{
topic: 'Latte (浅色)',
id: 'bd42e619051878b4',
style: {
background: '#ffffff',
color: '#444446',
},
},
{
topic: '深色主题',
id: 'bd42e97d7ac35e9a',
style: {
background: '#252526',
color: '#ffffff',
},
},
],
},
{
topic: '自定义 CSS 变量',
id: 'bd42e1d0163ebf06',
tags: ['灵活'],
},
{
topic: '调色板自定义',
id: 'bd42e1d0163ebf07',
tags: ['10 种颜色'],
},
],
},
],
expanded: true,
},
arrows: [
{
id: 'ac5fb1df7345e9c4',
label: '渲染',
from: 'beeb784cc189375f',
to: 'beeb7a6bec2d68f5',
delta1: {
x: 142.8828125,
y: -57,
},
delta2: {
x: 146.1171875,
y: 45,
},
bidirectional: false,
},
{
id: '4da8e3367b63b640',
label: '双向!',
from: '4da8dbbc7b71be99',
to: '4da8ded27033a710',
delta1: {
x: -186,
y: 7,
},
delta2: {
x: -155,
y: 28,
},
bidirectional: true,
style: {
stroke: '#8839ef',
labelColor: '#8839ef',
strokeWidth: '2',
strokeDasharray: '2,5',
opacity: '1',
},
},
],
summaries: [
{
id: 'a5e68e6a2ce1b648',
parent: 'bd42e1d0163ebf04',
start: 0,
end: 1,
label: '总结',
},
{
id: 'a5e6978f1bc69f4a',
parent: 'bd4313fbac40284b',
start: 3,
end: 5,
label: '总结',
},
],
direction: 2,
theme: {
name: 'Latte',
// 更新的调色板,颜色更鲜艳
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
// 增强的 CSS 变量,更好的样式控制
cssVar: {
'--node-gap-x': '30px',
'--node-gap-y': '10px',
'--main-gap-x': '32px',
'--main-gap-y': '12px',
'--root-radius': '30px',
'--main-radius': '20px',
'--root-color': '#ffffff',
'--root-bgcolor': '#4c4f69',
'--root-border-color': 'rgba(0, 0, 0, 0)',
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--topic-padding': '3px',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
'--selected': '#4dc4ff',
'--accent-color': '#e64553',
'--panel-color': '#444446',
'--panel-bgcolor': '#ffffff',
'--panel-border-color': '#eaeaea',
'--map-padding': '50px 80px',
},
},
}
export default aboutMindElixir

View File

@ -1,623 +0,0 @@
import type { MindElixirData, NodeObj } from '../index'
import { codeBlock, katexHTML, styledDiv } from './htmlText'
type NodeObjWithUseMd = NodeObj & { useMd?: boolean }
type MindElixirDataWithUseMd = Omit<MindElixirData, 'nodeData'> & {
nodeData: NodeObjWithUseMd & {
children?: NodeObjWithUseMd[]
}
}
const aboutMindElixir: MindElixirDataWithUseMd = {
nodeData: {
id: 'me-root',
topic: 'Mind Elixir',
tags: ['Mind Map Core'],
children: [
{
topic: 'logo2',
id: '56dae51a90d350a8',
direction: 0,
expanded: true,
children: [
{
id: 'use-image',
topic: 'mind-elixir',
image: {
url: 'https://raw.githubusercontent.com/ssshooter/mind-elixir-core/master/images/logo2.png',
height: 100,
width: 90,
fit: 'contain',
},
},
],
},
{
topic: 'What is Mind Elixir',
id: 'bd4313fbac40284b',
direction: 0,
expanded: true,
children: [
{
topic: 'A mind map core',
id: 'beeb823afd6d2114',
},
{
topic: 'Free',
id: 'c1f068377de9f3a0',
},
{
topic: 'Open-Source',
id: 'c1f06d38a09f23ca',
},
{
topic: 'Framework agnostic',
id: 'c1f06e4cbcf16463',
expanded: true,
children: [
{
topic: 'Use without JavaScript framework',
id: 'c1f06e4cbcf16464',
},
{
topic: 'Pluginable',
id: 'c1f06e4cbcf16465',
},
],
},
{
topic: 'Use in your own project',
id: 'c1f1f11a7fbf7550',
children: [
{
topic: "import MindElixir from 'mind-elixir'",
id: 'c1f1e245b0a89f9b',
},
{
topic: 'new MindElixir({...}).init(data)',
id: 'c1f1ebc7072c8928',
},
],
},
{
topic: 'Key Features',
id: 'c1f0723c07b408d7',
expanded: true,
children: [
{
topic: 'Fluent UX',
id: 'c1f09612fd89920d',
},
{
topic: 'Well designed',
id: 'c1f09612fd89920e',
},
{
topic: 'Mobile friendly',
id: 'c1f09612fd89920f',
},
{
topic: 'Lightweight & High performance',
id: 'c1f09612fd899210',
},
],
},
],
},
{
topic: 'Efficient Shortcuts',
id: 'bd1b66c4b56754d9',
direction: 0,
expanded: true,
children: [
{
topic: 'Tab - Create a child node',
id: 'bd1b6892bcab126a',
},
{
topic: 'Enter - Create a sibling node',
id: 'bd1b6b632a434b27',
},
{
topic: 'F1 - Center the Map',
id: 'bd1b983085187c0a',
},
{
topic: 'F2 - Begin Editing',
id: 'bd1b983085187c0b',
},
{
topic: 'Ctrl + C/V - Copy/Paste',
id: 'bd1b983085187c0c',
},
{
topic: 'Ctrl + +/- - Zoom In/Out',
id: 'bd1b983085187c0d',
},
],
},
{
topic: 'Advanced Features',
id: 'bd1b66c4b56754da',
direction: 0,
expanded: true,
children: [
{
topic: 'Bulk operations supported',
id: 'bd1b6892bcab126b',
tags: ['New'],
},
{
topic: 'Undo / Redo',
id: 'bd1b6b632a434b28',
tags: ['New'],
},
{
topic: 'Summarize nodes',
id: 'bd1b983085187c0e',
},
{
topic: 'Easily Styling with CSS variables',
id: 'bd1b983085187c0f',
tags: ['New'],
},
],
},
{
topic: 'Focus mode',
id: 'bd1b9b94a9a7a913',
direction: 1,
expanded: true,
children: [
{
topic: 'Right click and select Focus Mode',
id: 'bd1bb2ac4bbab458',
},
{
topic: 'Right click and select Cancel Focus Mode',
id: 'bd1bb4b14d6697c3',
},
],
},
{
topic: 'Left menu',
id: 'bd1b9d1816ede134',
direction: 0,
expanded: true,
children: [
{
topic: 'Node distribution',
id: 'bd1ba11e620c3c1a',
expanded: true,
children: [
{
topic: 'Left',
id: 'bd1c1cb51e6745d3',
},
{
topic: 'Right',
id: 'bd1c1e12fd603ff6',
},
{
topic: 'Both l & r',
id: 'bd1c1f03def5c97b',
},
],
},
],
},
{
topic: 'Bottom menu',
id: 'bd1ba66996df4ba4',
direction: 1,
expanded: true,
children: [
{
topic: 'Full screen',
id: 'bd1ba81d9bc95a7e',
},
{
topic: 'Return to Center',
id: 'bd1babdd5c18a7a2',
},
{
topic: 'Zoom in',
id: 'bd1bae68e0ab186e',
},
{
topic: 'Zoom out',
id: 'bd1bb06377439977',
},
],
},
{
topic: 'Link',
id: 'bd1beff607711025',
direction: 0,
expanded: true,
children: [
{
topic: 'Right click and select Link',
id: 'bd1bf320da90046a',
},
{
topic: 'Click the target you want to link',
id: 'bd1bf6f94ff2e642',
},
{
topic: 'Modify link with control points',
id: 'bd1c0c4a487bd036',
},
{
topic: 'Bidirectional link is',
id: '4da8dbbc7b71be99',
},
{
topic: 'Also available.',
id: '4da8ded27033a710',
},
],
},
{
topic: 'Node style',
id: 'bd1c217f9d0b20bd',
direction: 0,
expanded: true,
children: [
{
topic: 'Font Size',
id: 'bd1c24420cd2c2f5',
style: {
fontSize: '32px',
color: '#3298db',
},
},
{
topic: 'Font Color',
id: 'bd1c2a59b9a2739c',
style: {
color: '#c0392c',
},
},
{
topic: 'Background Color',
id: 'bd1c2de33f057eb4',
style: {
color: '#bdc3c7',
background: '#2c3e50',
},
},
{
topic: 'Add tags',
id: 'bd1cff58364436d0',
tags: ['Completed'],
},
{
topic: 'Add icons',
id: 'bd1d0317f7e8a61a',
icons: ['😂'],
tags: ['www'],
},
{
topic: 'Bolder',
id: 'bd41fd4ca32322a4',
style: {
fontWeight: 'bold',
},
},
{
topic: 'Hyper link',
id: 'bd41fd4ca32322a5',
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
},
],
},
{
topic: 'Draggable',
id: 'bd1f03fee1f63bc6',
direction: 1,
expanded: true,
children: [
{
topic: 'Drag a node to another node\nand the former one will become a child node of latter one',
id: 'bd1f07c598e729dc',
},
],
},
{
topic: 'Export & Import',
id: 'beeb7586973430db',
direction: 1,
expanded: true,
children: [
{
topic: 'Export as SVG',
id: 'beeb7a6bec2d68e6',
},
{
topic: 'Export as PNG',
id: 'beeb7a6bec2d68e7',
tags: ['New'],
},
{
topic: 'Export JSON data',
id: 'beeb784cc189375f',
},
{
topic: 'Export as HTML',
id: 'beeb7a6bec2d68f5',
},
],
},
{
topic: 'Ecosystem',
id: 'beeb7586973430dc',
direction: 1,
expanded: true,
children: [
{
topic: '@mind-elixir/node-menu',
id: 'beeb7586973430dd',
hyperLink: 'https://github.com/ssshooter/node-menu',
},
{
topic: '@mind-elixir/export-xmind',
id: 'beeb7586973430de',
hyperLink: 'https://github.com/ssshooter/export-xmind',
},
{
topic: 'mind-elixir-react',
id: 'beeb7586973430df',
hyperLink: 'https://github.com/ssshooter/mind-elixir-react',
},
],
},
{
topic: 'dangerouslySetInnerHTML',
id: 'c00a1cf60baa44f0',
style: {
background: '#f1c40e',
},
children: [
{
topic: 'Code Block',
id: 'c00a2264fdaw32612',
children: [
{
topic: '',
id: 'c00a2264f4532613',
dangerouslySetInnerHTML:
'<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">let</span> message <span class="token operator">=</span> <span class="token string">\'Hello world\'</span>\n<span class="token function">alert</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span></code></pre>',
},
],
},
{
topic: 'Customized Div',
id: 'c00a2264f4532615',
children: [
{
topic: '',
id: 'c00a2264f4532614',
dangerouslySetInnerHTML:
'<div><style>.title{font-size:50px}</style><div class="title">Title</div><div style="color: red; font-size: 20px;">Hello world</div></div>',
},
],
},
// {
// topic: 'Video',
// id: 'c00a2264ffadw19',
// children: [
// {
// topic: '',
// id: 'c00a2264f453fv14',
// dangerouslySetInnerHTML:
// '<iframe src="//player.bilibili.com/player.html?bvid=BV1aTxMehEjK&poster=1&autoplay=0&danmaku=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>',
// },
// ],
// },
],
direction: 1,
},
{
topic: 'KaTeX',
id: 'markdown-complex-math',
direction: 1,
expanded: true,
children: [
{
topic: 'Normal distribution: $$f(x) = \\frac{1}{\\sqrt{2\\pi\\sigma^2}} e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}$$',
id: 'markdown-normal-dist',
useMd: true,
},
{
topic: 'Fourier transform: $$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$',
id: 'markdown-fourier',
useMd: true,
},
{
topic: 'Taylor series: $$f(x) = \\sum_{n=0}^{\\infty} \\frac{f^{(n)}(a)}{n!}(x-a)^n$$',
id: 'markdown-taylor',
useMd: true,
},
{
topic: 'Schrödinger equation: $$i\\hbar\\frac{\\partial}{\\partial t}\\Psi = \\hat{H}\\Psi$$',
id: 'markdown-schrodinger',
useMd: true,
},
],
},
{
topic: 'Basic Markdown Examples',
id: 'markdown-basic-examples',
direction: 1,
expanded: true,
children: [
{
topic: '# Heading 1',
id: 'markdown-headings',
useMd: true,
},
{
topic: '**Bold text** and *italic text* and ***bold italic***',
id: 'markdown-emphasis',
useMd: true,
},
{
topic: '- Unordered list item 1\n- Unordered list item 2',
id: 'markdown-lists',
useMd: true,
},
{
topic: '[Link to GitHub](https://github.com) and `inline code`',
id: 'markdown-links-code',
useMd: true,
},
{
topic: '> This is a blockquote\n> with multiple lines',
id: 'markdown-blockquote',
useMd: true,
},
{
topic: '```javascript\nconst greeting = "Hello World!";\nconsole.log(greeting);\n```',
id: 'markdown-code-block',
useMd: true,
},
{
topic:
'| Column 1 | Column 2 | Column 3 |\n|----------|----------|----------|\n| Row 1 | Data 1 | Info 1 |\n| Row 2 | Data 2 | Info 2 |',
id: 'markdown-table',
useMd: true,
},
],
},
{
topic: 'Theme System',
id: 'bd42dad21aaf6baf',
direction: 1,
expanded: true,
children: [
{
topic: 'Built-in Themes',
id: 'bd42e1d0163ebf05',
expanded: true,
children: [
{
topic: 'Latte (Light)',
id: 'bd42e619051878b4',
style: {
background: '#ffffff',
color: '#444446',
},
},
{
topic: 'Dark Theme',
id: 'bd42e97d7ac35e9a',
style: {
background: '#252526',
color: '#ffffff',
},
},
],
},
{
topic: 'Custom CSS Variables',
id: 'bd42e1d0163ebf06',
tags: ['Flexible'],
},
{
topic: 'Color Palette Customization',
id: 'bd42e1d0163ebf07',
tags: ['10 Colors'],
},
],
},
],
expanded: true,
},
arrows: [
{
id: 'ac5fb1df7345e9c4',
label: 'Render',
from: 'beeb784cc189375f',
to: 'beeb7a6bec2d68f5',
delta1: {
x: 142.8828125,
y: -57,
},
delta2: {
x: 146.1171875,
y: 45,
},
bidirectional: false,
},
{
id: '4da8e3367b63b640',
label: 'Bidirectional!',
from: '4da8dbbc7b71be99',
to: '4da8ded27033a710',
delta1: {
x: -186,
y: 7,
},
delta2: {
x: -155,
y: 28,
},
bidirectional: true,
style: {
stroke: '#8839ef',
labelColor: '#8839ef',
strokeWidth: '2',
strokeDasharray: '2,5',
opacity: '1',
},
},
],
summaries: [
{
id: 'a5e68e6a2ce1b648',
parent: 'bd42e1d0163ebf04',
start: 0,
end: 1,
label: 'summary',
},
{
id: 'a5e6978f1bc69f4a',
parent: 'bd4313fbac40284b',
start: 3,
end: 5,
label: 'summary',
},
],
direction: 2,
theme: {
name: 'Latte',
// Updated color palette with more vibrant colors
palette: ['#dd7878', '#ea76cb', '#8839ef', '#e64553', '#fe640b', '#df8e1d', '#40a02b', '#209fb5', '#1e66f5', '#7287fd'],
// Enhanced CSS variables for better styling control
cssVar: {
'--node-gap-x': '30px',
'--node-gap-y': '10px',
'--main-gap-x': '32px',
'--main-gap-y': '12px',
'--root-radius': '30px',
'--main-radius': '20px',
'--root-color': '#ffffff',
'--root-bgcolor': '#4c4f69',
'--root-border-color': 'rgba(0, 0, 0, 0)',
'--main-color': '#444446',
'--main-bgcolor': '#ffffff',
'--topic-padding': '3px',
'--color': '#777777',
'--bgcolor': '#f6f6f6',
'--selected': '#4dc4ff',
'--accent-color': '#e64553',
'--panel-color': '#444446',
'--panel-bgcolor': '#ffffff',
'--panel-border-color': '#eaeaea',
'--map-padding': '50px 80px',
},
},
}
export default aboutMindElixir as MindElixirData

View File

@ -1,114 +0,0 @@
import type { MindElixirData } from '../index'
import MindElixir from '../index'
const mindElixirStruct: MindElixirData = {
direction: 1,
theme: MindElixir.DARK_THEME,
nodeData: {
id: 'me-root',
topic: 'HTML structure',
children: [
{
topic: 'div.map-container',
id: '33905a6bde6512e4',
expanded: true,
children: [
{
topic: 'div.map-canvas',
id: '33905d3c66649e8f',
tags: ['A special case of a `grp` tag'],
expanded: true,
children: [
{
topic: 'me-root',
id: '33906b754897b9b9',
tags: ['A special case of a `t` tag'],
expanded: true,
children: [{ topic: 'ME-TPC', id: '33b5cbc93b9968ab' }],
},
{
topic: 'children.box',
id: '33906db16ed7f956',
expanded: true,
children: [
{
topic: 'grp(group)',
id: '33907d9a3664cc8a',
expanded: true,
children: [
{
topic: 't(top)',
id: '3390856d09415b95',
expanded: true,
children: [
{
topic: 'tpc(topic)',
id: '33908dd36c7d32c5',
expanded: true,
children: [
{ topic: 'text', id: '3391630d4227e248' },
{ topic: 'icons', id: '33916d74224b141f' },
{ topic: 'tags', id: '33916421bfff1543' },
],
tags: ['E() function return'],
},
{
topic: 'epd(expander)',
id: '33909032ed7b5e8e',
tags: ['If had child'],
},
],
tags: ['createParent retun'],
},
{
topic: 'me-children',
id: '339087e1a8a5ea68',
expanded: true,
children: [
{
topic: 'me-wrapper',
id: '3390930112ea7367',
tags: ['what add node actually do is to append grp tag to children'],
},
{ topic: 'grp...', id: '3390940a8c8380a6' },
],
tags: ['layoutChildren return'],
},
{ topic: 'svg.subLines', id: '33908986b6336a4f' },
],
tags: ['have child'],
},
{
topic: 'me-wrapper',
id: '339081c3c5f57756',
expanded: true,
children: [
{
topic: 'ME-PARENT',
id: '33b6160ec048b997',
expanded: true,
children: [{ topic: 'ME-TPC', id: '33b616f9fe7763fc' }],
},
],
tags: ['no child'],
},
{ topic: 'grp...', id: '33b61346707af71a' },
],
},
{ topic: 'svg.lines', id: '3390707d68c0779d' },
{ topic: 'svg.linkcontroller', id: '339072cb6cf95295' },
{ topic: 'svg.topiclinks', id: '3390751acbdbdb9f' },
],
},
{ topic: 'cmenu', id: '33905f95aeab942d' },
{ topic: 'toolbar.rb', id: '339060ac0343f0d7' },
{ topic: 'toolbar.lt', id: '3390622b29323de9' },
{ topic: 'nmenu', id: '3390645e6d7c2b4e' },
],
},
],
},
arrows: [],
}
export default mindElixirStruct

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
export const katexHTML = `<div class="math math-display"><span class="katex-display"><span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.4em;vertical-align:-0.95em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size1">[</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.85em;"><span style="top:-3.01em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">x</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.35em;"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em;"></span><span class="arraycolsep" style="width:0.5em;"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.85em;"><span style="top:-3.01em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">y</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.35em;"><span></span></span></span></span></span></span></span><span class="mclose delimcenter" style="top:0em;"><span class="delimsizing size1">]</span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size3">[</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.45em;"><span style="top:-3.61em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">a</span></span></span><span style="top:-2.41em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">b</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.95em;"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em;"></span><span class="arraycolsep" style="width:0.5em;"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.45em;"><span style="top:-3.61em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">c</span></span></span><span style="top:-2.41em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s">&ZeroWidthSpace;</span></span><span class="vlist-r"><span class="vlist" style="height:0.95em;"><span></span></span></span></span></span></span></span><span class="mclose delimcenter" style="top:0em;"><span class="delimsizing size3">]</span></span></span></span></span></span></span></div>`
export const codeBlock = `<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">let</span> message <span class="token operator">=</span> <span class="token string">'Hello world'</span>
<span class="token function">alert</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span></code></pre>`
export const styledDiv = `<div><style>.title{font-size:50px}</style><div class="title">Title</div><div style="color: red; font-size: 20px;">Hello world</div></div>`

View File

@ -1,165 +0,0 @@
type LangPack = {
addChild: string
addParent: string
addSibling: string
removeNode: string
focus: string
cancelFocus: string
moveUp: string
moveDown: string
link: string
linkBidirectional: string
clickTips: string
summary: string
}
/**
* @public
*/
export type Locale = 'cn' | 'zh_CN' | 'zh_TW' | 'en' | 'ru' | 'ja' | 'pt' | 'it' | 'es' | 'fr' | 'ko'
const cn = {
addChild: '插入子节点',
addParent: '插入父节点',
addSibling: '插入同级节点',
removeNode: '删除节点',
focus: '专注',
cancelFocus: '取消专注',
moveUp: '上移',
moveDown: '下移',
link: '连接',
linkBidirectional: '双向连接',
clickTips: '请点击目标节点',
summary: '摘要',
}
const i18n: Record<Locale, LangPack> = {
cn,
zh_CN: cn,
zh_TW: {
addChild: '插入子節點',
addParent: '插入父節點',
addSibling: '插入同級節點',
removeNode: '刪除節點',
focus: '專注',
cancelFocus: '取消專注',
moveUp: '上移',
moveDown: '下移',
link: '連接',
linkBidirectional: '雙向連接',
clickTips: '請點擊目標節點',
summary: '摘要',
},
en: {
addChild: 'Add child',
addParent: 'Add parent',
addSibling: 'Add sibling',
removeNode: 'Remove node',
focus: 'Focus Mode',
cancelFocus: 'Cancel Focus Mode',
moveUp: 'Move up',
moveDown: 'Move down',
link: 'Link',
linkBidirectional: 'Bidirectional Link',
clickTips: 'Please click the target node',
summary: 'Summary',
},
ru: {
addChild: 'Добавить дочерний элемент',
addParent: 'Добавить родительский элемент',
addSibling: 'Добавить на этом уровне',
removeNode: 'Удалить узел',
focus: 'Режим фокусировки',
cancelFocus: 'Отменить режим фокусировки',
moveUp: 'Поднять выше',
moveDown: 'Опустить ниже',
link: 'Ссылка',
linkBidirectional: 'Двунаправленная ссылка',
clickTips: 'Пожалуйста, нажмите на целевой узел',
summary: 'Описание',
},
ja: {
addChild: '子ノードを追加する',
addParent: '親ノードを追加します',
addSibling: '兄弟ノードを追加する',
removeNode: 'ノードを削除',
focus: '集中',
cancelFocus: '集中解除',
moveUp: '上へ移動',
moveDown: '下へ移動',
link: 'コネクト',
linkBidirectional: '双方向リンク',
clickTips: 'ターゲットノードをクリックしてください',
summary: '概要',
},
pt: {
addChild: 'Adicionar item filho',
addParent: 'Adicionar item pai',
addSibling: 'Adicionar item irmao',
removeNode: 'Remover item',
focus: 'Modo Foco',
cancelFocus: 'Cancelar Modo Foco',
moveUp: 'Mover para cima',
moveDown: 'Mover para baixo',
link: 'Link',
linkBidirectional: 'Link bidirecional',
clickTips: 'Favor clicar no item alvo',
summary: 'Resumo',
},
it: {
addChild: 'Aggiungi figlio',
addParent: 'Aggiungi genitore',
addSibling: 'Aggiungi fratello',
removeNode: 'Rimuovi nodo',
focus: 'Modalità Focus',
cancelFocus: 'Annulla Modalità Focus',
moveUp: 'Sposta su',
moveDown: 'Sposta giù',
link: 'Collega',
linkBidirectional: 'Collegamento bidirezionale',
clickTips: 'Si prega di fare clic sul nodo di destinazione',
summary: 'Unisci nodi',
},
es: {
addChild: 'Agregar hijo',
addParent: 'Agregar padre',
addSibling: 'Agregar hermano',
removeNode: 'Eliminar nodo',
focus: 'Modo Enfoque',
cancelFocus: 'Cancelar Modo Enfoque',
moveUp: 'Mover hacia arriba',
moveDown: 'Mover hacia abajo',
link: 'Enlace',
linkBidirectional: 'Enlace bidireccional',
clickTips: 'Por favor haga clic en el nodo de destino',
summary: 'Resumen',
},
fr: {
addChild: 'Ajout enfant',
addParent: 'Ajout parent',
addSibling: 'Ajout voisin',
removeNode: 'Supprimer',
focus: 'Cibler',
cancelFocus: 'Retour',
moveUp: 'Monter',
moveDown: 'Descendre',
link: 'Lier',
linkBidirectional: 'Lien bidirectionnel',
clickTips: 'Cliquer sur le noeud cible',
summary: 'Annoter',
},
ko: {
addChild: '자식 추가',
addParent: '부모 추가',
addSibling: '형제 추가',
removeNode: '노드 삭제',
focus: '포커스 모드',
cancelFocus: '포커스 모드 취소',
moveUp: '위로 이동',
moveDown: '아래로 이동',
link: '연결',
linkBidirectional: '양방향 연결',
clickTips: '대상 노드를 클릭하십시오',
summary: '요약',
},
}
export default i18n

View File

@ -1,7 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1656654717242" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z" stroke-width="54" stroke='black' fill='white' ></path>
<path d="M682.666667 480h-138.666667V341.333333c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v138.666667H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h138.666667V682.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32v-138.666667H682.666667c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 910 B

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750169402629" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2170" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M639.328 416c8.032 0 16.096-3.008 22.304-9.056l202.624-197.184-0.8 143.808c-0.096 17.696 14.144 32.096 31.808 32.192 0.064 0 0.128 0 0.192 0 17.6 0 31.904-14.208 32-31.808l1.248-222.208c0-0.672-0.352-1.248-0.384-1.92 0.032-0.512 0.288-0.896 0.288-1.408 0.032-17.664-14.272-32-31.968-32.032L671.552 96l-0.032 0c-17.664 0-31.968 14.304-32 31.968C639.488 145.632 653.824 160 671.488 160l151.872 0.224-206.368 200.8c-12.672 12.32-12.928 32.608-0.64 45.248C622.656 412.736 630.976 416 639.328 416z" p-id="2171"></path><path d="M896.032 639.552 896.032 639.552c-17.696 0-32 14.304-32.032 31.968l-0.224 151.872-200.832-206.4c-12.32-12.64-32.576-12.96-45.248-0.64-12.672 12.352-12.928 32.608-0.64 45.248l197.184 202.624-143.808-0.8c-0.064 0-0.128 0-0.192 0-17.6 0-31.904 14.208-32 31.808-0.096 17.696 14.144 32.096 31.808 32.192l222.24 1.248c0.064 0 0.128 0 0.192 0 0.64 0 1.12-0.32 1.76-0.352 0.512 0.032 0.896 0.288 1.408 0.288l0.032 0c17.664 0 31.968-14.304 32-31.968L928 671.584C928.032 653.952 913.728 639.584 896.032 639.552z" p-id="2172"></path><path d="M209.76 159.744l143.808 0.8c0.064 0 0.128 0 0.192 0 17.6 0 31.904-14.208 32-31.808 0.096-17.696-14.144-32.096-31.808-32.192L131.68 95.328c-0.064 0-0.128 0-0.192 0-0.672 0-1.248 0.352-1.888 0.384-0.448 0-0.8-0.256-1.248-0.256 0 0-0.032 0-0.032 0-17.664 0-31.968 14.304-32 31.968L96 352.448c-0.032 17.664 14.272 32 31.968 32.032 0 0 0.032 0 0.032 0 17.664 0 31.968-14.304 32-31.968l0.224-151.936 200.832 206.4c6.272 6.464 14.624 9.696 22.944 9.696 8.032 0 16.096-3.008 22.304-9.056 12.672-12.32 12.96-32.608 0.64-45.248L209.76 159.744z" p-id="2173"></path><path d="M362.368 617.056l-202.624 197.184 0.8-143.808c0.096-17.696-14.144-32.096-31.808-32.192-0.064 0-0.128 0-0.192 0-17.6 0-31.904 14.208-32 31.808l-1.248 222.24c0 0.704 0.352 1.312 0.384 2.016 0 0.448-0.256 0.832-0.256 1.312-0.032 17.664 14.272 32 31.968 32.032L352.448 928c0 0 0.032 0 0.032 0 17.664 0 31.968-14.304 32-31.968s-14.272-32-31.968-32.032l-151.936-0.224 206.4-200.832c12.672-12.352 12.96-32.608 0.64-45.248S375.008 604.704 362.368 617.056z" p-id="2174"></path></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750169375313" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1775" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M639 463.30000001L639 285.1c0-36.90000001-26.4-68.5-61.3-68.5l-150.2 0c-1.5 0-3 0.1-4.5 0.3-10.2-38.7-45.5-67.3-87.5-67.3-50 0-90.5 40.5-90.5 90.5s40.5 90.5 90.5 90.5c42 0 77.3-28.6 87.5-67.39999999 1.4 0.3 2.9 0.4 4.5 0.39999999L577.7 263.6c6.8 0 14.3 8.9 14.3 21.49999999l0 427.00000001c0 12.7-7.40000001 21.5-14.30000001 21.5l-150.19999999 0c-1.5 0-3 0.2-4.5 0.4-10.2-38.8-45.5-67.3-87.5-67.3-50 0-90.5 40.5-90.5 90.4 0 49.9 40.5 90.6 90.5 90.59999999 42 0 77.3-28.6 87.5-67.39999999 1.4 0.2 2.9 0.4 4.49999999 0.4L577.7 780.7c34.80000001 0 61.3-31.6 61.3-68.50000001L639 510.3l79.1 0c10.4 38.5 45.49999999 67 87.4 67 50 0 90.5-40.5 90.5-90.5s-40.5-90.5-90.5-90.5c-41.79999999 0-77.00000001 28.4-87.4 67L639 463.30000001z" fill="currentColor" p-id="1776"></path></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750169573443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2883" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M514.133333 488.533333m-106.666666 0a106.666667 106.666667 0 1 0 213.333333 0 106.666667 106.666667 0 1 0-213.333333 0Z" fill="currentColor" p-id="2884"></path><path d="M512 64C264.533333 64 64 264.533333 64 512c0 236.8 183.466667 428.8 416 445.866667v-134.4c-53.333333-59.733333-200.533333-230.4-200.533333-334.933334 0-130.133333 104.533333-234.666667 234.666666-234.666666s234.666667 104.533333 234.666667 234.666666c0 61.866667-49.066667 153.6-145.066667 270.933334l-59.733333 68.266666V960C776.533333 942.933333 960 748.8 960 512c0-247.466667-200.533333-448-448-448z" fill="currentColor" p-id="2885"></path></svg>

Before

Width:  |  Height:  |  Size: 951 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1656655564985" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z" stroke-width="54" stroke='black' fill='white' ></path>
<path d="M682.666667 544H341.333333c-17.066667 0-32-14.933333-32-32s14.933333-32 32-32h341.333334c17.066667 0 32 14.933333 32 32s-14.933333 32-32 32z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 741 B

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750169667709" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3037" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M385 560.69999999L385 738.9c0 36.90000001 26.4 68.5 61.3 68.5l150.2 0c1.5 0 3-0.1 4.5-0.3 10.2 38.7 45.5 67.3 87.5 67.3 50 0 90.5-40.5 90.5-90.5s-40.5-90.5-90.5-90.5c-42 0-77.3 28.6-87.5 67.39999999-1.4-0.3-2.9-0.4-4.5-0.39999999L446.3 760.4c-6.8 0-14.3-8.9-14.3-21.49999999l0-427.00000001c0-12.7 7.40000001-21.5 14.30000001-21.5l150.19999999 0c1.5 0 3-0.2 4.5-0.4 10.2 38.8 45.5 67.3 87.5 67.3 50 0 90.5-40.5 90.5-90.4 0-49.9-40.5-90.6-90.5-90.59999999-42 0-77.3 28.6-87.5 67.39999999-1.4-0.2-2.9-0.4-4.49999999-0.4L446.3 243.3c-34.80000001 0-61.3 31.6-61.3 68.50000001L385 513.7l-79.1 0c-10.4-38.5-45.49999999-67-87.4-67-50 0-90.5 40.5-90.5 90.5s40.5 90.5 90.5 90.5c41.79999999 0 77.00000001-28.4 87.4-67L385 560.69999999z" fill="currentColor" p-id="3038"></path></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750169394918" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2021" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M851.91168 328.45312c-59.97056 0-108.6208 48.47104-108.91264 108.36992l-137.92768 38.4a109.14304 109.14304 0 0 0-63.46752-46.58688l1.39264-137.11872c47.29344-11.86816 82.31936-54.66624 82.31936-105.64096 0-60.15488-48.76288-108.91776-108.91776-108.91776s-108.91776 48.76288-108.91776 108.91776c0 49.18784 32.60928 90.75712 77.38368 104.27392l-1.41312 138.87488a109.19936 109.19936 0 0 0-63.50336 48.55808l-138.93632-39.48544 0.01024-0.72704c0-60.15488-48.76288-108.91776-108.91776-108.91776s-108.91776 48.75776-108.91776 108.91776c0 60.15488 48.76288 108.91264 108.91776 108.91264 39.3984 0 73.91232-20.92032 93.03552-52.2496l139.19232 39.552-0.00512 0.2304c0 25.8304 9.00096 49.5616 24.02816 68.23424l-90.14272 132.63872a108.7488 108.7488 0 0 0-34.2528-5.504c-60.15488 0-108.91776 48.768-108.91776 108.91776 0 60.16 48.76288 108.91776 108.91776 108.91776 60.16 0 108.92288-48.75776 108.92288-108.91776 0-27.14624-9.9328-51.968-26.36288-71.04l89.04704-131.03104a108.544 108.544 0 0 0 37.6832 6.70208 108.672 108.672 0 0 0 36.48512-6.272l93.13792 132.57216a108.48256 108.48256 0 0 0-24.69888 69.0688c0 60.16 48.768 108.92288 108.91776 108.92288 60.16 0 108.91776-48.76288 108.91776-108.92288 0-60.14976-48.75776-108.91776-108.91776-108.91776a108.80512 108.80512 0 0 0-36.69504 6.3488l-93.07136-132.48a108.48768 108.48768 0 0 0 24.79616-72.22784l136.09984-37.888c18.99008 31.93856 53.84192 53.3504 93.69088 53.3504 60.16 0 108.92288-48.75776 108.92288-108.91264-0.00512-60.15488-48.77312-108.92288-108.92288-108.92288z" p-id="2022"></path></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750169419447" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2480" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M863.328 482.56l-317.344-1.12L545.984 162.816c0-17.664-14.336-32-32-32s-32 14.336-32 32l0 318.4L159.616 480.064c-0.032 0-0.064 0-0.096 0-17.632 0-31.936 14.24-32 31.904C127.424 529.632 141.728 544 159.392 544.064l322.592 1.152 0 319.168c0 17.696 14.336 32 32 32s32-14.304 32-32l0-318.944 317.088 1.12c0.064 0 0.096 0 0.128 0 17.632 0 31.936-14.24 32-31.904C895.264 496.992 880.96 482.624 863.328 482.56z" p-id="2481"></path></svg>

Before

Width:  |  Height:  |  Size: 763 B

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750169426515" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2730" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M863.744 544 163.424 544c-17.664 0-32-14.336-32-32s14.336-32 32-32l700.32 0c17.696 0 32 14.336 32 32S881.44 544 863.744 544z" p-id="2731"></path></svg>

Before

Width:  |  Height:  |  Size: 484 B

View File

@ -1,374 +0,0 @@
.map-container {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC,
WenQuanYi Micro Hei, sans-serif;
user-select: none;
height: 100%;
width: 100%;
overflow: hidden; // prevent browser scroll container while dragging
font-size: 15px;
outline: none; // prevent browser default focus outline
touch-action: none; // use pointer events instead of touch events
* {
box-sizing: border-box; // must have, to make getComputedStyle work right
}
&::-webkit-scrollbar {
width: 0px;
height: 0px;
}
.selected {
outline: 2px solid var(--selected);
outline-offset: 1px;
}
.hyper-link {
text-decoration: none;
margin-left: 0.3em;
}
me-main > me-wrapper > me-parent > me-epd {
top: 50%;
transform: translateY(-50%);
}
me-epd {
top: 100%;
transform: translateY(-50%);
}
.lhs {
direction: rtl;
& > me-wrapper > me-parent > me-epd {
left: -10px;
}
me-epd {
left: 5px;
}
me-tpc {
direction: ltr;
}
}
.rhs {
& > me-wrapper > me-parent > me-epd {
right: -10px;
}
me-epd {
right: 5px;
}
}
background-color: var(--bgcolor);
.map-canvas {
position: relative;
user-select: none;
width: fit-content; // 100% width if not set
transform: scale(1);
me-nodes {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: max-content;
width: max-content;
padding: var(--map-padding);
}
}
me-main {
// primary node / main node
& > me-wrapper {
position: relative; // make subline svg's offsetParent be the main node
margin: var(--main-gap-y) var(--main-gap-x);
& > me-parent {
margin: 10px;
padding: 0;
& > me-tpc {
border-radius: var(--main-radius);
background-color: var(--main-bgcolor);
border: 2px solid var(--main-color);
color: var(--main-color);
padding: 8px 25px;
}
}
}
}
me-wrapper {
display: block;
pointer-events: none;
width: fit-content;
// position: relative;
}
me-children,
me-parent {
display: inline-block;
vertical-align: middle;
}
me-root {
position: relative;
margin: 45px 0;
z-index: 10;
me-tpc {
font-size: 25px;
color: var(--root-color);
padding: 10px 30px;
border-radius: var(--root-radius);
border: var(--root-border-color) 2px solid;
background-color: var(--root-bgcolor);
}
}
me-parent {
position: relative; // for locating expand button
cursor: pointer;
padding: 6px var(--node-gap-x);
margin-top: var(--node-gap-y);
z-index: 10;
me-tpc {
position: relative;
border-radius: 3px;
color: var(--color);
padding: var(--topic-padding);
// drag preview
.insert-preview {
position: absolute;
width: 100%;
left: 0px;
z-index: 9;
}
.show {
background: #7ad5ff;
pointer-events: none;
opacity: 0.7;
border-radius: 3px;
}
.before {
height: 14px;
top: -14px;
}
.in {
height: 100%;
top: 0px;
}
.after {
height: 14px;
bottom: -14px;
}
}
me-epd {
position: absolute;
height: 18px;
width: 18px;
opacity: 0.8;
background-image: url('./icons/add-circle.svg');
background-repeat: no-repeat;
background-size: contain;
background-position: center;
pointer-events: all;
z-index: 9;
&.minus {
background-image: url('./icons/minus-circle.svg') !important;
transition: opacity 0.3s;
opacity: 0;
@media (hover: hover) {
&:hover {
opacity: 0.8;
}
}
@media (hover: none) {
& {
opacity: 0.8;
}
}
}
}
}
// iconfont
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.lines,
.summary,
.subLines,
.topiclinks,
.linkcontroller {
position: absolute;
height: 102%;
width: 100%;
top: 0;
left: 0;
}
.topiclinks,
.linkcontroller,
.summary {
pointer-events: none;
z-index: 20;
}
.summary > g,
.topiclinks > g {
cursor: pointer;
pointer-events: stroke;
z-index: 20;
}
.lines,
.subLines {
pointer-events: none;
}
#input-box {
position: absolute;
top: 0;
left: 0;
width: max-content; // let words expand the div and keep max length at the same time
max-width: 35em;
direction: ltr;
user-select: auto;
pointer-events: auto;
color: var(--color);
background-color: var(--bgcolor);
z-index: 100;
}
me-tpc {
display: block;
max-width: 35em;
white-space: pre-wrap;
pointer-events: all;
& > * {
// tags,icons,images should not response to click event
pointer-events: none;
}
& > a,
& > iframe {
pointer-events: auto;
}
& > .text {
display: inline-block;
// Allow links inside markdown text to be clickable
a {
pointer-events: auto;
}
}
& > img {
display: block;
margin-bottom: 8px;
object-fit: cover;
}
}
.circle {
position: absolute;
height: 10px;
width: 10px;
margin-top: -5px;
margin-left: -5px;
border-radius: 100%;
background: #757575;
border: 2px solid #ffffff;
z-index: 50;
cursor: pointer;
}
.tags {
direction: ltr;
span {
display: inline-block;
border-radius: 3px;
padding: 2px 4px;
background: #d6f0f8;
color: #276f86;
margin: 0px;
font-size: 12px;
line-height: 1.3em;
margin-right: 4px;
margin-top: 2px;
}
}
.icons {
display: inline-block;
direction: ltr;
margin-left: 5px;
span {
display: inline-block;
line-height: 1.3em;
}
}
.mind-elixir-ghost {
position: fixed;
top: -100%;
left: -100%;
box-sizing: content-box;
opacity: 0.5;
background-color: var(--main-bgcolor);
border: 2px solid var(--main-color);
color: var(--main-color);
max-width: 200px;
width: fit-content;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 8px 16px;
border-radius: 6px;
}
.selection-area {
background: #4f90f22d;
border: 1px solid #4f90f2;
}
/* Markdown表格样式 */
.markdown-table {
border-collapse: collapse;
width: 100%;
margin: 4px 0;
font-size: 11px;
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
background-color: #fafafa;
overflow: hidden;
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
}
.markdown-table th,
.markdown-table td {
border: 1px solid #e0e0e0;
padding: 8px 12px;
text-align: left;
vertical-align: top;
position: relative;
white-space: normal !important; /* 覆盖MindElixir的pre-wrap */
}
.markdown-table th {
background-color: #f5f5f5;
font-weight: 600;
color: #333;
text-align: center;
border-bottom: 1px solid #d0d0d0;
}
.markdown-table td {
background-color: #fff;
}
.markdown-table tr:nth-child(even) td {
background-color: #f8f8f8;
}
.markdown-table tr:hover td {
background-color: #f0f8ff;
}
/* 移除多余的边框,保持简洁 */
.markdown-table th:not(:last-child),
.markdown-table td:not(:last-child) {
border-right: 1px solid #e0e0e0;
}
.markdown-table tr:not(:last-child) td {
border-bottom: 1px solid #e0e0e0;
}
}

View File

@ -1,198 +0,0 @@
import './index.less'
import { LEFT, RIGHT, SIDE, DARK_THEME, THEME } from './const'
import { generateUUID } from './utils/index'
import initMouseEvent from './mouse'
import { createBus } from './utils/pubsub'
import { findEle } from './utils/dom'
import { createLinkSvg, createLine } from './utils/svg'
import type { MindElixirData, MindElixirInstance, MindElixirMethods, Options } from './types/index'
import methods from './methods'
import { sub, main } from './utils/generateBranch'
import { version } from '../package.json'
import { createDragMoveHelper } from './utils/dragMoveHelper'
import type { Topic } from './docs'
// TODO show up animation
const $d = document
function MindElixir(
this: MindElixirInstance,
{
el,
direction,
locale,
draggable,
editable,
contextMenu,
toolBar,
keypress,
mouseSelectionButton,
selectionContainer,
before,
newTopicName,
allowUndo,
generateMainBranch,
generateSubBranch,
overflowHidden,
theme,
alignment,
scaleSensitivity,
scaleMax,
scaleMin,
handleWheel,
markdown,
imageProxy,
}: Options
): void {
let ele: HTMLElement | null = null
const elType = Object.prototype.toString.call(el)
if (elType === '[object HTMLDivElement]') {
ele = el as HTMLElement
} else if (elType === '[object String]') {
ele = document.querySelector(el as string) as HTMLElement
}
if (!ele) throw new Error('MindElixir: el is not a valid element')
ele.style.position = 'relative'
ele.innerHTML = ''
this.el = ele as HTMLElement
this.disposable = []
this.before = before || {}
this.locale = locale || 'en'
this.newTopicName = newTopicName || 'New Node'
this.contextMenu = contextMenu ?? true
this.toolBar = toolBar ?? true
this.keypress = keypress ?? true
this.mouseSelectionButton = mouseSelectionButton ?? 0
this.direction = direction ?? 1
this.draggable = draggable ?? true
this.editable = editable ?? true
this.allowUndo = allowUndo ?? true
this.scaleSensitivity = scaleSensitivity ?? 0.1
this.scaleMax = scaleMax ?? 1.4
this.scaleMin = scaleMin ?? 0.2
this.generateMainBranch = generateMainBranch || main
this.generateSubBranch = generateSubBranch || sub
this.overflowHidden = overflowHidden ?? false
this.alignment = alignment ?? 'root'
this.handleWheel = handleWheel ?? true
this.markdown = markdown || undefined // Custom markdown parser function
this.imageProxy = imageProxy || undefined // Image proxy function
// this.parentMap = {} // deal with large amount of nodes
this.currentNodes = [] // selected <tpc/> elements
this.currentArrow = null // the selected link svg element
this.scaleVal = 1
this.tempDirection = null
this.dragMoveHelper = createDragMoveHelper(this)
this.bus = createBus()
this.container = $d.createElement('div') // map container
this.selectionContainer = selectionContainer || this.container
this.container.className = 'map-container'
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
this.theme = theme || (mediaQuery.matches ? DARK_THEME : THEME)
// infrastructure
const canvas = $d.createElement('div') // map-canvas Element
canvas.className = 'map-canvas'
this.map = canvas
this.container.setAttribute('tabindex', '0')
this.container.appendChild(this.map)
this.el.appendChild(this.container)
this.nodes = $d.createElement('me-nodes')
this.lines = createLinkSvg('lines') // main link container
this.summarySvg = createLinkSvg('summary') // summary container
this.linkController = createLinkSvg('linkcontroller') // bezier controller container
this.P2 = $d.createElement('div') // bezier P2
this.P3 = $d.createElement('div') // bezier P3
this.P2.className = this.P3.className = 'circle'
this.P2.style.display = this.P3.style.display = 'none'
this.line1 = createLine() // bezier auxiliary line1
this.line2 = createLine() // bezier auxiliary line2
this.linkController.appendChild(this.line1)
this.linkController.appendChild(this.line2)
this.linkSvgGroup = createLinkSvg('topiclinks') // storage user custom link svg
this.map.appendChild(this.nodes)
if (this.overflowHidden) {
this.container.style.overflow = 'hidden'
} else {
this.disposable.push(initMouseEvent(this))
}
}
MindElixir.prototype = methods
Object.defineProperty(MindElixir.prototype, 'currentNode', {
get() {
return this.currentNodes[this.currentNodes.length - 1]
},
enumerable: true,
})
MindElixir.LEFT = LEFT
MindElixir.RIGHT = RIGHT
MindElixir.SIDE = SIDE
MindElixir.THEME = THEME
MindElixir.DARK_THEME = DARK_THEME
/**
* @memberof MindElixir
* @static
*/
MindElixir.version = version
/**
* @function
* @memberof MindElixir
* @static
* @name E
* @param {string} id Node id.
* @return {TargetElement} Target element.
* @example
* E('bd4313fbac40284b')
*/
MindElixir.E = findEle
/**
* @function new
* @memberof MindElixir
* @static
* @param {String} topic root topic
*/
if (import.meta.env.MODE !== 'lite') {
MindElixir.new = (topic: string): MindElixirData => ({
nodeData: {
id: generateUUID(),
topic: topic || 'new topic',
children: [],
},
})
}
export interface MindElixirCtor {
new (options: Options): MindElixirInstance
E: (id: string, el?: HTMLElement) => Topic
new: typeof MindElixir.new
version: string
LEFT: typeof LEFT
RIGHT: typeof RIGHT
SIDE: typeof SIDE
THEME: typeof THEME
DARK_THEME: typeof DARK_THEME
prototype: MindElixirMethods
}
export default MindElixir as unknown as MindElixirCtor
// types
export type * from './utils/pubsub'
export type * from './types/index'
export type * from './types/dom'

View File

@ -1,407 +0,0 @@
import type { Locale } from './i18n'
import { rmSubline } from './nodeOperation'
import type { Topic, Wrapper } from './types/dom'
import type { MindElixirData, MindElixirInstance, NodeObj } from './types/index'
import { fillParent, getTranslate, setExpand } from './utils/index'
function collectData(instance: MindElixirInstance) {
return {
nodeData: instance.isFocusMode ? instance.nodeDataBackup : instance.nodeData,
arrows: instance.arrows,
summaries: instance.summaries,
direction: instance.direction,
theme: instance.theme,
}
}
export const scrollIntoView = function (this: MindElixirInstance, el: HTMLElement) {
// scrollIntoView needs to be implemented manually because native scrollIntoView behaves incorrectly after transform
const container = this.container
const rect = el.getBoundingClientRect()
const containerRect = container.getBoundingClientRect()
const isOutOfView =
rect.top > containerRect.bottom || rect.bottom < containerRect.top || rect.left > containerRect.right || rect.right < containerRect.left
if (isOutOfView) {
// Calculate the offset between container center and element center
const elCenterX = rect.left + rect.width / 2
const elCenterY = rect.top + rect.height / 2
const containerCenterX = containerRect.left + containerRect.width / 2
const containerCenterY = containerRect.top + containerRect.height / 2
const offsetX = elCenterX - containerCenterX
const offsetY = elCenterY - containerCenterY
this.move(-offsetX, -offsetY, true)
}
}
export const selectNode = function (this: MindElixirInstance, tpc: Topic, isNewNode?: boolean, e?: MouseEvent): void {
// selectNode clears all selected nodes by default
this.clearSelection()
this.scrollIntoView(tpc)
this.selection.select(tpc)
if (isNewNode) {
this.bus.fire('selectNewNode', tpc.nodeObj)
}
}
export const selectNodes = function (this: MindElixirInstance, tpc: Topic[]): void {
// update currentNodes in selection.ts to keep sync with SelectionArea cache
this.selection.select(tpc)
}
export const unselectNodes = function (this: MindElixirInstance, tpc: Topic[]) {
this.selection.deselect(tpc)
}
export const clearSelection = function (this: MindElixirInstance) {
this.unselectNodes(this.currentNodes)
this.unselectSummary()
this.unselectArrow()
}
/**
* @function
* @instance
* @name getDataString
* @description Get all node data as string.
* @memberof MapInteraction
* @return {string}
*/
export const getDataString = function (this: MindElixirInstance) {
const data = collectData(this)
return JSON.stringify(data, (k, v) => {
if (k === 'parent' && typeof v !== 'string') return undefined
return v
})
}
/**
* @function
* @instance
* @name getData
* @description Get all node data as object.
* @memberof MapInteraction
* @return {Object}
*/
export const getData = function (this: MindElixirInstance) {
return JSON.parse(this.getDataString()) as MindElixirData
}
/**
* @function
* @instance
* @name enableEdit
* @memberof MapInteraction
*/
export const enableEdit = function (this: MindElixirInstance) {
this.editable = true
}
/**
* @function
* @instance
* @name disableEdit
* @memberof MapInteraction
*/
export const disableEdit = function (this: MindElixirInstance) {
this.editable = false
}
/**
* @function
* @instance
* @name scale
* @description Change the scale of the mind map.
* @memberof MapInteraction
* @param {number}
*/
export const scale = function (this: MindElixirInstance, scaleVal: number, offset: { x: number; y: number } = { x: 0, y: 0 }) {
if (scaleVal < this.scaleMin || scaleVal > this.scaleMax) return
const rect = this.container.getBoundingClientRect()
// refer to /refs/scale-calc.excalidraw for the process
// remove coordinate system influence and calculate quantities directly
// cursor xy
const xc = offset.x ? offset.x - rect.left - rect.width / 2 : 0
const yc = offset.y ? offset.y - rect.top - rect.height / 2 : 0
const { dx, dy } = getCenterDefault(this)
const oldTransform = this.map.style.transform
const { x: xCurrent, y: yCurrent } = getTranslate(oldTransform) // current offset
// before xy
const xb = xCurrent - dx
const yb = yCurrent - dy
const oldScale = this.scaleVal
// Note: cursor needs to be reversed, probably because transform itself is reversed
const xres = (-xc + xb) * (1 - scaleVal / oldScale)
const yres = (-yc + yb) * (1 - scaleVal / oldScale)
this.map.style.transform = `translate(${xCurrent - xres}px, ${yCurrent - yres}px) scale(${scaleVal})`
this.scaleVal = scaleVal
this.bus.fire('scale', scaleVal)
}
/**
* Better to use with option `alignment: 'nodes'`.
*/
export const scaleFit = function (this: MindElixirInstance) {
const heightPercent = this.nodes.offsetHeight / this.container.offsetHeight
const widthPercent = this.nodes.offsetWidth / this.container.offsetWidth
const scale = 1 / Math.max(1, Math.max(heightPercent, widthPercent))
this.scaleVal = scale
this.map.style.transform = 'scale(' + scale + ')'
this.bus.fire('scale', scale)
}
/**
* Move the map by `dx` and `dy`.
*/
export const move = function (this: MindElixirInstance, dx: number, dy: number, smooth = false) {
const { map, scaleVal, bus } = this
const transform = map.style.transform
let { x, y } = getTranslate(transform)
x += dx
y += dy
if (smooth) {
map.style.transition = 'transform 0.3s'
setTimeout(() => {
map.style.transition = 'none'
}, 300)
}
map.style.transform = `translate(${x}px, ${y}px) scale(${scaleVal})`
bus.fire('move', { dx, dy })
}
/**
*
*/
const getCenterDefault = (mei: MindElixirInstance) => {
const { container, map, nodes } = mei
const root = map.querySelector('me-root') as HTMLElement
const pT = root.offsetTop
const pL = root.offsetLeft
const pW = root.offsetWidth
const pH = root.offsetHeight
let dx, dy
if (mei.alignment === 'root') {
dx = container.offsetWidth / 2 - pL - pW / 2
dy = container.offsetHeight / 2 - pT - pH / 2
map.style.transformOrigin = `${pL + pW / 2}px 50%`
} else {
dx = (container.offsetWidth - nodes.offsetWidth) / 2
dy = (container.offsetHeight - nodes.offsetHeight) / 2
map.style.transformOrigin = `50% 50%`
}
return { dx, dy }
}
/**
* @function
* @instance
* @name toCenter
* @description Reset position of the map to center.
* @memberof MapInteraction
*/
export const toCenter = function (this: MindElixirInstance) {
const { map } = this
const { dx, dy } = getCenterDefault(this)
map.style.transform = `translate(${dx}px, ${dy}px) scale(${this.scaleVal})`
}
/**
* @function
* @instance
* @name install
* @description Install plugin.
* @memberof MapInteraction
*/
export const install = function (this: MindElixirInstance, plugin: (instance: MindElixirInstance) => void) {
plugin(this)
}
/**
* @function
* @instance
* @name focusNode
* @description Enter focus mode, set the target element as root.
* @memberof MapInteraction
* @param {TargetElement} el - Target element return by E('...'), default value: currentTarget.
*/
export const focusNode = function (this: MindElixirInstance, el: Topic) {
if (!el.nodeObj.parent) return
this.clearSelection()
if (this.tempDirection === null) {
this.tempDirection = this.direction
}
if (!this.isFocusMode) {
this.nodeDataBackup = this.nodeData // help reset focus mode
this.isFocusMode = true
}
this.nodeData = el.nodeObj
this.initRight()
this.toCenter()
}
/**
* @function
* @instance
* @name cancelFocus
* @description Exit focus mode.
* @memberof MapInteraction
*/
export const cancelFocus = function (this: MindElixirInstance) {
this.isFocusMode = false
if (this.tempDirection !== null) {
this.nodeData = this.nodeDataBackup
this.direction = this.tempDirection
this.tempDirection = null
this.refresh()
this.toCenter()
}
}
/**
* @function
* @instance
* @name initLeft
* @description Child nodes will distribute on the left side of the root node.
* @memberof MapInteraction
*/
export const initLeft = function (this: MindElixirInstance) {
this.direction = 0
this.refresh()
this.toCenter()
}
/**
* @function
* @instance
* @name initRight
* @description Child nodes will distribute on the right side of the root node.
* @memberof MapInteraction
*/
export const initRight = function (this: MindElixirInstance) {
this.direction = 1
this.refresh()
this.toCenter()
}
/**
* @function
* @instance
* @name initSide
* @description Child nodes will distribute on both left and right side of the root node.
* @memberof MapInteraction
*/
export const initSide = function (this: MindElixirInstance) {
this.direction = 2
this.refresh()
this.toCenter()
}
/**
* @function
* @instance
* @name setLocale
* @memberof MapInteraction
*/
export const setLocale = function (this: MindElixirInstance, locale: Locale) {
this.locale = locale
this.refresh()
}
export const expandNode = function (this: MindElixirInstance, el: Topic, isExpand?: boolean) {
const node = el.nodeObj
if (typeof isExpand === 'boolean') {
node.expanded = isExpand
} else if (node.expanded !== false) {
node.expanded = false
} else {
node.expanded = true
}
// Calculate position before expansion
const expanderRect = el.getBoundingClientRect()
const beforePosition = {
x: expanderRect.left,
y: expanderRect.top,
}
const parent = el.parentNode
const expander = parent.children[1]!
expander.expanded = node.expanded
expander.className = node.expanded ? 'minus' : ''
rmSubline(el)
if (node.expanded) {
const children = this.createChildren(
node.children!.map(child => {
const wrapper = this.createWrapper(child)
return wrapper.grp
})
)
parent.parentNode.appendChild(children)
} else {
const children = parent.parentNode.children[1]
children.remove()
}
this.linkDiv(el.closest('me-main > me-wrapper') as Wrapper)
// Calculate position after expansion and compensate for drift
const afterRect = el.getBoundingClientRect()
const afterPosition = {
x: afterRect.left,
y: afterRect.top,
}
// Calculate the drift and move to compensate
const driftX = beforePosition.x - afterPosition.x
const driftY = beforePosition.y - afterPosition.y
this.move(driftX, driftY)
this.bus.fire('expandNode', node)
}
export const expandNodeAll = function (this: MindElixirInstance, el: Topic, isExpand?: boolean) {
const node = el.nodeObj
const beforeRect = el.getBoundingClientRect()
const beforePosition = {
x: beforeRect.left,
y: beforeRect.top,
}
setExpand(node, isExpand ?? !node.expanded)
this.refresh()
const afterRect = this.findEle(node.id).getBoundingClientRect()
const afterPosition = {
x: afterRect.left,
y: afterRect.top,
}
const driftX = beforePosition.x - afterPosition.x
const driftY = beforePosition.y - afterPosition.y
this.move(driftX, driftY)
}
/**
* @function
* @instance
* @name refresh
* @description Refresh mind map, you can use it after modified `this.nodeData`
* @memberof MapInteraction
* @param {TargetElement} data mind elixir data
*/
export const refresh = function (this: MindElixirInstance, data?: MindElixirData) {
this.clearSelection()
if (data) {
data = JSON.parse(JSON.stringify(data)) as MindElixirData // it shouldn't contanimate the original data
this.nodeData = data.nodeData
this.arrows = data.arrows || []
this.summaries = data.summaries || []
data.theme && this.changeTheme(data.theme)
}
fillParent(this.nodeData)
// create dom element for every node
this.layout()
// generate links between nodes
this.linkDiv()
}

View File

@ -1,107 +0,0 @@
import { createPath, createLinkSvg } from './utils/svg'
import { getOffsetLT } from './utils/index'
import type { Wrapper, Topic } from './types/dom'
import type { DirectionClass, MindElixirInstance } from './types/index'
/**
* Link nodes with svg,
* only link specific node if `mainNode` is present
*
* procedure:
* 1. generate main link
* 2. generate links inside main node, if `mainNode` is presented, only generate the link of the specific main node
* 3. generate custom link
* 4. generate summary
* @param mainNode regenerate sublink of the specific main node
*/
const linkDiv = function (this: MindElixirInstance, mainNode?: Wrapper) {
console.time('linkDiv')
const root = this.map.querySelector('me-root') as HTMLElement
const pT = root.offsetTop
const pL = root.offsetLeft
const pW = root.offsetWidth
const pH = root.offsetHeight
const mainNodeList = this.map.querySelectorAll('me-main > me-wrapper')
this.lines.innerHTML = ''
for (let i = 0; i < mainNodeList.length; i++) {
const el = mainNodeList[i] as Wrapper
const tpc = el.querySelector<Topic>('me-tpc') as Topic
const { offsetLeft: cL, offsetTop: cT } = getOffsetLT(this.nodes, tpc)
const cW = tpc.offsetWidth
const cH = tpc.offsetHeight
const direction = el.parentNode.className as DirectionClass
const mainPath = this.generateMainBranch({ pT, pL, pW, pH, cT, cL, cW, cH, direction, containerHeight: this.nodes.offsetHeight })
const palette = this.theme.palette
const branchColor = tpc.nodeObj.branchColor || palette[i % palette.length]
tpc.style.borderColor = branchColor
this.lines.appendChild(createPath(mainPath, branchColor, '3'))
// generate link inside main node
if (mainNode && mainNode !== el) {
continue
}
const svg = createLinkSvg('subLines')
// svg tag name is lower case
const svgLine = el.lastChild as SVGSVGElement
if (svgLine.tagName === 'svg') svgLine.remove()
el.appendChild(svg)
traverseChildren(this, svg, branchColor, el, direction, true)
}
this.renderArrow()
this.renderSummary()
console.timeEnd('linkDiv')
this.bus.fire('linkDiv')
}
// core function of generate subLines
const traverseChildren = function (
mei: MindElixirInstance,
svgContainer: SVGSVGElement,
branchColor: string,
wrapper: Wrapper,
direction: DirectionClass,
isFirst?: boolean
) {
const parent = wrapper.firstChild
const children = wrapper.children[1].children
if (children.length === 0) return
const pT = parent.offsetTop
const pL = parent.offsetLeft
const pW = parent.offsetWidth
const pH = parent.offsetHeight
for (let i = 0; i < children.length; i++) {
const child = children[i]
const childP = child.firstChild
const cT = childP.offsetTop
const cL = childP.offsetLeft
const cW = childP.offsetWidth
const cH = childP.offsetHeight
const bc = childP.firstChild.nodeObj.branchColor || branchColor
const path = mei.generateSubBranch({ pT, pL, pW, pH, cT, cL, cW, cH, direction, isFirst })
svgContainer.appendChild(createPath(path, bc, '2'))
const expander = childP.children[1]
if (expander) {
// this property is added in the layout phase
if (!expander.expanded) continue
} else {
// expander not exist
continue
}
traverseChildren(mei, svgContainer, bc, child, direction)
}
}
export default linkDiv

View File

@ -1,131 +0,0 @@
import type { MindElixirInstance, MindElixirData } from './index'
import linkDiv from './linkDiv'
import contextMenu from './plugin/contextMenu'
import keypressInit from './plugin/keypress'
import nodeDraggable from './plugin/nodeDraggable'
import operationHistory from './plugin/operationHistory'
import toolBar from './plugin/toolBar'
import selection from './plugin/selection'
import { editTopic, createWrapper, createParent, createChildren, createTopic, findEle } from './utils/dom'
import { getObjById, generateNewObj, fillParent } from './utils/index'
import { layout } from './utils/layout'
import { changeTheme } from './utils/theme'
import * as interact from './interact'
import * as nodeOperation from './nodeOperation'
import * as arrow from './arrow'
import * as summary from './summary'
import * as exportImage from './plugin/exportImage'
export type OperationMap = typeof nodeOperation
export type Operations = keyof OperationMap
type NodeOperation = {
[K in Operations]: ReturnType<typeof beforeHook<K>>
}
function beforeHook<T extends Operations>(
fn: OperationMap[T],
fnName: T
): (this: MindElixirInstance, ...args: Parameters<OperationMap[T]>) => Promise<void> {
return async function (this: MindElixirInstance, ...args: Parameters<OperationMap[T]>) {
const hook = this.before[fnName]
if (hook) {
const res = await hook.apply(this, args)
if (!res) return
}
;(fn as any).apply(this, args)
}
}
const operations = Object.keys(nodeOperation) as Array<Operations>
const nodeOperationHooked = {} as NodeOperation
if (import.meta.env.MODE !== 'lite') {
for (let i = 0; i < operations.length; i++) {
const operation = operations[i]
nodeOperationHooked[operation] = beforeHook(nodeOperation[operation], operation)
}
}
export type MindElixirMethods = typeof methods
/**
* Methods that mind-elixir instance can use
*
* @public
*/
const methods = {
getObjById,
generateNewObj,
layout,
linkDiv,
editTopic,
createWrapper,
createParent,
createChildren,
createTopic,
findEle,
changeTheme,
...interact,
...(nodeOperationHooked as NodeOperation),
...arrow,
...summary,
...exportImage,
init(this: MindElixirInstance, data: MindElixirData) {
data = JSON.parse(JSON.stringify(data))
if (!data || !data.nodeData) return new Error('MindElixir: `data` is required')
if (data.direction !== undefined) {
this.direction = data.direction
}
this.changeTheme(data.theme || this.theme, false)
this.nodeData = data.nodeData
fillParent(this.nodeData)
this.arrows = data.arrows || []
this.summaries = data.summaries || []
this.tidyArrow()
// plugins
this.toolBar && toolBar(this)
if (import.meta.env.MODE !== 'lite') {
this.keypress && keypressInit(this, this.keypress)
if (this.editable) {
selection(this)
}
if (this.contextMenu) {
this.disposable.push(contextMenu(this, this.contextMenu))
}
this.draggable && this.disposable.push(nodeDraggable(this))
this.allowUndo && this.disposable.push(operationHistory(this))
}
this.layout()
this.linkDiv()
this.toCenter()
},
destroy(this: Partial<MindElixirInstance>) {
this.disposable!.forEach(fn => fn())
if (this.el) this.el.innerHTML = ''
this.el = undefined
this.nodeData = undefined
this.arrows = undefined
this.summaries = undefined
this.currentArrow = undefined
this.currentNodes = undefined
this.currentSummary = undefined
this.waitCopy = undefined
this.theme = undefined
this.direction = undefined
this.bus = undefined
this.container = undefined
this.map = undefined
this.lines = undefined
this.linkController = undefined
this.linkSvgGroup = undefined
this.P2 = undefined
this.P3 = undefined
this.line1 = undefined
this.line2 = undefined
this.nodes = undefined
this.selection?.destroy()
this.selection = undefined
},
}
export default methods

View File

@ -1,161 +0,0 @@
import { handleZoom } from './plugin/keypress'
import type { SummarySvgGroup } from './summary'
import type { Expander, CustomSvg, Topic } from './types/dom'
import type { MindElixirInstance } from './types/index'
import { isTopic, on } from './utils'
export default function (mind: MindElixirInstance) {
const { dragMoveHelper } = mind
const handleClick = (e: MouseEvent) => {
console.log('handleClick', e)
// Only handle primary button clicks
if (e.button !== 0) return
if (mind.helper1?.moved) {
mind.helper1.clear()
return
}
if (mind.helper2?.moved) {
mind.helper2.clear()
return
}
if (dragMoveHelper.moved) {
dragMoveHelper.clear()
return
}
const target = e.target as HTMLElement
if (target.tagName === 'ME-EPD') {
if (e.ctrlKey || e.metaKey) {
mind.expandNodeAll((target as Expander).previousSibling)
} else {
mind.expandNode((target as Expander).previousSibling)
}
} else if (target.tagName === 'ME-TPC' && mind.currentNodes.length > 1) {
// This is a bit complex, intertwined with selection and nodeDraggable
// The main conflict is between multi-node dragging and selecting a single node when multiple nodes are already selected
mind.selectNode(target as Topic)
} else if (!mind.editable) {
return
}
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
if (trySvg.getAttribute('class') === 'topiclinks') {
mind.selectArrow(target.parentElement as unknown as CustomSvg)
} else if (trySvg.getAttribute('class') === 'summary') {
mind.selectSummary(target.parentElement as unknown as SummarySvgGroup)
}
}
const handleDblClick = (e: MouseEvent) => {
if (!mind.editable) return
const target = e.target as HTMLElement
if (isTopic(target)) {
mind.beginEdit(target)
}
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
if (trySvg.getAttribute('class') === 'topiclinks') {
mind.editArrowLabel(target.parentElement as unknown as CustomSvg)
} else if (trySvg.getAttribute('class') === 'summary') {
mind.editSummary(target.parentElement as unknown as SummarySvgGroup)
}
}
let lastTap = 0
const handleTouchDblClick = (e: PointerEvent) => {
if (e.pointerType === 'mouse') return
const currentTime = new Date().getTime()
const tapLength = currentTime - lastTap
console.log('tapLength', tapLength)
if (tapLength < 300 && tapLength > 0) {
handleDblClick(e)
}
lastTap = currentTime
}
const handlePointerDown = (e: PointerEvent) => {
dragMoveHelper.moved = false
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
// Store initial position for movement calculation
dragMoveHelper.x = e.clientX
dragMoveHelper.y = e.clientY
const target = e.target as HTMLElement
if (target.className === 'circle') return
if (target.contentEditable !== 'plaintext-only') {
dragMoveHelper.mousedown = true
// Capture pointer to ensure we receive all pointer events even if pointer moves outside the element
target.setPointerCapture(e.pointerId)
}
}
const handlePointerMove = (e: PointerEvent) => {
// click trigger pointermove in windows chrome
if ((e.target as HTMLElement).contentEditable !== 'plaintext-only') {
// drag and move the map
// Calculate movement delta manually since pointer events don't have movementX/Y
const movementX = e.clientX - dragMoveHelper.x
const movementY = e.clientY - dragMoveHelper.y
dragMoveHelper.onMove(movementX, movementY)
}
dragMoveHelper.x = e.clientX
dragMoveHelper.y = e.clientY
}
const handlePointerUp = (e: PointerEvent) => {
const mouseMoveButton = mind.mouseSelectionButton === 0 ? 2 : 0
if (e.button !== mouseMoveButton && e.pointerType === 'mouse') return
const target = e.target as HTMLElement
// Release pointer capture
if (target.hasPointerCapture && target.hasPointerCapture(e.pointerId)) {
target.releasePointerCapture(e.pointerId)
}
dragMoveHelper.clear()
}
const handleContextMenu = (e: MouseEvent) => {
console.log('handleContextMenu', e)
e.preventDefault()
// Only handle right-click for context menu
if (e.button !== 2) return
if (!mind.editable) return
const target = e.target as HTMLElement
if (isTopic(target) && !target.classList.contains('selected')) {
mind.selectNode(target)
}
setTimeout(() => {
// delay to avoid conflict with click event on Mac
if (mind.dragMoveHelper.moved) return
mind.bus.fire('showContextMenu', e)
}, 200)
}
const handleWheel = (e: WheelEvent) => {
e.stopPropagation()
e.preventDefault()
if (e.ctrlKey || e.metaKey) {
if (e.deltaY < 0) handleZoom(mind, 'in', mind.dragMoveHelper)
else if (mind.scaleVal - mind.scaleSensitivity > 0) handleZoom(mind, 'out', mind.dragMoveHelper)
} else if (e.shiftKey) {
mind.move(-e.deltaY, 0)
} else {
mind.move(-e.deltaX, -e.deltaY)
}
}
const { container } = mind
const off = on([
{ dom: container, evt: 'pointerdown', func: handlePointerDown },
{ dom: container, evt: 'pointermove', func: handlePointerMove },
{ dom: container, evt: 'pointerup', func: handlePointerUp },
{ dom: container, evt: 'pointerup', func: handleTouchDblClick },
{ dom: container, evt: 'click', func: handleClick },
{ dom: container, evt: 'dblclick', func: handleDblClick },
{ dom: container, evt: 'contextmenu', func: handleContextMenu },
{ dom: container, evt: 'wheel', func: typeof mind.handleWheel === 'function' ? mind.handleWheel : handleWheel },
])
return off
}

View File

@ -1,314 +0,0 @@
import { fillParent, refreshIds, unionTopics } from './utils/index'
import { createExpander, shapeTpc } from './utils/dom'
import { deepClone } from './utils/index'
import type { Children, Topic } from './types/dom'
import { DirectionClass, type MindElixirInstance, type NodeObj } from './types/index'
import { insertNodeObj, insertParentNodeObj, moveUpObj, moveDownObj, removeNodeObj, moveNodeObj } from './utils/objectManipulation'
import { addChildDom, removeNodeDom } from './utils/domManipulation'
import { LEFT, RIGHT } from './const'
const typeMap: Record<string, InsertPosition> = {
before: 'beforebegin',
after: 'afterend',
}
export const rmSubline = function (tpc: Topic) {
const mainNode = tpc.parentElement.parentElement
const lc = mainNode.lastElementChild
if (lc?.tagName === 'svg') lc?.remove() // clear svg group of main node
}
export const reshapeNode = function (this: MindElixirInstance, tpc: Topic, patchData: Partial<NodeObj>) {
const nodeObj = tpc.nodeObj
const origin = deepClone(nodeObj)
// merge styles
if (origin.style && patchData.style) {
patchData.style = Object.assign(origin.style, patchData.style)
}
const newObj = Object.assign(nodeObj, patchData)
shapeTpc.call(this, tpc, newObj)
this.linkDiv()
this.bus.fire('operation', {
name: 'reshapeNode',
obj: newObj,
origin,
})
}
const addChildFunc = function (mei: MindElixirInstance, tpc: Topic, node?: NodeObj) {
if (!tpc) return null
const nodeObj = tpc.nodeObj
if (nodeObj.expanded === false) {
mei.expandNode(tpc, true)
// dom had resetted
tpc = mei.findEle(nodeObj.id) as Topic
}
const newNodeObj = node || mei.generateNewObj()
if (nodeObj.children) nodeObj.children.push(newNodeObj)
else nodeObj.children = [newNodeObj]
fillParent(mei.nodeData)
const { grp, top: newTop } = mei.createWrapper(newNodeObj)
addChildDom(mei, tpc, grp)
return { newTop, newNodeObj }
}
export const insertSibling = function (this: MindElixirInstance, type: 'before' | 'after', el?: Topic, node?: NodeObj) {
const nodeEle = el || this.currentNode
if (!nodeEle) return
const nodeObj = nodeEle.nodeObj
if (!nodeObj.parent) {
this.addChild()
return
} else if (!nodeObj.parent?.parent && nodeObj.parent?.children?.length === 1 && this.direction === 2) {
// add at least one node to another side
this.addChild(this.findEle(nodeObj.parent!.id), node)
return
}
const newNodeObj = node || this.generateNewObj()
if (!nodeObj.parent?.parent) {
const direction = nodeEle.closest('me-main')!.className === DirectionClass.LHS ? LEFT : RIGHT
newNodeObj.direction = direction
}
insertNodeObj(newNodeObj, type, nodeObj)
fillParent(this.nodeData)
const t = nodeEle.parentElement
console.time('insertSibling_DOM')
const { grp, top } = this.createWrapper(newNodeObj)
t.parentElement.insertAdjacentElement(typeMap[type], grp)
this.linkDiv(grp.offsetParent)
if (!node) {
this.editTopic(top.firstChild)
}
console.timeEnd('insertSibling_DOM')
this.bus.fire('operation', {
name: 'insertSibling',
type,
obj: newNodeObj,
})
this.selectNode(top.firstChild, true)
}
export const insertParent = function (this: MindElixirInstance, el?: Topic, node?: NodeObj) {
const nodeEle = el || this.currentNode
if (!nodeEle) return
rmSubline(nodeEle)
const nodeObj = nodeEle.nodeObj
if (!nodeObj.parent) {
return
}
const newNodeObj = node || this.generateNewObj()
insertParentNodeObj(nodeObj, newNodeObj)
fillParent(this.nodeData)
const grp0 = nodeEle.parentElement.parentElement
console.time('insertParent_DOM')
const { grp, top } = this.createWrapper(newNodeObj, true)
top.appendChild(createExpander(true))
grp0.insertAdjacentElement('afterend', grp)
const c = this.createChildren([grp0])
top.insertAdjacentElement('afterend', c)
this.linkDiv()
if (!node) {
this.editTopic(top.firstChild)
}
this.selectNode(top.firstChild, true)
console.timeEnd('insertParent_DOM')
this.bus.fire('operation', {
name: 'insertParent',
obj: newNodeObj,
})
}
export const addChild = function (this: MindElixirInstance, el?: Topic, node?: NodeObj) {
console.time('addChild')
const nodeEle = el || this.currentNode
if (!nodeEle) return
const res = addChildFunc(this, nodeEle, node)
if (!res) return
const { newTop, newNodeObj } = res
// 添加节点关注添加节点前选择的节点,所以先触发事件再选择节点
this.bus.fire('operation', {
name: 'addChild',
obj: newNodeObj,
})
console.timeEnd('addChild')
if (!node) {
this.editTopic(newTop.firstChild)
}
this.selectNode(newTop.firstChild, true)
}
export const copyNode = function (this: MindElixirInstance, node: Topic, to: Topic) {
console.time('copyNode')
const deepCloneObj = deepClone(node.nodeObj)
refreshIds(deepCloneObj)
const res = addChildFunc(this, to, deepCloneObj)
if (!res) return
const { newNodeObj } = res
console.timeEnd('copyNode')
this.selectNode(this.findEle(newNodeObj.id))
this.bus.fire('operation', {
name: 'copyNode',
obj: newNodeObj,
})
}
export const copyNodes = function (this: MindElixirInstance, tpcs: Topic[], to: Topic) {
tpcs = unionTopics(tpcs)
const objs = []
for (let i = 0; i < tpcs.length; i++) {
const node = tpcs[i]
const deepCloneObj = deepClone(node.nodeObj)
refreshIds(deepCloneObj)
const res = addChildFunc(this, to, deepCloneObj)
if (!res) return
const { newNodeObj } = res
objs.push(newNodeObj)
}
this.unselectNodes(this.currentNodes)
this.selectNodes(objs.map(obj => this.findEle(obj.id)))
this.bus.fire('operation', {
name: 'copyNodes',
objs,
})
}
export const moveUpNode = function (this: MindElixirInstance, el?: Topic) {
const nodeEle = el || this.currentNode
if (!nodeEle) return
const obj = nodeEle.nodeObj
moveUpObj(obj)
const grp = nodeEle.parentNode.parentNode
grp.parentNode.insertBefore(grp, grp.previousSibling)
this.linkDiv()
this.bus.fire('operation', {
name: 'moveUpNode',
obj,
})
}
export const moveDownNode = function (this: MindElixirInstance, el?: Topic) {
const nodeEle = el || this.currentNode
if (!nodeEle) return
const obj = nodeEle.nodeObj
moveDownObj(obj)
const grp = nodeEle.parentNode.parentNode
if (grp.nextSibling) {
grp.nextSibling.insertAdjacentElement('afterend', grp)
} else {
grp.parentNode.prepend(grp)
}
this.linkDiv()
this.bus.fire('operation', {
name: 'moveDownNode',
obj,
})
}
export const removeNodes = function (this: MindElixirInstance, tpcs: Topic[]) {
if (tpcs.length === 0) return
tpcs = unionTopics(tpcs)
for (const tpc of tpcs) {
const nodeObj = tpc.nodeObj
const siblingLength = removeNodeObj(nodeObj)
removeNodeDom(tpc, siblingLength)
}
const last = tpcs[tpcs.length - 1]
this.selectNode(this.findEle(last.nodeObj.parent!.id))
this.linkDiv()
// 删除关注的是删除后选择的节点,所以先选择节点再触发 removeNodes 事件可以在事件中通过 currentNodes 获取之后选择的节点
this.bus.fire('operation', {
name: 'removeNodes',
objs: tpcs.map(tpc => tpc.nodeObj),
})
}
export const moveNodeIn = function (this: MindElixirInstance, from: Topic[], to: Topic) {
from = unionTopics(from)
const toObj = to.nodeObj
if (toObj.expanded === false) {
// TODO
this.expandNode(to, true)
to = this.findEle(toObj.id) as Topic
}
console.time('moveNodeIn')
for (const f of from) {
const obj = f.nodeObj
moveNodeObj('in', obj, toObj)
fillParent(this.nodeData) // update parent property
const fromTop = f.parentElement
addChildDom(this, to, fromTop.parentElement)
}
this.linkDiv()
this.bus.fire('operation', {
name: 'moveNodeIn',
objs: from.map(f => f.nodeObj),
toObj,
})
console.timeEnd('moveNodeIn')
}
const moveNode = (from: Topic[], type: 'before' | 'after', to: Topic, mei: MindElixirInstance) => {
from = unionTopics(from)
if (type === 'after') {
from = from.reverse()
}
const toObj = to.nodeObj
const c: Children[] = []
for (const f of from) {
const obj = f.nodeObj
moveNodeObj(type, obj, toObj)
fillParent(mei.nodeData)
rmSubline(f)
const fromWrp = f.parentElement.parentNode
if (!c.includes(fromWrp.parentElement)) {
c.push(fromWrp.parentElement)
}
const toWrp = to.parentElement.parentNode
toWrp.insertAdjacentElement(typeMap[type], fromWrp)
}
// When nodes are moved away, the original parent node may become childless
// In this case, we need to clean up the related DOM structure:
// remove expander buttons and empty wrapper containers
for (const item of c) {
if (item.childElementCount === 0 && item.tagName !== 'ME-MAIN') {
item.previousSibling.children[1]!.remove()
item.remove()
}
}
mei.linkDiv()
mei.bus.fire('operation', {
name: type === 'before' ? 'moveNodeBefore' : 'moveNodeAfter',
objs: from.map(f => f.nodeObj),
toObj,
})
}
export const moveNodeBefore = function (this: MindElixirInstance, from: Topic[], to: Topic) {
moveNode(from, 'before', to, this)
}
export const moveNodeAfter = function (this: MindElixirInstance, from: Topic[], to: Topic) {
moveNode(from, 'after', to, this)
}
export const beginEdit = function (this: MindElixirInstance, el?: Topic) {
const nodeEle = el || this.currentNode
if (!nodeEle) return
if (nodeEle.nodeObj.dangerouslySetInnerHTML) return
this.editTopic(nodeEle)
}
export const setNodeTopic = function (this: MindElixirInstance, el: Topic, topic: string) {
el.text.textContent = topic
el.nodeObj.topic = topic
this.linkDiv()
}

View File

@ -1,66 +0,0 @@
.map-container .context-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99;
.menu-list {
position: fixed;
list-style: none;
margin: 0;
padding: 0;
color: var(--panel-color);
box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.2);
border-radius: 5px;
overflow: hidden;
li {
min-width: 200px;
overflow: hidden;
white-space: nowrap;
padding: 6px 10px;
background: var(--panel-bgcolor);
border-bottom: 1px solid var(--panel-border-color);
cursor: pointer;
span {
line-height: 20px;
}
a {
color: #333;
text-decoration: none;
}
&.disabled {
display: none;
}
&:hover {
filter: brightness(0.95);
}
&:last-child {
border-bottom: 0;
}
span:last-child {
float: right;
}
}
}
.key {
font-size: 10px;
background-color: #f1f1f1;
color: #333;
padding: 2px 5px;
border-radius: 3px;
}
}
.map-container .tips {
position: absolute;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
color: var(--panel-color);
background: var(--panel-bgcolor);
opacity: 0.8;
padding: 5px 10px;
border-radius: 5px;
font-weight: bold;
}

View File

@ -1,229 +0,0 @@
import i18n from '../i18n'
import type { Topic } from '../types/dom'
import type { MindElixirInstance } from '../types/index'
import { encodeHTML, isTopic } from '../utils/index'
import './contextMenu.less'
import type { ArrowOptions } from '../arrow'
export type ContextMenuOption = {
focus?: boolean
link?: boolean
extend?: {
name: string
key?: string
onclick: (e: MouseEvent) => void
}[]
}
export default function (mind: MindElixirInstance, option: true | ContextMenuOption) {
option =
option === true
? {
focus: true,
link: true,
}
: option
const createTips = (words: string) => {
const div = document.createElement('div')
div.innerText = words
div.className = 'tips'
return div
}
const createLi = (id: string, name: string, keyname: string) => {
const li = document.createElement('li')
li.id = id
li.innerHTML = `<span>${encodeHTML(name)}</span><span ${keyname ? 'class="key"' : ''}>${encodeHTML(keyname)}</span>`
return li
}
const locale = i18n[mind.locale] ? mind.locale : 'en'
const lang = i18n[locale]
const add_child = createLi('cm-add_child', lang.addChild, 'Tab')
const add_parent = createLi('cm-add_parent', lang.addParent, 'Ctrl + Enter')
const add_sibling = createLi('cm-add_sibling', lang.addSibling, 'Enter')
const remove_child = createLi('cm-remove_child', lang.removeNode, 'Delete')
const focus = createLi('cm-fucus', lang.focus, '')
const unfocus = createLi('cm-unfucus', lang.cancelFocus, '')
const up = createLi('cm-up', lang.moveUp, 'PgUp')
const down = createLi('cm-down', lang.moveDown, 'Pgdn')
const link = createLi('cm-link', lang.link, '')
const linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '')
const summary = createLi('cm-summary', lang.summary, '')
const menuUl = document.createElement('ul')
menuUl.className = 'menu-list'
menuUl.appendChild(add_child)
menuUl.appendChild(add_parent)
menuUl.appendChild(add_sibling)
menuUl.appendChild(remove_child)
if (option.focus) {
menuUl.appendChild(focus)
menuUl.appendChild(unfocus)
}
menuUl.appendChild(up)
menuUl.appendChild(down)
menuUl.appendChild(summary)
if (option.link) {
menuUl.appendChild(link)
menuUl.appendChild(linkBidirectional)
}
if (option && option.extend) {
for (let i = 0; i < option.extend.length; i++) {
const item = option.extend[i]
const dom = createLi(item.name, item.name, item.key || '')
menuUl.appendChild(dom)
dom.onclick = e => {
item.onclick(e)
}
}
}
const menuContainer = document.createElement('div')
menuContainer.className = 'context-menu'
menuContainer.appendChild(menuUl)
menuContainer.hidden = true
mind.container.append(menuContainer)
let isRoot = true
// Helper function to actually render and position context menu.
const showMenu = (e: MouseEvent) => {
console.log('showContextMenu', e)
const target = e.target as HTMLElement
if (isTopic(target)) {
if (target.parentElement!.tagName === 'ME-ROOT') {
isRoot = true
} else {
isRoot = false
}
if (isRoot) {
focus.className = 'disabled'
up.className = 'disabled'
down.className = 'disabled'
add_parent.className = 'disabled'
add_sibling.className = 'disabled'
remove_child.className = 'disabled'
} else {
focus.className = ''
up.className = ''
down.className = ''
add_parent.className = ''
add_sibling.className = ''
remove_child.className = ''
}
menuContainer.hidden = false
menuUl.style.top = ''
menuUl.style.bottom = ''
menuUl.style.left = ''
menuUl.style.right = ''
const rect = menuUl.getBoundingClientRect()
const height = menuUl.offsetHeight
const width = menuUl.offsetWidth
const relativeY = e.clientY - rect.top
const relativeX = e.clientX - rect.left
if (height + relativeY > window.innerHeight) {
menuUl.style.top = ''
menuUl.style.bottom = '0px'
} else {
menuUl.style.bottom = ''
menuUl.style.top = relativeY + 15 + 'px'
}
if (width + relativeX > window.innerWidth) {
menuUl.style.left = ''
menuUl.style.right = '0px'
} else {
menuUl.style.right = ''
menuUl.style.left = relativeX + 10 + 'px'
}
}
}
mind.bus.addListener('showContextMenu', showMenu)
menuContainer.onclick = e => {
if (e.target === menuContainer) menuContainer.hidden = true
}
add_child.onclick = () => {
mind.addChild()
menuContainer.hidden = true
}
add_parent.onclick = () => {
mind.insertParent()
menuContainer.hidden = true
}
add_sibling.onclick = () => {
if (isRoot) return
mind.insertSibling('after')
menuContainer.hidden = true
}
remove_child.onclick = () => {
if (isRoot) return
mind.removeNodes(mind.currentNodes || [])
menuContainer.hidden = true
}
focus.onclick = () => {
if (isRoot) return
mind.focusNode(mind.currentNode as Topic)
menuContainer.hidden = true
}
unfocus.onclick = () => {
mind.cancelFocus()
menuContainer.hidden = true
}
up.onclick = () => {
if (isRoot) return
mind.moveUpNode()
menuContainer.hidden = true
}
down.onclick = () => {
if (isRoot) return
mind.moveDownNode()
menuContainer.hidden = true
}
const linkFunc = (options?: ArrowOptions) => {
menuContainer.hidden = true
const from = mind.currentNode as Topic
const tips = createTips(lang.clickTips)
mind.container.appendChild(tips)
mind.map.addEventListener(
'click',
e => {
e.preventDefault()
tips.remove()
const target = e.target as Topic
if (target.parentElement.tagName === 'ME-PARENT' || target.parentElement.tagName === 'ME-ROOT') {
mind.createArrow(from, target, options)
} else {
console.log('link cancel')
}
},
{
once: true,
}
)
}
link.onclick = () => linkFunc()
linkBidirectional.onclick = () => linkFunc({ bidirectional: true })
summary.onclick = () => {
menuContainer.hidden = true
mind.createSummary()
mind.unselectNodes(mind.currentNodes)
}
return () => {
// maybe useful?
add_child.onclick = null
add_parent.onclick = null
add_sibling.onclick = null
remove_child.onclick = null
focus.onclick = null
unfocus.onclick = null
up.onclick = null
down.onclick = null
link.onclick = null
summary.onclick = null
menuContainer.onclick = null
mind.container.oncontextmenu = null
}
}

View File

@ -1,263 +0,0 @@
import type { Topic } from '../types/dom'
import type { MindElixirInstance } from '../types'
import { setAttributes } from '../utils'
import { getOffsetLT, isTopic } from '../utils'
const ns = 'http://www.w3.org/2000/svg'
function createSvgDom(height: string, width: string) {
const svg = document.createElementNS(ns, 'svg')
setAttributes(svg, {
version: '1.1',
xmlns: ns,
height,
width,
})
return svg
}
function lineHightToPadding(lineHeight: string, fontSize: string) {
return (parseInt(lineHeight) - parseInt(fontSize)) / 2
}
function generateSvgText(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: number, y: number) {
const g = document.createElementNS(ns, 'g')
let content = ''
if ((tpc as Topic).text) {
content = (tpc as Topic).text.textContent!
} else {
content = tpc.childNodes[0].textContent!
}
const lines = content!.split('\n')
lines.forEach((line, index) => {
const text = document.createElementNS(ns, 'text')
setAttributes(text, {
x: x + parseInt(tpcStyle.paddingLeft) + '',
y:
y +
parseInt(tpcStyle.paddingTop) +
lineHightToPadding(tpcStyle.lineHeight, tpcStyle.fontSize) * (index + 1) +
parseFloat(tpcStyle.fontSize) * (index + 1) +
'',
'text-anchor': 'start',
'font-family': tpcStyle.fontFamily,
'font-size': `${tpcStyle.fontSize}`,
'font-weight': `${tpcStyle.fontWeight}`,
fill: `${tpcStyle.color}`,
})
text.innerHTML = line
g.appendChild(text)
})
return g
}
function generateSvgTextUsingForeignObject(tpc: HTMLElement, tpcStyle: CSSStyleDeclaration, x: number, y: number) {
let content = ''
if ((tpc as Topic).nodeObj?.dangerouslySetInnerHTML) {
content = (tpc as Topic).nodeObj.dangerouslySetInnerHTML!
} else if ((tpc as Topic).text) {
content = (tpc as Topic).text.textContent!
} else {
content = tpc.childNodes[0].textContent!
}
const foreignObject = document.createElementNS(ns, 'foreignObject')
setAttributes(foreignObject, {
x: x + parseInt(tpcStyle.paddingLeft) + '',
y: y + parseInt(tpcStyle.paddingTop) + '',
width: tpcStyle.width,
height: tpcStyle.height,
})
const div = document.createElement('div')
setAttributes(div, {
xmlns: 'http://www.w3.org/1999/xhtml',
style: `font-family: ${tpcStyle.fontFamily}; font-size: ${tpcStyle.fontSize}; font-weight: ${tpcStyle.fontWeight}; color: ${tpcStyle.color}; white-space: pre-wrap;`,
})
div.innerHTML = content
foreignObject.appendChild(div)
return foreignObject
}
function createElBox(mei: MindElixirInstance, tpc: Topic) {
const tpcStyle = getComputedStyle(tpc)
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
const bg = document.createElementNS(ns, 'rect')
setAttributes(bg, {
x: x + '',
y: y + '',
rx: tpcStyle.borderRadius,
ry: tpcStyle.borderRadius,
width: tpcStyle.width,
height: tpcStyle.height,
fill: tpcStyle.backgroundColor,
stroke: tpcStyle.borderColor,
'stroke-width': tpcStyle.borderWidth,
})
return bg
}
function convertDivToSvg(mei: MindElixirInstance, tpc: HTMLElement, useForeignObject = false) {
const tpcStyle = getComputedStyle(tpc)
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
const bg = document.createElementNS(ns, 'rect')
setAttributes(bg, {
x: x + '',
y: y + '',
rx: tpcStyle.borderRadius,
ry: tpcStyle.borderRadius,
width: tpcStyle.width,
height: tpcStyle.height,
fill: tpcStyle.backgroundColor,
stroke: tpcStyle.borderColor,
'stroke-width': tpcStyle.borderWidth,
})
const g = document.createElementNS(ns, 'g')
g.appendChild(bg)
let text: SVGGElement | null
if (useForeignObject) {
text = generateSvgTextUsingForeignObject(tpc, tpcStyle, x, y)
} else text = generateSvgText(tpc, tpcStyle, x, y)
g.appendChild(text)
return g
}
function convertAToSvg(mei: MindElixirInstance, a: HTMLAnchorElement) {
const aStyle = getComputedStyle(a)
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, a)
const svgA = document.createElementNS(ns, 'a')
const text = document.createElementNS(ns, 'text')
setAttributes(text, {
x: x + '',
y: y + parseInt(aStyle.fontSize) + '',
'text-anchor': 'start',
'font-family': aStyle.fontFamily,
'font-size': `${aStyle.fontSize}`,
'font-weight': `${aStyle.fontWeight}`,
fill: `${aStyle.color}`,
})
text.innerHTML = a.textContent!
svgA.appendChild(text)
svgA.setAttribute('href', a.href)
return svgA
}
function convertImgToSvg(mei: MindElixirInstance, a: HTMLImageElement) {
const aStyle = getComputedStyle(a)
const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, a)
const svgI = document.createElementNS(ns, 'image')
setAttributes(svgI, {
x: x + '',
y: y + '',
width: aStyle.width + '',
height: aStyle.height + '',
href: a.src,
})
return svgI
}
const padding = 100
const head = `<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`
const generateSvg = (mei: MindElixirInstance, noForeignObject = false) => {
const mapDiv = mei.nodes
const height = mapDiv.offsetHeight + padding * 2
const width = mapDiv.offsetWidth + padding * 2
const svg = createSvgDom(height + 'px', width + 'px')
const g = document.createElementNS(ns, 'svg')
const bgColor = document.createElementNS(ns, 'rect')
setAttributes(bgColor, {
x: '0',
y: '0',
width: `${width}`,
height: `${height}`,
fill: mei.theme.cssVar['--bgcolor'] as string,
})
svg.appendChild(bgColor)
mapDiv.querySelectorAll('.subLines').forEach(item => {
const clone = item.cloneNode(true) as SVGSVGElement
const { offsetLeft, offsetTop } = getOffsetLT(mapDiv, item.parentElement as HTMLElement)
clone.setAttribute('x', `${offsetLeft}`)
clone.setAttribute('y', `${offsetTop}`)
g.appendChild(clone)
})
const mainLine = mapDiv.querySelector('.lines')?.cloneNode(true)
mainLine && g.appendChild(mainLine)
const topiclinks = mapDiv.querySelector('.topiclinks')?.cloneNode(true)
topiclinks && g.appendChild(topiclinks)
const summaries = mapDiv.querySelector('.summary')?.cloneNode(true)
summaries && g.appendChild(summaries)
mapDiv.querySelectorAll<Topic>('me-tpc').forEach(tpc => {
if (tpc.nodeObj.dangerouslySetInnerHTML) {
g.appendChild(convertDivToSvg(mei, tpc, noForeignObject ? false : true))
} else {
g.appendChild(createElBox(mei, tpc))
g.appendChild(convertDivToSvg(mei, tpc.text, noForeignObject ? false : true))
}
})
mapDiv.querySelectorAll('.tags > span').forEach(tag => {
g.appendChild(convertDivToSvg(mei, tag as HTMLElement))
})
mapDiv.querySelectorAll('.icons > span').forEach(icon => {
g.appendChild(convertDivToSvg(mei, icon as HTMLElement))
})
mapDiv.querySelectorAll('.hyper-link').forEach(hl => {
g.appendChild(convertAToSvg(mei, hl as HTMLAnchorElement))
})
mapDiv.querySelectorAll('img').forEach(img => {
g.appendChild(convertImgToSvg(mei, img))
})
setAttributes(g, {
x: padding + '',
y: padding + '',
overflow: 'visible',
})
svg.appendChild(g)
return svg
}
const generateSvgStr = (svgEl: SVGSVGElement, injectCss?: string) => {
if (injectCss) svgEl.insertAdjacentHTML('afterbegin', '<style>' + injectCss + '</style>')
return head + svgEl.outerHTML
}
function blobToUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = evt => {
resolve(evt.target!.result as string)
}
reader.onerror = err => {
reject(err)
}
reader.readAsDataURL(blob)
})
}
export const exportSvg = function (this: MindElixirInstance, noForeignObject = false, injectCss?: string) {
const svgEl = generateSvg(this, noForeignObject)
const svgString = generateSvgStr(svgEl, injectCss)
const blob = new Blob([svgString], { type: 'image/svg+xml' })
return blob
}
export const exportPng = async function (this: MindElixirInstance, noForeignObject = false, injectCss?: string): Promise<Blob | null> {
const blob = this.exportSvg(noForeignObject, injectCss)
// use base64 to bypass canvas taint
const url = await blobToUrl(blob)
return new Promise((resolve, reject) => {
const img = new Image()
img.setAttribute('crossOrigin', 'anonymous')
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')!
ctx.drawImage(img, 0, 0)
canvas.toBlob(resolve, 'image/png', 1)
}
img.src = url
img.onerror = reject
})
}

View File

@ -1,242 +0,0 @@
import type { Topic } from '../types/dom'
import type { KeypressOptions, MindElixirInstance } from '../types/index'
import { DirectionClass } from '../types/index'
import { setExpand } from '../utils'
const selectRootLeft = (mei: MindElixirInstance) => {
const tpcs = mei.map.querySelectorAll('.lhs>me-wrapper>me-parent>me-tpc')
mei.selectNode(tpcs[Math.ceil(tpcs.length / 2) - 1] as Topic)
}
const selectRootRight = (mei: MindElixirInstance) => {
const tpcs = mei.map.querySelectorAll('.rhs>me-wrapper>me-parent>me-tpc')
mei.selectNode(tpcs[Math.ceil(tpcs.length / 2) - 1] as Topic)
}
const selectRoot = (mei: MindElixirInstance) => {
mei.selectNode(mei.map.querySelector('me-root>me-tpc') as Topic)
}
const selectParent = function (mei: MindElixirInstance, currentNode: Topic) {
const parent = currentNode.parentElement.parentElement.parentElement.previousSibling
if (parent) {
const target = parent.firstChild
mei.selectNode(target)
}
}
const selectFirstChild = function (mei: MindElixirInstance, currentNode: Topic) {
const children = currentNode.parentElement.nextSibling
if (children && children.firstChild) {
const target = children.firstChild.firstChild.firstChild
mei.selectNode(target)
}
}
const handleLeftRight = function (mei: MindElixirInstance, direction: DirectionClass) {
const current = mei.currentNode || mei.currentNodes?.[0]
if (!current) return
const nodeObj = current.nodeObj
const main = current.offsetParent.offsetParent.parentElement
if (!nodeObj.parent) {
direction === DirectionClass.LHS ? selectRootLeft(mei) : selectRootRight(mei)
} else if (main.className === direction) {
selectFirstChild(mei, current)
} else {
if (!nodeObj.parent?.parent) {
selectRoot(mei)
} else {
selectParent(mei, current)
}
}
}
const handlePrevNext = function (mei: MindElixirInstance, direction: 'previous' | 'next') {
const current = mei.currentNode
if (!current) return
const nodeObj = current.nodeObj
if (!nodeObj.parent) return
const s = (direction + 'Sibling') as 'previousSibling' | 'nextSibling'
const sibling = current.parentElement.parentElement[s]
if (sibling) {
mei.selectNode(sibling.firstChild.firstChild)
} else {
// handle multiple nodes including last node
mei.selectNode(current)
}
}
export const handleZoom = function (
mei: MindElixirInstance,
direction: 'in' | 'out',
offset?: {
x: number
y: number
}
) {
const { scaleVal, scaleSensitivity } = mei
switch (direction) {
case 'in':
mei.scale(scaleVal + scaleSensitivity, offset)
break
case 'out':
mei.scale(scaleVal - scaleSensitivity, offset)
}
}
export default function (mind: MindElixirInstance, options: boolean | KeypressOptions) {
options = options === true ? {} : options
const handleRemove = () => {
if (mind.currentArrow) mind.removeArrow()
else if (mind.currentSummary) mind.removeSummary(mind.currentSummary.summaryObj.id)
else if (mind.currentNodes) {
mind.removeNodes(mind.currentNodes)
}
}
// Track key sequence for Ctrl+K+Ctrl+0
let ctrlKPressed = false
let ctrlKTimeout: number | null = null
const handleControlKPlusX = (e: KeyboardEvent) => {
const nodeData = mind.nodeData
if (e.key === '0') {
// Ctrl+K+Ctrl+0: Collapse all nodes
for (const node of nodeData.children!) {
setExpand(node, false)
}
}
if (e.key === '=') {
// Ctrl+K+Ctrl+1: Expand all nodes
for (const node of nodeData.children!) {
setExpand(node, true)
}
}
if (['1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key)) {
for (const node of nodeData.children!) {
setExpand(node, true, Number(e.key) - 1)
}
}
mind.refresh()
mind.toCenter()
ctrlKPressed = false
if (ctrlKTimeout) {
clearTimeout(ctrlKTimeout)
ctrlKTimeout = null
mind.container.removeEventListener('keydown'