feat: Enhance upload page tag suggestions with keyboard navigation and a top-aligned dropdown, while adding utility scripts for thumbnail copying and dummy data generation.
This commit is contained in:
@@ -251,6 +251,7 @@
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -307,13 +308,14 @@
|
||||
.tag-suggestions {
|
||||
/* (styles for dropdown remain similar, maybe cleaner shadow) */
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px 4px 0 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
@@ -329,7 +331,8 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-suggestion:hover {
|
||||
.tag-suggestion:hover,
|
||||
.tag-suggestion.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,42 +24,7 @@
|
||||
let tags = [];
|
||||
let selectedFile = null;
|
||||
|
||||
// Flash Message Logic
|
||||
const showFlash = (msg, type = 'success') => {
|
||||
const existing = document.querySelector('.flash-message');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const flash = document.createElement('div');
|
||||
flash.className = `flash-message ${type}`;
|
||||
flash.textContent = msg;
|
||||
|
||||
Object.assign(flash.style, {
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
padding: '15px 30px',
|
||||
borderRadius: '5px',
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
zIndex: '9999',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||
background: type === 'success' ? '#51cf66' : '#ff6b6b',
|
||||
opacity: '0',
|
||||
transition: 'opacity 0.3s'
|
||||
});
|
||||
|
||||
document.body.appendChild(flash);
|
||||
|
||||
// Fade in
|
||||
requestAnimationFrame(() => flash.style.opacity = '1');
|
||||
|
||||
// Remove after 5s
|
||||
setTimeout(() => {
|
||||
flash.style.opacity = '0';
|
||||
setTimeout(() => flash.remove(), 300);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
@@ -207,10 +172,42 @@
|
||||
updateSubmitButton();
|
||||
};
|
||||
|
||||
let currentFocus = -1;
|
||||
|
||||
const addActive = (x) => {
|
||||
if (!x) return false;
|
||||
removeActive(x);
|
||||
if (currentFocus >= x.length) currentFocus = 0;
|
||||
if (currentFocus < 0) currentFocus = (x.length - 1);
|
||||
x[currentFocus].classList.add("active");
|
||||
// Scroll to view
|
||||
x[currentFocus].scrollIntoView({ block: 'nearest' });
|
||||
};
|
||||
|
||||
const removeActive = (x) => {
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
x[i].classList.remove("active");
|
||||
}
|
||||
};
|
||||
|
||||
tagInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const x = tagSuggestions.getElementsByClassName("tag-suggestion");
|
||||
if (e.key === 'ArrowDown') {
|
||||
currentFocus++;
|
||||
addActive(x);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
currentFocus--;
|
||||
addActive(x);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addTag(tagInput.value);
|
||||
if (currentFocus > -1) {
|
||||
if (x) x[currentFocus].click();
|
||||
} else {
|
||||
addTag(tagInput.value);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
tagSuggestions.classList.remove('show');
|
||||
currentFocus = -1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -218,6 +215,7 @@
|
||||
tagInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
const query = tagInput.value.trim();
|
||||
currentFocus = -1; // Reset focus on new input
|
||||
|
||||
if (query.length < 2) {
|
||||
tagSuggestions.classList.remove('show');
|
||||
@@ -239,7 +237,10 @@
|
||||
tagSuggestions.classList.add('show');
|
||||
|
||||
tagSuggestions.querySelectorAll('.tag-suggestion').forEach(el => {
|
||||
el.addEventListener('click', () => addTag(el.textContent));
|
||||
el.addEventListener('click', () => {
|
||||
addTag(el.textContent);
|
||||
tagInput.focus();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
tagSuggestions.classList.remove('show');
|
||||
@@ -296,8 +297,7 @@
|
||||
if (res.success) {
|
||||
statusDiv.innerHTML = '✓ ' + res.msg;
|
||||
statusDiv.className = 'upload-status success';
|
||||
// Flash Message
|
||||
showFlash(res.msg, 'success');
|
||||
|
||||
|
||||
form.reset();
|
||||
tags = [];
|
||||
@@ -313,7 +313,7 @@
|
||||
if (res.repost) {
|
||||
statusDiv.innerHTML += ' <a href="/' + res.repost + '">View existing</a>';
|
||||
}
|
||||
showFlash('Upload failed: ' + res.msg, 'error');
|
||||
|
||||
}
|
||||
|
||||
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
||||
@@ -326,7 +326,7 @@
|
||||
xhr.onerror = () => {
|
||||
statusDiv.textContent = '✕ Upload failed. Please try again.';
|
||||
statusDiv.className = 'upload-status error';
|
||||
showFlash('Upload failed network error', 'error');
|
||||
|
||||
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
||||
submitBtn.querySelector('.btn-loading').style.display = 'none';
|
||||
progressContainer.style.display = 'none';
|
||||
@@ -340,7 +340,7 @@
|
||||
console.error(err);
|
||||
statusDiv.textContent = '✕ Upload failed: ' + err.message;
|
||||
statusDiv.className = 'upload-status error';
|
||||
showFlash('Upload failed: ' + err.message, 'error');
|
||||
|
||||
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
||||
submitBtn.querySelector('.btn-loading').style.display = 'none';
|
||||
updateSubmitButton();
|
||||
|
||||
Reference in New Issue
Block a user