Соединения и правка
По умолчанию карта только читает. Эта страница — про то, что позволяет менять нарисованное на ней: виды соединений и их легенду, окно соединения, меню по правому клику, правку сущностей и связей и рисование новой связи между двумя узлами.
Через всё это проходит одно правило: карта никогда не пишет в entities и links. Она сообщает, о чём попросили, а решает приложение. Каждая нагрузка ниже — новый объект, применить его ваша задача. Именно это и позволяет держать карту поверх стора, API, стека отмены — или ничего.
Виды соединений
linkTypes объявляет виды, которые есть в ваших данных. Одно объявление управляет тремя вещами сразу, и в этом весь смысл: цвет в легенде всегда принадлежит связи, которую действительно можно нарисовать, а редактируемый тип никогда не оказывается тем, о котором легенда не знает.
<script setup lang="ts">
import type { LinkTypeDef } from 'vue-asset-map'
const linkTypes: LinkTypeDef[] = [
{ id: 'fiber', label: 'Оптоволокно', color: '#2f7d4f', width: 2 },
{ id: 'wifi', label: 'Wi-Fi', color: '#c07b1e', dash: '2 3' },
{ id: 'vpn', label: 'VPN-туннель', color: '#5a5ee7', dash: '6 4' },
]
</script>
<template>
<AssetMap :entities="entities" :links="links" :link-types="linkTypes" show-legend />
</template>id сравнивается с link.type. Задавайте каждому виду не только color, но и dash: один цвет не переживает монохромную печать и красно-зелёную аномалию, а легенду должен читать каждый, кто видит карту.
Цвет вида применяется инлайном и намеренно не применяется, пока связь подсвечена, погашена или заражена. Инлайновый stroke побеждает любой класс, и, оставь мы его, эти состояния молча не отрисовались бы: в такие моменты то, чем связь является, менее важно, чем то, что с ней происходит.
Легенда
showLegend ставит её слева снизу — в последний угол, не занятый тулбаром и обзорной картой. В неё попадают только виды, которые рисуются по-особому: вид без цвета и штриха неотличим от дефолтного, а строка, которая ничего не объясняет, хуже её отсутствия. Каждый образец рисует настоящую линию — тот же цвет, тот же штрих, ту же толщину: легенде, которая лишь приблизительно похожа на карту, приходится верить на слово.
Окно соединения
Клик по связи открывает окно с тем, что она соединяет, её видом, слоями и meta. Если окну сущности для осмысленного содержимого нужен слот #details, то у связи форма достаточно фиксированная, чтобы пакет показал её сам.
Набор открытых окон — такое же состояние, как любое другое:
<AssetMap v-model:open-links="openLinks" :entities="entities" :links="links" />Агрегированное ребро не открывает ничего: за ним стоит сразу несколько связей и нет одной записи, которую можно было бы описать. Клик при этом по-прежнему приходит событием edge-click.
#link-details подменяет тело — так же, как #details у сущностей.
По одному окну за раз
Обычный клик теперь заменяет открытое, а не добавляет к нему: кликая по карте, читают по узлу за раз, а не закапывают её под окнами. detailsMode: 'stack' возвращает прежнее поведение.
В обоих режимах «Открыть в новом окне» из контекстного меню накапливает окна — оно ровно для этого и есть.
<AssetMap :entities="entities" :links="links" details-mode="stack" />Меню по правому клику
showContextMenu включает меню на узлах и на связях. На узле оно предлагает открыть сущность вторым окном, на связи — открыть её подробности. С editable оба добавляют удаление.
<AssetMap :entities="entities" :links="links" show-context-menu editable />По умолчанию выключено: своё меню всегда вешали через node-context-menu, и встроенное, всплывающее поверх него, стало бы неожиданностью для тех, кто так уже делает. Событие приходит в любом случае.
У группового узла меню нет: за ним не стоит сущности, к которой относился бы хоть один пункт.
Удаление
Удаление всегда проходит через подтверждение, и это подтверждение — собственное. Не window.confirm, который не стилизуется и выглядит как фишинг, и не библиотека диалогов, которая стала бы рантайм-зависимостью ради одного окошка. Оно собрано из тех же токенов, что и вся карта, поэтому следует теме. Фокус открывается на отмене: разрушительное действие не должно оказаться тем, на что попадёт случайный Enter.
Пока вы не удалите — ничего не удалено:
<AssetMap
:entities="entities"
:links="links"
show-context-menu
editable
@entity-delete="onEntityDelete"
@link-delete="onLinkDelete"
/>function onEntityDelete({ id }: { id: string }): void {
entities.value = entities.value.filter((entity) => entity.id !== id)
// Связи узла уходят вместе с ним, иначе построитель графа сочтёт их висячими.
links.value = links.value.filter((link) => link.source !== id && link.target !== id)
}Перед событием карта закрывает окно удаляемого и убирает его из выделения: оставить окно открытым над записью, которую приложению только что велели убрать, значило бы показывать то, чего вот-вот не станет.
Правка
editable добавляет под тело окна кнопку «Редактировать». Первое нажатие превращает тело в форму, второе — сохраняет и возвращает обратно.
Схему можно не объявлять: карта предложит те два поля, значение которых знает сама — label и status выбором по тем статусам, что вы уже назвали в themeConfig.statusColors (свободный текст, если не назвали ни одного). У связей та же любезность, поэтому одного editable хватает, чтобы правились обе записи, а не одна из них молча нет.
Для всего, что лежит в meta, схема нужна: ни имён, ни того, какие из них важны, пакет не угадает.
Схема — это данные, потому что правка — единственное, чего пакет не может сделать на вашем словаре: он знает, что у сущности есть label и meta, но не знает, что meta.speed меряется в мегабитах и какие из ваших двадцати полей вообще стоит показывать человеку.
<script setup lang="ts">
import type { FieldDef } from 'vue-asset-map'
const entityFields: FieldDef[] = [
{ key: 'label', label: 'Название' },
{
key: 'status',
label: 'Состояние',
type: 'select',
options: [
{ value: 'online', label: 'В сети' },
{ value: 'offline', label: 'Не в сети' },
],
},
{ key: 'meta.owner', label: 'Владелец' },
]
</script>
<template>
<AssetMap
:entities="entities"
:links="links"
:entity-fields="entityFields"
editable
@entity-update="({ id, entity }) => replaceEntity(id, entity)"
/>
</template>key — точечный путь. Он читается для показа и записывается при сохранении, а недостающие ветки создаются: meta.speed работает и на сущности, у которой meta ещё нет. Каждый уровень пути копируется, а не мутируется.
Типы полей: 'text' (по умолчанию), 'number', 'select' и 'textarea'. Поле с readonly показывается, но не правится.
На что можно опираться:
- В режиме правки форма занимает тело целиком, в том числе когда вы заполнили
#details. В представлении для чтения печатать негде, поэтому потребитель со своим телом всё равно получает рабочую форму по объявленным полям, а не кнопку, которая обещает несбыточное. - Нетронутая форма ничего не эмитит. Открыть редактор и закрыть — не правка, до
entity-updateона не доходит. - Пустое поле означает «нет значения», а не пустую строку: для необязательного поля это разные виды неверного.
- Неразобранное число в числовом поле сохраняет введённый текст. Молча превратить его в
NaNзначило бы уничтожить набранное. - Отмена выбрасывает черновик, как и замена записи под открытым окном.
- В форме одно поле на строку, подпись сверху, контрол во всю ширину окна. Чтение — это сканирование, поэтому читающий вид остаётся двухколоночным; правка — это набор текста, а контрол, ужатый в половину панели, и есть то, из-за чего форма кажется сделанной впопыхах.
Свои контролы
#details-edit подменяет форму целиком — для оформления, которое ничем не обязано инпутам пакета. Слот получает запись, объявленные поля, черновик, функцию записи в него и два действия, которые выполняют встроенные кнопки:
<template #details-edit="{ value, fields, values, setValue, save, cancel }">
<MyForm
:entity="value"
:fields="fields"
:draft="values"
@change="setValue"
@submit="save"
@cancel="cancel"
/>
</template>#link-edit делает то же для окон соединений. Ничего из переданного в эти слоты не является компонентом: форма, привязанная к контролам пакета, — ровно то, от чего эти слоты и позволяют уйти.
Связи устроены так же — через linkFields и link-update. Не объявляйте ничего, и при editable карта предложит разумный минимум: тип выбором по вашим linkTypes, плюс подпись и скорость. Это покрывает форму почти любой связи, и повторять её не приходится.
Рисование новой связи
С editable при наведении на узел по краям его круга появляются четыре точки захвата. Перетаскивание от одной из них к другому узлу создаёт кандидата в связи, и его окно открывается сразу в режиме правки, чтобы вид и скорость заполнялись, пока намерение ещё свежо.
Четыре, а не одна, потому что направление, в котором тянут, — это заявление о том, куда должна идти связь; одна ручка заставляла бы все связи выходить из одного и того же края круга. До наведения или фокуса точки скрыты: четыре кружка на каждом узле в покое — это четыре элемента интерфейса на узел на карте, рассчитанной на сотни.
Точки захвата объявлены после центральных ручек узла, и якорями нарисованных рёбер остаются именно центральные: ребро, не назвавшее ручку, разрешается в первую своего типа, поэтому появление точек не сдвигает ни одной линии. У групповых узлов их нет — у связи с группой не было бы сущности на одном из концов.
<AssetMap :entities="entities" :links="links" :link-types="linkTypes" editable @link-create="onLinkCreate" />function onLinkCreate({ link }: { link: AssetLink }): void {
links.value = [...links.value, link]
}Кандидат получает слой текущего представления: связь, нарисованная в виде, в нём и видна — исчезнуть сразу после создания было бы здесь очевидной ловушкой.
Пока форму не сохранили, связи не существует. link-create приходит на первом сохранении, а не на перетаскивании; закрыть окно раньше — значит отказаться от кандидата совсем. Перетаскивание, закончившееся там же, где началось, связью не считается.