ИНФОРМАТИКА · 11 КЛАСС
Веб-конструирование · Интерактивный конспект

§ 8. Понятие о каскадных таблицах стилей

Изменим оформление одного и того же HTML, исследуем каскад и наглядно измерим внутренние и внешние отступы.

Попробовать на модели ↓
Разбираемся в теме

Основные пункты параграфа

1

Правило и подключение стилей

CSS-правило связывает селектор с набором свойств и значений.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Правило и подключение стилей

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

В записи p { color: green; } селектор p выбирает абзацы, color — свойство, green — значение. Объявления разделяют точкой с запятой и заключают в фигурные скобки. Длина записывается 10px, без пробела между числом и единицей.

Стиль можно задать атрибутом style, блоком style в head либо внешним файлом .css через link rel="stylesheet" href="styles.css". Внешний файл удобен для единого оформления нескольких страниц. Сам способ хранения во внешнем файле не делает правило автоматически слабее любого правила в блоке style.

<link rel="stylesheet" href="styles.css">

/* В файле styles.css */
.note {
  color: #23452f;
  background: #edf4eb;
  padding: 16px;
}
Главная мысль: CSS-правило связывает селектор с набором свойств и значений.
2

Селекторы и каскад

При конфликте объявлений учитывают условия каскада, затем специфичность и порядок.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Селекторы и каскад

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

Для обычных авторских правил одного слоя, без !important, анимаций и переходов, селектор id специфичнее класса, а класс — селектора типа. При одинаковой специфичности побеждает более позднее правило. Обычный встроенный style имеет преимущество перед обычными правилами таблиц стилей.

В полной системе также учитываются происхождение, важность и слои. Поэтому правило «внешний CSS, затем style, затем атрибут» слишком грубо. Каскад разрешает конфликт для каждого свойства отдельно: цвет может прийти из одного правила, размер — из другого. Наследование переносит некоторые свойства от родителя, когда собственное значение не задано.

Главная мысль: При конфликте объявлений учитывают условия каскада, затем специфичность и порядок.
3

Шрифт, цвет и читаемость

Оформление должно поддерживать структуру и сохранять удобство чтения.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Шрифт, цвет и читаемость

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

font-family задаёт семейство шрифта, font-size — размер, font-weight — насыщенность, color — цвет текста, line-height — межстрочный интервал. text-align управляет выравниванием. Фон задают через background или background-color. Недостаточный контраст мешает чтению даже при правильном коде.

Класс .note можно назначить нескольким элементам, а одному элементу — несколько классов через пробел. id идентифицирует один элемент в документе. Для учебных имён удобно начинать с латинской буквы и использовать буквы, цифры, дефис и подчёркивание; реальные правила CSS шире этого упрощения.

Главная мысль: Оформление должно поддерживать структуру и сохранять удобство чтения.
4

Блочная модель и адаптивность

Контент, padding, border и margin занимают разные области вокруг элемента.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Блочная модель и адаптивность

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

При box-sizing: content-box заданная width относится к содержимому. Полная ширина рамки равна width плюс левый и правый padding и border. margin находится снаружи. При border-box padding и border входят в заданную ширину, пока размеры позволяют разместить их.

Для четырёх значений отступа порядок: сверху, справа, снизу, слева. Два значения задают вертикальные и горизонтальные отступы. Вертикальные margin некоторых обычных блоков могут схлопываться; не всякое расстояние равно простой сумме. Адаптивная страница использует гибкие размеры и медиазапросы, а затем проверяется в реальном узком окне.

Величина при width=200pxcontent-boxborder-box
Содержимое при padding 10px и border 2px200px176px
Ширина вместе с рамкой224px200px
Главная мысль: Контент, padding, border и margin занимают разные области вокруг элемента.
Интерактивная практика

Исследуйте и примените

Проверьте модель на нескольких наборах данных, затем выполните самостоятельное задание.

НАБЛЮДАЙТЕ И ПРОБУЙТЕ

Лаборатория: проверьте свой вариант

Меняйте данные и выполняйте шаги. Цвет выделяет текущие элементы; подписи объясняют результат. Сброс возвращает исходный опыт.

Соберём главное

Шесть выводов

01

CSS-правило связывает селектор с набором свойств и значений.

02

При конфликте объявлений учитывают условия каскада, затем специфичность и порядок.

03

Оформление должно поддерживать структуру и сохранять удобство чтения.

04

Контент, padding, border и margin занимают разные области вокруг элемента.

05

Пробел между числом и px делает обычную запись длины недопустимой.

06

Проверяйте оформление при изменении ширины окна и длины текста.

Самопроверка

Тест: 10 вопросов

Один верный ответ. За ответ — 0,5 балла. Откройте и проверьте себя.

1. Что является селектором в p {color:green}?

2. Как корректно записать длину?

3. Как подключают внешний CSS?

4. Всегда ли внешний CSS слабее блока style?

5. При прочих равных класс специфичнее…

6. При одинаковой специфичности побеждает…

7. Что задаёт padding?

8. Что задаёт margin?

9. Ширина content-box 200, padding 10, border 2 с каждой стороны?

10. Полная ширина элемента при border-box width=200px в том же примере?

ВыводыТестВ началоВсе параграфы
↑

Загрузка прогресса…

\n'));msg='title относится к метаданным документа, h1 — к видимому содержимому. Текст безопасно выводится как текст.';return} input('path','Фрагмент ссылки',30);html=pad(code('Перейти\n

Раздел

')+stats([['Совпадение фрагмента и id','одинаковые записи'],['Проверка учебного идентификатора',/^[a-zA-Z][\w-]*$/.test(S.path)?'подходит':'начните с латинской буквы; затем буквы, цифры, _ или -']]));msg='Это упрощённый фильтр для учебных примеров, не полная грамматика HTML id. Идентификатор должен быть уникален на странице.';} function editor(){if(m===1){next();html=flow(['Написать HTML','Сохранить .html в UTF-8','Открыть в браузере','Исправить и обновить'],S.step%4)+pad('

'+['Код описывает структуру.','Проверьте, что имя не заканчивается на .html.txt.','Браузер отображает документ, а не редакторский проект.','После изменения файла обновите открытую страницу.'][S.step%4]+'

');msg='Редактор и браузер выполняют разные функции.';return} if(m===2){check('aside','Добавить aside');html=pad('
header — заголовок сайта
main — основное содержание
'+(S.aside?'':'')+'
'+code('
…
\n\n
…
\n'+(S.aside?'\n':'')+''));msg='Смысловые элементы задают назначение областей. Их расположение определяется CSS.';return} if(m===4){select('path','Относительная ссылка',[['images/logo.svg','images/logo.svg'],['../index.html','../index.html'],['#team','#team'],['contacts.html','contacts.html']]);html=pad(stats([['Текущая страница','https://school.example/club/about.html'],['Результат',new URL(S.path,'https://school.example/club/about.html').href]])+code('Ссылка'));msg='Адрес вычисляется относительно текущей страницы. Модель не обращается к сайту; ../ поднимает на один уровень.';return} area('source','Редактор фрагмента HTML');html=pad('
'+cleanHTML(S.source,'preview-'+m+'-')+'
');msg='Поддерживаются заголовки, абзацы, списки, смысловые области, выделение и внутренние ссылки. Скрипты, стили, внешние ссылки и опасные элементы удаляются из предпросмотра. Это ограниченный учебный редактор.';} function css(){if(m===1||m===3){range('size','Размер текста, px',14,36);select('color','Цвет текста',[['#294d3a','Тёмно-зелёный'],['#704522','Коричневый'],['#304f76','Синий']]);html=pad('

Стиль меняет представление

