Skip to content

Commit f098576

Browse files
authored
Merge pull request #5 from martinRenou/dragndrop
Fix drag and drop
2 parents 491b754 + 7d21505 commit f098576

File tree

1 file changed

+160
-2
lines changed

1 file changed

+160
-2
lines changed

src/unfold.ts

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
22

3-
import { toArray } from '@lumino/algorithm';
3+
import { IDragEvent } from '@lumino/dragdrop';
4+
5+
import { ArrayExt, toArray } from '@lumino/algorithm';
6+
7+
import { ElementExt } from '@lumino/domutils';
48

59
import { PromiseDelegate, ReadonlyJSONObject } from '@lumino/coreutils';
610

7-
import { DOMUtils } from '@jupyterlab/apputils';
11+
import { DOMUtils, showErrorMessage } from '@jupyterlab/apputils';
812

913
import { JupyterFrontEnd } from '@jupyterlab/application';
1014

1115
import { Contents, ContentsManager } from '@jupyterlab/services';
1216

1317
import { DocumentRegistry } from '@jupyterlab/docregistry';
1418

19+
import { renameFile } from '@jupyterlab/docmanager';
20+
1521
import { PathExt } from '@jupyterlab/coreutils';
1622

1723
import {
@@ -29,6 +35,16 @@ import { IStateDB } from '@jupyterlab/statedb';
2935
// @ts-ignore
3036
import folderOpenSvgstr from '../style/icons/folder-open.svg';
3137

38+
/**
39+
* The class name added to drop targets.
40+
*/
41+
const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
42+
43+
/**
44+
* The mime type for a contents drag object.
45+
*/
46+
const CONTENTS_MIME = 'application/x-jupyter-icontents';
47+
3248
export const folderOpenIcon = new LabIcon({
3349
name: 'ui-components:folder-open',
3450
svgstr: folderOpenSvgstr
@@ -225,6 +241,148 @@ export class DirTreeListing extends DirListing {
225241
}
226242
}
227243
}
244+
245+
private _eventDragEnter(event: IDragEvent): void {
246+
if (event.mimeData.hasData(CONTENTS_MIME)) {
247+
// @ts-ignore
248+
const index = this._hitTestNodes(this._items, event);
249+
250+
let target: HTMLElement;
251+
if (index !== -1) {
252+
// @ts-ignore
253+
target = this._items[index];
254+
} else {
255+
target = event.target as HTMLElement;
256+
}
257+
target.classList.add(DROP_TARGET_CLASS);
258+
event.preventDefault();
259+
event.stopPropagation();
260+
}
261+
}
262+
263+
private _eventDragOver(event: IDragEvent): void {
264+
event.preventDefault();
265+
event.stopPropagation();
266+
event.dropAction = event.proposedAction;
267+
268+
const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
269+
if (dropTarget) {
270+
dropTarget.classList.remove(DROP_TARGET_CLASS);
271+
}
272+
273+
// @ts-ignore
274+
const index = this._hitTestNodes(this._items, event);
275+
276+
let target: HTMLElement;
277+
if (index !== -1) {
278+
// @ts-ignore
279+
target = this._items[index];
280+
} else {
281+
target = event.target as HTMLElement;
282+
}
283+
target.classList.add(DROP_TARGET_CLASS);
284+
}
285+
286+
private _eventDrop(event: IDragEvent): void {
287+
event.preventDefault();
288+
event.stopPropagation();
289+
// @ts-ignore
290+
clearTimeout(this._selectTimer);
291+
if (event.proposedAction === 'none') {
292+
event.dropAction = 'none';
293+
return;
294+
}
295+
if (!event.mimeData.hasData(CONTENTS_MIME)) {
296+
return;
297+
}
298+
299+
let target = event.target as HTMLElement;
300+
while (target && target.parentElement) {
301+
if (target.classList.contains(DROP_TARGET_CLASS)) {
302+
target.classList.remove(DROP_TARGET_CLASS);
303+
break;
304+
}
305+
target = target.parentElement;
306+
}
307+
308+
// Get the path based on the target node.
309+
// @ts-ignore
310+
const index = ArrayExt.firstIndexOf(this._items, target);
311+
let newDir: string;
312+
313+
if (index !== -1) {
314+
const item = toArray(this.model.items())[index];
315+
316+
if (item.type === 'directory') {
317+
newDir = item.path;
318+
} else {
319+
newDir = PathExt.dirname(item.path);
320+
}
321+
} else {
322+
newDir = '';
323+
}
324+
325+
// @ts-ignore
326+
const manager = this._manager;
327+
328+
// Handle the items.
329+
const promises: Promise<Contents.IModel | null>[] = [];
330+
const paths = event.mimeData.getData(CONTENTS_MIME) as string[];
331+
332+
if (event.ctrlKey && event.proposedAction === 'move') {
333+
event.dropAction = 'copy';
334+
} else {
335+
event.dropAction = event.proposedAction;
336+
}
337+
for (const path of paths) {
338+
const localPath = manager.services.contents.localPath(path);
339+
const name = PathExt.basename(localPath);
340+
const newPath = PathExt.join(newDir, name);
341+
// Skip files that are not moving.
342+
if (newPath === path) {
343+
continue;
344+
}
345+
346+
if (event.dropAction === 'copy') {
347+
promises.push(manager.copy(path, newDir));
348+
} else {
349+
promises.push(renameFile(manager, path, newPath));
350+
}
351+
}
352+
Promise.all(promises).catch(error => {
353+
void showErrorMessage(
354+
// @ts-ignore
355+
this._trans._p('showErrorMessage', 'Error while copying/moving files'),
356+
error
357+
);
358+
});
359+
}
360+
361+
private _hitTestNodes(nodes: HTMLElement[], event: MouseEvent): number {
362+
return ArrayExt.findFirstIndex(
363+
nodes,
364+
node =>
365+
ElementExt.hitTest(node, event.clientX, event.clientY) ||
366+
event.target === node
367+
);
368+
}
369+
370+
handleEvent(event: Event): void {
371+
switch (event.type) {
372+
case 'lm-dragenter':
373+
this._eventDragEnter(event as IDragEvent);
374+
break;
375+
case 'lm-dragover':
376+
this._eventDragOver(event as IDragEvent);
377+
break;
378+
case 'lm-drop':
379+
this._eventDrop(event as IDragEvent);
380+
break;
381+
default:
382+
super.handleEvent(event);
383+
break;
384+
}
385+
}
228386
}
229387

230388
/**

0 commit comments

Comments
 (0)