Ohjeet: screenshot-upload paste & drag-drop + kuva-lightbox

Screenshottien lisääminen ohjeisiin nyt helpompaa:
- Ctrl+V / Cmd+V: liitä kuvakaappaus suoraan leikepöydältä editoriin
- Drag & drop: raahaa kuvatiedostoja suoraan textarea-editoriin
- Upload-placeholder näkyy latauksen aikana (![Ladataan: ...]())
- Vihjeet editorin alla kertovat käytettävissä olevat tavat
- Kuva-lightbox: klikkaa kuvaa lukunäkymässä → avautuu isona
- Kuvien hover-efekti (zoom-in kursori, kevyt varjo)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 12:48:21 +02:00
parent 9cb2eeeb62
commit 42b815116b
3 changed files with 91 additions and 18 deletions

View File

@@ -412,8 +412,9 @@
<span style="flex:1;"></span> <span style="flex:1;"></span>
<button type="button" class="guide-tb-btn" id="btn-guide-preview-toggle">Esikatselu</button> <button type="button" class="guide-tb-btn" id="btn-guide-preview-toggle">Esikatselu</button>
</div> </div>
<textarea id="guide-form-content" rows="20" style="font-family:'SF Mono',Monaco,Consolas,monospace;font-size:0.88rem;line-height:1.6;resize:vertical;min-height:300px;" placeholder="Kirjoita Markdown-muodossa..."></textarea> <textarea id="guide-form-content" rows="20" style="font-family:'SF Mono',Monaco,Consolas,monospace;font-size:0.88rem;line-height:1.6;resize:vertical;min-height:300px;" placeholder="Kirjoita Markdown-muodossa...&#10;&#10;Vinkki: Liitä kuvakaappaus suoraan Ctrl+V / ⌘V tai raahaa kuvatiedosto tähän"></textarea>
<div id="guide-preview-pane" class="guide-content" style="display:none;padding:1rem;border:2px solid #e0e0e0;border-radius:8px;min-height:300px;background:#fafbfc;"></div> <div id="guide-preview-pane" class="guide-content" style="display:none;padding:1rem;border:2px solid #e0e0e0;border-radius:8px;min-height:300px;background:#fafbfc;"></div>
<div style="font-size:0.78rem;color:#999;margin-top:4px;">📋 Liitä screenshot: Ctrl+V &nbsp;|&nbsp; 📁 Raahaa kuva editoriin &nbsp;|&nbsp; 📷 Tai käytä Kuva-nappia</div>
</div> </div>
<label style="display:flex;align-items:center;gap:0.5rem;margin:0.75rem 0;font-size:0.9rem;cursor:pointer;"> <label style="display:flex;align-items:center;gap:0.5rem;margin:0.75rem 0;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" id="guide-form-pinned"> Kiinnitetty (näkyy aina listauksen alussa) <input type="checkbox" id="guide-form-pinned"> Kiinnitetty (näkyy aina listauksen alussa)

104
script.js
View File

