UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

261 lines (260 loc) 7.91 kB
import { Layer } from "../../../../services/Layer"; /** * CursorLayer provides cursor management for graph components. * * This layer manages cursor changes by applying cursor styles directly to the * graph root element, avoiding event blocking issues while maintaining * performance. * * ## Features: * - **Automatic mode**: Dynamically sets cursor based on component under mouse * - **Manual mode**: Allows explicit cursor control that overrides automatic behavior * * ## Usage: * ```typescript * // Automatic mode (default) * graph.getCursorLayer().isAuto(); // true * * // Manual mode * graph.lockCursor("grabbing"); * graph.unlockCursor(); // returns to auto mode * ``` * * @example * ```typescript * // The layer is automatically created and managed by the Graph * const cursorLayer = graph.getCursorLayer(); * * // Check current mode * console.log(cursorLayer.getMode()); // "auto" | "manual" * * // Set manual cursor * cursorLayer.lockCursor("wait"); * * // Return to automatic behavior * cursorLayer.unlockCursor(); * ``` */ export class CursorLayer extends Layer { /** * Creates a new CursorLayer instance. * * @param props - Configuration props for the layer */ constructor(props) { super({ // No HTML element needed - we'll apply cursor to the root element ...props, }); /** Current operating mode of the cursor layer */ this.mode = "auto"; } /** * Lifecycle method called after layer initialization. * Sets up event listeners for mouse tracking. */ afterInit() { super.afterInit(); // Always subscribe to graph events for mouse position tracking // Cursor is only applied in automatic mode this.subscribeToGraphEvents(); } /** * Subscribes to graph mouse events for cursor management. * Always tracks mouse position to maintain accurate state * when switching between manual and auto modes. * * @private */ subscribeToGraphEvents() { // Subscribe to mouseenter and mouseleave events from the graph // ALWAYS track state to know where the mouse is located this.onGraphEvent("mouseenter", (event) => { const target = event.detail?.target; // Always track the current target this.currentTarget = target; // Apply cursor only in automatic mode if (this.mode === "auto") { this.updateCursorForTarget(target); } }); this.onGraphEvent("mouseleave", () => { // Always track that mouse left the element this.currentTarget = undefined; // Apply cursor only in automatic mode if (this.mode === "auto") { this.applyCursor("auto"); } }); } /** * Updates the cursor based on the target component's cursor property. * * @param target - The component under the mouse cursor * @private */ updateCursorForTarget(target) { if (target && typeof target.isInteractive === "function" && target.isInteractive() && target.cursor) { this.applyCursor(target.cursor); } else { this.applyCursor("auto"); } } /** * Applies the specified cursor to the graph root element. * * @param cursor - The cursor type to apply * @private */ applyCursor(cursor) { const rootElement = this.props.graph.layers.$root; if (rootElement) { rootElement.style.cursor = cursor; } } unmountLayer() { super.unmountLayer(); const rootElement = this.props.graph.layers.$root; if (rootElement) { rootElement.style.cursor = "auto"; } } /** * Locks the cursor to a specific type, disabling automatic cursor changes. * * When locked, the cursor will not change automatically based on * component interactions until unlockCursor() is called. This effectively * "freezes" the cursor state until manually unlocked. * * @param cursor - The cursor type to lock to * * @example * ```typescript * // Lock to loading cursor during async operation * cursorLayer.lockCursor("wait"); * * // Lock to grabbing cursor during drag * cursorLayer.lockCursor("grabbing"); * ``` * * @see {@link unlockCursor} to return to automatic behavior */ lockCursor(cursor) { this.mode = "manual"; this.manualCursor = cursor; this.applyCursor(cursor); } /** * Unlocks the cursor and returns to automatic cursor management. * * The cursor will immediately update to reflect the current state * based on the component under the mouse (if any). This removes the * "lock" and allows the cursor to respond to component interactions again. * * @example * ```typescript * // Unlock to return to automatic behavior * cursorLayer.unlockCursor(); * * // If mouse is over a block, cursor will immediately show "grab" * // If mouse is over empty space, cursor will show "auto" * ``` * * @see {@link lockCursor} to disable automatic behavior */ unlockCursor() { this.mode = "auto"; this.manualCursor = undefined; // Immediately apply the correct cursor for the current state if (this.currentTarget) { this.updateCursorForTarget(this.currentTarget); } else { this.applyCursor("auto"); } } /** * Returns the current operating mode of the cursor layer. * * @returns The current mode ("auto" or "manual") * * @example * ```typescript * if (cursorLayer.getMode() === "manual") { * console.log("Cursor is manually controlled"); * } * ``` */ getMode() { return this.mode; } /** * Returns the currently set manual cursor type. * * @returns The manual cursor type, or undefined if not in manual mode * * @example * ```typescript * const manualCursor = cursorLayer.getManualCursor(); * if (manualCursor) { * console.log(`Manual cursor: ${manualCursor}`); * } * ``` */ getManualCursor() { return this.manualCursor; } /** * Checks if the cursor layer is currently in manual mode. * * @returns True if in manual mode, false otherwise * * @example * ```typescript * if (cursorLayer.isManual()) { * // Manual cursor is active * console.log("Manual cursor:", cursorLayer.getManualCursor()); * } * ``` */ isManual() { return this.mode === "manual"; } /** * Checks if the cursor layer is currently in automatic mode. * * @returns True if in automatic mode, false otherwise * * @example * ```typescript * if (cursorLayer.isAuto()) { * // Cursor changes automatically based on components * console.log("Current target:", cursorLayer.getCurrentTarget()); * } * ``` */ isAuto() { return this.mode === "auto"; } /** * Returns the component currently under the mouse cursor. * * This method is primarily intended for debugging and development. * The value is tracked regardless of the current mode. * * @returns The component under the cursor, or undefined if none * * @example * ```typescript * const target = cursorLayer.getCurrentTarget(); * if (target) { * console.log("Mouse over:", target.constructor.name); * console.log("Component cursor:", target.cursor); * } * ``` */ getCurrentTarget() { return this.currentTarget; } }