Last active
October 7, 2022 17:03
-
-
Save dianjuar/1fb69f3cd80af2a54915f87be8d421ef to your computer and use it in GitHub Desktop.
Create connecting lines between boxes with Angular
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
AfterViewInit, | |
Directive, | |
ElementRef, | |
Inject, | |
Input, | |
} from '@angular/core'; | |
import { DOCUMENT } from '@angular/common'; | |
import { MapPathsDirective } from './map-path.directive'; | |
type Point = { | |
x: number; | |
y: number; | |
}; | |
@Directive({ | |
selector: '[connectingLines]', | |
}) | |
export class ConnectingLinesDirective implements AfterViewInit { | |
/** | |
* When it's on, add some styles to the svg canvas | |
*/ | |
@Input() | |
public debugMode: boolean = false; | |
private svgCanvas: SVGElement | undefined = undefined; | |
constructor( | |
@Inject(DOCUMENT) private document: Document, | |
private host: ElementRef, | |
private paths: MapPathsDirective | |
) {} | |
ngAfterViewInit(): void { | |
const hostNativeElement: HTMLElement = this.host.nativeElement; | |
hostNativeElement.style.position = 'relative'; | |
// insert canvas into host | |
const canvas = this.generateSVGCanvas(); | |
this.svgCanvas = hostNativeElement.insertAdjacentElement( | |
'afterbegin', | |
canvas | |
) as SVGElement; | |
this.drawLines(); | |
} | |
/** | |
* Generates the svg canvas element with the necessary styles | |
*/ | |
private generateSVGCanvas(): SVGElement { | |
const svgCanvas = this.document.createElementNS( | |
'http://www.w3.org/2000/svg', | |
'svg' | |
); | |
svgCanvas.style.height = '100%'; | |
svgCanvas.style.width = '100%'; | |
svgCanvas.style.position = 'absolute'; | |
if (this.debugMode) { | |
svgCanvas.style.backgroundColor = 'rgba(255, 0, 0, 0.1)'; | |
svgCanvas.style.zIndex = '1'; | |
} | |
return svgCanvas; | |
} | |
private drawLines() { | |
const allStartCards = this.paths.getStartElements(); | |
Array.from(allStartCards).forEach(startElement => { | |
const endElement = this.paths.getEndElement(startElement); | |
this.drawLinesBetweenElements(startElement, endElement); | |
}); | |
} | |
private drawLinesBetweenElements(startElement: Element, endElement: Element) { | |
if (this.areElementsInRectLine(startElement, endElement)) { | |
this.drawRectLineBetween(startElement, endElement); | |
} else { | |
this.drawCurveLineBetween(startElement, endElement); | |
} | |
} | |
private areElementsInRectLine( | |
startElement: Element, | |
endElement: Element | |
): boolean { | |
return ( | |
startElement.getBoundingClientRect().y === | |
endElement.getBoundingClientRect().y | |
); | |
} | |
private drawRectLineBetween(startElement: Element, endElement: Element) { | |
const canvasRect = this.svgCanvas!.getBoundingClientRect(); | |
const startRect = startElement.getBoundingClientRect(); | |
const endRect = endElement.getBoundingClientRect(); | |
const startPoint: Point = { | |
x: this.getXPoint(startRect, canvasRect, 'start'), | |
y: this.getYPoint(startRect, canvasRect, 'horizontal'), | |
}; | |
const endPoint: Point = { | |
x: this.getXPoint(endRect, canvasRect, 'end'), | |
y: this.getYPoint(endRect, canvasRect, 'horizontal'), | |
}; | |
const path = this.createPath(startPoint, endPoint); | |
this.svgCanvas!.appendChild(path); | |
} | |
private drawCurveLineBetween(startElement: Element, endElement: Element) { | |
const canvasRect = this.svgCanvas!.getBoundingClientRect(); | |
const startRect = startElement.getBoundingClientRect(); | |
const endRect = endElement.getBoundingClientRect(); | |
const startPointHorizontal: Point = { | |
x: this.getXPoint(startRect, canvasRect, 'start'), | |
y: this.getYPoint(startRect, canvasRect, 'horizontal'), | |
}; | |
const endPointHorizontal: Point = { | |
x: this.getXPoint(endRect, canvasRect, 'vertical'), | |
y: this.getYPoint(startRect, canvasRect, 'horizontal'), | |
}; | |
const startPointVertical: Point = { | |
x: this.getXPoint(endRect, canvasRect, 'vertical'), | |
y: this.getYPoint(startRect, canvasRect, 'horizontal'), | |
}; | |
const endPointVertical: Point = { | |
x: this.getXPoint(endRect, canvasRect, 'vertical'), | |
y: this.getYPoint(endRect, canvasRect, 'vertical'), | |
}; | |
const pathHorizontal = this.createPath( | |
startPointHorizontal, | |
endPointHorizontal | |
); | |
const pathVertical = this.createPath( | |
startPointVertical, | |
endPointVertical, | |
this.paths.wantsOffset(startElement) | |
); | |
this.svgCanvas!.appendChild(pathHorizontal); | |
this.svgCanvas!.appendChild(pathVertical); | |
} | |
private getXPoint( | |
rect: DOMRect, | |
offset: DOMRect, | |
endOrStart: 'end' | 'start' | 'vertical' | |
): number { | |
let x: number; | |
switch (endOrStart) { | |
case 'start': | |
x = rect.right; | |
break; | |
case 'end': | |
x = rect.left; | |
break; | |
case 'vertical': | |
x = rect.left + rect.width / 2; | |
break; | |
} | |
return x - offset.x; | |
} | |
private getYPoint( | |
rect: DOMRect, | |
offset: DOMRect, | |
direction: 'horizontal' | 'vertical' | |
) { | |
let y: number; | |
if (direction === 'horizontal') { | |
y = rect.y + rect.height / 2; | |
} else { | |
y = rect.bottom; | |
} | |
return y - offset.y; | |
} | |
private createPath(start: Point, end: Point, withOffset?: boolean) { | |
const path = this.document.createElementNS( | |
'http://www.w3.org/2000/svg', | |
'line' | |
); | |
path.setAttribute('x1', start.x.toString()); | |
path.setAttribute('y1', start.y.toString()); | |
path.setAttribute('x2', end.x.toString()); | |
path.setAttribute('y2', end.y.toString()); | |
// Styles | |
path.setAttribute('stroke', 'lightgray'); | |
path.setAttribute('stroke-width', '2'); | |
path.setAttribute('stroke-dasharray', '2 2'); | |
path.setAttribute('stroke-dashoffset', withOffset ? '2' : '0'); | |
return path; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="process-map-container" connectingLines> | |
<kendo-stacklayout | |
[gap]="30" | |
orientation="horizontal" | |
[align]="{ horizontal: 'start' }" | |
> | |
<val-section-panel title="load" orientation="vertical"> | |
<!-- statements --> | |
<process-map-card-statement-files [mapPaths]="paths.statementFiles"> | |
</process-map-card-statement-files> | |
<!-- checks --> | |
<process-map-card-check-files [mapPaths]="paths.checkFiles"> | |
</process-map-card-check-files> | |
<!-- account extracts --> | |
<process-map-card-account-extracts [mapPaths]="paths.accountExtracts"> | |
</process-map-card-account-extracts> | |
</val-section-panel> | |
<val-section-panel title="refine" orientation="horizontal"> | |
<!-- reconcile --> | |
<process-map-card-reconcile [mapPaths]="paths.reconcile"> | |
</process-map-card-reconcile> | |
<!-- transaction --> | |
<process-map-card-transactions [mapPaths]="paths.transaction"> | |
</process-map-card-transactions> | |
<!-- match --> | |
<process-map-card-match [mapPaths]="paths.match"> | |
</process-map-card-match> | |
<!-- check bank match --> | |
<process-map-card-check-bank-match [mapPaths]="paths.checkBankMatch"> | |
</process-map-card-check-bank-match> | |
</val-section-panel> | |
<val-section-panel title="analyze" orientation="vertical"> | |
<!-- output --> | |
<process-map-card-outputs [mapPaths]="paths.outputs"> | |
</process-map-card-outputs> | |
</val-section-panel> | |
</kendo-stacklayout> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { DOCUMENT } from '@angular/common'; | |
import { Directive, ElementRef, Inject, Input, OnInit } from '@angular/core'; | |
export type MapIds = { | |
start?: string; | |
end?: string[]; | |
/** | |
* Used to align dots | |
*/ | |
withOffset?: boolean; | |
}; | |
@Directive({ | |
selector: '[mapPaths]', | |
}) | |
export class MapPathsDirective implements OnInit { | |
private readonly START_METADATA_ATTR = 'data-map-lines-start'; | |
private readonly END_METADATA_ATTR = 'data-map-lines-end'; | |
private readonly WITH_OFFSET_METADATA_ATTR = | |
'data-map-lines-withOffset'; | |
@Input('mapPaths') | |
pathsIds!: MapIds; | |
constructor( | |
private host: ElementRef, | |
@Inject(DOCUMENT) private document: Document | |
) {} | |
ngOnInit(): void { | |
this.setStartIds(this.pathsIds.start); | |
this.setEndIds(this.pathsIds.end); | |
this.setOffset(this.pathsIds.withOffset); | |
} | |
getStartElements() { | |
return this.document.querySelectorAll(`[${this.START_METADATA_ATTR}]`); | |
} | |
getEndElement(startElement: Element): Element { | |
const connectionId = startElement.attributes.getNamedItem( | |
this.START_METADATA_ATTR | |
)!.value; | |
const endElement = this.document.querySelector( | |
`[${this.buildEndConnectionId(connectionId)}]` | |
)!; | |
return endElement; | |
} | |
wantsOffset(startElement: Element): boolean { | |
return !!startElement.attributes.getNamedItem( | |
this.WITH_OFFSET_METADATA_ATTR | |
); | |
} | |
private buildEndConnectionId(id: string) { | |
return `${this.END_METADATA_ATTR}-${id}`; | |
} | |
private setStartIds(startId: MapIds['start']) { | |
if (!startId) { | |
return; | |
} | |
const nativeElement = this.host.nativeElement as Element; | |
nativeElement.setAttribute(this.START_METADATA_ATTR, startId); | |
} | |
private setEndIds(endIds: MapIds['end']) { | |
if (!endIds) { | |
return; | |
} | |
endIds.forEach(endId => { | |
const nativeElement = this.host.nativeElement as Element; | |
nativeElement.setAttribute(this.buildEndConnectionId(endId), ''); | |
}); | |
} | |
private setOffset(withOffset: MapIds['withOffset']) { | |
if (!withOffset) { | |
return; | |
} | |
const nativeElement = this.host.nativeElement as Element; | |
nativeElement.setAttribute(this.WITH_OFFSET_METADATA_ATTR, ''); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment