JavaScript Browser APIs

Introduction to Browser APIs

Modern web browsers provide a rich set of Application Programming Interfaces (APIs) that allow JavaScript code to interact with the browser environment, the operating system, and various hardware features. These APIs extend the capabilities of JavaScript beyond its core language features, enabling developers to build powerful and interactive web applications.

Understanding and utilizing these APIs effectively is crucial for creating dynamic user experiences, managing data, handling network requests, and optimizing performance.

Key Areas Covered by Browser APIs:
  • Document Object Model (DOM) Manipulation
  • Network Requests (Fetch, XMLHttpRequest)
  • Client-Side Storage (Web Storage, IndexedDB)
  • User Location (Geolocation)
  • Graphics and Multimedia (Canvas, WebGL, Web Audio)
  • Asynchronous Operations and Background Tasks (Web Workers, Service Workers)
  • Real-time Communication (WebSockets)
  • User Interface Interactions (Notifications, History API)

Core Browser APIs Overview

1. Document Object Model (DOM) API

The DOM represents the structure of an HTML or XML document as a tree of objects (nodes). The DOM API provides the interface for JavaScript to dynamically access and manipulate the content, structure, and style of these documents.

Understanding the DOM Tree

Every HTML element, attribute, and piece of text is represented as a node in the DOM tree. The document object is the root node.

  • Element Nodes: Represent HTML tags (e.g., <div>, <p>, <a>).
  • Text Nodes: Represent the text content within elements.
  • Attribute Nodes: Represent element attributes (e.g., id, class, href).
  • Comment Nodes: Represent HTML comments.

Selecting Elements (Finding Nodes)

Various methods allow you to find specific nodes in the DOM.

// --- Specific Element Selection --- 

// Select by ID (fastest, returns single element or null)
const mainTitle = document.getElementById('main-title');
console.log('Element by ID:', mainTitle);

// --- Multiple Element Selection --- 

// Select by Class Name (returns live HTMLCollection)
const primaryButtons = document.getElementsByClassName('btn-primary');
console.log('Elements by Class Name (Live HTMLCollection):', primaryButtons);
// Access elements like an array: primaryButtons[0]

// Select by Tag Name (returns live HTMLCollection)
const allParagraphs = document.getElementsByTagName('p');
console.log('Elements by Tag Name (Live HTMLCollection):', allParagraphs);

// --- Modern Selector-Based Selection --- 

// Select by CSS Selector (returns first matching element or null)
const firstProduct = document.querySelector('.product-list .product-item:first-child');
console.log('First Matching Element:', firstProduct);

// Select by CSS Selector (returns static NodeList)
const warningAlerts = document.querySelectorAll('div.alert.alert-warning');
console.log('All Matching Elements (Static NodeList):', warningAlerts);
// Iterate using forEach: warningAlerts.forEach(alert => { /* ... */ });

// Selecting within an element
const productList = document.getElementById('product-list');
const itemsInList = productList?.querySelectorAll('.product-item'); // Search only within productList
console.log('Items within specific list:', itemsInList);

Live vs. Static Collections: HTMLCollection (from getElementsByClassName/TagName) updates automatically if the DOM changes. NodeList (from querySelectorAll) is a static snapshot and does not update automatically.

Traversing the DOM (Moving Between Nodes)

Once you have selected an element, you can navigate to related elements.

const currentItem = document.getElementById('item-2');

if (currentItem) {
    // Get Parent
    const parentList = currentItem.parentNode; // Or currentItem.parentElement (often preferred)
    console.log('Parent Element:', parentList);

    // Get Children (includes text nodes, comments)
    const allChildrenNodes = parentList.childNodes;
    console.log('All Child Nodes:', allChildrenNodes);

    // Get Element Children (only element nodes - usually more useful)
    const elementChildren = parentList.children; // HTMLCollection
    console.log('Element Children:', elementChildren);

    // First and Last Element Child
    const firstChild = parentList.firstElementChild;
    const lastChild = parentList.lastElementChild;
    console.log('First Child:', firstChild, 'Last Child:', lastChild);

    // Get Siblings
    const nextSibling = currentItem.nextElementSibling;
    const prevSibling = currentItem.previousElementSibling;
    console.log('Next Sibling:', nextSibling, 'Previous Sibling:', prevSibling);
}

Manipulating Elements (Changing the DOM)

Create, add, remove, and modify elements and their content.

// --- Creating and Adding Elements ---

