Chapter 06 Popup Interface Development

Haiyue
37min

Chapter 6: Popup Interface Development

Learning Objectives

  1. Understand the purpose and characteristics of Chrome extension Popups
  2. Master HTML, CSS, and JavaScript development for Popup pages
  3. Learn communication between Popup and Background Script
  4. Implement responsive design and user experience optimization

6.1 Popup Overview

Popup is the small window interface displayed when users click the extension icon, serving as the primary entry point for user interaction with the extension.

6.1.1 Popup Characteristics

🔄 正在渲染 Mermaid 图表...

tip Core Features

  • Small window interface, typically 320-800px wide
  • Automatically closes when clicking outside the extension icon area
  • Reloads every time it opens
  • Can communicate with Background Script
  • Supports full web technology stack

6.1.2 Manifest Configuration

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "action": {
    "default_popup": "popup.html",
    "default_title": "My Extension",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "permissions": [
    "storage",
    "tabs"
  ]
}

6.2 HTML Structure Design

6.2.1 Basic HTML Template

<!-- popup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Extension</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <!-- Header Area -->
    <header class="header">
      <div class="logo">
        <img src="icons/icon48.png" alt="Logo">
        <h1>My Extension</h1>
      </div>
      <div class="status-indicator" id="statusIndicator">
        <span class="status-dot"></span>
        <span class="status-text">Connected</span>
      </div>
    </header>

    <!-- Main Content Area -->
    <main class="main-content">
      <!-- Action Buttons Group -->
      <section class="action-buttons">
        <button class="btn btn-primary" id="toggleFeature">
          <span class="btn-icon">🔧</span>
          <span class="btn-text">Enable Feature</span>
        </button>
        <button class="btn btn-secondary" id="analyzeePage">
          <span class="btn-icon">📊</span>
          <span class="btn-text">Analyze Page</span>
        </button>
      </section>

      <!-- Information Display Area -->
      <section class="info-section">
        <div class="info-card" id="pageInfo">
          <h3>Page Information</h3>
          <div class="info-content">
            <p class="url-info" id="urlInfo">Waiting to fetch...</p>
            <p class="title-info" id="titleInfo">Waiting to fetch...</p>
          </div>
        </div>

        <div class="info-card" id="statsInfo">
          <h3>Usage Statistics</h3>
          <div class="stats-grid">
            <div class="stat-item">
              <span class="stat-value" id="clickCount">0</span>
              <span class="stat-label">Click Count</span>
            </div>
            <div class="stat-item">
              <span class="stat-value" id="activeTime">0</span>
              <span class="stat-label">Active Time</span>
            </div>
          </div>
        </div>
      </section>

      <!-- Settings Area -->
      <section class="settings-section">
        <div class="setting-item">
          <label class="setting-label">
            <input type="checkbox" id="autoMode" class="setting-checkbox">
            <span class="checkmark"></span>
            Auto Mode
          </label>
        </div>
        <div class="setting-item">
          <label class="setting-label">Theme</label>
          <select id="themeSelect" class="setting-select">
            <option value="light">Light</option>
            <option value="dark">Dark</option>
            <option value="auto">Follow System</option>
          </select>
        </div>
      </section>
    </main>

    <!-- Footer Area -->
    <footer class="footer">
      <div class="footer-buttons">
        <button class="btn btn-small" id="settingsBtn">Settings</button>
        <button class="btn btn-small" id="helpBtn">Help</button>
      </div>
    </footer>
  </div>

  <!-- Loading Indicator -->
  <div class="loading-overlay" id="loadingOverlay" style="display: none;">
    <div class="spinner"></div>
    <p>Processing...</p>
  </div>

  <script src="popup.js"></script>
</body>
</html>

6.2.2 HTML Generator Configuration Example

// popup-generator.js - Chrome Extension Popup HTML Generator
class PopupGenerator {
  constructor() {
    this.title = "Chrome Extension";
    this.width = 400;
    this.height = 600;
    this.theme = "light";
    this.components = [];
  }

