Files
fiddle/public/js/presentation.js
root ae8dbafb20 Add Python REPL, instant deploy, Gist import, presentation mode, and CSS visual tools
- Python mode via Pyodide WASM runtime with stdout/stderr console integration
- Publish fiddles to clean /p/:slug URLs as standalone HTML pages
- Import code from GitHub Gist URLs with auto-detection of language/mode
- Presentation mode with slide management, fullscreen viewer, and keyboard nav
- Enable Monaco color decorators for inline CSS color pickers
- Extract reusable generateStandaloneHtml from export module
2026-02-27 15:50:55 -06:00

218 lines
6.4 KiB
JavaScript

import { listSlides, createSlide, deleteSlide } from './api.js';
import { getEditorValues } from './editors.js';
import { renderPreview } from './preview.js';
import { compileCss } from './preprocessors.js';
import { compileJs } from './js-preprocessors.js';
import { getCurrentMode, getCssType } from './editors.js';
import { getPref } from './preferences.js';
let slides = [];
let currentSlideIndex = 0;
let isPresenting = false;
const $ = (sel) => document.querySelector(sel);
function showToast(msg) {
const toast = $('#share-toast');
if (!toast) return;
toast.textContent = msg;
toast.classList.remove('hidden');
setTimeout(() => toast.classList.add('hidden'), 3000);
}
export async function openSlideManager(fiddleId) {
if (!fiddleId) {
showToast('Save the fiddle first to use presentation mode');
return;
}
const modal = $('#presentation-modal');
modal.classList.remove('hidden');
try {
const result = await listSlides(fiddleId);
slides = result.slides || [];
renderSlideList(fiddleId);
} catch (e) {
showToast('Failed to load slides: ' + e.message);
}
}
function renderSlideList(fiddleId) {
const list = $('#slide-list');
list.innerHTML = '';
if (!slides.length) {
list.innerHTML = '<div style="padding:16px;color:var(--text-dim);text-align:center">No slides yet. Add the current editor state as a slide.</div>';
} else {
slides.forEach((slide, i) => {
const item = document.createElement('div');
item.className = 'slide-item';
item.innerHTML = `
<span class="slide-number">${i + 1}</span>
<span class="slide-preview-text">${escHtml(slide.notes || slide.js.slice(0, 60) || slide.html.slice(0, 60) || '(empty)')}</span>
<button class="slide-delete-btn btn-small" data-id="${slide.id}">&times;</button>
`;
list.appendChild(item);
});
list.querySelectorAll('.slide-delete-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
try {
await deleteSlide(btn.dataset.id);
slides = slides.filter(s => s.id !== btn.dataset.id);
renderSlideList(fiddleId);
} catch (err) {
showToast('Delete failed: ' + err.message);
}
});
});
}
// Update slide count
const countEl = $('#slide-count');
if (countEl) countEl.textContent = `${slides.length} slide${slides.length !== 1 ? 's' : ''}`;
}
export async function addCurrentSlide(fiddleId) {
if (!fiddleId) return;
const { html, css, js } = getEditorValues();
const notes = prompt('Slide notes (optional):') || '';
try {
const slide = await createSlide(fiddleId, { html, css, js, notes });
slides.push(slide);
renderSlideList(fiddleId);
showToast(`Slide ${slides.length} added`);
} catch (e) {
showToast('Failed to add slide: ' + e.message);
}
}
export async function startPresentation(fiddleId) {
if (!fiddleId) {
showToast('Save the fiddle first');
return;
}
try {
const result = await listSlides(fiddleId);
slides = result.slides || [];
} catch (e) {
showToast('Failed to load slides: ' + e.message);
return;
}
if (!slides.length) {
showToast('Add at least one slide first');
return;
}
isPresenting = true;
currentSlideIndex = 0;
const overlay = $('#presentation-overlay');
overlay.classList.remove('hidden');
// Close the manager modal if open
$('#presentation-modal').classList.add('hidden');
renderCurrentSlide();
document.addEventListener('keydown', presentationKeyHandler);
}
export function stopPresentation() {
isPresenting = false;
const overlay = $('#presentation-overlay');
overlay.classList.add('hidden');
document.removeEventListener('keydown', presentationKeyHandler);
}
function presentationKeyHandler(e) {
if (!isPresenting) return;
if (e.key === 'Escape') {
e.preventDefault();
stopPresentation();
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') {
e.preventDefault();
if (currentSlideIndex < slides.length - 1) {
currentSlideIndex++;
renderCurrentSlide();
}
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
if (currentSlideIndex > 0) {
currentSlideIndex--;
renderCurrentSlide();
}
}
}
async function renderCurrentSlide() {
const slide = slides[currentSlideIndex];
if (!slide) return;
const counter = $('#pres-counter');
counter.textContent = `${currentSlideIndex + 1} / ${slides.length}`;
const notes = $('#pres-notes');
notes.textContent = slide.notes || '';
// Render the slide's code into the presentation iframe
const frame = $('#pres-preview-frame');
const mode = getCurrentMode();
const cssType = getCssType();
try {
const compiledCss = await compileCss(slide.css, cssType);
const result = await compileJs(slide.js, mode);
const { getFrameworkRuntime } = await import('./js-preprocessors.js');
const runtime = getFrameworkRuntime(mode);
const allCss = result.extraCss ? `${compiledCss}\n${result.extraCss}` : compiledCss;
const finalHtml = result.renderedHtml || slide.html;
const finalJs = result.renderedHtml ? '' : result.js;
let bodyContent;
if (mode === 'vue' || mode === 'svelte') {
bodyContent = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
} else if (runtime.bodyHtml) {
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
} else {
bodyContent = finalHtml;
}
const isModule = result.isModule || mode === 'svelte';
let scripts = '';
if (finalJs) {
if (isModule) {
scripts = `<script type="module">\n${finalJs}\n<\/script>`;
} else {
for (const url of runtime.scripts) {
scripts += `<script src="${url}"><\/script>\n`;
}
scripts += `<script>\n${finalJs}\n<\/script>`;
}
}
const previewTheme = getPref('previewTheme');
const darkCss = previewTheme === 'dark'
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
: '';
const doc = `<!DOCTYPE html>
<html><head><meta charset="UTF-8">${darkCss}<style>${allCss}</style></head>
<body>${bodyContent}${scripts}</body></html>`;
frame.srcdoc = doc;
} catch (e) {
frame.srcdoc = `<pre style="color:red;padding:20px">Error: ${escHtml(e.message)}</pre>`;
}
}
function escHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}