|
@@ -1,35 +1,112 @@
|
|
|
<template>
|
|
|
- <b-overlay :show="nodeTree === undefined" class="h-100">
|
|
|
- <node-map
|
|
|
- v-if="nodeTree !== undefined"
|
|
|
- v-bind="nodeTree"
|
|
|
- v-on="$listeners"
|
|
|
- />
|
|
|
+ <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 NodeMap from '@/components/NodeMap'
|
|
|
+import { trim, toCommaList } from '@/helpers/common'
|
|
|
+import { MapZoom } from '@/components/layouts'
|
|
|
|
|
|
|
|
|
export default {
|
|
|
- name: 'TreeMap',
|
|
|
+ name: 'LibraryTree',
|
|
|
|
|
|
components: {
|
|
|
- NodeMap
|
|
|
+ MapZoom
|
|
|
},
|
|
|
|
|
|
data () {
|
|
|
return {
|
|
|
- loading: true,
|
|
|
- mapData: undefined
|
|
|
+ activeNode: null
|
|
|
}
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
- ...mapGetters(['nodeTree'])
|
|
|
+ ...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
|
|
|
},
|
|
|
|
|
|
created () {
|
|
@@ -39,4 +116,58 @@ export default {
|
|
|
</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>
|