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

§ 10. Визуальное веб-конструирование

Соберём учебную страницу из блоков, изменим порядок и оформление, сравним широкий и узкий предпросмотр.

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

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

1

Визуальный метод

Конструктор позволяет управлять блоками и настройками, генерируя разметку и оформление.

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

Визуальный метод

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

WYSIWYG означает близость редактируемого представления к результату. Автор выбирает блоки, меняет текст и изображения, настраивает расположение. При этом инструмент создаёт HTML и CSS, а иногда и сценарии. Знание структуры страницы помогает понять ограничения конструктора и исправить результат.

Визуальный редактор может работать локально, онлайн-конструктор — через сервис. Возможности экспорта, домена, размещения и резервного копирования зависят от инструмента и условий использования. Нельзя считать, что любой конструктор бесплатно экспортирует сайт или автоматически делает его быстрым.

Главная мысль: Конструктор позволяет управлять блоками и настройками, генерируя разметку и оформление.
2

Структура и шаблон

Шаблон служит исходной заготовкой, которую адаптируют под цель и содержание.

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

Структура и шаблон

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

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

Внутри страницы сначала расставляют смысловые блоки: заголовок, краткое введение, иллюстрация, факты, переходы. Перетаскивание не является единственным способом: кнопки перемещения вверх и вниз удобны с клавиатуры и на телефоне. В тренажёре используется именно такой доступный способ.

Главная мысль: Шаблон служит исходной заготовкой, которую адаптируют под цель и содержание.
3

Содержание и единый стиль

Настройки темы помогают сохранять согласованность, но не заменяют проверку текста и изображений.

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

Содержание и единый стиль

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

Задайте ограниченный набор цветов, шрифтов и размеров заголовков. Одинаковые кнопки должны означать сходные действия. Изображения подбирают по смыслу, подписям и разрешённому использованию. Не оставляйте шаблонные телефоны, имена и пустые заглушки на готовой странице.

Большой набор декоративных эффектов не гарантирует хорошего дизайна. Проверяйте контраст, длину строки, размеры кнопок и порядок чтения. Изменение общей темы должно согласованно применяться к повторяющимся блокам. Частные переопределения используют осознанно, иначе страницы начинают выглядеть несвязанными.

Главная мысль: Настройки темы помогают сохранять согласованность, но не заменяют проверку текста и изображений.
4

Предпросмотр и готовность

Предпросмотр проверяет внешний вид, публикация делает сайт доступным по выбранному адресу.

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

Предпросмотр и готовность

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

После сборки проверяют широкий и узкий экран, длинные заголовки, изображения, ссылки и кнопки. Уменьшенная картинка широкого сайта не заменяет адаптивную перестройку. При изменении ширины блоки могут переходить в одну колонку, а меню — становиться компактнее.

Предпросмотр в конструкторе не равен публикации. Публикация, экспорт и резервная копия также различаются. Перед выпуском сохраняют рабочую версию и проверяют уже размещённый результат. Этот учебный конструктор меняет только локальную модель внутри конспекта и не публикует сайт в Интернет.

  1. Предпросмотр

    Проверить широкий и узкий экран.

  2. Проверка

    Пройти ссылки и выполнить действия.

  3. Сохранение

    Сохранить проект и резервную версию.

  4. Публикация

    Разместить сайт и проверить адрес.

Главная мысль: Предпросмотр проверяет внешний вид, публикация делает сайт доступным по выбранному адресу.
Интерактивная практика

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

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

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

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

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

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

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

01

Конструктор позволяет управлять блоками и настройками, генерируя разметку и оформление.

02

Шаблон служит исходной заготовкой, которую адаптируют под цель и содержание.

03

Настройки темы помогают сохранять согласованность, но не заменяют проверку текста и изображений.

04

Предпросмотр проверяет внешний вид, публикация делает сайт доступным по выбранному адресу.

05

Скорость и качество результата зависят от содержания и настроек, а не только от выбранного инструмента.

06

Предпросмотр, сохранение, экспорт и публикация — разные действия.

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

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

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

1. Что означает WYSIWYG?

2. Что задают прежде оформления?

3. Что делать с лишними шаблонными блоками?

4. Любой конструктор бесплатно экспортирует сайт?

5. Зачем общая тема оформления?

6. Что проверить после удаления страницы?

7. Предпросмотр равен публикации?

8. Уменьшенная широкая страница всегда адаптивна?

9. Чем можно заменить перетаскивание для порядка блоков?

10. Что важно перед выпуском?

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

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

\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(); });})();