Skip to content
Snippets Groups Projects
Tree.vue 8.44 KiB
Newer Older
<script setup lang="ts">
import { computed, ref } from 'vue';

interface Props {
  items?: {
    text: string;
    link?: string;
    color?: string;
    children?: {
      text: string;
      link?: string;
      color?: string;
      children?: {
        text: string;
        link?: string;
        color?: string;
      }[];
    }[];
  }[];
  maxWidth?: number;
  expand?: boolean;
  theme?:
    | 'white'
    | 'slate'
    | 'blue'
    | 'sky'
    | 'teal'
    | 'lime'
    | 'green'
    | 'yellow'
    | 'orange'
    | 'pink'
    | 'fuchsia'
    | 'purple'
    | 'indigo'
    | 'rose'
    | 'red'
    | 'black';
}
const props = defineProps<Props>();
const items = computed(() => props.items);
const colorTheme = computed(() => {
  if (!props?.theme) return '#ffffff';
  switch (props?.theme) {
    case 'white':
      return '#ffffff';
    case 'slate':
      return '#64748b';
    case 'blue':
      return '#3b82f6';
    case 'sky':
      return '#0ea5e9';
    case 'teal':
      return '#14b8a6';
    case 'lime':
      return '#84cc16';
    case 'green':
      return '#22c55e';
    case 'yellow':
      return '#eab308';
    case 'orange':
      return '#f97316';
    case 'pink':
      return '#ec4899';
    case 'fuchsia':
      return '#d946ef';
    case 'purple':
      return '#a855f7';
    case 'indigo':
      return '#6366f1';
    case 'rose':
      return '#f43f5e';
    case 'red':
      return '#ef4444';
    case 'black':
      return '#000000';
  }
  return '#ffffff';
});
const textColor = computed(() => {
  if (!props.theme) return '#000000';
  if (props.theme === 'white') return '#000000';
  return '#ffffff';
});

const state = ref([]);
const setInitialState = () => {
  if (!props?.items) return;
  for (let item of props.items) {
    state.value.push({
      isOpen: props?.expand ?? false,
      text: item.text
    });
    if (item.children) {
      for (let child of item.children) {
        state.value.push({
          isOpen: props?.expand ?? false,
          text: child.text
        });
        console.log('child', child);
        if (child.children) {
          for (let grandChild of child.children) {
            state.value.push({
              isOpen: props?.expand ?? false,
              text: grandChild.text
            });
          }
        }
      }
    }
  }
};
watch([items], () => {
  if (items.value) setInitialState();
});
const toggleIsOpen = (item) =>
  state.value.map((itemState) => {
    if (itemState.text === item.text) itemState.isOpen = !itemState.isOpen;
  });
</script>

