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;
|
padding: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,13 +308,14 @@
|
|||||||
.tag-suggestions {
|
.tag-suggestions {
|
||||||
/* (styles for dropdown remain similar, maybe cleaner shadow) */
|
/* (styles for dropdown remain similar, maybe cleaner shadow) */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 0 0 4px 4px;
|
border-radius: 4px 4px 0 0;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -329,7 +331,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-suggestion:hover {
|
.tag-suggestion:hover,
|
||||||
|
.tag-suggestion.active {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,42 +24,7 @@
|
|||||||
let tags = [];
|
let tags = [];
|
||||||
let selectedFile = null;
|
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 formatSize = (bytes) => {
|
||||||
const units = ['B', 'KB', 'MB', 'GB'];
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
@@ -207,10 +172,42 @@
|
|||||||
updateSubmitButton();
|
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) => {
|
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();
|
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', () => {
|
tagInput.addEventListener('input', () => {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
const query = tagInput.value.trim();
|
const query = tagInput.value.trim();
|
||||||
|
currentFocus = -1; // Reset focus on new input
|
||||||
|
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
tagSuggestions.classList.remove('show');
|
tagSuggestions.classList.remove('show');
|
||||||
@@ -239,7 +237,10 @@
|
|||||||
tagSuggestions.classList.add('show');
|
tagSuggestions.classList.add('show');
|
||||||
|
|
||||||
tagSuggestions.querySelectorAll('.tag-suggestion').forEach(el => {
|
tagSuggestions.querySelectorAll('.tag-suggestion').forEach(el => {
|
||||||
el.addEventListener('click', () => addTag(el.textContent));
|
el.addEventListener('click', () => {
|
||||||
|
addTag(el.textContent);
|
||||||
|
tagInput.focus();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
tagSuggestions.classList.remove('show');
|
tagSuggestions.classList.remove('show');
|
||||||
@@ -296,8 +297,7 @@
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
statusDiv.innerHTML = '✓ ' + res.msg;
|
statusDiv.innerHTML = '✓ ' + res.msg;
|
||||||
statusDiv.className = 'upload-status success';
|
statusDiv.className = 'upload-status success';
|
||||||
// Flash Message
|
|
||||||
showFlash(res.msg, 'success');
|
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
tags = [];
|
tags = [];
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
if (res.repost) {
|
if (res.repost) {
|
||||||
statusDiv.innerHTML += ' <a href="/' + res.repost + '">View existing</a>';
|
statusDiv.innerHTML += ' <a href="/' + res.repost + '">View existing</a>';
|
||||||
}
|
}
|
||||||
showFlash('Upload failed: ' + res.msg, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
xhr.onerror = () => {
|
xhr.onerror = () => {
|
||||||
statusDiv.textContent = '✕ Upload failed. Please try again.';
|
statusDiv.textContent = '✕ Upload failed. Please try again.';
|
||||||
statusDiv.className = 'upload-status error';
|
statusDiv.className = 'upload-status error';
|
||||||
showFlash('Upload failed network error', 'error');
|
|
||||||
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
||||||
submitBtn.querySelector('.btn-loading').style.display = 'none';
|
submitBtn.querySelector('.btn-loading').style.display = 'none';
|
||||||
progressContainer.style.display = 'none';
|
progressContainer.style.display = 'none';
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
statusDiv.textContent = '✕ Upload failed: ' + err.message;
|
statusDiv.textContent = '✕ Upload failed: ' + err.message;
|
||||||
statusDiv.className = 'upload-status error';
|
statusDiv.className = 'upload-status error';
|
||||||
showFlash('Upload failed: ' + err.message, 'error');
|
|
||||||
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
submitBtn.querySelector('.btn-text').style.display = 'inline';
|
||||||
submitBtn.querySelector('.btn-loading').style.display = 'none';
|
submitBtn.querySelector('.btn-loading').style.display = 'none';
|
||||||
updateSubmitButton();
|
updateSubmitButton();
|
||||||
|
|||||||
Reference in New Issue
Block a user