Excerpts

by Kuligaposten 2025-02-16

Creating a Searchable, Paginated Excerpt Viewer & PDF Generator with JavaScript

Creating a Searchable, Paginated Excerpt Viewer & PDF Generator with JavaScript

A JSON-driven approach using JavaScript, Bootstrap, and jsPDF

Introduction

If you have a large collection of articles, documentation, or blog posts, you might want to:

✅ Display short excerpts for quick browsing
✅ Let users toggle between excerpt & full content
✅ Provide search functionality for easy filtering
✅ Implement pagination to navigate large datasets
✅ Allow users to generate PDFs of content

In this guide, we’ll build a fully interactive, JSON-driven UI that supports search, pagination, and PDF generation—all powered by plain JavaScript
Pagination dynamically updates the displayed excerpts while ensuring search results remain accessible. It prevents performance issues when handling large datasets.


Technologies Used

  • JavaScript (ES6) – Handles data rendering & interactivity
  • Bootstrap 5 – Provides styling & responsive layout
  • jsPDF – Generates PDFs dynamically
  • JSON Data – Stores and retrieves content dynamically

Step 1: JSON-Driven Data

Instead of hardcoding content in HTML, we use a structured JSON dataset:

window.data = [
  {
    category: 'JavaScript',
    title: 'Understanding Closures',
    content:
      'A closure is a function that remembers variables from its enclosing scope even after execution...',
  },
  {
    category: 'CSS',
    title: 'Flexbox vs Grid Layout',
    content:
      'Flexbox is a one-dimensional layout system, whereas Grid allows precise two-dimensional placement...',
  },
  {
    category: 'Security',
    title: 'Understanding CORS',
    content:
      'Cross-Origin Resource Sharing (CORS) allows restricted resources on a webpage to be accessed from another domain...',
  },
];

Structured data format for easy updates
Can be fetched from an API or a static JSON file


Step 2: Search & Pagination Setup

We implement client-side search & pagination to make navigating large datasets smooth.

🔹 The Pagination & Search Class

class PaginatedSearch {
  constructor(data, itemsPerPage = 6) {
    this.data = data;
    this.itemsPerPage = itemsPerPage;
    this.currentPage = 1;
    this.filteredData = data; // Stores search results
    this.searchInput = document.getElementById('searchInput');

    // Attach search event listener
    this.searchInput.addEventListener('input', this.searchData.bind(this));

    this.renderCarCards();
  }

  // 🔍 Search Functionality
  searchData() {
    const query = this.searchInput.value.toLowerCase();
    this.filteredData = this.data.filter(
      (item) =>
        item.title.toLowerCase().includes(query) ||
        item.content.toLowerCase().includes(query)
    );
    this.currentPage = 1; // Reset to page 1 after search
    this.renderCarCards();
  }

  // 📄 Render Cards with Pagination
  renderCarCards() {
    const excerptCards = document.getElementById('excerptCards');
    excerptCards.innerHTML = '';

    const startIndex = (this.currentPage - 1) * this.itemsPerPage;
    const paginatedItems = this.filteredData.slice(
      startIndex,
      startIndex + this.itemsPerPage
    );

    let htmlString = '';
    if (paginatedItems.length === 0) {
      htmlString = `<p class="text-center text-muted">No results found.</p>`;
    } else {
      paginatedItems.forEach((item, index) => {
        const dataIndex = startIndex + index;
        htmlString += `<div class="col mb-4 gy-0">
          <div class="d-flex flex-column bg-info-subtle p-4 rounded-4 shadow h-100">
            <span class="badge rounded-pill bg-primary mb-2">${item.category}</span>
            <h4>${item.title}</h4>
            <p class="toggle-text flex-grow-1 excerpt" data-index="${dataIndex}" data-state="half">
              ${item.content.slice(0, Math.floor(item.content.length / 2)).trim() + '...'}
            </p>
            <button class="btn btn-sm btn-outline-primary mt-2 generate-pdf" data-index="${dataIndex}">
              Generate PDF
            </button>
          </div>
        </div>`;
      });
    }
    excerptCards.innerHTML = htmlString.trim();
    this.renderPaginationControls();
  }

