Skip to content

Commit b8e04b0

Browse files
Merge pull request #856 from aaclage/Feature/DragDropFilesControl
DragDropFiles Control and integration with ListView and FilePicker
2 parents 659d324 + 3a74e48 commit b8e04b0

19 files changed

+596
-86
lines changed
11.1 KB
Loading
18.2 KB
Loading
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# DragDropFiles
2+
3+
This control allows to drag and drop files in pre defined areas.
4+
5+
## How to use this control in your solutions
6+
7+
- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency.
8+
- Import the following modules to your component:
9+
10+
```TypeScript
11+
import { DragDropFiles } from "@pnp/spfx-controls-react/lib/DragDropFiles";
12+
```
13+
14+
- Use the DragDropFiles control in your code as follows:
15+
16+
```jsx
17+
<DragDropFiles
18+
dropEffect="copy"
19+
enable={true}
20+
onDrop={this._getDropFiles}
21+
iconName="Upload"
22+
labelMessage= "My custom upload File"
23+
>
24+
{/* Specify the components to load where Drag and drop area should work */}
25+
</DragDropFiles>
26+
```
27+
**Content with drag and drop applied**
28+
29+
```jsx
30+
<DragDropFiles
31+
dropEffect="copy"
32+
enable={true}
33+
onDrop={this._getDropFiles}
34+
iconName="Upload"
35+
labelMessage= "My custom upload File"
36+
>
37+
Drag and drop here...
38+
39+
</DragDropFiles>
40+
```
41+
![Custom html with drag and drop](../assets/DragDropFilesSample1.png)
42+
43+
**ListView with drag and drop applied**
44+
45+
![ListView control with drag and drop Control](../assets/ListView-DragDrop.png)
46+
47+
**FilePicker with drag and drop applied**
48+
49+
![FilePicker control with grouping](../assets/DragDropFilesSample2.png)
50+
51+
- With the `onDrop` handler you can define a method that returns files and files inside folders that where drag and drop by user.
52+
53+
**PS: New property "fullPath" was included in file object to allow identify dropped files based on Folders, this allow users to create associated folder path.**
54+
55+
```typescript
56+
private _getDropFiles = (files) => {
57+
for (var i = 0; i < files.length; i++) {
58+
console.log("Filename: " + files[i].name);
59+
console.log("Path: " + files[i].fullPath);
60+
}
61+
}
62+
```
63+
64+
## Implementation
65+
66+
The `DragDropFiles` can be configured with the following properties:
67+
68+
| Property | Type | Required | Description |
69+
| ---- | ---- | ---- | ---- |
70+
| dropEffect | string | no | Visual feedback given to user during a drag and drop operation (copy,move,link,none). Default value is `copy`. |
71+
| enable | boolean | no | Option allow control to be enable or disable. Default value is `true`|
72+
| labelMessage | string | no | Message displayed in drag drop preview. |
73+
| onDrop | any | no | Method that returns all Files[] from drag and drop file area. |
74+
| iconName | string | no | Icon Name from Office UI Fabric Icons. |
75+
76+
77+
78+
![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/DragDropFiles)