'+code('.note {\n color: '+S.color+';\n font-size: '+S.size+'px;\n}'));msg=m===1?'Селектор выбирает элемент, декларации задают свойства. Число и px записываются слитно.':'Одно правило можно вынести во внешний файл и применить на нескольких страницах. Модель показывает результат деклараций.';return} if(m===2||m===5){check('classRule','Правило класса .note');check('idRule','Правило идентификатора #sample');check('inlineRule','Обычный встроенный style');check('late','Позднее правило класса');let winner=S.inlineRule?'style':S.idRule?'#sample':S.classRule?S.late?'.note (позднее)':'.note':'p',color=S.inlineRule?'#304f76':S.idRule?'#704522':S.classRule?S.late?'#654777':'#34683e':'#294d3a',uid='css-sample-'+m;let rules=`#vis-${m} .css-example p {color:#294d3a}`+(S.classRule?`\n#vis-${m} .css-example .note {color:#34683e}`:'')+(S.idRule?`\n#vis-${m} .css-example #${uid} {color:#704522}`:'')+(S.classRule&&S.late?`\n#vis-${m} .css-example .note {color:#654777}`:'');html=pad('

Какой цвет победит?

'+stats([['Победитель',winner],['Ожидаемый цвет',color]])+code('p { color: #294d3a; }\n'+(S.classRule?'.note { color: #34683e; }\n':'')+(S.idRule?'#sample { color: #704522; }\n':'')+(S.classRule&&S.late?'.note { color: #654777; }\n':'')+(S.inlineRule?'style="color:#304f76"':'')));msg='Условия опыта: обычные авторские правила одного слоя, без !important, анимаций и переходов. При равной специфичности побеждает более позднее правило.';return} range('width','Заданная width, px',100,300);range('padding','Padding с каждой стороны, px',0,30);range('border','Border с каждой стороны, px',0,10);select('box','box-sizing',[['content-box','content-box'],['border-box','border-box']]);let outer=S.box==='content-box'?S.width+2*S.padding+2*S.border:S.width,inner=S.box==='content-box'?S.width:S.width-2*S.padding-2*S.border;html=pad('
Содержимое
'+stats([['Содержимое',inner+' px'],['Полная ширина',outer+' px']])+code('width: '+S.width+'px;\npadding: '+S.padding+'px;\nborder: '+S.border+'px solid;\nbox-sizing: '+S.box+';'));msg='Расчёт по горизонтали: содержимое + два отступа + две границы. Внешние margin в эту ширину не входят.';} function media(){if(m===5)select('kind','Медиа',[['image','Изображение'],['audio','Звук'],['video','Видео']]);if(m===1){check('decorative','Изображение только декоративное');input('alt','Текст alt',100);html=pad(''+esc(S.decorative?'':S.alt)+''+code(''+(S.decorative?'':S.alt)+'')+'

Текстовая альтернатива: '+esc(S.decorative?'пустая — декор пропускается':S.alt||'не задана')+'

');msg=S.decorative?'Пустой alt уместен для декора, когда изображение не добавляет смысла.':'Опишите смысл изображения в контексте; слово «картинка» само по себе не помогает.';return} if(S.kind==='image'){select('fit','object-fit',[['contain','contain'],['cover','cover'],['fill','fill']]);html=pad('
Пейзаж: горы и солнце
'+stats([['Источник','400 × 200'],['Рамка','200 × 200'],['Изображение',S.fit==='contain'?'200 × 100':S.fit==='cover'?'400 × 200, обрезка':'200 × 200, искажение']]));msg='contain сохраняет всё изображение, cover заполняет рамку с обрезкой, fill меняет пропорции.';return} let audio=S.kind==='audio';html=pad('<'+(audio?'audio':'video')+' class="local-media" controls preload="metadata" '+(!audio?'playsinline':'')+' src="'+(audio?'':'')+'">

'+(audio?'Звук: негромкий тон 440 Гц, 2 секунды.':'Видео: жёлтый круг движется по зелёному фону, 3 секунды; без звука.')+'

'+code('<'+(audio?'audio':'video')+' controls preload="metadata">\n \n'));msg='Нажмите встроенную кнопку воспроизведения. Запись включена в HTML; Интернет не нужен. Запуск звука остаётся под вашим управлением. Если формат не поддерживается браузером, доступно текстовое описание.';} function builder(){input('title','Название сайта',40);select('theme','Общий стиль',[['green','Зелёный'],['sand','Песочный'],['blue','Голубой']]);range('previewWidth','Ширина предпросмотра, px',240,600,10);select('selected','Блок для перестановки',S.blocks.map((x,i)=>[i,x]));ct+=btn('up','Поднять блок')+btn('down','Опустить блок');if(m>=3)check('aside','Показывать блок контактов');html=pad('
'+esc(S.title)+'
'+S.blocks.filter(x=>S.aside||x!=='Контакты').map(x=>'
'+esc(x)+'

'+({'О клубе':'Встречаемся, учимся и создаём.','Проекты':'Делимся результатами работы.','Контакты':'Как найти нашу команду.'}[x])+'

').join('')+'
'+stats([['Блоков',S.blocks.filter(x=>S.aside||x!=='Контакты').length],['Колонок',S.previewWidth<400?1:2]]));msg=['Визуальный конструктор собирает страницу из блоков. Измените заголовок и сравните результат.','Шаблон задаёт общую структуру; содержание нужно подготовить самостоятельно.','Меняйте порядок блоков. Общая тема применяется ко всему макету.','Проверьте узкую ширину: карточки перестраиваются в одну колонку.','Это локальный макет. Предпросмотр не публикует сайт в Интернете.'][m-1];} function site(){if(m===1){select('kind','Аудитория',[['','Новые участники'],['parents','Родители'],['team','Команда проекта']]);let k={'':0,parents:1,team:2}[S.kind];html=flow(['Аудитория','Задача','Раздел','Проверка'],S.step%4)+pad(table(['Решение','Пример'],[['Задача',['Понять, как вступить','Узнать расписание','Найти материалы'][k]],['Нужный раздел',['О клубе и контакты','Расписание','Проекты и ресурсы'][k]],['Критерий',['Путь к контакту найден','Время и место понятны','Материал открывается'][k]]]));next();msg='Структура сайта зависит от конкретной задачи посетителя.';return} check('links','Главная → Проекты');check('back','Проекты → Контакты');if(m>=4){check('goal','Проверено содержание');check('mobile','Проверена узкая версия');}let reachable=['Главная',...(S.links?['Проекты']:[]),...(S.links&&S.back?['Контакты']:[])];html=svg([80,320,560].map((x,i)=>''+tx(x,137,['Главная','Проекты','Контакты'][i],15)).join('')+''+tx(200,105,S.links?'есть ссылка':'нет ссылки',13)+tx(440,105,S.back?'есть ссылка':'нет ссылки',13))+pad(stats([['Достижимы с главной',reachable.length+' из 3'],['Не достижимы',['Главная','Проекты','Контакты'].filter(x=>!reachable.includes(x)).join(', ')||'нет']])+(m>=4?table(['Проверка','Результат'],[['Все разделы достижимы',reachable.length===3?'да':'нет'],['Содержание',S.goal?'проверено':'нужна проверка'],['Мобильная версия',S.mobile?'проверено':'нужна проверка']]):''));msg='В схеме только показанные направленные ссылки. Доступность файла сама по себе не означает, что посетитель найдёт путь к нему. Контрольный список не гарантирует отсутствие всех ошибок.';} function systems(){if(m===1||m===2||m===5){input('query','Поиск по названию',35);select('category','Раздел',[['all','Все'],['code','Программирование'],['web','Веб'],['society','Общество']]);check('available','Только доступные');const books=[['HTML с примерами','web',true],['Алгоритмы','code',true],['Информация и человек','society',false],['CSS на практике','web',false],['Язык Pascal','code',true]];let found=books.filter(x=>x[0].toLowerCase().includes(S.query.toLowerCase())&&(S.category==='all'||x[1]===S.category)&&(!S.available||x[2]));html=flow(['Данные каталога','Условия запроса','Обработка','Результат'],3)+pad(table(['Название','Раздел','Доступно'],found.map(x=>[x[0],{web:'Веб',code:'Программирование',society:'Общество'}[x[1]],x[2]?'да':'нет']))+stats([['Всего записей',books.length],['Найдено',found.length]])+'

Учебный вымышленный каталог. Данные находятся внутри страницы.

');msg='Фильтры соединены условием И. Система включает не только таблицу, но и правила, программное обеспечение и работу людей.';return} if(m===3){select('kind','Технология',[['cloud','Облачная услуга'],['grid','Распределённые вычисления'],['ar','Дополненная реальность'],['vr','Виртуальная реальность']]);let a={cloud:['Удалённый ресурс по сети','Сервис предоставляет хранение или обработку; условия доступа и защиты проверяют отдельно.'],grid:['Части вычислительной задачи','Несколько узлов совместно обрабатывают распределённую работу.'],ar:['Реальная среда + цифровой слой','Подсказки накладываются на вид окружающего мира.'],vr:['Искусственная среда','Пользователь взаимодействует с виртуальным пространством.']}[S.kind];html=flow(['Задача','Технология','Результат'],1)+pad('
'+a[0]+'

'+a[1]+'

');msg='Выбирают технологию по задаче и ограничениям, а не по названию.';return} choices('Библиотека предоставляет читателю поиск по электронному каталогу. Что является услугой?',['Сам поиск, предоставленный читателю','Набор записей каталога','Готовый справочник'],0,'Каталог — ресурс, готовый справочник может быть продуктом, предоставляемый поиск — услуга. Классификация зависит от роли в ситуации.');} function society(){if(m===1||m===2||m===5){check('training','Сотрудники обучены');check('access','Есть доступный интерфейс');check('backup','Есть план при отказе');let time=S.training?5:14,errors=S.training?1:4,excluded=S.access?0:3;html=flow(['Подготовка','Цифровой процесс','Проверка результата'],2)+pad(stats([['Условное время',time+' мин'],['Ошибок в учебном сценарии',errors],['Участников без доступа',excluded],['При отказе',S.backup?'переход к резервному порядку':'работа останавливается']])+'

Числа заданы условиями вымышленного опыта. Это не статистика и не прогноз.

');msg='Установка техники сама по себе не решает вопросы навыков, доступности и непрерывности работы.';return} if(m===3){choices('Найден яркий пост без автора и даты. Как использовать утверждение из него?',['Сначала проверить первоисточник и подтверждения','Считать верным из-за популярности','Переслать без проверки'],0,'Проверьте автора, дату, доказательства и независимые подтверждения. Популярность не заменяет проверку.');return} choices('Сервис принимает заявления только онлайн. Часть людей не имеет устойчивой связи. Что улучшит доступ?',['Добавить доступный альтернативный канал','Увеличить число рекламных баннеров','Скрыть инструкцию за регистрацией'],0,'Альтернативный канал и понятный доступный интерфейс помогают уменьшить исключение людей из процесса.');} function education(){if(m===1||m===4||m===5){range('reading','Изучение примера, минут',0,60,5);range('practice','Самостоятельная практика, минут',0,60,5);range('review','Проверка и исправления, минут',0,30,5);let sum=S.reading+S.practice+S.review;html=flow(['Понять пример','Выполнить самостоятельно','Проверить','Исправить'],S.step%4)+pad(stats([['Всего времени',sum+' мин'],['Практика',S.practice?'запланирована':'отсутствует'],['Обратная связь',S.review?'есть время':'не предусмотрена']])+'

Проверяемая цель: сделать адаптивную карточку и показать её при ширине 360 и 1200 пикселей.

');next();msg=sum===0?'План пуст. Добавьте время на действия.':!S.practice?'Одно чтение не подтверждает освоение. Добавьте самостоятельную задачу.':!S.review?'Оставьте время на проверку и исправления.':'План содержит изучение, практику и проверку. Его длительность не гарантирует освоение — нужен работающий результат.';return} if(m===2){select('connection','Условия',[['slow','Связь нестабильная'],['live','Нужен общий разговор сейчас'],['flex','Участники учатся в разное время']]);let k={slow:0,live:1,flex:2}[S.connection];html=pad(stats([['Подходящий вариант',['Загружаемый текст + отложенные ответы','Синхронное занятие','Асинхронное задание'][k]]])+'

'+['Материалы доступны после загрузки; вопросы можно передать позже.','Общение происходит одновременно; важны связь и общее расписание.','Каждый выбирает время, но срок и критерии результата остаются ясными.'][k]+'

');msg='Формат выбирают по условиям и задаче. Дистанционное обучение может быть и синхронным, и асинхронным.';return} select('kind','Задача',[['','Написать и проверить обработчик'],['design','Спроектировать понятную форму'],['data','Найти закономерности в данных'],['security','Подготовить план восстановления']]);html=pad(stats([['Основной акцент',{'':'Разработка',design:'Дизайн взаимодействия',data:'Аналитика',security:'Безопасность'}[S.kind]]])+'

Доказательство навыка: '+{'':'работающий пример и проверки пограничных значений.',design:'макет и результаты проверки с пользователями.',data:'воспроизводимый расчёт и объяснение ограничений.',security:'проверенный порядок восстановления и разбор результата.'}[S.kind]+'

');msg='Роли пересекаются. Название профессии не заменяет знакомство с её задачами.';} function security(){if(m===1){select('kind','Происшествие',[['','Посторонний прочитал закрытые данные'],['edit','Запись изменена без разрешения'],['down','Сервис перестал отвечать']]);html=flow(['Конфиденциальность','Целостность','Доступность'],{'':0,edit:1,down:2}[S.kind])+pad('

Основная нарушенная цель: '+{'':'конфиденциальность',edit:'целостность',down:'доступность'}[S.kind]+'.

');msg='Один реальный инцидент может нарушать несколько целей. Здесь выделено прямое последствие условия.';return} if(m===2){choices('В программе есть известная неисправленная ошибка защиты. Как назвать это слабое место?',['Уязвимость','Уже произошедший инцидент','Резервная копия'],0,'Уязвимость — слабое место. Возможное воздействие — угроза; произошедшее нарушение — инцидент.');return} if(m===3){choices('Знакомый аккаунт просит срочно прислать код входа. Ваше действие?',['Не передавать код; проверить через известный контакт','Отправить код, имя выглядит знакомым','Перейти по присланной ссылке и ввести пароль'],0,'Имя и фото могут быть подменены. Проверяйте через заранее известный независимый канал; код входа не сообщают собеседнику.');return} check('backup','Создана резервная копия');check('independent','Копия отделена от рабочей среды');check('tested','Восстановление проверено');range('detect','Обнаружение и решение, часов',1,8);range('restore','Восстановление по плану, часов',1,12);let valid=S.backup&&S.independent,known=valid&&S.tested;html=flow(['Подготовиться','Сохранить важные функции','Восстановить','Улучшить'],S.step%4)+pad(stats([['Состояние копии',!S.backup?'нет':!S.independent?'зависит от той же среды':!S.tested?'не проверена':'проверена и отделена'],['Условный простой',known?S.detect+S.restore+' ч':'время не определено условиями']])+'

В учебном сценарии простой = обнаружение и решение + восстановление. В реальности сроки зависят от многих факторов; это не прогноз риска.

');next();msg=known?'После восстановления выясняют причину и улучшают защиту.':!valid?'Синхронизация в той же среде может распространить повреждение. Нужна независимая копия.':'Наличие копии не подтверждает возможность восстановления. Нужна проверка.';} function hints(){stage.querySelectorAll('.v9-scroll,.site-scroll,.box-scroll').forEach(el=>{if(el.scrollWidth>el.clientWidth+1){el.tabIndex=0;el.setAttribute('aria-label','Область с горизонтальной прокруткой');let h=document.createElement('div');h.className='v9-scroll-hint';h.textContent='← Можно прокручивать в стороны →';el.after(h)}})} function render(keep=false){const restoreFocus=retainWorkshopFocus(root);html='';ct='';msg='';[null,events,designer,controls,graphics,apps,webConcepts,editor,css,media,builder,site,systems,society,education,security][n]();stage.innerHTML=html;feed.textContent=msg;let frag=document.createElement('div');frag.innerHTML=ct;const signature=b=>[...b.querySelectorAll('input,select,textarea,button')].map(el=>[el.tagName,el.type,el.dataset.key||el.dataset.act].join(':')).join('|');if(keep&&signature(tools)===signature(frag)){tools.querySelectorAll('[data-key]').forEach(old=>{let nxt=frag.querySelector('[data-key="'+old.dataset.key+'"]'+(old.type==='radio'?'[value="'+old.value+'"]':''));let st=old.closest('label').querySelector('strong'),ns=nxt?.closest('label').querySelector('strong');if(st&&ns)st.textContent=ns.textContent;if(nxt&&document.activeElement!==old){if(old.type==='checkbox'||old.type==='radio')old.checked=nxt.checked;else old.value=nxt.value}})}else{let old=document.activeElement,k=tools.contains(old)?old.dataset.key:null,start=old?.selectionStart,end=old?.selectionEnd;tools.innerHTML=ct;if(keep&&k){let el=tools.querySelector('[data-key="'+k+'"]');el?.focus({preventScroll:true});if(start!=null)try{el.setSelectionRange(start,end)}catch(e){}}}hints();root.dataset.state=JSON.stringify(S);bindDrawing();restoreFocus();stage.querySelectorAll('audio,video').forEach(el=>el.addEventListener('error',()=>{feed.textContent='Браузер не смог воспроизвести формат. Текстовое описание записи приведено рядом.'}));} function stop(){clearInterval(timer);timer=null;S.running=false;stage.querySelectorAll('audio,video').forEach(e=>e.pause())} function tick(){S.position+=S.distance;if(S.position>620)S.position=20;let dot=stage.querySelector('[data-moving]');if(dot)dot.setAttribute('cx',S.position);root.dataset.state=JSON.stringify(S)} function record(s){S.log.push(s);if(S.log.length>8)S.log.shift()} function action(a){if(a==='focus'){let b=stage.querySelector('[data-act="click"]');if(b&&!b.disabled){b.focus();feed.textContent='Focus() перевёл фокус на кнопку.'}else feed.textContent='Скрытая или недоступная кнопка не принимает фокус.';return}if(a==='step')S.step++;else if(a.startsWith('answer'))S.answer=Number(a.slice(6));else if(a==='click'){if(n===6){S.count++;render();return}if(S.enabled){if(S.linked){S.count++;record('button1 · Click → значение '+S.count)}else record('Click: пользовательский обработчик не назначен')}}else if(a==='event'){if(S.event==='Click'){if(S.enabled&&S.visible){if(S.linked){S.count++;record('button1 · Click → '+S.count)}else record('Click: нет назначенного обработчика')}else record('Кнопка не доступна для активации')}else record((S.event==='Load'?'form1':'timer1')+' · '+S.event+' → запись в журнал')}else if(a==='zero'){S.count=0;record('resetButton · Click → 0')}else if(a==='greet')record(S.name.trim()?'Здравствуйте, '+S.name.trim()+'!':'Сначала введите имя.');else if(a==='play'){if(S.running)stop();else{S.running=true;timer=setInterval(tick,S.interval)}}else if(a==='tick')tick();else if(a==='dialog'){S.chosen=S.color;S.dialog=true}else if(a==='apply'){S.color=S.chosen;S.dialog=false}else if(a==='cancel')S.dialog=false;else if(a==='save')S.saved=S.draft;else if(a==='restore')S.draft=S.saved;else if(a==='undo')S.strokes.pop();else if(a==='clearDrawing')S.strokes=[];else if(a==='up'||a==='down'){let i=S.selected,j=i+(a==='up'?-1:1);if(j>=0&&j{let r=board.getBoundingClientRect();return[Math.max(0,Math.min(600,(e.clientX-r.left)/r.width*600)),Math.max(0,Math.min(260,(e.clientY-r.top)/r.height*260))]};board.addEventListener('pointerdown',e=>{if(e.button!==0)return;e.preventDefault();board.setPointerCapture(e.pointerId);drawing=[point(e)];drawing.push([drawing[0][0]+.1,drawing[0][1]+.1]);path=document.createElementNS('http://www.w3.org/2000/svg','polyline');path.setAttribute('fill','none');path.setAttribute('stroke','#356b49');path.setAttribute('stroke-width','5');path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('points',drawing.map(p=>p.join(',')).join(' '));board.append(path)});board.addEventListener('pointermove',e=>{if(!drawing)return;drawing.push(point(e));path.setAttribute('points',drawing.map(p=>p.join(',')).join(' '))});const end=()=>{if(!drawing)return;S.strokes.push(drawing);drawing=null;root.dataset.state=JSON.stringify(S)};board.addEventListener('pointerup',end);board.addEventListener('pointercancel',end)} root.addEventListener('click',e=>{let b=e.target.closest('[data-act]');if(b&&root.contains(b))action(b.dataset.act);let a=e.target.closest('.web-preview a');if(a){e.preventDefault();let id=a.getAttribute('href')?.slice(1),target=id?document.getElementById(id):null;target?.scrollIntoView({block:'nearest'})}}); root.addEventListener('input',e=>{if(e.target.hasAttribute('data-demo-input')){record('textBox1 · TextChanged → '+e.target.value);stage.querySelector('.event-output').textContent=S.log.at(-1);root.dataset.state=JSON.stringify(S)}}); function update(e){let el=e.target,k=el.dataset.key;if(!k)return;let v=el.type==='checkbox'?el.checked:el.value;if(['range','number'].includes(el.type)){if(v===''||!Number.isFinite(Number(v))||Number(v)Number(el.max)){feed.textContent='Введите число от '+el.min+' до '+el.max+'. Пока используется предыдущее значение.';return}v=Number(v)}else if((el.tagName==='SELECT'||el.type==='radio')&&typeof S[k]==='number')v=Number(v);if(S.running)stop();S[k]=v;S.answer=-1;render(el.tagName!=='SELECT'&&el.type!=='checkbox')} tools.addEventListener('input',e=>{if(e.target.tagName!=='SELECT'&&e.target.type!=='checkbox')update(e)});tools.addEventListener('change',e=>{if(e.target.type==='number'){let el=e.target,v=el.value===''?S[el.dataset.key]:Number(el.value);if(!Number.isFinite(v))v=S[el.dataset.key];let step=Number(el.step)||1;el.value=Number(Math.min(Number(el.max),Math.max(Number(el.min),Number(el.min)+Math.round((v-Number(el.min))/step)*step)).toFixed(6));update(e)}else if(e.target.tagName==='SELECT'||e.target.type==='checkbox')update(e)}); root.querySelector('.iv-reset').onclick=()=>{stop();S=structuredClone(base);render()};root.addEventListener('pause-diagram',()=>{if(S.running){stop();render()}});document.addEventListener('visibilitychange',()=>{if(document.hidden){stop();if(n===4)render()}});init();render(); });})();