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; } }