@bigplay/tree-view-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/collapse.d.ts +11 -0
- package/dist/defaults.d.ts +8 -0
- package/dist/fromLevels.d.ts +24 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +355 -0
- package/dist/index.js.map +1 -0
- package/dist/injectStyles.d.ts +10 -0
- package/dist/layout.d.ts +22 -0
- package/dist/links.d.ts +13 -0
- package/dist/normalize.d.ts +24 -0
- package/dist/panZoom.d.ts +54 -0
- package/dist/types.d.ts +91 -0
- package/dist/viewport.d.ts +25 -0
- package/package.json +53 -0
- package/styles.css +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @bigplay/tree-view-core
|
|
2
|
+
|
|
3
|
+
Раскладка дерева на чистом TypeScript. Ни Vue, ни d3, ни других зависимостей.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i @bigplay/tree-view-core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { toTree, layoutTree } from '@bigplay/tree-view-core'
|
|
11
|
+
|
|
12
|
+
const layout = layoutTree(toTree(data), { direction: 'right-to-left' })
|
|
13
|
+
|
|
14
|
+
layout.nodes // [{ id, data, x, y, width, height, depth, collapsed, hasChildren }, ...]
|
|
15
|
+
layout.links // [{ id, source, target, path }, ...] — path готов для <path d="...">
|
|
16
|
+
layout.width // размер холста
|
|
17
|
+
layout.height
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Что внутри:
|
|
21
|
+
|
|
22
|
+
| Функция | Зачем |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `toTree(data, accessors)` | любой формат (вложенный или плоский) → дерево с `id` и `depth` |
|
|
25
|
+
| `fromLevels(levels)` | дерево из уровней, когда связи заданы позицией, а не ссылками |
|
|
26
|
+
| `applyCollapsed(roots, ids)` | убирает детей свёрнутых узлов, не меняя исходные данные |
|
|
27
|
+
| `layoutTree(roots, options)` | координаты узлов и линии |
|
|
28
|
+
| `buildPath(from, to, style, direction)` | SVG-путь: `elbow`, `curve`, `straight` |
|
|
29
|
+
| `fitToViewport` / `zoomAtPoint` / `panBy` | математика зума и перетаскивания |
|
|
30
|
+
| `PanZoomController` | перетаскивание и зум на голом DOM (мышь + тач) |
|
|
31
|
+
|
|
32
|
+
Все функции чистые и покрыты тестами — работают и в браузере, и на сервере.
|
|
33
|
+
|
|
34
|
+
Компоненты поверх этого пакета: [`@bigplay/tree-view`](https://www.npmjs.com/package/@bigplay/tree-view) (Vue 3),
|
|
35
|
+
[`@bigplay/tree-view-vue2`](https://www.npmjs.com/package/@bigplay/tree-view-vue2) (Vue 2).
|
|
36
|
+
|
|
37
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TreeNode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Возвращает КОПИЮ дерева, где у свёрнутых узлов дети убраны из раскладки.
|
|
4
|
+
*
|
|
5
|
+
* Важно: исходные данные не изменяются. Старая версия библиотеки писала
|
|
6
|
+
* `node.children = null` прямо в объект пользователя — из-за этого ломался
|
|
7
|
+
* реактивный пересчёт и «терялись» ветки. Здесь такого нет.
|
|
8
|
+
*/
|
|
9
|
+
export declare function applyCollapsed<T>(roots: TreeNode<T>[], collapsedIds: ReadonlySet<string>): TreeNode<T>[];
|
|
10
|
+
/** Возвращает новое множество свёрнутых узлов: был свёрнут — развернём, и наоборот. */
|
|
11
|
+
export declare function toggleCollapsed(collapsedIds: ReadonlySet<string>, id: string): Set<string>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TreeViewOptions } from './types';
|
|
2
|
+
/** Значения по умолчанию. Меняются пропом `options` — частично, по одному полю. */
|
|
3
|
+
export declare const DEFAULT_OPTIONS: TreeViewOptions;
|
|
4
|
+
/**
|
|
5
|
+
* Дополняет пользовательские настройки значениями по умолчанию.
|
|
6
|
+
* Благодаря этому можно передать только `{ nodeWidth: 240 }` и ничего не сломать.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveOptions(options?: Partial<TreeViewOptions>): TreeViewOptions;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TreeNode } from './types';
|
|
2
|
+
export interface FromLevelsOptions<T> {
|
|
3
|
+
/** Идентификатор узла. По умолчанию — поле `id`, иначе «номер уровня.номер в уровне». */
|
|
4
|
+
getId?: (item: T, level: number, index: number) => string | number | null | undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Собирает дерево из «уровней», когда в данных нет ссылок на родителя,
|
|
8
|
+
* а связь задана только позицией — так устроены bounty-сетки: пары групп
|
|
9
|
+
* одного раунда сходятся в одну группу следующего.
|
|
10
|
+
*
|
|
11
|
+
* Уровни передаются от самого раннего раунда к финалу:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* fromLevels([
|
|
15
|
+
* [g1, g2, g3, g4], // первый раунд
|
|
16
|
+
* [g5, g6], // второй: g1+g2 → g5, g3+g4 → g6
|
|
17
|
+
* [g7], // финал: g5+g6 → g7
|
|
18
|
+
* ])
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Если уровни делятся неровно (например 5 групп на 2), лишние узлы
|
|
22
|
+
* достаются последнему родителю — так же, как это выглядит в вёрстке.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fromLevels<T>(levels: T[][], options?: FromLevelsOptions<T>): TreeNode<T>[];
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const P={nodeWidth:180,nodeHeight:64,levelGap:60,siblingGap:16,direction:"top-to-bottom",linkStyle:"elbow"};function C(e){return{...P,...e}}function z(e){return e?.children}function I(e){return e?.id}function A(e,t={}){if(e==null)return[];const n=Array.isArray(e)?e:[e];return n.length===0?[]:t.getParentId?U(n,t):Y(n,t)}function Y(e,t){const n=t.getId??I,i=t.getChildren??z,r=new Set;function o(s,l,h,d){const a=L(n(s),d);S(a,r);const p=(i(s)??[]).map((m,g)=>o(m,l+1,a,`${d}.${g}`));return{id:a,data:s,depth:l,parentId:h,children:p,hasChildren:p.length>0,collapsed:!1}}return e.map((s,l)=>o(s,0,null,String(l)))}function U(e,t){const n=t.getId??I,i=new Set,r=e.map((h,d)=>{const a=L(n(h),String(d));return S(a,i),{id:a,data:h,depth:0,parentId:null,children:[],hasChildren:!1,collapsed:!1}}),o=new Map(r.map(h=>[h.id,h])),s=[];if(r.forEach((h,d)=>{const a=t.getParentId(e[d]),c=a==null?void 0:o.get(String(a));if(!c){s.push(h);return}h.parentId=c.id,c.children.push(h),c.hasChildren=!0}),W(s)!==r.length)throw new Error("[tree-view] В данных есть цикл: часть узлов ссылается друг на друга по кругу.");return s}function W(e){const t=[...e];let n=0;for(;t.length>0;){const i=t.shift();n+=1;for(const r of i.children)r.depth=i.depth+1,t.push(r)}return n}function L(e,t){return e==null||e===""?t:String(e)}function S(e,t){if(t.has(e))throw new Error(`[tree-view] Повторяющийся id узла: "${e}". Идентификаторы должны быть уникальными.`);t.add(e)}function _(e,t={}){const n=e.filter(o=>o.length>0);if(n.length===0)return[];const i=n.length-1,r=n.map((o,s)=>o.map((l,h)=>({id:k(l,s,h,t.getId),data:l,depth:i-s,parentId:null,children:[],hasChildren:!1,collapsed:!1})));for(let o=0;o<i;o+=1){const s=r[o],l=r[o+1],h=Math.ceil(s.length/l.length);s.forEach((d,a)=>{const c=l[Math.min(Math.floor(a/h),l.length-1)];d.parentId=c.id,c.children.push(d),c.hasChildren=!0})}return r[i]}function k(e,t,n,i){const r=i?i(e,t,n):e?.id;return r==null||r===""?`${t}.${n}`:String(r)}function X(e,t){if(t.size===0)return e;function n(i){const r=t.has(i.id);return{...i,collapsed:r,children:r?[]:i.children.map(n)}}return e.map(n)}function F(e,t){const n=new Set(e);return n.delete(t)||n.add(t),n}function u(e){return Math.round(e*100)/100}function H(e){return e==="top-to-bottom"||e==="bottom-to-top"}function v(e,t,n){const i=e.x+e.width/2,r=e.y+e.height/2,o=t.x+t.width/2,s=t.y+t.height/2;switch(n){case"top-to-bottom":return{from:{x:i,y:e.y+e.height},to:{x:o,y:t.y}};case"bottom-to-top":return{from:{x:i,y:e.y},to:{x:o,y:t.y+t.height}};case"left-to-right":return{from:{x:e.x+e.width,y:r},to:{x:t.x,y:s}};case"right-to-left":return{from:{x:e.x,y:r},to:{x:t.x+t.width,y:s}}}}function E(e,t,n,i){const r=u(e.x),o=u(e.y),s=u(t.x),l=u(t.y);if(n==="straight")return`M ${r} ${o} L ${s} ${l}`;const h=H(i),d=u((r+s)/2),a=u((o+l)/2);return n==="elbow"?h?`M ${r} ${o} L ${r} ${a} L ${s} ${a} L ${s} ${l}`:`M ${r} ${o} L ${d} ${o} L ${d} ${l} L ${s} ${l}`:h?`M ${r} ${o} C ${r} ${a} ${s} ${a} ${s} ${l}`:`M ${r} ${o} C ${d} ${o} ${d} ${l} ${s} ${l}`}function b(e,t){const n=new Map(e.map(r=>[r.id,r])),i=[];for(const r of e){const o=r.parentId==null?void 0:n.get(r.parentId);if(!o)continue;const{from:s,to:l}=v(o,r,t.direction);i.push({id:`${o.id}->${r.id}`,source:o,target:r,path:E(s,l,t.linkStyle,t.direction)})}return i}function N(e){const t=e.direction==="top-to-bottom"||e.direction==="bottom-to-top",n=t?e.nodeHeight:e.nodeWidth,i=t?e.nodeWidth:e.nodeHeight;return{vertical:t,alongSize:n,acrossSize:i,alongStep:n+e.levelGap,acrossStep:i+e.siblingGap}}function M(e){const t=new Map;let n=0,i=0;function r(o){if(o.depth>i&&(i=o.depth),o.children.length===0){const h=n;return n+=1,t.set(o.id,h),h}const s=o.children.map(r),l=(s[0]+s[s.length-1])/2;return t.set(o.id,l),l}return e.forEach(r),{orders:t,maxOrder:Math.max(0,n-1),maxDepth:i}}function T(e){const t=[];function n(i){t.push(i),i.children.forEach(n)}return e.forEach(n),t}function Z(e,t){const n=C(t),i=N(n);if(e.length===0)return{nodes:[],links:[],width:0,height:0};const{orders:r,maxOrder:o,maxDepth:s}=M(e),l=s*i.alongStep+i.alongSize,h=o*i.acrossStep+i.acrossSize,d=n.direction==="bottom-to-top"||n.direction==="right-to-left",a=T(e).map(c=>{const p=c.depth*i.alongStep,m=r.get(c.id)*i.acrossStep,g=d?l-i.alongSize-p:p;return{id:c.id,data:c.data,depth:c.depth,parentId:c.parentId,x:i.vertical?m:g,y:i.vertical?g:m,width:n.nodeWidth,height:n.nodeHeight,hasChildren:c.hasChildren,collapsed:c.collapsed}});return{nodes:a,links:b(a,n),width:i.vertical?h:l,height:i.vertical?l:h}}const $={min:.2,max:3},x={x:0,y:0,scale:1};function w(e,t,n){return Math.min(Math.max(e,t),n)}function O(e,t,n=24,i=$){if(e.width<=0||e.height<=0||t.width<=0||t.height<=0)return{...x};const r=Math.max(t.width-n*2,1),o=Math.max(t.height-n*2,1),s=Math.min(r/e.width,o/e.height,1),l=w(s,i.min,i.max);return{scale:l,x:(t.width-e.width*l)/2,y:(t.height-e.height*l)/2}}function D(e,t,n){return{...e,x:e.x+t,y:e.y+n}}function y(e,t,n,i=$){const r=w(e.scale*t,i.min,i.max);if(r===e.scale)return e;const o=(n.x-e.x)/e.scale,s=(n.y-e.y)/e.scale;return{scale:r,x:n.x-o*r,y:n.y-s*r}}function G(e){return`translate(${e.x}px, ${e.y}px) scale(${e.scale})`}const f=1.2;class R{constructor(t){this.params=t,this.transform={...x},this.element=null,this.pointers=new Map,this.lastPinchDistance=0,this.onPointerDown=n=>{this.isPannable()&&(n.target?.closest?.("[data-tv-no-pan]")||(this.pointers.set(n.pointerId,{x:n.clientX,y:n.clientY}),this.element?.setPointerCapture?.(n.pointerId)))},this.onPointerMove=n=>{const i=this.pointers.get(n.pointerId);if(i){if(this.pointers.set(n.pointerId,{x:n.clientX,y:n.clientY}),this.pointers.size===2){this.pinch();return}this.set(D(this.transform,n.clientX-i.x,n.clientY-i.y))}},this.onPointerUp=n=>{this.pointers.delete(n.pointerId),this.pointers.size<2&&(this.lastPinchDistance=0)},this.onWheel=n=>{if(!this.isZoomable())return;n.preventDefault();const i=n.deltaY<0?f:1/f;this.set(y(this.transform,i,this.toLocalPoint(n),this.limits))}}attach(t){this.detach(),this.element=t,t.addEventListener("pointerdown",this.onPointerDown),t.addEventListener("pointermove",this.onPointerMove),t.addEventListener("pointerup",this.onPointerUp),t.addEventListener("pointercancel",this.onPointerUp),t.addEventListener("wheel",this.onWheel,{passive:!1})}detach(){const t=this.element;t&&(t.removeEventListener("pointerdown",this.onPointerDown),t.removeEventListener("pointermove",this.onPointerMove),t.removeEventListener("pointerup",this.onPointerUp),t.removeEventListener("pointercancel",this.onPointerUp),t.removeEventListener("wheel",this.onWheel),this.element=null,this.pointers.clear(),this.lastPinchDistance=0)}fit(t,n=24){const i=this.element;i&&this.set(O(t,{width:i.clientWidth,height:i.clientHeight},n,this.limits))}zoomIn(){this.zoomToCenter(f)}zoomOut(){this.zoomToCenter(1/f)}reset(){this.set({...x})}set(t){this.transform={...t},this.params.onChange(this.transform)}get limits(){return this.params.limits?.()??$}isPannable(){return this.params.pannable?.()??!0}isZoomable(){return this.params.zoomable?.()??!0}toLocalPoint(t){const n=this.element?.getBoundingClientRect();return{x:t.clientX-(n?.left??0),y:t.clientY-(n?.top??0)}}zoomToCenter(t){const n=this.element,i={x:(n?.clientWidth??0)/2,y:(n?.clientHeight??0)/2};this.set(y(this.transform,t,i,this.limits))}pinch(){if(!this.isZoomable())return;const[t,n]=[...this.pointers.values()];if(!t||!n)return;const i=Math.hypot(t.x-n.x,t.y-n.y);if(this.lastPinchDistance>0&&i>0){const r=this.toLocalPoint({clientX:(t.x+n.x)/2,clientY:(t.y+n.y)/2});this.set(y(this.transform,i/this.lastPinchDistance,r,this.limits))}this.lastPinchDistance=i}}function B(e,t="tree-view-styles"){if(typeof document>"u"||document.getElementById(t))return;const n=document.createElement("style");n.id=t,n.textContent=e,document.head.appendChild(n)}exports.DEFAULT_OPTIONS=P;exports.DEFAULT_SCALE_LIMITS=$;exports.IDENTITY_TRANSFORM=x;exports.PanZoomController=R;exports.ZOOM_STEP=f;exports.applyCollapsed=X;exports.assignOrders=M;exports.buildLinks=b;exports.buildPath=E;exports.clamp=w;exports.fitToViewport=O;exports.flatten=T;exports.fromLevels=_;exports.getAnchors=v;exports.injectStyles=B;exports.layoutTree=Z;exports.panBy=D;exports.resolveOptions=C;exports.toCssTransform=G;exports.toTree=A;exports.toggleCollapsed=F;exports.zoomAtPoint=y;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/defaults.ts","../src/normalize.ts","../src/fromLevels.ts","../src/collapse.ts","../src/links.ts","../src/layout.ts","../src/viewport.ts","../src/panZoom.ts","../src/injectStyles.ts"],"sourcesContent":["import type { TreeViewOptions } from './types'\n\n/** Значения по умолчанию. Меняются пропом `options` — частично, по одному полю. */\nexport const DEFAULT_OPTIONS: TreeViewOptions = {\n nodeWidth: 180,\n nodeHeight: 64,\n levelGap: 60,\n siblingGap: 16,\n direction: 'top-to-bottom',\n linkStyle: 'elbow',\n}\n\n/**\n * Дополняет пользовательские настройки значениями по умолчанию.\n * Благодаря этому можно передать только `{ nodeWidth: 240 }` и ничего не сломать.\n */\nexport function resolveOptions(options?: Partial<TreeViewOptions>): TreeViewOptions {\n return { ...DEFAULT_OPTIONS, ...options }\n}\n","import type { TreeNode } from './types'\n\n/**\n * Как читать чужие данные.\n * Любую из функций можно не передавать — тогда работает поведение по умолчанию.\n */\nexport interface Accessors<T> {\n /** Идентификатор узла. По умолчанию — поле `id`, а если его нет — путь вида `\"0.1.2\"`. */\n getId?: (item: T) => string | number | null | undefined\n /** Дети узла. По умолчанию — поле `children`. */\n getChildren?: (item: T) => T[] | null | undefined\n /**\n * Идентификатор родителя. Если передан — данные считаются\n * ПЛОСКИМ списком (`[{ id, parentId }, ...]`), а не вложенным деревом.\n */\n getParentId?: (item: T) => string | number | null | undefined\n}\n\n/** Читает `item.children`, если пользователь не задал свой аксессор. */\nfunction defaultGetChildren<T>(item: T): T[] | null | undefined {\n return (item as { children?: T[] })?.children\n}\n\n/** Читает `item.id`, если пользователь не задал свой аксессор. */\nfunction defaultGetId<T>(item: T): string | number | null | undefined {\n return (item as { id?: string | number })?.id\n}\n\n/**\n * Превращает входные данные в массив корней дерева.\n *\n * Поддерживает два формата:\n * 1. Вложенный: `{ id: 1, children: [...] }` или массив таких объектов;\n * 2. Плоский: `[{ id: 2, parentId: 1 }, ...]` — включается передачей `getParentId`.\n */\nexport function toTree<T>(input: T | T[] | null | undefined, accessors: Accessors<T> = {}): TreeNode<T>[] {\n if (input == null) return []\n\n const items = Array.isArray(input) ? input : [input]\n if (items.length === 0) return []\n\n return accessors.getParentId\n ? fromFlatList(items, accessors as Accessors<T> & { getParentId: NonNullable<Accessors<T>['getParentId']> })\n : fromNested(items, accessors)\n}\n\n/** Вложенный формат: рекурсивно обходим `children`. */\nfunction fromNested<T>(items: T[], accessors: Accessors<T>): TreeNode<T>[] {\n const getId = accessors.getId ?? defaultGetId\n const getChildren = accessors.getChildren ?? defaultGetChildren\n const usedIds = new Set<string>()\n\n /** `path` — запасной идентификатор: он стабилен, пока не меняется порядок данных. */\n function build(item: T, depth: number, parentId: string | null, path: string): TreeNode<T> {\n const id = toId(getId(item), path)\n assertUniqueId(id, usedIds)\n\n const rawChildren = getChildren(item) ?? []\n const children = rawChildren.map((child, index) => build(child, depth + 1, id, `${path}.${index}`))\n\n return { id, data: item, depth, parentId, children, hasChildren: children.length > 0, collapsed: false }\n }\n\n return items.map((item, index) => build(item, 0, null, String(index)))\n}\n\n/** Плоский формат: собираем детей по `parentId`. */\nfunction fromFlatList<T>(\n items: T[],\n accessors: Accessors<T> & { getParentId: NonNullable<Accessors<T>['getParentId']> },\n): TreeNode<T>[] {\n const getId = accessors.getId ?? defaultGetId\n const usedIds = new Set<string>()\n\n // Шаг 1: заготовки узлов без связей.\n const nodes = items.map((item, index) => {\n const id = toId(getId(item), String(index))\n assertUniqueId(id, usedIds)\n return { id, data: item, depth: 0, parentId: null, children: [], hasChildren: false, collapsed: false } as TreeNode<T>\n })\n\n const byId = new Map(nodes.map((node) => [node.id, node]))\n const roots: TreeNode<T>[] = []\n\n // Шаг 2: каждого ребёнка кладём в массив его родителя.\n nodes.forEach((node, index) => {\n const rawParentId = accessors.getParentId(items[index]!)\n const parent = rawParentId == null ? undefined : byId.get(String(rawParentId))\n\n if (!parent) {\n roots.push(node)\n return\n }\n node.parentId = parent.id\n parent.children.push(node)\n parent.hasChildren = true\n })\n\n // Шаг 3: считаем глубину сверху вниз. Заодно ловим циклы (A → B → A).\n const visited = setDepths(roots)\n if (visited !== nodes.length) {\n throw new Error('[tree-view] В данных есть цикл: часть узлов ссылается друг на друга по кругу.')\n }\n\n return roots\n}\n\n/** Проставляет `depth` обходом в ширину и возвращает число посещённых узлов. */\nfunction setDepths<T>(roots: TreeNode<T>[]): number {\n const queue = [...roots]\n let visited = 0\n\n while (queue.length > 0) {\n const node = queue.shift()!\n visited += 1\n for (const child of node.children) {\n child.depth = node.depth + 1\n queue.push(child)\n }\n }\n return visited\n}\n\n/** Приводит идентификатор к строке, подставляя путь, если его нет. */\nfunction toId(rawId: string | number | null | undefined, fallback: string): string {\n return rawId == null || rawId === '' ? fallback : String(rawId)\n}\n\n/** Одинаковые id ломают отрисовку молча — лучше сразу сказать об этом вслух. */\nfunction assertUniqueId(id: string, usedIds: Set<string>): void {\n if (usedIds.has(id)) {\n throw new Error(`[tree-view] Повторяющийся id узла: \"${id}\". Идентификаторы должны быть уникальными.`)\n }\n usedIds.add(id)\n}\n","import type { TreeNode } from './types'\n\nexport interface FromLevelsOptions<T> {\n /** Идентификатор узла. По умолчанию — поле `id`, иначе «номер уровня.номер в уровне». */\n getId?: (item: T, level: number, index: number) => string | number | null | undefined\n}\n\n/**\n * Собирает дерево из «уровней», когда в данных нет ссылок на родителя,\n * а связь задана только позицией — так устроены bounty-сетки: пары групп\n * одного раунда сходятся в одну группу следующего.\n *\n * Уровни передаются от самого раннего раунда к финалу:\n *\n * ```ts\n * fromLevels([\n * [g1, g2, g3, g4], // первый раунд\n * [g5, g6], // второй: g1+g2 → g5, g3+g4 → g6\n * [g7], // финал: g5+g6 → g7\n * ])\n * ```\n *\n * Если уровни делятся неровно (например 5 групп на 2), лишние узлы\n * достаются последнему родителю — так же, как это выглядит в вёрстке.\n */\nexport function fromLevels<T>(levels: T[][], options: FromLevelsOptions<T> = {}): TreeNode<T>[] {\n const filled = levels.filter((level) => level.length > 0)\n if (filled.length === 0) return []\n\n const lastLevel = filled.length - 1\n\n // Сначала создаём узлы всех уровней, связи проставим следующим шагом.\n const nodes: TreeNode<T>[][] = filled.map((level, levelIndex) =>\n level.map((item, index) => ({\n id: resolveId(item, levelIndex, index, options.getId),\n data: item,\n // Финал — корень, поэтому глубина считается от последнего уровня.\n depth: lastLevel - levelIndex,\n parentId: null,\n children: [],\n hasChildren: false,\n collapsed: false,\n })),\n )\n\n for (let levelIndex = 0; levelIndex < lastLevel; levelIndex += 1) {\n const current = nodes[levelIndex]!\n const parents = nodes[levelIndex + 1]!\n // Сколько узлов этого уровня приходится на одного родителя.\n const perParent = Math.ceil(current.length / parents.length)\n\n current.forEach((node, index) => {\n const parent = parents[Math.min(Math.floor(index / perParent), parents.length - 1)]!\n node.parentId = parent.id\n parent.children.push(node)\n parent.hasChildren = true\n })\n }\n\n return nodes[lastLevel]!\n}\n\nfunction resolveId<T>(\n item: T,\n level: number,\n index: number,\n getId: FromLevelsOptions<T>['getId'],\n): string {\n const raw = getId ? getId(item, level, index) : (item as { id?: string | number })?.id\n return raw == null || raw === '' ? `${level}.${index}` : String(raw)\n}\n","import type { TreeNode } from './types'\n\n/**\n * Возвращает КОПИЮ дерева, где у свёрнутых узлов дети убраны из раскладки.\n *\n * Важно: исходные данные не изменяются. Старая версия библиотеки писала\n * `node.children = null` прямо в объект пользователя — из-за этого ломался\n * реактивный пересчёт и «терялись» ветки. Здесь такого нет.\n */\nexport function applyCollapsed<T>(roots: TreeNode<T>[], collapsedIds: ReadonlySet<string>): TreeNode<T>[] {\n if (collapsedIds.size === 0) return roots\n\n function walk(node: TreeNode<T>): TreeNode<T> {\n const collapsed = collapsedIds.has(node.id)\n return {\n ...node,\n collapsed,\n children: collapsed ? [] : node.children.map(walk),\n }\n }\n\n return roots.map(walk)\n}\n\n/** Возвращает новое множество свёрнутых узлов: был свёрнут — развернём, и наоборот. */\nexport function toggleCollapsed(collapsedIds: ReadonlySet<string>, id: string): Set<string> {\n const next = new Set(collapsedIds)\n if (!next.delete(id)) next.add(id)\n return next\n}\n","import type { Direction, LayoutLink, LayoutNode, LinkStyle, Point, TreeViewOptions } from './types'\n\n/** Округление до сотых: убирает «хвосты» вроде 12.000000001 в атрибуте `d`. */\nfunction round(value: number): number {\n return Math.round(value * 100) / 100\n}\n\n/** Вертикальные направления рисуются сверху вниз, горизонтальные — слева направо. */\nfunction isVertical(direction: Direction): boolean {\n return direction === 'top-to-bottom' || direction === 'bottom-to-top'\n}\n\n/**\n * Точки, из которой линия выходит, и в которую приходит.\n * Всегда середина стороны карточки, обращённой к собеседнику.\n */\nexport function getAnchors<T>(\n source: LayoutNode<T>,\n target: LayoutNode<T>,\n direction: Direction,\n): { from: Point; to: Point } {\n const sourceCenterX = source.x + source.width / 2\n const sourceCenterY = source.y + source.height / 2\n const targetCenterX = target.x + target.width / 2\n const targetCenterY = target.y + target.height / 2\n\n switch (direction) {\n case 'top-to-bottom':\n return { from: { x: sourceCenterX, y: source.y + source.height }, to: { x: targetCenterX, y: target.y } }\n case 'bottom-to-top':\n return { from: { x: sourceCenterX, y: source.y }, to: { x: targetCenterX, y: target.y + target.height } }\n case 'left-to-right':\n return { from: { x: source.x + source.width, y: sourceCenterY }, to: { x: target.x, y: targetCenterY } }\n case 'right-to-left':\n return { from: { x: source.x, y: sourceCenterY }, to: { x: target.x + target.width, y: targetCenterY } }\n }\n}\n\n/** Строит атрибут `d` для `<path>`: прямая, «ступенька» или кривая Безье. */\nexport function buildPath(from: Point, to: Point, style: LinkStyle, direction: Direction): string {\n const x1 = round(from.x)\n const y1 = round(from.y)\n const x2 = round(to.x)\n const y2 = round(to.y)\n\n if (style === 'straight') {\n return `M ${x1} ${y1} L ${x2} ${y2}`\n }\n\n const vertical = isVertical(direction)\n // Середина между узлами — по той оси, вдоль которой растёт дерево.\n const midX = round((x1 + x2) / 2)\n const midY = round((y1 + y2) / 2)\n\n if (style === 'elbow') {\n return vertical\n ? `M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`\n : `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`\n }\n\n return vertical\n ? `M ${x1} ${y1} C ${x1} ${midY} ${x2} ${midY} ${x2} ${y2}`\n : `M ${x1} ${y1} C ${midX} ${y1} ${midX} ${y2} ${x2} ${y2}`\n}\n\n/** Для каждого узла с родителем создаёт линию «родитель → ребёнок». */\nexport function buildLinks<T>(nodes: LayoutNode<T>[], options: TreeViewOptions): LayoutLink<T>[] {\n const byId = new Map(nodes.map((node) => [node.id, node]))\n const links: LayoutLink<T>[] = []\n\n for (const node of nodes) {\n const parent = node.parentId == null ? undefined : byId.get(node.parentId)\n if (!parent) continue\n\n const { from, to } = getAnchors(parent, node, options.direction)\n links.push({\n id: `${parent.id}->${node.id}`,\n source: parent,\n target: node,\n path: buildPath(from, to, options.linkStyle, options.direction),\n })\n }\n\n return links\n}\n","import { resolveOptions } from './defaults'\nimport { buildLinks } from './links'\nimport type { Layout, LayoutNode, TreeNode, TreeViewOptions } from './types'\n\n/**\n * Раскладка работает в двух «своих» осях, чтобы не писать четыре варианта кода:\n * - along — ось роста дерева (глубина): уровень 0, уровень 1, ...\n * - across — ось поперёк уровня: соседи стоят здесь друг за другом.\n * В самом конце пара (along, across) превращается в привычные (x, y).\n */\ninterface Axis {\n vertical: boolean\n /** Размер карточки вдоль оси роста. */\n alongSize: number\n /** Размер карточки поперёк оси роста. */\n acrossSize: number\n /** Шаг между уровнями (размер + зазор). */\n alongStep: number\n /** Шаг между соседями (размер + зазор). */\n acrossStep: number\n}\n\nfunction getAxis(options: TreeViewOptions): Axis {\n const vertical = options.direction === 'top-to-bottom' || options.direction === 'bottom-to-top'\n const alongSize = vertical ? options.nodeHeight : options.nodeWidth\n const acrossSize = vertical ? options.nodeWidth : options.nodeHeight\n\n return {\n vertical,\n alongSize,\n acrossSize,\n alongStep: alongSize + options.levelGap,\n acrossStep: acrossSize + options.siblingGap,\n }\n}\n\n/**\n * Считает позицию каждого узла поперёк уровня («номер колонки»).\n *\n * Правило всего одно и его легко проверить руками:\n * - лист встаёт в следующую свободную колонку (0, 1, 2, ...);\n * - родитель встаёт ровно посередине между первым и последним ребёнком.\n *\n * Поэтому соседние ветки никогда не накладываются друг на друга.\n */\nexport function assignOrders<T>(roots: TreeNode<T>[]): {\n orders: Map<string, number>\n maxOrder: number\n maxDepth: number\n} {\n const orders = new Map<string, number>()\n let nextLeafOrder = 0\n let maxDepth = 0\n\n function place(node: TreeNode<T>): number {\n if (node.depth > maxDepth) maxDepth = node.depth\n\n if (node.children.length === 0) {\n const order = nextLeafOrder\n nextLeafOrder += 1\n orders.set(node.id, order)\n return order\n }\n\n const childOrders = node.children.map(place)\n const order = (childOrders[0]! + childOrders[childOrders.length - 1]!) / 2\n orders.set(node.id, order)\n return order\n }\n\n roots.forEach(place)\n\n return { orders, maxOrder: Math.max(0, nextLeafOrder - 1), maxDepth }\n}\n\n/** Разворачивает дерево в плоский массив (порядок — «сверху вниз», как в данных). */\nexport function flatten<T>(roots: TreeNode<T>[]): TreeNode<T>[] {\n const result: TreeNode<T>[] = []\n\n function walk(node: TreeNode<T>): void {\n result.push(node)\n node.children.forEach(walk)\n }\n\n roots.forEach(walk)\n return result\n}\n\n/**\n * Главная функция раскладки: из дерева делает координаты и линии.\n * Чистая — одни и те же данные всегда дают один и тот же результат.\n */\nexport function layoutTree<T>(roots: TreeNode<T>[], options?: Partial<TreeViewOptions>): Layout<T> {\n const resolved = resolveOptions(options)\n const axis = getAxis(resolved)\n\n if (roots.length === 0) {\n return { nodes: [], links: [], width: 0, height: 0 }\n }\n\n const { orders, maxOrder, maxDepth } = assignOrders(roots)\n\n // Габариты содержимого: последний уровень + последняя колонка.\n const alongExtent = maxDepth * axis.alongStep + axis.alongSize\n const acrossExtent = maxOrder * axis.acrossStep + axis.acrossSize\n // У «перевёрнутых» направлений уровень 0 должен оказаться в конце холста.\n const mirrored = resolved.direction === 'bottom-to-top' || resolved.direction === 'right-to-left'\n\n const nodes: LayoutNode<T>[] = flatten(roots).map((node) => {\n const along = node.depth * axis.alongStep\n const across = orders.get(node.id)! * axis.acrossStep\n const alongPosition = mirrored ? alongExtent - axis.alongSize - along : along\n\n return {\n id: node.id,\n data: node.data,\n depth: node.depth,\n parentId: node.parentId,\n x: axis.vertical ? across : alongPosition,\n y: axis.vertical ? alongPosition : across,\n width: resolved.nodeWidth,\n height: resolved.nodeHeight,\n hasChildren: node.hasChildren,\n collapsed: node.collapsed,\n }\n })\n\n return {\n nodes,\n links: buildLinks(nodes, resolved),\n width: axis.vertical ? acrossExtent : alongExtent,\n height: axis.vertical ? alongExtent : acrossExtent,\n }\n}\n","import type { Point, Size, Transform } from './types'\n\n/** Ограничения зума. */\nexport interface ScaleLimits {\n min: number\n max: number\n}\n\nexport const DEFAULT_SCALE_LIMITS: ScaleLimits = { min: 0.2, max: 3 }\n\n/** Начальное состояние холста: ничего не сдвинуто, масштаб 1. */\nexport const IDENTITY_TRANSFORM: Transform = { x: 0, y: 0, scale: 1 }\n\n/** Зажимает число в диапазоне [min, max]. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Подбирает масштаб и сдвиг так, чтобы всё дерево влезло в видимую область\n * и оказалось по центру. `padding` — отступ от краёв в пикселях.\n */\nexport function fitToViewport(\n content: Size,\n viewport: Size,\n padding = 24,\n limits: ScaleLimits = DEFAULT_SCALE_LIMITS,\n): Transform {\n if (content.width <= 0 || content.height <= 0 || viewport.width <= 0 || viewport.height <= 0) {\n return { ...IDENTITY_TRANSFORM }\n }\n\n const availableWidth = Math.max(viewport.width - padding * 2, 1)\n const availableHeight = Math.max(viewport.height - padding * 2, 1)\n // Берём меньший из двух коэффициентов — иначе дерево вылезет за край.\n const raw = Math.min(availableWidth / content.width, availableHeight / content.height, 1)\n const scale = clamp(raw, limits.min, limits.max)\n\n return {\n scale,\n x: (viewport.width - content.width * scale) / 2,\n y: (viewport.height - content.height * scale) / 2,\n }\n}\n\n/** Сдвигает холст на `dx`/`dy` пикселей экрана. */\nexport function panBy(transform: Transform, dx: number, dy: number): Transform {\n return { ...transform, x: transform.x + dx, y: transform.y + dy }\n}\n\n/**\n * Меняет масштаб так, чтобы точка `point` (координаты экрана) осталась\n * под курсором — это и есть «зум к курсору».\n */\nexport function zoomAtPoint(\n transform: Transform,\n factor: number,\n point: Point,\n limits: ScaleLimits = DEFAULT_SCALE_LIMITS,\n): Transform {\n const scale = clamp(transform.scale * factor, limits.min, limits.max)\n if (scale === transform.scale) return transform\n\n // Точка в координатах содержимого — она не должна поменяться.\n const contentX = (point.x - transform.x) / transform.scale\n const contentY = (point.y - transform.y) / transform.scale\n\n return { scale, x: point.x - contentX * scale, y: point.y - contentY * scale }\n}\n\n/** Готовая CSS-строка для `transform` холста. */\nexport function toCssTransform(transform: Transform): string {\n return `translate(${transform.x}px, ${transform.y}px) scale(${transform.scale})`\n}\n","import {\n DEFAULT_SCALE_LIMITS,\n IDENTITY_TRANSFORM,\n fitToViewport,\n panBy,\n zoomAtPoint,\n type ScaleLimits,\n} from './viewport'\nimport type { Size, Transform } from './types'\n\n/** Во сколько раз меняется масштаб за одно движение колеса или клик по кнопке. */\nexport const ZOOM_STEP = 1.2\n\nexport interface PanZoomParams {\n /** Можно ли двигать холст. По умолчанию да. */\n pannable?: () => boolean\n /** Можно ли менять масштаб. По умолчанию да. */\n zoomable?: () => boolean\n /** Пределы масштаба. */\n limits?: () => ScaleLimits\n /** Вызывается после каждого изменения — здесь фреймворк обновляет свой state. */\n onChange: (transform: Transform) => void\n}\n\n/**\n * Перетаскивание и зум без привязки к фреймворку: и Vue 2, и Vue 3, и что угодно\n * ещё просто создают контроллер и подписываются на `onChange`.\n *\n * Слушаем pointer-события — они одинаково работают мышью и пальцем,\n * поэтому отдельного кода для мобильных не нужно.\n */\nexport class PanZoomController {\n transform: Transform = { ...IDENTITY_TRANSFORM }\n\n private element: HTMLElement | null = null\n /** Активные касания: id указателя → последняя позиция на экране. */\n private readonly pointers = new Map<number, { x: number; y: number }>()\n /** Расстояние между двумя пальцами на прошлом событии — для «щипка». */\n private lastPinchDistance = 0\n\n constructor(private readonly params: PanZoomParams) {}\n\n /** Подписывается на события элемента. Повторный вызов сначала отписывается. */\n attach(element: HTMLElement): void {\n this.detach()\n this.element = element\n\n element.addEventListener('pointerdown', this.onPointerDown)\n element.addEventListener('pointermove', this.onPointerMove)\n element.addEventListener('pointerup', this.onPointerUp)\n element.addEventListener('pointercancel', this.onPointerUp)\n // passive: false — иначе браузер не даст отменить прокрутку страницы.\n element.addEventListener('wheel', this.onWheel, { passive: false })\n }\n\n /** Снимает все слушатели. Обязательно вызывать при размонтировании. */\n detach(): void {\n const element = this.element\n if (!element) return\n\n element.removeEventListener('pointerdown', this.onPointerDown)\n element.removeEventListener('pointermove', this.onPointerMove)\n element.removeEventListener('pointerup', this.onPointerUp)\n element.removeEventListener('pointercancel', this.onPointerUp)\n element.removeEventListener('wheel', this.onWheel)\n\n this.element = null\n this.pointers.clear()\n this.lastPinchDistance = 0\n }\n\n /** Вписывает содержимое в размер элемента и центрирует. */\n fit(content: Size, padding = 24): void {\n const element = this.element\n if (!element) return\n\n this.set(\n fitToViewport(\n content,\n { width: element.clientWidth, height: element.clientHeight },\n padding,\n this.limits,\n ),\n )\n }\n\n zoomIn(): void {\n this.zoomToCenter(ZOOM_STEP)\n }\n\n zoomOut(): void {\n this.zoomToCenter(1 / ZOOM_STEP)\n }\n\n reset(): void {\n this.set({ ...IDENTITY_TRANSFORM })\n }\n\n /** Устанавливает состояние напрямую (например, восстановление после перезагрузки). */\n set(transform: Transform): void {\n this.transform = { ...transform }\n this.params.onChange(this.transform)\n }\n\n private get limits(): ScaleLimits {\n return this.params.limits?.() ?? DEFAULT_SCALE_LIMITS\n }\n\n private isPannable(): boolean {\n return this.params.pannable?.() ?? true\n }\n\n private isZoomable(): boolean {\n return this.params.zoomable?.() ?? true\n }\n\n /** Переводит координаты экрана в координаты внутри элемента. */\n private toLocalPoint(point: { clientX: number; clientY: number }) {\n const rect = this.element?.getBoundingClientRect()\n return { x: point.clientX - (rect?.left ?? 0), y: point.clientY - (rect?.top ?? 0) }\n }\n\n private zoomToCenter(factor: number): void {\n const element = this.element\n const center = { x: (element?.clientWidth ?? 0) / 2, y: (element?.clientHeight ?? 0) / 2 }\n this.set(zoomAtPoint(this.transform, factor, center, this.limits))\n }\n\n // Стрелочные функции — чтобы `this` оставался контроллером и чтобы\n // removeEventListener получил ту же самую ссылку на обработчик.\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n if (!this.isPannable()) return\n // Элементу с атрибутом data-tv-no-pan разрешаем свои жесты (кнопки, инпуты).\n if ((event.target as HTMLElement | null)?.closest?.('[data-tv-no-pan]')) return\n\n this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n this.element?.setPointerCapture?.(event.pointerId)\n }\n\n private readonly onPointerMove = (event: PointerEvent): void => {\n const previous = this.pointers.get(event.pointerId)\n if (!previous) return\n\n this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n\n if (this.pointers.size === 2) {\n this.pinch()\n return\n }\n\n this.set(panBy(this.transform, event.clientX - previous.x, event.clientY - previous.y))\n }\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n this.pointers.delete(event.pointerId)\n if (this.pointers.size < 2) this.lastPinchDistance = 0\n }\n\n private readonly onWheel = (event: WheelEvent): void => {\n if (!this.isZoomable()) return\n event.preventDefault()\n\n const factor = event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP\n this.set(zoomAtPoint(this.transform, factor, this.toLocalPoint(event), this.limits))\n }\n\n /** Два пальца: масштабируем вокруг точки ровно между ними. */\n private pinch(): void {\n if (!this.isZoomable()) return\n\n const [first, second] = [...this.pointers.values()]\n if (!first || !second) return\n\n const distance = Math.hypot(first.x - second.x, first.y - second.y)\n\n if (this.lastPinchDistance > 0 && distance > 0) {\n const middle = this.toLocalPoint({\n clientX: (first.x + second.x) / 2,\n clientY: (first.y + second.y) / 2,\n })\n this.set(zoomAtPoint(this.transform, distance / this.lastPinchDistance, middle, this.limits))\n }\n\n this.lastPinchDistance = distance\n }\n}\n","/**\n * Вставляет стили в документ один раз.\n *\n * Зачем: чтобы в проекте не приходилось писать лишний\n * `import '@bigplay/tree-view/style.css'` — всё работает сразу после установки.\n *\n * На сервере (SSR) функция ничего не делает: там нет `document`,\n * а на клиенте стили появятся при гидрации.\n */\nexport function injectStyles(css: string, id = 'tree-view-styles'): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(id)) return\n\n const style = document.createElement('style')\n style.id = id\n style.textContent = css\n\n document.head.appendChild(style)\n}\n"],"names":["DEFAULT_OPTIONS","resolveOptions","options","defaultGetChildren","item","defaultGetId","toTree","input","accessors","items","fromFlatList","fromNested","getId","getChildren","usedIds","build","depth","parentId","path","id","toId","assertUniqueId","children","child","index","nodes","byId","node","roots","rawParentId","parent","setDepths","queue","visited","rawId","fallback","fromLevels","levels","filled","level","lastLevel","levelIndex","resolveId","current","parents","perParent","raw","applyCollapsed","collapsedIds","walk","collapsed","toggleCollapsed","next","round","value","isVertical","direction","getAnchors","source","target","sourceCenterX","sourceCenterY","targetCenterX","targetCenterY","buildPath","from","to","style","x1","y1","x2","y2","vertical","midX","midY","buildLinks","links","getAxis","alongSize","acrossSize","assignOrders","orders","nextLeafOrder","maxDepth","place","order","childOrders","flatten","result","layoutTree","resolved","axis","maxOrder","alongExtent","acrossExtent","mirrored","along","across","alongPosition","DEFAULT_SCALE_LIMITS","IDENTITY_TRANSFORM","clamp","min","max","fitToViewport","content","viewport","padding","limits","availableWidth","availableHeight","scale","panBy","transform","dx","dy","zoomAtPoint","factor","point","contentX","contentY","toCssTransform","ZOOM_STEP","PanZoomController","params","event","previous","element","rect","center","first","second","distance","middle","injectStyles","css"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Публичный API пакета `@bigplay/tree-view-core`. */
|
|
2
|
+
export * from './types';
|
|
3
|
+
export { DEFAULT_OPTIONS, resolveOptions } from './defaults';
|
|
4
|
+
export { toTree, type Accessors } from './normalize';
|
|
5
|
+
export { fromLevels, type FromLevelsOptions } from './fromLevels';
|
|
6
|
+
export { applyCollapsed, toggleCollapsed } from './collapse';
|
|
7
|
+
export { layoutTree, assignOrders, flatten } from './layout';
|
|
8
|
+
export { buildLinks, buildPath, getAnchors } from './links';
|
|
9
|
+
export { PanZoomController, ZOOM_STEP, type PanZoomParams } from './panZoom';
|
|
10
|
+
export { injectStyles } from './injectStyles';
|
|
11
|
+
export { clamp, fitToViewport, panBy, zoomAtPoint, toCssTransform, IDENTITY_TRANSFORM, DEFAULT_SCALE_LIMITS, type ScaleLimits, } from './viewport';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
const I = {
|
|
2
|
+
nodeWidth: 180,
|
|
3
|
+
nodeHeight: 64,
|
|
4
|
+
levelGap: 60,
|
|
5
|
+
siblingGap: 16,
|
|
6
|
+
direction: "top-to-bottom",
|
|
7
|
+
linkStyle: "elbow"
|
|
8
|
+
};
|
|
9
|
+
function L(e) {
|
|
10
|
+
return { ...I, ...e };
|
|
11
|
+
}
|
|
12
|
+
function S(e) {
|
|
13
|
+
return e?.children;
|
|
14
|
+
}
|
|
15
|
+
function w(e) {
|
|
16
|
+
return e?.id;
|
|
17
|
+
}
|
|
18
|
+
function H(e, t = {}) {
|
|
19
|
+
if (e == null) return [];
|
|
20
|
+
const n = Array.isArray(e) ? e : [e];
|
|
21
|
+
return n.length === 0 ? [] : t.getParentId ? E(n, t) : b(n, t);
|
|
22
|
+
}
|
|
23
|
+
function b(e, t) {
|
|
24
|
+
const n = t.getId ?? w, i = t.getChildren ?? S, r = /* @__PURE__ */ new Set();
|
|
25
|
+
function o(s, h, l, d) {
|
|
26
|
+
const c = P(n(s), d);
|
|
27
|
+
v(c, r);
|
|
28
|
+
const p = (i(s) ?? []).map((f, m) => o(f, h + 1, c, `${d}.${m}`));
|
|
29
|
+
return { id: c, data: s, depth: h, parentId: l, children: p, hasChildren: p.length > 0, collapsed: !1 };
|
|
30
|
+
}
|
|
31
|
+
return e.map((s, h) => o(s, 0, null, String(h)));
|
|
32
|
+
}
|
|
33
|
+
function E(e, t) {
|
|
34
|
+
const n = t.getId ?? w, i = /* @__PURE__ */ new Set(), r = e.map((l, d) => {
|
|
35
|
+
const c = P(n(l), String(d));
|
|
36
|
+
return v(c, i), { id: c, data: l, depth: 0, parentId: null, children: [], hasChildren: !1, collapsed: !1 };
|
|
37
|
+
}), o = new Map(r.map((l) => [l.id, l])), s = [];
|
|
38
|
+
if (r.forEach((l, d) => {
|
|
39
|
+
const c = t.getParentId(e[d]), a = c == null ? void 0 : o.get(String(c));
|
|
40
|
+
if (!a) {
|
|
41
|
+
s.push(l);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
l.parentId = a.id, a.children.push(l), a.hasChildren = !0;
|
|
45
|
+
}), M(s) !== r.length)
|
|
46
|
+
throw new Error("[tree-view] В данных есть цикл: часть узлов ссылается друг на друга по кругу.");
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
function M(e) {
|
|
50
|
+
const t = [...e];
|
|
51
|
+
let n = 0;
|
|
52
|
+
for (; t.length > 0; ) {
|
|
53
|
+
const i = t.shift();
|
|
54
|
+
n += 1;
|
|
55
|
+
for (const r of i.children)
|
|
56
|
+
r.depth = i.depth + 1, t.push(r);
|
|
57
|
+
}
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
function P(e, t) {
|
|
61
|
+
return e == null || e === "" ? t : String(e);
|
|
62
|
+
}
|
|
63
|
+
function v(e, t) {
|
|
64
|
+
if (t.has(e))
|
|
65
|
+
throw new Error(`[tree-view] Повторяющийся id узла: "${e}". Идентификаторы должны быть уникальными.`);
|
|
66
|
+
t.add(e);
|
|
67
|
+
}
|
|
68
|
+
function G(e, t = {}) {
|
|
69
|
+
const n = e.filter((o) => o.length > 0);
|
|
70
|
+
if (n.length === 0) return [];
|
|
71
|
+
const i = n.length - 1, r = n.map(
|
|
72
|
+
(o, s) => o.map((h, l) => ({
|
|
73
|
+
id: D(h, s, l, t.getId),
|
|
74
|
+
data: h,
|
|
75
|
+
// Финал — корень, поэтому глубина считается от последнего уровня.
|
|
76
|
+
depth: i - s,
|
|
77
|
+
parentId: null,
|
|
78
|
+
children: [],
|
|
79
|
+
hasChildren: !1,
|
|
80
|
+
collapsed: !1
|
|
81
|
+
}))
|
|
82
|
+
);
|
|
83
|
+
for (let o = 0; o < i; o += 1) {
|
|
84
|
+
const s = r[o], h = r[o + 1], l = Math.ceil(s.length / h.length);
|
|
85
|
+
s.forEach((d, c) => {
|
|
86
|
+
const a = h[Math.min(Math.floor(c / l), h.length - 1)];
|
|
87
|
+
d.parentId = a.id, a.children.push(d), a.hasChildren = !0;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return r[i];
|
|
91
|
+
}
|
|
92
|
+
function D(e, t, n, i) {
|
|
93
|
+
const r = i ? i(e, t, n) : e?.id;
|
|
94
|
+
return r == null || r === "" ? `${t}.${n}` : String(r);
|
|
95
|
+
}
|
|
96
|
+
function Z(e, t) {
|
|
97
|
+
if (t.size === 0) return e;
|
|
98
|
+
function n(i) {
|
|
99
|
+
const r = t.has(i.id);
|
|
100
|
+
return {
|
|
101
|
+
...i,
|
|
102
|
+
collapsed: r,
|
|
103
|
+
children: r ? [] : i.children.map(n)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return e.map(n);
|
|
107
|
+
}
|
|
108
|
+
function _(e, t) {
|
|
109
|
+
const n = new Set(e);
|
|
110
|
+
return n.delete(t) || n.add(t), n;
|
|
111
|
+
}
|
|
112
|
+
function u(e) {
|
|
113
|
+
return Math.round(e * 100) / 100;
|
|
114
|
+
}
|
|
115
|
+
function z(e) {
|
|
116
|
+
return e === "top-to-bottom" || e === "bottom-to-top";
|
|
117
|
+
}
|
|
118
|
+
function T(e, t, n) {
|
|
119
|
+
const i = e.x + e.width / 2, r = e.y + e.height / 2, o = t.x + t.width / 2, s = t.y + t.height / 2;
|
|
120
|
+
switch (n) {
|
|
121
|
+
case "top-to-bottom":
|
|
122
|
+
return { from: { x: i, y: e.y + e.height }, to: { x: o, y: t.y } };
|
|
123
|
+
case "bottom-to-top":
|
|
124
|
+
return { from: { x: i, y: e.y }, to: { x: o, y: t.y + t.height } };
|
|
125
|
+
case "left-to-right":
|
|
126
|
+
return { from: { x: e.x + e.width, y: r }, to: { x: t.x, y: s } };
|
|
127
|
+
case "right-to-left":
|
|
128
|
+
return { from: { x: e.x, y: r }, to: { x: t.x + t.width, y: s } };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function O(e, t, n, i) {
|
|
132
|
+
const r = u(e.x), o = u(e.y), s = u(t.x), h = u(t.y);
|
|
133
|
+
if (n === "straight")
|
|
134
|
+
return `M ${r} ${o} L ${s} ${h}`;
|
|
135
|
+
const l = z(i), d = u((r + s) / 2), c = u((o + h) / 2);
|
|
136
|
+
return n === "elbow" ? l ? `M ${r} ${o} L ${r} ${c} L ${s} ${c} L ${s} ${h}` : `M ${r} ${o} L ${d} ${o} L ${d} ${h} L ${s} ${h}` : l ? `M ${r} ${o} C ${r} ${c} ${s} ${c} ${s} ${h}` : `M ${r} ${o} C ${d} ${o} ${d} ${h} ${s} ${h}`;
|
|
137
|
+
}
|
|
138
|
+
function Y(e, t) {
|
|
139
|
+
const n = new Map(e.map((r) => [r.id, r])), i = [];
|
|
140
|
+
for (const r of e) {
|
|
141
|
+
const o = r.parentId == null ? void 0 : n.get(r.parentId);
|
|
142
|
+
if (!o) continue;
|
|
143
|
+
const { from: s, to: h } = T(o, r, t.direction);
|
|
144
|
+
i.push({
|
|
145
|
+
id: `${o.id}->${r.id}`,
|
|
146
|
+
source: o,
|
|
147
|
+
target: r,
|
|
148
|
+
path: O(s, h, t.linkStyle, t.direction)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return i;
|
|
152
|
+
}
|
|
153
|
+
function W(e) {
|
|
154
|
+
const t = e.direction === "top-to-bottom" || e.direction === "bottom-to-top", n = t ? e.nodeHeight : e.nodeWidth, i = t ? e.nodeWidth : e.nodeHeight;
|
|
155
|
+
return {
|
|
156
|
+
vertical: t,
|
|
157
|
+
alongSize: n,
|
|
158
|
+
acrossSize: i,
|
|
159
|
+
alongStep: n + e.levelGap,
|
|
160
|
+
acrossStep: i + e.siblingGap
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function A(e) {
|
|
164
|
+
const t = /* @__PURE__ */ new Map();
|
|
165
|
+
let n = 0, i = 0;
|
|
166
|
+
function r(o) {
|
|
167
|
+
if (o.depth > i && (i = o.depth), o.children.length === 0) {
|
|
168
|
+
const l = n;
|
|
169
|
+
return n += 1, t.set(o.id, l), l;
|
|
170
|
+
}
|
|
171
|
+
const s = o.children.map(r), h = (s[0] + s[s.length - 1]) / 2;
|
|
172
|
+
return t.set(o.id, h), h;
|
|
173
|
+
}
|
|
174
|
+
return e.forEach(r), { orders: t, maxOrder: Math.max(0, n - 1), maxDepth: i };
|
|
175
|
+
}
|
|
176
|
+
function X(e) {
|
|
177
|
+
const t = [];
|
|
178
|
+
function n(i) {
|
|
179
|
+
t.push(i), i.children.forEach(n);
|
|
180
|
+
}
|
|
181
|
+
return e.forEach(n), t;
|
|
182
|
+
}
|
|
183
|
+
function F(e, t) {
|
|
184
|
+
const n = L(t), i = W(n);
|
|
185
|
+
if (e.length === 0)
|
|
186
|
+
return { nodes: [], links: [], width: 0, height: 0 };
|
|
187
|
+
const { orders: r, maxOrder: o, maxDepth: s } = A(e), h = s * i.alongStep + i.alongSize, l = o * i.acrossStep + i.acrossSize, d = n.direction === "bottom-to-top" || n.direction === "right-to-left", c = X(e).map((a) => {
|
|
188
|
+
const p = a.depth * i.alongStep, f = r.get(a.id) * i.acrossStep, m = d ? h - i.alongSize - p : p;
|
|
189
|
+
return {
|
|
190
|
+
id: a.id,
|
|
191
|
+
data: a.data,
|
|
192
|
+
depth: a.depth,
|
|
193
|
+
parentId: a.parentId,
|
|
194
|
+
x: i.vertical ? f : m,
|
|
195
|
+
y: i.vertical ? m : f,
|
|
196
|
+
width: n.nodeWidth,
|
|
197
|
+
height: n.nodeHeight,
|
|
198
|
+
hasChildren: a.hasChildren,
|
|
199
|
+
collapsed: a.collapsed
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
nodes: c,
|
|
204
|
+
links: Y(c, n),
|
|
205
|
+
width: i.vertical ? l : h,
|
|
206
|
+
height: i.vertical ? h : l
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const $ = { min: 0.2, max: 3 }, y = { x: 0, y: 0, scale: 1 };
|
|
210
|
+
function C(e, t, n) {
|
|
211
|
+
return Math.min(Math.max(e, t), n);
|
|
212
|
+
}
|
|
213
|
+
function k(e, t, n = 24, i = $) {
|
|
214
|
+
if (e.width <= 0 || e.height <= 0 || t.width <= 0 || t.height <= 0)
|
|
215
|
+
return { ...y };
|
|
216
|
+
const r = Math.max(t.width - n * 2, 1), o = Math.max(t.height - n * 2, 1), s = Math.min(r / e.width, o / e.height, 1), h = C(s, i.min, i.max);
|
|
217
|
+
return {
|
|
218
|
+
scale: h,
|
|
219
|
+
x: (t.width - e.width * h) / 2,
|
|
220
|
+
y: (t.height - e.height * h) / 2
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function U(e, t, n) {
|
|
224
|
+
return { ...e, x: e.x + t, y: e.y + n };
|
|
225
|
+
}
|
|
226
|
+
function x(e, t, n, i = $) {
|
|
227
|
+
const r = C(e.scale * t, i.min, i.max);
|
|
228
|
+
if (r === e.scale) return e;
|
|
229
|
+
const o = (n.x - e.x) / e.scale, s = (n.y - e.y) / e.scale;
|
|
230
|
+
return { scale: r, x: n.x - o * r, y: n.y - s * r };
|
|
231
|
+
}
|
|
232
|
+
function N(e) {
|
|
233
|
+
return `translate(${e.x}px, ${e.y}px) scale(${e.scale})`;
|
|
234
|
+
}
|
|
235
|
+
const g = 1.2;
|
|
236
|
+
class B {
|
|
237
|
+
constructor(t) {
|
|
238
|
+
this.params = t, this.transform = { ...y }, this.element = null, this.pointers = /* @__PURE__ */ new Map(), this.lastPinchDistance = 0, this.onPointerDown = (n) => {
|
|
239
|
+
this.isPannable() && (n.target?.closest?.("[data-tv-no-pan]") || (this.pointers.set(n.pointerId, { x: n.clientX, y: n.clientY }), this.element?.setPointerCapture?.(n.pointerId)));
|
|
240
|
+
}, this.onPointerMove = (n) => {
|
|
241
|
+
const i = this.pointers.get(n.pointerId);
|
|
242
|
+
if (i) {
|
|
243
|
+
if (this.pointers.set(n.pointerId, { x: n.clientX, y: n.clientY }), this.pointers.size === 2) {
|
|
244
|
+
this.pinch();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.set(U(this.transform, n.clientX - i.x, n.clientY - i.y));
|
|
248
|
+
}
|
|
249
|
+
}, this.onPointerUp = (n) => {
|
|
250
|
+
this.pointers.delete(n.pointerId), this.pointers.size < 2 && (this.lastPinchDistance = 0);
|
|
251
|
+
}, this.onWheel = (n) => {
|
|
252
|
+
if (!this.isZoomable()) return;
|
|
253
|
+
n.preventDefault();
|
|
254
|
+
const i = n.deltaY < 0 ? g : 1 / g;
|
|
255
|
+
this.set(x(this.transform, i, this.toLocalPoint(n), this.limits));
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** Подписывается на события элемента. Повторный вызов сначала отписывается. */
|
|
259
|
+
attach(t) {
|
|
260
|
+
this.detach(), this.element = t, t.addEventListener("pointerdown", this.onPointerDown), t.addEventListener("pointermove", this.onPointerMove), t.addEventListener("pointerup", this.onPointerUp), t.addEventListener("pointercancel", this.onPointerUp), t.addEventListener("wheel", this.onWheel, { passive: !1 });
|
|
261
|
+
}
|
|
262
|
+
/** Снимает все слушатели. Обязательно вызывать при размонтировании. */
|
|
263
|
+
detach() {
|
|
264
|
+
const t = this.element;
|
|
265
|
+
t && (t.removeEventListener("pointerdown", this.onPointerDown), t.removeEventListener("pointermove", this.onPointerMove), t.removeEventListener("pointerup", this.onPointerUp), t.removeEventListener("pointercancel", this.onPointerUp), t.removeEventListener("wheel", this.onWheel), this.element = null, this.pointers.clear(), this.lastPinchDistance = 0);
|
|
266
|
+
}
|
|
267
|
+
/** Вписывает содержимое в размер элемента и центрирует. */
|
|
268
|
+
fit(t, n = 24) {
|
|
269
|
+
const i = this.element;
|
|
270
|
+
i && this.set(
|
|
271
|
+
k(
|
|
272
|
+
t,
|
|
273
|
+
{ width: i.clientWidth, height: i.clientHeight },
|
|
274
|
+
n,
|
|
275
|
+
this.limits
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
zoomIn() {
|
|
280
|
+
this.zoomToCenter(g);
|
|
281
|
+
}
|
|
282
|
+
zoomOut() {
|
|
283
|
+
this.zoomToCenter(1 / g);
|
|
284
|
+
}
|
|
285
|
+
reset() {
|
|
286
|
+
this.set({ ...y });
|
|
287
|
+
}
|
|
288
|
+
/** Устанавливает состояние напрямую (например, восстановление после перезагрузки). */
|
|
289
|
+
set(t) {
|
|
290
|
+
this.transform = { ...t }, this.params.onChange(this.transform);
|
|
291
|
+
}
|
|
292
|
+
get limits() {
|
|
293
|
+
return this.params.limits?.() ?? $;
|
|
294
|
+
}
|
|
295
|
+
isPannable() {
|
|
296
|
+
return this.params.pannable?.() ?? !0;
|
|
297
|
+
}
|
|
298
|
+
isZoomable() {
|
|
299
|
+
return this.params.zoomable?.() ?? !0;
|
|
300
|
+
}
|
|
301
|
+
/** Переводит координаты экрана в координаты внутри элемента. */
|
|
302
|
+
toLocalPoint(t) {
|
|
303
|
+
const n = this.element?.getBoundingClientRect();
|
|
304
|
+
return { x: t.clientX - (n?.left ?? 0), y: t.clientY - (n?.top ?? 0) };
|
|
305
|
+
}
|
|
306
|
+
zoomToCenter(t) {
|
|
307
|
+
const n = this.element, i = { x: (n?.clientWidth ?? 0) / 2, y: (n?.clientHeight ?? 0) / 2 };
|
|
308
|
+
this.set(x(this.transform, t, i, this.limits));
|
|
309
|
+
}
|
|
310
|
+
/** Два пальца: масштабируем вокруг точки ровно между ними. */
|
|
311
|
+
pinch() {
|
|
312
|
+
if (!this.isZoomable()) return;
|
|
313
|
+
const [t, n] = [...this.pointers.values()];
|
|
314
|
+
if (!t || !n) return;
|
|
315
|
+
const i = Math.hypot(t.x - n.x, t.y - n.y);
|
|
316
|
+
if (this.lastPinchDistance > 0 && i > 0) {
|
|
317
|
+
const r = this.toLocalPoint({
|
|
318
|
+
clientX: (t.x + n.x) / 2,
|
|
319
|
+
clientY: (t.y + n.y) / 2
|
|
320
|
+
});
|
|
321
|
+
this.set(x(this.transform, i / this.lastPinchDistance, r, this.limits));
|
|
322
|
+
}
|
|
323
|
+
this.lastPinchDistance = i;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function R(e, t = "tree-view-styles") {
|
|
327
|
+
if (typeof document > "u" || document.getElementById(t)) return;
|
|
328
|
+
const n = document.createElement("style");
|
|
329
|
+
n.id = t, n.textContent = e, document.head.appendChild(n);
|
|
330
|
+
}
|
|
331
|
+
export {
|
|
332
|
+
I as DEFAULT_OPTIONS,
|
|
333
|
+
$ as DEFAULT_SCALE_LIMITS,
|
|
334
|
+
y as IDENTITY_TRANSFORM,
|
|
335
|
+
B as PanZoomController,
|
|
336
|
+
g as ZOOM_STEP,
|
|
337
|
+
Z as applyCollapsed,
|
|
338
|
+
A as assignOrders,
|
|
339
|
+
Y as buildLinks,
|
|
340
|
+
O as buildPath,
|
|
341
|
+
C as clamp,
|
|
342
|
+
k as fitToViewport,
|
|
343
|
+
X as flatten,
|
|
344
|
+
G as fromLevels,
|
|
345
|
+
T as getAnchors,
|
|
346
|
+
R as injectStyles,
|
|
347
|
+
F as layoutTree,
|
|
348
|
+
U as panBy,
|
|
349
|
+
L as resolveOptions,
|
|
350
|
+
N as toCssTransform,
|
|
351
|
+
H as toTree,
|
|
352
|
+
_ as toggleCollapsed,
|
|
353
|
+
x as zoomAtPoint
|
|
354
|
+
};
|
|
355
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/defaults.ts","../src/normalize.ts","../src/fromLevels.ts","../src/collapse.ts","../src/links.ts","../src/layout.ts","../src/viewport.ts","../src/panZoom.ts","../src/injectStyles.ts"],"sourcesContent":["import type { TreeViewOptions } from './types'\n\n/** Значения по умолчанию. Меняются пропом `options` — частично, по одному полю. */\nexport const DEFAULT_OPTIONS: TreeViewOptions = {\n nodeWidth: 180,\n nodeHeight: 64,\n levelGap: 60,\n siblingGap: 16,\n direction: 'top-to-bottom',\n linkStyle: 'elbow',\n}\n\n/**\n * Дополняет пользовательские настройки значениями по умолчанию.\n * Благодаря этому можно передать только `{ nodeWidth: 240 }` и ничего не сломать.\n */\nexport function resolveOptions(options?: Partial<TreeViewOptions>): TreeViewOptions {\n return { ...DEFAULT_OPTIONS, ...options }\n}\n","import type { TreeNode } from './types'\n\n/**\n * Как читать чужие данные.\n * Любую из функций можно не передавать — тогда работает поведение по умолчанию.\n */\nexport interface Accessors<T> {\n /** Идентификатор узла. По умолчанию — поле `id`, а если его нет — путь вида `\"0.1.2\"`. */\n getId?: (item: T) => string | number | null | undefined\n /** Дети узла. По умолчанию — поле `children`. */\n getChildren?: (item: T) => T[] | null | undefined\n /**\n * Идентификатор родителя. Если передан — данные считаются\n * ПЛОСКИМ списком (`[{ id, parentId }, ...]`), а не вложенным деревом.\n */\n getParentId?: (item: T) => string | number | null | undefined\n}\n\n/** Читает `item.children`, если пользователь не задал свой аксессор. */\nfunction defaultGetChildren<T>(item: T): T[] | null | undefined {\n return (item as { children?: T[] })?.children\n}\n\n/** Читает `item.id`, если пользователь не задал свой аксессор. */\nfunction defaultGetId<T>(item: T): string | number | null | undefined {\n return (item as { id?: string | number })?.id\n}\n\n/**\n * Превращает входные данные в массив корней дерева.\n *\n * Поддерживает два формата:\n * 1. Вложенный: `{ id: 1, children: [...] }` или массив таких объектов;\n * 2. Плоский: `[{ id: 2, parentId: 1 }, ...]` — включается передачей `getParentId`.\n */\nexport function toTree<T>(input: T | T[] | null | undefined, accessors: Accessors<T> = {}): TreeNode<T>[] {\n if (input == null) return []\n\n const items = Array.isArray(input) ? input : [input]\n if (items.length === 0) return []\n\n return accessors.getParentId\n ? fromFlatList(items, accessors as Accessors<T> & { getParentId: NonNullable<Accessors<T>['getParentId']> })\n : fromNested(items, accessors)\n}\n\n/** Вложенный формат: рекурсивно обходим `children`. */\nfunction fromNested<T>(items: T[], accessors: Accessors<T>): TreeNode<T>[] {\n const getId = accessors.getId ?? defaultGetId\n const getChildren = accessors.getChildren ?? defaultGetChildren\n const usedIds = new Set<string>()\n\n /** `path` — запасной идентификатор: он стабилен, пока не меняется порядок данных. */\n function build(item: T, depth: number, parentId: string | null, path: string): TreeNode<T> {\n const id = toId(getId(item), path)\n assertUniqueId(id, usedIds)\n\n const rawChildren = getChildren(item) ?? []\n const children = rawChildren.map((child, index) => build(child, depth + 1, id, `${path}.${index}`))\n\n return { id, data: item, depth, parentId, children, hasChildren: children.length > 0, collapsed: false }\n }\n\n return items.map((item, index) => build(item, 0, null, String(index)))\n}\n\n/** Плоский формат: собираем детей по `parentId`. */\nfunction fromFlatList<T>(\n items: T[],\n accessors: Accessors<T> & { getParentId: NonNullable<Accessors<T>['getParentId']> },\n): TreeNode<T>[] {\n const getId = accessors.getId ?? defaultGetId\n const usedIds = new Set<string>()\n\n // Шаг 1: заготовки узлов без связей.\n const nodes = items.map((item, index) => {\n const id = toId(getId(item), String(index))\n assertUniqueId(id, usedIds)\n return { id, data: item, depth: 0, parentId: null, children: [], hasChildren: false, collapsed: false } as TreeNode<T>\n })\n\n const byId = new Map(nodes.map((node) => [node.id, node]))\n const roots: TreeNode<T>[] = []\n\n // Шаг 2: каждого ребёнка кладём в массив его родителя.\n nodes.forEach((node, index) => {\n const rawParentId = accessors.getParentId(items[index]!)\n const parent = rawParentId == null ? undefined : byId.get(String(rawParentId))\n\n if (!parent) {\n roots.push(node)\n return\n }\n node.parentId = parent.id\n parent.children.push(node)\n parent.hasChildren = true\n })\n\n // Шаг 3: считаем глубину сверху вниз. Заодно ловим циклы (A → B → A).\n const visited = setDepths(roots)\n if (visited !== nodes.length) {\n throw new Error('[tree-view] В данных есть цикл: часть узлов ссылается друг на друга по кругу.')\n }\n\n return roots\n}\n\n/** Проставляет `depth` обходом в ширину и возвращает число посещённых узлов. */\nfunction setDepths<T>(roots: TreeNode<T>[]): number {\n const queue = [...roots]\n let visited = 0\n\n while (queue.length > 0) {\n const node = queue.shift()!\n visited += 1\n for (const child of node.children) {\n child.depth = node.depth + 1\n queue.push(child)\n }\n }\n return visited\n}\n\n/** Приводит идентификатор к строке, подставляя путь, если его нет. */\nfunction toId(rawId: string | number | null | undefined, fallback: string): string {\n return rawId == null || rawId === '' ? fallback : String(rawId)\n}\n\n/** Одинаковые id ломают отрисовку молча — лучше сразу сказать об этом вслух. */\nfunction assertUniqueId(id: string, usedIds: Set<string>): void {\n if (usedIds.has(id)) {\n throw new Error(`[tree-view] Повторяющийся id узла: \"${id}\". Идентификаторы должны быть уникальными.`)\n }\n usedIds.add(id)\n}\n","import type { TreeNode } from './types'\n\nexport interface FromLevelsOptions<T> {\n /** Идентификатор узла. По умолчанию — поле `id`, иначе «номер уровня.номер в уровне». */\n getId?: (item: T, level: number, index: number) => string | number | null | undefined\n}\n\n/**\n * Собирает дерево из «уровней», когда в данных нет ссылок на родителя,\n * а связь задана только позицией — так устроены bounty-сетки: пары групп\n * одного раунда сходятся в одну группу следующего.\n *\n * Уровни передаются от самого раннего раунда к финалу:\n *\n * ```ts\n * fromLevels([\n * [g1, g2, g3, g4], // первый раунд\n * [g5, g6], // второй: g1+g2 → g5, g3+g4 → g6\n * [g7], // финал: g5+g6 → g7\n * ])\n * ```\n *\n * Если уровни делятся неровно (например 5 групп на 2), лишние узлы\n * достаются последнему родителю — так же, как это выглядит в вёрстке.\n */\nexport function fromLevels<T>(levels: T[][], options: FromLevelsOptions<T> = {}): TreeNode<T>[] {\n const filled = levels.filter((level) => level.length > 0)\n if (filled.length === 0) return []\n\n const lastLevel = filled.length - 1\n\n // Сначала создаём узлы всех уровней, связи проставим следующим шагом.\n const nodes: TreeNode<T>[][] = filled.map((level, levelIndex) =>\n level.map((item, index) => ({\n id: resolveId(item, levelIndex, index, options.getId),\n data: item,\n // Финал — корень, поэтому глубина считается от последнего уровня.\n depth: lastLevel - levelIndex,\n parentId: null,\n children: [],\n hasChildren: false,\n collapsed: false,\n })),\n )\n\n for (let levelIndex = 0; levelIndex < lastLevel; levelIndex += 1) {\n const current = nodes[levelIndex]!\n const parents = nodes[levelIndex + 1]!\n // Сколько узлов этого уровня приходится на одного родителя.\n const perParent = Math.ceil(current.length / parents.length)\n\n current.forEach((node, index) => {\n const parent = parents[Math.min(Math.floor(index / perParent), parents.length - 1)]!\n node.parentId = parent.id\n parent.children.push(node)\n parent.hasChildren = true\n })\n }\n\n return nodes[lastLevel]!\n}\n\nfunction resolveId<T>(\n item: T,\n level: number,\n index: number,\n getId: FromLevelsOptions<T>['getId'],\n): string {\n const raw = getId ? getId(item, level, index) : (item as { id?: string | number })?.id\n return raw == null || raw === '' ? `${level}.${index}` : String(raw)\n}\n","import type { TreeNode } from './types'\n\n/**\n * Возвращает КОПИЮ дерева, где у свёрнутых узлов дети убраны из раскладки.\n *\n * Важно: исходные данные не изменяются. Старая версия библиотеки писала\n * `node.children = null` прямо в объект пользователя — из-за этого ломался\n * реактивный пересчёт и «терялись» ветки. Здесь такого нет.\n */\nexport function applyCollapsed<T>(roots: TreeNode<T>[], collapsedIds: ReadonlySet<string>): TreeNode<T>[] {\n if (collapsedIds.size === 0) return roots\n\n function walk(node: TreeNode<T>): TreeNode<T> {\n const collapsed = collapsedIds.has(node.id)\n return {\n ...node,\n collapsed,\n children: collapsed ? [] : node.children.map(walk),\n }\n }\n\n return roots.map(walk)\n}\n\n/** Возвращает новое множество свёрнутых узлов: был свёрнут — развернём, и наоборот. */\nexport function toggleCollapsed(collapsedIds: ReadonlySet<string>, id: string): Set<string> {\n const next = new Set(collapsedIds)\n if (!next.delete(id)) next.add(id)\n return next\n}\n","import type { Direction, LayoutLink, LayoutNode, LinkStyle, Point, TreeViewOptions } from './types'\n\n/** Округление до сотых: убирает «хвосты» вроде 12.000000001 в атрибуте `d`. */\nfunction round(value: number): number {\n return Math.round(value * 100) / 100\n}\n\n/** Вертикальные направления рисуются сверху вниз, горизонтальные — слева направо. */\nfunction isVertical(direction: Direction): boolean {\n return direction === 'top-to-bottom' || direction === 'bottom-to-top'\n}\n\n/**\n * Точки, из которой линия выходит, и в которую приходит.\n * Всегда середина стороны карточки, обращённой к собеседнику.\n */\nexport function getAnchors<T>(\n source: LayoutNode<T>,\n target: LayoutNode<T>,\n direction: Direction,\n): { from: Point; to: Point } {\n const sourceCenterX = source.x + source.width / 2\n const sourceCenterY = source.y + source.height / 2\n const targetCenterX = target.x + target.width / 2\n const targetCenterY = target.y + target.height / 2\n\n switch (direction) {\n case 'top-to-bottom':\n return { from: { x: sourceCenterX, y: source.y + source.height }, to: { x: targetCenterX, y: target.y } }\n case 'bottom-to-top':\n return { from: { x: sourceCenterX, y: source.y }, to: { x: targetCenterX, y: target.y + target.height } }\n case 'left-to-right':\n return { from: { x: source.x + source.width, y: sourceCenterY }, to: { x: target.x, y: targetCenterY } }\n case 'right-to-left':\n return { from: { x: source.x, y: sourceCenterY }, to: { x: target.x + target.width, y: targetCenterY } }\n }\n}\n\n/** Строит атрибут `d` для `<path>`: прямая, «ступенька» или кривая Безье. */\nexport function buildPath(from: Point, to: Point, style: LinkStyle, direction: Direction): string {\n const x1 = round(from.x)\n const y1 = round(from.y)\n const x2 = round(to.x)\n const y2 = round(to.y)\n\n if (style === 'straight') {\n return `M ${x1} ${y1} L ${x2} ${y2}`\n }\n\n const vertical = isVertical(direction)\n // Середина между узлами — по той оси, вдоль которой растёт дерево.\n const midX = round((x1 + x2) / 2)\n const midY = round((y1 + y2) / 2)\n\n if (style === 'elbow') {\n return vertical\n ? `M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`\n : `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`\n }\n\n return vertical\n ? `M ${x1} ${y1} C ${x1} ${midY} ${x2} ${midY} ${x2} ${y2}`\n : `M ${x1} ${y1} C ${midX} ${y1} ${midX} ${y2} ${x2} ${y2}`\n}\n\n/** Для каждого узла с родителем создаёт линию «родитель → ребёнок». */\nexport function buildLinks<T>(nodes: LayoutNode<T>[], options: TreeViewOptions): LayoutLink<T>[] {\n const byId = new Map(nodes.map((node) => [node.id, node]))\n const links: LayoutLink<T>[] = []\n\n for (const node of nodes) {\n const parent = node.parentId == null ? undefined : byId.get(node.parentId)\n if (!parent) continue\n\n const { from, to } = getAnchors(parent, node, options.direction)\n links.push({\n id: `${parent.id}->${node.id}`,\n source: parent,\n target: node,\n path: buildPath(from, to, options.linkStyle, options.direction),\n })\n }\n\n return links\n}\n","import { resolveOptions } from './defaults'\nimport { buildLinks } from './links'\nimport type { Layout, LayoutNode, TreeNode, TreeViewOptions } from './types'\n\n/**\n * Раскладка работает в двух «своих» осях, чтобы не писать четыре варианта кода:\n * - along — ось роста дерева (глубина): уровень 0, уровень 1, ...\n * - across — ось поперёк уровня: соседи стоят здесь друг за другом.\n * В самом конце пара (along, across) превращается в привычные (x, y).\n */\ninterface Axis {\n vertical: boolean\n /** Размер карточки вдоль оси роста. */\n alongSize: number\n /** Размер карточки поперёк оси роста. */\n acrossSize: number\n /** Шаг между уровнями (размер + зазор). */\n alongStep: number\n /** Шаг между соседями (размер + зазор). */\n acrossStep: number\n}\n\nfunction getAxis(options: TreeViewOptions): Axis {\n const vertical = options.direction === 'top-to-bottom' || options.direction === 'bottom-to-top'\n const alongSize = vertical ? options.nodeHeight : options.nodeWidth\n const acrossSize = vertical ? options.nodeWidth : options.nodeHeight\n\n return {\n vertical,\n alongSize,\n acrossSize,\n alongStep: alongSize + options.levelGap,\n acrossStep: acrossSize + options.siblingGap,\n }\n}\n\n/**\n * Считает позицию каждого узла поперёк уровня («номер колонки»).\n *\n * Правило всего одно и его легко проверить руками:\n * - лист встаёт в следующую свободную колонку (0, 1, 2, ...);\n * - родитель встаёт ровно посередине между первым и последним ребёнком.\n *\n * Поэтому соседние ветки никогда не накладываются друг на друга.\n */\nexport function assignOrders<T>(roots: TreeNode<T>[]): {\n orders: Map<string, number>\n maxOrder: number\n maxDepth: number\n} {\n const orders = new Map<string, number>()\n let nextLeafOrder = 0\n let maxDepth = 0\n\n function place(node: TreeNode<T>): number {\n if (node.depth > maxDepth) maxDepth = node.depth\n\n if (node.children.length === 0) {\n const order = nextLeafOrder\n nextLeafOrder += 1\n orders.set(node.id, order)\n return order\n }\n\n const childOrders = node.children.map(place)\n const order = (childOrders[0]! + childOrders[childOrders.length - 1]!) / 2\n orders.set(node.id, order)\n return order\n }\n\n roots.forEach(place)\n\n return { orders, maxOrder: Math.max(0, nextLeafOrder - 1), maxDepth }\n}\n\n/** Разворачивает дерево в плоский массив (порядок — «сверху вниз», как в данных). */\nexport function flatten<T>(roots: TreeNode<T>[]): TreeNode<T>[] {\n const result: TreeNode<T>[] = []\n\n function walk(node: TreeNode<T>): void {\n result.push(node)\n node.children.forEach(walk)\n }\n\n roots.forEach(walk)\n return result\n}\n\n/**\n * Главная функция раскладки: из дерева делает координаты и линии.\n * Чистая — одни и те же данные всегда дают один и тот же результат.\n */\nexport function layoutTree<T>(roots: TreeNode<T>[], options?: Partial<TreeViewOptions>): Layout<T> {\n const resolved = resolveOptions(options)\n const axis = getAxis(resolved)\n\n if (roots.length === 0) {\n return { nodes: [], links: [], width: 0, height: 0 }\n }\n\n const { orders, maxOrder, maxDepth } = assignOrders(roots)\n\n // Габариты содержимого: последний уровень + последняя колонка.\n const alongExtent = maxDepth * axis.alongStep + axis.alongSize\n const acrossExtent = maxOrder * axis.acrossStep + axis.acrossSize\n // У «перевёрнутых» направлений уровень 0 должен оказаться в конце холста.\n const mirrored = resolved.direction === 'bottom-to-top' || resolved.direction === 'right-to-left'\n\n const nodes: LayoutNode<T>[] = flatten(roots).map((node) => {\n const along = node.depth * axis.alongStep\n const across = orders.get(node.id)! * axis.acrossStep\n const alongPosition = mirrored ? alongExtent - axis.alongSize - along : along\n\n return {\n id: node.id,\n data: node.data,\n depth: node.depth,\n parentId: node.parentId,\n x: axis.vertical ? across : alongPosition,\n y: axis.vertical ? alongPosition : across,\n width: resolved.nodeWidth,\n height: resolved.nodeHeight,\n hasChildren: node.hasChildren,\n collapsed: node.collapsed,\n }\n })\n\n return {\n nodes,\n links: buildLinks(nodes, resolved),\n width: axis.vertical ? acrossExtent : alongExtent,\n height: axis.vertical ? alongExtent : acrossExtent,\n }\n}\n","import type { Point, Size, Transform } from './types'\n\n/** Ограничения зума. */\nexport interface ScaleLimits {\n min: number\n max: number\n}\n\nexport const DEFAULT_SCALE_LIMITS: ScaleLimits = { min: 0.2, max: 3 }\n\n/** Начальное состояние холста: ничего не сдвинуто, масштаб 1. */\nexport const IDENTITY_TRANSFORM: Transform = { x: 0, y: 0, scale: 1 }\n\n/** Зажимает число в диапазоне [min, max]. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Подбирает масштаб и сдвиг так, чтобы всё дерево влезло в видимую область\n * и оказалось по центру. `padding` — отступ от краёв в пикселях.\n */\nexport function fitToViewport(\n content: Size,\n viewport: Size,\n padding = 24,\n limits: ScaleLimits = DEFAULT_SCALE_LIMITS,\n): Transform {\n if (content.width <= 0 || content.height <= 0 || viewport.width <= 0 || viewport.height <= 0) {\n return { ...IDENTITY_TRANSFORM }\n }\n\n const availableWidth = Math.max(viewport.width - padding * 2, 1)\n const availableHeight = Math.max(viewport.height - padding * 2, 1)\n // Берём меньший из двух коэффициентов — иначе дерево вылезет за край.\n const raw = Math.min(availableWidth / content.width, availableHeight / content.height, 1)\n const scale = clamp(raw, limits.min, limits.max)\n\n return {\n scale,\n x: (viewport.width - content.width * scale) / 2,\n y: (viewport.height - content.height * scale) / 2,\n }\n}\n\n/** Сдвигает холст на `dx`/`dy` пикселей экрана. */\nexport function panBy(transform: Transform, dx: number, dy: number): Transform {\n return { ...transform, x: transform.x + dx, y: transform.y + dy }\n}\n\n/**\n * Меняет масштаб так, чтобы точка `point` (координаты экрана) осталась\n * под курсором — это и есть «зум к курсору».\n */\nexport function zoomAtPoint(\n transform: Transform,\n factor: number,\n point: Point,\n limits: ScaleLimits = DEFAULT_SCALE_LIMITS,\n): Transform {\n const scale = clamp(transform.scale * factor, limits.min, limits.max)\n if (scale === transform.scale) return transform\n\n // Точка в координатах содержимого — она не должна поменяться.\n const contentX = (point.x - transform.x) / transform.scale\n const contentY = (point.y - transform.y) / transform.scale\n\n return { scale, x: point.x - contentX * scale, y: point.y - contentY * scale }\n}\n\n/** Готовая CSS-строка для `transform` холста. */\nexport function toCssTransform(transform: Transform): string {\n return `translate(${transform.x}px, ${transform.y}px) scale(${transform.scale})`\n}\n","import {\n DEFAULT_SCALE_LIMITS,\n IDENTITY_TRANSFORM,\n fitToViewport,\n panBy,\n zoomAtPoint,\n type ScaleLimits,\n} from './viewport'\nimport type { Size, Transform } from './types'\n\n/** Во сколько раз меняется масштаб за одно движение колеса или клик по кнопке. */\nexport const ZOOM_STEP = 1.2\n\nexport interface PanZoomParams {\n /** Можно ли двигать холст. По умолчанию да. */\n pannable?: () => boolean\n /** Можно ли менять масштаб. По умолчанию да. */\n zoomable?: () => boolean\n /** Пределы масштаба. */\n limits?: () => ScaleLimits\n /** Вызывается после каждого изменения — здесь фреймворк обновляет свой state. */\n onChange: (transform: Transform) => void\n}\n\n/**\n * Перетаскивание и зум без привязки к фреймворку: и Vue 2, и Vue 3, и что угодно\n * ещё просто создают контроллер и подписываются на `onChange`.\n *\n * Слушаем pointer-события — они одинаково работают мышью и пальцем,\n * поэтому отдельного кода для мобильных не нужно.\n */\nexport class PanZoomController {\n transform: Transform = { ...IDENTITY_TRANSFORM }\n\n private element: HTMLElement | null = null\n /** Активные касания: id указателя → последняя позиция на экране. */\n private readonly pointers = new Map<number, { x: number; y: number }>()\n /** Расстояние между двумя пальцами на прошлом событии — для «щипка». */\n private lastPinchDistance = 0\n\n constructor(private readonly params: PanZoomParams) {}\n\n /** Подписывается на события элемента. Повторный вызов сначала отписывается. */\n attach(element: HTMLElement): void {\n this.detach()\n this.element = element\n\n element.addEventListener('pointerdown', this.onPointerDown)\n element.addEventListener('pointermove', this.onPointerMove)\n element.addEventListener('pointerup', this.onPointerUp)\n element.addEventListener('pointercancel', this.onPointerUp)\n // passive: false — иначе браузер не даст отменить прокрутку страницы.\n element.addEventListener('wheel', this.onWheel, { passive: false })\n }\n\n /** Снимает все слушатели. Обязательно вызывать при размонтировании. */\n detach(): void {\n const element = this.element\n if (!element) return\n\n element.removeEventListener('pointerdown', this.onPointerDown)\n element.removeEventListener('pointermove', this.onPointerMove)\n element.removeEventListener('pointerup', this.onPointerUp)\n element.removeEventListener('pointercancel', this.onPointerUp)\n element.removeEventListener('wheel', this.onWheel)\n\n this.element = null\n this.pointers.clear()\n this.lastPinchDistance = 0\n }\n\n /** Вписывает содержимое в размер элемента и центрирует. */\n fit(content: Size, padding = 24): void {\n const element = this.element\n if (!element) return\n\n this.set(\n fitToViewport(\n content,\n { width: element.clientWidth, height: element.clientHeight },\n padding,\n this.limits,\n ),\n )\n }\n\n zoomIn(): void {\n this.zoomToCenter(ZOOM_STEP)\n }\n\n zoomOut(): void {\n this.zoomToCenter(1 / ZOOM_STEP)\n }\n\n reset(): void {\n this.set({ ...IDENTITY_TRANSFORM })\n }\n\n /** Устанавливает состояние напрямую (например, восстановление после перезагрузки). */\n set(transform: Transform): void {\n this.transform = { ...transform }\n this.params.onChange(this.transform)\n }\n\n private get limits(): ScaleLimits {\n return this.params.limits?.() ?? DEFAULT_SCALE_LIMITS\n }\n\n private isPannable(): boolean {\n return this.params.pannable?.() ?? true\n }\n\n private isZoomable(): boolean {\n return this.params.zoomable?.() ?? true\n }\n\n /** Переводит координаты экрана в координаты внутри элемента. */\n private toLocalPoint(point: { clientX: number; clientY: number }) {\n const rect = this.element?.getBoundingClientRect()\n return { x: point.clientX - (rect?.left ?? 0), y: point.clientY - (rect?.top ?? 0) }\n }\n\n private zoomToCenter(factor: number): void {\n const element = this.element\n const center = { x: (element?.clientWidth ?? 0) / 2, y: (element?.clientHeight ?? 0) / 2 }\n this.set(zoomAtPoint(this.transform, factor, center, this.limits))\n }\n\n // Стрелочные функции — чтобы `this` оставался контроллером и чтобы\n // removeEventListener получил ту же самую ссылку на обработчик.\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n if (!this.isPannable()) return\n // Элементу с атрибутом data-tv-no-pan разрешаем свои жесты (кнопки, инпуты).\n if ((event.target as HTMLElement | null)?.closest?.('[data-tv-no-pan]')) return\n\n this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n this.element?.setPointerCapture?.(event.pointerId)\n }\n\n private readonly onPointerMove = (event: PointerEvent): void => {\n const previous = this.pointers.get(event.pointerId)\n if (!previous) return\n\n this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n\n if (this.pointers.size === 2) {\n this.pinch()\n return\n }\n\n this.set(panBy(this.transform, event.clientX - previous.x, event.clientY - previous.y))\n }\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n this.pointers.delete(event.pointerId)\n if (this.pointers.size < 2) this.lastPinchDistance = 0\n }\n\n private readonly onWheel = (event: WheelEvent): void => {\n if (!this.isZoomable()) return\n event.preventDefault()\n\n const factor = event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP\n this.set(zoomAtPoint(this.transform, factor, this.toLocalPoint(event), this.limits))\n }\n\n /** Два пальца: масштабируем вокруг точки ровно между ними. */\n private pinch(): void {\n if (!this.isZoomable()) return\n\n const [first, second] = [...this.pointers.values()]\n if (!first || !second) return\n\n const distance = Math.hypot(first.x - second.x, first.y - second.y)\n\n if (this.lastPinchDistance > 0 && distance > 0) {\n const middle = this.toLocalPoint({\n clientX: (first.x + second.x) / 2,\n clientY: (first.y + second.y) / 2,\n })\n this.set(zoomAtPoint(this.transform, distance / this.lastPinchDistance, middle, this.limits))\n }\n\n this.lastPinchDistance = distance\n }\n}\n","/**\n * Вставляет стили в документ один раз.\n *\n * Зачем: чтобы в проекте не приходилось писать лишний\n * `import '@bigplay/tree-view/style.css'` — всё работает сразу после установки.\n *\n * На сервере (SSR) функция ничего не делает: там нет `document`,\n * а на клиенте стили появятся при гидрации.\n */\nexport function injectStyles(css: string, id = 'tree-view-styles'): void {\n if (typeof document === 'undefined') return\n if (document.getElementById(id)) return\n\n const style = document.createElement('style')\n style.id = id\n style.textContent = css\n\n document.head.appendChild(style)\n}\n"],"names":["DEFAULT_OPTIONS","resolveOptions","options","defaultGetChildren","item","defaultGetId","toTree","input","accessors","items","fromFlatList","fromNested","getId","getChildren","usedIds","build","depth","parentId","path","id","toId","assertUniqueId","children","child","index","nodes","byId","node","roots","rawParentId","parent","setDepths","queue","visited","rawId","fallback","fromLevels","levels","filled","level","lastLevel","levelIndex","resolveId","current","parents","perParent","raw","applyCollapsed","collapsedIds","walk","collapsed","toggleCollapsed","next","round","value","isVertical","direction","getAnchors","source","target","sourceCenterX","sourceCenterY","targetCenterX","targetCenterY","buildPath","from","to","style","x1","y1","x2","y2","vertical","midX","midY","buildLinks","links","getAxis","alongSize","acrossSize","assignOrders","orders","nextLeafOrder","maxDepth","place","order","childOrders","flatten","result","layoutTree","resolved","axis","maxOrder","alongExtent","acrossExtent","mirrored","along","across","alongPosition","DEFAULT_SCALE_LIMITS","IDENTITY_TRANSFORM","clamp","min","max","fitToViewport","content","viewport","padding","limits","availableWidth","availableHeight","scale","panBy","transform","dx","dy","zoomAtPoint","factor","point","contentX","contentY","toCssTransform","ZOOM_STEP","PanZoomController","params","event","previous","element","rect","center","first","second","distance","middle","injectStyles","css"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Вставляет стили в документ один раз.
|
|
3
|
+
*
|
|
4
|
+
* Зачем: чтобы в проекте не приходилось писать лишний
|
|
5
|
+
* `import '@bigplay/tree-view/style.css'` — всё работает сразу после установки.
|
|
6
|
+
*
|
|
7
|
+
* На сервере (SSR) функция ничего не делает: там нет `document`,
|
|
8
|
+
* а на клиенте стили появятся при гидрации.
|
|
9
|
+
*/
|
|
10
|
+
export declare function injectStyles(css: string, id?: string): void;
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Layout, TreeNode, TreeViewOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Считает позицию каждого узла поперёк уровня («номер колонки»).
|
|
4
|
+
*
|
|
5
|
+
* Правило всего одно и его легко проверить руками:
|
|
6
|
+
* - лист встаёт в следующую свободную колонку (0, 1, 2, ...);
|
|
7
|
+
* - родитель встаёт ровно посередине между первым и последним ребёнком.
|
|
8
|
+
*
|
|
9
|
+
* Поэтому соседние ветки никогда не накладываются друг на друга.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assignOrders<T>(roots: TreeNode<T>[]): {
|
|
12
|
+
orders: Map<string, number>;
|
|
13
|
+
maxOrder: number;
|
|
14
|
+
maxDepth: number;
|
|
15
|
+
};
|
|
16
|
+
/** Разворачивает дерево в плоский массив (порядок — «сверху вниз», как в данных). */
|
|
17
|
+
export declare function flatten<T>(roots: TreeNode<T>[]): TreeNode<T>[];
|
|
18
|
+
/**
|
|
19
|
+
* Главная функция раскладки: из дерева делает координаты и линии.
|
|
20
|
+
* Чистая — одни и те же данные всегда дают один и тот же результат.
|
|
21
|
+
*/
|
|
22
|
+
export declare function layoutTree<T>(roots: TreeNode<T>[], options?: Partial<TreeViewOptions>): Layout<T>;
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Direction, LayoutLink, LayoutNode, LinkStyle, Point, TreeViewOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Точки, из которой линия выходит, и в которую приходит.
|
|
4
|
+
* Всегда середина стороны карточки, обращённой к собеседнику.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getAnchors<T>(source: LayoutNode<T>, target: LayoutNode<T>, direction: Direction): {
|
|
7
|
+
from: Point;
|
|
8
|
+
to: Point;
|
|
9
|
+
};
|
|
10
|
+
/** Строит атрибут `d` для `<path>`: прямая, «ступенька» или кривая Безье. */
|
|
11
|
+
export declare function buildPath(from: Point, to: Point, style: LinkStyle, direction: Direction): string;
|
|
12
|
+
/** Для каждого узла с родителем создаёт линию «родитель → ребёнок». */
|
|
13
|
+
export declare function buildLinks<T>(nodes: LayoutNode<T>[], options: TreeViewOptions): LayoutLink<T>[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TreeNode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Как читать чужие данные.
|
|
4
|
+
* Любую из функций можно не передавать — тогда работает поведение по умолчанию.
|
|
5
|
+
*/
|
|
6
|
+
export interface Accessors<T> {
|
|
7
|
+
/** Идентификатор узла. По умолчанию — поле `id`, а если его нет — путь вида `"0.1.2"`. */
|
|
8
|
+
getId?: (item: T) => string | number | null | undefined;
|
|
9
|
+
/** Дети узла. По умолчанию — поле `children`. */
|
|
10
|
+
getChildren?: (item: T) => T[] | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Идентификатор родителя. Если передан — данные считаются
|
|
13
|
+
* ПЛОСКИМ списком (`[{ id, parentId }, ...]`), а не вложенным деревом.
|
|
14
|
+
*/
|
|
15
|
+
getParentId?: (item: T) => string | number | null | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Превращает входные данные в массив корней дерева.
|
|
19
|
+
*
|
|
20
|
+
* Поддерживает два формата:
|
|
21
|
+
* 1. Вложенный: `{ id: 1, children: [...] }` или массив таких объектов;
|
|
22
|
+
* 2. Плоский: `[{ id: 2, parentId: 1 }, ...]` — включается передачей `getParentId`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function toTree<T>(input: T | T[] | null | undefined, accessors?: Accessors<T>): TreeNode<T>[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ScaleLimits } from './viewport';
|
|
2
|
+
import { Size, Transform } from './types';
|
|
3
|
+
/** Во сколько раз меняется масштаб за одно движение колеса или клик по кнопке. */
|
|
4
|
+
export declare const ZOOM_STEP = 1.2;
|
|
5
|
+
export interface PanZoomParams {
|
|
6
|
+
/** Можно ли двигать холст. По умолчанию да. */
|
|
7
|
+
pannable?: () => boolean;
|
|
8
|
+
/** Можно ли менять масштаб. По умолчанию да. */
|
|
9
|
+
zoomable?: () => boolean;
|
|
10
|
+
/** Пределы масштаба. */
|
|
11
|
+
limits?: () => ScaleLimits;
|
|
12
|
+
/** Вызывается после каждого изменения — здесь фреймворк обновляет свой state. */
|
|
13
|
+
onChange: (transform: Transform) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Перетаскивание и зум без привязки к фреймворку: и Vue 2, и Vue 3, и что угодно
|
|
17
|
+
* ещё просто создают контроллер и подписываются на `onChange`.
|
|
18
|
+
*
|
|
19
|
+
* Слушаем pointer-события — они одинаково работают мышью и пальцем,
|
|
20
|
+
* поэтому отдельного кода для мобильных не нужно.
|
|
21
|
+
*/
|
|
22
|
+
export declare class PanZoomController {
|
|
23
|
+
private readonly params;
|
|
24
|
+
transform: Transform;
|
|
25
|
+
private element;
|
|
26
|
+
/** Активные касания: id указателя → последняя позиция на экране. */
|
|
27
|
+
private readonly pointers;
|
|
28
|
+
/** Расстояние между двумя пальцами на прошлом событии — для «щипка». */
|
|
29
|
+
private lastPinchDistance;
|
|
30
|
+
constructor(params: PanZoomParams);
|
|
31
|
+
/** Подписывается на события элемента. Повторный вызов сначала отписывается. */
|
|
32
|
+
attach(element: HTMLElement): void;
|
|
33
|
+
/** Снимает все слушатели. Обязательно вызывать при размонтировании. */
|
|
34
|
+
detach(): void;
|
|
35
|
+
/** Вписывает содержимое в размер элемента и центрирует. */
|
|
36
|
+
fit(content: Size, padding?: number): void;
|
|
37
|
+
zoomIn(): void;
|
|
38
|
+
zoomOut(): void;
|
|
39
|
+
reset(): void;
|
|
40
|
+
/** Устанавливает состояние напрямую (например, восстановление после перезагрузки). */
|
|
41
|
+
set(transform: Transform): void;
|
|
42
|
+
private get limits();
|
|
43
|
+
private isPannable;
|
|
44
|
+
private isZoomable;
|
|
45
|
+
/** Переводит координаты экрана в координаты внутри элемента. */
|
|
46
|
+
private toLocalPoint;
|
|
47
|
+
private zoomToCenter;
|
|
48
|
+
private readonly onPointerDown;
|
|
49
|
+
private readonly onPointerMove;
|
|
50
|
+
private readonly onPointerUp;
|
|
51
|
+
private readonly onWheel;
|
|
52
|
+
/** Два пальца: масштабируем вокруг точки ровно между ними. */
|
|
53
|
+
private pinch;
|
|
54
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Все типы библиотеки живут здесь, чтобы их было легко найти.
|
|
3
|
+
* Ни один файл в core не зависит от Vue — это чистый TypeScript.
|
|
4
|
+
*/
|
|
5
|
+
/** Куда растёт дерево: корень сверху / снизу / слева / справа. */
|
|
6
|
+
export type Direction = 'top-to-bottom' | 'bottom-to-top' | 'left-to-right' | 'right-to-left';
|
|
7
|
+
/** Форма соединительной линии между родителем и ребёнком. */
|
|
8
|
+
export type LinkStyle = 'straight' | 'elbow' | 'curve';
|
|
9
|
+
/** Настройки раскладки. Все размеры — в пикселях. */
|
|
10
|
+
export interface TreeViewOptions {
|
|
11
|
+
/** Ширина карточки узла. */
|
|
12
|
+
nodeWidth: number;
|
|
13
|
+
/** Высота карточки узла. */
|
|
14
|
+
nodeHeight: number;
|
|
15
|
+
/** Пустое место между уровнями (между родителем и детьми). */
|
|
16
|
+
levelGap: number;
|
|
17
|
+
/** Пустое место между соседними узлами одного уровня. */
|
|
18
|
+
siblingGap: number;
|
|
19
|
+
/** Направление роста дерева. */
|
|
20
|
+
direction: Direction;
|
|
21
|
+
/** Форма линий. */
|
|
22
|
+
linkStyle: LinkStyle;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Узел дерева после нормализации входных данных.
|
|
26
|
+
* Ссылки на родителя тут нет специально — только `parentId`,
|
|
27
|
+
* иначе объект становится циклическим и его нельзя ни сравнить, ни залогировать.
|
|
28
|
+
*/
|
|
29
|
+
export interface TreeNode<T> {
|
|
30
|
+
/** Стабильный идентификатор. Используется как ключ во `v-for`. */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Исходный объект пользователя — его и отдаём в слот. */
|
|
33
|
+
data: T;
|
|
34
|
+
/** 0 у корня, 1 у его детей и так далее. */
|
|
35
|
+
depth: number;
|
|
36
|
+
/** Идентификатор родителя или `null` у корня. */
|
|
37
|
+
parentId: string | null;
|
|
38
|
+
/** Дети, участвующие в раскладке (у свёрнутого узла — пустой массив). */
|
|
39
|
+
children: TreeNode<T>[];
|
|
40
|
+
/** Были ли дети в исходных данных (остаётся `true`, даже если узел свёрнут). */
|
|
41
|
+
hasChildren: boolean;
|
|
42
|
+
/** Свёрнут ли узел. */
|
|
43
|
+
collapsed: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** Узел с посчитанными координатами. `x`/`y` — левый верхний угол карточки. */
|
|
46
|
+
export interface LayoutNode<T> {
|
|
47
|
+
id: string;
|
|
48
|
+
data: T;
|
|
49
|
+
depth: number;
|
|
50
|
+
parentId: string | null;
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
hasChildren: boolean;
|
|
56
|
+
collapsed: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** Линия между двумя узлами вместе с готовым атрибутом `d` для `<path>`. */
|
|
59
|
+
export interface LayoutLink<T> {
|
|
60
|
+
/** Ключ для `v-for`: `"<id родителя>-><id ребёнка>"`. */
|
|
61
|
+
id: string;
|
|
62
|
+
source: LayoutNode<T>;
|
|
63
|
+
target: LayoutNode<T>;
|
|
64
|
+
/** Значение атрибута `d` SVG-пути. */
|
|
65
|
+
path: string;
|
|
66
|
+
}
|
|
67
|
+
/** Результат раскладки: что рисовать и какого размера получился холст. */
|
|
68
|
+
export interface Layout<T> {
|
|
69
|
+
nodes: LayoutNode<T>[];
|
|
70
|
+
links: LayoutLink<T>[];
|
|
71
|
+
/** Ширина содержимого в пикселях. */
|
|
72
|
+
width: number;
|
|
73
|
+
/** Высота содержимого в пикселях. */
|
|
74
|
+
height: number;
|
|
75
|
+
}
|
|
76
|
+
/** Точка на плоскости. */
|
|
77
|
+
export interface Point {
|
|
78
|
+
x: number;
|
|
79
|
+
y: number;
|
|
80
|
+
}
|
|
81
|
+
/** Размер прямоугольника. */
|
|
82
|
+
export interface Size {
|
|
83
|
+
width: number;
|
|
84
|
+
height: number;
|
|
85
|
+
}
|
|
86
|
+
/** Сдвиг и масштаб холста (панорамирование + зум). */
|
|
87
|
+
export interface Transform {
|
|
88
|
+
x: number;
|
|
89
|
+
y: number;
|
|
90
|
+
scale: number;
|
|
91
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Point, Size, Transform } from './types';
|
|
2
|
+
/** Ограничения зума. */
|
|
3
|
+
export interface ScaleLimits {
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_SCALE_LIMITS: ScaleLimits;
|
|
8
|
+
/** Начальное состояние холста: ничего не сдвинуто, масштаб 1. */
|
|
9
|
+
export declare const IDENTITY_TRANSFORM: Transform;
|
|
10
|
+
/** Зажимает число в диапазоне [min, max]. */
|
|
11
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Подбирает масштаб и сдвиг так, чтобы всё дерево влезло в видимую область
|
|
14
|
+
* и оказалось по центру. `padding` — отступ от краёв в пикселях.
|
|
15
|
+
*/
|
|
16
|
+
export declare function fitToViewport(content: Size, viewport: Size, padding?: number, limits?: ScaleLimits): Transform;
|
|
17
|
+
/** Сдвигает холст на `dx`/`dy` пикселей экрана. */
|
|
18
|
+
export declare function panBy(transform: Transform, dx: number, dy: number): Transform;
|
|
19
|
+
/**
|
|
20
|
+
* Меняет масштаб так, чтобы точка `point` (координаты экрана) осталась
|
|
21
|
+
* под курсором — это и есть «зум к курсору».
|
|
22
|
+
*/
|
|
23
|
+
export declare function zoomAtPoint(transform: Transform, factor: number, point: Point, limits?: ScaleLimits): Transform;
|
|
24
|
+
/** Готовая CSS-строка для `transform` холста. */
|
|
25
|
+
export declare function toCssTransform(transform: Transform): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bigplay/tree-view-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Раскладка дерева: чистые функции без привязки к фреймворку",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"styles.css"
|
|
13
|
+
],
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"./style.css": "./styles.css"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "vite build",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.19.0",
|
|
34
|
+
"happy-dom": "^15.11.7",
|
|
35
|
+
"typescript": "^5.8.3",
|
|
36
|
+
"vite": "^7.1.2",
|
|
37
|
+
"vite-plugin-dts": "^4.5.4",
|
|
38
|
+
"vitest": "^3.2.4"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/dadashi44/tree-view.git",
|
|
43
|
+
"directory": "packages/core"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/dadashi44/tree-view#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/dadashi44/tree-view/issues"
|
|
48
|
+
},
|
|
49
|
+
"author": "Daniyar Kudaibergenev",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/styles.css
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Стили намеренно простые и без !important — любой из них легко перебить.
|
|
3
|
+
* Настройка внешнего вида — через CSS-переменные ниже.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
.tree-view {
|
|
7
|
+
/* Цвета и размеры, которые можно переопределить у себя в проекте. */
|
|
8
|
+
--tv-link-color: #cbd5e1;
|
|
9
|
+
--tv-link-width: 2px;
|
|
10
|
+
--tv-node-bg: #ffffff;
|
|
11
|
+
--tv-node-color: #0f172a;
|
|
12
|
+
--tv-node-border: 1px solid #e2e8f0;
|
|
13
|
+
--tv-node-radius: 10px;
|
|
14
|
+
--tv-node-shadow: 0 1px 2px rgb(15 23 42 / 8%);
|
|
15
|
+
--tv-node-padding: 8px 12px;
|
|
16
|
+
--tv-accent: #2563eb;
|
|
17
|
+
|
|
18
|
+
position: relative;
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
/* Без этого мобильный браузер сам скроллит страницу вместо перетаскивания. */
|
|
23
|
+
touch-action: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tree-view--pannable {
|
|
27
|
+
cursor: grab;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* size="content": размер задаётся инлайном, ничего не обрезаем. */
|
|
31
|
+
.tree-view--content {
|
|
32
|
+
width: auto;
|
|
33
|
+
height: auto;
|
|
34
|
+
overflow: visible;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Холст с деревом: его двигает и масштабирует transform. */
|
|
38
|
+
.tree-view__canvas {
|
|
39
|
+
position: absolute;
|
|
40
|
+
top: 0;
|
|
41
|
+
left: 0;
|
|
42
|
+
transform-origin: 0 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.tree-view__links {
|
|
46
|
+
position: absolute;
|
|
47
|
+
top: 0;
|
|
48
|
+
left: 0;
|
|
49
|
+
overflow: visible;
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.tree-view__link {
|
|
54
|
+
fill: none;
|
|
55
|
+
stroke: var(--tv-link-color);
|
|
56
|
+
stroke-width: var(--tv-link-width);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.tree-view__node {
|
|
60
|
+
position: absolute;
|
|
61
|
+
box-sizing: border-box;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: stretch;
|
|
64
|
+
justify-content: stretch;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Карточка по умолчанию. Если используете слот #node — она не рисуется вовсе. */
|
|
68
|
+
.tree-view-card {
|
|
69
|
+
box-sizing: border-box;
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 8px;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: space-between;
|
|
74
|
+
width: 100%;
|
|
75
|
+
height: 100%;
|
|
76
|
+
padding: var(--tv-node-padding);
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
color: var(--tv-node-color);
|
|
79
|
+
background: var(--tv-node-bg);
|
|
80
|
+
border: var(--tv-node-border);
|
|
81
|
+
border-radius: var(--tv-node-radius);
|
|
82
|
+
box-shadow: var(--tv-node-shadow);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.tree-view-card__label {
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
text-overflow: ellipsis;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.tree-view-card__toggle {
|
|
92
|
+
flex: none;
|
|
93
|
+
width: 20px;
|
|
94
|
+
height: 20px;
|
|
95
|
+
padding: 0;
|
|
96
|
+
font: inherit;
|
|
97
|
+
line-height: 1;
|
|
98
|
+
color: var(--tv-accent);
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
background: none;
|
|
101
|
+
border: none;
|
|
102
|
+
}
|