123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import {Component, createContext, createRef} from 'react';
- import {
- clamp,
- rectOfContent,
- toPercent,
- } from 'sentry/components/performance/waterfall/utils';
- import {setBodyUserSelect, UserSelectValues} 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;
|