Browse Source

update library opening strategy

axolotle 3 years ago
parent
commit
ecc5253f38

+ 4 - 0
src/App.vue

@@ -79,4 +79,8 @@ export default {
     opacity: .25;
   }
 }
+
+#app > header {
+  border-bottom: $line;
+}
 </style>

+ 2 - 2
src/assets/scss/abstracts/_variables.scss

@@ -53,8 +53,8 @@ $grid-breakpoints: (
   xs: 0,
   sm: 576px,
   md: 1024px,
-  // lg: 992px,
-  // xl: 1200px
+  lg: 1200px,
+  xl: 1440px,
 );
 
 $layout-bp: md;

+ 1 - 2
src/main.js

@@ -10,8 +10,7 @@ import store from './store'
 
 Vue.use(BootstrapVue, {
   BButton: { pill: true },
-  BCardHeader: { headerBgVariant: 'white' },
-  BCardFooter: { footerBgVariant: 'white' }
+  BOverlay: { blur: null, spinnerType: 'grow', opacity: 0 }
 })
 Vue.use(Meta)
 Vue.use(Vue2TouchEvents)

+ 2 - 1
src/messages/fr.json

@@ -41,5 +41,6 @@
   "siblings": "Textes rebonds",
   "text": {
     "read": "Voir le texte"
-  }
+  },
+  "from": "à partir de :"
 }

+ 47 - 121
src/pages/Library.vue

@@ -1,101 +1,55 @@
 <template>
   <div class="library">
     <!-- BACKGROUND (mode) -->
-    <component :is="mode" @open="openText" />
-
-    <!-- FOREGROUND (texts) -->
-    <div v-if="parents.length" class="main-texts-container">
-      <section
-        v-for="(parent, i) in parents" :key="parent.id"
-        class="main-text-container text-break"
-        :style="`--n: ${i};`"
-        :class="{ 'text-blur': i !== parents.length - 1 }"
-      >
-        <text-card
-          :id="parent.id"
-          :children="parent.children"
-          class="text-card-main"
-          @open="openText" @close="closeText"
-          @click.native="onMainTextClick(i)"
-        />
-
-        <section class="sub-texts-container">
-          <text-card
-            v-for="(id, j) in parent.children" :key="id"
-            :id="id"
-            class="text-card-sub text-break"
-            :style="`--n: ${j};`"
-            @close="closeText(parent.id, $event)"
-            @click.native="onSubTextClick(i, j)"
-          />
-        </section>
-      </section>
-    </div>
+    <component :is="'library-' + mode" @open-node="openNode" />
+
+    <!-- FOREGROUND (nodes) -->
+    <node-book
+      v-if="nodebook.length"
+      :nodes="nodebook"
+      class="library-node-book no-events-container"
+      @open-node="openNode"
+      @close-node="closeNode"
+    />
   </div>
 </template>
 
 <script>
