file upload preview in editor on /new (#171)
- images render fitted into the editor area (object-fit contain), editor hidden - text files load content into the editor; title placeholder convention (Python.py, Text.txt fallback) - title auto-fills only when blank; never overwrites a typed title - external JS + CSS classes only, CSP-safe (no inline styles/scripts)
This commit is contained in:
@@ -744,6 +744,19 @@ button[type="submit"]:focus-visible,
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.dropzone.dragover { border-width: 2px; }
|
||||
/* #171: uploaded-file preview replaces the editor area; image fitted inside */
|
||||
.file-preview {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.editor-wrap.previewing .editor,
|
||||
.editor-wrap.previewing .gutter { display: none; }
|
||||
|
||||
.file-chip {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
|
||||
@@ -230,6 +230,64 @@ function humanSize(n) {
|
||||
return n + ' B';
|
||||
}
|
||||
|
||||
// #171: text-file extensions -> language key for title placeholder
|
||||
// convention (e.g. Python.py); fallback Text.
|
||||
const LANG_BY_EXT = {
|
||||
py: 'python', go: 'go', js: 'javascript', mjs: 'javascript', ts: 'typescript',
|
||||
tsx: 'typescript', rs: 'rust', c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp',
|
||||
java: 'java', cs: 'csharp', sh: 'bash', bash: 'bash', sql: 'sql', yml: 'yaml',
|
||||
yaml: 'yaml', json: 'json', html: 'html', htm: 'html', css: 'css', xml: 'xml',
|
||||
php: 'php', rb: 'ruby', pl: 'perl', lua: 'lua', toml: 'toml', ini: 'ini',
|
||||
diff: 'diff', md: 'markdown', txt: 'text', log: 'text',
|
||||
};
|
||||
const TEXT_EXTS = new Set(Object.keys(LANG_BY_EXT));
|
||||
|
||||
function extOf(name) {
|
||||
const i = name.lastIndexOf('.');
|
||||
return i >= 0 ? name.slice(i + 1).toLowerCase() : '';
|
||||
}
|
||||
|
||||
// #171: when a file is attached it takes over the main editing area.
|
||||
// Images render fitted into the editor area; text files load their
|
||||
// content into the editor. Title auto-fills only if still blank (house
|
||||
// rule: never overwrite a typed title).
|
||||
const IMAGE_RE = /^image\//;
|
||||
let previewURL = null;
|
||||
|
||||
async function showFileInEditor(file) {
|
||||
const wrap = document.querySelector('.editor-wrap');
|
||||
const img = $('file-preview');
|
||||
if (IMAGE_RE.test(file.type)) {
|
||||
if (previewURL) URL.revokeObjectURL(previewURL);
|
||||
previewURL = URL.createObjectURL(file);
|
||||
img.src = previewURL;
|
||||
img.alt = file.name;
|
||||
wrap.classList.add('previewing');
|
||||
img.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
wrap.classList.remove('previewing');
|
||||
img.classList.add('hidden');
|
||||
if (!TEXT_EXTS.has(extOf(file.name))) return; // unknown binary: leave editor alone
|
||||
try {
|
||||
const text = await file.text();
|
||||
if (!content.value.trim()) {
|
||||
content.value = text;
|
||||
updateGutter();
|
||||
guessLang();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function clearPreview() {
|
||||
const wrap = document.querySelector('.editor-wrap');
|
||||
const img = $('file-preview');
|
||||
wrap.classList.remove('previewing');
|
||||
img.classList.add('hidden');
|
||||
img.removeAttribute('src');
|
||||
if (previewURL) { URL.revokeObjectURL(previewURL); previewURL = null; }
|
||||
}
|
||||
|
||||
function setAttachedFile(file) {
|
||||
if (!file) return clearAttachedFile();
|
||||
if (file.size > MAX_FILE_BYTES) {
|
||||
@@ -239,6 +297,23 @@ function setAttachedFile(file) {
|
||||
attachedFile = file;
|
||||
renderFileChip();
|
||||
setHidden('file-text-note', false);
|
||||
// #171: title auto-fill — images take the file name; text files use the
|
||||
// language-placeholder convention (e.g. Python.py, fallback Text.txt).
|
||||
// Only when the title is still blank; never overwrite a typed title.
|
||||
if (!$('title').value.trim()) {
|
||||
if (IMAGE_RE.test(file.type)) {
|
||||
$('title').value = file.name;
|
||||
} else {
|
||||
const lang = LANG_BY_EXT[extOf(file.name)];
|
||||
if (lang) {
|
||||
const fn = defaultFilename(lang);
|
||||
if (fn) $('title').value = fn;
|
||||
} else if (TEXT_EXTS.has(extOf(file.name))) {
|
||||
$('title').value = defaultFilename('text') || 'Text.txt';
|
||||
}
|
||||
}
|
||||
}
|
||||
showFileInEditor(file);
|
||||
}
|
||||
|
||||
function clearAttachedFile() {
|
||||
@@ -246,6 +321,7 @@ function clearAttachedFile() {
|
||||
$('file-input').value = '';
|
||||
renderFileChip();
|
||||
setHidden('file-text-note', true);
|
||||
clearPreview();
|
||||
}
|
||||
|
||||
function renderFileChip() {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<div class="float editor-wrap">
|
||||
<div class="gutter" id="gutter">1</div>
|
||||
<textarea class="editor" id="content" placeholder="Paste your code, text, or notes here…" spellcheck="false"></textarea>
|
||||
<img id="file-preview" class="file-preview hidden" alt="File preview">
|
||||
</div>
|
||||
<div class="created-banner" id="created"></div>
|
||||
<div class="actionbar">
|
||||
|
||||
Reference in New Issue
Block a user