GrTree
Берут, когда данные вложены.
Обзор
Когда брать
- данные вложены — файлы, категории, оргструктура, разделы: уровни и есть суть данных;
- узлы отмечают галочками — с наследованием на родителей или строго (
checkStrictly); - дети грузятся по требованию —
lazyсloadвместо выгрузки всего дерева разом; - дерево фильтруют —
filterNodeMethodоставляет совпавшие узлы вместе с их ветками; - узлы переносят —
draggableработает мышью, пальцем и с клавиатуры.
Когда взять другое
| Нужно | Берите |
|---|---|
| Дерево нужно в панели выбора, а не на экране | GrTreeSelect |
| Уровень один | GrList |
| Данные табличные | GrDataTable |
| Порядок в плоском списке | GrSortableList |
| Разделы, из которых открыт один | GrCollapse |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrTree :data="data" defaultExpandAll />Примеры 3 у каждого — живое демо и его код
Файлы репозитория
Вид
explorer— набор собственных переменных дерева, разложенный вstyle, рядом — моноширинное семейство;branch-line="elbow"рисует колено каждой связи, а слот по умолчанию даёт ему значок файла, в который упереться.vendor—lazy:loadприносит детей при раскрытии. Файлы переносятся черезdraggable— мышью или Shift со стрелками;allow-dropне пускает их в файлы,allow-dragзакрепляет README, аnode-dropиnode-context-menuсообщают о сделанном.billing-service Drag a file into another folder, or focus it and use Shift with the arrowssrcinvoicesprorate.tsrender.tspaymentsindex.tsvendorREADME.mdПрава роли по разделам
show-checkboxсv-model:checked-keysпревращает дерево в набор прав, а наполовину отмеченный раздел — честный ответ «часть»: событиеcheckиgetHalfCheckedKeys()называют такие разделы для сводки.check-strictlyразвязывает родителей и детей, когда раздел — право сам по себе. Видpickerдаёт строки в 38px и увеличенный квадрат: цель — квадрат, а не подпись.Role · Accountant6 rights · every area whole or noneInvoicesView invoicesCreate and edit draftsSend to customersVoid issued invoicesPaymentsView paymentsIssue refundsReportsRevenueTax summary
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
dataобязательный | T[] | — | Корневые узлы в исходной форме. Дерево их не копирует: перенос и `appendNode` меняют эти массивы на месте. |
props | GrTreePropsMap | undefined | {
children: "children",
label: "label",
} | Имена полей в данных: `children`, `label`, `isLeaf`. По умолчанию `children` и `label`. |
nodeKey | "id" | Extract<keyof T, string> | undefined | "id" as any | Поле с уникальным ключом узла: на нём держатся раскрытие, отметки, перенос. По умолчанию `id`. |
defaultExpandedKeys | GrTreeKey[] | undefined | [] | Стартовый набор раскрытых узлов. Новый массив в пропе заменяет раскрытие целиком. |
defaultExpandAll | boolean | undefined | false | Раскрывать узлы, как только они появились в данных. Уже свёрнутое руками не раскрывается обратно на каждом обновлении `data`. |
filterNodeMethod | GrTreeFilterNodeMethod<T> | undefined | — | Своё правило совпадения узла с фильтром; `undefined` из него — правило по умолчанию: подстрока подписи без учёта регистра. Совпавший узел остаётся видимым вместе с предками. |
filterValue | string | undefined | undefined | Значение фильтра. Проп, а не только метод `filter()`: обёртке иначе приходится держать `ref` на дерево и гонять значение в обход собственного реактивного контура. Результат фильтрации дерево сообщает событием `filter` — без него нельзя отличить «данных нет» от «поиск ничего не нашёл», а это разные экраны. |
lazy | boolean | undefined | false | Ленивый режим: дети ветки приходят по её раскрытию через `load`. Узел считается разворачиваемым, пока не доказано обратное — полем `isLeaf` из карты `props`. |
load | GrTreeLoad<T> | undefined | undefined | Загрузчик ветки. `resolve` дописывает детей в данные узла. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер строки: высота, отступы, иконки и кегль подписи. |
indent | number | undefined | 0 | Шаг отступа уровня в пикселях. `0` — значение из темы (`--gr-tree-indent-step`). |
virtual | boolean | undefined | false | Виртуализация: в DOM живёт только окно вокруг вьюпорта. Требует `maxHeight` — без ограниченной высоты окна прокрутки не существует; скроллером в этом режиме становится сам корень дерева. Включается осознанно: на списке в сотню узлов выигрыша нет, а в DOM остаётся только окно — вместе с ним меняется и то, что находит `querySelector` потребителя. Ограничение: перетаскивание работает по отрисованным строкам — уронить узел на тот, которого нет на экране, нельзя. |
maxHeight | string | number | undefined | undefined | Максимальная высота дерева со своим скроллером — всегда, не только с `virtual`: тот лишь кладёт поверх окно строк. Число — пиксели. |
highlightCurrent | boolean | undefined | true | Подсвечивать строку текущего узла. По умолчанию `true`. |
expandIcon | string | Component | undefined | undefined | Иконка свёрнутого узла: Vue-компонент либо класс иконки (`'i-lucide-plus'` — правило для класса заводит приложение: фабрике движка или провайдером, см. `docs/installation.md#иконки`). Не задана — встроенная стрелка. |
collapseIcon | string | Component | undefined | undefined | Иконка раскрытого узла. Не задана — та же встроенная стрелка, повёрнутая. |
toggleIconRotate | boolean | undefined | true | Поворот иконки раскрытого узла на 90°: хватает одной `expandIcon`. По умолчанию `true`. |
branchLine | boolean | GrTreeBranchLine | undefined | "line" | Направляющие уровней. `true` — то же, что `'line'`: одна вертикаль на уровень. `'elbow'` добавляет горизонтальное колено к строке и обрывает линию на середине последнего ребёнка — так видно не только уровень, но и то, какие ветки ещё продолжаются. |
branchLineColor | GrTreeBranchLineColor<T> | undefined | undefined | Цвет направляющих: CSS-значение или функция от узла. Не задан — `--gr-tree-branch-line-default-color`, иначе цвет рамки. |
branchLineActiveColor | GrTreeBranchLineColor<T> | undefined | undefined | Цвет направляющей у ветки текущего узла: значение или функция. Не задан — `branchLineColor`. |
rowClass | GrTreeNodeClass<T> | undefined | Класс строки узла — значением или функцией от строки (`node`, `isExpanded`, `isLeaf`, `isMatched`). Перебивает кегль и цвет дерева: так делают ступени по уровням. |
dragHandleClass | GrTreeNodeClass<T> | undefined | Класс ручки переноса — значением или функцией от строки. |
toggleClass | GrTreeNodeClass<T> | undefined | Класс кнопки раскрытия — значением или функцией от строки. |
toggleIconClass | GrTreeNodeClass<T> | undefined | Класс иконки раскрытия — значением или функцией от строки. |
toggleSpacerClass | GrTreeNodeClass<T> | undefined | Класс заглушки на месте кнопки раскрытия у листа — значением или функцией от строки. |
contentClass | GrTreeNodeClass<T> | undefined | Класс обёртки содержимого строки (подписи или слота) — значением или функцией от строки. |
dragLabel | string | undefined | undefined | i18n-метка кнопки "Перетащить" (default: 'Drag'). |
expandLabel | string | undefined | undefined | i18n-метка кнопки "Развернуть" (default: 'Expand'). |
collapseLabel | string | undefined | undefined | i18n-метка кнопки "Свернуть" (default: 'Collapse'). |
showCheckbox | boolean | undefined | false | Чекбоксы у узлов: множественный выбор поверх дерева. |
currentKeyv-model | GrTreeKey | null | undefined | undefined | Текущий узел — `v-model:current-key`. Не задан — дерево ведёт текущий узел само, как и раньше. Задан — источник правды снаружи: подсветка строки и признак, по которому обёртка рисует собственные детали, перестают быть двумя разными состояниями, способными разойтись на такт. |
checkedKeysv-model | GrTreeKey[] | undefined | undefined | Отмеченные ключи (`v-model:checked-keys`). |
defaultCheckedKeys | GrTreeKey[] | undefined | [] | Стартовые отметки, когда `checkedKeys` не ведут снаружи. Читаются один раз, при создании. |
checkStrictly | boolean | undefined | false | Не связывать родителей и детей: каждый узел отмечается сам по себе. |
expandOnClickNode | boolean | undefined | false | Клик по строке раскрывает/сворачивает узел, а не только выбирает его. |
accordion | boolean | undefined | false | На каждом уровне раскрыт максимум один узел. |
draggable | boolean | undefined | false | Перенос узлов мышью, пальцем и `Shift` со стрелкой. Ограничивают `allowDrag` и `allowDrop`. |
dragHandleIcon | string | Component | undefined | undefined | Иконка ручки переноса: компонент, класс иконки или ничего — тогда встроенная. |
allowDrop |
| — | Можно ли уронить узел на цель: `prev` — перед ней, `inner` — внутрь, `next` — после. |
allowDrag | ((draggingNode: GrTreeNode<T>) => boolean) | undefined | — | Можно ли взять узел: `false` выключает его ручку переноса. |
dragHandleVisibility | "auto" | "hover" | "always" | undefined | "auto" | Когда показывать ручку переноса. `hover` — только под курсором; `always` — всегда; `auto` (по умолчанию) — `always` там, где наведения не бывает (`@media (hover: none)`), иначе `hover`. На тач-устройстве ручка «по наведению» недостижима вовсе, то есть перетаскивания там нет — а это не решение дизайна, а отсутствие функции. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | { node: GrTreeNode<T>; data: T; } | Содержимое строки вместо подписи: значок, счётчик, метка. Получает `node` и `data`. |
События
| Event | Сигнатура | Описание |
|---|---|---|
nodeClick | (e: "nodeClick", data: T, node: GrTreeNode<T>): void | Узел выбран кликом или `Enter` и стал текущим. |
nodeExpand | (e: "nodeExpand", data: T, node: GrTreeNode<T>): void | Узел раскрыт кнопкой, клавиатурой или кликом при `expandOnClickNode`. |
nodeCollapse | (e: "nodeCollapse", data: T, node: GrTreeNode<T>): void | Узел свёрнут — пользователем или `accordion`, когда раскрыли соседа. |
nodeDrop |
| Узел перенесён указателем или `Shift` со стрелкой и уже переставлен в `data`. `dropType` — `prev`, `inner` или `next` относительно `dropNode`. |
nodeContextMenu | (e: "nodeContextMenu", evt: MouseEvent, data: T, node: GrTreeNode<T>): void | Контекстное меню строки. Событие — первым аргументом: `preventDefault()` и своё меню. |
update:currentKey | (e: "update:currentKey", key: GrTreeKey | undefined): void | Текущий узел сменился кликом или `Enter` (`v-model:current-key`). |
filter |
| Результат фильтрации. Без него потребитель не отличит «данных нет» от «поиск ничего не нашёл» — а это разные пустые экраны: второй пользователь может исправить сам. |
update:checkedKeys | (e: "update:checkedKeys", keys: GrTreeKey[]): void | Отмеченные ключи (`v-model:checked-keys`) после отметки пользователем или методом; родители, отмеченные каскадом, входят тоже. |
check |
| Отметка узла сменилась — `Space`, кликом или `setChecked()`; с итоговыми наборами ключей. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrTreeKey, GrTreeNodeDropType, GrTreeAllowDropType } from '@feugene/granularity/components/GrTree'Файлdist/types/components/GrTree/grTreeTypes.d.ts
type GrTreeKey = string | number;type GrTreeNodeDropType = 'prev' | 'inner' | 'next';type GrTreeAllowDropType = GrTreeNodeDropType;type GrTreeNode<T = any> = {
key: GrTreeKey;
label: string;
data: T;
level: number;
parent?: GrTreeNode<T>;
childNodes: GrTreeNode<T>[];
};type GrTreeNodeTarget<T = any> = GrTreeKey | GrTreeNode<T> | T;type GrTreeInstance<T = any> = {
filter: (value: string) => void;Файлdist/types/components/GrTree/grTreeProps.d.ts
type GrTreePropsMap = {
children?: string;
label?: string;
/** Поле данных «узел — лист». Нужно ленивому режиму: детей ещё нет, а раскрывать нечего. */
isLeaf?: string;
};type GrTreeFilterNodeMethod<T extends object = any> = (value: string, data: T, node?: GrTreeNode<T>) => boolean;
export type GrTreeBranchLine = 'line' | 'elbow';
export type GrTreeBranchLineColor<T extends object = any> = string | ((node: GrTreeNode<T>) => string | undefined | null);
export type GrTreeVisibleRow<T extends object> = {
node: GrTreeNode<T>;
isExpanded: boolean;
isLeaf: boolean;
isMatched: boolean;
/**
* Место узла среди соседей. В плоском DOM группы нет, поэтому структуру
* дерева диктору сообщают `aria-level` + `aria-posinset` + `aria-setsize`.
*/
posInSet: number;
setSize: number;
/** Идёт ли сейчас ленивая загрузка детей этого узла. */
isLoading: boolean;
/** Цвета направляющих для уровней предков (пусто, если `branchLine` выключен). */
branchColors: string[];
};
export type GrTreeClassValue = HTMLAttributes['class'];
export type GrTreeNodeClass<T extends object> = GrTreeClassValue | ((row: GrTreeVisibleRow<T>) => GrTreeClassValue);
export type GrTreeDataProps<T extends object> = {
/**
* Корневые узлы в исходной форме. Дерево их не копирует: перенос и `appendNode` меняют эти
* массивы на месте.
*/
data: T[];
/** Имена полей в данных: `children`, `label`, `isLeaf`. По умолчанию `children` и `label`. */
props?: GrTreePropsMap;
/** Поле с уникальным ключом узла: на нём держатся раскрытие, отметки, перенос. По умолчанию `id`. */
nodeKey?: NodeKeyProp<T> | 'id';
/** Стартовый набор раскрытых узлов. Новый массив в пропе заменяет раскрытие целиком. */
defaultExpandedKeys?: GrTreeKey[];
/**
* Раскрывать узлы, как только они появились в данных. Уже свёрнутое руками
* не раскрывается обратно на каждом обновлении `data`.
*/
defaultExpandAll?: boolean;
/**
* Своё правило совпадения узла с фильтром; `undefined` из него — правило по умолчанию: подстрока
* подписи без учёта регистра. Совпавший узел остаётся видимым вместе с предками.
*/
filterNodeMethod?: GrTreeFilterNodeMethod<T>;
/**
* Значение фильтра. Проп, а не только метод `filter()`: обёртке иначе
* приходится держать `ref` на дерево и гонять значение в обход собственного
* реактивного контура.
*
* Результат фильтрации дерево сообщает событием `filter` — без него нельзя
* отличить «данных нет» от «поиск ничего не нашёл», а это разные экраны.
*/
filterValue?: string;
/**
* Ленивый режим: дети ветки приходят по её раскрытию через `load`.
* Узел считается разворачиваемым, пока не доказано обратное — полем `isLeaf`
* из карты `props`.
*/
lazy?: boolean;
/** Загрузчик ветки. `resolve` дописывает детей в данные узла. */
load?: GrTreeLoad<T>;
};
export type GrTreeLoad<T extends object = any> = (node: GrTreeNode<T>, resolve: (children: T[]) => void) => void;
export type GrTreeViewProps<T extends object> = {
/** Размер строки: высота, отступы, иконки и кегль подписи. */
size?: GrTreeSize;
/** Шаг отступа уровня в пикселях. `0` — значение из темы (`--gr-tree-indent-step`). */
indent?: number;
/**
* Виртуализация: в DOM живёт только окно вокруг вьюпорта.
*
* Требует `maxHeight` — без ограниченной высоты окна прокрутки не существует;
* скроллером в этом режиме становится сам корень дерева.
*
* Включается осознанно: на списке в сотню узлов выигрыша нет, а в DOM остаётся
* только окно — вместе с ним меняется и то, что находит `querySelector`
* потребителя.
*
* Ограничение: перетаскивание работает по отрисованным строкам — уронить узел
* на тот, которого нет на экране, нельзя.
*/
virtual?: boolean;
/**
* Максимальная высота дерева со своим скроллером — всегда, не только с
* `virtual`: тот лишь кладёт поверх окно строк. Число — пиксели.
*/
maxHeight?: number | string;
/** Подсвечивать строку текущего узла. По умолчанию `true`. */
highlightCurrent?: boolean;
/**
* Иконка свёрнутого узла: Vue-компонент либо класс иконки (`'i-lucide-plus'` —
* правило для класса заводит приложение: фабрике движка или провайдером,
* см. `docs/installation.md#иконки`).
* Не задана — встроенная стрелка.
*/
expandIcon?: string | Component;
/** Иконка раскрытого узла. Не задана — та же встроенная стрелка, повёрнутая. */
collapseIcon?: string | Component;
/** Поворот иконки раскрытого узла на 90°: хватает одной `expandIcon`. По умолчанию `true`. */
toggleIconRotate?: boolean;
/**
* Направляющие уровней. `true` — то же, что `'line'`: одна вертикаль на
* уровень. `'elbow'` добавляет горизонтальное колено к строке и обрывает
* линию на середине последнего ребёнка — так видно не только уровень, но и
* то, какие ветки ещё продолжаются.
*/
branchLine?: boolean | GrTreeBranchLine;
/**
* Цвет направляющих: CSS-значение или функция от узла. Не задан —
* `--gr-tree-branch-line-default-color`, иначе цвет рамки.
*/
branchLineColor?: GrTreeBranchLineColor<T>;
/** Цвет направляющей у ветки текущего узла: значение или функция. Не задан — `branchLineColor`. */
branchLineActiveColor?: GrTreeBranchLineColor<T>;
/**
* Класс строки узла — значением или функцией от строки (`node`, `isExpanded`, `isLeaf`,
* `isMatched`). Перебивает кегль и цвет дерева: так делают ступени по уровням.
*/
rowClass?: GrTreeNodeClass<T>;
/** Класс ручки переноса — значением или функцией от строки. */
dragHandleClass?: GrTreeNodeClass<T>;
/** Класс кнопки раскрытия — значением или функцией от строки. */
toggleClass?: GrTreeNodeClass<T>;
/** Класс иконки раскрытия — значением или функцией от строки. */
toggleIconClass?: GrTreeNodeClass<T>;
/** Класс заглушки на месте кнопки раскрытия у листа — значением или функцией от строки. */
toggleSpacerClass?: GrTreeNodeClass<T>;
/** Класс обёртки содержимого строки (подписи или слота) — значением или функцией от строки. */
contentClass?: GrTreeNodeClass<T>;
/** i18n-метка кнопки "Перетащить" (default: 'Drag'). */
dragLabel?: string;
/** i18n-метка кнопки "Развернуть" (default: 'Expand'). */
expandLabel?: string;
/** i18n-метка кнопки "Свернуть" (default: 'Collapse'). */
collapseLabel?: string;
};
export type GrTreeInteractionProps<T extends object> = {
/** Клик по строке раскрывает/сворачивает узел, а не только выбирает его. */
expandOnClickNode?: boolean;
/** На каждом уровне раскрыт максимум один узел. */
accordion?: boolean;
/** Перенос узлов мышью, пальцем и `Shift` со стрелкой. Ограничивают `allowDrag` и `allowDrop`. */
draggable?: boolean;
/** Иконка ручки переноса: компонент, класс иконки или ничего — тогда встроенная. */
dragHandleIcon?: string | Component;
/** Можно ли уронить узел на цель: `prev` — перед ней, `inner` — внутрь, `next` — после. */
allowDrop?: (draggingNode: GrTreeNode<T>, dropNode: GrTreeNode<T>, type: GrTreeAllowDropType) => boolean;
/** Можно ли взять узел: `false` выключает его ручку переноса. */
allowDrag?: (draggingNode: GrTreeNode<T>) => boolean;
/**
* Когда показывать ручку переноса.
*
* `hover` — только под курсором; `always` — всегда; `auto` (по умолчанию) —
* `always` там, где наведения не бывает (`@media (hover: none)`), иначе
* `hover`. На тач-устройстве ручка «по наведению» недостижима вовсе, то есть
* перетаскивания там нет — а это не решение дизайна, а отсутствие функции.
*/
dragHandleVisibility?: 'hover' | 'always' | 'auto';
};
export type GrTreeSelectionProps = {
/** Чекбоксы у узлов: множественный выбор поверх дерева. */
showCheckbox?: boolean;
/**
* Текущий узел — `v-model:current-key`.
*
* Не задан — дерево ведёт текущий узел само, как и раньше. Задан — источник
* правды снаружи: подсветка строки и признак, по которому обёртка рисует
* собственные детали, перестают быть двумя разными состояниями, способными
* разойтись на такт.
*/
currentKey?: GrTreeKey | null;
/** Отмеченные ключи (`v-model:checked-keys`). */
checkedKeys?: GrTreeKey[];
/** Стартовые отметки, когда `checkedKeys` не ведут снаружи. Читаются один раз, при создании. */
defaultCheckedKeys?: GrTreeKey[];
/** Не связывать родителей и детей: каждый узел отмечается сам по себе. */
checkStrictly?: boolean;
};
export type GrTreeProps<T extends object> = GrTreeDataProps<T> & GrTreeViewProps<T> & GrTreeSelectionProps & GrTreeInteractionProps<T>;type GrTreeBranchLineColor<T extends object = any> = string | ((node: GrTreeNode<T>) => string | undefined | null);
export type GrTreeVisibleRow<T extends object> = {type GrTreeVisibleRow<T extends object> = {
node: GrTreeNode<T>;
isExpanded: boolean;
isLeaf: boolean;
isMatched: boolean;
/**
* Место узла среди соседей. В плоском DOM группы нет, поэтому структуру
* дерева диктору сообщают `aria-level` + `aria-posinset` + `aria-setsize`.
*/
posInSet: number;
setSize: number;
/** Идёт ли сейчас ленивая загрузка детей этого узла. */
isLoading: boolean;
/** Цвета направляющих для уровней предков (пусто, если `branchLine` выключен). */
branchColors: string[];
};type GrTreeNodeClass<T extends object> = GrTreeClassValue | ((row: GrTreeVisibleRow<T>) => GrTreeClassValue);
export type GrTreeDataProps<T extends object> = {type GrTreeLoad<T extends object = any> = (node: GrTreeNode<T>, resolve: (children: T[]) => void) => void;
export type GrTreeViewProps<T extends object> = {
/** Размер строки: высота, отступы, иконки и кегль подписи. */
size?: GrTreeSize;
/** Шаг отступа уровня в пикселях. `0` — значение из темы (`--gr-tree-indent-step`). */
indent?: number;
/**
* Виртуализация: в DOM живёт только окно вокруг вьюпорта.
*
* Требует `maxHeight` — без ограниченной высоты окна прокрутки не существует;
* скроллером в этом режиме становится сам корень дерева.
*
* Включается осознанно: на списке в сотню узлов выигрыша нет, а в DOM остаётся
* только окно — вместе с ним меняется и то, что находит `querySelector`
* потребителя.
*
* Ограничение: перетаскивание работает по отрисованным строкам — уронить узел
* на тот, которого нет на экране, нельзя.
*/
virtual?: boolean;
/**
* Максимальная высота дерева со своим скроллером — всегда, не только с
* `virtual`: тот лишь кладёт поверх окно строк. Число — пиксели.
*/
maxHeight?: number | string;
/** Подсвечивать строку текущего узла. По умолчанию `true`. */
highlightCurrent?: boolean;
/**
* Иконка свёрнутого узла: Vue-компонент либо класс иконки (`'i-lucide-plus'` —type GrTreeSelectionProps = {
/** Чекбоксы у узлов: множественный выбор поверх дерева. */
showCheckbox?: boolean;
/**
* Текущий узел — `v-model:current-key`.
*
* Не задан — дерево ведёт текущий узел само, как и раньше. Задан — источник
* правды снаружи: подсветка строки и признак, по которому обёртка рисует
* собственные детали, перестают быть двумя разными состояниями, способными
* разойтись на такт.
*/
currentKey?: GrTreeKey | null;
/** Отмеченные ключи (`v-model:checked-keys`). */
checkedKeys?: GrTreeKey[];
/** Стартовые отметки, когда `checkedKeys` не ведут снаружи. Читаются один раз, при создании. */
defaultCheckedKeys?: GrTreeKey[];
/** Не связывать родителей и детей: каждый узел отмечается сам по себе. */
checkStrictly?: boolean;
};type GrTreeProps<T extends object> = GrTreeDataProps<T> & GrTreeViewProps<T> & GrTreeSelectionProps & GrTreeInteractionProps<T>;Файлdist/types/composables/internal/treeChecking.d.ts
type GrTreeCheckState = typeof GR_TREE_CHECK_STATES[number];Файлdist/types/components/GrTree/config.d.ts
const grTreeConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTree">;Файлdist/types/components/GrTree/defaults.d.ts
Пропы GrTree, настраиваемые глобально через componentDefaults.
interface GrTreeConfigurableProps {
size: GrComponentSize;
}Файлdist/types/components/GrTree/grTreeStyles.d.ts
type GrTreeSize = GrComponentSize;Готовые виды дерева — наборы значений тех же --gr-tree-*, что задаёт
size. Не проп: второй канал спорил бы с размером за одни переменные, а
набор с ним композируется — потребитель раскладывает его в style и при
необходимости переопределяет по одной строке.
Шрифт и значки в набор не входят: семейства у дерева своего токена нет, а
значки — это expandIcon/collapseIcon и слот строки, то есть решение
потребителя, а не вида.
type GrTreeView = 'explorer' | 'rail' | 'outline' | 'picker';const grTreeViewVars: Record<GrTreeView, Record<string, string>>;Файлdist/types/components/GrTree/safelist.d.ts
Пуст: составных классов у дерева нет. Утилиты стоят в шаблоне литералами,
строку рисуют собственные классы gr-tree__* из <style>, и granum
извлекает всё это из чанков компонента сам, включая общие. Классы из пропов
(rowClass, expandIcon/collapseIcon/dragHandleIcon и т. п.) приходят
от потребителя — их генерирует его сборка, а не наша.
const grTreeSafelist: readonly [];Файлdist/types/components/GrTree/GrTree.vue.d.ts
interface GrTreeEmits<T extends Record<string, any> = any> {
/** Узел выбран кликом или `Enter` и стал текущим. */
(e: 'nodeClick', data: T, node: GrTreeNode<T>): void;
/** Узел раскрыт кнопкой, клавиатурой или кликом при `expandOnClickNode`. */
(e: 'nodeExpand', data: T, node: GrTreeNode<T>): void;
/** Узел свёрнут — пользователем или `accordion`, когда раскрыли соседа. */
(e: 'nodeCollapse', data: T, node: GrTreeNode<T>): void;
/**
* Узел перенесён указателем или `Shift` со стрелкой и уже переставлен в `data`. `dropType` —
* `prev`, `inner` или `next` относительно `dropNode`.
*/
(e: 'nodeDrop', draggingNode: GrTreeNode<T>, dropNode: GrTreeNode<T>, dropType: GrTreeNodeDropType): void;
/** Контекстное меню строки. Событие — первым аргументом: `preventDefault()` и своё меню. */
(e: 'nodeContextMenu', evt: MouseEvent, data: T, node: GrTreeNode<T>): void;
/** Текущий узел сменился кликом или `Enter` (`v-model:current-key`). */
(e: 'update:currentKey', key: GrTreeKey | undefined): void;
/**
* Результат фильтрации. Без него потребитель не отличит «данных нет» от
* «поиск ничего не нашёл» — а это разные пустые экраны: второй пользователь
* может исправить сам.
*/
(e: 'filter', info: {
value: string;
visibleCount: number;
matchedCount: number;
}): void;
/**
* Отмеченные ключи (`v-model:checked-keys`) после отметки пользователем или методом; родители,
* отмеченные каскадом, входят тоже.
*/
(e: 'update:checkedKeys', keys: GrTreeKey[]): void;
/** Отметка узла сменилась — `Space`, кликом или `setChecked()`; с итоговыми наборами ключей. */
(e: 'check', data: T, node: GrTreeNode<T>, info: {
checkedKeys: GrTreeKey[];
halfCheckedKeys: GrTreeKey[];
}): void;
}Доступность
Паттерн APG
tree (roving tabindex)
Клавиши
↓ / ↑→←Home / End
| Клавиша | Действие |
|---|---|
| ↓ / ↑ | по видимым узлам |
| → | раскрыть/внутрь |
| ← | свернуть/к родителю |
| Home / End | к краям |
| Enter | выбрать |
| Space | выбрать (а при showCheckbox — переключить отметку) |
| * | раскрыть всех соседей уровня |
| — | печатные символы — typeahead; при draggable — Shift+↑/↓ переставить среди соседей |
| — | Shift+→ сделать дочерним предыдущего соседа |
| — | Shift+← вынести к родителю |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Разметка: плоский список вместо вложенного DOM
Дерево рендерится одним списком строк, а не вложенными компонентами: узел
любого уровня — это div[role="treeitem"] в общем контейнере role="tree",
отступ задаёт padding-left строки. Иерархию несут aria-level,
aria-posinset и aria-setsize — при плоском представлении паттерн tree
требует все три.
Практические следствия:
- глубина дерева больше не превращается в глубину дерева компонентов: раскрытые 2 000 узлов — это 2 000 строк, а не 2 000 инстансов со своими вычислениями;
- подсветка строки (наведение, текущий узел) и кольцо фокуса начинаются от
отступа уровня, а не от левого края дерева: иначе фон накрывал бы направляющие
предков — линию ветки не было бы видно ровно там, где она нужна. Рисует их
отдельный слой строки, вставленный слева на
--gr-tree-row-indent; - шаг отступа настраивается пропом
indent(в пикселях) или темой —--gr-tree-indent-step.
Готовые виды
Вид — это набор значений тех же --gr-tree-*, что задаёт size, а не проп:
второй канал спорил бы с размером за одни переменные. Наборы экспортируются и
раскладываются в style, поэтому композируются с size и переопределяются по
одной строке.
<script setup lang="ts">
import { grTreeViewVars } from '@feugene/granularity'
import { GrTree } from '@feugene/granularity/components/GrTree'
</script>
<template>
<GrTree :data="nodes" node-key="id" branch-line="elbow" :style="grTreeViewVars.explorer" />
</template>| Вид | Для чего | Что меняет |
|---|---|---|
explorer | файлы, пространства имён, тысячи узлов | строка 24px и кегль под неё, волосяные нейтральные направляющие, колена связей, подпись ветки без утолщения |
rail | навигация по разделам, боковая панель | строка 30px, выбор — сплошная плашка, скруглённая только справа, с перекрашенным текстом |
outline | оглавление рядом с текстом | ни подложек, ни направляющих: выбор отмечен полосой у края списка и цветом, вес отдан уровню |
picker | отбор узлов отметками | строка 38px и увеличенный квадрат: цель нажатия — сам квадрат, а не подпись |
picker отметок не включает — это ортогональная ось (showCheckbox), и она
работает в любом виде. Набор задаёт под неё только плотность.
Семейство шрифта в набор не входит, а кегль входит: своего токена для
семейства у дерева нет, а высота строки без кегля недостижима — её держит текст,
а не min-height. Моноширинный набор проводника подмешивается рядом:
:style="{ ...grTreeViewVars.explorer, fontFamily: 'var(--gr-font-mono)' }".
Значки и хвост строки — тоже не вид, а слот. Проводнику значок папки и файла
нужен не для красоты: колено связи упирается в начало содержимого, и без значка
черта обрывается в пустоте перед подписью. Содержимое строки растянуто на всю
свободную ширину, поэтому счётчик или метка прижимаются вправо обычным
margin-left: auto.
Набор кладётся на само дерево, а не на обёртку. Значения по умолчанию дерево
объявляет на своём корне, поэтому переменная, выставленная предком, до строки не
доходит — её перекрывает собственное объявление. Так же поступает и
GrTreeSections со своим видом строк: отдаёт его внутреннему дереву, а не
секции.
rowClass перебивает кегль и цвет дерева: обе величины объявлены на корне и
строкой наследуются, а не задаются ей. Ступени по уровням иначе не сделать —
уровень строки знает только разметка.
Своего вида не хватило — берите набор за основу и переопределяйте переменные
поштучно; список — в ../tokens.md.
Направляющие уровней
branchLine по умолчанию 'line': без направляющих дерево читается как список с
отступами, и уровень видно только по сдвигу подписи.
'elbow' добавляет горизонтальное колено к строке и обрывает вертикаль на
середине последнего ребёнка. Разница не косметическая: линия начинает соединять
узлы, а не только отмечать уровень, и по обрыву видно, какие ветки ещё
продолжаются. Колено получает только направляющая собственного уровня строки — у
транзитных предков черта указывала бы на строку, к которой они не относятся.
false — явный отказ.
Фильтрация
Значение фильтра — проп filterValue; метод filter() остаётся для
императивных сценариев. Результат дерево сообщает событием filter:
visibleCount — сколько строк реально видно (совпадения плюс раскрытые ради них
родители), matchedCount — сколько узлов совпало само.
Число наружу нужно не для счётчика: без него потребитель не отличит «данных нет» от «поиск ничего не нашёл», а это разные пустые экраны — второй пользователь может исправить сам, первый нет.
Высота и виртуализация
maxHeight ограничивает дерево всегда: корень получает max-height и свой
скролл, и длинное дерево не вырастает на всю страницу.
virtual вместе с maxHeight оставляет в DOM только окно вокруг вьюпорта —
именно плоская разметка выше это и открывает. Скроллером становится сам корень
дерева, а срезанное сверху и снизу держат его отступы: обёртки между
role="tree" и role="treeitem" отняли бы у роли обязательных потомков.
<GrTree :data="nodes" node-key="id" virtual :max-height="400" />aria-setsize и aria-posinset остаются от полного набора, а не от окна —
иначе диктор объявлял бы «1 из 20» на списке в тысячу узлов.
Включать осознанно: на сотне строк выигрыша нет, а разметка меняется, и вместе с
ней — то, что находит querySelector потребителя. Высота строки берётся из
--gr-tree-row-min-height как оценка и уточняется замером отрисованных
строк. Устройство и ограничения — virtual-list.md.
Раскрытие
| Проп | Что делает |
|---|---|
defaultExpandedKeys | стартовый набор раскрытых узлов |
defaultExpandAll | раскрывает узел в момент его появления в данных |
expandOnClickNode | клик по строке раскрывает узел, а не только выбирает |
accordion | на каждом уровне раскрыт максимум один узел |
defaultExpandAll намеренно не «держит всё раскрытым»: он отмечает уже
виденные ключи, поэтому свёрнутое руками не разворачивается обратно на каждом
обновлении data. Иначе живое дерево из БД схлопывало бы работу пользователя
при любом рефетче.
expandOnClickNode действует только на мышь. Enter в паттерне tree закреплён
за выбором, и подмешивать в него раскрытие нельзя — иначе выбрать папку с
клавиатуры стало бы невозможно.
Иконки
expandIcon, collapseIcon и dragHandleIcon не заданы — дерево рисует свои
встроенные, и от конфига приложения они не зависят. Своя иконка передаётся
Vue-компонентом либо классом иконки (i-lucide-* — правило для него заводит
приложение: фабрике движка или провайдером, иначе — Vue-компонент; см.
«Иконки»).
toggleIconRotate поворачивает иконку раскрытия на 90° вместо смены картинки —
поэтому одной expandIcon обычно достаточно.
Чекбоксы
<GrTree v-model:checked-keys="checked" :data="data" node-key="id" show-checkbox />Отметки связаны по дереву: отмеченный родитель отмечает всех потомков, частично
отмеченный показывает aria-checked="mixed". checkStrictly эту связь
отключает — каждый узел отвечает сам за себя.
Состояние объявляется на самом узле (aria-checked на treeitem), а
видимый квадратик декоративен и скрыт от диктора: вкладывать интерактивный
чекбокс внутрь роли-виджета нельзя — роль объявляет потомков презентационными.
Корень при этом становится aria-multiselectable="true".
Space при включённых чекбоксах переключает отметку (это и есть выбор в
multi-select дереве), Enter по-прежнему выбирает узел.
Императивно: getCheckedKeys({ leafOnly }), setCheckedKeys(),
getHalfCheckedKeys(), setChecked(node, checked). checkedKeys снаружи может
содержать только листья — родители посчитаются сами.
Ленивая подгрузка
<GrTree :data="roots" node-key="id" lazy :load="loadChildren" />function loadChildren(node: GrTreeNode<Folder>, resolve: (children: Folder[]) => void) {
fetchChildren(node.key).then(resolve)
}В ленивом режиме узел считается разворачиваемым, пока не доказано обратное:
лист объявляется полем isLeaf в данных (имя настраивается картой props).
Ветка грузится по первому раскрытию — повторное запроса не делает; на время
запроса строка показывает спиннер и помечается aria-busy.
Загруженные дети живут в состоянии компонента, а не дописываются в data:
проп реактивным быть не обязан, а показать пришедшее дерево должно в любом
случае. Пустой ответ делает ветку листом.
defaultExpandAll незагруженные ветки не трогает — иначе первый рендер дёрнул
бы весь бэкенд.
Выбор и объявление скринридеру
Текущий узел задаётся пропом — v-model:current-key, — а не только методом
setCurrentKey(). Разница не в удобстве: подсветку строки рисует дерево, а всё,
что вокруг неё (заголовок панели, доступность действий), считает потребитель по
своему состоянию. Пока это два владельца одного понятия, они способны разойтись
на такт: строка подсвечена, а панель показывает другое. С пропом источник правды
один, и обёртке не нужен ни ref на дерево, ни nextTick вокруг вызова.
Проп не задан — дерево ведёт текущий узел само, как и раньше; метод остаётся для императивных сценариев.
aria-selected="true" стоит только на выбранном узле. Проставлять
false на каждом — значит заставить диктора проговаривать «не выбрано» на
каждом шаге навигации; для одиночного выбора APG этого не требует.
Фокус
Один узел на всё дерево держит tabindex="0" (roving tabindex), остальные
недостижимы Tab. DOM-узлы строк лежат в общем реестре key → element:
навигация стрелками не должна обходить DOM поддерева на каждое нажатие.
Снаружи фокус ставится через focus(key?) — без аргумента на держателя roving
tabindex. Именно им пользуется GrTreeSelect, чтобы отдать дереву клавиатуру.
При виртуализации порядок обязателен: сперва прокрутка к узлу, и только
следующим тиком фокус. Узла вне окна в DOM нет, и focus() по нему уронил бы
фокус на body вместе с размонтированной строкой.
Обратный случай — строка с фокусом уезжает из окна при прокрутке — дерево разбирает само: фокус переходит на корень, и следующая же стрелка возвращает его на видимую строку. Условие ровно одно: фокус был на этой строке. Прокрутка дерева, которого пользователь не касался, фокус страницы не трогает.
Drag & drop
draggable включает перетаскивание, allowDrag/allowDrop его ограничивают.
Ручка переноса видна по dragHandleVisibility. По умолчанию auto: под
курсором там, где наведение есть, и всегда там, где его не бывает
(@media (hover: none)). Это не украшение: на тач-устройстве событий наведения
нет вовсе, поэтому ручка «по наведению» не появляется никогда — то есть жеста,
которым начинается перетаскивание, на экране просто нет, хотя touch-action: none
у ручки стоит ровно ради него. Режимы hover и always фиксируют поведение
явно.
Решает это медиазапрос, а не matchMedia: ответ нужен и на сервере, а
спрашивать среду в первом рендере нельзя — гидрация разойдётся.
Клавиатурный путь от этого не зависит: Shift со стрелкой двигает узел без
всякой ручки.
Обработчик drop гасит дефолт браузера до всех проверок: если событие
дошло до дерева, дефолт — это навигация по брошенной ссылке или открытие
брошенного файла поверх страницы.
Свёрнутая ветка под узлом раскрывается сама — если целиться внутрь неё (средняя треть строки) и задержаться там. Иначе положить узел внутрь свёрнутой ветки на нужное место было нельзя вовсе: перенос пришлось бы бросить, раскрыть её руками и начать заново.
Только на «внутрь»: целясь в соседа сверху или снизу, пользователь не просил менять раскладку, а раскрытие увело бы строки из-под курсора вместе с точкой, в которую он метил.
С virtual цель — отрисованная строка, и это свойство указателя, а не
недоделка: навестись на то, чего нет на экране, всё равно нечем. У края списка
работает автопрокрутка, и попадание пересчитывается на каждом её кадре —
доехавшая до окна строка становится целью сразу. Клавиатурный перенос от этого
не зависит вовсе: он ходит по модели.
Контекстное меню
nodeContextMenu отдаёт исходное событие первым аргументом — вызвать
preventDefault() и показать своё меню решает потребитель.
Токены
Размер выражен переменными --gr-tree-* (высота строки, отступы, размер
иконок, кегль), а не утилитарными классами: те же переменные объявлены точками
кастомизации, и size задаёт им дефолты, а не спорит с ними вторым каналом.
Цвета выводятся из --gr-primary/--gr-muted через color-mix — без hex-
фолбэков, которые в тёмной теме давали непредсказуемый результат.
Фон строки безопасен при любом значении, включая непрозрачное. Подсветку
рисует отдельный слой (::before), вставленный слева на отступ уровня: фон на
самой коробке шёл бы от левого края дерева и накрывал направляющие предков. Слой
уведён под содержимое строки (z-index: -1), а строка изолирована
(isolation: isolate) — без изоляции отрицательный слой уехал бы за фон
ближайшего предка с заливкой, и подсветка пропала бы на карточке.
Пара обязательна целиком, и обе половины держит e2e-гейт
apps/showcase/e2e/geometry.spec.ts: он задаёт непрозрачный фон и меряет по
пикселям, что подпись видна и что подсветка при этом есть. В jsdom это не
ловится — CSS компонента там не применяется, а полупрозрачные дефолты
маскируют дефект и в браузере.
Пропы этого примера