// Create a new 
  • element const newItem = document.createElement('li'); newItem.textContent = 'Item 4 (New)'; // Set text content safely newItem.setAttribute('id', 'item-4'); // Append to the end of a parent const list = document.getElementById('my-list'); list?.appendChild(newItem); // Insert before a specific element const item3 = document.getElementById('item-3'); const anotherNewItem = document.createElement('li'); anotherNewItem.innerHTML = '<strong>Item 2.5</strong> (Inserted)'; // Can insert HTML (use carefully) list?.insertBefore(anotherNewItem, item3); // --- Removing and Replacing Elements --- // Remove an element const itemToRemove = document.getElementById('item-1'); itemToRemove?.remove(); // Modern and simple // Or: itemToRemove.parentNode.removeChild(itemToRemove); // Older way // Replace an element const itemToReplace = document.getElementById('item-2'); const replacementItem = document.createElement('li'); replacementItem.textContent = 'Item 2 (Replaced)'; itemToReplace?.parentNode?.replaceChild(replacementItem, itemToReplace); // --- Modifying Content and Attributes --- const heading = document.getElementById('main-title'); if (heading) { heading.textContent = 'Updated Main Title'; // Safer for plain text // heading.innerHTML = 'Updated <em>Main</em> Title'; // Use for HTML, risk of XSS if content is user-generated // Work with attributes heading.setAttribute('data-status', 'updated'); const status = heading.getAttribute('data-status'); console.log('Heading status:', status); heading.removeAttribute('data-old-attr'); console.log('Has class `main`?', heading.hasAttribute('class')); // Work with classes (recommended over className string manipulation) heading.classList.add('highlighted', 'important'); heading.classList.remove('old-class'); heading.classList.toggle('active'); // Adds if absent, removes if present console.log('Heading classes:', heading.classList); // Work with styles (directly sets inline styles) heading.style.color = 'purple'; heading.style.fontSize = '2.5rem'; // Use camelCase for CSS properties with hyphens heading.style.backgroundColor = '#f0f0f0'; // Remove a style // heading.style.backgroundColor = ''; }
  • Event Handling (Responding to Interactions)

    The DOM allows you to listen for and react to events triggered by user actions (clicks, keypresses, mouse movements) or browser actions (page load, resize).

    Adding Event Listeners
    const actionButton = document.getElementById('action-button');
    
    function onButtonClick(event) {
        console.log('--- Button Click Event ---');
        console.log('Event Type:', event.type); // e.g., 'click'
        console.log('Target Element:', event.target); // The element that triggered the event (the button)
        console.log('Current Target:', event.currentTarget); // The element the listener is attached to (usually same as target here)
        console.log('Timestamp:', event.timeStamp);
        console.log('Mouse Coordinates (relative to window):', event.clientX, event.clientY);
        
        // Prevent default action (e.g., form submission, link navigation)
        // event.preventDefault();
        
        // Stop the event from bubbling up to parent elements
        // event.stopPropagation();
        
        event.target.textContent = 'Clicked!';
        event.target.disabled = true;
    }
    
    if (actionButton) {
        // Recommended: addEventListener(eventType, handlerFunction, options/useCapture)
        actionButton.addEventListener('click', onButtonClick);
        
        // Add another listener for mouse over
        actionButton.addEventListener('mouseover', () => {
            actionButton.style.backgroundColor = 'lightblue';
        });
        actionButton.addEventListener('mouseout', () => {
            actionButton.style.backgroundColor = ''; // Reset style
        });
    }
    
    // Removing Event Listeners (requires the exact same function reference)
    function removeListenerExample() {
        if (actionButton) {
            actionButton.removeEventListener('click', onButtonClick);
            console.log('Click listener removed.');
        }
    }
    // setTimeout(removeListenerExample, 5000); // Example: remove after 5 seconds
    
    Event Propagation (Bubbling and Capturing)

    Events typically travel in two phases:

    1. Capturing Phase: The event travels down from the window to the target element. Listeners attached with useCapture: true (or the third argument as true) fire during this phase.
    2. Bubbling Phase: The event travels back up from the target element to the window. This is the default phase for listeners (useCapture: false).

    event.stopPropagation() prevents the event from traveling further (up or down depending on the phase).

    Event Delegation

    Instead of adding listeners to many individual child elements, add a single listener to a common parent. Check event.target inside the handler to determine which child was interacted with. This is more efficient, especially for dynamically added elements.

    const userList = document.getElementById('user-list');
    
    userList?.addEventListener('click', function(event) {
        // Check if the clicked element is a button inside a list item
        if (event.target && event.target.matches('li button.delete-btn')) {
            const listItem = event.target.closest('li'); // Find the parent 
  • const userName = listItem?.dataset.userName || 'Unknown User'; console.log(`Delete button clicked for user: ${userName}`); // listItem?.remove(); // Example: Remove the list item } });
  • 2. Fetch API

    A modern, promise-based interface for making network requests (HTTP and others). It supersedes the older XMLHttpRequest object, offering a cleaner and more powerful API.

    Basic GET Request & Response Object

    The fetch() function initiates a request and returns a promise that resolves to the Response object representing the server's response.

    fetch('https://jsonplaceholder.typicode.com/posts/1')
      .then(response => {
        console.log('--- Response Object ---');
        console.log('Status Code:', response.status); // e.g., 200, 404, 500
        console.log('Status Text:', response.statusText); // e.g., "OK", "Not Found"
        console.log('OK Status (200-299):', response.ok); // boolean
        console.log('Response Type:', response.type); // e.g., 'basic', 'cors'
        console.log('Response URL:', response.url);
    
        // Accessing Headers
        console.log('Content-Type Header:', response.headers.get('Content-Type'));
        // Iterate over headers
        // for (let [key, value] of response.headers) {
        //   console.log(`${key}: ${value}`);
        // }
    
        // IMPORTANT: Check response.ok before processing the body!
        if (!response.ok) {
          // Handle HTTP errors (e.g., 404 Not Found, 500 Internal Server Error)
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
    
        // The response body is a ReadableStream. Use methods to consume it:
        return response.json(); // Consumes the body and parses as JSON
        // return response.text(); // Consumes the body as plain text
        // return response.blob(); // Consumes the body as a Blob (binary data)
        // return response.formData(); // Consumes the body as FormData
        // return response.arrayBuffer(); // Consumes the body as an ArrayBuffer
      })
      .then(data => {
        console.log('Processed Body Data (JSON):', data);
        // Use the data (e.g., display title: data.title)
      })
      .catch(error => {
        // Handles network errors (e.g., DNS resolution failure, server unreachable)
        // AND errors thrown manually (like the HTTP status check above)
        console.error('Fetch Operation Failed:', error);
      });

    Important: The promise returned by fetch() only rejects on network errors, not on HTTP error statuses (like 4xx or 5xx). You **must** check response.ok or response.status manually.

    Making POST, PUT, DELETE Requests

    Use the second argument of fetch(), an options object, to configure the request method, headers, and body.

    POST Request (Sending JSON)
    const newPost = {
      title: 'My New Post',
      body: 'This is the content of the post.',
      userId: 5,
    };
    
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        // Tells the server we are sending JSON
        'Content-Type': 'application/json; charset=UTF-8',
        // Example Authorization header
        // 'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
      },
      // Convert the JavaScript object to a JSON string for the body
      body: JSON.stringify(newPost),
    })
    .then(response => {
        if (!response.ok) {
            // Maybe read error details from response body if server sends them
            // return response.json().then(errData => { throw new Error(...) });
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json(); // Often, POST responses return the created object with an ID
    })
    .then(createdPost => {
      console.log('POST Success:', createdPost);
    })
    .catch(error => {
      console.error('POST Error:', error);
    });
    POST Request (Sending FormData)

    Useful for sending form data, including file uploads. The browser automatically sets the correct Content-Type (multipart/form-data).

    const formData = new FormData();
    formData.append('username', 'testuser');
    formData.append('email', 'test@example.com');
    // Example: Append a file from an input element
    // const fileInput = document.getElementById('file-input');
    // if (fileInput.files.length > 0) {
    //     formData.append('profilePic', fileInput.files[0], 'avatar.jpg');
    // }
    
    fetch('/api/users', {
      method: 'POST',
      // NO 'Content-Type' header needed here - browser sets it with boundary
      body: formData, 
    })
    .then(response => response.json())
    .then(result => console.log('FormData POST Success:', result))
    .catch(error => console.error('FormData POST Error:', error));
    PUT Request (Updating Data)
    const updatedData = { title: 'Updated Title', body: 'Updated content.' };
    
    fetch('https://jsonplaceholder.typicode.com/posts/1', { // Specify the ID of the resource to update
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updatedData),
    })
    .then(response => response.json())
    .then(data => console.log('PUT Success:', data))
    .catch(error => console.error('PUT Error:', error));
    DELETE Request
    fetch('https://jsonplaceholder.typicode.com/posts/1', { // Specify the ID to delete
      method: 'DELETE',
    })
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      // DELETE often returns no body or an empty body, status code 200 or 204
      console.log('DELETE Success, Status:', response.status);
      // return response.json(); // Only if the server sends a JSON confirmation
    })
    .catch(error => {
      console.error('DELETE Error:', error);
    });

    Using Request and Headers Objects

    For more complex scenarios or reusability, you can create explicit Request and Headers objects.

    // Create Headers
    const myHeaders = new Headers();
    myHeaders.append('Content-Type', 'application/json');
    myHeaders.append('X-Custom-Header', 'MyValue');
    
    // Create Request object
    const requestOptions = {
        method: 'GET',
        headers: myHeaders,
        mode: 'cors', // Handling Cross-Origin Requests
        cache: 'no-cache', // Control caching behavior
        // Other options: redirect, referrer, integrity, etc.
    };
    const myRequest = new Request('https://jsonplaceholder.typicode.com/users/2', requestOptions);
    
    // Use the Request object with fetch
    fetch(myRequest)
        .then(response => response.json())
        .then(data => console.log('Fetched using Request object:', data))
        .catch(error => console.error('Error with Request object:', error));
    

    Aborting Fetch Requests

    Use the AbortController API to cancel an ongoing fetch request, useful for scenarios like user cancelling an action or implementing timeouts.

    // Create an AbortController
    const controller = new AbortController();
    const signal = controller.signal; // Get the signal object
    
    // Set a timeout to abort the request after 3 seconds
    const timeoutId = setTimeout(() => {
        console.log('Aborting fetch due to timeout...');
        controller.abort(); 
    }, 3000);
    
    console.log('Starting fetch with abort signal...');
    fetch('https://httpbin.org/delay/5', { // This endpoint delays response for 5 seconds
      signal: signal // Pass the signal to the fetch options
    })
    .then(response => {
        clearTimeout(timeoutId); // Clear the timeout if fetch completes in time
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.text(); 
    })
    .then(data => {
        console.log('Fetch completed successfully (within timeout):', data);
    })
    .catch(error => {
        clearTimeout(timeoutId); // Clear timeout in case of other errors
        if (error.name === 'AbortError') {
            console.error('Fetch aborted:', error.message);
        } else {
            console.error('Fetch error (not abort):', error);
        }
    });
    
    // To manually abort (e.g., on a button click):
    // document.getElementById('cancelBtn').addEventListener('click', () => controller.abort());
    

    Using Async/Await

    Async/await provides a more synchronous-looking way to handle the promises involved in fetching data.

    async function fetchUserData(userId) {
        try {
            const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
            
            if (!response.ok) {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }
            
            const userData = await response.json();
            console.log('User data (async/await):', userData);
            return userData;
            
        } catch (error) {
            console.error('Failed to fetch user data:', error);
            // Optionally re-throw or return a specific error state
            return null; 
        }
    }
    
    // Call the async function
    fetchUserData(3);
    fetchUserData(999); // Example of handling a 404 error

    3. Web Storage API

    Provides simple mechanisms for web pages to store key-value data locally within the user's browser, persisting across page loads. It consists of two main objects: localStorage and sessionStorage.

    localStorage vs. sessionStorage

    Feature localStorage sessionStorage
    Persistence Persists until explicitly deleted (by user or code). Data remains after browser closes and reopens. Persists only for the duration of the page session (until the tab/window is closed).
    Scope Scoped to the document's origin (protocol + host + port). Accessible from any script from the same origin. Scoped to the document's origin AND the specific browser tab/window instance. Data in one tab is not accessible from another tab, even with the same origin.
    Storage Limit Typically 5-10MB per origin (browser-dependent). Typically 5-10MB per origin (browser-dependent).
    Availability Available in all windows/tabs from the same origin. Isolated to the specific tab/window that created it.

    Core Methods (Common to both)

    Both localStorage and sessionStorage share the same API methods.

    // Using localStorage for examples (same methods apply to sessionStorage)
    
    // 1. setItem(key, value): Store a key-value pair. Both must be strings.
    localStorage.setItem('username', 'Alice');
    localStorage.setItem('theme', 'dark');
    localStorage.setItem('loginTimestamp', new Date().toISOString());
    
    // 2. getItem(key): Retrieve the value associated with a key (returns string or null).
    const username = localStorage.getItem('username');
    console.log('Retrieved username:', username); // Output: Alice
    const nonExistent = localStorage.getItem('nonExistentKey');
    console.log('Non-existent key:', nonExistent); // Output: null
    
    // 3. removeItem(key): Remove a specific key-value pair.
    localStorage.removeItem('theme');
    console.log('Theme after removal:', localStorage.getItem('theme')); // Output: null
    
    // 4. clear(): Remove ALL key-value pairs for the current origin.
    // localStorage.clear(); // Use with caution!
    // console.log('Username after clear:', localStorage.getItem('username')); // Output: null (if clear() was run)
    
    // 5. length: Get the number of stored items.
    console.log('Number of items in localStorage:', localStorage.length);
    
    // 6. key(index): Get the key at a specific numerical index (order is not guaranteed).
    if (localStorage.length > 0) {
        const firstKey = localStorage.key(0);
        console.log('Key at index 0:', firstKey);
        console.log('Value for first key:', localStorage.getItem(firstKey));
    }
    
    // Alternative Access (like an object - less recommended due to potential conflicts)
    // localStorage.username = 'Bob'; // Equivalent to setItem
    // console.log(localStorage['username']); // Equivalent to getItem
    // delete localStorage.username; // Equivalent to removeItem

    Storing Non-String Data (Objects, Arrays)

    Web Storage only stores strings. To store complex data like objects or arrays, you must serialize them (e.g., using JSON.stringify) before storing and deserialize (JSON.parse) upon retrieval.

    const userSettings = {
        notifications: true,
        fontSize: 14,
        tags: ['javascript', 'webdev']
    };
    
    // Store the object
    try {
        localStorage.setItem('userSettings', JSON.stringify(userSettings));
        console.log('Stored user settings object.');
    } catch (e) {
        console.error('Error stringifying user settings:', e);
    }
    
    // Retrieve and parse the object
    let retrievedSettings = null;
    const settingsString = localStorage.getItem('userSettings');
    if (settingsString) {
        try {
            retrievedSettings = JSON.parse(settingsString);
            console.log('Retrieved user settings:', retrievedSettings);
            console.log('Retrieved font size:', retrievedSettings.fontSize); // Access properties
        } catch (e) {
            console.error('Error parsing stored settings:', e);
            // Handle potential corruption of stored data
        }
    }
    

    Use Cases

    • User Preferences: Storing theme choices, font sizes, layout settings (localStorage).
    • Offline Data Cache: Caching non-critical application data retrieved from the server (localStorage).
    • Session State: Keeping track of temporary states within a single user session, like items in a shopping cart before login (sessionStorage).
    • Drafts: Saving user input in forms temporarily in case of accidental navigation (sessionStorage or localStorage).
    • Feature Flags: Storing simple flags to enable/disable features for testing (localStorage).

    Performance and Security Considerations

    • Synchronous Operations: All Web Storage operations (setItem, getItem, etc.) are synchronous. Heavy usage, especially within loops, can block the main thread and impact UI responsiveness. Use judiciously.
    • Limited Size: Typically 5-10MB per origin. Not suitable for very large datasets. Check for potential errors if the quota is exceeded (usually throws a QuotaExceededError).
    • String-Only: Requires serialization/deserialization for complex data.
    • Security Risk (XSS): Data stored in Web Storage is accessible via JavaScript from the same origin. If your site is vulnerable to Cross-Site Scripting (XSS), malicious scripts could steal or modify this data. Do NOT store sensitive information (like passwords, session tokens, personal data) in Web Storage. Use secure, HTTP-only cookies or server-side sessions for sensitive data.
    Warning: Due to its synchronous nature and potential security risks, prefer IndexedDB for storing large amounts of structured data or data requiring more complex querying. Use secure cookies for session management and sensitive tokens.

    The `storage` Event

    Browsers fire a storage event on the window object in other open tabs/windows from the same origin whenever localStorage is modified (added, updated, or removed). It does *not* fire in the same tab that made the change.

    window.addEventListener('storage', (event) => {
      console.log('--- Storage Event Detected ---');
      console.log('Key changed:', event.key); // Key that was changed (null if clear() was called)
      console.log('Old value:', event.oldValue); // Previous value (null if new item or clear())
      console.log('New value:', event.newValue); // New value (null if removed or clear())
      console.log('URL of page that made change:', event.url);
      console.log('Storage Area:', event.storageArea); // The localStorage object itself
    
      // Example: Reload user settings if they changed in another tab
      if (event.key === 'userSettings') {
        const newSettings = event.newValue ? JSON.parse(event.newValue) : null;
        console.log('User settings updated in another tab. Applying changes...');
        // applyUserSettings(newSettings);
      }
    });
    
    // To test: Open two tabs from the same origin.
    // In Tab 1, execute: localStorage.setItem('testKey', 'hello ' + Date.now());
    // Observe the console logs in Tab 2.

    Note: The storage event is not fired for sessionStorage changes.

    4. Geolocation API

    Allows web applications to access the user's geographical location, typically with their permission. Access is provided through the navigator.geolocation object.

    Note: Accessing geolocation requires a secure context (HTTPS) in most modern browsers, except for localhost development.

    Checking for Support

    Before using the API, it's good practice to check if the browser supports it.

    if ('geolocation' in navigator) {
      console.log('Geolocation API is available.');
      // Proceed to use the API
    } else {
      console.error('Geolocation API is not supported by this browser.');
      // Provide fallback or inform the user
    }

    getCurrentPosition() - Getting a Single Location Fix

    Requests the user's current position once. It takes up to three arguments: a success callback, an optional error callback, and an optional options object.

    Syntax
    navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options);
    Success Callback

    Called when the location is successfully retrieved. It receives a GeolocationPosition object as an argument.

    function successCallback(position) {
      console.log('--- Geolocation Position ---');
      console.log('Timestamp:', new Date(position.timestamp).toLocaleString());
    
      const coords = position.coords;
      console.log('Latitude:', coords.latitude);
      console.log('Longitude:', coords.longitude);
      console.log('Accuracy (meters):', coords.accuracy);
    
      // Optional properties (may be null)
      console.log('Altitude (meters):', coords.altitude);
      console.log('Altitude Accuracy (meters):', coords.altitudeAccuracy);
      console.log('Heading (degrees from North):', coords.heading);
      console.log('Speed (meters/second):', coords.speed);
    
      // Example: Display on a map (requires a mapping library like Leaflet or Google Maps)
      // showMap(coords.latitude, coords.longitude);
    }
    
    Error Callback

    Called when an error occurs while trying to get the location. It receives a GeolocationPositionError object.

    function errorCallback(error) {
      console.error('--- Geolocation Error ---');
      console.error('Error Code:', error.code);
      console.error('Error Message:', error.message);
    
      switch (error.code) {
        case error.PERMISSION_DENIED:
          console.error("User denied the request for Geolocation.");
          // Guide user on how to enable permissions
          break;
        case error.POSITION_UNAVAILABLE:
          console.error("Location information is unavailable.");
          // Network issue or location sources disabled
          break;
        case error.TIMEOUT:
          console.error("The request to get user location timed out.");
          // Try again or inform user
          break;
        case error.UNKNOWN_ERROR:
          console.error("An unknown error occurred.");
          break;
      }
    }
    

    Error Codes:

    • 1: PERMISSION_DENIED - User blocked access.
    • 2: POSITION_UNAVAILABLE - Internal error, location sources disabled, or network issue.
    • 3: TIMEOUT - Failed to get location within the specified time.
    Options Object

    An optional object to fine-tune the location request.

    const options = {
      enableHighAccuracy: true, // Request the most accurate position possible (can consume more power/time). Default: false.
      timeout: 10000,           // Maximum time (ms) allowed to get position. Default: Infinity.
      maximumAge: 60000         // Maximum age (ms) of a cached position to accept. Default: 0 (always get fresh). Set to Infinity to only use cached.
    };
    
    // Putting it all together
    if ('geolocation' in navigator) {
      console.log('Requesting current position...');
      navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options);
    } else {
      console.error('Geolocation API not supported.');
    }
    

    watchPosition() - Tracking Location Changes

    Registers a handler function that will be called automatically each time the device's position changes. It also accepts success, error, and options arguments. It returns an ID that can be used to stop watching.

    let watchId = null;
    
    function startWatching() {
        if ('geolocation' in navigator) {
            if (watchId) {
                console.log('Already watching position.');
                return;
            }
            console.log('Starting position watch...');
            // Same success and error callbacks as getCurrentPosition can be used
            // Often uses lower accuracy or longer maximumAge for efficiency
            const watchOptions = { enableHighAccuracy: false, timeout: 15000, maximumAge: 30000 };
            watchId = navigator.geolocation.watchPosition(handlePositionUpdate, errorCallback, watchOptions);
            console.log('Watch ID:', watchId);
        } else {
            console.error('Geolocation API not supported.');
        }
    }
    
    function handlePositionUpdate(position) {
        console.log('--- Position Update ---');
        console.log('Time:', new Date(position.timestamp).toLocaleTimeString());
        console.log(`Lat: ${position.coords.latitude.toFixed(4)}, Lon: ${position.coords.longitude.toFixed(4)}`);
        // Update map marker, display coordinates, etc.
    }
    
    function stopWatching() {
        if (watchId !== null && navigator.geolocation) {
            console.log('Stopping position watch with ID:', watchId);
            navigator.geolocation.clearWatch(watchId);
            watchId = null;
        } else {
            console.log('Not currently watching position.');
        }
    }
    
    // Example Usage:
    // startWatching();
    // setTimeout(stopWatching, 60000); // Stop watching after 60 seconds
    

    Permissions, Privacy, and Security

    • User Consent is Mandatory: Browsers **must** ask for the user's permission before sharing location data. The request UI varies by browser.
    • HTTPS Required: As mentioned, secure contexts are generally required.
    • Transparency: Clearly explain to users *why* you need their location and *how* it will be used.
    • Least Privilege: Only ask for location when necessary. Don't request high accuracy if low accuracy suffices. Stop watching when the feature is no longer active.
    • Data Handling: Handle retrieved location data responsibly. Avoid logging or storing precise locations unless essential and with appropriate security measures.
    • Fallback Mechanisms: Provide alternative functionality if the user denies permission or if location services are unavailable.

    Use Cases

    • Displaying the user's location on a map.
    • Providing location-based recommendations (nearby restaurants, events).
    • Geotagging photos or posts.
    • Turn-by-turn navigation (requires continuous updates with watchPosition).
    • Weather applications showing local forecasts.
    • Fitness tracking applications.

    5. Canvas API

    Provides a powerful way to draw graphics, manipulate images, and create animations directly within an HTML <canvas> element using JavaScript. It's primarily used for 2D graphics, although WebGL (accessed via canvas) enables 3D rendering.

    Setting Up the Canvas

    First, add a <canvas> element to your HTML, giving it an ID and dimensions.

    <!-- Add fallback content for browsers that don't support canvas -->
    <canvas id="myCanvas" width="600" height="400" style="border: 1px solid black;">
      Your browser does not support the HTML canvas element.
    </canvas>

    Then, get a reference to the element and its 2D rendering context in JavaScript.

    const canvas = document.getElementById('myCanvas');
    
    // Check if canvas is supported and get the context
    if (canvas.getContext) {
      const ctx = canvas.getContext('2d'); 
      console.log('2D context obtained:', ctx);
      // All drawing operations happen on the 'ctx' object
      
      // --- Start Drawing Examples Here --- 
    
    } else {
      console.error('Canvas not supported or context could not be created.');
      // Handle lack of support
    }

    Basic Drawing: Rectangles

    Methods for drawing filled, outlined, or clearing rectangular areas.

    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
      
      // Set fill color
      ctx.fillStyle = 'rgb(200, 0, 0)'; // Red
      // Draw a filled rectangle: fillRect(x, y, width, height)
      ctx.fillRect(10, 10, 100, 50);
    
      // Set stroke (outline) color
      ctx.strokeStyle = 'rgba(0, 0, 200, 0.5)'; // Semi-transparent blue
      // Draw a rectangular outline: strokeRect(x, y, width, height)
      ctx.strokeRect(120, 10, 100, 50);
    
      // Clear a rectangular area, making it transparent: clearRect(x, y, width, height)
      ctx.clearRect(30, 20, 40, 30); 
    }

    Drawing Paths (Lines, Shapes)

    Paths are used to create complex shapes, lines, curves, etc.

    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
    
      // --- Draw a Triangle --- 
      ctx.beginPath(); // Start a new path
      ctx.moveTo(75, 100); // Move pen to starting point (x, y)
      ctx.lineTo(125, 100); // Draw line to (x, y)
      ctx.lineTo(100, 150); // Draw line to next point
      ctx.closePath(); // Close the path (connects last point to first)
      
      // Style and draw the path
      ctx.lineWidth = 3;
      ctx.strokeStyle = '#33cc33'; // Green
      ctx.stroke(); // Draw the outline
    
      // --- Draw a Filled Shape (Pentagon) --- 
      ctx.beginPath();
      ctx.moveTo(200, 125);
      ctx.lineTo(240, 100);
      ctx.lineTo(240, 150);
      ctx.lineTo(200, 175);
      ctx.lineTo(160, 150);
      ctx.lineTo(160, 100);
      ctx.closePath();
    
      ctx.fillStyle = '#cc33cc'; // Purple
      ctx.fill(); // Fill the closed path
    }

    Drawing Arcs and Circles

    The arc() method creates arcs or circles.

    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
    
      // arc(x, y, radius, startAngle, endAngle, anticlockwise?)
      // Angles are in radians (Math.PI radians = 180 degrees)
      
      // Full circle
      ctx.beginPath();
      ctx.arc(300, 125, 40, 0, Math.PI * 2); // Full circle
      ctx.fillStyle = 'orange';
      ctx.fill();
    
      // Pac-man shape (arc segment)
      ctx.beginPath();
      ctx.moveTo(400, 125); // Move to center
      ctx.arc(400, 125, 30, 0.2 * Math.PI, 1.8 * Math.PI, false); // Draw arc
      ctx.closePath(); // Line back to center
      ctx.fillStyle = 'yellow';
      ctx.fill();
    }

    Drawing Text

    Add text directly to the canvas.

    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
      
      ctx.font = 'bold 24px Arial';
      ctx.fillStyle = 'navy';
      ctx.textAlign = 'center'; // 'left', 'right', 'center', 'start', 'end'
      ctx.textBaseline = 'middle'; // 'top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'
    
      // fillText(text, x, y, maxWidth?)
      ctx.fillText('Hello Canvas!', canvas.width / 2, 200);
    
      ctx.strokeStyle = 'red';
      ctx.lineWidth = 0.5;
      // strokeText(text, x, y, maxWidth?)
      ctx.strokeText('Outlined Text', canvas.width / 2, 240);
    }

    Styling

    Control the appearance of drawings.

    • Colors: fillStyle, strokeStyle (accepts CSS color strings: names, hex, rgb, rgba, hsl, hsla).
    • Line Styles: lineWidth (number), lineCap ('butt', 'round', 'square'), lineJoin ('round', 'bevel', 'miter'), miterLimit (number), setLineDash([segments]), lineDashOffset.
    • Gradients:
      • createLinearGradient(x0, y0, x1, y1)
      • createRadialGradient(x0, y0, r0, x1, y1, r1)
      • Gradients need color stops added: gradient.addColorStop(offset, color) (offset 0.0 to 1.0).
      • Assign the gradient object to fillStyle or strokeStyle.
    • Patterns: createPattern(image, repetition) (image can be <img>, <canvas>, <video>; repetition: 'repeat', 'repeat-x', 'repeat-y', 'no-repeat'). Assign the pattern object to fillStyle or strokeStyle.
    • Shadows: shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor.
    • Global Alpha: globalAlpha (0.0 to 1.0 for transparency).
    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
      
      // Linear Gradient Example
      const linGrad = ctx.createLinearGradient(0, 250, 0, 350);
      linGrad.addColorStop(0, 'blue');
      linGrad.addColorStop(0.5, 'cyan');
      linGrad.addColorStop(1, 'white');
      ctx.fillStyle = linGrad;
      ctx.fillRect(10, 250, 150, 100);
    
      ctx.strokeStyle = 'red';
      ctx.lineWidth = 0.5;
      // strokeText(text, x, y, maxWidth?)
      ctx.strokeText('Outlined Text', canvas.width / 2, 240);
    }

    Transformations

    Modify the coordinate system to move, rotate, or scale drawings without changing the drawing commands themselves.

    • translate(x, y): Moves the origin (0,0 point) of the canvas.
    • rotate(angle): Rotates the canvas around the current origin (angle in radians).
    • scale(x, y): Scales the drawing units (x for horizontal, y for vertical).
    • transform(a, b, c, d, e, f): Applies a custom transformation matrix.
    • setTransform(a, b, c, d, e, f): Resets the current transform and applies a new one.
    • resetTransform(): Resets the transform to the identity matrix (no transformation).

    Important: Transformations stack. Use save() and restore() to isolate transformations.

    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
    
      ctx.fillStyle = 'gray';
      ctx.fillRect(350, 260, 80, 40); // Original rectangle
    
      // Translate, Rotate, and draw again
      ctx.translate(390, 280); // Move origin to the center of the rectangle (350 + 80/2, 260 + 40/2)
      ctx.rotate(Math.PI / 6); // Rotate 30 degrees (PI/6 radians)
      ctx.fillStyle = 'lightblue';
      // Draw relative to the NEW origin (-width/2, -height/2)
      ctx.fillRect(-40, -20, 80, 40);
      
      // Reset transform for subsequent drawings
      ctx.resetTransform(); 
    }

    Drawing Images

    Draw existing images (from <img> tags, other canvases, or created dynamically) onto the canvas.

    if (canvas.getContext) {
      const ctx = canvas.getContext('2d');
      const img = new Image(); 
      img.onload = function() {
        // Draw image once loaded: drawImage(image, dx, dy)
        ctx.drawImage(img, 450, 50);
    
        // Draw scaled image: drawImage(image, dx, dy, dWidth, dHeight)
        ctx.drawImage(img, 450, 150, img.width / 2, img.height / 2);
    
        // Draw portion (sprite): drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
        // (Draws area from sx,sy with sWidth,sHeight of source onto dx,dy with dWidth,dHeight on canvas)
        ctx.drawImage(img, 10, 10, 30, 30, 450, 250, 60, 60);
      };
      img.onerror = function() {
          console.error('Failed to load image.');
      };
      img.src = 'path/to/your/image.png'; // Ensure image is loaded before drawing!
    }

    Pixel Manipulation

    Access and modify the raw pixel data of the canvas.

    • createImageData(width, height) or createImageData(imageData): Creates a new, blank ImageData object.
    • getImageData(sx, sy, sw, sh): Returns an ImageData object representing the pixel data for a specified rectangle.
    • putImageData(imageData, dx, dy, dirtyX?, dirtyY?, dirtyWidth?, dirtyHeight?): Puts pixel data from an ImageData object back onto the canvas.

    The ImageData object has width, height, and a data property (a Uint8ClampedArray). The data array contains RGBA values for each pixel sequentially (R, G, B, A, R, G, B, A,...).

    function makeGrayscale() {
        if (canvas.getContext) {
            const ctx = canvas.getContext('2d');
            // Ensure something is drawn first
            ctx.fillStyle = 'magenta'; 
            ctx.fillRect(10, 300, 100, 50);
            
            try {
                const imageData = ctx.getImageData(10, 300, 100, 50);
                const data = imageData.data;
                for (let i = 0; i < data.length; i += 4) {
                    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
                    data[i] = avg; // Red
                    data[i + 1] = avg; // Green
                    data[i + 2] = avg; // Blue
                    // data[i + 3] is Alpha - leave unchanged
                }
                ctx.putImageData(imageData, 120, 300); // Draw modified data next to original
                console.log('Grayscale filter applied.');
            } catch (e) {
                console.error('Pixel manipulation error (maybe CORS issue if image is external?):', e);
            }
        }
    }
    // makeGrayscale(); // Call this after drawing something

    Note: getImageData is subject to cross-origin security restrictions if external images are drawn onto the canvas without appropriate CORS headers.

    Saving and Restoring State

    save() pushes the current drawing state (styles, transformations, clipping regions) onto a stack. restore() pops the last saved state off the stack, restoring the context to how it was before save() was called. Essential for isolating drawing operations, especially with transformations.

    if (canvas.getContext) {
        const ctx = canvas.getContext('2d');
    
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, 50, 50); // Black square
    
        ctx.save(); // Save current state (black fill)
    
        ctx.fillStyle = 'red';
        ctx.translate(70, 0);
        ctx.fillRect(0, 0, 50, 50); // Red translated square
        
        ctx.save(); // Save current state (red fill, translated)
        ctx.fillStyle = 'blue';
        ctx.scale(0.5, 0.5);
        ctx.fillRect(0, 60, 50, 50); // Blue, translated, scaled square
        ctx.restore(); // Restore to: red fill, translated
    
        ctx.fillRect(0, 120, 50, 50); // Red translated square (state restored)
        ctx.restore(); // Restore to: black fill, no translation
    
        ctx.fillRect(140, 0, 50, 50); // Black square (state restored)
    }

    Animation

    Use window.requestAnimationFrame(callback) for smooth, efficient animations. The browser calls the callback function just before the next repaint.

    let x = 0;
    let direction = 1;
    
    function drawAnimationFrame() {
        if (canvas.getContext) {
            const ctx = canvas.getContext('2d');
            const canvasWidth = canvas.width;
            const canvasHeight = canvas.height; // Assuming fixed height for example area
            const boxSize = 30;
    
            // Clear the animation area (or entire canvas)
            ctx.clearRect(0, canvasHeight - 50, canvasWidth, 50);
    
            // Update position
            x += 2 * direction;
            if (x + boxSize > canvasWidth || x < 0) {
                direction *= -1; // Reverse direction at edges
            }
    
            // Draw the object
            ctx.fillStyle = 'green';
            ctx.fillRect(x, canvasHeight - 40, boxSize, boxSize);
            
            // Request the next frame
            requestAnimationFrame(drawAnimationFrame);
        }
    }
    
    // Start the animation loop
    // requestAnimationFrame(drawAnimationFrame); 
    console.log('Animation setup complete. Uncomment the line above to start.');
    

    Use Cases

    • Interactive Games (2D)
    • Data Visualization (Charts, Graphs)
    • Image Editing and Manipulation Tools
    • Creative Coding and Generative Art
    • Complex UI elements and Effects
    • Video Processing and Effects

    6. Web Audio API

    Provides a powerful and versatile system for controlling audio processing and synthesis directly in the browser. It allows developers to load, generate, manipulate, and analyze audio with high precision and low latency.

    The Audio Context

    The core of the Web Audio API is the AudioContext. It acts as a central controller for the audio graph.

    // Create an AudioContext instance
    // Needs user interaction (click, etc.) to start/resume in most browsers
    let audioContext;
    try {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      console.log('AudioContext state:', audioContext.state);
      // Resume if suspended (requires user gesture)
      if (audioContext.state === 'suspended') {
        document.body.addEventListener('click', () => audioContext.resume(), { once: true });
      }
    } catch(e) {
      console.error('Web Audio API not supported:', e);
    }
    Autoplay Policy: Most browsers require user interaction (like a click) to start or resume an AudioContext.

    Audio Nodes and the Audio Graph

    Audio processing happens through a series of connected Audio Nodes. These nodes form a directed graph, starting from source nodes, passing through processing/effect nodes, and finally reaching the destination node (usually the speakers).

    • Source Nodes: Generate or provide audio data.
      • AudioBufferSourceNode: Plays audio data from an AudioBuffer (loaded file).
      • OscillatorNode: Generates a periodic waveform (sine, square, sawtooth, triangle).
      • MediaElementAudioSourceNode: Uses audio from an HTML <audio> or <video> element.
      • MediaStreamAudioSourceNode: Uses audio from a MediaStream (e.g., microphone input).
    • Processing/Effect Nodes: Modify the audio signal.
      • GainNode: Controls volume.
      • BiquadFilterNode: Implements filters (lowpass, highpass, etc.).
      • DelayNode: Introduces a delay.
      • StereoPannerNode: Positions sound in stereo space.
      • (and many more...)
    • Destination Node: The final output.
      • audioContext.destination: Represents the default audio output (speakers).
      • MediaStreamAudioDestinationNode: Routes audio to a MediaStream.

    Nodes are connected using the connect() method: sourceNode.connect(processingNode); processingNode.connect(audioContext.destination);

    Loading and Playing Audio Files

    Use fetch to get the audio file data as an ArrayBuffer, then audioContext.decodeAudioData() to convert it into an AudioBuffer that can be played by an AudioBufferSourceNode.

    let audioBuffer = null;
    
    async function loadAudio(url) {
      if (!audioContext) {
          console.error('AudioContext not available.');
          return null;
      }
      try {
          console.log(`Fetching audio: ${url}...`);
          const response = await fetch(url);
          if (!response.ok) {
              throw new Error(`HTTP error! status: ${response.status}`);
          }
          const arrayBuffer = await response.arrayBuffer();
          console.log('Decoding audio data...');
          // Use Promise-based decodeAudioData
          audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
          console.log('Audio loaded and decoded successfully!');
          return audioBuffer;
      } catch (error) {
          console.error('Error loading or decoding audio:', error);
          audioBuffer = null;
          return null;
      }
    }
    
    function playAudio() {
        if (!audioContext || !audioBuffer) {
            console.error('AudioContext not ready or audio buffer not loaded.');
            if (audioContext && audioContext.state === 'suspended') {
                 console.log('AudioContext suspended. Click page to resume.');
            }
            return;
        }
        
        // Ensure context is running
        if (audioContext.state === 'running') {
            // Create a source node each time you want to play
            const source = audioContext.createBufferSource();
            source.buffer = audioBuffer; // Assign the loaded buffer
            
            // Connect source directly to destination (speakers)
            source.connect(audioContext.destination);
            
            console.log('Starting playback...');
            source.start(0); // Start playing immediately (offset 0)
            
            // Optional: Clean up the source node after it finishes playing
            source.onended = () => {
                console.log('Playback finished.');
                source.disconnect(); // Disconnect node
            };
        } else {
            console.log('AudioContext not running. Click page to resume.');
        }
    }
    
    // Example Usage (load first, then play, likely triggered by user action)
    // loadAudio('path/to/your/sound.wav').then(() => {
    //    document.getElementById('playButton').onclick = playAudio; 
    // });
    console.log('Audio loading/playback functions defined. Call loadAudio() and attach playAudio() to a button.');
    

    Generating Sounds with OscillatorNode

    Create simple tones programmatically without loading external files.

    function playTone(frequency = 440, duration = 1, type = 'sine') {
        if (!audioContext || audioContext.state !== 'running') {
            console.error('AudioContext not ready or running.');
            return;
        }
    
        // Create Oscillator node
        const oscillator = audioContext.createOscillator();
        oscillator.type = type; // 'sine', 'square', 'sawtooth', 'triangle', 'custom'
        oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); // Set frequency in Hz (A4 note)
    
        // Optional: Create a Gain node to control volume and prevent clipping
        const gainNode = audioContext.createGain();
        gainNode.gain.setValueAtTime(0.5, audioContext.currentTime); // Start at half volume
        // Fade out smoothly at the end
        gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration - 0.05);
    
        // Connect oscillator -> gain -> destination
        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);
    
        console.log(`Playing ${type} tone at ${frequency} Hz for ${duration}s...`);
        oscillator.start(audioContext.currentTime); // Start now
        oscillator.stop(audioContext.currentTime + duration); // Stop after 'duration' seconds
    
        oscillator.onended = () => {
            console.log('Tone finished.');
            oscillator.disconnect();
            gainNode.disconnect();
        };
    }
    
    // Example Usage (likely triggered by user action):
    // document.getElementById('playToneButton').onclick = () => playTone(440, 0.5, 'sine');
    // document.getElementById('playSquareButton').onclick = () => playTone(220, 0.5, 'square');
    console.log('playTone function defined. Attach to buttons.');
    

    Applying Simple Effects (Gain & Filter)

    Modify the audio signal by inserting processing nodes into the graph.

    function playAudioWithEffects() {
        if (!audioContext || !audioBuffer || audioContext.state !== 'running') {
            console.error('AudioContext/buffer not ready or context not running.');
            return;
        }
    
        const source = audioContext.createBufferSource();
        source.buffer = audioBuffer;
    
        // Create Gain node for volume control
        const gainNode = audioContext.createGain();
        gainNode.gain.value = 0.8; // Set volume (0 to 1+)
    
        // Create BiquadFilter node (Low-pass filter example)
        const filterNode = audioContext.createBiquadFilter();
        filterNode.type = 'lowpass'; // 'lowpass', 'highpass', 'bandpass', 'notch', etc.
        filterNode.frequency.value = 1000; // Cutoff frequency in Hz (lower value = more muffled)
        // filterNode.Q.value = 1; // Quality factor (resonance)
    
        // Connect the graph: source -> filter -> gain -> destination
        source.connect(filterNode);
        filterNode.connect(gainNode);
        gainNode.connect(audioContext.destination);
    
        console.log('Starting playback with filter and gain...');
        source.start(0);
    
        source.onended = () => {
            console.log('Playback with effects finished.');
            source.disconnect();
            filterNode.disconnect();
            gainNode.disconnect();
        };
    }
    
    // Example Usage (assuming audio loaded):
    // document.getElementById('playEffectsButton').onclick = playAudioWithEffects;
    console.log('playAudioWithEffects function defined. Attach to button.');
    

    Use Cases

    • Games: Sound effects, background music, spatial audio.
    • Interactive Applications: UI sound feedback, audio visualizations.
    • Music Production/Playback: Synthesizers, sequencers, effects processors, sophisticated audio players.
    • Accessibility: Auditory feedback for visually impaired users.
    • Communication: Real-time audio processing in WebRTC applications.

    Key Considerations

    • Complexity: Can be significantly more complex than the basic HTML <audio> element for simple playback.
    • Performance: Designed for high performance, but complex audio graphs can still consume CPU resources.
    • User Interaction Required: Remember the autoplay policy requires user interaction to start/resume the AudioContext.
    • Garbage Collection: Disconnect nodes when they are no longer needed to allow them to be garbage collected, especially for long-running applications.
    {{ ... }}