<template>
  <ul
    :style="`background-color: ${colorTheme ?? 'white'}; max-width: ${maxWidth ?? 300}px`"
    class="tree"
  >
    <li
      v-for="item of items"
      :key="item.text"
      :class="[
        'item flex',
        {
          openItem: state.find((itemState) => itemState.text === item.text && itemState.isOpen)
        }
      ]"
    >
      <svg
        v-if="item.children"
        :class="[
          'openButton',
          {
            openButtonOpened: state.find(
              (itemState) => itemState.text === item.text && itemState.isOpen
            )
          }
        ]"
        width="25px"
        height="25px"
        viewBox="0 -0.5 28 28"
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
        xmlns:xlink="http://www.w3.org/1999/xlink"
        xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
        @click.prevent="toggleIsOpen(item)"
      >
        <g
          id="Page-1"
          stroke="none"
          stroke-width="1"
          fill="none"
          fill-rule="evenodd"
          sketch:type="MSPage"
        >
          <g
            id="Icon-Set-Filled"
            sketch:type="MSLayerGroup"
            transform="translate(-156.000000, -623.000000)"
            :fill="textColor ?? '#000000'"
          >
            <path
              id="open"
              d="M183,647.998 L157,647.998 C156.448,647.998 156,648.446 156,648.999 C156,649.552 156.448,650 157,650 L183,650 C183.552,650 184,649.552 184,648.999 C184,648.446 183.552,647.998 183,647.998 L183,647.998 Z M158.014,645.995 L182.018,645.995 C184.375,645.995 184.296,644.608 183.628,643.574 L171.44,624.555 C170.882,623.771 169.22,623.703 168.56,624.555 L156.372,643.574 C155.768,644.703 155.687,645.995 158.014,645.995 L158.014,645.995 Z"
              sketch:type="MSShapeGroup"
            ></path>
          </g>
        </g>
      </svg>
      <div
        :class="[
          'content',
          {
            openContent: state.find((itemState) => itemState.text === item.text && itemState.isOpen)
          }
        ]"
      >
        <a :href="item.link" class="text">{{ item.text }}</a>
        <div class="children">
          <div v-for="child of item.children" :key="child.text" class="flex item">
            <svg
              v-if="child.children"
              :class="[
                'openButton',
                {
                  openButtonOpened: state.find(
                    (itemState) => itemState.text === child.text && itemState.isOpen
                  )
                }
              ]"
              width="25px"
              height="25px"
              viewBox="0 -0.5 28 28"
              version="1.1"
              xmlns="http://www.w3.org/2000/svg"
              xmlns:xlink="http://www.w3.org/1999/xlink"
              xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
              @click.prevent="toggleIsOpen(child)"
            >
              <g
                id="Page-1"
                stroke="none"
                stroke-width="1"
                fill="none"
                fill-rule="evenodd"
                sketch:type="MSPage"
              >
                <g
                  id="Icon-Set-Filled"
                  sketch:type="MSLayerGroup"
                  transform="translate(-156.000000, -623.000000)"
                  :fill="textColor ?? '#000000'"
                >
                  <path
                    id="open"
                    d="M183,647.998 L157,647.998 C156.448,647.998 156,648.446 156,648.999 C156,649.552 156.448,650 157,650 L183,650 C183.552,650 184,649.552 184,648.999 C184,648.446 183.552,647.998 183,647.998 L183,647.998 Z M158.014,645.995 L182.018,645.995 C184.375,645.995 184.296,644.608 183.628,643.574 L171.44,624.555 C170.882,623.771 169.22,623.703 168.56,624.555 L156.372,643.574 C155.768,644.703 155.687,645.995 158.014,645.995 L158.014,645.995 Z"
                    sketch:type="MSShapeGroup"
                  ></path>
                </g>
              </g>
            </svg>
            <div
              :class="[
                'content',
                {
                  openContent: state.find(
                    (itemState) => itemState.text === child.text && itemState.isOpen
                  )
                }
              ]"
            >
              <a :href="child.link" class="text">{{ child.text }}</a>
              <div class="children">
                <div v-for="grandChild of child.children" :key="grandChild.text" class="flex item">
                  <div
                    :class="[
                      'content',
                      {
                        openContent: state.find(
                          (itemState) => itemState.text === grandChild.text && itemState.isOpen
                        )
                      }
                    ]"
                  >
                    <p :href="grandChild.link" class="text">{{ grandChild.text }}</p>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </li>
  </ul>
</template>

<style scoped>
.tree {
  padding: 15px 25px 15px 15px;
}
.item {
  position: relative;
}
.content {
  width: 100%;
  padding-left: 25px;
  position: relative;
  overflow: hidden;
  transition: all 0.3s ease;
  background-color: v-bind(colorTheme);
}
.text {
  display: block;
  position: relative;
  padding: 4px 0 4px 5px;
  z-index: 3;
  color: v-bind(textColor);
  background-color: v-bind(colorTheme);
  word-break: break-word;
}
.openButton {
  position: absolute;
  z-index: 3;
  top: 5px;
  left: 0;
  cursor: pointer;
  padding: 5px;
  margin: -5px -5px -5px 0;
  transition: transform 0.3s ease;
}
.openButtonOpened {
  transform: rotate(180deg);
}
.children {
  width: 100%;
  padding-left: 10px;
  opacity: 0;
  max-height: 0;
  transform: translateY(-100%);
  transition: all 0.3s ease;
}
.openContent > .children {
  transform: translateY(0);
  opacity: 1;
  max-height: 1000px;
}
.flex {
  display: flex;
  align-items: start;
  justify-content: end;
}
</style>