diff --git a/package-lock.json b/package-lock.json
index f59cd8c..c93f624 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,9 +20,11 @@
"@ckeditor/vite-plugin-ckeditor5": "^0.1.1",
"@fontsource/source-sans-pro": "^4.5.11",
"@fortawesome/fontawesome-free": "^6.4.0",
+ "@types/cytoscape": "^3.19.9",
"@types/marked": "^4.0.8",
"@vueuse/components": "^10.1.0",
"@vueuse/core": "^10.1.2",
+ "cytoscape": "^3.25.0",
"daisyui": "^2.51.6",
"date-fns": "^2.29.3",
"dompurify": "^3.0.2",
@@ -1644,6 +1646,11 @@
"integrity": "sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==",
"dev": true
},
+ "node_modules/@types/cytoscape": {
+ "version": "3.19.9",
+ "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.19.9.tgz",
+ "integrity": "sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg=="
+ },
"node_modules/@types/dompurify": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz",
@@ -2720,6 +2727,18 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
+ "node_modules/cytoscape": {
+ "version": "3.25.0",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.25.0.tgz",
+ "integrity": "sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==",
+ "dependencies": {
+ "heap": "^0.2.6",
+ "lodash": "^4.17.21"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/daisyui": {
"version": "2.51.6",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.51.6.tgz",
@@ -3772,6 +3791,11 @@
"he": "bin/he"
}
},
+ "node_modules/heap": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
+ "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
+ },
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -4252,8 +4276,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
@@ -7605,6 +7628,11 @@
"integrity": "sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==",
"dev": true
},
+ "@types/cytoscape": {
+ "version": "3.19.9",
+ "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.19.9.tgz",
+ "integrity": "sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg=="
+ },
"@types/dompurify": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz",
@@ -8352,6 +8380,15 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
+ "cytoscape": {
+ "version": "3.25.0",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.25.0.tgz",
+ "integrity": "sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==",
+ "requires": {
+ "heap": "^0.2.6",
+ "lodash": "^4.17.21"
+ }
+ },
"daisyui": {
"version": "2.51.6",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.51.6.tgz",
@@ -9140,6 +9177,11 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
+ "heap": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
+ "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
+ },
"hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -9486,8 +9528,7 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.21",
diff --git a/package.json b/package.json
index 338a2f9..545e393 100644
--- a/package.json
+++ b/package.json
@@ -25,9 +25,11 @@
"@ckeditor/vite-plugin-ckeditor5": "^0.1.1",
"@fontsource/source-sans-pro": "^4.5.11",
"@fortawesome/fontawesome-free": "^6.4.0",
+ "@types/cytoscape": "^3.19.9",
"@types/marked": "^4.0.8",
"@vueuse/components": "^10.1.0",
"@vueuse/core": "^10.1.2",
+ "cytoscape": "^3.25.0",
"daisyui": "^2.51.6",
"date-fns": "^2.29.3",
"dompurify": "^3.0.2",
diff --git a/src/App.vue b/src/App.vue
index 725ecb9..21305b2 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -28,7 +28,7 @@ const sideBarCollapsed = ref(false)
@update="(note) => updateNote(note.id, note)"
/>
-
+
diff --git a/src/components/SideBar.vue b/src/components/SideBar.vue
index 4960f39..af23f14 100644
--- a/src/components/SideBar.vue
+++ b/src/components/SideBar.vue
@@ -48,7 +48,7 @@ const emit = defineEmits<{
+import {
+ notesRelations,
+ getNoteById,
+ activeNote,
+ setActiveNote,
+ rootNote
+} from '@/composables/useNotes'
+import cytoscape from 'cytoscape'
+import shortid from 'shortid'
+
+const renderMindmap = () => {
+ const mindmapCanvas = mindmapElement.value
+ if (!mindmapCanvas) return
+ const style = {
+ contextedBlue: '#1e4bc4',
+ nodeBackground: '#6c757d',
+ edge: '#ced4da'
+ }
+
+ const boundingBox = {
+ x1: 0,
+ y1: 0,
+ w: mindmapCanvas.clientWidth,
+ h: mindmapCanvas.clientHeight
+ }
+ const elements = {
+ nodes: nodes.value,
+ edges: [
+ ...links.value.map((link) => ({ data: { id: `${link.source}-${link.target}`, ...link } }))
+ ]
+ }
+ const cy = cytoscape({
+ container: mindmapCanvas,
+ elements,
+ layout: {
+ name: 'cose',
+
+ // Called on `layoutready`
+ ready: function () {},
+
+ // Called on `layoutstop`
+ stop: function () {},
+
+ // Whether to animate while running the layout
+ // true : Animate continuously as the layout is running
+ // false : Just show the end result
+ // 'end' : Animate with the end result, from the initial positions to the end positions
+ animate: false,
+
+ // Easing of the animation for animate:'end'
+ animationEasing: undefined,
+
+ // The duration of the animation for animate:'end'
+ animationDuration: undefined,
+
+ // A function that determines whether the node should be animated
+ // All nodes animated by default on animate enabled
+ // Non-animated nodes are positioned immediately when the layout starts
+ animateFilter: function (node, i) {
+ return true
+ },
+
+ // The layout animates only after this many milliseconds for animate:true
+ // (prevents flashing on fast runs)
+ animationThreshold: 250,
+
+ // Number of iterations between consecutive screen positions update
+ refresh: 20,
+
+ // Whether to fit the network view after when done
+ fit: true,
+
+ // Padding on fit
+ padding: 30,
+
+ // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
+ // boundingBox: undefined,
+ boundingBox,
+
+ // Excludes the label when calculating node bounding boxes for the layout algorithm
+ nodeDimensionsIncludeLabels: false,
+
+ // Randomize the initial positions of the nodes (true) or use existing positions (false)
+ randomize: false,
+
+ // Extra spacing between components in non-compound graphs
+ componentSpacing: 40,
+
+ // Node repulsion (non overlapping) multiplier
+ nodeRepulsion: function (node) {
+ return 2048
+ },
+
+ // Node repulsion (overlapping) multiplier
+ nodeOverlap: 4,
+
+ // Ideal edge (non nested) length
+ idealEdgeLength: function (edge) {
+ return 32
+ },
+
+ // Divisor to compute edge forces
+ edgeElasticity: function (edge) {
+ return 32
+ },
+
+ // Nesting factor (multiplier) to compute ideal edge length for nested edges
+ nestingFactor: 1.2,
+
+ // Gravity force (constant)
+ gravity: 1,
+
+ // Maximum number of iterations to perform
+ numIter: 1000,
+
+ // Initial temperature (maximum node displacement)
+ initialTemp: 1000,
+
+ // Cooling factor (how the temperature is reduced between consecutive iterations
+ coolingFactor: 0.99,
+
+ // Lower temperature threshold (below this point the layout will end)
+ minTemp: 1.0
+ },
+ // userZoomingEnabled: false,
+ userPanningEnabled: false,
+ pixelRatio: window.devicePixelRatio ? window.devicePixelRatio * 1.5 : 'auto',
+ style: [
+ // the stylesheet for the graph
+ {
+ selector: 'node',
+ style: {
+ 'background-color': style.nodeBackground,
+ label: 'data(title)',
+ 'font-family': 'Source Sans Pro, sans-serif',
+ 'font-weight': 400,
+ 'font-size': '1.5em',
+ 'text-events': 'yes'
+ }
+ },
+ {
+ selector: '.current',
+ style: {
+ 'background-color': style.contextedBlue
+ }
+ },
+ {
+ selector: '.mouseover',
+ style: {
+ 'background-color': style.contextedBlue,
+ color: style.contextedBlue
+ }
+ },
+ {
+ selector: 'edge',
+ style: {
+ width: 3,
+ 'line-color': style.edge,
+ 'target-arrow-color': style.edge,
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'bezier'
+ }
+ }
+ ]
+ })
+ cy.nodes().forEach((node) => {
+ if (node.data('id') === activeNote.value?.id) node.addClass('current')
+ })
+ cy.nodes().on('tap', (event) => {
+ setActiveNote(event.target.data('id'))
+ })
+ cy.on('mouseover', 'node', (event) => {
+ event.target.addClass('mouseover')
+ mindmapCanvas.classList.add('mouseover')
+ })
+ cy.on('mouseout', 'node', (event) => {
+ event.target.removeClass('mouseover')
+ mindmapCanvas.classList.remove('mouseover')
+ })
+}
+
+const mindmapElement = ref(null)
+
+interface Mindmap {
+ id: string
+ notes: string[]
+ isRoot: boolean
+}
+const selectedMindmap = ref()
+const mindmaps = computed(() => {
+ const mindmaps = Object.entries(notesRelations.value).reduce((mindmaps, [noteId, relations]) => {
+ const atomicMindmap = [noteId, ...relations.to, ...relations.from]
+ const indices = mindmaps
+ .filter(
+ (mindmap) => [...mindmap].filter((noteId) => atomicMindmap.includes(noteId)).length > 0
+ )
+ .map((mindmap) => mindmaps.indexOf(mindmap))
+ if (indices.length > 0) {
+ const index = indices[0]
+ const currentMindmap = indices.reduce(
+ (mindmap, index) => mindmap.concat(mindmaps[index]),
+ [] as string[]
+ )
+ indices.forEach((index, i) => {
+ if (i !== 0) mindmaps.splice(index, 1)
+ })
+ mindmaps[index] = [...currentMindmap, ...atomicMindmap].filter(
+ (item, index, arr) => arr.indexOf(item) === index
+ )
+ } else {
+ mindmaps.push(atomicMindmap)
+ }
+ return mindmaps
+ }, [] as string[][])
+ return mindmaps
+ .filter((mindmap) => mindmap.length > 1)
+ .sort((a, b) => {
+ return a.includes(rootNote.value?.id || '') ? b.length - a.length : 1
+ })
+ .slice(0, 5)
+ .map((mindmap): Mindmap => {
+ const isRoot = mindmap.includes(rootNote.value?.id || '')
+ return { id: shortid.generate(), notes: mindmap, isRoot }
+ })
+})
+watch(
+ mindmaps,
+ () => {
+ if (!selectedMindmap.value) selectedMindmap.value = mindmaps.value[0]
+ },
+ { immediate: true }
+)
+watch(selectedMindmap, () => setTimeout(() => renderMindmap(), 0), { immediate: true })
+
+const nodes = computed(() => {
+ return (
+ Object.entries(notesRelations.value)
+ // .filter(([, relations]) => relations.to.length > 0)
+ .filter(([noteId]) => selectedMindmap.value?.notes.includes(noteId))
+ .map(([noteId]) => {
+ return {
+ data: {
+ id: noteId,
+ title: getNoteById(noteId)?.title
+ }
+ }
+ })
+ )
+})
+
+const links = computed(() => {
+ return Object.entries(notesRelations.value)
+ .filter(([noteId]) => selectedMindmap.value?.notes.includes(noteId))
+ .filter(([, relations]) => relations.to.length > 0)
+ .map(([noteId, relations]) => {
+ return relations.to.map((to) => ({
+ source: noteId,
+ target: to
+ }))
+ })
+ .reduce((arr, elem) => arr.concat(elem), [])
+})
+
+
+
+
+
diff --git a/vite.config.ts b/vite.config.ts
index 4196134..044204d 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -15,11 +15,11 @@ export default defineConfig({
vue(),
AutoImport({ imports: ['vue'] }),
Components(),
- ckeditor5({ theme: require.resolve('@ckeditor/ckeditor5-theme-lark') }),
+ ckeditor5({ theme: require.resolve('@ckeditor/ckeditor5-theme-lark') })
],
resolve: {
alias: {
- '@': fileURLToPath(new URL('./src', import.meta.url)),
- },
- },
+ '@': fileURLToPath(new URL('./src', import.meta.url))
+ }
+ }
})