Files
enfrancais-app/src/pages/library/LibraryTree.vue
T

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>