  renderPaginationControls() {
    const pagination = document.getElementById('pagination');
    pagination.innerHTML = '';
    const totalPages = Math.ceil(this.filteredData.length / this.itemsPerPage);
    if (totalPages <= 1) return;

    let paginationHTML = `<nav><ul class="pagination justify-content-center">`;
    for (let i = 1; i <= totalPages; i++) {
      paginationHTML += `<li class="page-item ${i === this.currentPage ? 'active' : ''}">
        <a class="page-link" href="#" data-page="${i}">${i}</a></li>`;
    }
    paginationHTML += `</ul></nav>`;
    pagination.innerHTML = paginationHTML;

    document.querySelectorAll('.page-link').forEach((button) => {
      button.addEventListener('click', (event) => {
        event.preventDefault();
        this.currentPage = parseInt(event.target.getAttribute('data-page'));
        this.renderCarCards();
      });
    });
  }
}

Dynamically updates the search results & pagination
Keeps everything fast with client-side filtering


Step 3: Toggling Full Content

function toggleContent(element, index) {
  const isHalfContent = element.getAttribute('data-state') !== 'full';

  element.style.opacity = 0;
  setTimeout(() => {
    element.textContent = isHalfContent
      ? window.data[index].content
      : window.data[index].content
          .slice(0, Math.floor(window.data[index].content.length / 2))
          .trim() + '...';

    element.setAttribute('data-state', isHalfContent ? 'full' : 'half');
    element.style.opacity = 1;
  }, 250);
}

Smooth content transition using opacity
Prevents text from being interpreted as HTML


Step 4: Generating PDFs

function generatePdf(index) {
  const { jsPDF } = window.jspdf;
  const doc = new jsPDF();

  const item = window.data[index];
  const title = item.title.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
  const content = item.content.replace(/&lt;/g, '<').replace(/&gt;/g, '>');

  doc.setProperties({
    title: title,
    subject: item.category,
    author: 'Kuligaposten',
  });

  doc.setFont('helvetica', 'bold');
  doc.setFontSize(16);
  doc.text(title, 10, 20);

  doc.setFont('helvetica', 'normal');
  doc.setFontSize(12);
  doc.text(content, 10, 30, { maxWidth: 180 });

  const pdfBlob = doc.output('blob');
  const pdfUrl = URL.createObjectURL(pdfBlob);
  window.open(pdfUrl, '_blank'); // Open in new tab
}

Opens PDF in a new tab to bypass Chrome sandbox restrictions
Formats title & content properly for readability


📌 How to use it

// Initialize the PaginatedSearch instance globally
let paginatedSearchInstance;

// Initial setup
document.addEventListener('DOMContentLoaded', () => {
  // Create the instance of PaginatedSearch
  paginatedSearchInstance = new PaginatedSearch(window.data);
});

The Final Result

JSON-driven content
Client-side search & pagination
Smooth excerpt toggling
Instant PDF generation

With this script, you can efficiently display, search, and manage excerpts while allowing users to generate PDFs. This approach keeps the UI clean and user-friendly, making it ideal for blogs, documentation sites, and educational resources.

This script is lightweight, efficient, and easy to integrate into any blog or documentation site!

Since Chrome blocks Blob URLs in sandboxed iframes, we use window.open() to ensure the PDF always loads properly.

👉 Click an excerpt to expand the full content, or generate a PDF for offline reading.

Column 1Column 2
Cell 1Cell 2
Cell 3Cell 4
Cell 1Cell 2
Cell 3Cell 4
Cell 1Cell 2
Cell 3Cell 4
Cell 1Cell 2
Cell 3Cell 4
Cell 1Cell 2
Cell 3Cell 4
Cell 1Cell 2
Cell 3Cell 4
Cell 1Cell 2
Cell 3Cell 4
Cell 1Cell 2
Cell 3Cell 4
Back to Home