Skip to content

Commit 9a78f90

Browse files
feat(client):appended svg pan initialisation and customised buttons
* feat(client): appended svg-pan-zoom lib * feat(client): appended svg pan initialisation and customised buttons
1 parent 8956a43 commit 9a78f90

File tree

9 files changed

+241
-111
lines changed

9 files changed

+241
-111
lines changed
Loading
+1
Loading
+1
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,111 @@
1-
<svg
2-
class="roadmap-container"
3-
width="100%"
4-
height="100%"
5-
xmlns:svg="http://www.w3.org/1999/html"
6-
>
7-
<svg:foreignObject
8-
style="transform: translateX(calc(50% - {{ layoutEl.clientWidth / 2 }}px))"
9-
height="0"
10-
width="0"
11-
overflow="visible"
1+
<div class="relative h-full w-full">
2+
<svg
3+
#roadmap
4+
height="100%"
5+
width="100%"
6+
xmlns:svg="http://www.w3.org/1999/html"
127
>
13-
<div
14-
#layoutEl
15-
class="relative flex h-fit w-fit translate-y-16 flex-col items-center gap-16"
8+
<svg:foreignObject
9+
overflow="visible"
10+
style="transform: translateX(calc(50% - {{
11+
layoutEl.clientWidth / 2
12+
}}px))"
1613
>
17-
@for (layer of roadmapLayers(); track layer.parentNode.id) {
18-
<div #layerEl class="flex w-full flex-col items-center gap-16">
19-
@if (!$last) {
20-
@let layerHeightWithGap = layerEl.clientHeight + 64 - 24 || 0;
21-
<svg
22-
[attr.height]="layerHeightWithGap"
23-
style="position: absolute"
24-
overflow="visible"
25-
width="100"
26-
xmlns="http://www.w3.org/2000/svg"
27-
>
28-
<defs>
29-
<marker
30-
id="arrowhead"
31-
markerWidth="6"
32-
markerHeight="8"
33-
refX="0"
34-
refY="4"
35-
orient="auto"
36-
>
37-
<polygon points="0 0, 6 4, 0 8" fill="#FDF5FD" />
38-
</marker>
39-
</defs>
40-
<line
41-
[attr.y2]="layerHeightWithGap"
42-
x1="50"
43-
y1="0"
44-
x2="50"
45-
stroke="white"
46-
stroke-width="4"
47-
marker-end="url(#arrowhead)"
48-
/>
49-
</svg>
50-
}
14+
<div
15+
#layoutEl
16+
class="relative flex h-fit w-fit translate-y-16 flex-col items-center gap-16"
17+
>
18+
@for (layer of roadmapLayers(); track layer.parentNode.id) {
19+
<div #layerEl class="flex w-full flex-col items-center gap-16">
20+
@if (!$last) {
21+
@let layerHeightWithGap = layerEl.clientHeight + 64 - 24 || 0;
22+
<svg
23+
[attr.height]="layerHeightWithGap"
24+
style="position: absolute"
25+
overflow="visible"
26+
width="100"
27+
xmlns="http://www.w3.org/2000/svg"
28+
>
29+
<defs>
30+
<marker
31+
id="arrowhead"
32+
markerWidth="6"
33+
markerHeight="8"
34+
refX="0"
35+
refY="4"
36+
orient="auto"
37+
>
38+
<polygon points="0 0, 6 4, 0 8" fill="#FDF5FD" />
39+
</marker>
40+
</defs>
41+
<line
42+
[attr.y2]="layerHeightWithGap"
43+
x1="50"
44+
y1="0"
45+
x2="50"
46+
stroke="white"
47+
stroke-width="4"
48+
marker-end="url(#arrowhead)"
49+
/>
50+
</svg>
51+
}
5152

52-
<!-- layer parent node-->
53-
@if (layer.parentNode.nodeType === 'primary') {
54-
<al-ui-roadmap-primary-node [node]="layer.parentNode" />
55-
} @else if (layer.parentNode.nodeType === 'angular-love') {
56-
<al-ui-roadmap-angular-love-node
57-
[node]="layer.parentNode"
58-
></al-ui-roadmap-angular-love-node>
59-
}
53+
<!-- layer parent node-->
54+
@if (layer.parentNode.nodeType === 'primary') {
55+
<al-ui-roadmap-primary-node [node]="layer.parentNode" />
56+
} @else if (layer.parentNode.nodeType === 'angular-love') {
57+
<al-ui-roadmap-angular-love-node
58+
[node]="layer.parentNode"
59+
></al-ui-roadmap-angular-love-node>
60+
}
6061

61-
<!-- layer child nodes-->
62-
@if (layer.childNodes?.length) {
63-
@let shift =
64-
(allChildNodesEl.clientWidth / 2 - leftChildNodesEl.clientWidth ||
65-
0) - 32;
66-
<div
67-
#allChildNodesEl
68-
class="flex gap-16"
69-
style="transform: translate({{ shift }}px)"
70-
>
71-
<div #leftChildNodesEl class="flex gap-12">
72-
@let centerShift = layerEl.clientWidth / 2 - shift;
73-
@for (node of layer.childNodes | leftSlice; track node.id) {
62+
<!-- layer child nodes-->
63+
@if (layer.childNodes?.length) {
64+
@let shift =
65+
(allChildNodesEl.clientWidth / 2 -
66+
leftChildNodesEl.clientWidth || 0) - 32;
67+
<div
68+
#allChildNodesEl
69+
class="flex gap-16"
70+
style="transform: translate({{ shift }}px)"
71+
>
72+
<div #leftChildNodesEl class="flex gap-12">
73+
@let centerShift = layerEl.clientWidth / 2 - shift;
74+
@for (node of layer.childNodes | leftSlice; track node.id) {
75+
@let arrowShift =
76+
secondaryNode.offsetLeft -
77+
centerShift +
78+
secondaryNode.clientWidth / 2;
79+
<svg
80+
[attr.height]="64"
81+
style="position: absolute; top: -64px; left: {{
82+
centerShift
83+
}}px"
84+
overflow="visible"
85+
width="1"
86+
xmlns="http://www.w3.org/2000/svg"
87+
>
88+
<svg:path
89+
[attr.d]="arrowShift | secondaryArrow: $first"
90+
fill="none"
91+
stroke="#FDF5FD"
92+
stroke-width="2"
93+
stroke-linecap="round"
94+
stroke-linejoin="round"
95+
/>
96+
</svg>
97+
<div #secondaryNode>
98+
@if (node.nodeType === 'secondary') {
99+
<al-ui-roadmap-secondary-node [node]="node" />
100+
} @else if (node.nodeType === 'cluster') {
101+
<al-ui-roadmap-cluster
102+
[cluster]="$any(node)"
103+
></al-ui-roadmap-cluster>
104+
}
105+
</div>
106+
}
107+
</div>
108+
@for (node of layer.childNodes | rightSlice; track node.id) {
74109
@let arrowShift =
75110
secondaryNode.offsetLeft -
76111
centerShift +
@@ -85,7 +120,7 @@
85120
xmlns="http://www.w3.org/2000/svg"
86121
>
87122
<svg:path
88-
[attr.d]="arrowShift | secondaryArrow: $first"
123+
[attr.d]="arrowShift | secondaryArrow: $last"
89124
fill="none"
90125
stroke="#FDF5FD"
91126
stroke-width="2"
@@ -104,43 +139,22 @@
104139
</div>
105140
}
106141
</div>
107-
@for (node of layer.childNodes | rightSlice; track node.id) {
108-
@let arrowShift =
109-
secondaryNode.offsetLeft -
110-
centerShift +
111-
secondaryNode.clientWidth / 2;
112-
<svg
113-
[attr.height]="64"
114-
style="position: absolute; top: -64px; left: {{
115-
centerShift
116-
}}px"
117-
overflow="visible"
118-
width="1"
119-
xmlns="http://www.w3.org/2000/svg"
120-
>
121-
<svg:path
122-
[attr.d]="arrowShift | secondaryArrow: $last"
123-
fill="none"
124-
stroke="#FDF5FD"
125-
stroke-width="2"
126-
stroke-linecap="round"
127-
stroke-linejoin="round"
128-
/>
129-
</svg>
130-
<div #secondaryNode>
131-
@if (node.nodeType === 'secondary') {
132-
<al-ui-roadmap-secondary-node [node]="node" />
133-
} @else if (node.nodeType === 'cluster') {
134-
<al-ui-roadmap-cluster
135-
[cluster]="$any(node)"
136-
></al-ui-roadmap-cluster>
137-
}
138-
</div>
139-
}
140-
</div>
141-
}
142-
</div>
143-
}
144-
</div>
145-
</svg:foreignObject>
146-
</svg>
142+
}
143+
</div>
144+
}
145+
</div>
146+
</svg:foreignObject>
147+
</svg>
148+
<div
149+
class="absolute bottom-4 right-4 flex h-fit w-fit flex-col items-center gap-8"
150+
>
151+
@for (control of controls; track $index) {
152+
<al-ui-roadmap-svg-control
153+
[size]="control.size"
154+
[event]="control.event"
155+
[iconName]="control.name"
156+
(resizeRoadmap)="resizeRoadmap($event)"
157+
/>
158+
}
159+
</div>
160+
</div>

libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts

+80-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1+
import { isPlatformBrowser } from '@angular/common';
12
import {
3+
AfterViewInit,
24
ChangeDetectionStrategy,
35
Component,
46
computed,
7+
ElementRef,
8+
inject,
9+
PLATFORM_ID,
510
signal,
11+
viewChild,
612
} from '@angular/core';
13+
import { FastSvgComponent } from '@push-based/ngx-fast-svg';
714

815
import { SecondaryArrowPipe } from './secondary-arrow.pipe';
916
import { LeftSlicePipe, RightSlicePipe } from './slice.pipes';
1017
import { UiRoadmapAngularLoveNodeComponent } from './ui/ui-roadmap-angular-love-node.component';
1118
import { UiRoadmapClusterComponent } from './ui/ui-roadmap-cluster.component';
1219
import { UiRoadmapPrimaryNodeComponent } from './ui/ui-roadmap-primary-node.component';
1320
import { UiRoadmapSecondaryNodeComponent } from './ui/ui-roadmap-secondary-node.component';
21+
import { UiRoadmapSvgControlComponent } from './ui/ui-roadmap-svg-control.component';
1422

1523
export type NodeType = 'primary' | 'secondary' | 'cluster' | 'angular-love';
1624

25+
export type EventType = 'increment' | 'decrement' | 'reset' | 'zoom-reset';
26+
27+
export interface Control {
28+
size: string;
29+
name: string;
30+
event: EventType;
31+
}
32+
1733
export interface RoadmapNodeDTO {
1834
id: string;
1935
previousNodeId?: string;
@@ -37,6 +53,14 @@ export interface RoadmapLayer {
3753
childNodes: RoadmapNode[];
3854
}
3955

56+
const svgPanZoomInitialConfig = {
57+
fit: false,
58+
center: false,
59+
minZoom: 0.5,
60+
maxZoom: 2.5,
61+
zoomScaleSensitivity: 0.1,
62+
};
63+
4064
@Component({
4165
selector: 'al-feature-roadmap',
4266
imports: [
@@ -46,13 +70,19 @@ export interface RoadmapLayer {
4670
UiRoadmapPrimaryNodeComponent,
4771
UiRoadmapAngularLoveNodeComponent,
4872
UiRoadmapSecondaryNodeComponent,
73+
UiRoadmapSvgControlComponent,
4974
SecondaryArrowPipe,
75+
FastSvgComponent,
5076
],
5177
templateUrl: './feature-roadmap.component.html',
5278
styleUrl: './feature-roadmap.component.scss',
5379
changeDetection: ChangeDetectionStrategy.OnPush,
5480
})
55-
export class FeatureRoadmapComponent {
81+
export class FeatureRoadmapComponent implements AfterViewInit {
82+
private readonly _platform = inject(PLATFORM_ID);
83+
private _svgPanZoom!: SvgPanZoom.Instance;
84+
private readonly _svgRoadmap = viewChild<ElementRef<SVGElement>>('roadmap');
85+
5686
private readonly nodesDto = signal<RoadmapNodeDTO[]>([
5787
{
5888
id: '2',
@@ -98,6 +128,42 @@ export class FeatureRoadmapComponent {
98128
},
99129
]);
100130

131+
protected readonly controls: Control[] = [
132+
{
133+
event: 'increment',
134+
size: '24',
135+
name: 'zoom-in',
136+
},
137+
{
138+
event: 'reset',
139+
size: '24',
140+
name: 'circle-center',
141+
},
142+
{
143+
event: 'zoom-reset',
144+
size: '24',
145+
name: 'zoom-reset',
146+
},
147+
{
148+
event: 'decrement',
149+
size: '24',
150+
name: 'zoom-out',
151+
},
152+
];
153+
154+
async ngAfterViewInit() {
155+
if (isPlatformBrowser(this._platform)) {
156+
await this.initSvgPanZoom();
157+
}
158+
}
159+
160+
resizeRoadmap(event: EventType): void {
161+
if (event === 'reset') this._svgPanZoom.reset();
162+
if (event === 'decrement') this._svgPanZoom.zoomOut();
163+
if (event === 'increment') this._svgPanZoom.zoomIn();
164+
if (event === 'zoom-reset') this._svgPanZoom.resetZoom();
165+
}
166+
101167
protected readonly roadmapLayers = computed<RoadmapLayer[]>(() => {
102168
const nodeDtoMap = this.nodesDto().reduce(
103169
(acc, node) => ({ ...acc, [node.id]: node }),
@@ -208,4 +274,17 @@ export class FeatureRoadmapComponent {
208274
...layers,
209275
];
210276
});
277+
278+
private async initSvgPanZoom() {
279+
const svgPanZoomModule = await import('svg-pan-zoom');
280+
const svgPanZoom: SvgPanZoom.Instance =
281+
(svgPanZoomModule as any)['default'] || svgPanZoomModule;
282+
283+
const svgRoadmap = this._svgRoadmap();
284+
if (svgRoadmap) {
285+
this._svgPanZoom = svgPanZoom(svgRoadmap.nativeElement, {
286+
...svgPanZoomInitialConfig,
287+
});
288+
}
289+
}
211290
}

0 commit comments

Comments
 (0)