  setConfig(title, width = 400, height = 600, theme = "light") {
    this.title = title;
    this.width = width;
    this.height = height;
    this.theme = theme;
  }

  addHeader(logoPath, statusText = "Connected") {
    this.components.push({
      type: "header",
      logo: logoPath,
      title: this.title,
      status: statusText
    });
  }

  addButtonGroup(buttons) {
    this.components.push({
      type: "button_group",
      buttons: buttons
    });
  }

  addInfoCard(title, contentType = "text", contentId = "") {
    this.components.push({
      type: "info_card",
      title: title,
      contentType: contentType,
      contentId: contentId
    });
  }

  addSettingsSection(settings) {
    this.components.push({
      type: "settings",
      settings: settings
    });
  }

  generateHTML() {
    const componentsHTML = this._generateComponents();
    return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${this.title}</title>
    <link rel="stylesheet" href="popup.css">
    <style>
        body { width: ${this.width}px; min-height: ${this.height}px; }
    </style>
</head>
<body class="${this.theme}-theme">
    <div class="container">
        ${componentsHTML}
    </div>
    <script src="popup.js"></script>
</body>
</html>`;
  }

  _generateComponents() {
    return this.components.map(component => {
      switch (component.type) {
        case "header":
          return this._generateHeader(component);
        case "button_group":
          return this._generateButtonGroup(component);
        case "info_card":
          return this._generateInfoCard(component);
        case "settings":
          return this._generateSettings(component);
        default:
          return '';
      }
    }).join('\n');
  }

  _generateHeader(header) {
    return `
        <header class="header">
            <div class="logo">
                <img src="${header.logo}" alt="Logo">
                <h1>${header.title}</h1>
            </div>
            <div class="status-indicator">
                <span class="status-dot"></span>
                <span class="status-text">${header.status}</span>
            </div>
        </header>`;
  }

  _generateButtonGroup(buttonGroup) {
    const buttonsHTML = buttonGroup.buttons.map(button => `
            <button class="btn btn-${button.style || 'primary'}" id="${button.id}">
                <span class="btn-icon">${button.icon || '🔧'}</span>
                <span class="btn-text">${button.text}</span>
            </button>`).join('');

    return `
        <section class="action-buttons">
            ${buttonsHTML}
        </section>`;
  }

  _generateInfoCard(infoCard) {
    return `
        <section class="info-section">
            <div class="info-card" id="${infoCard.contentId}">
                <h3>${infoCard.title}</h3>
                <div class="info-content">
                    <p class="info-text">Waiting to fetch...</p>
                </div>
            </div>
        </section>`;
  }

  _generateSettings(settingsSection) {
    const settingsHTML = settingsSection.settings.map(setting => {
      if (setting.type === "checkbox") {
        return `
                <div class="setting-item">
                    <label class="setting-label">
                        <input type="checkbox" id="${setting.id}" class="setting-checkbox">
                        <span class="checkmark"></span>
                        ${setting.label}
                    </label>
                </div>`;
      } else if (setting.type === "select") {
        const optionsHTML = setting.options.map(option =>
          `<option value="${option.value}">${option.text}</option>`
        ).join('');

        return `
                <div class="setting-item">
                    <label class="setting-label">${setting.label}</label>
                    <select id="${setting.id}" class="setting-select">
                        ${optionsHTML}
                    </select>
                </div>`;
      }
      return '';
    }).join('');

    return `
        <section class="settings-section">
            ${settingsHTML}
        </section>`;
  }
}

// Usage example
const generator = new PopupGenerator();
generator.setConfig("My Chrome Extension", 400, 600, "light");

// Add components
generator.addHeader("icons/icon48.png", "Connected");

generator.addButtonGroup([
  { id: "toggleFeature", text: "Enable Feature", icon: "🔧", style: "primary" },
  { id: "analyzePage", text: "Analyze Page", icon: "📊", style: "secondary" }
]);

generator.addInfoCard("Page Information", "text", "pageInfo");

generator.addSettingsSection([
  { type: "checkbox", id: "autoMode", label: "Auto Mode" },
  {
    type: "select",
    id: "themeSelect",
    label: "Theme",
    options: [
      { value: "light", text: "Light" },
      { value: "dark", text: "Dark" },
      { value: "auto", text: "Follow System" }
    ]
  }
]);

// Generate HTML
const htmlContent = generator.generateHTML();
console.log("Generated Popup HTML:");
console.log(htmlContent.substring(0, 500) + "...");  // Display first 500 characters

6.3 CSS Styling Design

6.3.1 Modern Styling

/* popup.css */
:root {
  --primary-color: #4285f4;
  --secondary-color: #34a853;
  --danger-color: #ea4335;
  --warning-color: #fbbc05;
  --text-primary: #202124;
  --text-secondary: #5f6368;
  --background-primary: #ffffff;
  --background-secondary: #f8f9fa;
  --border-color: #dadce0;
  --shadow-light: 0 1px 3px rgba(0, 0, 0, 0.1);
  --shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
  --border-radius: 8px;
  --transition: all 0.2s ease;
}

/* Dark theme */
.dark-theme {
  --text-primary: #e8eaed;
  --text-secondary: #9aa0a6;
  --background-primary: #202124;
  --background-secondary: #303134;
  --border-color: #5f6368;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 400px;
  min-height: 500px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-primary);
  background-color: var(--background-primary);
  overflow-x: hidden;
}

.container {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* Header styles */
.header {
  padding: 16px;
  border-bottom: 1px solid var(--border-color);
  background-color: var(--background-primary);
}

.logo {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.logo img {
  width: 24px;
  height: 24px;
  border-radius: 4px;
}

.logo h1 {
  font-size: 18px;
  font-weight: 500;
  color: var(--text-primary);
}

.status-indicator {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-secondary);
}

.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: var(--secondary-color);
}

/* Main content area */
.main-content {
  flex: 1;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* Button styles */
.action-buttons {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.btn {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border: none;
  border-radius: var(--border-radius);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: var(--transition);
  text-align: left;
}

.btn:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-medium);
}

.btn:active {
  transform: translateY(0);
}

.btn-primary {
  background-color: var(--primary-color);
  color: white;
}

.btn-primary:hover {
  background-color: #3367d6;
}

.btn-secondary {
  background-color: var(--background-secondary);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
}

.btn-secondary:hover {
  background-color: var(--border-color);
}

.btn-small {
  padding: 8px 12px;
  font-size: 12px;
}

.btn-icon {
  font-size: 16px;
}

/* Info cards */
.info-section {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.info-card {
  padding: 16px;
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  background-color: var(--background-secondary);
}

.info-card h3 {
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 8px;
  color: var(--text-primary);
}

.info-content {
  font-size: 12px;
  color: var(--text-secondary);
}

.url-info, .title-info {
  margin-bottom: 4px;
  word-break: break-word;
}

/* Stats grid */
.stats-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.stat-item {
  text-align: center;
}

.stat-value {
  display: block;
  font-size: 20px;
  font-weight: 600;
  color: var(--primary-color);
}

.stat-label {
  display: block;
  font-size: 11px;
  color: var(--text-secondary);
  margin-top: 2px;
}

/* Settings area */
.settings-section {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border-color);
}

.setting-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.setting-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-primary);
  cursor: pointer;
}

/* Custom checkbox */
.setting-checkbox {
  display: none;
}

.checkmark {
  width: 16px;
  height: 16px;
  border: 2px solid var(--border-color);
  border-radius: 3px;
  position: relative;
  transition: var(--transition);
}

.setting-checkbox:checked + .checkmark {
  background-color: var(--primary-color);
  border-color: var(--primary-color);
}

.setting-checkbox:checked + .checkmark::after {
  content: '✓';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: white;
  font-size: 10px;
  font-weight: bold;
}

/* Select box */
.setting-select {
  padding: 6px 8px;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  background-color: var(--background-primary);
  color: var(--text-primary);
  font-size: 12px;
  cursor: pointer;
}

.setting-select:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}

/* Footer area */
.footer {
  padding: 12px 16px;
  border-top: 1px solid var(--border-color);
  background-color: var(--background-secondary);
}

.footer-buttons {
  display: flex;
  gap: 8px;
  justify-content: center;
}

/* Loading indicator */
.loading-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.9);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid var(--border-color);
  border-top: 3px solid var(--primary-color);
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 16px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* Responsive design */
@media (max-width: 320px) {
  body {
    width: 300px;
  }

  .container {
    padding: 0 8px;
  }

  .btn {
    padding: 10px 12px;
    font-size: 13px;
  }
}

/* Animation effects */
.fade-in {
  animation: fadeIn 0.3s ease;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.slide-in {
  animation: slideIn 0.3s ease;
}

@keyframes slideIn {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

/* Focus accessibility */
button:focus,
select:focus,
input:focus {
  outline: 2px solid var(--primary-color);
  outline-offset: 2px;
}

/* Scrollbar styles */
::-webkit-scrollbar {
  width: 6px;
}

::-webkit-scrollbar-track {
  background: var(--background-secondary);
}

::-webkit-scrollbar-thumb {
  background: var(--border-color);
  border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--text-secondary);
}

6.4 JavaScript Interaction Logic

6.4.1 Main Script File

// popup.js
class PopupManager {
  constructor() {
    this.isInitialized = false;
    this.currentTab = null;
    this.settings = {
      autoMode: false,
      theme: 'light'
    };

    this.init();
  }

  async init() {
    try {
      // Show loading state
      this.showLoading(true);

      // Get current tab
      await this.getCurrentTab();

      // Load settings
      await this.loadSettings();

      // Initialize UI
      this.initializeUI();

      // Bind events
      this.bindEvents();

      // Update page information
      await this.updatePageInfo();

      // Update statistics
      await this.updateStats();

      this.isInitialized = true;
      this.showLoading(false);

    } catch (error) {
      console.error('Initialization failed:', error);
      this.showError('Initialization failed, please try again');
    }
  }

  async getCurrentTab() {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    this.currentTab = tabs[0];
  }

  async loadSettings() {
    const result = await chrome.storage.sync.get(['autoMode', 'theme']);
    this.settings.autoMode = result.autoMode || false;
    this.settings.theme = result.theme || 'light';
  }

  initializeUI() {
    // Set theme
    document.body.className = `${this.settings.theme}-theme`;

    // Set checkbox state
    const autoModeCheckbox = document.getElementById('autoMode');
    if (autoModeCheckbox) {
      autoModeCheckbox.checked = this.settings.autoMode;
    }

    // Set theme selection
    const themeSelect = document.getElementById('themeSelect');
    if (themeSelect) {
      themeSelect.value = this.settings.theme;
    }

    // Add animation classes
    document.querySelectorAll('.info-card, .action-buttons').forEach(el => {
      el.classList.add('fade-in');
    });
  }

  bindEvents() {
    // Feature toggle button
    const toggleFeatureBtn = document.getElementById('toggleFeature');
    if (toggleFeatureBtn) {
      toggleFeatureBtn.addEventListener('click', () => this.toggleFeature());
    }

    // Analyze page button
    const analyzePageBtn = document.getElementById('analyzeePage');
    if (analyzePageBtn) {
      analyzePageBtn.addEventListener('click', () => this.analyzePage());
    }

    // Auto mode toggle
    const autoModeCheckbox = document.getElementById('autoMode');
    if (autoModeCheckbox) {
      autoModeCheckbox.addEventListener('change', (e) => {
        this.updateSetting('autoMode', e.target.checked);
      });
    }

    // Theme selection
    const themeSelect = document.getElementById('themeSelect');
    if (themeSelect) {
      themeSelect.addEventListener('change', (e) => {
        this.updateTheme(e.target.value);
      });
    }

    // Settings button
    const settingsBtn = document.getElementById('settingsBtn');
    if (settingsBtn) {
      settingsBtn.addEventListener('click', () => this.openSettings());
    }

    // Help button
    const helpBtn = document.getElementById('helpBtn');
    if (helpBtn) {
      helpBtn.addEventListener('click', () => this.openHelp());
    }
  }

  async updatePageInfo() {
    if (!this.currentTab) return;

    const urlInfo = document.getElementById('urlInfo');
    const titleInfo = document.getElementById('titleInfo');

    if (urlInfo) {
      urlInfo.textContent = `URL: ${this.currentTab.url}`;
    }

    if (titleInfo) {
      titleInfo.textContent = `Title: ${this.currentTab.title}`;
    }
  }

  async updateStats() {
    try {
      // Get statistics from background script
      const response = await this.sendMessage({ action: 'getStats' });

      if (response.success) {
        const clickCount = document.getElementById('clickCount');
        const activeTime = document.getElementById('activeTime');

        if (clickCount) {
          clickCount.textContent = response.data.clickCount || 0;
        }

        if (activeTime) {
          activeTime.textContent = this.formatTime(response.data.activeTime || 0);
        }
      }
    } catch (error) {
      console.error('Failed to get statistics:', error);
    }
  }

  async toggleFeature() {
    try {
      this.showLoading(true);

      const response = await this.sendMessage({
        action: 'toggleFeature',
        tabId: this.currentTab.id
      });

      if (response.success) {
        // Update button state
        const btn = document.getElementById('toggleFeature');
        const btnText = btn.querySelector('.btn-text');

        if (response.enabled) {
          btnText.textContent = 'Disable Feature';
          btn.classList.add('btn-danger');
          btn.classList.remove('btn-primary');
        } else {
          btnText.textContent = 'Enable Feature';
          btn.classList.add('btn-primary');
          btn.classList.remove('btn-danger');
        }

        this.showSuccess('Feature ' + (response.enabled ? 'enabled' : 'disabled'));
      } else {
        this.showError(response.error || 'Operation failed');
      }
    } catch (error) {
      this.showError('Operation failed: ' + error.message);
    } finally {
      this.showLoading(false);
    }
  }

  async analyzePage() {
    try {
      this.showLoading(true);

      const response = await this.sendMessage({
        action: 'analyzePage',
        tabId: this.currentTab.id
      });

      if (response.success) {
        this.displayAnalysisResults(response.data);
        this.showSuccess('Page analysis completed');
      } else {
        this.showError(response.error || 'Analysis failed');
      }
    } catch (error) {
      this.showError('Analysis failed: ' + error.message);
    } finally {
      this.showLoading(false);
    }
  }

  displayAnalysisResults(data) {
    // Create analysis results display
    const resultsHtml = `
      <div class="analysis-results">
        <h4>Analysis Results</h4>
        <ul>
          <li>Page Elements: ${data.elementCount}</li>
          <li>Image Count: ${data.imageCount}</li>
          <li>Link Count: ${data.linkCount}</li>
          <li>Load Time: ${data.loadTime}ms</li>
        </ul>
      </div>
    `;

    const pageInfo = document.getElementById('pageInfo');
    if (pageInfo) {
      pageInfo.querySelector('.info-content').innerHTML = resultsHtml;
    }
  }

  async updateSetting(key, value) {
    this.settings[key] = value;

    try {
      await chrome.storage.sync.set({ [key]: value });

      // Notify background script of settings change
      this.sendMessage({
        action: 'settingChanged',
        key: key,
        value: value
      });

    } catch (error) {
      console.error('Failed to save settings:', error);
    }
  }

  async updateTheme(theme) {
    this.settings.theme = theme;
    document.body.className = `${theme}-theme`;

    await this.updateSetting('theme', theme);
  }

  openSettings() {
    chrome.runtime.openOptionsPage();
  }

  openHelp() {
    chrome.tabs.create({
      url: 'https://example.com/help'
    });
  }

  // Utility methods
  async sendMessage(message) {
    return new Promise((resolve) => {
      chrome.runtime.sendMessage(message, (response) => {
        resolve(response || { success: false, error: 'No response' });
      });
    });
  }

  formatTime(seconds) {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);

    if (hours > 0) {
      return `${hours}h ${minutes}m`;
    } else {
      return `${minutes}m`;
    }
  }

  showLoading(show) {
    const overlay = document.getElementById('loadingOverlay');
    if (overlay) {
      overlay.style.display = show ? 'flex' : 'none';
    }
  }

  showSuccess(message) {
    this.showToast(message, 'success');
  }

  showError(message) {
    this.showToast(message, 'error');
  }

  showToast(message, type = 'info') {
    // Create toast message
    const toast = document.createElement('div');
    toast.className = `toast toast-${type}`;
    toast.textContent = message;

    // Add styles
    toast.style.cssText = `
      position: fixed;
      top: 16px;
      right: 16px;
      padding: 12px 16px;
      border-radius: 4px;
      color: white;
      font-size: 12px;
      z-index: 10000;
      animation: slideIn 0.3s ease;
      background-color: ${type === 'success' ? '#34a853' : type === 'error' ? '#ea4335' : '#4285f4'};
    `;

    document.body.appendChild(toast);

    // Auto remove after 3 seconds
    setTimeout(() => {
      toast.style.animation = 'fadeOut 0.3s ease';
      setTimeout(() => {
        if (toast.parentNode) {
          toast.parentNode.removeChild(toast);
        }
      }, 300);
    }, 3000);
  }
}

// Initialize after document is loaded
document.addEventListener('DOMContentLoaded', () => {
  new PopupManager();
});

// Add keyboard shortcut support
document.addEventListener('keydown', (e) => {
  // Ctrl/Cmd + K: Quick feature toggle
  if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
    e.preventDefault();
    document.getElementById('toggleFeature')?.click();
  }

  // Escape: Close popup
  if (e.key === 'Escape') {
    window.close();
  }
});

6.4.2 Interaction Logic Demo Example

// popup-demo.js - Chrome Extension Popup Interaction Manager Demo
class PopupManagerDemo {
  constructor() {
    this.isInitialized = false;
    this.currentTab = null;
    this.settings = {
      autoMode: false,
      theme: 'light'
    };
    this.stats = {
      clickCount: 0,
      activeTime: 0
    };
  }

  async initialize() {
    console.log("Initializing Popup Manager...");

    await this.loadCurrentTab();
    await this.loadSettings();
    await this.updateStats();

    this.isInitialized = true;
    console.log("Popup Manager initialized");
  }

  async loadCurrentTab() {
    // Simulate getting current tab
    this.currentTab = {
      id: 1,
      url: 'https://www.example.com',
      title: 'Example Page',
      active: true
    };
    console.log(`Loaded tab: ${this.currentTab.title}`);
  }

  async loadSettings() {
    // Simulate loading settings from storage
    console.log("Loading settings:");
    for (const [key, value] of Object.entries(this.settings)) {
      console.log(`  ${key}: ${value}`);
    }
  }

  async updateStats() {
    // Simulate updating statistics
    this.stats.clickCount += 1;
    this.stats.activeTime += 30; // Add 30 seconds active time
    console.log(`Stats updated: clickCount=${this.stats.clickCount}, activeTime=${this.stats.activeTime}s`);
  }

  async toggleFeature() {
    try {
      // Simulate feature toggle
      const currentState = this.settings.featureEnabled || false;
      const newState = !currentState;

      this.settings.featureEnabled = newState;

      console.log(`Feature state toggled: ${currentState} -> ${newState}`);

      return {
        success: true,
        enabled: newState,
        message: `Feature ${newState ? 'enabled' : 'disabled'}`
      };

    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  async analyzePage() {
    try {
      console.log("Starting page analysis...");

      // Simulate page analysis
      await new Promise(resolve => setTimeout(resolve, 1000));

      const analysisData = {
        elementCount: 245,
        imageCount: 12,
        linkCount: 38,
        loadTime: 1250,
        timestamp: new Date().toISOString()
      };

      console.log("Page analysis completed:");
      for (const [key, value] of Object.entries(analysisData)) {
        console.log(`  ${key}: ${value}`);
      }

      return {
        success: true,
        data: analysisData
      };

    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  async updateSetting(key, value) {
    const oldValue = this.settings[key];
    this.settings[key] = value;

    console.log(`Setting updated: ${key}: ${oldValue} -> ${value}`);

    // Simulate saving to storage
    await this.saveSettings();
  }

  async saveSettings() {
    console.log("Saving settings to storage...");
    // Simulate storage operation
  }

  getPageInfo() {
    if (!this.currentTab) {
      return { url: 'Unknown', title: 'Unknown' };
    }

    return {
      url: this.currentTab.url,
      title: this.currentTab.title
    };
  }

  formatTime(seconds) {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);

    if (hours > 0) {
      return `${hours}h ${minutes}m`;
    } else {
      return `${minutes}m`;
    }
  }
}

// Usage example
async function demoPopupInteraction() {
  const popup = new PopupManagerDemo();

  // Initialize
  await popup.initialize();

  // Get page info
  const pageInfo = popup.getPageInfo();
  console.log('\nPage Information:');
  console.log(`  URL: ${pageInfo.url}`);
  console.log(`  Title: ${pageInfo.title}`);

  // Toggle feature
  const result = await popup.toggleFeature();
  console.log('\nFeature toggle result:', result);

  // Analyze page
  const analysisResult = await popup.analyzePage();
  console.log('\nAnalysis result:', analysisResult);

  // Update settings
  await popup.updateSetting('autoMode', true);
  await popup.updateSetting('theme', 'dark');

  // Display formatted time
  const formattedTime = popup.formatTime(popup.stats.activeTime);
  console.log(`\nFormatted time: ${formattedTime}`);
}

// Run demo (commented out for actual execution)
// demoPopupInteraction();

6.5 Communication with Background Script

6.5.1 Message Passing Mechanism

// Message handling in background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('Background received message:', message);

  switch (message.action) {
    case 'getStats':
      handleGetStats(sendResponse);
      return true; // Async response

    case 'toggleFeature':
      handleToggleFeature(message.tabId, sendResponse);
      return true;

    case 'analyzePage':
      handleAnalyzePage(message.tabId, sendResponse);
      return true;

    case 'settingChanged':
      handleSettingChanged(message.key, message.value, sendResponse);
      return true;

    default:
      sendResponse({ success: false, error: 'Unknown action' });
  }
});

async function handleGetStats(sendResponse) {
  try {
    const stats = await chrome.storage.local.get(['clickCount', 'activeTime']);
    sendResponse({
      success: true,
      data: {
        clickCount: stats.clickCount || 0,
        activeTime: stats.activeTime || 0
      }
    });
  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

async function handleToggleFeature(tabId, sendResponse) {
  try {
    // Get current feature state
    const result = await chrome.storage.sync.get(['featureEnabled']);
    const currentState = result.featureEnabled || false;
    const newState = !currentState;

    // Save new state
    await chrome.storage.sync.set({ featureEnabled: newState });

    // Send message to content script
    chrome.tabs.sendMessage(tabId, {
      action: 'updateFeatureState',
      enabled: newState
    });

    sendResponse({
      success: true,
      enabled: newState
    });

    // Update click statistics
    const stats = await chrome.storage.local.get(['clickCount']);
    await chrome.storage.local.set({
      clickCount: (stats.clickCount || 0) + 1
    });

  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

async function handleAnalyzePage(tabId, sendResponse) {
  try {
    // Execute page analysis script
    const results = await chrome.scripting.executeScript({
      target: { tabId: tabId },
      func: analyzePageContent
    });

    if (results && results[0]) {
      sendResponse({
        success: true,
        data: results[0].result
      });
    } else {
      sendResponse({ success: false, error: 'Analysis failed' });
    }

  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

// Page analysis function (injected into the page)
function analyzePageContent() {
  const elements = document.querySelectorAll('*');
  const images = document.querySelectorAll('img');
  const links = document.querySelectorAll('a');

  const performanceEntries = performance.getEntriesByType('navigation');
  const loadTime = performanceEntries.length > 0
    ? Math.round(performanceEntries[0].loadEventEnd - performanceEntries[0].fetchStart)
    : 0;

  return {
    elementCount: elements.length,
    imageCount: images.length,
    linkCount: links.length,
    loadTime: loadTime
  };
}

warning Common Issues

  1. Message Timeout: Use return true before sendResponse to keep message channel open
  2. Context Loss: All state is lost after Popup closes, important data needs to be stored
  3. Permission Restrictions: Some APIs require corresponding permission declarations
  4. Cross-Origin Issues: Access to external resources needs to be declared in manifest

6.6 Responsive Design and Optimization

6.6.1 Adaptive Layout

/* Responsive breakpoints */
@media (max-width: 400px) {
  body {
    width: 320px;
  }

  .stats-grid {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .action-buttons {
    flex-direction: column;
  }
}

@media (max-height: 500px) {
  .main-content {
    gap: 12px;
  }

  .info-card {
    padding: 12px;
  }
}

/* High DPI screen optimization */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
  .logo img {
    image-rendering: -webkit-optimize-contrast;
    image-rendering: crisp-edges;
  }
}

6.6.2 Performance Optimization

// Debounce function
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Optimized settings update
const updateSettingDebounced = debounce(async (key, value) => {
  await chrome.storage.sync.set({ [key]: value });
}, 300);

// Virtual scrolling (for large data lists)
class VirtualList {
  constructor(container, itemHeight, renderItem) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.renderItem = renderItem;
    this.items = [];
    this.visibleStart = 0;
    this.visibleEnd = 0;
  }

  setItems(items) {
    this.items = items;
    this.update();
  }

  update() {
    const containerHeight = this.container.clientHeight;
    const visibleCount = Math.ceil(containerHeight / this.itemHeight);
    const startIndex = Math.max(0, this.visibleStart);
    const endIndex = Math.min(this.items.length, startIndex + visibleCount + 1);

    // Clear container
    this.container.innerHTML = '';

    // Render visible items
    for (let i = startIndex; i < endIndex; i++) {
      const element = this.renderItem(this.items[i], i);
      element.style.position = 'absolute';
      element.style.top = `${i * this.itemHeight}px`;
      element.style.height = `${this.itemHeight}px`;
      this.container.appendChild(element);
    }

    // Set container total height
    this.container.style.height = `${this.items.length * this.itemHeight}px`;
  }
}

tip Optimization Recommendations

  1. Reduce Package Size: Compress CSS/JS files, remove unused code
  2. Lazy Loading: Delay loading of non-critical content
  3. Caching Strategy: Properly use localStorage caching
  4. Animation Optimization: Use CSS transform instead of position changes
  5. Image Optimization: Use WebP format, set appropriate sizes

note Summary This chapter provided detailed coverage of Chrome extension Popup interface development, including HTML structure design, CSS styling, JavaScript interaction logic implementation, and communication mechanisms with Background Script. Mastering these skills will help you create user-friendly extension interfaces. In the next chapter, we’ll learn how to develop Options pages to provide richer configuration features.

Categories