diff --git a/client/src/app/App.js b/client/src/app/App.js
index c9fd1aa636..5076abd579 100644
--- a/client/src/app/App.js
+++ b/client/src/app/App.js
@@ -1,11 +1,6 @@
import React, { PureComponent } from 'react';
-import { WithCache } from './cached';
-
-import {
- Fill,
- SlotFillRoot
-} from './slot-fill';
+import debug from 'debug';
import {
assign,
@@ -13,11 +8,19 @@ import {
forEach
} from 'min-dash';
+import { WithCache } from './cached';
+
+import { DropZone } from './drop-zone';
+
+import {
+ Fill,
+ SlotFillRoot
+} from './slot-fill';
+
import Toolbar from './Toolbar';
import Log from './Log';
-import debug from 'debug';
import { ModalConductor } from './modals';
@@ -420,8 +423,7 @@ export class App extends PureComponent {
} = this.state;
const {
- dialog,
- fileSystem
+ dialog
} = globals;
const providers = tabsProvider.getProviders();
@@ -437,17 +439,7 @@ export class App extends PureComponent {
return;
}
- const files = await Promise.all(filePaths.map(async (filePath) => {
- const fileType = getFileTypeFromExtension(filePath);
-
- const provider = tabsProvider.getProvider(fileType);
-
- const encoding = provider.encoding ? provider.encoding : ENCODING_UTF8;
-
- return await fileSystem.readFile(filePath, {
- encoding
- });
- }));
+ const files = await Promise.all(filePaths.map(this.readFileFromPath));
await this.openFiles(files);
}
@@ -587,6 +579,23 @@ export class App extends PureComponent {
return openedTabs.reverse();
}
+ readFileFromPath = filePath => {
+ const {
+ globals,
+ tabsProvider
+ } = this.props;
+
+ const fileType = getFileTypeFromExtension(filePath);
+
+ const provider = tabsProvider.getProvider(fileType);
+
+ const encoding = provider.encoding ? provider.encoding : ENCODING_UTF8;
+
+ return globals.fileSystem.readFile(filePath, {
+ encoding
+ });
+ }
+
findOpenTab(file) {
const {
@@ -1295,6 +1304,14 @@ export class App extends PureComponent {
this.triggerAction('close-tab', { tabId: tab.id }).catch(console.error);
}
+ handleDrop = async (event) => {
+ const filePaths = Array.from(event.dataTransfer.files).map(({ path }) => path);
+
+ const files = await Promise.all(filePaths.map(this.readFileFromPath));
+
+ this.openFiles(files);
+ }
+
loadConfig = (key, ...args) => {
return this.props.globals.config.get(key, this.state.activeTab, ...args);
}
@@ -1351,144 +1368,152 @@ export class App extends PureComponent {
const isDirty = this.isDirty(activeTab);
return (
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- tabState.exportAs &&
+
+
- }
-
-
+
+
+ }
+
+
+
+
+
+ {
+
+ }
+
+
+
+
+
-
- {
-
- }
-
-
-
-
-
-
-
-
+
+
+
+
);
}
}
@@ -1517,6 +1542,8 @@ function missingProvider(providerType) {
class LoadingTab extends PureComponent {
+ triggerAction() {}
+
render() {
return (
diff --git a/client/src/app/drop-zone/DropZone.js b/client/src/app/drop-zone/DropZone.js
new file mode 100644
index 0000000000..216312c35e
--- /dev/null
+++ b/client/src/app/drop-zone/DropZone.js
@@ -0,0 +1,91 @@
+import React from 'react';
+
+import css from './DropZone.less';
+
+
+export class DropZone extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ draggingOver: false
+ };
+ }
+
+ handleDragOver = event => {
+ if (!this.isDragAllowed(event)) {
+ return;
+ }
+
+ event.preventDefault();
+ event.dataTransfer.dropEffect = 'copy';
+
+ if (this.state.draggingOver) {
+ return;
+ }
+
+ event.stopPropagation();
+
+ this.setState({ draggingOver: true });
+ }
+
+ /**
+ * @param {DragEvent} event
+ */
+ isDragAllowed(event) {
+ const { dataTransfer } = event;
+
+ return Array.from(dataTransfer.items)
+ .some(({ kind, type }) => type === 'file' || kind === 'file');
+ }
+
+ handleDragLeave = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.state.draggingOver && !event.relatedTarget) {
+ this.setState({ draggingOver: false });
+ }
+ }
+
+ handleDrop = async (event) => {
+ if (!this.state.draggingOver) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.setState({ draggingOver: false });
+
+ this.props.onDrop(event);
+ }
+
+ render() {
+ return (
+
+ { this.state.draggingOver ? : null }
+ { this.props.children }
+
+ );
+ }
+}
+
+DropZone.defaultProps = {
+ onDrop: () => {}
+};
+
+function DropOverlay() {
+ return (
+
+ );
+}
diff --git a/client/src/app/drop-zone/DropZone.less b/client/src/app/drop-zone/DropZone.less
new file mode 100644
index 0000000000..744eca9ad4
--- /dev/null
+++ b/client/src/app/drop-zone/DropZone.less
@@ -0,0 +1,36 @@
+:local(.DropZone) {
+ height: 100%;
+ width: 100%;
+}
+
+:local(.DropOverlay) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+
+ padding: 120px 50px;
+
+ background: rgba(255, 255, 255, 0.8);
+
+ pointer-events: none;
+
+ z-index: 1000;
+
+ .box {
+ text-align: center;
+ border: dashed 4px #DDD;
+ height: 100%;
+ vertical-align: middle;
+
+ > div {
+ font-size: 26px;
+ color: #999;
+ margin: auto;
+
+ top: 20%;
+ position: relative;
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/src/app/drop-zone/__tests__/DropZoneSpec.js b/client/src/app/drop-zone/__tests__/DropZoneSpec.js
new file mode 100644
index 0000000000..5297e71eda
--- /dev/null
+++ b/client/src/app/drop-zone/__tests__/DropZoneSpec.js
@@ -0,0 +1,176 @@
+/* global sinon */
+
+import React from 'react';
+
+import {
+ shallow
+} from 'enzyme';
+
+import { DropZone } from '../DropZone';
+
+describe('', function() {
+
+ describe('#render', function() {
+
+ it('should render', function() {
+ shallow();
+ });
+
+ });
+
+
+ describe('#handleDragOver', function() {
+
+ it('should not render overlay during drag without file', function() {
+
+ // given
+ const wrapper = shallow();
+
+ // when
+ const event = new MockDragEvent();
+
+ wrapper.simulate('dragover', event);
+
+ // then
+ expect(wrapper.find('DropOverlay').exists()).to.be.false;
+
+ });
+
+
+ it('should render overlay during drag with a file', function() {
+
+ // given
+ const wrapper = shallow();
+
+ // when
+ const event = new MockDragEvent({
+ type: 'file'
+ });
+
+ wrapper.simulate('dragover', event);
+
+ // then
+ expect(wrapper.find('DropOverlay').exists()).to.be.true;
+
+ });
+
+ });
+
+
+ describe('#handleDragLeave', function() {
+
+ it('should not render overlay when drag is over', function() {
+
+ // given
+ const wrapper = shallow();
+
+ const dragOverEvent = new MockDragEvent({
+ type: 'file'
+ });
+ const dragLeaveEvent = new MockDragEvent({
+ type: 'file'
+ });
+
+ dragLeaveEvent.relatedTarget = null;
+
+ // when
+ wrapper.simulate('dragover', dragOverEvent);
+ wrapper.simulate('dragleave', dragLeaveEvent);
+
+ // then
+ expect(wrapper.find('DropOverlay').exists()).to.be.false;
+
+ });
+
+
+ it('should render overlay when dragging over elements', function() {
+
+ // given
+ const wrapper = shallow();
+
+ const dragOverEvent = new MockDragEvent({
+ type: 'file'
+ });
+ const dragLeaveEvent = new MockDragEvent({
+ type: 'file'
+ });
+
+ dragLeaveEvent.relatedTarget = document.createElement('div');
+
+ // when
+ wrapper.simulate('dragover', dragOverEvent);
+ wrapper.simulate('dragleave', dragLeaveEvent);
+
+ // then
+ expect(wrapper.find('DropOverlay').exists()).to.be.true;
+
+ });
+
+ });
+
+
+ describe('#handleDrop', function() {
+
+ it('should not render overlay when file is dropped', function() {
+
+ // given
+ const wrapper = shallow();
+
+ const dragOverEvent = new MockDragEvent({
+ type: 'file'
+ });
+ const dropEvent = new MockDragEvent({
+ type: 'file'
+ });
+
+ // when
+ wrapper.simulate('dragover', dragOverEvent);
+ wrapper.simulate('drop', dropEvent);
+
+ // then
+ expect(wrapper.find('DropOverlay').exists()).to.be.false;
+
+ });
+
+
+ it('should call passed onDrop prop with event', function() {
+
+ // given
+ const dropSpy = sinon.spy();
+
+ const wrapper = shallow();
+
+ const dragOverEvent = new MockDragEvent({
+ type: 'file'
+ });
+ const dropEvent = new MockDragEvent({
+ type: 'file'
+ });
+
+ // when
+ wrapper.simulate('dragover', dragOverEvent);
+ wrapper.simulate('drop', dropEvent);
+
+ // then
+ expect(dropSpy).to.be.calledOnce;
+ expect(dropSpy.getCall(0).args).to.have.lengthOf(1);
+ expect(dropSpy.getCall(0).args[0]).to.have.property('dataTransfer');
+
+ });
+
+ });
+
+});
+
+
+
+// helper /////
+class MockDragEvent {
+ constructor(...items) {
+ this.dataTransfer = { items };
+ }
+
+ preventDefault() {}
+
+ stopPropagation() {}
+}
diff --git a/client/src/app/drop-zone/index.js b/client/src/app/drop-zone/index.js
new file mode 100644
index 0000000000..185ac896a7
--- /dev/null
+++ b/client/src/app/drop-zone/index.js
@@ -0,0 +1 @@
+export { DropZone } from './DropZone';