В прошлой статье мы добавили поддержку тёмной темы на странице. Вот короткая версия:
- тёмная тема — это когда фон делают тёмным, а текст светлым, чтобы было удобно читать в темноте;
- браузеры умеют определять тёмную тему на устройстве и включать её на странице, если для этого на странице есть специальная настройка в CSS;
- вместо автоматического включения можно добавить ручной переключатель.
В прошлый раз мы проверили, как работает автопереключение, потом убрали его и добавили ручной режим. Сегодня объединим эти два подхода, а заодно научим браузер сохранять наши настройки.
Добавляем автопереключатель на страницу
Добавим автопереключение тёмной темы на страницу следующим способом:
- Мы оставляем ручной выбор темы на случай, если пользователь захочет сам выбрать, что ему удобнее.
- Ниже добавляем переключатель авторежима тёмной темы — когда он включён, используются системные настройки.
- После включения авторежима ручной выбор будет недоступен — мы его скроем с экрана.
- После выключения ручной выбор снова становится доступен.
Чтобы не городить с нуля элементы интерфейса, мы позаимствуем готовые компоненты из фреймворка Bootstrap 5. Оттуда нам нужен переключатель-слайдер. Мы вставим его в HTML-страницу после абзаца с ручным переключением. Сразу добавим вызов функции autoDarkLight()
при нажатии на переключатель:
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault" onclick="autoDarkLight()" >
<label class="form-check-label" for="flexSwitchCheckDefault">Автонастройка</label>
</div>
Настраиваем авторежим
Мы уже выяснили, что для автоматического включения тёмной темы достаточно добавить в стили такой медиазапрос:
@media screen and (prefers-color-scheme: dark) {
body {
background: black;
color: white;
}
}
Нам нужно добавить это в стили при включении авторежима и убрать оттуда при его выключении. Чтобы получить доступ к таблице стилей, добавим в самое начало скрипта dark.js
такое:
// находим стили по тегу
const style = document.getElementsByTagName("style")[0];
// получаем доступ к разделу стилей
const styleSheet = style.sheet;
Теперь наша задача — написать функцию autoDarkLight()
и добавить в неё всего одну проверку: нажат переключатель или нет. Если нажат — добавляем медиазапрос в стили, а если не нажат — убираем его оттуда.
Хитрость в том, что медиазапрос добавится в самый конец стилей, а значит, он применится самым последним. Получается, нам неважно, какие стили у страницы стояли до этого, — всё равно последним сработает наш автовыбор тёмной темы.
// переменная для временного хранения индекса медиазапроса
var temp;
function autoDarkLight() {
// если переключатель установлен
if (flag.checked) {
// создаём новый медиазапрос
const mediaRuleText = `@media screen and (prefers-color-scheme: dark) {
body {
background: black;
color: white;
}
}`;
// добавляем его на страницу
const mediaRuleIndex = styleSheet.insertRule(mediaRuleText);
// сохраняем индекс запроса в стилях
temp = mediaRuleIndex;
// скрываем ручной переключатель
p.style.visibility = "hidden";
// если переключатель не установлен
} else {
// удаляем медиазапрос по индексу
styleSheet.deleteRule(temp);
// показываем ручной переключатель
p.style.visibility = "visible";
}
}
Сохраняем настройки
Страница теперь умеет переключаться в тёмный режим и автоматически, и вручную, но при перезагрузке всё слетает — все режимы нужно заново включать самому.
Чтобы сайт запоминал, что мы сделали в настройках, используем локальное хранилище браузера (оно же localStorage). Логика будет такая:
- При каждом переключении мы записываем в хранилище текущее значение переключателя. Если в памяти нет вообще никакого значения, значит, мы ещё не трогали этот переключатель.
- Когда страница загрузилась, мы достаём из памяти по очереди значение каждого переключателя и применяем их на страницу.
Применяем — это значит, что мы виртуально нажимаем на переключатели, если они активны. Это позволяет сразу установить всё в то положение, которое было до перезагрузки. Читайте комментарии, чтобы разобраться в логике работы с хранилищем:
// выполняется сразу после загрузки страницы
window.addEventListener('DOMContentLoaded', (event) => {
// если в нашем хранилище есть информация о том, что мы установили вручную тёмную тему
if (localStorage.getItem('selected') == 'true') {
// виртуально нажимаем на ручной переключатель темы
darkLight();
// если такой информации там нет —
} else {
// сохраняем текущее значение темы
localStorage.setItem('selected',dark);
}
// если в нашем хранилище есть информация о том, что мы установили авторежим
if (localStorage.getItem('auto') == 'true') {
// виртуально включаем переключатель авторежима
flag.checked = true;
// вызываем обработчик автопереключателя
autoDarkLight();
// если такой информации там нет
} else {
// значит, мы просто сохраняем значение false – авторежим выключен
localStorage.setItem('auto',false);
}
});
Последнее, что нам осталось сделать, — сохранять в память текущее положение переключателей во время их изменения. Для этого добавим в самый конец функции darkLight() такую строку:
// сохраняем в памяти статус ручного переключателя
localStorage.setItem('selected',dark);
И то же самое сделаем в функции autoDarkLight() — добавим сохранение после установки видимости ручного переключателя:
localStorage.setItem('auto',true);
← если переключатель установлен;
localStorage.setItem('auto',false);
← если переключатель не установлен.
Результат
У нас получился красивый переключатель, который заставляет страницу слушаться системных настроек.
Также мы научились хранить состояние переключателей на устройстве.
Всё это живёт на нашей колхозной странице, на которую мы с каждым новым проектом добавляем всё больше элементов. Однажды она будет похожа на лавку торговца на турецком «Гранд базаре», а пока вот результат:
Посмотреть работу переключателей на странице проекта
Из интересного в этом проекте — симпатичный интерфейсный компонент в Bootstrap 5. В одной из будущих статей разберём, что там вообще интересного подвезли.
<!DOCTYPE html>
<html>
<!-- служебная часть -->
<head>
<!-- заголовок страницы -->
<title>Михаил Максимов — преподаватель информатики</title>
<!-- настраиваем служебную информацию для браузеров -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- загружаем Бутстрап -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<style type="text/css">
.theme-dark {
background: black;
color: white;
}
.theme-light {
background: white;
color: black;
}
img{
max-width: 100%;
max-height: 100%;
}
h1{
font-size:50px;
margin-top: 30px;
margin-bottom: 20px;
}
h2{
margin-top: 40px;
margin-bottom: 20px;
}
p {
font-size: 18px;
}
.switch {
cursor: help;
}
</style>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(80335906, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/80335906" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
<!-- Put this script tag to the <head> of your page -->
<script type="text/javascript" src="https://vk.com/js/api/openapi.js?169"></script>
<script type="text/javascript">
VK.init({apiId: 7836404, onlyWidgets: true});
</script>
</head>
<body id="main">
<div class="container" >
<div class="row">
<div class="col-12">
<div class="ya-site-form ya-site-form_inited_no" data-bem="{"action":"https://yandex.ru/search/site/","arrow":false,"bg":"transparent","fontsize":12,"fg":"#000000","language":"ru","logo":"rb","publicname":"Найти на сайте","suggest":true,"target":"_self","tld":"ru","type":2,"usebigdictionary":true,"searchid":2532273,"input_fg":"#000000","input_bg":"#ffffff","input_fontStyle":"normal","input_fontWeight":"normal","input_placeholder":"Поиск по сайту","input_placeholderColor":"#000000","input_borderColor":"#7f9db9"}"><form action="https://yandex.ru/search/site/" method="get" target="_self" accept-charset="utf-8"><input type="hidden" name="searchid" value="2532273"/><input type="hidden" name="l10n" value="ru"/><input type="hidden" name="reqenc" value=""/><input type="search" name="text" value=""/><input type="submit" value="Найти"/></form></div><style type="text/css">.ya-page_js_yes .ya-site-form_inited_no { display: none; }</style><script type="text/javascript">(function(w,d,c){var s=d.createElement('script'),h=d.getElementsByTagName('script')[0],e=d.documentElement;if((' '+e.className+' ').indexOf(' ya-page_js_yes ')===-1){e.className+=' ya-page_js_yes';}s.type='text/javascript';s.async=true;s.charset='utf-8';s.src=(d.location.protocol==='https:'?'https:':'http:')+'//site.yandex.net/v2.0/js/all.js';h.parentNode.insertBefore(s,h);(w[c]||(w[c]=[])).push(function(){Ya.Site.Form.init()})})(window,document,'yandex_site_callbacks');</script>
<p id="select" onclick="darkLight()" style="cursor: help;">Включить тёмную тему</p>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault" onclick="autoDarkLight()" >
<label class="form-check-label" for="flexSwitchCheckDefault">Автонастройка</label>
</div>
<h1>Михаил Максимов</h1>
</div>
</div>
</div>
<div class="container" >
<div class="row">
<div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
<p>Я преподаю информатику с 2008 года, когда предмет ещё назывался ИКТ. Начинал со школы, учил детей разбираться в программировании и сдавать ЕГЭ на 90 баллов и выше. За два года вывел нашу школу на второе место в районе по олимпиадам по информатике. Вёл два класса коррекции — пятый и одиннадцатый — и знаю, как объяснить основы теории вероятности даже тем, кто не хочет ничему учиться.</p>
<p>В 2012 защитил кандидатскую диссертацию по обучению информатике детей с недостатком внимания и стал внештатным преподавателем РГСУМ им. Макаренко. Параллельно с этим веду курсы по программированию «IDDQD» и записываю подкаст «Прогрокаст» с аудиторией 25 000 человек.</p>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
<img src="sq_me.jpg" >
</div>
</div>
</div>
<div class="container" >
<div class="row">
<div class="col-12">
<h2>Мои научные работы</h2>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3" >
<p><a href="https://thecode.media/baboolya/">Задача про бабушку и помидоры</a></p>
<p><a href="https://thecode.media/electrician/">Хитрый электрик</a></p>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3">
<p><a href="https://thecode.media/le-timer/">Как сделать свой таймер-напоминалку</a></p>
<p><a href="https://thecode.media/sublime-one-love/">Почему Sublime Text — это круто</a></p>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3">
<p><a href="https://thecode.media/est-tri-shkatulki/">Поговорим о Якубовиче</a></p>
<p><a href="https://thecode.media/content-manager/">Как стать контент-менеджером</a></p>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3">
<p><a href="https://thecode.media/batareyki-besyat/">Задача про сторожа и фонарик</a></p>
<p><a href="https://thecode.media/variables/">О названиях функций</a></p>
</div>
</div>
</div>
<div class="container" >
<div class="row">
<div class="col-12">
<h2>Контакты для связи</h2>
</div>
</div>
</div>
<div class="container" >
<div class="row">
<div class="col-12">
<p>Телефон: +7 (123) 456-78-90</p>
<p>Почта: <a href="mailto: mihailmaximov@gmail.com">mikemaximov@gmail.com</a></p>
<p>Скайп: mihailmaximov</p>
<p>Телеграм: @mihailmaximov</p>
</div>
</div>
</div>
<!-- Put this div tag to the place, where the Comments block will be -->
<div id="vk_comments"></div>
<script type="text/javascript">
VK.Widgets.Comments("vk_comments", {limit: 10, attach: "*"});
</script>
<script type="text/javascript" src="dark.js"></script>
</body>
<!-- конец всей страницы -->
</html>
// на старте тёмная тема не установлена
var dark = false;
// получаем доступ ко всей странице и к абзацу с переключателем
var a = document.body;
var p = document.getElementById("select")
// находим стили по тегу
const style = document.getElementsByTagName("style")[0];
// получаем доступ к разделу стилей
const styleSheet = style.sheet;
var flag = document.getElementById("flexSwitchCheckDefault");
// эта функция будет срабатывать при нажатии на переключатель
function darkLight() {
// если тёмная тема не активна
if (!dark) {
// добавляем класс с тёмной темой ко всей странице
a.className = "theme-dark";
// меняем надпись на переключателе
p.innerHTML = "Включить светлую тему";
// а если активна —
} else {
// добавляем класс со светлой темой ко всей странице
a.className = "theme-light";
// меняем надпись на переключателе
p.innerHTML = "Включить тёмную тему";
}
// меняем значение темы на противоположное
dark = !dark;
// сохраняем в памяти статус ручного переключателя
localStorage.setItem('selected',dark);
}
// переменная для временного хранения индекса медиазапроса
var temp;
function autoDarkLight() {
// если переключатель установлен
if (flag.checked) {
// создаём новый медиазапрос
const mediaRuleText = `@media screen and (prefers-color-scheme: dark) {
body {
background: black;
color: white;
}
}`;
// добавляем его на страницу
const mediaRuleIndex = styleSheet.insertRule(mediaRuleText);
// сохраняем индекс запроса в стилях
temp = mediaRuleIndex;
// скрываем ручной переключатель
p.style.visibility = "hidden";
// сохраняем положение переключателя в памяти браузера
localStorage.setItem('auto',true);
// если переключатель не установлен
} else {
// удаляем медиазапрос по индексу
styleSheet.deleteRule(temp);
// показываем ручной переключатель
p.style.visibility = "visible";
// сохраняем положение переключателя в памяти браузера
localStorage.setItem('auto',false);
}
}
// выполняется сразу после загрузки страницы
window.addEventListener('DOMContentLoaded', (event) => {
// если в нашем хранилище есть информация о том, что мы установили вручную тёмную тему
if (localStorage.getItem('selected') == 'true') {
// виртуально нажимаем на ручной переключатель темы
darkLight();
// если такой информации там нет —
} else {
// сохраняем текущее значение темы
localStorage.setItem('selected',dark);
}
// если в нашем хранилище есть информация о том, что мы установили авторежим
if (localStorage.getItem('auto') == 'true') {
// виртуально включаем переключатель авторежима
flag.checked = true;
// вызываем обработчик автопереключателя
autoDarkLight();
// если такой информации там нет
} else {
// значит мы просто сохраняем значение false – авторежим выключен
localStorage.setItem('auto',false);
}
});