Scroll to top
Hướng dẫn làm TOC (Table of Contents) bằng JS - Tạo menu bài viết đơn giản và thuận tiện

Hướng dẫn làm TOC (Table of Contents) bằng JS - Tạo menu bài viết đơn giản và thuận tiện

ByYuto 14/10/2024 06:36
15min read

Bài viết này sẽ hướng dẫn cách tạo Table of Contents (TOC) đơn giản và thuận tiện bằng cách sử dụng JavaScript. Giúp cho người dùng dễ dàng tìm kiếm và truy cập các nội dung trong bài viết.

Để tạo ra một trang web chuyên nghiệp và thu hút người dùng, việc tạo một Table of Contents (TOC) là rất quan trọng. TOC không chỉ giúp cho người dùng dễ dàng tìm kiếm và truy cập các nội dung trong bài viết của bạn, mà còn giúp cho trang web của bạn trở nên chuyên nghiệp hơn.

Trong bài viết này, mình sẽ hướng dẫn bạn cách sử dụng JavaScript để tạo một TOC tự động từ các tiêu đề trong một bài viết. Điều này sẽ giúp cho người dùng dễ dàng tìm kiếm và truy cập các nội dung trong bài viết một cách nhanh chóng và thuận tiện.

Giới thiệu về TOC và vai trò của nó trong trang web

TOC (Table of Contents) là một phần quan trọng của trang web, đặc biệt là đối với các bài viết dài. TOC giúp người dùng dễ dàng tìm kiếm và truy cập các nội dung trong bài viết một cách nhanh chóng và thuận tiện.

TOC thường được đặt ở đầu bài viết hoặc bên cạnh nội dung chính. Nó bao gồm các liên kết đến các phần khác nhau của bài viết và giúp người dùng di chuyển đến các phần mà họ quan tâm. TOC giúp người dùng tiết kiệm thời gian và không bỏ lỡ bất kỳ thông tin quan trọng nào.

Table of Contents (TOC) là gì

Các bước để tạo TOC tự động từ các tiêu đề trong trang web bằng JavaScript

Tạo TOC trong HTML

Đầu tiên chúng ta tạo thẻ div rỗng với ID là toc, được bọc bởi thẻ div với class là toc-wrap. Khổi được bọc bởi toc-wrap này chính là nơi các mục trong Table of Contents xuất hiện.

html
Copy
1
2
3
4
5
6
7
8
<div class="toc-wrap">
    <div class="toc-title">Table of Contents</div>
    <div id="toc"></div>
</div>

<article>
  <!-- Đây là bên trong nội dung bài viết cần tạo TOC -->
</article>

Tạo TOC tự động từ các tiêu đề trong bài viết

Đây là một đoạn code JavaScript được sử dụng để tạo mục lục cho bài viết dựa trên các tiêu đề được định dạng đúng trong thẻ article.

js
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
// Lấy tất cả các heading trong bài viết
const headings = document.querySelectorAll('article h1, article h2, article h3, article h4, article h5, article h6')
if (headings.length === 0) return

// Khai bào nơi mà TOC sẽ được chèn vào
const tocContainer = document.querySelector('#toc')

// Xác định cấp độ bắt đầu của TOC (bởi vì không phải bài viết nào cũng có thẻ H1, hoặc H2)
const startingLevel = headings[0].tagName[1]

// Tạo TOC rỗng
const toc = document.createElement('ul')

// Theo dõi các cấp độ heading trước đó
const prevLevels = [0, 0, 0, 0, 0, 0]

