Popup: ввод заметки и безопасный вывод
Manifest открывает локальную страницу расширения. Добавим поле заметки, кнопку и строку состояния. Результат этой главы намеренно ограничен интерфейсом: введённый текст появляется как черновик, но ещё не сохраняется. Так мы увидим, какие данные принадлежат открытому DOM и почему закрытие popup нельзя считать надёжным способом хранения пользовательской работы.
Popup похож на обычную страницу по доступным DOM-операциям, однако живёт иначе. Пользователь открывает небольшой интерфейс для короткого действия, затем возвращается к вкладке. После закрытия обработчики и значения полей текущего экземпляра исчезают. Поэтому задача разработчика — быстро выполнить понятную операцию и показать её состояние. Долгую процедуру импорта позже вынесем в options, где закрытие краткого окна меньше мешает чтению и подтверждению.
Поле и событие
textarea получает id note, maxlength 2000 и доступное название aria-label. Ограничение в интерфейсе помогает пользователю, но не станет единственной проверкой данных. Позже worker повторно проверит длину, поскольку сообщение можно сформировать иным способом. Такое дублирование имеет разные обязанности: поле ограничивает удобный ввод, а принимающий контекст защищает договор операции. Не следует считать DOM-атрибут гарантией для всех сообщений расширения.
Кнопка имеет type="button", поэтому её смысл не зависит от случайной формы-родителя. Мы находим её через querySelector и подписываем обработчик click через addEventListener. Код подключён внешним popup.js, без inline-обработчика в HTML. Это позволяет читать структуру интерфейса отдельно от поведения и не смешивать пользовательскую строку с исполняемым содержимым. В выбранном пакете все выполняемые файлы локальны и входят в снимок.
Обработчик берёт value из textarea. Это текст, который пользователь написал сейчас. Он ещё не является объектом заметки с номером, датой и адресом. Поэтому надпись начинается со слова «Черновик». Если значение пустое, выводим предложение ввести текст. Мы не сообщаем «Сохранено», потому что storage в этой ступени не подключён. Точность сообщения состояния важна: интерфейс не должен обещать факт, которого код не совершил.
Полные файлы шага
Показанные файлы полностью совпадают с выбранным состоянием архива. Остальные согласованные файлы находятся в examples/browser-extension-lab/lesson-02/.
popup.html
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Заметки читателя</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Заметки читателя</h1>
<p>Сохраняйте собственную заметку о выбранной странице.</p>
<textarea id="note" maxlength="2000" aria-label="Ваша заметка"></textarea>
<button id="save" type="button">Сохранить страницу</button>
<button id="settings" type="button">Настройки</button>
<p id="status" role="status" aria-live="polite"></p>
<ul id="notes"></ul>
<script type="module" src="popup.js"></script>
</body>
</html>
popup.js
const status = document.querySelector("#status");
document.querySelector("#save").addEventListener("click", () => {
const value = document.querySelector("#note").value;
status.textContent = value ? `Черновик: ${value}` : "Введите свою заметку";
});
document.querySelector("#settings").hidden = true;
style.css
body {
font-family: system-ui, sans-serif;
margin: 1rem;
min-width: 300px;
max-width: 700px;
}
button, input, textarea { font: inherit; }
textarea, input[type="search"] { display: block; width: 95%; margin: .5rem 0; }
button { margin: .3rem .3rem .3rem 0; }
li { margin: .7rem 0; overflow-wrap: anywhere; }
#status { min-height: 1.5em; }
Ожидаемый результат
Это ожидаемый договор для самостоятельного исследования; расширение не устанавливалось и не запускалось.
Ввод собственной строки → «Черновик: …»
Пустое поле → «Введите свою заметку»
Текст <b> остаётся буквальным текстом.
Закрытие popup уничтожает этот несохранённый черновик.
TextContent и чужой текст
Для вывода используется textContent. Строка с символами должна остаться строкой, а не стать жирным HTML-элементом. Этот выбор станет особенно важным после чтения заголовка чужого сайта и импорта JSON. Чужой текст может содержать знаки разметки, но расширение отображает содержание, а не исполняет полученный HTML. Не выбираем innerHTML ради удобного объединения строки: в нашем проекте нет задачи разрешать произвольную разметку заметок.
Строка состояния имеет role="status" и aria-live="polite". Это помогает передать изменившийся текст без необходимости искать его визуально после каждого нажатия. Мы не гарантируем одинаковый звук или время объявления во всех вспомогательных средствах: это зависит от среды читателя. Однако семантическая разметка задаёт понятную цель элемента. В уроке не нужен отдельный всплывающий alert для обычного черновика, который уже находится рядом с полем.
HTML также содержит будущий список notes и кнопку settings, но JavaScript пока скрывает настройки. Эти элементы подготовлены как часть последовательного интерфейса; они ещё не обещают работающую страницу options. Скрытая кнопка не используется для защиты privileged API, поскольку доступа к этим операциям сейчас нет. Позже worker получит настоящую проверку отправителя и команд. Внешний вид кнопки и право выполнить действие необходимо рассматривать отдельно.
Короткое состояние интерфейса
В style.css задаём системный шрифт, отступы и ширину полей. Размеры просты, потому что главный результат урока связан с событием и текстом. Не создаём собственную библиотеку компонентов для одной операции. Кнопки и textarea сохраняют нативную семантику, а строки могут переноситься. Это важно для длинной заметки: текст не должен вынудить popup расширяться без понятной границы. Постоянный список появится позднее, но основные правила вывода останутся теми же.
Ожидаемый положительный сценарий: человек вводит «Сравнить два объяснения диапазонов» и нажимает кнопку. Строка состояния показывает этот текст после «Черновик». Если закрыть popup и открыть снова, поле окажется в новом исходном состоянии. Это не потеря уже сохранённой записи, потому что запись не производилась. Такой опыт читатель может провести самостоятельно; автор расширение не устанавливал и не наблюдал этот экран в браузере.
Проверочный ошибочный вариант — ввести текст, похожий на HTML. Он должен отображаться буквально. Ещё один вариант — нажать кнопку с пустым полем: ожидается просьба ввода. Мы не добавляем универсальную ловушку всех исключений, поскольку текущая операция синхронна и имеет два определённых исхода. Когда появятся асинхронные API браузера, понадобятся try/catch и явное ожидание завершения, иначе обещание сохранения появится слишком рано.
Ввод и факт остаются разными
Представьте, что пользователь пишет заметку, но ещё не нажал кнопку. Текст находится только в DOM. Даже после нажатия в этой версии он переходит лишь в строку состояния того же окна. Обе области исчезают вместе с popup. Настоящее сохранение будет иметь третий этап: проверенный объект передаётся worker, storage подтверждает запись, интерфейс получает ответ и только затем показывает успех. Сейчас полезно заранее различить эти границы, чтобы позднее не перепутать сообщение отправки с завершённым фактом.
Полные HTML, JavaScript и CSS на этой странице совпадают со снимком lesson-02. При переносе берите весь каталог, потому что старый HTML не подключает popup.js и не содержит нужные id. Результат главы — управляемый ввод и безопасный вывод текста. Следующий урок вынесет обработку событий в service worker и покажет, почему общий фон Manifest V3 не должен хранить заметки в глобальном массиве, даже если такой массив кажется удобным при первом чтении.
Вывод строки и изменение поля имеют разные роли. TextContent назначается элементу статуса, а value относится к редактируемому textarea. Если вместо value менять текстовый узел внутри textarea после пользовательского ввода, легко получить непонятное расхождение исходного содержимого и текущего значения. В нашем обработчике источник читается через value, а результат выводится отдельно. Так пользователь продолжает видеть свой черновик и не теряет его из-за попытки превратить поле ввода одновременно в журнал сообщений программы.
Документация и исходники
Механизмы главы сверены с первичными источниками: документация 1, документация 2, документация 3. Полный архив «Заметок читателя» содержит все двенадцать состояний. Установка, запуск и публикация не выполнялись.