@gravity-ui/graph
Version:
Modern graph editor component
205 lines (204 loc) • 7.17 kB
JavaScript
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;
}
}