docs/documentation/docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ The following controls are currently available:
5959
- [Charts](./controls/ChartControl) (makes it easy to integrate [Chart.js](https://www.chartjs.org/) charts into web part)
6060
- [ComboBoxListItemPicker](./controls/ComboBoxListItemPicker) (allows to select one or more items from a list)
6161
- [Dashboard](./controls/Dashboard) (Control to render dashboard in Microsoft Teams)
62+
- [DragDropFiles](./controls/DragDropFiles) (Allow drag and drop of files in selected areas)
6263
- [DateTimePicker](./controls/DateTimePicker) (DateTime Picker)
6364
- [FieldCollectionData](./controls/FieldCollectionData) (control gives you the ability to insert a list / collection data which can be used in your web part / application customizer)
6465
- [FilePicker](./controls/FilePicker) (control that allows to browse and select a file from various places)

src/DragDropFiles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './controls/dragDropFiles/index';

src/controls/listView/ListView.DragDrop.module.scss renamed to src/controls/dragDropFiles/DragDropFiles.module.scss

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
.DragDropArea {
1+
.DragDropFilesArea {
22
position: relative;
33
min-height: 150px;
44
}
55

6-
.DragDropAreaBorder {
6+
.DragDropFilesAreaLeave {
7+
position: relative;
8+
}
9+
10+
.DragDropFilesAreaBorder {
711
border: dashed grey 1px;
812
background-color: rgba(255,255,255,.6);
913
position: absolute;
@@ -14,7 +18,7 @@
1418
z-index: 1;
1519
}
1620

17-
.DragDropAreaZone {
21+
.DragDropFilesAreaZone {
1822
position: absolute;
1923
top: 35%;
2024
right: 0;
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import * as React from 'react';
2+
import styles from './DragDropFiles.module.scss';
3+
import { Icon } from 'office-ui-fabric-react/lib/Icon';
4+
import { IDragDropFilesState, IDragDropFilesProps } from './IDragDropFiles';
5+
import * as strings from 'ControlStrings';
6+
7+
/**
8+
* DragDropFiles Class Control
9+
*/
10+
export class DragDropFiles extends React.Component<IDragDropFilesProps, IDragDropFilesState> {
11+
private dragCounter = 0;
12+
private dropRef = React.createRef<HTMLDivElement>();
13+
private _LabelMessage;
14+
private _IconName;
15+
private _dropEffect;
16+
private _enable;
17+
18+
constructor(props: IDragDropFilesProps) {
19+
super(props);
20+
// Initialize state
21+
this.state = {
22+
dragStatus: false
23+
};
24+
25+
}
26+
27+
/**
28+
* Lifecycle hook when component is mounted
29+
*/
30+
public componentDidMount(): void {
31+
const { dropEffect, enable } = this.props;
32+
if (dropEffect == undefined || dropEffect == "") {
33+
this._dropEffect = "copy";
34+
} else {
35+
this._dropEffect = dropEffect;
36+
}
37+
if (enable == undefined) {
38+
this._enable = true;
39+
} else {
40+
this._enable = enable;
41+
}
42+
// Add EventListeners for drag zone area
43+
let divDropArea = this.dropRef.current;
44+
if(this._enable == true)
45+
{
46+
divDropArea.addEventListener('dragenter', this.handleonDragEnter);
47+
divDropArea.addEventListener('dragleave', this.handleonDragLeave);
48+
divDropArea.addEventListener('dragover', this.handleonDragOver);
49+
divDropArea.addEventListener('drop', this.handleonDrop);
50+
}
51+
}
52+
53+
/**
54+
* Stop listeners from onDragOver event.
55+
* @param e
56+
*/
57+
private handleonDragOver = (e) => {
58+
e.preventDefault();
59+
e.stopPropagation();
60+
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
61+
e.dataTransfer.dropEffect = e.dataTransfer.dropEffect = this.props.dropEffect;
62+
}
63+
}
64+
/**
65+
* Stop listeners from onDragEnter event, enable drag and drop view.
66+
* @param e
67+
*/
68+
private handleonDragEnter = (e) => {
69+
e.preventDefault();
70+
e.stopPropagation();
71+
this.dragCounter++;
72+
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
73+
e.dataTransfer.dropEffect = this._dropEffect;
74+
this.setState({ dragStatus: true });
75+
}
76+
}
77+
/**
78+
* Stop listeners from ondragenter event, disable drag and drop view.
79+
* @param e
80+
*/
81+
private handleonDragLeave = (e) => {
82+
e.preventDefault();
83+
e.stopPropagation();
84+
this.dragCounter--;
85+
if (this.dragCounter === 0) {
86+
this.setState({ dragStatus: false });
87+
}
88+
}
89+
/**
90+
* Stop listeners from onDrop event and load files to property onDrop.
91+
* @param e
92+
*/
93+
private handleonDrop = async (e) => {
94+
e.preventDefault();
95+
e.stopPropagation();
96+
this.setState({ dragStatus: false });
97+
if (e.dataTransfer && e.dataTransfer.items) {
98+
this.props.onDrop(await this.getFilesAsync(e));
99+
}
100+
e.dataTransfer.clearData();
101+
this.dragCounter = 0;
102+
}
103+
104+
/**
105+
* Add File to Array Files of type File[]
106+
* https://www.meziantou.net/upload-files-and-directories-using-an-input-drag-and-drop-or-copy-and-paste-with.htm
107+
* @param dataTransfer
108+
*/
109+
private getFilesAsync = async (e) => {
110+
const Customfiles = e.dataTransfer;
111+
const items = Customfiles.items;
112+
const Directory = [];
113+
let entry:any;
114+
const files: File[] = [];
115+
for (let i = 0; i < items.length; i++) {
116+
const item = items[i];
117+
if (item.kind === "file") {
118+
/**
119+
* This method retrieves Files from Folders
120+
* defensive code to only use method when exist in browser if not only return files.
121+
* https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
122+
*/
123+
if (item.getAsEntry) {
124+
entry = item.getAsEntry();
125+
if (entry.isDirectory) {
126+
Directory.push(entry);
127+
} else {
128+
const file = item.getAsFile();
129+
if (file) {
130+
file.fullPath = "";
131+
files.push(file);
132+
}
133+
}
134+
} else if (item.webkitGetAsEntry) {
135+
entry = item.webkitGetAsEntry();
136+
if (entry.isDirectory) {
137+
Directory.push(entry);
138+
} else {
139+
const file = item.getAsFile();
140+
if (file) {
141+
file.fullPath = "";
142+
files.push(file);
143+
}
144+
}
145+
} else if ("function" == typeof item.getAsFile) {
146+
const file = item.getAsFile();
147+
if (file) {
148+
file.fullPath = "";
149+
files.push(file);
150+
}
151+
}
152+
continue;
153+
}
154+
}
155+
if (Directory.length > 0) {
156+
const entryContent = await this.readEntryContentAsync(Directory);
157+
files.push(...entryContent);
158+
}
159+
return files;
160+
}
161+
162+
// Returns a promise with all the files of the directory hierarchy
163+
/**
164+
*
165+
* @param entry
166+
*/
167+
private readEntryContentAsync = (Directory) => {
168+
return new Promise<File[]>((resolve, reject) => {
169+
let reading = 0;
170+
const contents: File[] = [];
171+
Directory.forEach(entry => {
172+
readEntry(entry, "");
173+
});
174+
175+
function readEntry(entry, path) {
176+
if (entry.isDirectory) {
177+
readReaderContent(entry.createReader());
178+
} else {
179+
reading++;
180+
entry.file(file => {
181+
reading--;
182+
file.fullPath = path;
183+
contents.push(file);
184+
185+
if (reading === 0) {
186+
resolve(contents);
187+
}
188+
});
189+
}
190+
}
191+
192+
function readReaderContent(reader) {
193+
reading++;
194+
195+
reader.readEntries((entries) => {
196+
reading--;
197+
for (const entry of entries) {
198+
readEntry(entry, entry.fullPath);
199+
}
200+
201+
if (reading === 0) {
202+
resolve(contents);
203+
}
204+
});
205+
}
206+
});
207+
}
208+
209+
/**
210+
* Default React component render method
211+
*/
212+
public render(): React.ReactElement<IDragDropFilesProps> {
213+
let { dragStatus } = this.state;
214+
let { labelMessage, iconName } = this.props;
215+
216+
if (labelMessage == undefined || labelMessage == "") {
217+
this._LabelMessage = "";
218+
} else {
219+
this._LabelMessage = labelMessage;
220+
}
221+
if (iconName == undefined || iconName == "") {
222+
this._IconName = "";
223+
} else {
224+
this._IconName = iconName;
225+
}
226+
return (
227+
<div className={(dragStatus && this._enable) ? styles.DragDropFilesArea : styles.DragDropFilesAreaLeave}
228+
ref={this.dropRef}>
229+
{(dragStatus && this._enable) &&
230+
<div className={styles.DragDropFilesAreaBorder}>
231+
<div className={styles.DragDropFilesAreaZone}>
232+
<Icon iconName={this._IconName} className="ms-IconExample" />
233+
<div>{this._LabelMessage}</div>
234+
</div>
235+
</div>
236+
}
237+
{this.props.children}
238+
</div>
239+
);
240+
}
241+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export interface IDragDropFilesProps {
2+
/**
3+
* Specifies the dropEffect, default is 'none'
4+
* https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect
5+
*/
6+
dropEffect?: string;
7+
/**
8+
* Specifies the label of the file picker button
9+
*/
10+
labelMessage?: string;
11+
12+
/**
13+
* Specifies the icon to display
14+
*/
15+
iconName?: string;
16+
/**
17+
* Handler to return the files from drag and drop.
18+
**/
19+
onDrop?: any;
20+
/**
21+
* Specify if drag and drop option is enable.
22+
**/
23+
enable?: boolean;
24+
}
25+
26+
export interface IDragDropFilesState {
27+
dragStatus?: boolean;
28+
}

src/controls/dragDropFiles/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './IDragDropFiles';
2+
export * from './DragDropFiles';

0 commit comments

Comments
 (0)