mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
254 lines
7.1 KiB
TypeScript
254 lines
7.1 KiB
TypeScript
import {Component, createContext, createRef} from 'react';
|
|
|
|
import {rectOfContent} from 'sentry/components/performance/waterfall/utils';
|
|
import clamp from 'sentry/utils/number/clamp';
|
|
import toPercent from 'sentry/utils/number/toPercent';
|
|
import type {UserSelectValues} from 'sentry/utils/userselect';
|
|
import {setBodyUserSelect} from 'sentry/utils/userselect';
|
|
|
|
// divider handle is positioned at 50% width from the left-hand side
|
|
const DEFAULT_DIVIDER_POSITION = 0.4;
|
|
|
|
const selectRefs = (
|
|
refs: Array<React.RefObject<HTMLDivElement>>,
|
|
transform: (dividerDOM: HTMLDivElement) => void
|
|
) => {
|
|
refs.forEach(ref => {
|
|
if (ref.current) {
|
|
transform(ref.current);
|
|
}
|
|
});
|
|
};
|
|
|
|
export type DividerHandlerManagerChildrenProps = {
|
|
addDividerLineRef: () => React.RefObject<HTMLDivElement>;
|
|
addGhostDividerLineRef: () => React.RefObject<HTMLDivElement>;
|
|
dividerPosition: number;
|
|
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
|
setHover: (nextHover: boolean) => void;
|
|
};
|
|
|
|
type StateType = {
|
|
dividerPosition: number; // between 0 and 1
|
|
};
|
|
|
|
const DividerManagerContext = createContext<DividerHandlerManagerChildrenProps>({
|
|
dividerPosition: DEFAULT_DIVIDER_POSITION,
|
|
onDragStart: () => {},
|
|
setHover: () => {},
|
|
addDividerLineRef: () => createRef<HTMLDivElement>(),
|
|
addGhostDividerLineRef: () => createRef<HTMLDivElement>(),
|
|
});
|
|
|
|
type PropType = {
|
|
children: React.ReactNode;
|
|
|
|
// this is the DOM element where the drag events occur. it's also the reference point
|
|
// for calculating the relative mouse x coordinate.
|
|
interactiveLayerRef: React.RefObject<HTMLDivElement>;
|
|
};
|
|
|
|
export class Provider extends Component<PropType, StateType> {
|
|
state: StateType = {
|
|
dividerPosition: DEFAULT_DIVIDER_POSITION,
|
|
};
|
|
|
|
componentWillUnmount() {
|
|
this.cleanUpListeners();
|
|
}
|
|
|
|
previousUserSelect: UserSelectValues | null = null;
|
|
dividerHandlePosition: number = DEFAULT_DIVIDER_POSITION;
|
|
isDragging: boolean = false;
|
|
dividerLineRefs: Array<React.RefObject<HTMLDivElement>> = [];
|
|
ghostDividerLineRefs: Array<React.RefObject<HTMLDivElement>> = [];
|
|
|
|
hasInteractiveLayer = (): boolean => !!this.props.interactiveLayerRef.current;
|
|
|
|
addDividerLineRef = () => {
|
|
const ref = createRef<HTMLDivElement>();
|
|
this.dividerLineRefs.push(ref);
|
|
return ref;
|
|
};
|
|
|
|
addGhostDividerLineRef = () => {
|
|
const ref = createRef<HTMLDivElement>();
|
|
this.ghostDividerLineRefs.push(ref);
|
|
return ref;
|
|
};
|
|
|
|
setHover = (nextHover: boolean) => {
|
|
if (this.isDragging) {
|
|
return;
|
|
}
|
|
|
|
selectRefs(this.dividerLineRefs, dividerDOM => {
|
|
if (nextHover) {
|
|
dividerDOM.classList.add('hovering');
|
|
return;
|
|
}
|
|
|
|
dividerDOM.classList.remove('hovering');
|
|
});
|
|
};
|
|
|
|
onDragStart = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
if (this.isDragging || event.type !== 'mousedown' || !this.hasInteractiveLayer()) {
|
|
return;
|
|
}
|
|
|
|
event.stopPropagation();
|
|
|
|
// prevent the user from selecting things outside the minimap when dragging
|
|
// the mouse cursor inside the minimap
|
|
|
|
this.previousUserSelect = setBodyUserSelect({
|
|
userSelect: 'none',
|
|
MozUserSelect: 'none',
|
|
msUserSelect: 'none',
|
|
webkitUserSelect: 'none',
|
|
});
|
|
|
|
// attach event listeners so that the mouse cursor does not select text during a drag
|
|
window.addEventListener('mousemove', this.onDragMove);
|
|
window.addEventListener('mouseup', this.onDragEnd);
|
|
|
|
this.setHover(true);
|
|
|
|
// indicate drag has begun
|
|
|
|
this.isDragging = true;
|
|
|
|
selectRefs(this.dividerLineRefs, (dividerDOM: HTMLDivElement) => {
|
|
dividerDOM.style.backgroundColor = 'rgba(73,80,87,0.75)';
|
|
dividerDOM.style.cursor = 'col-resize';
|
|
});
|
|
|
|
selectRefs(this.ghostDividerLineRefs, (dividerDOM: HTMLDivElement) => {
|
|
dividerDOM.style.cursor = 'col-resize';
|
|
|
|
const {parentNode} = dividerDOM;
|
|
|
|
if (!parentNode) {
|
|
return;
|
|
}
|
|
|
|
const container = parentNode as HTMLDivElement;
|
|
container.style.display = 'block';
|
|
});
|
|
};
|
|
|
|
onDragMove = (event: MouseEvent) => {
|
|
if (!this.isDragging || event.type !== 'mousemove' || !this.hasInteractiveLayer()) {
|
|
return;
|
|
}
|
|
|
|
const rect = rectOfContent(this.props.interactiveLayerRef.current!);
|
|
|
|
// mouse x-coordinate relative to the interactive layer's left side
|
|
const rawMouseX = (event.pageX - rect.x) / rect.width;
|
|
|
|
const min = 0;
|
|
const max = 1;
|
|
|
|
// clamp rawMouseX to be within [0, 1]
|
|
this.dividerHandlePosition = clamp(rawMouseX, min, max);
|
|
|
|
const dividerHandlePositionString = toPercent(this.dividerHandlePosition);
|
|
|
|
selectRefs(this.ghostDividerLineRefs, (dividerDOM: HTMLDivElement) => {
|
|
const {parentNode} = dividerDOM;
|
|
|
|
if (!parentNode) {
|
|
return;
|
|
}
|
|
|
|
const container = parentNode as HTMLDivElement;
|
|
|
|
container.style.width = `calc(${dividerHandlePositionString} + 0.5px)`;
|
|
});
|
|
};
|
|
|
|
onDragEnd = (event: MouseEvent) => {
|
|
if (!this.isDragging || event.type !== 'mouseup' || !this.hasInteractiveLayer()) {
|
|
return;
|
|
}
|
|
|
|
// remove listeners that were attached in onDragStart
|
|
|
|
this.cleanUpListeners();
|
|
|
|
// restore body styles
|
|
|
|
if (this.previousUserSelect) {
|
|
setBodyUserSelect(this.previousUserSelect);
|
|
this.previousUserSelect = null;
|
|
}
|
|
|
|
// indicate drag has ended
|
|
|
|
this.isDragging = false;
|
|
|
|
this.setHover(false);
|
|
|
|
selectRefs(this.dividerLineRefs, (dividerDOM: HTMLDivElement) => {
|
|
dividerDOM.style.backgroundColor = '';
|
|
dividerDOM.style.cursor = '';
|
|
});
|
|
|
|
selectRefs(this.ghostDividerLineRefs, (dividerDOM: HTMLDivElement) => {
|
|
dividerDOM.style.cursor = '';
|
|
|
|
const {parentNode} = dividerDOM;
|
|
|
|
if (!parentNode) {
|
|
return;
|
|
}
|
|
|
|
const container = parentNode as HTMLDivElement;
|
|
container.style.display = 'none';
|
|
});
|
|
|
|
this.setState({
|
|
// commit dividerHandlePosition to be dividerPosition
|
|
dividerPosition: this.dividerHandlePosition,
|
|
});
|
|
};
|
|
|
|
cleanUpListeners = () => {
|
|
if (this.isDragging) {
|
|
// we only remove listeners during a drag
|
|
window.removeEventListener('mousemove', this.onDragMove);
|
|
window.removeEventListener('mouseup', this.onDragEnd);
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const childrenProps = {
|
|
dividerPosition: this.state.dividerPosition,
|
|
setHover: this.setHover,
|
|
onDragStart: this.onDragStart,
|
|
addDividerLineRef: this.addDividerLineRef,
|
|
addGhostDividerLineRef: this.addGhostDividerLineRef,
|
|
};
|
|
|
|
// NOTE: <DividerManagerContext.Provider /> will not re-render its children
|
|
// - if the `value` prop changes, and
|
|
// - if the `children` prop stays the same
|
|
//
|
|
// Thus, only <DividerManagerContext.Consumer /> components will re-render.
|
|
// This is an optimization for when childrenProps changes, but this.props does not change.
|
|
//
|
|
// We prefer to minimize the amount of top-down prop drilling from this component
|
|
// to the respective divider components.
|
|
|
|
return (
|
|
<DividerManagerContext.Provider value={childrenProps}>
|
|
{this.props.children}
|
|
</DividerManagerContext.Provider>
|
|
);
|
|
}
|
|
}
|
|
|
|
export const Consumer = DividerManagerContext.Consumer;
|