dragManager.tsx 11 KB

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