186 lines
5.6 KiB
JavaScript
186 lines
5.6 KiB
JavaScript
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
const DEFAULT_CLASS_NAME = "info-card-cruise-link";
|
|
const DEFAULT_DRAW_ANIMATION_NAME = "cruiseConnectorDraw";
|
|
|
|
function createSvgElement(tagName) {
|
|
return document.createElementNS(SVG_NS, tagName);
|
|
}
|
|
|
|
export function createElbowConnectorPoints(source, target, options = {}) {
|
|
if (!source || !target) return null;
|
|
|
|
const {
|
|
startFrom = "source",
|
|
sourceGapPx = 12,
|
|
targetGapPx = 8,
|
|
elbowOffsetPx = 18,
|
|
elbowDropPx = 14,
|
|
} = options;
|
|
|
|
const sourcePoint = { x: Number(source.x), y: Number(source.y) };
|
|
const targetPoint = { x: Number(target.x), y: Number(target.y) };
|
|
if (
|
|
!Number.isFinite(sourcePoint.x) ||
|
|
!Number.isFinite(sourcePoint.y) ||
|
|
!Number.isFinite(targetPoint.x) ||
|
|
!Number.isFinite(targetPoint.y)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const horizontalDirection = sourcePoint.x <= targetPoint.x ? 1 : -1;
|
|
const startX = sourcePoint.x + horizontalDirection * sourceGapPx;
|
|
const startY = sourcePoint.y;
|
|
const endX = targetPoint.x - horizontalDirection * targetGapPx;
|
|
const endY = targetPoint.y;
|
|
const elbowX = endX - horizontalDirection * elbowOffsetPx;
|
|
const elbowY = Math.min(startY, endY) + elbowDropPx;
|
|
|
|
if (Math.abs(endX - startX) < 8 && Math.abs(endY - startY) < 8) {
|
|
return null;
|
|
}
|
|
|
|
const orderedPoints = [
|
|
{ x: startX, y: startY },
|
|
{ x: elbowX, y: elbowY },
|
|
{ x: endX, y: endY },
|
|
];
|
|
|
|
return {
|
|
points: startFrom === "target" ? orderedPoints.slice().reverse() : orderedPoints,
|
|
start: startFrom === "target" ? orderedPoints[2] : orderedPoints[0],
|
|
end: startFrom === "target" ? orderedPoints[0] : orderedPoints[2],
|
|
};
|
|
}
|
|
|
|
export class CalloutConnector {
|
|
constructor({
|
|
container = null,
|
|
containerId = "container",
|
|
className = DEFAULT_CLASS_NAME,
|
|
drawAnimationName = DEFAULT_DRAW_ANIMATION_NAME,
|
|
} = {}) {
|
|
this.container = container;
|
|
this.containerId = containerId;
|
|
this.className = className;
|
|
this.drawAnimationName = drawAnimationName;
|
|
this.connectorEl = null;
|
|
this.polylineEl = null;
|
|
this.startpointEl = null;
|
|
this.endpointEl = null;
|
|
}
|
|
|
|
resolveContainer() {
|
|
if (this.container instanceof HTMLElement) return this.container;
|
|
this.container = document.getElementById(this.containerId);
|
|
return this.container instanceof HTMLElement ? this.container : null;
|
|
}
|
|
|
|
ensure() {
|
|
if (this.connectorEl instanceof SVGSVGElement) {
|
|
return this.connectorEl;
|
|
}
|
|
|
|
const container = this.resolveContainer();
|
|
if (!container) return null;
|
|
|
|
const connector = createSvgElement("svg");
|
|
connector.setAttribute("class", this.className);
|
|
connector.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`);
|
|
connector.setAttribute("preserveAspectRatio", "none");
|
|
|
|
const polyline = createSvgElement("polyline");
|
|
const startpoint = createSvgElement("circle");
|
|
const endpoint = createSvgElement("circle");
|
|
startpoint.setAttribute("r", "4");
|
|
endpoint.setAttribute("r", "4");
|
|
|
|
connector.append(startpoint, polyline, endpoint);
|
|
container.appendChild(connector);
|
|
|
|
connector.addEventListener("animationend", (event) => {
|
|
if (
|
|
event.animationName === this.drawAnimationName &&
|
|
this.connectorEl?.classList.contains("is-visible")
|
|
) {
|
|
if (this.polylineEl) {
|
|
this.polylineEl.style.strokeDashoffset = "0";
|
|
}
|
|
this.connectorEl?.classList.remove("is-animating");
|
|
}
|
|
});
|
|
|
|
this.connectorEl = connector;
|
|
this.polylineEl = polyline;
|
|
this.startpointEl = startpoint;
|
|
this.endpointEl = endpoint;
|
|
return connector;
|
|
}
|
|
|
|
isVisible() {
|
|
return this.connectorEl?.classList.contains("is-visible") === true;
|
|
}
|
|
|
|
isAnimating() {
|
|
return this.connectorEl?.classList.contains("is-animating") === true;
|
|
}
|
|
|
|
hide() {
|
|
const connector = this.ensure();
|
|
if (!connector) return;
|
|
connector.classList.remove("is-visible", "is-animating");
|
|
}
|
|
|
|
render(path, { animate = false } = {}) {
|
|
const connector = this.ensure();
|
|
if (
|
|
!connector ||
|
|
!this.polylineEl ||
|
|
!this.startpointEl ||
|
|
!this.endpointEl ||
|
|
!Array.isArray(path?.points) ||
|
|
path.points.length < 2
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const viewWidth = window.innerWidth;
|
|
const viewHeight = window.innerHeight;
|
|
connector.setAttribute("viewBox", `0 0 ${viewWidth} ${viewHeight}`);
|
|
|
|
const pointsText = path.points
|
|
.map((point) => `${point.x.toFixed(2)},${point.y.toFixed(2)}`)
|
|
.join(" ");
|
|
this.polylineEl.setAttribute("points", pointsText);
|
|
this.startpointEl.setAttribute("cx", path.start.x.toFixed(2));
|
|
this.startpointEl.setAttribute("cy", path.start.y.toFixed(2));
|
|
this.endpointEl.setAttribute("cx", path.end.x.toFixed(2));
|
|
this.endpointEl.setAttribute("cy", path.end.y.toFixed(2));
|
|
|
|
const totalLength =
|
|
typeof this.polylineEl.getTotalLength === "function"
|
|
? this.polylineEl.getTotalLength()
|
|
: 0;
|
|
|
|
this.polylineEl.style.strokeDasharray = totalLength > 0 ? `${totalLength}` : "";
|
|
this.polylineEl.style.strokeDashoffset =
|
|
totalLength > 0 ? `${animate ? totalLength : 0}` : "";
|
|
connector.style.setProperty(
|
|
"--connector-length",
|
|
totalLength > 0 ? `${totalLength}` : "0px",
|
|
);
|
|
connector.classList.add("is-visible");
|
|
|
|
if (animate && totalLength > 0) {
|
|
connector.classList.remove("is-animating");
|
|
void connector.getBoundingClientRect();
|
|
this.polylineEl.style.strokeDashoffset = `${totalLength}`;
|
|
connector.classList.add("is-animating");
|
|
} else {
|
|
connector.classList.remove("is-animating");
|
|
}
|
|
|
|
return totalLength > 0;
|
|
}
|
|
}
|