@@ -3498,6 +3498,20 @@ let guideCategories = [];
let currentGuideId = null; let currentGuideId = null;
// Markdown-renderöijä (kevyt, ei ulkoisia kirjastoja) // Markdown-renderöijä (kevyt, ei ulkoisia kirjastoja)
// Kuva-lightbox ohjeissa
function openGuideLightbox(src, alt) {
let overlay = document.getElementById('guide-lightbox');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'guide-lightbox';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:10000;cursor:zoom-out;padding:2rem;';
overlay.addEventListener('click', () => overlay.style.display = 'none');
document.body.appendChild(overlay);
}
overlay.innerHTML = `<img src="${src}" alt="${alt}" style="max-width:95%;max-height:95%;border-radius:8px;box-shadow:0 8px 40px rgba(0,0,0,0.5);">`;
overlay.style.display = 'flex';
}
function renderMarkdown(md) { function renderMarkdown(md) {
if (!md) return ''; if (!md) return '';
let html = esc(md); let html = esc(md);
@@ -3513,8 +3527,8 @@ function renderMarkdown(md) {
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>'); html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Kuvat (ennen linkkejä!) // Kuvat (ennen linkkejä!) — klikkaa avataksesi isompana
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:8px;margin:0.5rem 0;">'); html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="guide-img" onclick="openGuideLightbox(this.src, this.alt)" title="Klikkaa suurentaaksesi">');
// Linkit // Linkit
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
// Lainaukset // Lainaukset
@@ -3746,13 +3760,15 @@ document.getElementById('btn-guide-preview-toggle')?.addEventListener('click', (
} }
}); });
// Kuva-upload // Kuva-upload: yhteinen upload-funktio
document.getElementById('btn-guide-image')?.addEventListener('click', () => { async function guideUploadImage(file) {
document.getElementById('guide-image-input')?.click(); const ta = document.getElementById('guide-form-content');
}); if (!ta) return;
document.getElementById('guide-image-input')?.addEventListener('change', async (e) => { const pos = ta.selectionStart;
const file = e.target.files[0]; // Näytä upload-placeholder
if (!file) return; const placeholder = `![Ladataan: ${file.name}...]()`;
ta.value = ta.value.substring(0, pos) + placeholder + ta.value.substring(ta.selectionEnd);
ta.focus();
const formData = new FormData(); const formData = new FormData();
formData.append('image', file); formData.append('image', file);
try { try {
@@ -3761,16 +3777,70 @@ document.getElementById('guide-image-input')?.addEventListener('change', async (
}); });
const result = await res.json(); const result = await res.json();
if (!res.ok) throw new Error(result.error || 'Virhe'); if (!res.ok) throw new Error(result.error || 'Virhe');
// Lisää Markdown-kuvatagi editoriin
const ta = document.getElementById('guide-form-content');
const pos = ta.selectionStart;
const mdImg = `![${file.name}](${result.url})`; const mdImg = `![${file.name}](${result.url})`;
ta.value = ta.value.substring(0, pos) + mdImg + ta.value.substring(ta.selectionEnd); ta.value = ta.value.replace(placeholder, mdImg);
ta.focus(); } catch (err) {
ta.selectionStart = ta.selectionEnd = pos + mdImg.length; ta.value = ta.value.replace(placeholder, '');
} catch (err) { alert(err.message); } alert('Kuvan lataus epäonnistui: ' + err.message);
e.target.value = ''; // nollaa input }
}
// Toolbar-nappi
document.getElementById('btn-guide-image')?.addEventListener('click', () => {
document.getElementById('guide-image-input')?.click();
}); });
document.getElementById('guide-image-input')?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) await guideUploadImage(file);
e.target.value = '';
});
// Paste screenshot leikepöydältä (Ctrl+V / Cmd+V)
document.getElementById('guide-form-content')?.addEventListener('paste', async (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (file) {
// Anna tiedostolle nimi aikaleimalla
const ext = file.type.split('/')[1] || 'png';
const named = new File([file], `screenshot-${Date.now()}.${ext}`, { type: file.type });
await guideUploadImage(named);
}
return;
}
}
});
// Drag & drop kuvat editoriin
const guideTA = document.getElementById('guide-form-content');
if (guideTA) {
guideTA.addEventListener('dragover', (e) => {
if (e.dataTransfer?.types?.includes('Files')) {
e.preventDefault();
guideTA.style.borderColor = 'var(--primary-color)';
guideTA.style.background = '#f0f7ff';
}
});
guideTA.addEventListener('dragleave', () => {
guideTA.style.borderColor = '';
guideTA.style.background = '';
});
guideTA.addEventListener('drop', async (e) => {
guideTA.style.borderColor = '';
guideTA.style.background = '';
const files = e.dataTransfer?.files;
if (!files?.length) return;
for (const file of files) {
if (file.type.startsWith('image/')) {
e.preventDefault();
await guideUploadImage(file);
}
}
});
}
// Kategorianhallinta // Kategorianhallinta
document.getElementById('btn-manage-guide-cats')?.addEventListener('click', () => { document.getElementById('btn-manage-guide-cats')?.addEventListener('click', () => {

View File

@@ -1121,6 +1121,8 @@ span.empty {
.guide-content hr { border:none; border-top:2px solid #f0f2f5; margin:1.5rem 0; } .guide-content hr { border:none; border-top:2px solid #f0f2f5; margin:1.5rem 0; }
.guide-tb-btn { background:#f0f2f5; border:1px solid #ddd; padding:4px 10px; border-radius:5px; font-size:0.78rem; font-weight:600; cursor:pointer; color:#555; transition:all 0.15s; } .guide-tb-btn { background:#f0f2f5; border:1px solid #ddd; padding:4px 10px; border-radius:5px; font-size:0.78rem; font-weight:600; cursor:pointer; color:#555; transition:all 0.15s; }
.guide-tb-btn:hover { background:var(--primary-color); color:#fff; border-color:var(--primary-color); } .guide-tb-btn:hover { background:var(--primary-color); color:#fff; border-color:var(--primary-color); }
.guide-img { max-width:100%; border-radius:8px; margin:0.75rem 0; cursor:zoom-in; transition:transform 0.15s, box-shadow 0.15s; border:1px solid #e8e8e8; }
.guide-img:hover { transform:scale(1.01); box-shadow:0 4px 16px rgba(0,0,0,0.12); }
/* Role badge */ /* Role badge */
.role-badge { .role-badge {