// Lặp qua từng heading và thêm chúng vào TOC
for (let i = 0; i < headings.length; i++) {
  const heading = headings[i]
  const level = parseInt(heading.tagName[1])

  // Tăng các cấp độ trước đó lên đến cấp độ hiện tại
  prevLevels[level - 1]++
  for (let j = level; j < prevLevels.length; j++) {
    prevLevels[j] = 0
  }

  // Tạo số mục cho mục đó dựa trên các cấp độ trước đó
  // và loại bỏ số 0 nếu trường hợp h1 -> h3 (không có h2)
  // Sẽ tạo ra các đề mục ví dụ như:
  // 1. Heading h1a
  //     1.1 Heading h2
  // 2. Heading h1b
  //          2.1 Heading h3 (đẹp hơn 2.0.1 Heading h3)
  const sectionNumber = prevLevels.slice(startingLevel - 1, level).join('.').replace(/\.0/g, "")

  // Tạo ID mới và gán vào heading
  // Phải làm phần này để click vào mục lục có thể di chuyển đến được.
  const newHeadingId = `${heading.textContent.toLowerCase().replace(/ /g, '-')}`
  heading.id = newHeadingId

  // Tạo liên kết mục cho heading
  const anchor = document.createElement('a')
  anchor.setAttribute('href', `#${newHeadingId}`)
  anchor.textContent = heading.textContent

  // Thêm event listener để cuộn đến liên kết khi nhấp chuột
  anchor.addEventListener('click', (event) => {
    event.preventDefault()
    const targetId = event.target.getAttribute('href').slice(1)
    const targetElement = document.getElementById(targetId)
    targetElement.scrollIntoView({ behavior: 'smooth' })
    // Thêm anchor vào URL khi click
    history.pushState(null, null, `#${targetId}`)
  })

  // Tạo thẻ <li> để thêm vào TOC
  const listItem = document.createElement('li')
  listItem.textContent = sectionNumber
  listItem.appendChild(anchor)

  // Thêm CSS class cho từng mục lục
  // Ví dụ "toc-item toc-h1", "toc-item toc-h2"
  const className = `toc-${heading.tagName.toLowerCase()}`
  listItem.classList.add('toc-item')
  listItem.classList.add(className)

  // Bỏ thẻ <li> vừa tạo vào TOC
  toc.appendChild(listItem)
}

// Thêm các TOC item vào toc contaner
tocContainer.innerHTML = ''  
tocContainer.appendChild(toc)

Tiếp theo là một vài đọc CSS để TOC dễ nhìn và đẹp hơn.

scss
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.toc-wrap {
  // Hiện thanh scroll dọc nếu TOC quá dài
  ::-webkit-scrollbar {
    -webkit-appearance: none;
    width: 7px;
  }
  ::-webkit-scrollbar-thumb {
    border-radius: 5px;
    background-color: #cccccc;
    -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5);
  }

  border-radius: 0.375rem;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  background-color: #f9fafb;
  border-width: 1px;
  border-color: #d2d6dc;

  .toc-title {
    text-align: center;
    font-size: 2rem;
    margin-top: 1rem;  
  }

  #toc {
    max-height: calc(100vh - 150px);
    padding: 1rem;
    overflow-y: scroll;    
    a {
      &:hover {
        text-decoration: underline;
      }
    }
    .toc-active {
      font-weight: bold;
      color: #2563eb;    
    }
    .toc-item {
      padding: 0.1em 0;
      a {
        padding: 0.25em 0.5em;
      }
    }
    .toc-h2 { margin-left: 0.5em }
    .toc-h3 { margin-left: 1.75em }
    .toc-h4 { margin-left: 3em }
    .toc-h5 { margin-left: 4.25em }
    .toc-h6 { margin-left: 5.5em }
  }
}

Như vậy là cơ bản đã xong. Khi bạn vào bài viết thì TOC sẽ được tự động tạo ra giống như ảnh dưới đây, khi click vào thì sẽ scroll nhẹ nhàng đến nội dung tương ứng.

huong-dan-tao-toc-javascript

Thay đổi màu sắc của TOC item khi scroll trang web để giúp người dùng dễ dàng nhận biết vị trí của mình trên trang web

Một trong những tính năng đặc biệt và hữu ích của TOC là giúp người dùng dễ dàng nhận biết vị trí của mình đang đọc trên bài viết. Bằng cách thay đổi màu sắc của các TOC item khi người dùng cuộn trang web, bạn giúp cho người dùng có thể nhận biết được vị trí đang đọc và giúp tăng trải nghiệm người dùng. Điều này cũng giúp cho trang web của bạn trở nên chuyên nghiệp và thu hút hơn người dùng.

