Add browse dashboard, tags, visibility control, export, QR sharing, and embed mode
- Browse dashboard at / with search, framework filter, tag pills, and pagination - Tags system with autocomplete datalist and per-fiddle tag management - Listed/unlisted toggle for visibility control (unlisted still accessible via direct URL) - Export standalone HTML with inlined CSS/JS and framework CDN tags - QR code modal for sharing fiddle URLs - Embed mode at /embed/:id for minimal preview-only rendering - Extract shared loadScript() utility from 4 files into utils.js - Database schema: listed column, tags and fiddle_tags tables with index
This commit is contained in:
@@ -8,14 +8,17 @@ import { renderPreview } from './preview.js';
|
||||
import { initConsole, clearConsole } from './console-panel.js';
|
||||
import { compileCss } from './preprocessors.js';
|
||||
import { compileJs } from './js-preprocessors.js';
|
||||
import { createFiddle, loadFiddle, updateFiddle } from './api.js';
|
||||
import { createFiddle, loadFiddle, updateFiddle, listTags } from './api.js';
|
||||
import { getPref, setPref } from './preferences.js';
|
||||
import { initEmmet } from './emmet.js';
|
||||
import { initKeybindings } from './keybindings.js';
|
||||
import { initResizer, clearInlineSizes } from './resizer.js';
|
||||
import { exportHtml } from './export.js';
|
||||
import { showQrModal } from './qr.js';
|
||||
|
||||
let currentId = null;
|
||||
let debounceTimer = null;
|
||||
let currentTags = [];
|
||||
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
|
||||
@@ -89,12 +92,14 @@ async function save() {
|
||||
const title = $('#title-input').value || 'Untitled';
|
||||
const css_type = getCssType();
|
||||
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
|
||||
const listed = $('#listed-checkbox').checked ? 1 : 0;
|
||||
const tags = currentTags.slice();
|
||||
try {
|
||||
if (currentId) {
|
||||
await updateFiddle(currentId, { title, html, css, css_type, js, js_type });
|
||||
await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, tags });
|
||||
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
|
||||
} else {
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type });
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, tags });
|
||||
currentId = result.id;
|
||||
history.pushState(null, '', `/f/${currentId}`);
|
||||
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
|
||||
@@ -109,8 +114,10 @@ async function fork() {
|
||||
const title = ($('#title-input').value || 'Untitled') + ' (fork)';
|
||||
const css_type = getCssType();
|
||||
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
|
||||
const listed = $('#listed-checkbox').checked ? 1 : 0;
|
||||
const tags = currentTags.slice();
|
||||
try {
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type });
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, tags });
|
||||
currentId = result.id;
|
||||
$('#title-input').value = title;
|
||||
history.pushState(null, '', `/f/${currentId}`);
|
||||
@@ -136,6 +143,11 @@ async function loadFromUrl() {
|
||||
// Restore CSS type
|
||||
setCssType(fiddle.css_type || 'css');
|
||||
|
||||
// Restore listed/tags
|
||||
$('#listed-checkbox').checked = fiddle.listed !== 0;
|
||||
currentTags = (fiddle.tags || []).map(t => t.name);
|
||||
renderTags();
|
||||
|
||||
setEditorValues(fiddle);
|
||||
setTimeout(run, 100);
|
||||
} catch (e) {
|
||||
@@ -221,6 +233,43 @@ async function init() {
|
||||
}
|
||||
});
|
||||
|
||||
// Tags input
|
||||
const tagsInput = $('#tags-input');
|
||||
tagsInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
const val = tagsInput.value.trim().replace(/,/g, '');
|
||||
if (val && !currentTags.includes(val)) {
|
||||
currentTags.push(val);
|
||||
renderTags();
|
||||
}
|
||||
tagsInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Load tag suggestions
|
||||
loadTagSuggestions();
|
||||
|
||||
// Export & QR buttons
|
||||
$('#btn-export').addEventListener('click', async () => {
|
||||
const mode = getCurrentMode();
|
||||
const { html, css, js } = getEditorValues();
|
||||
const cssType = getCssType();
|
||||
const title = $('#title-input').value || 'Untitled';
|
||||
try {
|
||||
const compiledCss = await compileCss(css, cssType);
|
||||
const result = await compileJs(js, mode);
|
||||
exportHtml({ title, html, css: compiledCss, js: result.js, mode, extraCss: result.extraCss, isModule: result.isModule });
|
||||
} catch (e) {
|
||||
showToast(`Export failed: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn-qr').addEventListener('click', () => {
|
||||
const url = currentId ? `${location.origin}/f/${currentId}` : location.href;
|
||||
showQrModal(url);
|
||||
});
|
||||
|
||||
// Load fiddle from URL if present
|
||||
loadFromUrl();
|
||||
|
||||
@@ -231,4 +280,32 @@ async function init() {
|
||||
});
|
||||
}
|
||||
|
||||
function renderTags() {
|
||||
const container = $('#tags-display');
|
||||
container.innerHTML = '';
|
||||
for (const tag of currentTags) {
|
||||
const pill = document.createElement('span');
|
||||
pill.className = 'tag-pill';
|
||||
pill.innerHTML = `${tag}<button class="tag-remove">×</button>`;
|
||||
pill.querySelector('.tag-remove').addEventListener('click', () => {
|
||||
currentTags = currentTags.filter(t => t !== tag);
|
||||
renderTags();
|
||||
});
|
||||
container.appendChild(pill);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTagSuggestions() {
|
||||
try {
|
||||
const { tags } = await listTags();
|
||||
const datalist = $('#tags-datalist');
|
||||
datalist.innerHTML = '';
|
||||
for (const t of tags) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.name;
|
||||
datalist.appendChild(opt);
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user