rtw-waypoint.ts
1/**
2 * Custom event emitted when hovering over a waypoint element.
3 * Includes the gid (global ID) of the waypoint.
4 */
5export class WaypointHoverEvent extends Event {
6 readonly gid: string;
7
8 constructor(gid: string) {
9 super("waypoint:hover", {
10 bubbles: true,
11 cancelable: true,
12 composed: true,
13 });
14 this.gid = gid;
15 }
16}
17
18/**
19 * Custom event emitted when clicking on a waypoint element.
20 * Includes the gid (global ID) of the waypoint.
21 */
22export class WaypointClickEvent extends Event {
23 readonly gid: string;
24
25 constructor(gid: string) {
26 super("waypoint:click", {
27 bubbles: true,
28 cancelable: true,
29 composed: true,
30 });
31 this.gid = gid;
32 }
33}
34
35/**
36 * Custom web component for displaying a waypoint with hover and click interactions.
37 *
38 * Attributes:
39 * - type: Waypoint type/category
40 * - id: Unique identifier
41 * - point: Geographic coordinates [lon, lat]
42 * - label: Display text (rendered in a span)
43 * - gid: Global ID (emitted in events)
44 * - country: Country code
45 * - from: Routing from other waypoint
46 * - cons: Constraint (before, after, speed, etc.)
47 * - error: Array of routing error codes (e.g., ["RouteTooFar"])
48 */
49export class RTWWaypoint extends HTMLElement {
50 private boundHandleHover: (() => void) | null = null;
51 private boundHandleClick: ((ev: MouseEvent) => void) | null = null;
52
53 static get observedAttributes() {
54 return [
55 "type",
56 "id",
57 "point",
58 "label",
59 "gid",
60 "country",
61 "from",
62 "cons",
63 "nonroutable",
64 "error",
65 ];
66 }
67
68 private updateErrorState() {
69 const errorAttr = this.getAttribute("error");
70 let errors: string[] = [];
71
72 try {
73 if (errorAttr) {
74 errors = JSON.parse(errorAttr);
75 }
76 } catch {
77 errors = [];
78 }
79
80 const hasErrors = Array.isArray(errors) && errors.length > 0;
81
82 if (hasErrors) {
83 this.classList.add("error");
84 if (errors.includes("RouteTooFar")) {
85 this.title = "Too far away. Add a closer point.";
86 } else {
87 this.title = "Routing error: " + errors.join(", ");
88 }
89 } else {
90 this.classList.remove("error");
91 this.title = "";
92 }
93 }
94
95 attributeChangedCallback(
96 name: string,
97 oldValue: string | null,
98 newValue: string | null,
99 ) {
100 if (oldValue === newValue) return;
101
102 switch (name) {
103 case "label":
104 this.textContent = newValue || "";
105 break;
106 case "error":
107 this.updateErrorState();
108 break;
109 // Other attributes are stored on the element and accessed via getAttribute when needed
110 }
111 }
112
113 connectedCallback() {
114 // Set text content directly on the element (idempotent - safe on reconnect)
115 this.textContent = this.getAttribute("label") || "";
116
117 // Set initial error state
118 this.updateErrorState();
119
120 // Create bound event handlers
121 this.boundHandleHover = () => {
122 console.debug("waypoint hover");
123 const gid = this.getAttribute("gid");
124 if (gid) {
125 this.dispatchEvent(new WaypointHoverEvent(gid));
126 }
127 };
128
129 this.boundHandleClick = (ev) => {
130 ev.preventDefault();
131 console.debug("waypoint click");
132 const gid = this.getAttribute("gid");
133 if (gid) {
134 this.dispatchEvent(new WaypointClickEvent(gid));
135 }
136 };
137
138 // Attach event listeners to the element itself
139 this.addEventListener("mouseenter", this.boundHandleHover);
140 this.addEventListener("click", this.boundHandleClick);
141 }
142
143 disconnectedCallback() {
144 // Remove event listeners from the element
145 if (this.boundHandleHover) {
146 this.removeEventListener("mouseenter", this.boundHandleHover);
147 }
148 if (this.boundHandleClick) {
149 this.removeEventListener("click", this.boundHandleClick);
150 }
151
152 // Clean up references
153 this.boundHandleHover = null;
154 this.boundHandleClick = null;
155 }
156}
157
158// Register the custom element if not already defined
159if (!customElements.get("rtw-waypoint")) {
160 customElements.define("rtw-waypoint", RTWWaypoint);
161}