-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathindex.js
205 lines (178 loc) · 7.41 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';
import variables from '../../styles/variables';
import DragAndDropPropTypes from './dragAndDropPropTypes';
import withNavigationFocus from '../withNavigationFocus';
const COPY_DROP_EFFECT = 'copy';
const NONE_DROP_EFFECT = 'none';
const propTypes = {
...DragAndDropPropTypes,
/** Callback to fire when a file has being dragged over the text input & report body. This prop is necessary to be inlined to satisfy the linter */
onDragOver: DragAndDropPropTypes.onDragOver,
/** Guard for accepting drops in drop zone. Drag event is passed to this function as first parameter. This prop is necessary to be inlined to satisfy the linter */
shouldAcceptDrop: PropTypes.func,
/** Whether drag & drop should be disabled */
disabled: PropTypes.bool,
/** Rendered child component */
children: PropTypes.node.isRequired,
};
const defaultProps = {
onDragOver: () => {},
shouldAcceptDrop: (e) => {
if (e.dataTransfer.types) {
for (let i = 0; i < e.dataTransfer.types.length; i++) {
if (e.dataTransfer.types[i] === 'Files') {
return true;
}
}
}
return false;
},
disabled: false,
};
class DragAndDrop extends React.Component {
constructor(props) {
super(props);
this.throttledDragOverHandler = _.throttle(this.dragOverHandler.bind(this), 100);
this.throttledDragNDropWindowResizeListener = _.throttle(this.dragNDropWindowResizeListener.bind(this), 100);
this.dropZoneDragHandler = this.dropZoneDragHandler.bind(this);
this.dropZoneDragListener = this.dropZoneDragListener.bind(this);
/*
Last detected drag state on the dropzone -> we start with dragleave since user is not dragging initially.
This state is updated when drop zone is left/entered entirely(not taking the children in the account) or entire window is left
*/
this.dropZoneDragState = 'dragleave';
}
componentDidMount() {
if (this.props.disabled) {
return;
}
this.addEventListeners();
}
componentDidUpdate(prevProps) {
if (this.props.isFocused !== prevProps.isFocused) {
if (!this.props.isFocused) {
this.removeEventListeners();
} else {
this.addEventListeners();
}
}
const isDisabled = this.props.disabled;
if (isDisabled === prevProps.disabled) {
return;
}
if (isDisabled) {
this.removeEventListeners();
} else {
this.addEventListeners();
}
}
componentWillUnmount() {
if (this.props.disabled) {
return;
}
this.removeEventListeners();
}
addEventListeners() {
this.dropZone = document.getElementById(this.props.dropZoneId);
this.dropZoneRect = this.calculateDropZoneClientReact();
document.addEventListener('dragover', this.dropZoneDragListener);
document.addEventListener('dragenter', this.dropZoneDragListener);
document.addEventListener('dragleave', this.dropZoneDragListener);
document.addEventListener('drop', this.dropZoneDragListener);
window.addEventListener('resize', this.throttledDragNDropWindowResizeListener);
}
removeEventListeners() {
document.removeEventListener('dragover', this.dropZoneDragListener);
document.removeEventListener('dragenter', this.dropZoneDragListener);
document.removeEventListener('dragleave', this.dropZoneDragListener);
document.removeEventListener('drop', this.dropZoneDragListener);
window.removeEventListener('resize', this.throttledDragNDropWindowResizeListener);
}
/**
* @param {Object} event native Event
*/
dragOverHandler(event) {
this.props.onDragOver(event);
}
dragNDropWindowResizeListener() {
// Update bounding client rect on window resize
this.dropZoneRect = this.calculateDropZoneClientReact();
}
calculateDropZoneClientReact() {
const boundingClientRect = this.dropZone.getBoundingClientRect();
// Handle edge case when we are under responsive breakpoint the browser doesn't normalize rect.left to 0 and rect.right to window.innerWidth
return {
width: boundingClientRect.width,
left: window.innerWidth <= variables.mobileResponsiveWidthBreakpoint ? 0 : boundingClientRect.left,
right: window.innerWidth <= variables.mobileResponsiveWidthBreakpoint ? window.innerWidth : boundingClientRect.right,
top: boundingClientRect.top,
bottom: boundingClientRect.bottom,
};
}
/**
* @param {Object} event native Event
*/
dropZoneDragHandler(event) {
// Setting dropEffect for dragover is required for '+' icon on certain platforms/browsers (eg. Safari)
switch (event.type) {
case 'dragover':
// Continuous event -> can hurt performance, be careful when subscribing
// eslint-disable-next-line no-param-reassign
event.dataTransfer.dropEffect = COPY_DROP_EFFECT;
this.throttledDragOverHandler(event);
break;
case 'dragenter':
// Avoid reporting onDragEnter for children views -> not performant
if (this.dropZoneDragState === 'dragleave') {
this.dropZoneDragState = 'dragenter';
// eslint-disable-next-line no-param-reassign
event.dataTransfer.dropEffect = COPY_DROP_EFFECT;
this.props.onDragEnter(event);
}
break;
case 'dragleave':
if (this.dropZoneDragState === 'dragenter') {
if (
event.clientY <= this.dropZoneRect.top ||
event.clientY >= this.dropZoneRect.bottom ||
event.clientX <= this.dropZoneRect.left ||
event.clientX >= this.dropZoneRect.right ||
// Cancel drag when file manager is on top of the drop zone area - works only on chromium
(event.target.getAttribute('id') === this.props.activeDropZoneId && !event.relatedTarget)
) {
this.dropZoneDragState = 'dragleave';
this.props.onDragLeave(event);
}
}
break;
case 'drop':
this.dropZoneDragState = 'dragleave';
this.props.onDrop(event);
break;
default:
break;
}
}
/**
* Handles all types of drag-N-drop events on the drop zone associated with composer
*
* @param {Object} event native Event
*/
dropZoneDragListener(event) {
event.preventDefault();
if (this.dropZone.contains(event.target) && this.props.shouldAcceptDrop(event)) {
this.dropZoneDragHandler(event);
} else {
// eslint-disable-next-line no-param-reassign
event.dataTransfer.dropEffect = NONE_DROP_EFFECT;
}
}
render() {
return this.props.children;
}
}
DragAndDrop.propTypes = propTypes;
DragAndDrop.defaultProps = defaultProps;
export default withNavigationFocus(DragAndDrop);