Content Scripts

Enhancing SEQTA Learn pages with content scripts

Content Scripts Overview

Content scripts run in the context of web pages and can access and modify the DOM of SEQTA Learn pages.

Content Script Architecture

Content scripts are injected into SEQTA Learn pages to:

  • Enhance the UI
  • Add new features
  • Modify existing behavior
  • Listen for page events

Basic Content Script

Simple Enhancement

// src/content/main.ts
(function() {
  'use strict';
  
  // Wait for page to load
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
  
  function init() {
    // Enhance SEQTA page
    enhancePage();
  }
  
  function enhancePage() {
    // Add custom styles
    injectStyles();
    
    // Enhance UI elements
    enhanceUI();
    
    // Add new features
    addFeatures();
  }
})();

DOM Manipulation

Safe DOM Manipulation

// Wait for element to appear
function waitForElement(selector: string): Promise<Element> {
  return new Promise((resolve) => {
    const element = document.querySelector(selector);
    if (element) {
      resolve(element);
      return;
    }
    
    const observer = new MutationObserver(() => {
      const element = document.querySelector(selector);
      if (element) {
        observer.disconnect();
        resolve(element);
      }
    });
    
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  });
}

// Usage
const element = await waitForElement('.seqta-content');
enhanceElement(element);

Injecting Styles

function injectStyles(css: string) {
  const style = document.createElement('style');
  style.textContent = css;
  document.head.appendChild(style);
}

// Usage
injectStyles(`
  .enhanced-element {
    background: var(--accent-color);
    padding: 1rem;
  }
`);

Message Passing

Sending Messages to Background

// Request settings
chrome.runtime.sendMessage(
  { action: 'getSettings' },
  (response) => {
    if (response?.settings) {
      applySettings(response.settings);
    }
  }
);

// Notify background of page action
chrome.runtime.sendMessage({
  action: 'pageLoaded',
  url: window.location.href
});

Receiving Messages from Background

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'toggleFeature') {
    const enabled = message.enabled;
    toggleFeature(message.featureId, enabled);
    sendResponse({ success: true });
  }
  
  if (message.action === 'updateSettings') {
    applySettings(message.settings);
    sendResponse({ success: true });
  }
  
  return true; // Keep channel open for async response
});

Feature Modules

Modular Feature System

// src/content/features/dark-mode.ts
export class DarkModeFeature {
  private enabled = false;
  
  init() {
    this.enabled = true;
    this.applyDarkMode();
  }
  
  destroy() {
    this.enabled = false;
    this.removeDarkMode();
  }
  
  private applyDarkMode() {
    document.documentElement.classList.add('betterseqta-dark');
  }
  
  private removeDarkMode() {
    document.documentElement.classList.remove('betterseqta-dark');
  }
}

// src/content/main.ts
import { DarkModeFeature } from './features/dark-mode';

const features = new Map<string, Feature>();

function initFeatures() {
  // Load enabled features from settings
  chrome.storage.sync.get(['features'], (result) => {
    const enabledFeatures = result.features || [];
    
    enabledFeatures.forEach(featureId => {
      loadFeature(featureId);
    });
  });
}

async function loadFeature(featureId: string) {
  switch (featureId) {
    case 'darkMode':
      const darkMode = new DarkModeFeature();
      darkMode.init();
      features.set('darkMode', darkMode);
      break;
    // ... other features
  }
}

Event Handling

Page Events

// Listen for navigation
let currentUrl = window.location.href;

const observer = new MutationObserver(() => {
  if (window.location.href !== currentUrl) {
    currentUrl = window.location.href;
    handlePageChange();
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

function handlePageChange() {
  // Re-initialize features for new page
  initFeatures();
}

User Interactions

// Enhance buttons
document.addEventListener('click', (event) => {
  const target = event.target as HTMLElement;
  
  if (target.matches('.seqta-button')) {
    enhanceButton(target);
  }
}, true); // Use capture phase

Storage Access

Reading Settings

chrome.storage.sync.get(['theme', 'features'], (result) => {
  const theme = result.theme || 'light';
  const features = result.features || [];
  
  applyTheme(theme);
  enableFeatures(features);
});

Watching for Changes

chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'sync') {
    if (changes.theme) {
      applyTheme(changes.theme.newValue);
    }
    
    if (changes.features) {
      updateFeatures(changes.features.newValue);
    }
  }
});

Best Practices

1. Avoid Conflicts

// ✅ Good - Use unique class names
element.classList.add('betterseqta-enhanced');

// ❌ Avoid - Generic names
element.classList.add('enhanced');

2. Clean Up

// Clean up when page unloads
window.addEventListener('beforeunload', () => {
  features.forEach(feature => feature.destroy());
  observer.disconnect();
});

3. Error Handling

try {
  enhancePage();
} catch (error) {
  console.error('BetterSEQTA-Plus: Error enhancing page', error);
  // Report to background
  chrome.runtime.sendMessage({
    action: 'error',
    error: error.message
  });
}

4. Performance

// ✅ Good - Debounce expensive operations
const debouncedEnhance = debounce(() => {
  enhanceElements();
}, 100);

// ❌ Avoid - Run on every mutation
observer.observe(document.body, {
  childList: true,
  subtree: true,
  callback: enhanceElements // Too frequent!
});

Next Steps