/ Gists / 16 TOC
On gists

16 TOC

cdruc vue

Toc.vue Raw #

// https://tallpad.com/series/vuejs-misc/lessons/table-of-contents-component-using-vuejs

<script setup>
import {onMounted, ref, computed} from "vue";
import TocList from "./TocList.vue";
import {slugifyWithCounter} from "@sindresorhus/slugify";

const props = defineProps({
  contentSelector: {
    type: String,
    required: true,
  },
});

const slugify = slugifyWithCounter();

const headings = ref([]);

onMounted(() => {
  window.document
    .querySelector(props.contentSelector)
    .querySelectorAll("h1, h2, h3, h4, h5, h6")
    .forEach(el => {
      let id = slugify(el.innerText);
      el.setAttribute("id", id);

      headings.value.push({
        id: id,
        level: parseInt(el.tagName.charAt(1), 10),
        content: el.innerText,
        subheadings: [],
      });
    });
});

const groupedHeadings = computed(() => {
  let items = [...headings.value];

  for (let i = items.length - 1; i >= 0; i--) {
    let currentItem = items[i];

    let parentItem = items.findLast((item, index) => {
      return item.level < currentItem.level && index < i;
    });

    if (parentItem) {
      parentItem.subheadings.unshift(currentItem);
      items.splice(i, 1);
    }
  }

  return items;
});
</script>

<template>
  <div class="bg-slate-50 -mx-6 p-6" v-if="groupedHeadings.length">
    <h3
      class="border-b-2 border-slate-300 inline-block uppercase font-bold tracking-wide text-slate-800 mb-5"
    >
      Contents:
    </h3>

    <TocList :items="groupedHeadings" />
  </div>
</template>

TocList.vue Raw #

<script setup>
const props = defineProps({
  items: Array,
});
</script>

<template>
  <ul class="space-y-3">
    <li v-for="item in items">
      <a
        :href="`#${item.id}`"
        class="font-semibold group-[.subheadings]:font-normal hover:text-orange-500 hover:underline underline-offset-4"
      >
        {{ item.content }}
      </a>

      <TocList
        class="mt-3 ml-5 group subheadings"
        v-if="item.subheadings.length"
        :items="item.subheadings"
      />
    </li>
  </ul>
</template>