allow paste of images on mobile
This commit is contained in:
@@ -11,12 +11,15 @@
|
||||
<div class="bg-white rounded-lg border border-slate-300 shadow-sm overflow-hidden">
|
||||
<div class="p-4 sm:p-6">
|
||||
<%= form_with url: visual_results_domain_posts_path, method: :post, multipart: true, class: "flex flex-col gap-4" do |form| %>
|
||||
<div id="drag-drop-area" class="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center mb-4 transition-colors duration-200">
|
||||
<div id="drag-drop-area" class="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center mb-4 transition-colors duration-200 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 relative" tabindex="0">
|
||||
<!-- Hidden input for mobile paste functionality -->
|
||||
<input type="text" id="paste-input" class="absolute opacity-0 pointer-events-none" style="left: -9999px;" autocomplete="off">
|
||||
<div class="flex flex-col items-center justify-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<p class="text-slate-600 font-medium">Drag and drop an image here</p>
|
||||
<p class="text-slate-600 font-medium hidden sm:block">Drag and drop an image here</p>
|
||||
<p class="text-slate-600 font-medium sm:hidden block">tap here to paste an image from the clipboard</p>
|
||||
<p class="text-xs text-slate-500">or use one of the options below</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,6 +53,11 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const dragDropArea = document.getElementById('drag-drop-area');
|
||||
const fileInput = document.getElementById('image-file-input');
|
||||
const pasteInput = document.getElementById('paste-input');
|
||||
|
||||
// Detect if user is on mobile device
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
||||
|
||||
// Prevent default drag behaviors
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
@@ -70,6 +78,44 @@
|
||||
// Handle dropped files
|
||||
dragDropArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
// Handle click/tap on drag-drop area
|
||||
dragDropArea.addEventListener('click', async function() {
|
||||
if (isMobile) {
|
||||
// On mobile, focus the hidden input to enable paste context menu
|
||||
pasteInput.focus();
|
||||
// Try modern Clipboard API first if available
|
||||
if (navigator.clipboard && navigator.clipboard.read) {
|
||||
try {
|
||||
await tryClipboardAPIRead();
|
||||
} catch (err) {
|
||||
// Fallback to showing instruction for manual paste
|
||||
showMobilePasteInstruction();
|
||||
}
|
||||
} else {
|
||||
showMobilePasteInstruction();
|
||||
}
|
||||
} else {
|
||||
// On desktop, focus the main area for keyboard paste
|
||||
dragDropArea.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for paste events
|
||||
pasteInput.addEventListener('paste', handlePaste, false);
|
||||
dragDropArea.addEventListener('paste', handlePaste, false);
|
||||
document.addEventListener('paste', handlePaste, false);
|
||||
|
||||
// Handle keyboard events for accessibility
|
||||
dragDropArea.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (isMobile) {
|
||||
pasteInput.focus();
|
||||
showMobilePasteInstruction();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -85,34 +131,138 @@
|
||||
dragDropArea.classList.remove('bg-blue-50');
|
||||
}
|
||||
|
||||
function showMobilePasteInstruction() {
|
||||
// Create a temporary instruction message for mobile
|
||||
const instruction = document.createElement('p');
|
||||
instruction.textContent = 'Long press this area and select "Paste" from the menu, or use your browser\'s paste option';
|
||||
instruction.className = 'text-sm text-blue-600 mt-2 paste-instruction';
|
||||
|
||||
// Remove any previous instruction
|
||||
const previousInstruction = dragDropArea.querySelector('.paste-instruction');
|
||||
if (previousInstruction) {
|
||||
previousInstruction.remove();
|
||||
}
|
||||
|
||||
dragDropArea.appendChild(instruction);
|
||||
|
||||
// Remove instruction after 5 seconds (longer for mobile users to read)
|
||||
setTimeout(() => {
|
||||
if (instruction.parentNode) {
|
||||
instruction.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async function tryClipboardAPIRead() {
|
||||
try {
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
for (const clipboardItem of clipboardItems) {
|
||||
for (const type of clipboardItem.types) {
|
||||
if (type.startsWith('image/')) {
|
||||
const blob = await clipboardItem.getType(type);
|
||||
const file = new File([blob], 'clipboard-image.png', { type: blob.type });
|
||||
handleImageFile(file);
|
||||
return; // Successfully handled
|
||||
}
|
||||
}
|
||||
}
|
||||
// No image found
|
||||
showFeedbackMessage('No image found in clipboard. Copy an image first, then try again.', 'text-amber-600');
|
||||
} catch (err) {
|
||||
// API failed, throw to trigger fallback
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const clipboardItems = e.clipboardData.items;
|
||||
let imageFile = null;
|
||||
|
||||
// Look for image data in clipboard
|
||||
for (let i = 0; i < clipboardItems.length; i++) {
|
||||
const item = clipboardItems[i];
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
imageFile = item.getAsFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFile) {
|
||||
handleImageFile(imageFile);
|
||||
} else {
|
||||
// Show message if no image found in clipboard
|
||||
showFeedbackMessage('No image found in clipboard. Copy an image first, then paste here.', 'text-amber-600');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length) {
|
||||
// Check if the dropped file is an image
|
||||
const file = files[0];
|
||||
if (file.type.match('image.*')) {
|
||||
// Update the file input with the dropped file
|
||||
fileInput.files = files;
|
||||
|
||||
// Show file name as feedback
|
||||
const fileName = document.createElement('p');
|
||||
fileName.textContent = `Selected: ${file.name}`;
|
||||
fileName.className = 'text-sm text-blue-600 mt-2';
|
||||
|
||||
// Remove any previous file name
|
||||
const previousFileName = dragDropArea.querySelector('.text-blue-600');
|
||||
if (previousFileName) {
|
||||
previousFileName.remove();
|
||||
}
|
||||
|
||||
dragDropArea.appendChild(fileName);
|
||||
handleImageFile(file);
|
||||
} else {
|
||||
// Alert if file is not an image
|
||||
alert('Please drop an image file.');
|
||||
showFeedbackMessage('Please drop an image file.', 'text-red-600');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageFile(file) {
|
||||
// Create a new FileList-like object
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
fileInput.files = dataTransfer.files;
|
||||
|
||||
// Show success feedback
|
||||
const fileName = file.name || 'Pasted image';
|
||||
showFeedbackMessage(`Selected: ${fileName}`, 'text-green-600');
|
||||
|
||||
// Add visual feedback to the drag-drop area
|
||||
highlight();
|
||||
setTimeout(unhighlight, 1000);
|
||||
}
|
||||
|
||||
function showFeedbackMessage(message, className) {
|
||||
// Create feedback message
|
||||
const feedback = document.createElement('p');
|
||||
feedback.textContent = message;
|
||||
feedback.className = `text-sm ${className} mt-2 feedback-message`;
|
||||
|
||||
// Remove any previous feedback
|
||||
const previousFeedback = dragDropArea.querySelector('.feedback-message');
|
||||
if (previousFeedback) {
|
||||
previousFeedback.remove();
|
||||
}
|
||||
|
||||
dragDropArea.appendChild(feedback);
|
||||
|
||||
// Remove feedback after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (feedback.parentNode) {
|
||||
feedback.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Add additional mobile support
|
||||
if (isMobile) {
|
||||
// Enable long press context menu on the hidden input
|
||||
pasteInput.addEventListener('contextmenu', function(e) {
|
||||
// Allow context menu to show (which includes paste option)
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Handle touch start to prepare for paste
|
||||
dragDropArea.addEventListener('touchstart', function(e) {
|
||||
// Focus the hidden input to enable paste functionality
|
||||
setTimeout(() => {
|
||||
pasteInput.focus();
|
||||
}, 100);
|
||||
}, { passive: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
<i class="fas fa-chart-bar mr-3 w-4 text-slate-400"></i>
|
||||
<span>Prometheus</span>
|
||||
<% end %>
|
||||
<%= link_to log_entries_path, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
|
||||
<i class="fas fa-list mr-3 w-4 text-slate-400"></i>
|
||||
<span>Log Entries</span>
|
||||
<% end %>
|
||||
<div class="my-2 border-t border-slate-200"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
Reference in New Issue
Block a user