Introduction to HTML5 APIs
HTML5 APIs represent a collection of powerful browser-based interfaces that extend the capabilities of modern web applications far beyond simple document rendering. These APIs provide standardized methods for handling geolocation, persistent storage, background processing, real-time graphics, and much more—all without requiring third-party plugins or external frameworks. They form the backbone of modern Progressive Web Apps and rich interactive experiences.
What Are HTML5 APIs?
HTML5 APIs are JavaScript interfaces built into modern web browsers that expose device capabilities, system services, and advanced functionality to web applications. They are defined as part of the HTML5 specification and related W3C/WHATWG standards, providing consistent, cross-browser access to features that were previously only available to native applications. These APIs work alongside HTML markup and CSS styling, accessed entirely through JavaScript running in the browser sandbox.
Why HTML5 APIs Matter
The significance of HTML5 APIs cannot be overstated in today's web ecosystem. They enable web applications to function offline, process data in background threads, create hardware-accelerated 2D/3D graphics, access device sensors, manage browser history programmatically, and deliver native-like experiences. This convergence of web and native capabilities has transformed what users expect from a web application. Developers can now build single-page applications, offline-first experiences, real-time collaboration tools, and immersive visualizations—all deployable instantly via a URL with no installation required.
Geolocation API
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Overview and Use Cases
The Geolocation API allows web applications to access the user's physical location, subject to user permission. Common use cases include store finders, delivery tracking, localized content, fitness tracking apps, and real-time navigation assistants. The API supports both one-time position queries and continuous location monitoring through watch methods.
Basic Implementation
The Geolocation API is accessed through navigator.geolocation. Before calling any method, you should verify browser support and handle permission scenarios gracefully. Here is a complete working example that obtains the user's position and displays it:
// Check for Geolocation API support
if ('geolocation' in navigator) {
const geoOptions = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000
};
function successCallback(position) {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
const accuracy = position.coords.accuracy;
const altitude = position.coords.altitude;
const speed = position.coords.speed;
const timestamp = new Date(position.timestamp);
console.log(`Latitude: ${latitude.toFixed(6)}`);
console.log(`Longitude: ${longitude.toFixed(6)}`);
console.log(`Accuracy: ${accuracy} meters`);
console.log(`Timestamp: ${timestamp.toLocaleString()}`);
// Display on a map or update UI
document.getElementById('location-display').innerHTML = `
You are at ${latitude.toFixed(4)}, ${longitude.toFixed(4)}
(accurate to within ${Math.round(accuracy)} meters)
`;
}
function errorCallback(error) {
let message = '';
switch(error.code) {
case error.PERMISSION_DENIED:
message = 'User denied the geolocation request.';
break;
case error.POSITION_UNAVAILABLE:
message = 'Location information is unavailable.';
break;
case error.TIMEOUT:
message = 'The request to get user location timed out.';
break;
default:
message = 'An unknown error occurred.';
break;
}
console.error('Geolocation error:', message);
document.getElementById('location-display').textContent = message;
}
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, geoOptions);
} else {
console.error('Geolocation is not supported by this browser.');
}
Watching Position Changes
For real-time tracking scenarios, use watchPosition() which returns a watch ID that can be cleared later:
let watchID = null;
function startWatching() {
watchID = navigator.geolocation.watchPosition(
(position) => {
console.log(`Updated position: ${position.coords.latitude}, ${position.coords.longitude}`);
// Update a real-time map marker here
},
(error) => {
console.error('Watch error:', error.message);
},
{ enableHighAccuracy: true, maximumAge: 0, timeout: 5000 }
);
}
function stopWatching() {
if (watchID !== null) {
navigator.geolocation.clearWatch(watchID);
watchID = null;
console.log('Stopped watching position.');
}
}
Best Practices for Geolocation
- Always request location in response to a user gesture—don't call geolocation methods on page load without explicit user intent
- Handle all three error states—permission denied, unavailable, and timeout—with user-friendly fallbacks
- Use appropriate accuracy settings—
enableHighAccuracy: trueconsumes more battery; use it only when precision is critical - Set reasonable timeouts—a 10-second timeout is usually sufficient for high-accuracy requests
- Cache position data with maximumAge—avoid repeated calls when stale data is acceptable
- Request HTTPS—many browsers require a secure context for geolocation access
Web Storage API
Understanding localStorage and sessionStorage
The Web Storage API provides two mechanisms for storing key-value pairs on the client: localStorage persists data indefinitely across browser sessions, while sessionStorage clears data when the tab or window closes. Both offer a synchronous, string-based interface with approximately 5–10 MB of storage per origin, depending on the browser.
Complete localStorage Example
// Check if storage is available
function storageAvailable(type) {
try {
const storage = window[type];
const testKey = '__storage_test__';
storage.setItem(testKey, testKey);
storage.removeItem(testKey);
return true;
} catch (e) {
return e instanceof DOMException &&
(e.code === 22 || e.code === 1014 ||
e.name === 'QuotaExceededError' ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
storage && storage.length !== 0;
}
}
if (storageAvailable('localStorage')) {
// Storing complex data requires serialization
const userPreferences = {
theme: 'dark',
fontSize: 16,
notifications: true,
lastVisit: new Date().toISOString()
};
// Save to localStorage
localStorage.setItem('preferences', JSON.stringify(userPreferences));
localStorage.setItem('visitCount', '42');
// Retrieve and parse
const savedPreferences = JSON.parse(localStorage.getItem('preferences'));
const visitCount = parseInt(localStorage.getItem('visitCount'), 10) || 0;
console.log('Loaded preferences:', savedPreferences);
console.log('Visit count:', visitCount);
// Increment visit count
localStorage.setItem('visitCount', (visitCount + 1).toString());
// Remove a specific item
localStorage.removeItem('temporaryData');
// Clear all storage for this origin
// localStorage.clear();
// Iterate through all stored keys
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`Key: ${key}, Value: ${value}`);
}
// Listen for storage changes from other tabs/windows
window.addEventListener('storage', (event) => {
console.log(`Storage changed: ${event.key} changed from ${event.oldValue} to ${event.newValue}`);
console.log(`Affected storage area: ${event.storageArea === localStorage ? 'localStorage' : 'sessionStorage'}`);
console.log(`Source URL: ${event.url}`);
});
}
sessionStorage for Temporary Data
// sessionStorage works identically but is scoped to the tab session
// Data survives page refreshes but not tab close/open
// Store form state before navigation
function saveFormState() {
const formData = {
name: document.getElementById('name').value,
email: document.getElementById('email').value,
message: document.getElementById('message').value
};
sessionStorage.setItem('formDraft', JSON.stringify(formData));
}
// Restore form state on page load
function restoreFormState() {
const saved = sessionStorage.getItem('formDraft');
if (saved) {
const formData = JSON.parse(saved);
document.getElementById('name').value = formData.name || '';
document.getElementById('email').value = formData.email || '';
document.getElementById('message').value = formData.message || '';
}
}
// Attach save on navigation
window.addEventListener('beforeunload', saveFormState);
document.addEventListener('DOMContentLoaded', restoreFormState);
Best Practices for Web Storage
- Always wrap storage access in try-catch—quota limits and privacy modes can throw exceptions
- Serialize complex data with JSON—storage only accepts strings; use
JSON.stringify()andJSON.parse() - Don't store sensitive information—storage is accessible via JavaScript and browser dev tools; never store passwords, tokens, or personal data without encryption
- Use the storage event for cross-tab synchronization—it fires only in other tabs, not the one making the change
- Consider IndexedDB for larger datasets—when you need query capabilities, transactions, or storage beyond 10 MB
- Implement a fallback strategy—check
storageAvailable()and degrade gracefully when storage is unavailable
Canvas API
The HTML5 Canvas Element
The Canvas API provides a programmatic drawing surface for 2D graphics, enabling pixel-level manipulation, shapes, text rendering, image compositing, and animation. It is hardware-accelerated in modern browsers and forms the foundation of web-based games, data visualizations, image editors, and generative art. The API operates through a drawing context obtained from a <canvas> element.
Drawing Fundamentals
// HTML:
const canvas = document.getElementById('myCanvas');
// Check for canvas support
if (!canvas.getContext) {
console.error('Canvas not supported');
} else {
const ctx = canvas.getContext('2d');
// ---- Basic Shapes ----
// Rectangle with fill
ctx.fillStyle = 'rgb(70, 130, 180)';
ctx.fillRect(50, 50, 200, 100);
// Rectangle with stroke (outline)
ctx.strokeStyle = 'rgb(200, 50, 50)';
ctx.lineWidth = 4;
ctx.strokeRect(300, 50, 200, 100);
// Clear a region
ctx.clearRect(100, 70, 100, 60);
// ---- Paths and Lines ----
ctx.beginPath();
ctx.moveTo(50, 200);
ctx.lineTo(200, 300);
ctx.lineTo(350, 220);
ctx.lineTo(500, 350);
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// ---- Circles and Arcs ----
ctx.beginPath();
ctx.arc(400, 400, 80, 0, Math.PI * 2, false); // Full circle
ctx.fillStyle = 'rgba(255, 200, 50, 0.7)';
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.stroke();
// ---- Text Rendering ----
ctx.font = 'bold 36px "Segoe UI", sans-serif';
ctx.fillStyle = '#222';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Canvas API', 400, 480);
// Measure text width for layout calculations
const textMetrics = ctx.measureText('Canvas API');
console.log('Text width:', textMetrics.width);
// ---- Gradients ----
const gradient = ctx.createLinearGradient(600, 50, 750, 150);
gradient.addColorStop(0, '#ff6b6b');
gradient.addColorStop(0.5, '#feca57');
gradient.addColorStop(1, '#48dbfb');
ctx.fillStyle = gradient;
ctx.fillRect(600, 50, 150, 100);
// ---- Images ----
const img = new Image();
img.onload = () => {
// Draw image with scaling
ctx.drawImage(img, 600, 200, 150, 100);
// Draw a portion of the image: source x, y, w, h, dest x, y, w, h
// ctx.drawImage(img, 10, 10, 50, 50, 600, 350, 100, 100);
};
img.src = 'https://example.com/sample-image.jpg';
}
Animation Loop
const canvas = document.getElementById('animationCanvas');
const ctx = canvas.getContext('2d');
let ballX = 50;
let ballY = 50;
let velocityX = 3;
let velocityY = 2;
const ballRadius = 15;
function animate() {
// Clear the entire canvas each frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Update position
ballX += velocityX;
ballY += velocityY;
// Bounce off walls
if (ballX + ballRadius > canvas.width || ballX - ballRadius < 0) {
velocityX = -velocityX;
}
if (ballY + ballRadius > canvas.height || ballY - ballRadius < 0) {
velocityY = -velocityY;
}
// Draw the ball
ctx.beginPath();
ctx.arc(ballX, ballY, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = 'crimson';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.stroke();
// Continue the animation loop
requestAnimationFrame(animate);
}
// Start the animation
requestAnimationFrame(animate);
Best Practices for Canvas
- Use
requestAnimationFramefor animation loops—it synchronizes with the display refresh rate and pauses when the tab is hidden - Clear only the dirty regions when possible—
clearRect()on the entire canvas can be expensive; use layered canvases for complex scenes - Batch similar drawing operations—minimize state changes (fillStyle, strokeStyle) by grouping same-style draws together
- Pre-render static elements to offscreen canvases—draw complex unchanging elements once to an offscreen canvas, then composite with
drawImage() - Always set canvas dimensions via attributes, not CSS—CSS scaling distorts the coordinate system; use
canvas.widthandcanvas.heightproperties - Handle high-DPI displays—account for
window.devicePixelRatioto avoid blurry rendering on Retina displays
Web Workers API
Enabling Multithreading in the Browser
Web Workers allow you to run JavaScript code in background threads, completely independent of the main UI thread. This prevents heavy computation from blocking user interactions, scrolling, and animations. Workers communicate with the main thread through a message-passing system using postMessage() and onmessage event handlers. There are three types: dedicated workers (single tab), shared workers (multiple tabs/browsing contexts), and service workers (network proxy and offline cache).
Dedicated Worker Implementation
First, create the worker script file heavy-computation.js:
// heavy-computation.js — This runs in a separate thread
self.onmessage = function(event) {
const data = event.data;
console.log('Worker received:', data);
// Perform CPU-intensive work without blocking the UI
const startTime = performance.now();
// Example: Calculate prime numbers up to a limit
function findPrimes(limit) {
const primes = [];
const sieve = new Uint8Array(limit + 1);
for (let i = 2; i <= limit; i++) {
if (sieve[i] === 0) {
primes.push(i);
for (let j = i * i; j <= limit; j += i) {
sieve[j] = 1;
}
}
}
return primes;
}
const primes = findPrimes(data.limit || 100000);
const endTime = performance.now();
// Send result back to the main thread
self.postMessage({
primesCount: primes.length,
lastTenPrimes: primes.slice(-10),
computationTime: endTime - startTime
});
};
// Optional: handle errors in the worker
self.onerror = function(error) {
console.error('Worker error:', error.message);
};
Now, the main thread code that creates and communicates with the worker:
// Main thread (app.js)
// Check for Worker support
if (window.Worker) {
const worker = new Worker('heavy-computation.js');
// Listen for messages from the worker
worker.onmessage = function(event) {
const result = event.data;
console.log(`Found ${result.primesCount} primes`);
console.log(`Last 10 primes: ${result.lastTenPrimes.join(', ')}`);
console.log(`Computation took ${result.computationTime.toFixed(2)} ms in worker`);
document.getElementById('result').textContent =
`Found ${result.primesCount} primes in ${result.computationTime.toFixed(0)} ms`;
};
// Handle worker errors
worker.onerror = function(error) {
console.error('Worker error on line', error.lineno, ':', error.message);
document.getElementById('result').textContent = 'Computation failed';
};
// Send data to the worker
document.getElementById('calculate-btn').addEventListener('click', () => {
const limit = parseInt(document.getElementById('limit-input').value, 10) || 50000;
document.getElementById('result').textContent = 'Calculating...';
worker.postMessage({ limit: limit });
});
// Terminate the worker when done (or on page unload)
window.addEventListener('beforeunload', () => {
worker.terminate();
});
} else {
console.error('Web Workers are not supported in this browser.');
// Implement fallback: run computation on main thread with progress indication
}
Transferring Data Efficiently
// Transferring ownership of large buffers for zero-copy performance
const largeArrayBuffer = new ArrayBuffer(1024 * 1024 * 10); // 10 MB
const worker = new Worker('data-processor.js');
// Transfer the buffer ownership to the worker (main thread loses access)
worker.postMessage({ buffer: largeArrayBuffer, metadata: { id: 123 } },
[largeArrayBuffer]); // List of transferable objects
// After transfer, largeArrayBuffer.byteLength === 0 in main thread
console.log('Buffer transferred, main thread byteLength:', largeArrayBuffer.byteLength);
Best Practices for Web Workers
- Use workers for CPU-intensive tasks only—don't offload I/O or simple DOM updates; the overhead of message serialization can outweigh benefits for lightweight operations
- Transfer large data with the transferable interface—use the second argument of
postMessage()to transferArrayBufferownership instead of copying - Always terminate workers when no longer needed—use
worker.terminate()or let them self-terminate withself.close()in the worker script - Handle errors robustly—both
worker.onerrorin the main thread andself.onerrorin the worker should be implemented - Consider using a worker pool for multiple concurrent tasks—limit the pool size to
navigator.hardwareConcurrencyor slightly less - Don't access the DOM from workers—workers have no access to
document,window, or DOM methods; they exist in a separate global scope
History API
Single-Page Application Routing
The History API enables manipulation of the browser session history, allowing developers to change the URL displayed in the address bar without triggering a full page reload. This is essential for single-page applications (SPAs) where navigation happens client-side. The API provides pushState(), replaceState(), and the popstate event for handling back/forward navigation.
Complete SPA Router Example
// Simple client-side router using History API
class SPARouter {
constructor() {
this.routes = new Map();
this.currentRoute = null;
// Bind to popstate for back/forward navigation
window.addEventListener('popstate', (event) => {
this.handleRoute(window.location.pathname, event.state || {});
});
// Intercept link clicks for internal navigation
document.addEventListener('click', (event) => {
const link = event.target.closest('a[data-spa-link]');
if (link && link.href.startsWith(window.location.origin)) {
event.preventDefault();
const path = link.getAttribute('href');
const state = { fromLink: true, timestamp: Date.now() };
this.navigate(path, state);
}
});
}
register(path, handler) {
this.routes.set(path, handler);
}
navigate(url, state = {}, title = '') {
// Push new state onto history stack
window.history.pushState(state, title, url);
this.handleRoute(url, state);
}
replace(url, state = {}, title = '') {
// Replace current state without adding to history stack
window.history.replaceState(state, title, url);
this.handleRoute(url, state);
}
handleRoute(path, state) {
const handler = this.routes.get(path);
const container = document.getElementById('app-content');
if (handler) {
this.currentRoute = path;
handler({ container, state, path });
} else {
// Fallback for unknown routes
container.innerHTML = '404 - Page Not Found
';
}
}
init() {
// Handle the initial page load
const initialState = { initialLoad: true };
window.history.replaceState(initialState, '', window.location.pathname);
this.handleRoute(window.location.pathname, initialState);
}
}
// Usage example
const router = new SPARouter();
router.register('/', ({ container }) => {
container.innerHTML = `
Home Page
Welcome! This content is rendered client-side without a page reload.
Go to About
`;
});
router.register('/about', ({ container }) => {
container.innerHTML = `
About Us
We are a company that builds SPAs using the History API.
Back to Home
`;
});
router.register('/contact', ({ container }) => {
container.innerHTML = `
Contact
Email us at info@example.com
Back to Home
`;
});
router.init();
Programmatic Back/Forward Navigation
// Navigate programmatically
document.getElementById('back-btn').addEventListener('click', () => {
window.history.back(); // Equivalent to user clicking browser back button
});
document.getElementById('forward-btn').addEventListener('click', () => {
window.history.forward();
});
document.getElementById('go-btn').addEventListener('click', () => {
const steps = parseInt(document.getElementById('steps-input').value, 10);
window.history.go(steps); // Negative for back, positive for forward
});
// Check history length
console.log(`History entries: ${window.history.length}`);
Best Practices for History API
- Always handle the
popstateevent—this is triggered by browser back/forward buttons; failing to handle it breaks navigation - Use meaningful state objects—store the minimum data needed to restore the page state; avoid storing large objects in history state
- Implement proper 404 handling—on page reload at a SPA route, the server must serve the application shell, not a real 404
- Limit state object size—browsers typically restrict state to approximately 640 KB; large states can cause errors
- Combine with
replaceState()for redirects—use replaceState when the current URL should not appear in the back/forward history
Drag and Drop API
Native Drag-and-Drop Capabilities
The Drag and Drop API enables elements to be dragged within a page or between applications. It provides a complete event-driven system with drag initiation, drag-over feedback, and drop handling. The API supports both intra-page element reordering and file imports from the operating system desktop.
Complete Drag-and-Drop Implementation
// HTML structure for a sortable list:
//
// - Item 1
// - Item 2
// - Item 3
//
const sortableList = document.getElementById('sortable-list');
let draggedElement = null;
let draggedOverElement = null;
// ---- Drag Start ----
sortableList.addEventListener('dragstart', (event) => {
draggedElement = event.target;
// Make the dragged element semi-transparent
event.target.style.opacity = '0.4';
// Store data to be transferred
event.dataTransfer.setData('text/plain', event.target.dataset.id);
event.dataTransfer.effectAllowed = 'move';
// Set custom drag image (optional)
const dragImage = event.target.cloneNode(true);
dragImage.style.opacity = '0.8';
dragImage.style.position = 'absolute';
dragImage.style.top = '-9999px';
document.body.appendChild(dragImage);
event.dataTransfer.setDragImage(dragImage, 20, 20);
// Clean up the drag image clone after a short delay
setTimeout(() => document.body.removeChild(dragImage), 0);
});
// ---- Drag Over (allow drop) ----
sortableList.addEventListener('dragover', (event) => {
event.preventDefault(); // Required to allow a drop
event.dataTransfer.dropEffect = 'move';
draggedOverElement = event.target.closest('li');
});
// ---- Drag Enter (visual feedback) ----
sortableList.addEventListener('dragenter', (event) => {
event.preventDefault();
const targetLi = event.target.closest('li');
if (targetLi && targetLi !== draggedElement) {
targetLi.style.borderTop = '3px solid #2196F3';
}
});
// ---- Drag Leave ----
sortableList.addEventListener('dragleave', (event) => {
const targetLi = event.target.closest('li');
if (targetLi) {
targetLi.style.borderTop = '';
}
});
// ---- Drop ----
sortableList.addEventListener('drop', (event) => {
event.preventDefault();
const targetLi = event.target.closest('li');
if (targetLi && draggedElement !== targetLi) {
// Reorder elements in the DOM
const parent = draggedElement.parentNode;
const referenceNode = targetLi;
if (draggedElement.compareDocumentPosition(referenceNode) & Node.DOCUMENT_POSITION_FOLLOWING) {
// draggedElement is before referenceNode
parent.insertBefore(draggedElement, referenceNode.nextSibling);
} else {
parent.insertBefore(draggedElement, referenceNode);
}
// Reset styles
targetLi.style.borderTop = '';
}
// Reset the dragged element
draggedElement.style.opacity = '1';
draggedElement = null;
});
// ---- Drag End (cleanup) ----
sortableList.addEventListener('dragend', (event) => {
event.target.style.opacity = '1';
// Remove any remaining visual indicators
const allItems = sortableList.querySelectorAll('li');
allItems.forEach(item => item.style.borderTop = '');
draggedElement = null;
draggedOverElement = null;
});
File Drop from Desktop
const dropZone = document.getElementById('file-drop-zone');
// Prevent default to allow drops
dropZone.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (event) => {
event.preventDefault();
dropZone.classList.remove('drag-over');
const files = event.dataTransfer.files;
console.log(`Dropped ${files.length} file(s)`);
for (let i = 0; i < files.length; i++) {
const file = files[i];
console.log(`File: ${file.name}, Size: ${file.size} bytes, Type: ${file.type}`);
// Read file contents (demonstration with text files)
if (file.type.startsWith('text/')) {
const reader = new FileReader();
reader.onload = (e) => {
console.log(`Contents of ${file.name}:`, e.target.result.substring(0, 200));
};
reader.readAsText(file);
}
}
});
Best Practices for Drag and Drop
- Always call
preventDefault()on dragover—without it, the drop event will never fire - Provide clear visual feedback—use dragenter/dragleave to highlight valid drop targets and indicate where the item will land
- Handle both mouse and touch interactions—the Drag and Drop API does not work on mobile touch devices natively; consider pointer events as a fallback
- Set
draggable="true"explicitly—only elements with this attribute (or with it set via JS) can initiate drag operations - Clean up drag state in dragend—always reset opacity, borders, and global variables when the drag ends, whether successful or canceled
File API
Reading Files Client-Side
The File API provides the ability to read file contents and access file metadata entirely on the client side, without uploading to a server. It works in conjunction with the <input type="file"> element and the Drag and Drop API. The API exposes File, FileList, FileReader, and Blob interfaces for handling binary data.
Complete File Reading Example
// HTML:
//
//
const fileInput = document.getElementById('file-input');
const previewContainer = document.getElementById('preview-container');
const fileInfo = document.getElementById('file-info');
fileInput.addEventListener('change', (event) => {
const files = event.target.files; // FileList object
fileInfo.innerHTML = '';
previewContainer.innerHTML = '';
if (files.length === 0) return;
// Display file metadata
let infoHTML = `Selected ${files.length} file(s)
`;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const lastModified = new Date(file.lastModified).toLocaleString();
infoHTML += `
${file.name}
Size: ${formatFileSize(file.size)}
Type: ${file.type || 'unknown'}
Last modified: ${lastModified}
`;
}
fileInfo.innerHTML = infoHTML;
// Process each file
for (let i = 0; i < files.length; i++) {
processFile(files[i]);
}
});
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' bytes';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(2) + ' MB';
}
function processFile(file) {
const reader = new FileReader();