-import { CardList, CardMap, TreeMap } from './library'
-import TextCard from '@/components/text/TextCard'
+import { mapGetters } from 'vuex'
+
+import {
+  LibraryList,
+  LibraryMap,
+  LibraryTree
+} from './library'
+import { NodeBook } from '@/components/layouts'
 
 
 export default {
   name: 'Library',
 
   components: {
-    CardList,
-    CardMap,
-    TreeMap,
-    TextCard
+    LibraryList,
+    LibraryMap,
+    LibraryTree,
+    NodeBook
   },
 
   props: {
-    mode: { type: String, required: true },
-    texts: { type: Array, required: true }
+    query: { type: Object, required: true }
   },
 
   computed: {
-    parents () {
-      return this.texts.map(text => {
-        const [id, ...children] = text
-        return { id, children }
-      })
+    ...mapGetters(['mode', 'nodebook'])
+  },
+
+  watch: {
+    query (query) {
+      this.$store.dispatch('UPDATE_NODEBOOK', query)
     }
   },
 
   methods: {
-    openText (id, childId) {
-      const parent = this.parents.find(p => p.id === id)
-      if (parent && (childId === undefined || parent.children.includes(childId))) return
-
-      if (!parent) {
-        this.parents.push({ id, children: childId ? [childId] : [] })
-      } else if (childId) {
-        parent.children.push(childId)
-      }
-      this.updateQuery(this.parents)
-    },
-
-    closeText (id, childId) {
-      const parent = this.parents.find(p => p.id === id)
-      if (!childId) {
-        this.updateQuery(this.parents.filter(p => p !== parent))
-      } else {
-        parent.children = parent.children.filter(childId_ => childId_ !== childId)
-        this.updateQuery(this.parents)
-      }
-    },
-
-    updateQuery (parents) {
-      // Update the route's query (will not reload the page) and let vue determine what changed.
-      this.$router.push({
-        query: {
-          mode: this.mode,
-          texts: parents.map(parent => [parent.id, ...parent.children])
-        }
-      })
-    },
-
     onMainTextClick (mainTextIndex) {
       if (mainTextIndex === this.texts.length - 1) return
       this.parents.push(this.parents.splice(mainTextIndex, 1)[0])
@@ -118,7 +72,19 @@ export default {
       if (needUpdate) {
         this.updateQuery(this.parents)
       }
+    },
+
+    openNode (parentId, childId) {
+      this.$store.dispatch('OPEN_NODE', [parentId, childId])
+    },
+
+    closeNode (parentId, childId) {
+      this.$store.dispatch('CLOSE_NODE', [parentId, childId])
     }
+  },
+
+  created () {
+    this.$store.dispatch('UPDATE_NODEBOOK', this.query)
   }
 }
 </script>
@@ -126,53 +92,13 @@ export default {
 <style lang="scss" scoped>
 .library {
   height: 100%;
-  width: 100%;
-  overflow: hidden;
-}
-
-.main-texts-container {
-  position: relative;
-  top: -100%;
 
-  height: 100%;
-  pointer-events: none;
-}
-
-.main-text-container {
-  border-top: 2px solid $black;
-  display: flex;
-  width: 100%;
-
-  > * {
-    flex-basis: 50%;
+  &-node-book {
+    position: relative;
+    margin-bottom: -100%;
+    // compensate header border
+    height: calc(100% + 2px);
+    top: calc(-100% - 2px);
   }
 }
-
-.sub-texts-container {
-  position: relative;
-  height: 100%;
-}
-
-.text-card {
-  pointer-events: auto;
-  width: 100%;
-}
-
-.text-break {
-  position: absolute;
-  height: calc(100% - (var(--n) * #{$text-card-header-height}));
-  top: calc(var(--n) * #{$text-card-header-height});
-  z-index: var(--n);
-}
-
-.text-blur::before {
-  content: '';
-  pointer-events: none;
-  position: absolute;
-  z-index: 1;
-  display: block;
-  width: 100%;
-  height: 100%;
-  background-color: rgba($white, .5);
-}
 </style>

+ 19 - 20
src/pages/library/LibraryOptions.vue

@@ -6,14 +6,14 @@
     >
       <b-form-radio-group
         id="mode-select"
-        v-model="currentMode" :options="modes"
+        v-model="activeMode" :options="modes"
         name="mode-select" :aria-describedby="ariaDescribedby"
         buttons button-variant="outline-dark"
       />
     </b-form-group>
 
     <b-form-group
-      v-if="currentMode !== 'tree-map'"
+      v-if="mode !== 'tree'"
       :label="$t('options.filters.title')"
     >
       <b-form-group
@@ -22,8 +22,7 @@
       >
         <multiple-tags-select
           id="tags-select" :button-text="$t('options.filters.choices.tags')"
-          v-model="selectedTags" :options="tags"
-          :disabled="true"
+          v-model="selectedTags" :options="tagsOptions"
         />
       </b-form-group>
 
@@ -80,20 +79,13 @@ export default {
     MultipleTagsSelect
   },
 
-  props: {
-    show: { type: Boolean, required: true },
-    mode: { type: String, required: true },
-    tags: { type: Array, default: () => ([]) }
-  },
-
   data () {
     return {
       modes: [
-        { text: this.$t('options.display.choices.tree-map'), value: 'tree-map' },
-        { text: this.$t('options.display.choices.card-map'), value: 'card-map' },
-        { text: this.$t('options.display.choices.card-list'), value: 'card-list' }
+        { text: this.$t('options.display.choices.tree-map'), value: 'tree' },
+        { text: this.$t('options.display.choices.card-map'), value: 'map' },
+        { text: this.$t('options.display.choices.card-list'), value: 'list' }
       ],
-      currentMode: this.mode,
       selectedTags: [],
       strangeness: 0,
       textDepartId: undefined,
@@ -102,7 +94,11 @@ export default {
   },
 
   computed: {
-    ...mapGetters(['nodesDepartsOptions']),
+    ...mapGetters(['mode', 'nodebook', 'nodesDepartsOptions', 'tagsOptions']),
+
+    show () {
+      return this.nodebook.length === 0
+    },
 
     nodeDepartId: {
       get () {
@@ -111,12 +107,15 @@ export default {
       set (value) {
         this.$store.dispatch('SET_NODE_DEPART_ID', value)
       }
-    }
-  },
+    },
 
-  watch: {
-    currentMode (mode) {
-      this.$router.push({ query: { mode } })
+    activeMode: {
+      get () {
+        return this.mode
+      },
+      set (value) {
+        this.$store.dispatch('UPDATE_QUERY_MODE', value)
+      }
     }
   }
 }

+ 1 - 13
src/router/routes.js

@@ -28,19 +28,7 @@ export default [
       options: () => import(/* webpackChunkName: "library" */ '@/pages/library/LibraryOptions')
     },
     props: {
-      default: ({ query }) => {
-        let { mode = 'tree-map', texts = [] } = query
-        if (typeof texts === 'string') texts = [texts]
-        // In case of a reload or direct link, vue-router doesn't turn the query string into an array.
-        if (texts && texts.length && typeof texts[0] === 'string') {
-          texts = texts.map(text => text.split(',').map(id => parseInt(id)))
-        }
-        return { mode, texts }
-      },
-      options: ({ query }) => ({
-        show: !('texts' in query && query.texts.length),
-        mode: query.mode || 'tree-map'
-      })
+      default: ({ query }) => ({ query })
     }
   },
 

+ 83 - 2
src/store/modules/library.js

@@ -1,10 +1,12 @@
 import Vue from 'vue'
+import router from '@/router'
 
 import api from '@/api'
 import {
   AllTags
 } from '@/api/queries'
 import {
+  parseNodesQueryParams,
   getUniqueNodesIds,
   getRelatedNodesIds,
   buildTree
@@ -16,10 +18,38 @@ export default {
     tags: undefined,
     strangeness: [0, 1, 2, 3, 4],
     nodeDepartId: undefined,
-    trees: {}
+    trees: {},
+    mode: undefined,
+    nodebook: undefined
   },
 
   mutations: {
+    'SET_MODE' (state, mode) {
+      state.mode = mode
+    },
+
+    'SET_NODEBOOK' (state, nodes) {
+      state.nodebook = nodes
+    },
+
+    'ADD_NODEBOOK_NODE' (state, [stack, parentId, childId]) {
+      console.log('ADD_NODEBOOK_NODE', stack, parentId, childId)
+      if (stack === undefined) {
+        stack = childId === undefined ? [parentId] : [parentId, childId]
+        state.nodebook.push(stack)
+      } else {
+        stack.push(childId)
+      }
+    },
+
+    'REMOVE_NODEBOOK_NODE' (state, [stack, nodeId]) {
+      if (stack[0] === nodeId) {
+        state.nodebook.splice(state.nodebook.indexOf(stack), 1)
+      } else {
+        stack.splice(stack.indexOf(nodeId), 1)
+      }
+    },
+
     'SET_TAGS' (state, tags) {
       state.tags = tags
     },
@@ -34,9 +64,30 @@ export default {
   },
 
   actions: {
+    async 'UPDATE_NODEBOOK' ({ state, commit, dispatch }, query) {
+      const { mode = 'tree', nodebook } = parseNodesQueryParams(query)
+      commit('SET_MODE', mode)
+      await dispatch('GET_NODES', { ids: [].concat(...nodebook), dataLevel: 'full' })
+      commit('SET_NODEBOOK', nodebook)
+    },
+
+    'UPDATE_QUERY_NODES' ({ state }, from) {
+      const query = {
+        mode: state.mode,
+        nodes: [...state.nodebook.map(ids => ids.join(','))]
+      }
+      router.push({ query })
+    },
+
+    'UPDATE_QUERY_MODE' (store, mode) {
+      if (router.currentRoute.query.mode === mode) return
+      router.push({ query: { mode } })
+    },
+
     async 'INIT_LIBRARY' ({ state, commit, dispatch, rootState }) {
       const departIds = await dispatch('GET_ALL_NODES_IDS', 'depart')
       await dispatch('GET_NODES', { ids: departIds, dataLevel: 'initial' })
+      dispatch('GET_ALL_TAGS')
       return departIds
     },
 
@@ -78,12 +129,42 @@ export default {
       }
 
       return getters.nodeTree
+    },
+
+    async 'OPEN_NODE' ({ state, commit, dispatch }, [parentId, childId]) {
+      const stack = state.nodebook.find(stack => stack[0] === parentId)
+      if (stack && (childId === undefined || stack.includes(childId))) return
+      commit('ADD_NODEBOOK_NODE', [stack, parentId, childId])
+      dispatch('UPDATE_QUERY_NODES')
+    },
+
+    'CLOSE_NODE' ({ state, commit, dispatch }, [parentId, childId]) {
+      const stack = state.nodebook.find(stack => stack.includes(parentId) && (childId ? stack.includes(childId) : true))
+      commit('REMOVE_NODEBOOK_NODE', [stack, childId || parentId])
+      dispatch('UPDATE_QUERY_NODES')
     }
   },
 
   getters: {
+    mode: (state) => state.mode,
+
+    nodebook: (state, getters, rootState) => {
+      if (!state.nodebook) return []
+      return state.nodebook.map(([parentId, ...childrenIds]) => {
+        return {
+          parent: rootState.nodes[parentId],
+          children: childrenIds.map(id => rootState.nodes[id])
+        }
+      })
+    },
+
+    activeNodes: (state) => {
+      if (!state.nodebook) return []
+      return state.nodebook.reduce((acc, ids) => [...acc, ...ids], [])
+    },
+
     tagsOptions: state => {
-      if (state.tags === undefined) return
+      if (state.tags === undefined) return []
       return state.tags.map(tag => ({ value: tag.id, text: tag.name }))
     },
 

+ 11 - 0
src/store/utils.js

@@ -29,6 +29,17 @@ export const VARIANT_IDS = Object.fromEntries(
 )
 
 
+export function parseNodesQueryParams ({ mode, nodes = [] }) {
+  let nodebook = typeof nodes === 'string' ? [nodes] : nodes
+  nodebook = nodebook.map(ids => {
+    return typeof ids === 'string'
+      ? ids.split(',').map(id => parseInt(id))
+      : [...ids]
+  })
+  return { mode, nodebook }
+}
+
+
 export function getUniqueNodesIds (tree) {
   function extractId (ids, node) {
     ids.add(node.id)