@gravity-ui/graph
Version:
Modern graph editor component
192 lines (191 loc) • 7.41 kB
JavaScript
import merge from "lodash/merge";
import StyleObserver from "style-observer";
import { Layer } from "../../services/Layer";
import { DEFAULT_CSS_VARIABLES_LAYER_PROPS, SUPPORTED_CSS_VARIABLES } from "./constants";
import { filterSupportedCSSChanges, mapCSSChangesToGraphColors, mapCSSChangesToGraphConstants } from "./mapping";
/**
* CSSVariablesLayer: Synchronizes CSS variables with graph colors and constants
*
* Creates an empty HTML div with specified CSS class and monitors CSS variable changes
* using style-observer package. Automatically maps CSS variables to TGraphColors and
* TGraphConstants and applies changes via graph.setColors() and graph.setConstants().
*/
export class CSSVariablesLayer extends Layer {
constructor(props) {
const finalProps = { ...DEFAULT_CSS_VARIABLES_LAYER_PROPS, ...props };
super(finalProps);
this.state = {
isObserving: false,
colors: {},
constants: {},
};
this.containerElement = null;
this.styleObserver = null;
}
propsChanged(nextProps) {
super.propsChanged(nextProps);
// If containerClass changed, we need to recreate the container and restart observing
if (this.props.containerClass !== nextProps.containerClass) {
if (this.props.debug) {
console.log("CSSVariablesLayer: Container class changed from", this.props.containerClass, "to", nextProps.containerClass);
}
this.stopObserving();
this.removeContainerElement();
this.createContainerElement();
this.startObserving();
}
}
afterInit() {
this.createContainerElement();
this.startObserving();
super.afterInit();
}
unmount() {
this.stopObserving();
this.removeContainerElement();
super.unmount();
}
/**
* Creates the container HTML element with specified CSS class
*/
createContainerElement() {
if (!this.getHTML()) {
if (this.props.debug) {
console.warn("CSSVariablesLayer: HTML element not available");
}
return;
}
this.containerElement = document.createElement("div");
this.containerElement.className = this.props.containerClass;
// Make the container invisible and non-interactive
this.containerElement.style.cssText = `
position: absolute;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
visibility: hidden;
`;
this.getHTML()?.appendChild(this.containerElement);
if (this.props.debug) {
console.log("CSSVariablesLayer: Container element created with class", this.props.containerClass);
console.log("CSSVariablesLayer: Container element:", this.containerElement);
console.log("CSSVariablesLayer: Container in DOM:", document.contains(this.containerElement));
}
}
/**
* Removes the container element from DOM
*/
removeContainerElement() {
if (this.containerElement && this.containerElement.parentNode) {
this.containerElement.parentNode.removeChild(this.containerElement);
}
this.containerElement = null;
}
createStyleObserver() {
return new StyleObserver((records) => {
// Convert StyleObserver records to our CSSVariableChange format
const changes = records.map((record) => ({
name: record.property,
value: record.value,
oldValue: record.oldValue,
}));
if (this.props.debug) {
console.log("CSSVariablesLayer: CSS variable changes detected:", changes);
}
this.handleCSSVariableChanges(changes);
}, {
targets: [this.containerElement],
properties: Array.from(SUPPORTED_CSS_VARIABLES),
});
}
/**
* Starts observing CSS variable changes using style-observer
*/
startObserving() {
if (!this.containerElement) {
if (this.props.debug) {
console.warn("CSSVariablesLayer: Cannot start observing - container element not available");
}
return;
}
if (this.state.isObserving) {
return;
}
try {
// Convert Set to Array for style-observer
const variablesToObserve = Array.from(SUPPORTED_CSS_VARIABLES);
this.styleObserver = this.createStyleObserver();
this.setState({ isObserving: true });
if (this.props.debug) {
console.log("CSSVariablesLayer: Started observing", variablesToObserve.length, "CSS variables");
console.log("CSSVariablesLayer: Observing element:", this.containerElement);
console.log("CSSVariablesLayer: Variables to observe:", variablesToObserve);
}
}
catch (error) {
console.error("CSSVariablesLayer: Failed to start observing CSS variables:", error);
}
}
/**
* Stops observing CSS variable changes
*/
stopObserving() {
if (this.styleObserver) {
this.styleObserver.unobserve(this.containerElement);
this.styleObserver = null;
}
this.setState({ isObserving: false });
if (this.props.debug) {
console.log("CSSVariablesLayer: Stopped observing CSS variables");
}
}
/**
* Handles CSS variable changes from style-observer
*/
handleCSSVariableChanges(changes) {
// Filter to only supported variables
const supportedChanges = filterSupportedCSSChanges(changes);
if (supportedChanges.length === 0) {
return;
}
if (this.props.debug) {
console.log("CSSVariablesLayer: CSS variable changes detected:", supportedChanges);
}
// Apply changes to graph
this.applyChangesToGraph(supportedChanges);
// Call user callback if provided
if (this.props.onChange) {
this.props.onChange(supportedChanges);
}
}
/**
* Applies CSS variable changes to graph colors and constants
*/
applyChangesToGraph(changes) {
try {
// Map changes to graph colors
const colorChanges = mapCSSChangesToGraphColors(changes);
const constantChanges = mapCSSChangesToGraphConstants(changes);
if (Object.keys(colorChanges).length === 0 && Object.keys(constantChanges).length === 0) {
return;
}
const colors = merge({}, this.state.colors, colorChanges);
const constants = merge({}, this.state.constants, constantChanges);
this.setState({ colors, constants });
this.props.graph.setColors(colors);
if (this.props.debug) {
console.log("CSSVariablesLayer: Applied color changes:", colorChanges);
}
this.props.graph.setConstants(constants);
if (this.props.debug) {
console.log("CSSVariablesLayer: Applied constant changes:", constantChanges);
}
}
catch (error) {
console.error("CSSVariablesLayer: Failed to apply changes to graph:", error);
}
}
}