182 lines
4.1 KiB
Vue
182 lines
4.1 KiB
Vue
<template>
|
|
<b-overlay :show="nodeTree.nodes.length === 0" class="h-100">
|
|
<map-zoom id="library-tree" :min-zoom="0.3" :max-zoom="1">
|
|
<path
|
|
v-for="link in nodeTree.links"
|
|
:key="`${link.source.data.id}_${link.target.data.id}`"
|
|
:d="lineGenerator([link.source, link.target])"
|
|
:class="['svg-link', (link.linkType || link.target.data.linkType)]"
|
|
/>
|
|
|
|
<circle
|
|
v-for="node in nodeTree.nodes"
|
|
:key="'circle' + node.data.id"
|
|
:cx="node.x"
|
|
:cy="node.y"
|
|
class="svg-dot"
|
|
:class="['svg-dot-' + node.data.variant, { 'origin': node.parent === null }]"
|
|
tabindex="0"
|
|
@click="onNodeClick(node.data)"
|
|
@keyup.enter="onNodeClick(node.data)"
|
|
@mouseenter="activeNode = node"
|
|
/>
|
|
|
|
<g>
|
|
<rect class="svg-overlay" />
|
|
<foreignObject
|
|
v-if="activeNode"
|
|
:x="activeNode.x"
|
|
:y="activeNode.y"
|
|
>
|
|
<dot-button
|
|
:variant="activeNode.data.variant"
|
|
class="active"
|
|
@click="onNodeClick(activeNode.data)"
|
|
@mouseleave="activeNode = null"
|
|
>
|
|
<template v-if="activeNode.data.variant !== 'depart'">
|
|
{{ $t('variants.' + activeNode.data.variant) }}<br>
|
|
</template>
|
|
{{ toCommaList(activeNode.data.authors) }},<br>
|
|
<span v-if="activeNode.data.preTitle" v-html="trim(activeNode.data.preTitle) + ','" />
|
|
<span v-html="trim(activeNode.data.title) || '<em>pas de titre ital</em>'" />
|
|
</dot-button>
|
|
</foreignObject>
|
|
</g>
|
|
</map-zoom>
|
|
</b-overlay>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapGetters } from 'vuex'
|
|
import { line } from 'd3-shape'
|
|
import { forceSimulation, forceLink, forceManyBody, forceX, forceY } from 'd3-force'
|
|
|
|
import { trim, toCommaList } from '@/helpers/common'
|
|
import { MapZoom } from '@/components/layouts'
|
|
|
|
|
|
export default {
|
|
name: 'LibraryTree',
|
|
|
|
components: {
|
|
MapZoom
|
|
},
|
|
|
|
data () {
|
|
return {
|
|
activeNode: null
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['nodeTree']),
|
|
|
|
// ONE TIME GETTER
|
|
simulation () {
|
|
return forceSimulation()
|
|
.force('charge', forceManyBody().strength((d) => {
|
|
if (d.data.linkType === 'parents') return -2000
|
|
if (d.data.linkType === 'siblings') return -500
|
|
return -1000
|
|
}))
|
|
.force('link', forceLink().id(d => d.id).distance((d) => {
|
|
if (d.linkType === 'parents') return 200
|
|
if (d.linkType === 'siblings') return 100
|
|
return 0
|
|
}).strength(0.5))
|
|
.force('x', forceX())
|
|
.force('y', forceY())
|
|
},
|
|
|
|
// ONE TIME GETTER
|
|
lineGenerator () {
|
|
return line().x(node => node.x).y(node => node.y)
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
nodeTree (tree) {
|
|
this.activeNode = null
|
|
this.simulation.nodes(tree.nodes)
|
|
this.simulation.force('link').links(tree.links)
|
|
this.simulation.alpha(0.5).restart()
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
toCommaList,
|
|
trim,
|
|
|
|
onNodeClick (node) {
|
|
if (node.parents && node.parents.length) {
|
|
this.$emit('open-node', { parentId: node.parents[0].id, childId: node.id })
|
|
} else {
|
|
this.$emit('open-node', { parentId: node.id })
|
|
}
|
|
}
|
|
},
|
|
|
|
created () {
|
|
this.$store.dispatch('INIT_LIBRARY_TREE')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.svg {
|
|
&-link {
|
|
stroke: grey;
|
|
vector-effect: non-scaling-stroke;
|
|
|
|
&.siblings {
|
|
stroke-dasharray: 4;
|
|
}
|
|
|
|
&.parents {
|
|
stroke: red;
|
|
opacity: .3;
|
|
}
|
|
}
|
|
|
|
&-dot {
|
|
r: 9.5px;
|
|
|
|
@each $color, $value in $theme-colors {
|
|
&-#{$color} {
|
|
fill: $value;
|
|
|
|
@if $color == 'depart' {
|
|
stroke: $black;
|
|
stroke-width: 3px;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.origin {
|
|
r: 15px;
|
|
}
|
|
}
|
|
|
|
&-overlay {
|
|
width: 100%;
|
|
height: 100%;
|
|
x: -50%;
|
|
y: -50%;
|
|
fill: transparent;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
foreignObject {
|
|
height: 1px;
|
|
width: 1px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.dot-btn {
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
</style>
|