Chapter 07: Options Page
Chapter 7: Options Page
Learning Objectives
- Understand the purpose and characteristics of the Options page
- Master the development techniques for complex settings interfaces
- Learn data validation and settings synchronization mechanisms
- Implement user-friendly configuration experiences
7.1 Options Page Overview
The Options page serves as the central settings hub for Chrome extensions, offering more comprehensive configuration features and greater operational space than a Popup.
7.1.1 Characteristics and Purpose
tip Core Features
- An independent, full webpage with more space
- Supports complex forms and interactions
- Can include multiple configuration categories
- Setting changes affect extension behavior
- Supports importing/exporting configurations
7.1.2 Manifest Configuration
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"options_page": "options.html",
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"permissions": [
"storage"
]
}
7.2 Complete Options Page Design
7.2.1 HTML Structure
<!-- options.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Extension Settings</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<div class="container">
<!-- Header Navigation -->
<header class="header">
<div class="header-content">
<div class="logo-section">
<img src="icons/icon48.png" alt="Logo" class="logo">
<div class="title-section">
<h1>Extension Settings</h1>
<p class="version">Version 1.0.0</p>
</div>
</div>
<div class="header-actions">
<button class="btn btn-secondary" id="exportBtn">Export Settings</button>
<button class="btn btn-secondary" id="importBtn">Import Settings</button>
<input type="file" id="importFile" accept=".json" style="display: none;">
</div>
</div>
</header>
<div class="main-wrapper">
<!-- Sidebar Navigation -->
<nav class="sidebar">
<ul class="nav-list">
<li class="nav-item active" data-tab="general">
<span class="nav-icon">⚙️</span>
<span class="nav-text">General Settings</span>
</li>
<li class="nav-item" data-tab="features">
<span class="nav-icon">🛠️</span>
<span class="nav-text">Feature Configuration</span>
</li>
<li class="nav-item" data-tab="appearance">
<span class="nav-icon">🎨</span>
<span class="nav-text">Appearance Settings</span>
</li>
<li class="nav-item" data-tab="advanced">
<span class="nav-icon">🔧</span>
<span class="nav-text">Advanced Settings</span>
</li>
<li class="nav-item" data-tab="about">
<span class="nav-icon">ℹ️</span>
<span class="nav-text">About</span>
</li>
</ul>
</nav>
<!-- Main Content Area -->
<main class="main-content">
<!-- General Settings -->
<section class="tab-content active" id="general">
<div class="content-header">
<h2>General Settings</h2>
<p>Configure basic behavior and functionality of the extension</p>
</div>
<div class="settings-group">
<h3>Basic Functionality</h3>
<div class="setting-item">
<div class="setting-label">
<label for="enableExtension">Enable Extension</label>
<p class="setting-description">Control the global switch of the extension</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="enableExtension">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="autoStart">Auto Start on Browser Launch</label>
<p class="setting-description">Automatically activate the extension when the browser starts</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="autoStart">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="updateInterval">Update Interval</label>
<p class="setting-description">Time interval for automatic data updates</p>
</div>
<div class="setting-control">
<select id="updateInterval" class="setting-select">
<option value="1">1 Minute</option>
<option value="5">5 Minutes</option>
<option value="10">10 Minutes</option>
<option value="30">30 Minutes</option>
<option value="60">1 Hour</option>
</select>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="maxCacheSize">Cache Size Limit</label>
<p class="setting-description">Maximum cache data size (MB)</p>
</div>
<div class="setting-control">
<div class="input-group">
<input type="range" id="cacheSlider" min="10" max="500" value="100" class="range-input">
<input type="number" id="maxCacheSize" min="10" max="500" value="100" class="number-input">
<span class="input-unit">MB</span>
</div>
</div>
</div>
</div>
<div class="settings-group">
<h3>Notification Settings</h3>
<div class="setting-item">
<div class="setting-label">
<label for="enableNotifications">Enable Notifications</label>
<p class="setting-description">Allow the extension to display desktop notifications</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="enableNotifications">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item notification-dependent" style="display: none;">
<div class="setting-label">
<label for="notificationSound">Notification Sound</label>
<p class="setting-description">Play a sound alert for notifications</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="notificationSound">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</section>
<!-- Feature Configuration -->
<section class="tab-content" id="features">
<div class="content-header">
<h2>Feature Configuration</h2>
<p>Enable or disable specific feature modules</p>
</div>
<div class="settings-group">
<h3>Core Features</h3>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-header">
<div class="feature-icon">📊</div>
<div class="feature-info">
<h4>Data Analysis</h4>
<p>Analyze web page data and user behavior</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="featureAnalytics">
<span class="toggle-slider"></span>
</label>
</div>
<div class="feature-settings" id="analyticsSettings" style="display: none;">
<div class="sub-setting">
<label>
<input type="checkbox" id="trackClicks"> Track click events
</label>
</div>
<div class="sub-setting">
<label>
<input type="checkbox" id="trackPageViews"> Track page views
</label>
</div>
</div>
</div>
<div class="feature-card">
<div class="feature-header">
<div class="feature-icon">🔒</div>
<div class="feature-info">
<h4>Privacy Protection</h4>
<p>Block tracking and protect privacy</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="featurePrivacy">
<span class="toggle-slider"></span>
</label>
</div>
<div class="feature-settings" id="privacySettings" style="display: none;">
<div class="sub-setting">
<label>
<input type="checkbox" id="blockTrackers"> Block trackers
</label>
</div>
<div class="sub-setting">
<label>
<input type="checkbox" id="blockAds"> Block ads
</label>
</div>
</div>
</div>
</div>
</div>
<div class="settings-group">
<h3>Website Whitelist</h3>
<div class="whitelist-section">
<div class="input-group">
<input type="url" id="whitelistInput" placeholder="Enter website domain, e.g.: example.com">
<button type="button" id="addWhitelistBtn" class="btn btn-primary">Add</button>
</div>
<ul class="whitelist-list" id="whitelistList">
<!-- Dynamically generated whitelist items -->
</ul>
</div>
</div>
</section>
<!-- Appearance Settings -->
<section class="tab-content" id="appearance">
<div class="content-header">
<h2>Appearance Settings</h2>
<p>Customize interface appearance and theme</p>
</div>
<div class="settings-group">
<h3>Theme Settings</h3>
<div class="theme-selector">
<div class="theme-option" data-theme="light">
<div class="theme-preview light-preview">
<div class="preview-header"></div>
<div class="preview-content">
<div class="preview-line"></div>
<div class="preview-line short"></div>
</div>
</div>
<div class="theme-info">
<h4>Light Theme</h4>
<p>Classic light interface</p>
</div>
<input type="radio" name="theme" value="light" id="themeLight">
</div>
<div class="theme-option" data-theme="dark">
<div class="theme-preview dark-preview">
<div class="preview-header"></div>
<div class="preview-content">
<div class="preview-line"></div>
<div class="preview-line short"></div>
</div>
</div>
<div class="theme-info">
<h4>Dark Theme</h4>
<p>Eye-friendly dark interface</p>
</div>
<input type="radio" name="theme" value="dark" id="themeDark">
</div>
<div class="theme-option" data-theme="auto">
<div class="theme-preview auto-preview">
<div class="preview-header"></div>
<div class="preview-content">
<div class="preview-line"></div>
<div class="preview-line short"></div>
</div>
</div>
<div class="theme-info">
<h4>Follow System</h4>
<p>Automatically switch themes</p>
</div>
<input type="radio" name="theme" value="auto" id="themeAuto">
</div>
</div>
</div>
<div class="settings-group">
<h3>Font Settings</h3>
<div class="setting-item">
<div class="setting-label">
<label for="fontSize">Font Size</label>
<p class="setting-description">Adjust interface font size</p>
</div>
<div class="setting-control">
<div class="input-group">
<input type="range" id="fontSizeSlider" min="12" max="18" value="14" class="range-input">
<span class="font-size-display">14px</span>
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="fontFamily">Font Family</label>
<p class="setting-description">Select interface font</p>
</div>
<div class="setting-control">
<select id="fontFamily" class="setting-select">
<option value="system">System Default</option>
<option value="sans-serif">Sans-serif</option>
<option value="serif">Serif</option>
<option value="monospace">Monospace</option>
</select>
</div>
</div>
</div>
</section>
<!-- Advanced Settings -->
<section class="tab-content" id="advanced">
<div class="content-header">
<h2>Advanced Settings</h2>
<p>Configuration options for advanced users</p>
</div>
<div class="settings-group">
<h3>Developer Options</h3>
<div class="setting-item">
<div class="setting-label">
<label for="debugMode">Debug Mode</label>
<p class="setting-description">Enable detailed debugging information</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="debugMode">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="logLevel">Log Level</label>
<p class="setting-description">Control the verbosity of log output</p>
</div>
<div class="setting-control">
<select id="logLevel" class="setting-select">
<option value="error">Errors Only</option>
<option value="warn">Warnings and Above</option>
<option value="info">Info and Above</option>
<option value="debug">Debug and Above</option>
</select>
</div>
</div>
</div>
<div class="settings-group">
<h3>Data Management</h3>
<div class="setting-item">
<div class="setting-label">
<label>Clear Cache</label>
<p class="setting-description">Delete all cached data</p>
</div>
<div class="setting-control">
<button type="button" id="clearCacheBtn" class="btn btn-secondary">Clear Cache</button>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label>Reset Settings</label>
<p class="setting-description">Restore all settings to default values</p>
</div>
<div class="setting-control">
<button type="button" id="resetSettingsBtn" class="btn btn-danger">Reset Settings</button>
</div>
</div>
</div>
</section>
<!-- About Page -->
<section class="tab-content" id="about">
<div class="content-header">
<h2>About Extension</h2>
<p>Version information and related links</p>
</div>
<div class="about-content">
<div class="about-card">
<div class="about-icon">
<img src="icons/icon128.png" alt="Extension Icon">
</div>
<div class="about-info">
<h3>My Chrome Extension</h3>
<p class="version-info">Version 1.0.0</p>
<p class="description">This is a powerful Chrome extension that helps users improve browsing efficiency.</p>
</div>
</div>
<div class="links-section">
<h4>Related Links</h4>
<ul class="links-list">
<li><a href="#" id="homepageLink" target="_blank">Official Website</a></li>
<li><a href="#" id="supportLink" target="_blank">Technical Support</a></li>
<li><a href="#" id="feedbackLink" target="_blank">Feedback</a></li>
<li><a href="#" id="privacyLink" target="_blank">Privacy Policy</a></li>
</ul>
</div>
<div class="changelog-section">
<h4>Changelog</h4>
<div class="changelog-list">
<div class="changelog-item">
<div class="changelog-version">v1.0.0</div>
<div class="changelog-date">2025-12-03</div>
<div class="changelog-content">
<ul>
<li>Initial version release</li>
<li>Basic functionality complete</li>
<li>User interface optimized</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<!-- Save Status Notification -->
<div class="save-status" id="saveStatus" style="display: none;">
<span class="status-icon">✓</span>
<span class="status-text">Settings Saved</span>
</div>
</div>
<!-- Confirmation Dialog -->
<div class="modal-overlay" id="confirmModal" style="display: none;">
<div class="modal">
<div class="modal-header">
<h3 id="modalTitle">Confirm Operation</h3>
</div>
<div class="modal-body">
<p id="modalMessage">Are you sure you want to perform this operation?</p>
</div>
<div class="modal-footer">
<button type="button" id="modalCancel" class="btn btn-secondary">Cancel</button>
<button type="button" id="modalConfirm" class="btn btn-primary">Confirm</button>
</div>
</div>
</div>
<script src="options.js"></script>
</body>
</html>
7.2.2 Options Page Generator Configuration
Configuration structure and example for dynamically generating the Options page:
// options_generator.js - Chrome Extension Options Page Generator
class OptionsGenerator {
constructor() {
this.title = "Extension Settings";
this.version = "1.0.0";
this.tabs = [];
this.settingsStructure = {};
}
addTab(tabId, icon, title, description) {
const tab = {
id: tabId,
icon: icon,
title: title,
description: description,
groups: []
};
this.tabs.push(tab);
}
addSettingsGroup(tabId, groupTitle, settings) {
const tab = this.tabs.find(t => t.id === tabId);
if (tab) {
const group = {
title: groupTitle,
settings: settings
};
tab.groups.push(group);
}
}
generateSettingHTML(setting) {
const settingType = setting.type || 'checkbox';
const settingId = setting.id || '';
const label = setting.label || '';
const description = setting.description || '';
if (settingType === 'checkbox') {
return `
<div class="setting-item">
<div class="setting-label">
<label for="${settingId}">${label}</label>
<p class="setting-description">${description}</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="${settingId}">
<span class="toggle-slider"></span>
</label>
</div>
</div>`;
}
if (settingType === 'select') {
const optionsHTML = setting.options.map(option =>
`<option value="${option.value}">${option.text}</option>`
).join('');
return `
<div class="setting-item">
<div class="setting-label">
<label for="${settingId}">${label}</label>
<p class="setting-description">${description}</p>
</div>
<div class="setting-control">
<select id="${settingId}" class="setting-select">
${optionsHTML}
</select>
</div>
</div>`;
}
if (settingType === 'range') {
const minVal = setting.min || 0;
const maxVal = setting.max || 100;
const defaultVal = setting.default || 50;
const unit = setting.unit || '';
return `
<div class="setting-item">
<div class="setting-label">
<label for="${settingId}">${label}</label>
<p class="setting-description">${description}</p>
</div>
<div class="setting-control">
<div class="input-group">
<input type="range" id="${settingId}Slider" min="${minVal}"
max="${maxVal}" value="${defaultVal}" class="range-input">
<input type="number" id="${settingId}" min="${minVal}"
max="${maxVal}" value="${defaultVal}" class="number-input">
<span class="input-unit">${unit}</span>
</div>
</div>
</div>`;
}
return '';
}
generateTabContent(tab) {
const groupsHTML = tab.groups.map(group => {
const settingsHTML = group.settings.map(setting =>
this.generateSettingHTML(setting)
).join('');
return `
<div class="settings-group">
<h3>${group.title}</h3>
${settingsHTML}
</div>`;
}).join('');
return `
<section class="tab-content" id="${tab.id}">
<div class="content-header">
<h2>${tab.title}</h2>
<p>${tab.description}</p>
</div>
${groupsHTML}
</section>`;
}
generateNavigation() {
const navItems = this.tabs.map((tab, i) => {
const activeClass = i === 0 ? ' active' : '';
return `
<li class="nav-item${activeClass}" data-tab="${tab.id}">
<span class="nav-icon">${tab.icon}</span>
<span class="nav-text">${tab.title}</span>
</li>`;
}).join('');
return `
<nav class="sidebar">
<ul class="nav-list">
${navItems}
</ul>
</nav>`;
}
generateCompleteHTML() {
const navigationHTML = this.generateNavigation();
const contentHTML = this.tabs.map((tab, i) => {
const activeClass = i === 0 ? ' active' : '';
let tabHTML = this.generateTabContent(tab);
tabHTML = tabHTML.replace('class="tab-content"',
`class="tab-content${activeClass}"`);
return tabHTML;
}).join('');
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="options.css">
</head>
<body>
<div class="container">
<header class="header">
<div class="header-content">
<div class="logo-section">
<img src="icons/icon48.png" alt="Logo" class="logo">
<div class="title-section">
<h1>${this.title}</h1>
<p class="version">Version ${this.version}</p>
</div>
</div>
</div>
</header>
<div class="main-wrapper">
${navigationHTML}
<main class="main-content">
${contentHTML}
</main>
</div>
</div>
<script src="options.js"></script>
</body>
</html>`;
}
}
// Usage example
function createSampleOptions() {
const generator = new OptionsGenerator();
// Add General Settings tab
generator.addTab('general', '⚙️', 'General Settings', 'Configure basic behavior and functionality of the extension');
generator.addSettingsGroup('general', 'Basic Functionality', [
{
type: 'checkbox',
id: 'enableExtension',
label: 'Enable Extension',
description: 'Control the global switch of the extension'
},
{
type: 'select',
id: 'updateInterval',
label: 'Update Interval',
description: 'Time interval for automatic data updates',
options: [
{ value: '1', text: '1 Minute' },
{ value: '5', text: '5 Minutes' },
{ value: '10', text: '10 Minutes' }
]
},
{
type: 'range',
id: 'maxCacheSize',
label: 'Cache Size Limit',
description: 'Maximum cache data size',
min: 10,
max: 500,
default: 100,
unit: 'MB'
}
]);
// Add Appearance Settings tab
generator.addTab('appearance', '🎨', 'Appearance Settings', 'Customize interface appearance and theme');
generator.addSettingsGroup('appearance', 'Theme Settings', [
{
type: 'select',
id: 'theme',
label: 'Theme',
description: 'Select interface theme',
options: [
{ value: 'light', text: 'Light Theme' },
{ value: 'dark', text: 'Dark Theme' },
{ value: 'auto', text: 'Follow System' }
]
},
{
type: 'range',
id: 'fontSize',
label: 'Font Size',
description: 'Adjust interface font size',
min: 12,
max: 18,
default: 14,
unit: 'px'
}
]);
return generator.generateCompleteHTML();
}
// Generate example page
const sampleHTML = createSampleOptions();
console.log("Generated Options page HTML:", sampleHTML.substring(0, 1000) + "...");
7.3.1 Modern Layout Styles
/* options.css */
:root {
--primary-color: #4285f4;
--secondary-color: #34a853;
--danger-color: #ea4335;
--warning-color: #fbbc05;
--text-primary: #202124;
--text-secondary: #5f6368;
--text-muted: #9aa0a6;
--background-primary: #ffffff;
--background-secondary: #f8f9fa;
--background-tertiary: #e8f0fe;
--border-color: #dadce0;
--border-light: #f1f3f4;
--shadow-light: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-heavy: 0 8px 16px rgba(0, 0, 0, 0.15);
--border-radius: 8px;
--border-radius-large: 12px;
--transition: all 0.2s ease;
--sidebar-width: 240px;
}
/* Dark theme */
.dark-theme {
--text-primary: #e8eaed;
--text-secondary: #9aa0a6;
--text-muted: #5f6368;
--background-primary: #202124;
--background-secondary: #303134;
--background-tertiary: #1a73e8;
--border-color: #5f6368;
--border-light: #3c4043;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
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 {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header styles */
.header {
background-color: var(--background-primary);
border-bottom: 1px solid var(--border-color);
padding: 20px 24px;
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-light);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
}
.logo-section {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
width: 40px;
height: 40px;
border-radius: var(--border-radius);
}
.title-section h1 {
font-size: 24px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 2px;
}
.version {
font-size: 12px;
color: var(--text-secondary);
}
.header-actions {
display: flex;
gap: 12px;
}
/* Main layout */
.main-wrapper {
flex: 1;
display: flex;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Sidebar navigation */
.sidebar {
width: var(--sidebar-width);
background-color: var(--background-secondary);
border-right: 1px solid var(--border-color);
padding: 24px 0;
position: sticky;
top: 81px;
height: calc(100vh - 81px);
overflow-y: auto;
}
.nav-list {
list-style: none;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
cursor: pointer;
transition: var(--transition);
color: var(--text-secondary);
}
.nav-item:hover {
background-color: var(--background-tertiary);
color: var(--primary-color);
}
.nav-item.active {
background-color: var(--background-tertiary);
color: var(--primary-color);
border-right: 3px solid var(--primary-color);
}
.nav-icon {
font-size: 18px;
width: 24px;
text-align: center;
}
.nav-text {
font-weight: 500;
}
/* Main content area */
.main-content {
flex: 1;
padding: 24px;
overflow-y: auto;
max-height: calc(100vh - 81px);
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
.content-header {
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-light);
}
.content-header h2 {
font-size: 28px;
font-weight: 400;
color: var(--text-primary);
margin-bottom: 8px;
}
.content-header p {
font-size: 16px;
color: var(--text-secondary);
}
/* Settings group */
.settings-group {
margin-bottom: 40px;
background-color: var(--background-primary);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-large);
overflow: hidden;
box-shadow: var(--shadow-light);
}
.settings-group h3 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
padding: 16px 20px;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--border-light);
margin: 0;
}
/* Setting item */
.setting-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--border-light);
}
.setting-item:last-child {
border-bottom: none;
}
.setting-label {
flex: 1;
margin-right: 20px;
}
.setting-label label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
display: block;
margin-bottom: 4px;
cursor: pointer;
}
.setting-description {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.4;
}
.setting-control {
flex-shrink: 0;
display: flex;
align-items: center;
}
/* Toggle switch styles */
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: var(--transition);
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: var(--transition);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--primary-color);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
}
/* Select box styles */
.setting-select {
padding: 8px 32px 8px 12px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--background-primary);
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 8px center;
background-repeat: no-repeat;
background-size: 16px;
min-width: 120px;
}
.setting-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
}
/* Input group styles */
.input-group {
display: flex;
align-items: center;
gap: 12px;
}
.range-input {
width: 120px;
height: 6px;
border-radius: 3px;
background: var(--border-color);
outline: none;
appearance: none;
}
.range-input::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
box-shadow: var(--shadow-light);
}
.range-input::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: none;
box-shadow: var(--shadow-light);
}
.number-input {
width: 60px;
padding: 6px 8px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--background-primary);
color: var(--text-primary);
font-size: 13px;
text-align: center;
}
.input-unit {
font-size: 12px;
color: var(--text-secondary);
min-width: 20px;
}
/* Button styles */
.btn {
padding: 8px 16px;
border: none;
border-radius: var(--border-radius);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 6px;
text-decoration: none;
}
.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-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #d33b2c;
}
/* Feature cards */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.feature-card {
border: 1px solid var(--border-light);
border-radius: var(--border-radius-large);
padding: 20px;
background-color: var(--background-primary);
box-shadow: var(--shadow-light);
transition: var(--transition);
}
.feature-card:hover {
box-shadow: var(--shadow-medium);
}
.feature-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.feature-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--background-tertiary);
border-radius: var(--border-radius);
}
.feature-info {
flex: 1;
}
.feature-info h4 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 4px;
}
.feature-info p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.4;
}
.feature-settings {
padding-top: 12px;
border-top: 1px solid var(--border-light);
}
.sub-setting {
padding: 8px 0;
}
.sub-setting label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
}
/* Theme selector */
.theme-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.theme-option {
border: 2px solid var(--border-light);
border-radius: var(--border-radius-large);
padding: 16px;
cursor: pointer;
transition: var(--transition);
position: relative;
}
.theme-option:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-medium);
}
.theme-option input[type="radio"] {
position: absolute;
top: 12px;
right: 12px;
}
.theme-preview {
width: 100%;
height: 80px;
border-radius: var(--border-radius);
margin-bottom: 12px;
overflow: hidden;
border: 1px solid var(--border-light);
}
.light-preview {
background: linear-gradient(to bottom, #f8f9fa 30%, #ffffff 30%);
}
.dark-preview {
background: linear-gradient(to bottom, #303134 30%, #202124 30%);
}
.auto-preview {
background: linear-gradient(to right, #f8f9fa 50%, #303134 50%);
}
.preview-header {
height: 30%;
background-color: rgba(0, 0, 0, 0.1);
}
.preview-content {
padding: 8px;
}
.preview-line {
height: 4px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 2px;
margin-bottom: 4px;
}
.preview-line.short {
width: 60%;
}
.theme-info h4 {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 2px;
}
.theme-info p {
font-size: 12px;
color: var(--text-secondary);
}
/* Whitelist styles */
.whitelist-section .input-group {
margin-bottom: 16px;
}
.whitelist-section input[type="url"] {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 14px;
}
.whitelist-list {
list-style: none;
border: 1px solid var(--border-light);
border-radius: var(--border-radius);
max-height: 200px;
overflow-y: auto;
}
.whitelist-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
}
.whitelist-item:last-child {
border-bottom: none;
}
.whitelist-url {
font-size: 14px;
color: var(--text-primary);
}
.remove-btn {
color: var(--danger-color);
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--border-radius);
font-size: 12px;
}
.remove-btn:hover {
background-color: var(--danger-color);
color: white;
}
/* About page styles */
.about-content {
display: flex;
flex-direction: column;
gap: 32px;
}
.about-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
background-color: var(--background-secondary);
border-radius: var(--border-radius-large);
border: 1px solid var(--border-light);
}
.about-icon img {
width: 64px;
height: 64px;
border-radius: var(--border-radius-large);
}
.about-info h3 {
font-size: 20px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 4px;
}
.version-info {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.description {
font-size: 14px;
color: var(--text-primary);
line-height: 1.5;
}
.links-section h4,
.changelog-section h4 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 12px;
}
.links-list {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.links-list a {
color: var(--primary-color);
text-decoration: none;
font-size: 14px;
padding: 6px 12px;
border-radius: var(--border-radius);
border: 1px solid var(--primary-color);
transition: var(--transition);
}
.links-list a:hover {
background-color: var(--primary-color);
color: white;
}
.changelog-list {
background-color: var(--background-secondary);
border-radius: var(--border-radius-large);
overflow: hidden;
border: 1px solid var(--border-light);
}
.changelog-item {
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
}
.changelog-item:last-child {
border-bottom: none;
}
.changelog-version {
font-size: 14px;
font-weight: 600;
color: var(--primary-color);
}
.changelog-date {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.changelog-content ul {
margin-left: 16px;
}
.changelog-content li {
font-size: 13px;
color: var(--text-primary);
line-height: 1.4;
margin-bottom: 2px;
}
/* Save status notification */
.save-status {
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 16px;
background-color: var(--secondary-color);
color: white;
border-radius: var(--border-radius);
display: flex;
align-items: center;
gap: 8px;
box-shadow: var(--shadow-heavy);
z-index: 1000;
animation: slideInRight 0.3s ease;
}
.status-icon {
font-weight: bold;
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background-color: var(--background-primary);
border-radius: var(--border-radius-large);
box-shadow: var(--shadow-heavy);
max-width: 400px;
width: 90%;
overflow: hidden;
}
.modal-header {
padding: 16px 20px;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--border-light);
}
.modal-header h3 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
}
.modal-body {
padding: 20px;
}
.modal-body p {
font-size: 14px;
color: var(--text-primary);
line-height: 1.5;
}
.modal-footer {
padding: 16px 20px;
background-color: var(--background-secondary);
border-top: 1px solid var(--border-light);
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInRight {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
/* Responsive design */
@media (max-width: 768px) {
.main-wrapper {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
position: static;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.nav-list {
display: flex;
overflow-x: auto;
padding: 0 12px;
}
.nav-item {
white-space: nowrap;
padding: 12px 16px;
border-right: none;
border-bottom: 3px solid transparent;
}
.nav-item.active {
border-right: none;
border-bottom-color: var(--primary-color);
}
.main-content {
max-height: none;
}
.header-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.feature-grid {
grid-template-columns: 1fr;
}
.theme-selector {
grid-template-columns: 1fr;
}
.about-card {
flex-direction: column;
text-align: center;
}
}
@media (max-width: 480px) {
.header {
padding: 16px;
}
.main-content {
padding: 16px;
}
.setting-item {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.setting-control {
align-self: flex-end;
}
}
/* Print styles */
@media print {
.sidebar,
.header-actions,
.save-status,
.modal-overlay {
display: none;
}
.main-wrapper {
flex-direction: column;
}
.main-content {
max-height: none;
}
.tab-content {
display: block !important;
page-break-before: always;
}
.tab-content:first-child {
page-break-before: auto;
}
}
7.4 JavaScript Interaction Logic
7.4.1 Complete Options Script
// options.js
class OptionsManager {
constructor() {
this.settings = {};
this.defaults = {
enableExtension: true,
autoStart: false,
updateInterval: 5,
maxCacheSize: 100,
enableNotifications: true,
notificationSound: false,
featureAnalytics: false,
featurePrivacy: true,
theme: 'light',
fontSize: 14,
fontFamily: 'system',
debugMode: false,
logLevel: 'error',
whitelist: []
};
this.init();
}
async init() {
try {
await this.loadSettings();
this.setupNavigation();
this.bindEvents();
this.populateUI();
this.setupDependencies();
console.log('Options page initialized');
} catch (error) {
console.error('Initialization failed:', error);
this.showToast('Initialization failed, please refresh the page', 'error');
}
}
async loadSettings() {
try {
const result = await chrome.storage.sync.get(null);
this.settings = { ...this.defaults, ...result };
} catch (error) {
console.error('Failed to load settings:', error);
this.settings = { ...this.defaults };
}
}
setupNavigation() {
const navItems = document.querySelectorAll('.nav-item');
const tabContents = document.querySelectorAll('.tab-content');
navItems.forEach(item => {
item.addEventListener('click', () => {
const tabId = item.dataset.tab;
// Update navigation state
navItems.forEach(nav => nav.classList.remove('active'));
item.classList.add('active');
// Show corresponding tab content
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === tabId) {
content.classList.add('active');
}
});
});
});
}
bindEvents() {
// Event binding for general settings items
this.bindCheckboxes();
this.bindSelects();
this.bindRanges();
this.bindButtons();
this.bindSpecialControls();
}
bindCheckboxes() {
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const key = e.target.id;
const value = e.target.checked;
this.updateSetting(key, value);
});
});
}
bindSelects() {
const selects = document.querySelectorAll('select');
selects.forEach(select => {
select.addEventListener('change', (e) => {
const key = e.target.id;
const value = e.target.value;
if (key === 'updateInterval') {
this.updateSetting(key, parseInt(value));
} else {
this.updateSetting(key, value);
}
});
});
}
bindRanges() {
// Cache size slider
const cacheSlider = document.getElementById('cacheSlider');
const cacheInput = document.getElementById('maxCacheSize');
if (cacheSlider && cacheInput) {
const updateCacheSize = (value) => {
cacheSlider.value = value;
cacheInput.value = value;
this.updateSetting(key, parseInt(value));
};
cacheSlider.addEventListener('input', (e) => {
updateCacheSize(e.target.value);
});
cacheInput.addEventListener('change', (e) => {
updateCacheSize(e.target.value);
});
}
// Font size slider
const fontSizeSlider = document.getElementById('fontSizeSlider');
const fontSizeDisplay = document.querySelector('.font-size-display');
if (fontSizeSlider && fontSizeDisplay) {
fontSizeSlider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
fontSizeDisplay.textContent = `${value}px`;
this.updateSetting('fontSize', value);
this.applyFontSize(value);
});
}
}
bindButtons() {
// Export settings
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) {
exportBtn.addEventListener('click', () => this.exportSettings());
}
// Import settings
const importBtn = document.getElementById('importBtn');
const importFile = document.getElementById('importFile');
if (importBtn && importFile) {
importBtn.addEventListener('click', () => importFile.click());
importFile.addEventListener('change', (e) => this.importSettings(e));
}
// Clear cache
const clearCacheBtn = document.getElementById('clearCacheBtn');
if (clearCacheBtn) {
clearCacheBtn.addEventListener('click', () => this.clearCache());
}
// Reset settings
const resetBtn = document.getElementById('resetSettingsBtn');
if (resetBtn) {
resetBtn.addEventListener('click', () => this.resetSettings());
}
// Whitelist management
const addWhitelistBtn = document.getElementById('addWhitelistBtn');
if (addWhitelistBtn) {
addWhitelistBtn.addEventListener('click', () => this.addToWhitelist());
}
// Feature card toggling
this.bindFeatureCards();
}
bindSpecialControls() {
// Theme selection
const themeRadios = document.querySelectorAll('input[name="theme"]');
themeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
this.updateSetting('theme', e.target.value);
this.applyTheme(e.target.value);
}
});
});
// Notification settings dependencies
const notificationToggle = document.getElementById('enableNotifications');
if (notificationToggle) {
notificationToggle.addEventListener('change', (e) => {
this.toggleNotificationDependencies(e.target.checked);
});
}
}
bindFeatureCards() {
const featureToggles = {
'featureAnalytics': 'analyticsSettings',
'featurePrivacy': 'privacySettings'
};
Object.entries(featureToggles).forEach(([toggleId, settingsId]) => {
const toggle = document.getElementById(toggleId);
const settings = document.getElementById(settingsId);
if (toggle && settings) {
toggle.addEventListener('change', (e) => {
settings.style.display = e.target.checked ? 'block' : 'none';
});
}
});
}
populateUI() {
// Populate checkboxes
Object.keys(this.settings).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = this.settings[key];
} else if (element.tagName === 'SELECT') {
element.value = this.settings[key];
} else if (element.type === 'number' || element.type === 'range') {
element.value = this.settings[key];
}
}
});
// Set theme radio button
const themeRadio = document.getElementById(`theme${this.settings.theme.charAt(0).toUpperCase() + this.settings.theme.slice(1)}`);
if (themeRadio) {
themeRadio.checked = true;
}
// Synchronize slider and input fields
this.syncRangeInputs();
// Populate whitelist
this.populateWhitelist();
// Apply current theme
this.applyTheme(this.settings.theme);
// Apply font size
this.applyFontSize(this.settings.fontSize);
// Update feature card states
this.updateFeatureCards();
}
7.4.1 Complete Options Script (Continued)
syncRangeInputs() {
// Cache size
const cacheSlider = document.getElementById('cacheSlider');
const cacheInput = document.getElementById('maxCacheSize');
if (cacheSlider && cacheInput) {
const value = this.settings.maxCacheSize;
cacheSlider.value = value;
cacheInput.value = value;
}
// Font size
const fontSizeSlider = document.getElementById('fontSizeSlider');
const fontSizeDisplay = document.querySelector('.font-size-display');
if (fontSizeSlider && fontSizeDisplay) {
const value = this.settings.fontSize;
fontSizeSlider.value = value;
fontSizeDisplay.textContent = `${value}px`;
}
}
setupDependencies() {
// Notification settings dependencies
this.toggleNotificationDependencies(this.settings.enableNotifications);
}
toggleNotificationDependencies(enabled) {
const dependentElements = document.querySelectorAll('.notification-dependent');
dependentElements.forEach(el => {
el.style.display = enabled ? 'flex' : 'none';
});
}
updateFeatureCards() {
const featureToggles = {
'featureAnalytics': 'analyticsSettings',
'featurePrivacy': 'privacySettings'
};
Object.entries(featureToggles).forEach(([toggleId, settingsId]) => {
const toggle = document.getElementById(toggleId);
const settings = document.getElementById(settingsId);
if (toggle && settings) {
settings.style.display = toggle.checked ? 'block' : 'none';
}
});
}
async updateSetting(key, value) {
this.settings[key] = value;
try {
await chrome.storage.sync.set({ [key]: value });
this.showSaveStatus();
// Notify background script of setting change
chrome.runtime.sendMessage({
action: 'settingChanged',
key: key,
value: value
});
} catch (error) {
console.error('Failed to save settings:', error);
this.showToast('Failed to save settings', 'error');
}
}
applyTheme(theme) {
document.body.className = `${theme}-theme`;
// If auto theme, decide based on system settings
if (theme === 'auto') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.body.className = `${isDark ? 'dark' : 'light'}-theme`;
}
}
applyFontSize(size) {
document.documentElement.style.setProperty('--base-font-size', `${size}px`);
}
// Whitelist management
populateWhitelist() {
const whitelistList = document.getElementById('whitelistList');
if (!whitelistList) return;
whitelistList.innerHTML = '';
this.settings.whitelist.forEach((domain, index) => {
const listItem = document.createElement('li');
listItem.className = 'whitelist-item';
listItem.innerHTML = `
<span class="whitelist-url">${domain}</span>
<button type="button" class="remove-btn" data-index="${index}">Remove</button>
`;
listItem.querySelector('.remove-btn').addEventListener('click', () => {
this.removeFromWhitelist(index);
});
whitelistList.appendChild(listItem);
});
}
addToWhitelist() {
const input = document.getElementById('whitelistInput');
if (!input) return;
const domain = input.value.trim();
if (!domain) {
this.showToast('Please enter a valid domain', 'warning');
return;
}
// Simple domain validation
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-_.]*\.[a-zA-Z]{2,}$/;
if (!domainRegex.test(domain)) {
this.showToast('Please enter a valid domain format', 'warning');
return;
}
if (this.settings.whitelist.includes(domain)) {
this.showToast('This domain is already in the whitelist', 'warning');
return;
}
this.settings.whitelist.push(domain);
this.updateSetting('whitelist', this.settings.whitelist);
this.populateWhitelist();
input.value = '';
this.showToast('Domain added to whitelist', 'success');
}
removeFromWhitelist(index) {
this.settings.whitelist.splice(index, 1);
this.updateSetting('whitelist', this.settings.whitelist);
this.populateWhitelist();
this.showToast('Domain removed from whitelist', 'success');
}
// Configuration import/export
exportSettings() {
const dataStr = JSON.stringify(this.settings, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = `extension-settings-${new Date().toISOString().split('T')[0]}.json`;
link.click();
this.showToast('Configuration exported', 'success');
}
async importSettings(event) {
const file = event.target.files[0];
if (!file) return;
try {
const text = await file.text();
const importedSettings = JSON.parse(text);
// Validate imported settings
const validSettings = {};
Object.keys(this.defaults).forEach(key => {
if (importedSettings.hasOwnProperty(key)) {
validSettings[key] = importedSettings[key];
}
});
this.showConfirmDialog(
'Confirm Import',
'Importing configuration will overwrite current settings, are you sure you want to continue?',
async () => {
this.settings = { ...this.defaults, ...validSettings };
await chrome.storage.sync.set(this.settings);
this.populateUI();
this.showToast('Configuration imported', 'success');
}
);
} catch (error) {
console.error('Import failed:', error);
this.showToast('Invalid configuration file format', 'error');
}
// Clear file selection
event.target.value = '';
}
// Data management
async clearCache() {
this.showConfirmDialog(
'Clear Cache',
'Are you sure you want to clear all cached data? This operation cannot be undone.',
async () => {
try {
await chrome.storage.local.clear();
this.showToast('Cache cleared', 'success');
} catch (error) {
console.error('Failed to clear cache:', error);
this.showToast('Failed to clear cache', 'error');
}
}
);
}
async resetSettings() {
this.showConfirmDialog(
'Reset Settings',
'Are you sure you want to restore all settings to default values? This operation cannot be undone.',
async () => {
try {
this.settings = { ...this.defaults };
await chrome.storage.sync.set(this.settings);
this.populateUI();
this.showToast('Settings reset', 'success');
} catch (error) {
console.error('Failed to reset settings:', error);
this.showToast('Failed to reset settings', 'error');
}
}
);
}
// UI feedback methods
showSaveStatus() {
const saveStatus = document.getElementById('saveStatus');
if (saveStatus) {
saveStatus.style.display = 'flex';
setTimeout(() => {
saveStatus.style.display = 'none';
}, 2000);
}
}
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
border-radius: 6px;
color: white;
font-size: 14px;
z-index: 10000;
animation: slideInRight 0.3s ease;
background-color: ${
type === 'success' ? '#34a853' :
type === 'error' ? '#ea4335' :
type === 'warning' ? '#fbbc05' : '#4285f4'
};
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 3000);
}
showConfirmDialog(title, message, onConfirm) {
const modal = document.getElementById('confirmModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalConfirm = document.getElementById('modalConfirm');
const modalCancel = document.getElementById('modalCancel');
if (modal && modalTitle && modalMessage && modalConfirm && modalCancel) {
modalTitle.textContent = title;
modalMessage.textContent = message;
const hideModal = () => {
modal.style.display = 'none';
};
modalConfirm.onclick = () => {
hideModal();
onConfirm();
};
modalCancel.onclick = hideModal;
modal.onclick = (e) => {
if (e.target === modal) hideModal();
};
modal.style.display = 'flex';
}
}
}
// Initialization
document.addEventListener('DOMContentLoaded', () => {
new OptionsManager();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + S: Quick save
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
// Trigger save status display
const event = new Event('change');
document.querySelector('input[type="checkbox"]')?.dispatchEvent(event);
}
// Escape: Close modal
if (e.key === 'Escape') {
const modal = document.getElementById('confirmModal');
if (modal && modal.style.display === 'flex') {
modal.style.display = 'none';
}
}
});
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
const currentTheme = document.querySelector('input[name="theme"]:checked')?.value;
if (currentTheme === 'auto') {
document.body.className = `${e.matches ? 'dark' : 'light'}-theme`;
}
});
7.4.2 Extending Settings Manager Functionality
Advanced functionality implementation for the extension settings manager:
// settings_manager.js - Chrome Extension Settings Manager
class SettingsManager {
constructor(configFile = "extension_settings") {
this.configFile = configFile;
this.defaults = {
enableExtension: true,
autoStart: false,
updateInterval: 5,
maxCacheSize: 100,
enableNotifications: true,
notificationSound: false,
featureAnalytics: false,
featurePrivacy: true,
theme: 'light',
fontSize: 14,
fontFamily: 'system',
debugMode: false,
logLevel: 'error',
whitelist: []
};
this.settings = {};
this.loadSettings();
}
async loadSettings() {
try {
const result = await chrome.storage.sync.get(null);
this.settings = { ...this.defaults, ...result };
console.log("Settings loaded successfully");
} catch (error) {
console.error("Failed to load settings:", error);
this.settings = { ...this.defaults };
}
}
async saveSettings() {
try {
await chrome.storage.sync.set(this.settings);
console.log("Settings saved successfully");
return true;
} catch (error) {
console.error("Failed to save settings:", error);
return false;
}
}
getSetting(key, defaultValue = null) {
return this.settings[key] !== undefined ? this.settings[key] : defaultValue;
}
async updateSetting(key, value) {
if (key in this.defaults) {
const oldValue = this.settings[key];
this.settings[key] = value;
if (await this.saveSettings()) {
console.log(`Setting updated: ${key}: ${oldValue} -> ${value}`);
this.onSettingChanged(key, value, oldValue);
return true;
} else {
// Rollback changes
this.settings[key] = oldValue;
return false;
}
} else {
console.error(`Unknown setting key: ${key}`);
return false;
}
}
onSettingChanged(key, newValue, oldValue) {
if (key === 'theme') {
console.log(`Theme changed to: ${newValue}`);
} else if (key === 'enableExtension') {
console.log(`Extension ${newValue ? 'enabled' : 'disabled'}`);
} else if (key === 'whitelist') {
if (newValue.length > oldValue.length) {
console.log("New whitelist domain added");
} else if (newValue.length < oldValue.length) {
console.log("Whitelist domain removed");
}
}
}
async addToWhitelist(domain) {
if (!this.isValidDomain(domain)) {
console.error(`Invalid domain: ${domain}`);
return false;
}
const whitelist = this.settings.whitelist || [];
if (whitelist.includes(domain)) {
console.log(`Domain already exists: ${domain}`);
return false;
}
whitelist.push(domain);
return await this.updateSetting('whitelist', whitelist);
}
async removeFromWhitelist(domain) {
const whitelist = this.settings.whitelist || [];
if (!whitelist.includes(domain)) {
console.log(`Domain not in whitelist: ${domain}`);
return false;
}
const newWhitelist = whitelist.filter(d => d !== domain);
return await this.updateSetting('whitelist', newWhitelist);
}
isValidDomain(domain) {
const pattern = /^[a-zA-Z0-9][a-zA-Z0-9-_.]*\.[a-zA-Z]{2,}$/;
return pattern.test(domain);
}
async exportSettings(filename = null) {
if (!filename) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
filename = `extension_settings_export_${timestamp}.json`;
}
try {
const exportData = {
exported_at: new Date().toISOString(),
version: '1.0.0',
settings: this.settings
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = filename;
link.click();
console.log(`Settings exported to: ${filename}`);
return filename;
} catch (error) {
console.error("Failed to export settings:", error);
return "";
}
}
async importSettings(file) {
try {
const text = await file.text();
const importData = JSON.parse(text);
let importedSettings;
if ('settings' in importData) {
importedSettings = importData.settings;
} else {
importedSettings = importData;
}
// Validate and import settings
const validSettings = {};
Object.keys(this.defaults).forEach(key => {
if (key in importedSettings) {
validSettings[key] = importedSettings[key];
}
});
this.settings = { ...this.defaults, ...validSettings };
if (await this.saveSettings()) {
console.log(`Settings imported from file`);
return true;
} else {
return false;
}
} catch (error) {
console.error("Failed to import settings:", error);
return false;
}
}
async resetSettings() {
this.settings = { ...this.defaults };
if (await this.saveSettings()) {
console.log("Settings reset to default values");
return true;
}
return false;
}
getAllSettings() {
return { ...this.settings };
}
validateSettings() {
const errors = [];
// Validate update interval
const interval = this.settings.updateInterval || 5;
if (!Number.isInteger(interval) || interval < 1 || interval > 60) {
errors.push("Update interval must be an integer between 1-60");
}
// Validate cache size
const cacheSize = this.settings.maxCacheSize || 100;
if (!Number.isInteger(cacheSize) || cacheSize < 10 || cacheSize > 500) {
errors.push("Cache size must be an integer between 10-500");
}
// Validate font size
const fontSize = this.settings.fontSize || 14;
if (!Number.isInteger(fontSize) || fontSize < 12 || fontSize > 18) {
errors.push("Font size must be an integer between 12-18");
}
// Validate theme
const theme = this.settings.theme || 'light';
if (!['light', 'dark', 'auto'].includes(theme)) {
errors.push("Theme must be one of light, dark, or auto");
}
// Validate whitelist
const whitelist = this.settings.whitelist || [];
if (!Array.isArray(whitelist)) {
errors.push("Whitelist must be an array");
} else {
whitelist.forEach(domain => {
if (!this.isValidDomain(domain)) {
errors.push(`Invalid whitelist domain: ${domain}`);
}
});
}
return errors;
}
printSettingsSummary() {
console.log("\n=== Extension Settings Summary ===");
console.log(`Extension Status: ${this.settings.enableExtension ? 'Enabled' : 'Disabled'}`);
console.log(`Theme: ${this.settings.theme || 'light'}`);
console.log(`Update Interval: ${this.settings.updateInterval || 5} minutes`);
console.log(`Cache Size: ${this.settings.maxCacheSize || 100}MB`);
console.log(`Notifications: ${this.settings.enableNotifications ? 'Enabled' : 'Disabled'}`);
console.log(`Whitelisted Domains: ${(this.settings.whitelist || []).length}`);
console.log(`Debug Mode: ${this.settings.debugMode ? 'Enabled' : 'Disabled'}`);
console.log("==================\n");
}
}
// Usage example
async function demoSettingsManager() {
const manager = new SettingsManager();
// Wait for settings to load
await manager.loadSettings();
// Display current settings summary
manager.printSettingsSummary();
// Update some settings
await manager.updateSetting('theme', 'dark');
await manager.updateSetting('updateInterval', 10);
await manager.updateSetting('enableNotifications', false);
// Add whitelist domains
await manager.addToWhitelist('example.com');
await manager.addToWhitelist('google.com');
// Validate settings
const errors = manager.validateSettings();
if (errors.length > 0) {
console.log("Settings validation errors:");
errors.forEach(error => console.log(` - ${error}`));
} else {
console.log("All settings validated successfully");
}
// Export settings
const exportFile = await manager.exportSettings();
// Display final settings summary
manager.printSettingsSummary();
return manager;
}
// Example: Using in an extension
// const settingsManager = new SettingsManager();
Settings Manager Functionality Table
| Function | Method | Description |
|---|---|---|
| Load Settings | loadSettings() | Loads all settings from chrome.storage.sync |
| Save Settings | saveSettings() | Saves settings to chrome.storage.sync |
| Get Setting | getSetting(key, default) | Gets a single setting value |
| Update Setting | updateSetting(key, value) | Updates a single setting and saves it |
| Export Settings | exportSettings(filename) | Exports settings to a JSON file |
| Import Settings | importSettings(file) | Imports settings from a JSON file |
| Reset Settings | resetSettings() | Restores all settings to default values |
| Validate Settings | validateSettings() | Validates the validity of all settings |
| Whitelist Management | addToWhitelist(domain) | Adds a domain to the whitelist |
| Whitelist Management | removeFromWhitelist(domain) | Removes a domain from the whitelist |
warning Best Practices
- Data Validation: Always validate user-entered setting values
- Default Values: Provide reasonable default values for all setting items
- Error Handling: Gracefully handle storage failure scenarios
- User Experience: Provide instant visual feedback
- Performance Optimization: Avoid frequent storage operations
tip Extended Features
- Support setting grouping and searching
- Implement setting item dependencies
- Add setting validation rules
- Support batch operations
- Provide advanced user mode
note Summary This chapter introduces the complete development process for Chrome extension Options pages, including complex HTML structure, modern CSS styles, comprehensive JavaScript interaction logic, and best practices for settings management. An excellent Options page can significantly enhance user experience and extension usability. The next chapter will cover messaging mechanisms to enable efficient communication between extension components.