BT  ·  2026-06-08

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}