present.js
js
// Presentation mode script
// Used by both present.html and preview.html
// Requires: window.DECK_CONTENT (JSON string or object)
// Optional: window.DECK_ID, window.ESCAPE_URL (where Escape key navigates)
let currentSlide = 0;
let slides = [];
document.addEventListener('DOMContentLoaded', () => {
  if (!window.DECK_CONTENT) return;
  const content = typeof window.DECK_CONTENT === 'string'
    ? JSON.parse(window.DECK_CONTENT)
    : window.DECK_CONTENT;
  slides = content.slides || [];
  if (slides.length === 0) return;
  // Apply theme from deck
  if (content.theme) {
    const root = document.getElementById('html-root');
    if (root) root.setAttribute('data-theme', content.theme);
  }
  renderSlides();
  showSlide(0);
  setupKeyboardNav();
  if (typeof hljs !== 'undefined') {
    hljs.highlightAll();
  }
});
function renderSlides() {
  const container = document.getElementById('slides-container');
  container.innerHTML = slides.map((slide, i) => `
    <div class="slide bg-base-200" id="slide-${i}">
      ${slide.blocks.map(block => renderBlock(block)).join('')}
    </div>
  `).join('');
}
function renderBlock(block) {
  switch (block.type) {
    case 'heading':
      const sizes = { 1: 'text-6xl', 2: 'text-5xl', 3: 'text-4xl' };
      return `<h${block.level} class="${sizes[block.level] || sizes[1]} font-bold text-base-content mb-6">${escapeHtml(block.content)}</h${block.level}>`;
    case 'text':
      return `<p class="text-2xl text-base-content/80 mb-6 leading-relaxed">${escapeHtml(block.content)}</p>`;
    case 'code':
      return `<pre class="rounded-xl overflow-hidden mb-6 text-lg"><code class="language-${block.language || 'javascript'}">${escapeHtml(block.content)}</code></pre>`;
    case 'image':
      return block.url ? `
        <figure class="mb-6 flex flex-col items-center">
          <img src="${escapeHtml(block.url)}" alt="${escapeHtml(block.caption || '')}" class="max-h-[60vh] rounded-xl" />
          ${block.caption ? `<figcaption class="text-center text-lg text-base-content/50 mt-4">${escapeHtml(block.caption)}</figcaption>` : ''}
        </figure>
      ` : '';
    case 'list':
      const tag = block.style === 'number' ? 'ol' : 'ul';
      return `<${tag} class="list-${block.style === 'number' ? 'decimal' : 'disc'} list-inside text-2xl text-base-content/80 mb-6 space-y-2">
        ${(block.items || []).map(item => `<li>${escapeHtml(item)}</li>`).join('')}
      </${tag}>`;
    case 'quote':
      return `
        <blockquote class="border-l-4 border-primary pl-6 py-4 mb-6">
          <p class="text-3xl italic text-base-content/80">${escapeHtml(block.content)}</p>
          ${block.author ? `<cite class="text-xl text-base-content/50 block mt-4">— ${escapeHtml(block.author)}</cite>` : ''}
        </blockquote>
      `;
    case 'divider':
      return `<hr class="border-base-content/10 my-8" />`;
    default:
      return '';
  }
}
function escapeHtml(text) {
  if (!text) return '';
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}
function showSlide(index) {
  if (index < 0 || index >= slides.length) return;
  // Update slide classes
  slides.forEach((_, i) => {
    const el = document.getElementById(`slide-${i}`);
    el.classList.remove('active', 'prev');
    if (i < index) el.classList.add('prev');
    if (i === index) el.classList.add('active');
  });
  currentSlide = index;
  // Update counter
  document.getElementById('slide-counter').textContent = `${index + 1} / ${slides.length}`;
  // Update progress bar
  const progress = ((index + 1) / slides.length) * 100;
  document.getElementById('progress-bar').style.width = `${progress}%`;
}
function nextSlide() {
  if (currentSlide < slides.length - 1) {
    showSlide(currentSlide + 1);
  }
}
function prevSlide() {
  if (currentSlide > 0) {
    showSlide(currentSlide - 1);
  }
}
function setupKeyboardNav() {
  document.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'ArrowRight':
      case ' ':
      case 'Enter':
        e.preventDefault();
        nextSlide();
        break;
      case 'ArrowLeft':
      case 'Backspace':
        e.preventDefault();
        prevSlide();
        break;
      case 'Escape':
        // Use custom escape URL if provided, otherwise default to deck view
        if (window.ESCAPE_URL) {
          window.location.href = window.ESCAPE_URL;
        } else if (window.DECK_ID) {
          window.location.href = `/deck/${window.DECK_ID}`;
        }
        break;
      case 'Home':
        showSlide(0);
        break;
      case 'End':
        showSlide(slides.length - 1);
        break;
    }
  });
  // Touch/click navigation
  document.addEventListener('click', (e) => {
    if (e.target.closest('.exit-btn')) return;
    if (e.clientX > window.innerWidth / 2) {
      nextSlide();
    } else {
      prevSlide();
    }
  });
}
72f0edf

Add rich text formatting and block styling to editor

Connor McCutcheon
@connor
0 stars

Pitch decks to help promote Skyscape Apps

Sign in to comment Sign In