dragManager.tsx 11 KB

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