UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

205 lines (204 loc) 7.17 kB
import intersects from "intersects"; import { Component } from "../../../lib/Component"; const listeners = new WeakMap(); function createSyntheticHoverEvent(type) { return new MouseEvent(type, { bubbles: false, cancelable: false, clientX: 0, clientY: 0, }); } export class EventedComponent extends Component { constructor(props, parent) { super({ ...props, interactive: props.interactive ?? true, }, parent); this.evented = true; this._eventedAreas = new Map(); } isInteractive() { return this.props.interactive; } setInteractive(interactive) { this.setProps({ interactive }); } get events() { if (!listeners.has(this)) { listeners.set(this, new Map()); } return listeners.get(this); } unmount() { listeners.delete(this); super.unmount(); } willRender() { if (this._hoveredEventedAreaKey !== undefined) { const area = this._eventedAreas.get(this._hoveredEventedAreaKey); if (area) { const handler = area.params.mouseleave; this._prevHoveredAreaLeaveHandler = typeof handler === "function" ? handler : undefined; } } this._eventedAreas.clear(); super.willRender(); } didRender() { super.didRender(); if (this._hoveredEventedAreaKey !== undefined && !this._eventedAreas.has(this._hoveredEventedAreaKey)) { this._prevHoveredAreaLeaveHandler?.(createSyntheticHoverEvent("mouseleave")); this._hoveredEventedAreaKey = undefined; } this._prevHoveredAreaLeaveHandler = undefined; } _areaHitTest(area, hitBoxData) { const onHitBox = area.params.onHitBox; if (onHitBox) return onHitBox(hitBoxData); const { x, y, width, height } = area.rect; return intersects.boxBox(x, y, width, height, hitBoxData.minX, hitBoxData.minY, hitBoxData.maxX - hitBoxData.minX, hitBoxData.maxY - hitBoxData.minY); } _trackAreaHover() { if (this._eventedAreas.size === 0 || !this._lastHitBoxData) { this._clearAreaHover(true); return; } const hitBoxData = this._lastHitBoxData; let newHoveredKey; for (const [key, area] of this._eventedAreas) { if (this._areaHitTest(area, hitBoxData)) { newHoveredKey = key; break; } } if (newHoveredKey === this._hoveredEventedAreaKey) return; const prevArea = this._hoveredEventedAreaKey !== undefined ? this._eventedAreas.get(this._hoveredEventedAreaKey) : undefined; if (prevArea) { const leaveHandler = prevArea.params.mouseleave; if (typeof leaveHandler === "function") { leaveHandler(createSyntheticHoverEvent("mouseleave")); } } this._hoveredEventedAreaKey = newHoveredKey; if (newHoveredKey !== undefined) { const nextArea = this._eventedAreas.get(newHoveredKey); if (nextArea) { const enterHandler = nextArea.params.mouseenter; if (typeof enterHandler === "function") { enterHandler(createSyntheticHoverEvent("mouseenter")); } } } this.performRender(); } _clearAreaHover(scheduleRender = false) { if (this._hoveredEventedAreaKey !== undefined) { const area = this._eventedAreas.get(this._hoveredEventedAreaKey); if (area) { const leaveHandler = area.params.mouseleave; if (typeof leaveHandler === "function") { leaveHandler(createSyntheticHoverEvent("mouseleave")); } } this._hoveredEventedAreaKey = undefined; if (scheduleRender) { this.performRender(); } } } eventedArea(fn, params) { const state = { hovered: this._hoveredEventedAreaKey === params.key, }; const rect = fn(state); this._eventedAreas.set(params.key, { rect, params }); return rect; } handleEvent(_) { // noop } listenEvents(events, cbOrObject = this) { const unsubs = events.map((eventName) => { return this.addEventListener(eventName, cbOrObject); }); return unsubs; } addEventListener(type, cbOrObject) { const cbs = this.events.get(type) || new Set(); cbs.add(cbOrObject); this.events.set(type, cbs); return () => this.removeEventListener(type, cbOrObject); } removeEventListener(type, cbOrObject) { const cbs = this.events.get(type); if (cbs) { cbs.delete(cbOrObject); } } _fireEvent(cmp, event) { if (cmp instanceof EventedComponent && !cmp.isInteractive?.()) { return; } const handlers = listeners.get(cmp)?.get?.(event.type); handlers?.forEach((cb) => { if (typeof cb === "function") { return cb(event); } else if (cb instanceof Component && "handleEvent" in cb && typeof cb.handleEvent === "function") { return cb.handleEvent?.(event); } return undefined; }); if (cmp instanceof EventedComponent && cmp._eventedAreas.size > 0 && cmp._lastHitBoxData) { if (event.type === "mouseenter" || event.type === "mouseleave") return; const hitBoxData = cmp._lastHitBoxData; for (const area of cmp._eventedAreas.values()) { const handler = area.params[event.type]; if (typeof handler !== "function") continue; if (cmp._areaHitTest(area, hitBoxData)) { handler(event); } } } } dispatchEvent(event) { return this._dipping(this, event); } _dipping(startParent, event) { let stopPropagation = false; let parent = startParent; event.stopPropagation = () => { stopPropagation = true; }; do { if ((parent instanceof EventedComponent && !parent.isInteractive?.()) || !this._hasListener(parent, event.type)) { parent = parent.getParent(); continue; } this._fireEvent(parent, event); if (stopPropagation) { return false; } parent = parent.getParent(); } while (parent); return true; } _hasListener(comp, type) { if (listeners.get(comp)?.has?.(type)) return true; if (comp._eventedAreas?.size > 0) { for (const area of comp._eventedAreas.values()) { if (typeof area.params[type] === "function") return true; } } return false; } }