Выделение, порядок и свойства фигур
Перетаскивание удобно для примерного расположения, но не заменяет редактор свойств. Теперь добавим список фигур, числовые поля, подпись, цвет и управление слоями. Все действия будут менять ту же модель, которую рисует SVG. К концу урока пользователь сможет выбрать фигуру через обычную HTML-кнопку и применить точные координаты без движения указателя по полотну.
Весь интерфейс находится в index.html и main.mjs архива серии. Пример пока не запускался. Следующие сценарии описывают ожидаемый результат. Используем операции модели из прошлых уроков; историю изменений подробно разберём далее.
Выделение хранит идентификатор
Текущее выделение — это строка selected или null. Мы не сохраняем отдельную копию выбранной фигуры. При обновлении интерфейса находим объект в актуальном документе по id. Это позволяет корректно работать после отмены, импорта и перестановки массива: выбранная фигура может получить новые свойства, но связь остаётся по идентификатору.
Если после удаления или импорта такого идентификатора больше нет, redraw() устанавливает selected = null. Панель свойств отключается. Нельзя оставлять форму связанной с объектом, который уже исчез из документа: следующее применение значений изменило бы устаревшую копию или завершилось обращением к отсутствующему элементу.
Рендерер получает выделение отдельно и показывает обводку. Список слоёв использует aria-pressed, чтобы состояние кнопки было доступно не только визуально. Выбор фигуры через список переводит фокус в поле x, откуда пользователь может начать точное редактирование. Это конкретное продолжение действия, а не попытка сделать весь рисунок интерактивным для клавиатуры сразу.
Список показывает верхние фигуры первыми
Документ хранит порядок от нижнего слоя к верхнему. В списке удобнее показать верхнюю фигуру первой. Поэтому при построении HTML используем копию массива в обратном порядке. Это такое же разделение порядка хранения и порядка обхода, как при поиске попадания указателя.
for (const shape of [...doc.shapes].reverse()) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = `${shape.label || shape.type} (${shape.id})`;
button.setAttribute('aria-pressed', String(selected === shape.id));
// Обработчик выбора добавляется в main.mjs.
}
Это фрагмент построения списка. Полный код добавляет кнопку в li, подключает обработчик и помещает элемент в контейнер. Подпись выводится через textContent. Пустая строка заменяется названием типа, а идентификатор помогает отличить одинаково подписанные фигуры. Модель допускает такие одинаковые подписи, потому что уникальность обеспечивается другим полем.
В первой версии список заново строится при завершённых изменениях. Это простой способ не держать старые ссылки на DOM-элементы. При дальнейшем развитии потребуется отдельно проверить сохранение фокуса после действий внутри списка: пересоздание кнопки может переводить пользователя из ожидаемого места. Текущее управление свойствами и шаговыми кнопками уменьшает число таких переходов, но не отменяет необходимость проверки.
Форма применяет изменение целиком
В форме находятся x, y, width, height, label и fill. На отправку вызываем preventDefault(), чтобы браузер не перезагрузил страницу. Числовые значения читаются через Number(), строковые остаются строками. Затем изменяем копию документа, проверяем её и только после успеха передаём в commit().
const doc = history.value;
const shape = doc.shapes.find(item => item.id === selected);
for (const key of ['x', 'y', 'width', 'height']) {
shape[key] = Number(q(key).value);
}
shape.label = q('label').value;
shape.fill = q('fill').value;
commit(validateDocument(doc), 'Свойства применены.');
Это центральная часть обработчика, окружённая try/catch и проверкой наличия выделения. Геттер history.value возвращает копию, поэтому заполнение объекта до проверки не портит уже принятый документ. Если новая ширина выносит фигуру за страницу, проверка выбрасывает ошибку. Пользователь получает сообщение, а прежняя схема остаётся рабочей.
Одно нажатие «Применить» образует одну операцию. Мы не создаём отдельную запись истории для каждого поля. Это соответствует намерению человека: он настроил фигуру и подтвердил набор свойств. Если позднее появится редактирование при каждом нажатии клавиши, потребуется явная группировка такого ввода, иначе отмена станет слишком мелкой.
Порядок слоёв
Функция reorder(doc, id, delta) меняет выбранный элемент местами с соседним. Значение 1 передвигает его выше, -1 — ниже. Названия кнопок должны отражать один шаг, а не обещать немедленный перенос на самый верх или в самый низ. В первой версии используется именно соседняя перестановка.
При попытке поднять уже верхнюю фигуру массив не меняется. История сравнивает новый документ с текущим и не добавляет одинаковый снимок. Это делает крайнее положение нормальным исходом. Команда не обязана выдавать исключение только потому, что больше некуда двигаться.
| Действие | Изменение модели | Изменение выделения |
|---|---|---|
| Выбрать в списке | Нет | Новый id |
| Поднять на слой | Порядок массива | Сохраняется |
| Удалить | Фигура исключается | Сбрасывается при обновлении |
| Добавить | Новая фигура в конце | Новый id |
Новая фигура получает UUID, допустимый тип из списка, умеренные размеры и начальное положение 20, 20. Перед добавлением проверяем предел в 200 объектов. Удаление фильтрует массив по идентификатору. Все эти пути заканчиваются общим commit(), а не отдельным сохранением или отдельной перерисовкой.
Свойство type не редактируется в панели существующей фигуры: его выбирают при добавлении. Переключение прямоугольника на эллипс могло бы сохранить ограничивающий прямоугольник, но это отдельное действие, которое нужно назвать и включить в историю. Простое появление дополнительного поля без описания перехода усложнило бы понимание результата. Сейчас панель отвечает за свойства выбранного типа.
Уникальный UUID создаётся только для нового объекта. При изменении подписи, перестановке или восстановлении истории идентификатор сохраняется. Импорт приносит собственные идентификаторы и проверяет их уникальность внутри файла. Первая версия заменяет документ целиком, поэтому не разрешает конфликты имён между двумя объединяемыми схемами. Для будущего объединения потребуется явное переназначение ключей и связанных ссылок.
Точное движение без перетаскивания
Четыре кнопки сдвига используют moveShape() с шагом десять единиц. Таким образом, клавиатурный пользователь получает то же ограничение границ страницы, что и пользователь указателя. Не создаём второй алгоритм, который мог бы вынести фигуру за лист. Поля дают произвольную точность, кнопки — быстрый повторяемый шаг.
При самостоятельном разборе выберите эллипс через список, поменяйте его подпись и поднимите над прямоугольником. Ожидается сохранение выделения и согласованный порядок рисунка и списка. Затем задайте недопустимую ширину: документ должен остаться прежним. Удаление выделенной фигуры должно отключить форму, а отмена — вернуть данные без устаревшей ссылки на объект.
Эти сценарии пока не выполнялись. Мы получили несколько способов управления одной моделью и единый путь принятия изменений. Теперь самое время сделать отмену и повтор действия предсказуемыми: пользователь должен возвращать намерения, а не случайные промежуточные события браузера.