TreeNav.vue 4.13 KB
Newer Older
1
<template lang="pug">
2 3 4 5 6
.treeview
  tree-level(
    :depth='0'
    :parent-id='null'
  )
7 8 9
</template>

<script setup>
10
import { useI18n } from 'vue-i18n'
11
import { computed, onMounted, provide, reactive, toRef } from 'vue'
12 13 14
import { findKey } from 'lodash-es'

import TreeLevel from './TreeLevel.vue'
15 16 17

// PROPS

18
const props = defineProps({
19
  nodes: {
20 21 22 23
    type: Object,
    default: () => ({})
  },
  roots: {
24 25
    type: Array,
    default: () => []
26 27 28 29
  },
  selected: {
    type: String,
    default: null
30 31 32 33
  },
  useLazyLoad: {
    type: Boolean,
    default: false
34 35 36 37 38 39 40 41
  },
  contextActionList: {
    type: Array,
    default: () => ['newFolder', 'duplicate', 'rename', 'move', 'del']
  },
  displayMode: {
    type: String,
    default: 'title'
42 43 44 45 46
  }
})

// EMITS

47
const emit = defineEmits(['update:selected', 'lazyLoad', 'contextAction'])
48

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
// I18N

const { t } = useI18n()

// Context Actions

const contextActions = {
  newFolder: {
    icon: 'las la-plus-circle',
    iconColor: 'blue',
    label: t('common.actions.newFolder')
  },
  duplicate: {
    icon: 'las la-copy',
    iconColor: 'teal',
    label: t('common.actions.duplicate') + '...'
  },
  rename: {
    icon: 'las la-redo',
    iconColor: 'teal',
    label: t('common.actions.rename') + '...'
  },
  move: {
    icon: 'las la-arrow-right',
    iconColor: 'teal',
    label: t('common.actions.moveTo') + '...'
  },
  del: {
    icon: 'las la-trash-alt',
    iconColor: 'negative',
    label: t('common.actions.delete'),
    labelColor: 'negative'
  }
}
provide('contextActionList', props.contextActionList.map(key => ({
  key,
  ...contextActions[key],
  handler: (nodeId) => {
    emit('contextAction', nodeId, key)
  }
})))

91 92 93
// DATA

const state = reactive({
94
  loaded: {},
95 96 97
  opened: {}
})

98
// COMPUTED
99 100 101 102 103 104 105 106 107 108 109 110

const selection = computed({
  get () {
    return props.selected
  },
  set (val) {
    emit('update:selected', val)
  }
})

// METHODS

111
function emitLazyLoad (nodeId, isCurrent, clb) {
112
  if (props.useLazyLoad) {
113
    emit('lazyLoad', nodeId, isCurrent, clb)
114 115 116 117 118
  } else {
    clb.done()
  }
}

119 120 121 122 123 124
function setOpened (nodeId) {
  state.opened[nodeId] = true
}
function isLoaded (nodeId) {
  return state.loaded[nodeId]
}
125 126 127 128 129
function setLoaded (nodeId, value) {
  state.loaded[nodeId] = value
}
function resetLoaded () {
  state.loaded = {}
130 131
}

132 133
// PROVIDE

134
provide('roots', toRef(props, 'roots'))
135
provide('nodes', props.nodes)
136
provide('loaded', state.loaded)
137
provide('opened', state.opened)
138
provide('displayMode', toRef(props, 'displayMode'))
139
provide('selection', selection)
140
provide('emitLazyLoad', emitLazyLoad)
141

142 143 144 145 146
// EXPOSE

defineExpose({
  setOpened,
  isLoaded,
147
  setLoaded,
148 149 150
  resetLoaded
})

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
// MOUNTED

onMounted(() => {
  if (props.selected) {
    let foundRoot = false
    let currentId = props.selected
    while (!foundRoot) {
      const parentId = findKey(props.nodes, n => n.children?.includes(currentId))
      if (parentId) {
        state.opened[parentId] = true
        currentId = parentId
      } else {
        foundRoot = true
      }
    }
    state.opened[props.selected] = true
  }
})
169 170

</script>
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193

<style lang="scss">
.treeview {
  &-level {
    list-style: none;
    padding-left: 19px;
  }

  > .treeview-level {
    padding-left: 0;

    > .treeview-node {
      border-left: none;

      > .treeview-label {
        border-radius: 5px;
      }
    }
  }

  &-node {
    display: block;
    border-left: 2px solid rgba(0,0,0,.05);
194 195 196 197 198 199 200

    @at-root .body--light & {
      border-left: 2px solid rgba(0,0,0,.05);
    }
    @at-root .body--dark & {
      border-left: 2px solid rgba(255,255,255,.1);
    }
201 202 203 204 205 206 207 208 209 210 211
  }

  &-label {
    padding: 4px 8px;
    border-radius: 0 5px 5px 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    transition: background-color .4s ease;

    &:hover, &:focus, &.active {
212 213 214 215 216 217
      @at-root .body--light & {
        background-color: rgba(0,0,0,.05);
      }
      @at-root .body--dark & {
        background-color: rgba(255,255,255,.1);
      }
218 219 220 221 222
    }

    > .q-icon {
      margin-right: 5px;
    }
223 224 225 226

    &-text {
      flex: 1 0;
    }
227 228 229 230 231 232 233 234 235 236 237 238 239 240
  }

  // Animations

  &-enter-active, &-leave-active {
    transition: all 0.2s ease;
  }

  &-enter-from, &-leave-to {
    transform: translateY(-10px);
    opacity: 0;
  }
}
</style>