dragManager.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import {Component} from 'react';
  2. import {rectOfContent} from 'sentry/components/performance/waterfall/utils';
  3. import clamp from 'sentry/utils/number/clamp';
  4. import {PerformanceInteraction} from 'sentry/utils/performanceForSentry';
  5. import type {UserSelectValues} from 'sentry/utils/userselect';
  6. import {setBodyUserSelect} from 'sentry/utils/userselect';
  7. // we establish the minimum window size so that the window size of 0% is not possible
  8. const MINIMUM_WINDOW_SIZE = 0.5 / 100; // 0.5% window size
  9. enum ViewHandleType {
  10. LEFT,
  11. RIGHT,
  12. }
  13. export type DragManagerChildrenProps = {
  14. // handles
  15. isDragging: boolean;
  16. // between 0 to 1
  17. // window selection
  18. isWindowSelectionDragging: boolean;
  19. leftHandlePosition: number;
  20. // left-side handle
  21. onLeftHandleDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
  22. // between 0 to 1
  23. // right-side handle
  24. onRightHandleDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
  25. onWindowSelectionDragStart: (
  26. event: React.MouseEvent<HTMLDivElement, MouseEvent>
  27. ) => void;
  28. rightHandlePosition: number;
  29. // between 0 to 1
  30. viewWindowEnd: number;
  31. // window sizes
  32. viewWindowStart: number;
  33. // between 0 (0%) and 1 (100%)
  34. windowSelectionCurrent: number;
  35. windowSelectionInitial: number;
  36. // between 0 (0%) and 1 (100%)
  37. windowSelectionSize: number; // between 0 to 1
  38. };
  39. type DragManagerProps = {
  40. children: (props: DragManagerChildrenProps) => JSX.Element;
  41. // this is the DOM element where the drag events occur. it's also the reference point
  42. // for calculating the relative mouse x coordinate.
  43. interactiveLayerRef: React.RefObject<HTMLDivElement>;
  44. };
  45. type DragManagerState = {
  46. currentDraggingHandle: ViewHandleType | undefined;
  47. // draggable handles
  48. isDragging: boolean;
  49. // window selection
  50. isWindowSelectionDragging: boolean;
  51. leftHandlePosition: number;
  52. rightHandlePosition: number;
  53. viewWindowEnd: number;
  54. // window sizes
  55. viewWindowStart: number;
  56. windowSelectionCurrent: number;
  57. windowSelectionInitial: number;
  58. windowSelectionSize: number;
  59. };
  60. class DragManager extends Component<DragManagerProps, DragManagerState> {
  61. state: DragManagerState = {
  62. // draggable handles
  63. isDragging: false,
  64. currentDraggingHandle: void 0,
  65. leftHandlePosition: 0, // positioned on the left-most side at 0%
  66. rightHandlePosition: 1, // positioned on the right-most side at 100%
  67. // window selection
  68. isWindowSelectionDragging: false,
  69. windowSelectionInitial: 0, // between 0 (0%) and 1 (100%)
  70. windowSelectionCurrent: 0, // between 0 (0%) and 1 (100%)
  71. windowSelectionSize: 0,
  72. // window sizes
  73. viewWindowStart: 0,
  74. viewWindowEnd: 1,
  75. };
  76. componentWillUnmount() {
  77. this.cleanUpListeners();
  78. }
  79. previousUserSelect: UserSelectValues | null = null;
  80. hasInteractiveLayer = (): boolean => !!this.props.interactiveLayerRef.current;
  81. onDragStart =
  82. (viewHandle: ViewHandleType) =>
  83. (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
  84. const isDragging = this.state.isDragging || this.state.isWindowSelectionDragging;
  85. if (isDragging || event.type !== 'mousedown' || !this.hasInteractiveLayer()) {
  86. return;
  87. }
  88. PerformanceInteraction.startInteraction('SpanTreeHandleDrag');
  89. // prevent the user from selecting things outside the minimap when dragging
  90. // the mouse cursor outside the minimap
  91. this.previousUserSelect = setBodyUserSelect({
  92. userSelect: 'none',
  93. MozUserSelect: 'none',
  94. msUserSelect: 'none',
  95. webkitUserSelect: 'none',
  96. });
  97. // attach event listeners so that the mouse cursor can drag outside of the
  98. // minimap
  99. window.addEventListener('mousemove', this.onDragMove);
  100. window.addEventListener('mouseup', this.onDragEnd);
  101. // indicate drag has begun
  102. this.setState({
  103. isDragging: true,
  104. isWindowSelectionDragging: false,
  105. currentDraggingHandle: viewHandle,
  106. });
  107. };
  108. onLeftHandleDragStart = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
  109. this.onDragStart(ViewHandleType.LEFT)(event);
  110. };
  111. onRightHandleDragStart = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
  112. this.onDragStart(ViewHandleType.RIGHT)(event);
  113. };
  114. onDragMove = (event: MouseEvent) => {
  115. if (
  116. !this.state.isDragging ||
  117. event.type !== 'mousemove' ||
  118. !this.hasInteractiveLayer()
  119. ) {
  120. return;
  121. }
  122. const rect = rectOfContent(this.props.interactiveLayerRef.current!);
  123. // mouse x-coordinate relative to the interactive layer's left side
  124. const rawMouseX = (event.pageX - rect.x) / rect.width;
  125. switch (this.state.currentDraggingHandle) {
  126. case ViewHandleType.LEFT: {
  127. const min = 0;
  128. const max = this.state.rightHandlePosition - MINIMUM_WINDOW_SIZE;
  129. this.setState({
  130. // clamp rawMouseX to be within [0, rightHandlePosition - MINIMUM_WINDOW_SIZE]
  131. leftHandlePosition: clamp(rawMouseX, min, max),
  132. });
  133. break;
  134. }
  135. case ViewHandleType.RIGHT: {
  136. const min = this.state.leftHandlePosition + MINIMUM_WINDOW_SIZE;
  137. const max = 1;
  138. this.setState({
  139. // clamp rawMouseX to be within [leftHandlePosition + MINIMUM_WINDOW_SIZE, 1]
  140. rightHandlePosition: clamp(rawMouseX, min, max),
  141. });
  142. break;
  143. }
  144. default: {
  145. throw Error('this.state.currentDraggingHandle is undefined');
  146. }
  147. }
  148. };
  149. onDragEnd = (event: MouseEvent) => {
  150. if (
  151. !this.state.isDragging ||
  152. event.type !== 'mouseup' ||
  153. !this.hasInteractiveLayer()
  154. ) {
  155. return;
  156. }
  157. PerformanceInteraction.finishInteraction();
  158. // remove listeners that were attached in onDragStart
  159. this.cleanUpListeners();
  160. // restore body styles
  161. if (this.previousUserSelect) {
  162. setBodyUserSelect(this.previousUserSelect);
  163. this.previousUserSelect = null;
  164. }
  165. // indicate drag has ended
  166. switch (this.state.currentDraggingHandle) {
  167. case ViewHandleType.LEFT: {
  168. this.setState(state => ({
  169. isDragging: false,
  170. currentDraggingHandle: void 0,
  171. // commit leftHandlePosition to be viewWindowStart
  172. viewWindowStart: state.leftHandlePosition,
  173. }));
  174. return;
  175. }
  176. case ViewHandleType.RIGHT: {
  177. this.setState(state => ({
  178. isDragging: false,
  179. currentDraggingHandle: void 0,
  180. // commit rightHandlePosition to be viewWindowEnd
  181. viewWindowEnd: state.rightHandlePosition,
  182. }));
  183. return;
  184. }
  185. default: {
  186. throw Error('this.state.currentDraggingHandle is undefined');
  187. }
  188. }
  189. };
  190. onWindowSelectionDragStart = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
  191. const isDragging = this.state.isDragging || this.state.isWindowSelectionDragging;
  192. if (isDragging || event.type !== 'mousedown' || !this.hasInteractiveLayer()) {
  193. return;
  194. }
  195. PerformanceInteraction.startInteraction('SpanTreeWindowDrag');
  196. // prevent the user from selecting things outside the minimap when dragging
  197. // the mouse cursor outside the minimap
  198. this.previousUserSelect = setBodyUserSelect({
  199. userSelect: 'none',
  200. MozUserSelect: 'none',
  201. msUserSelect: 'none',
  202. webkitUserSelect: 'none',
  203. });
  204. // attach event listeners so that the mouse cursor can drag outside of the
  205. // minimap
  206. window.addEventListener('mousemove', this.onWindowSelectionDragMove);
  207. window.addEventListener('mouseup', this.onWindowSelectionDragEnd);
  208. // indicate drag has begun
  209. const rect = rectOfContent(this.props.interactiveLayerRef.current!);
  210. // mouse x-coordinate relative to the interactive layer's left side
  211. const rawMouseX = (event.pageX - rect.x) / rect.width;
  212. this.setState({
  213. isDragging: false,
  214. isWindowSelectionDragging: true,
  215. windowSelectionInitial: rawMouseX, // between 0 (0%) and 1 (100%)
  216. windowSelectionCurrent: rawMouseX, // between 0 (0%) and 1 (100%)
  217. });
  218. };
  219. onWindowSelectionDragMove = (event: MouseEvent) => {
  220. if (
  221. !this.state.isWindowSelectionDragging ||
  222. event.type !== 'mousemove' ||
  223. !this.hasInteractiveLayer()
  224. ) {
  225. return;
  226. }
  227. const rect = rectOfContent(this.props.interactiveLayerRef.current!);
  228. // mouse x-coordinate relative to the interactive layer's left side
  229. const rawMouseX = (event.pageX - rect.x) / rect.width;
  230. const min = 0;
  231. const max = 1;
  232. // clamp rawMouseX to be within [0, 1]
  233. const windowSelectionCurrent = clamp(rawMouseX, min, max);
  234. const windowSelectionSize = clamp(
  235. Math.abs(this.state.windowSelectionInitial - windowSelectionCurrent),
  236. min,
  237. max
  238. );
  239. this.setState({
  240. windowSelectionCurrent,
  241. windowSelectionSize,
  242. });
  243. };
  244. onWindowSelectionDragEnd = (event: MouseEvent) => {
  245. if (
  246. !this.state.isWindowSelectionDragging ||
  247. event.type !== 'mouseup' ||
  248. !this.hasInteractiveLayer()
  249. ) {
  250. return;
  251. }
  252. PerformanceInteraction.finishInteraction();
  253. // remove listeners that were attached in onWindowSelectionDragStart
  254. this.cleanUpListeners();
  255. // restore body styles
  256. if (this.previousUserSelect) {
  257. setBodyUserSelect(this.previousUserSelect);
  258. this.previousUserSelect = null;
  259. }
  260. // indicate drag has ended
  261. this.setState(state => {
  262. let viewWindowStart = Math.min(
  263. state.windowSelectionInitial,
  264. state.windowSelectionCurrent
  265. );
  266. let viewWindowEnd = Math.max(
  267. state.windowSelectionInitial,
  268. state.windowSelectionCurrent
  269. );
  270. // enforce minimum window size
  271. if (viewWindowEnd - viewWindowStart < MINIMUM_WINDOW_SIZE) {
  272. viewWindowEnd = viewWindowStart + MINIMUM_WINDOW_SIZE;
  273. if (viewWindowEnd > 1) {
  274. viewWindowEnd = 1;
  275. viewWindowStart = 1 - MINIMUM_WINDOW_SIZE;
  276. }
  277. }
  278. return {
  279. isWindowSelectionDragging: false,
  280. windowSelectionInitial: 0,
  281. windowSelectionCurrent: 0,
  282. windowSelectionSize: 0,
  283. leftHandlePosition: viewWindowStart,
  284. rightHandlePosition: viewWindowEnd,
  285. viewWindowStart,
  286. viewWindowEnd,
  287. };
  288. });
  289. };
  290. cleanUpListeners = () => {
  291. if (this.state.isDragging) {
  292. window.removeEventListener('mousemove', this.onDragMove);
  293. window.removeEventListener('mouseup', this.onDragEnd);
  294. }
  295. if (this.state.isWindowSelectionDragging) {
  296. window.removeEventListener('mousemove', this.onWindowSelectionDragMove);
  297. window.removeEventListener('mouseup', this.onWindowSelectionDragEnd);
  298. }
  299. };
  300. render() {
  301. const childrenProps = {
  302. isDragging: this.state.isDragging,
  303. // left handle
  304. onLeftHandleDragStart: this.onLeftHandleDragStart,
  305. leftHandlePosition: this.state.leftHandlePosition,
  306. // right handle
  307. onRightHandleDragStart: this.onRightHandleDragStart,
  308. rightHandlePosition: this.state.rightHandlePosition,
  309. // window selection
  310. isWindowSelectionDragging: this.state.isWindowSelectionDragging,
  311. windowSelectionInitial: this.state.windowSelectionInitial,
  312. windowSelectionCurrent: this.state.windowSelectionCurrent,
  313. windowSelectionSize: this.state.windowSelectionSize,
  314. onWindowSelectionDragStart: this.onWindowSelectionDragStart,
  315. // window sizes
  316. viewWindowStart: this.state.viewWindowStart,
  317. viewWindowEnd: this.state.viewWindowEnd,
  318. };
  319. return this.props.children(childrenProps);
  320. }
  321. }
  322. export default DragManager;