- 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
218 lines
6.4 KiB
JavaScript
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}">×</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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|