Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit 442678d

Browse files
boopeshmahendrannethip
authored andcommitted
Add drag and drop to move items in FileTreeView (#13546)
* Add drag and drop to move items in FileTreeView Todo: * Handle move errors. * Add support for moving to root directory. * Add support for moving items to root directory * Check item dropped onto itself or parent directory * Add support for moving items by dropping on files * Create dragItem action * Close directory on drag * Open directory on Drop * Open directory on drag over * Address review comments * Add tests for moveItem in FileTreeViewModel * Fix style issues on drag * Make styles fast * Change fileindex to update the moved entry * Fix lint mistakes * Set drag image as item name * Check if directory is open before opening directory on drop * Refactor code * Check if item is dropped onto itself or parent directory * Move selected flag when item is moved * Use filter instead of forEach * Change implementation to reuse rename workflow * Fix tests * Add docs and comments * Address review comments * Add Error handling * Make directory open and adding new item independent This makes adding the moved item to the new directory independent of whether the directory is loaded or not by creating a notFullyLoaded directory for the new directory.
1 parent 92b872e commit 442678d

8 files changed

Lines changed: 393 additions & 72 deletions

File tree

src/filesystem/FileIndex.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
define(function (require, exports, module) {
3030
"use strict";
3131

32+
var FileUtils = require("file/FileUtils");
33+
3234
/**
3335
* @constructor
3436
*/
@@ -110,7 +112,9 @@ define(function (require, exports, module) {
110112
*/
111113
FileIndex.prototype.entryRenamed = function (oldPath, newPath, isDirectory) {
112114
var path,
113-
renameMap = {};
115+
renameMap = {},
116+
oldParentPath = FileUtils.getParentPath(oldPath),
117+
newParentPath = FileUtils.getParentPath(newPath);
114118

115119
// Find all entries affected by the rename and put into a separate map.
116120
for (path in this._index) {
@@ -138,6 +142,30 @@ define(function (require, exports, module) {
138142
item._setPath(renameMap[path]);
139143
}
140144
}
145+
146+
147+
// If file path is changed, i.e the file is moved
148+
// Remove the moved entry from old Directory and add it to new Directory
149+
if (oldParentPath !== newParentPath) {
150+
var oldDirectory = this._index[oldParentPath],
151+
newDirectory = this._index[newParentPath],
152+
renamedEntry;
153+
154+
if (oldDirectory && oldDirectory._contents) {
155+
oldDirectory._contents = oldDirectory._contents.filter(function(entry) {
156+
if (entry.fullPath === newPath) {
157+
renamedEntry = entry;
158+
return false;
159+
}
160+
return true;
161+
});
162+
}
163+
164+
if (newDirectory && newDirectory._contents && renamedEntry) {
165+
renamedEntry._setPath(newPath);
166+
newDirectory._contents.push(renamedEntry);
167+
}
168+
}
141169
};
142170

143171
/**

src/project/FileTreeView.js

Lines changed: 138 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ define(function (require, exports, module) {
176176
* this component, so we keep the model up to date by sending every update via an action.
177177
*/
178178
handleInput: function (e) {
179-
this.props.actions.setRenameValue(this.refs.name.value.trim());
179+
this.props.actions.setRenameValue(this.props.parentPath + this.refs.name.value.trim());
180180

181181
if (e.keyCode !== KeyEvent.DOM_VK_LEFT &&
182182
e.keyCode !== KeyEvent.DOM_VK_RIGHT) {
@@ -195,6 +195,97 @@ define(function (require, exports, module) {
195195
}
196196
};
197197

198+
/**
199+
* This is a mixin that provides drag and drop move function.
200+
*/
201+
var dragAndDrop = {
202+
handleDrag: function(e) {
203+
// Disable drag when renaming
204+
if (this.props.entry.get("rename")) {
205+
e.preventDefault();
206+
e.stopPropagation();
207+
return false;
208+
}
209+
210+
// Pass the dragged item path.
211+
e.dataTransfer.setData("text", JSON.stringify({
212+
path: this.myPath()
213+
}));
214+
215+
this.props.actions.dragItem(this.myPath());
216+
217+
this.setDragImage(e);
218+
e.stopPropagation();
219+
},
220+
handleDrop: function(e) {
221+
var data = JSON.parse(e.dataTransfer.getData("text"));
222+
223+
this.props.actions.moveItem(data.path, this.myPath());
224+
this.setDraggedOver(false);
225+
226+
this.clearDragTimeout();
227+
e.stopPropagation();
228+
},
229+
230+
handleDragEnd: function(e) {
231+
this.clearDragTimeout();
232+
},
233+
234+
handleDragOver: function(e) {
235+
var data = JSON.parse(e.dataTransfer.getData("text"));
236+
237+
if (data.path === this.myPath() || FileUtils.getParentPath(data.path) === this.myPath()) {
238+
e.preventDefault();
239+
e.stopPropagation();
240+
return;
241+
}
242+
var self = this;
243+
this.setDraggedOver(true);
244+
245+
// Open the directory tree when item is dragged over a directory
246+
if (!this.dragOverTimeout) {
247+
this.dragOverTimeout = window.setTimeout(function() {
248+
self.props.actions.setDirectoryOpen(self.myPath(), true);
249+
self.dragOverTimeout = null;
250+
}, 800);
251+
}
252+
253+
e.preventDefault(); // Allow the drop
254+
e.stopPropagation();
255+
},
256+
257+
handleDragLeave: function(e) {
258+
this.setDraggedOver(false);
259+
this.clearDragTimeout();
260+
},
261+
262+
clearDragTimeout: function() {
263+
if (this.dragOverTimeout) {
264+
clearTimeout(this.dragOverTimeout);
265+
this.dragOverTimeout = null;
266+
}
267+
},
268+
setDraggedOver: function(draggedOver) {
269+
if (this.state.draggedOver !== draggedOver) {
270+
this.setState({
271+
draggedOver: draggedOver
272+
});
273+
}
274+
},
275+
276+
setDragImage: function(e) {
277+
var div = window.document.createElement('div');
278+
div.style.position = 'absolute';
279+
div.style.color = '#fff';
280+
div.textContent = this.props.name;
281+
window.document.body.appendChild(div);
282+
e.dataTransfer.setDragImage(div, -10, -10);
283+
setTimeout(function() {
284+
window.document.body.removeChild(div);
285+
}, 0);
286+
}
287+
};
288+
198289
/**
199290
* @private
200291
*
@@ -265,7 +356,6 @@ define(function (require, exports, module) {
265356
if (this.props.entry.get("rename")) {
266357
return;
267358
}
268-
e.preventDefault();
269359
}
270360
};
271361

@@ -363,7 +453,7 @@ define(function (require, exports, module) {
363453
* * forceRender: causes the component to run render
364454
*/
365455
var fileNode = Preact.createFactory(Preact.createClass({
366-
mixins: [contextSettable, pathComputer, extendable],
456+
mixins: [contextSettable, pathComputer, extendable, dragAndDrop],
367457

368458
/**
369459
* Ensures that we always have a state object.
@@ -504,7 +594,9 @@ define(function (require, exports, module) {
504594
className: this.getClasses("jstree-leaf"),
505595
onClick: this.handleClick,
506596
onMouseDown: this.handleMouseDown,
507-
onDoubleClick: this.handleDoubleClick
597+
onDoubleClick: this.handleDoubleClick,
598+
draggable: true,
599+
onDragStart: this.handleDrag
508600
},
509601
DOM.ins({
510602
className: "jstree-icon"
@@ -645,7 +737,13 @@ define(function (require, exports, module) {
645737
* * forceRender: causes the component to run render
646738
*/
647739
directoryNode = Preact.createFactory(Preact.createClass({
648-
mixins: [contextSettable, pathComputer, extendable],
740+
mixins: [contextSettable, pathComputer, extendable, dragAndDrop],
741+
742+
getInitialState: function() {
743+
return {
744+
draggedOver: false
745+
};
746+
},
649747

650748
/**
651749
* We need to update this component if the sort order changes or our entry object
@@ -656,7 +754,8 @@ define(function (require, exports, module) {
656754
return nextProps.forceRender ||
657755
this.props.entry !== nextProps.entry ||
658756
this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst ||
659-
this.props.extensions !== nextProps.extensions;
757+
this.props.extensions !== nextProps.extensions ||
758+
(nextState !== undefined && this.state.draggedOver !== nextState.draggedOver);
660759
},
661760

662761
/**
@@ -744,11 +843,22 @@ define(function (require, exports, module) {
744843
'context-node': entry.get("context")
745844
});
746845

846+
var nodeClasses = "jstree-" + nodeClass;
847+
if (this.state.draggedOver) {
848+
nodeClasses += " jstree-draggedOver";
849+
}
850+
747851
var liArgs = [
748852
{
749-
className: this.getClasses("jstree-" + nodeClass),
853+
className: this.getClasses(nodeClasses),
750854
onClick: this.handleClick,
751-
onMouseDown: this.handleMouseDown
855+
onMouseDown: this.handleMouseDown,
856+
draggable: true,
857+
onDragStart: this.handleDrag,
858+
onDrop: this.handleDrop,
859+
onDragEnd: this.handleDragEnd,
860+
onDragOver: this.handleDragOver,
861+
onDragLeave: this.handleDragLeave
752862
},
753863
_createAlignedIns(this.props.depth)
754864
];
@@ -1001,6 +1111,19 @@ define(function (require, exports, module) {
10011111
this.props.selectionViewInfo !== nextProps.selectionViewInfo;
10021112
},
10031113

1114+
handleDrop: function(e) {
1115+
var data = JSON.parse(e.dataTransfer.getData("text"));
1116+
this.props.actions.moveItem(data.path, this.props.parentPath);
1117+
e.stopPropagation();
1118+
},
1119+
1120+
/**
1121+
* Allow the Drop
1122+
*/
1123+
handleDragOver: function(e) {
1124+
e.preventDefault();
1125+
},
1126+
10041127
render: function () {
10051128
var selectionBackground = fileSelectionBox({
10061129
ref: "selectionBackground",
@@ -1042,10 +1165,15 @@ define(function (require, exports, module) {
10421165
actions: this.props.actions,
10431166
forceRender: this.props.forceRender,
10441167
platform: this.props.platform
1045-
});
1168+
}),
1169+
args = {
1170+
onDrop: this.handleDrop,
1171+
onDragOver: this.handleDragOver
1172+
};
1173+
10461174

10471175
return DOM.div(
1048-
null,
1176+
args,
10491177
contents,
10501178
selectionBackground,
10511179
contextBackground,

src/project/FileTreeViewModel.js

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -461,34 +461,57 @@ define(function (require, exports, module) {
461461
};
462462

463463
/**
464-
* Changes the name of the item at the `currentPath` to `newName`.
464+
* Changes the path of the item at the `currentPath` to `newPath`.
465465
*
466466
* @param {string} currentPath project relative file path to the current item
467-
* @param {string} newName Name to give the item
467+
* @param {string} newPath project relative new path to give the item
468468
*/
469-
FileTreeViewModel.prototype.renameItem = function (currentPath, newName) {
469+
FileTreeViewModel.prototype.renameItem = function (oldPath, newPath) {
470470
var treeData = this._treeData,
471-
objectPath = _filePathToObjectPath(treeData, currentPath);
471+
oldObjectPath = _filePathToObjectPath(treeData, oldPath),
472+
newDirectoryPath = FileUtils.getParentPath(newPath),
473+
newObjectPath = _filePathToObjectPath(treeData, newDirectoryPath);
472474

473-
if (!objectPath) {
475+
if (!oldObjectPath || !newObjectPath) {
474476
return;
475477
}
476478

477-
var originalName = _.last(objectPath),
478-
currentObject = treeData.getIn(objectPath);
479+
var originalName = _.last(oldObjectPath),
480+
newName = FileUtils.getBaseName(newPath),
481+
currentObject;
479482

480483
// Back up to the parent directory
481-
objectPath.pop();
484+
oldObjectPath.pop();
482485

483-
treeData = treeData.updateIn(objectPath, function (directory) {
486+
// Remove the oldPath
487+
treeData = treeData.updateIn(oldObjectPath, function (directory) {
488+
currentObject = directory.get(originalName);
484489
directory = directory.delete(originalName);
485-
directory = directory.set(newName, currentObject);
486490
return directory;
487491
});
488492

493+
// Add the newPath
494+
495+
// If the new directory is not loaded, create a not fully loaded directory there,
496+
// so that we can add the new item as a child of new directory
497+
if (!this.isPathLoaded(newDirectoryPath)) {
498+
treeData = treeData.updateIn(newObjectPath, _createNotFullyLoadedDirectory);
499+
}
500+
501+
// If item moved to root directory, objectPath should not have "children",
502+
// otherwise the objectPath should have "children"
503+
if (newObjectPath.length > 0) {
504+
newObjectPath.push("children");
505+
}
506+
507+
treeData = treeData.updateIn(newObjectPath, function (children) {
508+
return children.set(newName, currentObject);
509+
});
510+
489511
this._commit(treeData);
490512
};
491513

514+
492515
/**
493516
* @private
494517
*

0 commit comments

Comments
 (0)