js
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  // Thêm event listener cho window object để lắng nghe sự kiện scroll
  window.addEventListener('scroll', function() {
    let scroll = window.scrollY // Lấy giá trị scrollY của màn hình
    let height = window.innerHeight //Lấy chiều cao của màn hình
    let offset = 200

    headings.forEach(function (heading, index) {
      let i = index + 1
      let target = document.querySelector('#toc li:nth-of-type(' + i + ') > a') // Lấy phần tử target dựa trên số thứ tự
      let pos = heading.getBoundingClientRect().top + scroll // Lấy vị trí của heading
      if (!target) return

      // Nếu scroll lớn hơn vị trí của phần tử hiện tại trừ đi chiều cao màn hình cộng với offset
      if (scroll > pos - height + offset) { // Nếu cuộn quá vị trí của heading
        // Nếu phần tử tiếp theo tồn tại (không phải là phần tử cuối cùng)
        if (headings[index + 1] !== undefined) { 
          // Lấy vị trí của phần tử tiếp theo
          let next_pos = headings[index + 1].getBoundingClientRect().top + scroll
          // Nếu scroll vượt qua vị trí của phần tử tiếp theo
          if (scroll > next_pos - height + offset) {
            target.classList.remove('toc-active')
          } else if (i === 1 && tocContainer.classList.contains('active') === false) { // Phần tử đầu tiên
            target.classList.add('toc-active')
            tocContainer.classList.add('active')
          } else { // Nếu không có phần tử tiếp theo trong danh sách heading
            target.classList.add('toc-active')
          }
        } else { //Nếu là heading cuối cùng
          target.classList.add('toc-active')
        }
      } else { // Nếu scroll không vượt qua heading
        target.classList.remove('toc-active')
        if (i === 1 && tocContainer.classList.contains('active')) { // Nếu chưa cuộn đến heading đầu tiên
          tocContainer.classList.remove('active')
        }
      }
    })
  })

Đoạn code JavaScript trên tuy khá khó hiểu, nhưng sau khi thêm vào thì khi đọc bài viết, chúng ta cuộn đến phần tiêu đề nào thì TOC item sẽ đổi màu theo CSS đã định nghĩa trước đó, giúp người đọc dễ nhận biết được đang ở phần nào của bài viết.

huong-dan-toc-javascript

Khi tải lại trang thì tự động cuộn đến anchor trên URL

Khi người dùng chia sẻ một đường dẫn đến bài viết, có thể họ sẽ bao gồm một anchor trong URL để truy cập trực tiếp đến một phần cụ thể của trang web. Tuy nhiên, khi người dùng refresh trang, trang web sẽ tự động quay lại vị trí đầu tiên của trang web, không phải vị trí của anchor được chèn vào URL. Điều này có thể gây khó chịu và mất thời gian cho người dùng.

Ví dụ một đường URL bài viết như sau:
https://yutojp.com/articles/huong-dan-dung-ai-ve-tranh-bang-midjourney-cuc-nhanh#hướng-dẫn-vẽ-tranh-bằng-ai-với-midjourney
Trong URL trên, phần #hướng-dẫn-vẽ-tranh-bằng-ai-với-midjourney được gọi là anchor.

Để giải quyết vấn đề này, bạn có thể thêm đoạn code JavaScript sau để khi người dùng refresh trang, trang web sẽ tự động scroll đến vị trí của anchor được chèn vào URL.

js
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
  // Kiểm tra xem URL có chứa anchor hay không
  if (window.location.hash) {
    // Decode hash để lấy ID của anchor
    const anchorId = decodeURIComponent(window.location.hash.slice(1));

    // Lấy phần tử có ID tương ứng với anchor
    const anchorElement = document.getElementById(anchorId);

    // Nếu phần tử tồn tại, cuộn mượt đến phần tử đó 
    if (anchorElement) {
      anchorElement.scrollIntoView({behavior: 'smooth'});
    }
  }

Kết luận

Trong bài viết này, mình đã hướng dẫn cách tạo TOC tự động và thay đổi màu sắc của TOC item khi scroll trang web bằng JavaScript. Bằng cách sử dụng TOC, người dùng có thể dễ dàng truy cập các nội dung trong trang web một cách nhanh chóng và thuận tiện hơn.

Tùy chỉnh TOC cũng giúp cho trang web của bạn trở nên chuyên nghiệp hơn, hy vọng bài viết này sẽ giúp bạn tạo ra một trang web thu hút và tiện lợi hơn cho người dùng.

Đánh giá bài viết: 3.9/5 (22 đánh giá)
Bạn chưa đánh giá

Bình luận

Author
hoclaptrinh.io author
Tác giả:Yuto Yasunaga

Mình là một full stack developer, tốt nghiệp và làm việc tại Nhật Bản. Trang web này là nơi mình tổng hợp, đúc kết và lưu trữ lại những kiến thức trong quá trình học và làm việc liên quan đến IT.
Hy vọng những bài viết ở website này sẽ có ích cho bạn.