Создание связей
Соединение рисует сам пользователь: тянет от одного порта к другому, кликает два порта по очереди или делает то же с клавиатуры. Библиотека сама ничего не добавляет. Она сообщает о запросе событием connection:create-request, а решает ваш код — добавляет соединение в свои данные, спрашивает сервер или отказывает. Это то же разделение, что у connection:delete-request.
Режим по умолчанию выключен. Пока он выключен, нет ни ручек, ни tabindex у портов, ни живой области, а вывод такой же, как раньше.
Включение
const linker = createVisualLinker(diagramEl, {
interaction: { connect: { enabled: true } },
})
linker.on('connection:create-request', ({ from, to }) => {
connections.push({ id: `${from.portId}>${to.portId}`, from, to })
linker.setConnections(connections)
})В Vue слушайте @connection-create-request и размечайте порты (v-vl-port) — см. «В Vue» ниже.
Опции
interaction.connect — это ConnectConfig:
enabled—boolean· по умолчанию:false.mode—'drag' | 'click' | 'both'· по умолчанию:'both'. Как начинается соединение: нажатием на ручку и перетаскиванием, кликом по ней и затем кликом по цели, или любым из двух способов.from—'port' | 'block' | 'both'· по умолчанию:'port'. Что может начать соединение.'block'разрешает сам блок (его тело), а не только порт.to—'port' | 'block' | 'both'· по умолчанию:'port'. Что может его закончить. С'block'у конца нетportId, и линия подходит к границе блока (side: 'auto').handle—'hover' | 'always' | 'never'· по умолчанию:'hover'. Видимая точка у порта: пока указатель над его блоком, всегда или никогда. Область нажатия остаётся в любом случае.snap—number, px · по умолчанию:24. Расстояние от указателя, в пределах которого линия притягивается к ближайшему допустимому порту.allowSelf—boolean· по умолчанию:false. Соединение блока с самим собой.allowDuplicates—boolean· по умолчанию:false. Второе соединение между той же упорядоченной парой концов.autoScroll—boolean· по умолчанию:false. Если тянуть у верхнего или нижнего края прокручиваемого списка, список прокручивается.preview—LineStyle & LineOptions· по умолчанию: видlines, пунктир, прозрачность0.8. Вид временной линии (curve,color,width,dashed,opacity).announce—{ start, candidate, complete, cancel, invalid }· по умолчанию: английские тексты. См. «Доступность» ниже.validate—(from, to) => boolean | string· по умолчанию: не задана. Вернитеfalse, чтобы запретить, или строку с причиной: её озвучит живая область, и она придёт вconnection:create-cancel.message.
validate — функция, поэтому её можно задать только в config у createVisualLinker(), <VisualLinker> и useVisualLinker(). Модуль Nuxt принимает только JSON: в типе его опций validate нет, а переданная туда функция отбрасывается с предупреждением.
Какие концы могут соединяться
Порт или блок говорит, чем он может быть, через connect:
'source'— отсюда соединение можно начать.'target'— здесь соединение можно закончить.'both'— и то, и другое. Это значение по умолчанию, пока режим включён.false— вообще не участвует в режиме: ни ручки, ни остановки фокуса.
connect можно задать у блока и у порта; значение порта сильнее значения его блока. В разметке это атрибут data-vl-connect (source, target, both, false) и опция connect у v-vl-block / v-vl-port.
Три способа соединить
Перетаскивание. Нажмите на ручку порта, ведите — за указателем идёт пунктирная линия — и отпустите над другим допустимым портом. Допустимые цели подсвечиваются; в пределах snap пикселей линия перескакивает к ближайшей. Отпускание над допустимым портом даёт connection:create-request; отпускание над пустым местом, над запрещённым портом, Escape или отмена указателя дают connection:create-cancel. Нажатие без движения считается первым кликом способа «два клика» (в режиме 'both').
Два клика. Кликните ручку, затем кликните допустимый порт. Клик по пустому месту или Escape отменяет. Между кликами временная линия идёт за указателем.
Клавиатура. Порты, которые могут соединяться, становятся фокусируемыми (tabindex="0", role="button", aria-label — Port: block / port или собственный ariaLabel порта; значения, которые вы уже задали, сохраняются). Enter или Space на источнике начинает; Tab, Shift+Tab и стрелки переходят по допустимым целям в порядке документа; Enter или Space заканчивает; Escape отменяет. До этого фокус остаётся среди допустимых портов.
Жест, начатый на ручке порта, — всегда соединение. На остальной площади перетаскиваемого блока жест по-прежнему перетаскивает блок. При from: 'block' тело блока начинает соединение перетаскиванием, только если блок не перетаскиваемый; клик по телу начинает способ «два клика» в режимах 'click' и 'both'.
События
connect:start—{ from }. Соединение начато.connection:create-request—{ from, to, via, modifiers }.via—'drag' | 'click' | 'keyboard';modifiers—{ shift, ctrl, alt, meta }на момент завершения.fromиto—ConnectionEndpointв порядке жеста; уtoнетportId, если конец — блок.connection:create-cancel—{ from, reason, message? }.reason—'escape' | 'no-target' | 'invalid' | 'pointer-cancel' | 'api';message— строка, которую вернулаvalidate.
Из кода
Свои обработчики кликов могут управлять тем же режимом, например когда у строк уже есть собственные обработчики:
startConnect(from)—boolean. Начинает сfrom;false, если режим выключен или конец недопустим. Работает при любомmode.completeConnect(to)—boolean. Заканчивает вtoи испускаетconnection:create-requestсvia: 'click';false, если ничего не идёт илиtoнедопустим.cancelConnect(reason?)— отменяет с причиной'api'.isConnecting()—{ active, from? }.
При выключенном режиме все они возвращают false и ничего не делают.
Внешний вид и состояния
Пока соединение создаётся, порты проходят через четыре состояния:
source— откуда ведут.candidate— допустимая цель.target— допустимая цель, к которой притянулась линия.invalid— запрещённый порт под указателем.
Каждое оформляется через ports.connect.{source, candidate, target, invalid} (поля точки порта: radius, fill, stroke, strokeWidth, opacity). Они ложатся поверх всех остальных состояний. Цвета берутся из трёх токенов темы: connectPreview, connectValid и connectInvalid (--vl-connect-preview-color, --vl-connect-valid-color, --vl-connect-invalid-color).
Ваша собственная разметка может следовать за этим, потому что библиотека помечает её атрибутами и не добавляет классов:
data-vl-connecting="true"на контейнере, пока соединение создаётся;data-vl-connect-state="source | candidate | target | invalid"на элементе порта (или блока, если конец — блок).
.row[data-vl-connect-state='candidate'] {
background: #e6f6ee;
}
.row[data-vl-connect-state='invalid'] {
background: #fbe3e3;
}Все атрибуты снимаются по окончании жеста.
Доступность
- Режим полностью работает с клавиатуры.
- Состояние не передаётся одним цветом: то же самое говорят атрибуты выше и живая область.
- Визуально скрытая область
aria-live="polite"внутри слоя озвучивает начало, каждого кандидата, результат и отказ. Вannounceзадайте свои тексты — строки с{from}и{to}или функции от{ from, to, message }. prefers-reduced-motionотключает анимированный поток; временная линия не анимируется.
В Vue
<script setup lang="ts">
const config = {
interaction: {
connect: {
enabled: true,
validate: (from, to) => !taken(to) || 'Это поле уже сопоставлено',
},
},
}
function onCreate({ from, to }) {
rules.value.push({ id: `${from.portId}>${to.portId}`, from, to })
}
</script>
<template>
<VisualLinker
ref="linker"
:connections="rules"
:config="config"
@connection-create-request="onCreate"
@connection-create-cancel="(e) => log(e.reason)"
>
<div v-vl-block="'source'">
<div v-vl-port="{ id: 'email', side: 'right' }">email</div>
</div>
<div v-vl-block="'target'">
<div v-vl-port="{ id: 'mail', side: 'left', connect: 'target' }">mail</div>
</div>
</VisualLinker>
</template>Компонент испускает connect-start, connection-create-request и connection-create-cancel. Через ref шаблона он отдаёт startConnect, completeConnect, cancelConnect, isConnecting, batch и stats; те же функции возвращает useVisualLinker().