@jupyterlab/filebrowser 4.0.0-alpha.2 → 4.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/crumbs.ts ADDED
@@ -0,0 +1,441 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { DOMUtils, showErrorMessage } from '@jupyterlab/apputils';
5
+ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
6
+ import { renameFile } from '@jupyterlab/docmanager';
7
+ import {
8
+ ITranslator,
9
+ nullTranslator,
10
+ TranslationBundle
11
+ } from '@jupyterlab/translation';
12
+ import {
13
+ ellipsesIcon,
14
+ homeIcon as preferredIcon,
15
+ folderIcon as rootIcon
16
+ } from '@jupyterlab/ui-components';
17
+ import { ArrayExt } from '@lumino/algorithm';
18
+ import { ElementExt } from '@lumino/domutils';
19
+ import { Drag } from '@lumino/dragdrop';
20
+ import { Message } from '@lumino/messaging';
21
+ import { Widget } from '@lumino/widgets';
22
+ import { FileBrowserModel } from './model';
23
+
24
+ /**
25
+ * The class name added to the breadcrumb node.
26
+ */
27
+ const BREADCRUMB_CLASS = 'jp-BreadCrumbs';
28
+
29
+ /**
30
+ * The class name for the breadcrumbs home node
31
+ */
32
+ const BREADCRUMB_ROOT_CLASS = 'jp-BreadCrumbs-home';
33
+
34
+ /**
35
+ * The class name for the breadcrumbs preferred node
36
+ */
37
+ const BREADCRUMB_PREFERRED_CLASS = 'jp-BreadCrumbs-preferred';
38
+
39
+ /**
40
+ * The class name added to the breadcrumb node.
41
+ */
42
+ const BREADCRUMB_ITEM_CLASS = 'jp-BreadCrumbs-item';
43
+
44
+ /**
45
+ * Bread crumb paths.
46
+ */
47
+ const BREAD_CRUMB_PATHS = ['/', '../../', '../', ''];
48
+
49
+ /**
50
+ * The mime type for a contents drag object.
51
+ */
52
+ const CONTENTS_MIME = 'application/x-jupyter-icontents';
53
+
54
+ /**
55
+ * The class name added to drop targets.
56
+ */
57
+ const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
58
+
59
+ /**
60
+ * A class which hosts folder breadcrumbs.
61
+ */
62
+ export class BreadCrumbs extends Widget {
63
+ /**
64
+ * Construct a new file browser crumb widget.
65
+ *
66
+ * @param model - The file browser view model.
67
+ */
68
+ constructor(options: BreadCrumbs.IOptions) {
69
+ super();
70
+ this.translator = options.translator || nullTranslator;
71
+ this._trans = this.translator.load('jupyterlab');
72
+ this._model = options.model;
73
+ this.addClass(BREADCRUMB_CLASS);
74
+ this._crumbs = Private.createCrumbs();
75
+ this._crumbSeps = Private.createCrumbSeparators();
76
+ const hasPreferred = PageConfig.getOption('preferredPath');
77
+ this._hasPreferred = hasPreferred && hasPreferred !== '/' ? true : false;
78
+ if (this._hasPreferred) {
79
+ this.node.appendChild(this._crumbs[Private.Crumb.Preferred]);
80
+ }
81
+ this.node.appendChild(this._crumbs[Private.Crumb.Home]);
82
+ this._model.refreshed.connect(this.update, this);
83
+ }
84
+
85
+ /**
86
+ * Handle the DOM events for the bread crumbs.
87
+ *
88
+ * @param event - The DOM event sent to the widget.
89
+ *
90
+ * #### Notes
91
+ * This method implements the DOM `EventListener` interface and is
92
+ * called in response to events on the panel's DOM node. It should
93
+ * not be called directly by user code.
94
+ */
95
+ handleEvent(event: Event): void {
96
+ switch (event.type) {
97
+ case 'click':
98
+ this._evtClick(event as MouseEvent);
99
+ break;
100
+ case 'lm-dragenter':
101
+ this._evtDragEnter(event as Drag.Event);
102
+ break;
103
+ case 'lm-dragleave':
104
+ this._evtDragLeave(event as Drag.Event);
105
+ break;
106
+ case 'lm-dragover':
107
+ this._evtDragOver(event as Drag.Event);
108
+ break;
109
+ case 'lm-drop':
110
+ this._evtDrop(event as Drag.Event);
111
+ break;
112
+ default:
113
+ return;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * A message handler invoked on an `'after-attach'` message.
119
+ */
120
+ protected onAfterAttach(msg: Message): void {
121
+ super.onAfterAttach(msg);
122
+ this.update();
123
+ const node = this.node;
124
+ node.addEventListener('click', this);
125
+ node.addEventListener('lm-dragenter', this);
126
+ node.addEventListener('lm-dragleave', this);
127
+ node.addEventListener('lm-dragover', this);
128
+ node.addEventListener('lm-drop', this);
129
+ }
130
+
131
+ /**
132
+ * A message handler invoked on a `'before-detach'` message.
133
+ */
134
+ protected onBeforeDetach(msg: Message): void {
135
+ super.onBeforeDetach(msg);
136
+ const node = this.node;
137
+ node.removeEventListener('click', this);
138
+ node.removeEventListener('lm-dragenter', this);
139
+ node.removeEventListener('lm-dragleave', this);
140
+ node.removeEventListener('lm-dragover', this);
141
+ node.removeEventListener('lm-drop', this);
142
+ }
143
+
144
+ /**
145
+ * A handler invoked on an `'update-request'` message.
146
+ */
147
+ protected onUpdateRequest(msg: Message): void {
148
+ // Update the breadcrumb list.
149
+ const contents = this._model.manager.services.contents;
150
+ const localPath = contents.localPath(this._model.path);
151
+ Private.updateCrumbs(
152
+ this._crumbs,
153
+ this._crumbSeps,
154
+ localPath,
155
+ this._hasPreferred
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Handle the `'click'` event for the widget.
161
+ */
162
+ private _evtClick(event: MouseEvent): void {
163
+ // Do nothing if it's not a left mouse press.
164
+ if (event.button !== 0) {
165
+ return;
166
+ }
167
+
168
+ // Find a valid click target.
169
+ let node = event.target as HTMLElement;
170
+ while (node && node !== this.node) {
171
+ if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS)) {
172
+ this._model
173
+ .cd(PageConfig.getOption('preferredPath'))
174
+ .catch(error =>
175
+ showErrorMessage(this._trans.__('Open Error'), error)
176
+ );
177
+
178
+ // Stop the event propagation.
179
+ event.preventDefault();
180
+ event.stopPropagation();
181
+ return;
182
+ }
183
+ if (
184
+ node.classList.contains(BREADCRUMB_ITEM_CLASS) ||
185
+ node.classList.contains(BREADCRUMB_ROOT_CLASS)
186
+ ) {
187
+ const index = ArrayExt.findFirstIndex(
188
+ this._crumbs,
189
+ value => value === node
190
+ );
191
+ this._model
192
+ .cd(BREAD_CRUMB_PATHS[index])
193
+ .catch(error =>
194
+ showErrorMessage(this._trans.__('Open Error'), error)
195
+ );
196
+
197
+ // Stop the event propagation.
198
+ event.preventDefault();
199
+ event.stopPropagation();
200
+ return;
201
+ }
202
+ node = node.parentElement as HTMLElement;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Handle the `'lm-dragenter'` event for the widget.
208
+ */
209
+ private _evtDragEnter(event: Drag.Event): void {
210
+ if (event.mimeData.hasData(CONTENTS_MIME)) {
211
+ const index = ArrayExt.findFirstIndex(this._crumbs, node =>
212
+ ElementExt.hitTest(node, event.clientX, event.clientY)
213
+ );
214
+ if (index !== -1) {
215
+ if (index !== Private.Crumb.Current) {
216
+ this._crumbs[index].classList.add(DROP_TARGET_CLASS);
217
+ event.preventDefault();
218
+ event.stopPropagation();
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Handle the `'lm-dragleave'` event for the widget.
226
+ */
227
+ private _evtDragLeave(event: Drag.Event): void {
228
+ event.preventDefault();
229
+ event.stopPropagation();
230
+ const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
231
+ if (dropTarget) {
232
+ dropTarget.classList.remove(DROP_TARGET_CLASS);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Handle the `'lm-dragover'` event for the widget.
238
+ */
239
+ private _evtDragOver(event: Drag.Event): void {
240
+ event.preventDefault();
241
+ event.stopPropagation();
242
+ event.dropAction = event.proposedAction;
243
+ const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
244
+ if (dropTarget) {
245
+ dropTarget.classList.remove(DROP_TARGET_CLASS);
246
+ }
247
+ const index = ArrayExt.findFirstIndex(this._crumbs, node =>
248
+ ElementExt.hitTest(node, event.clientX, event.clientY)
249
+ );
250
+ if (index !== -1) {
251
+ this._crumbs[index].classList.add(DROP_TARGET_CLASS);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Handle the `'lm-drop'` event for the widget.
257
+ */
258
+ private _evtDrop(event: Drag.Event): void {
259
+ event.preventDefault();
260
+ event.stopPropagation();
261
+ if (event.proposedAction === 'none') {
262
+ event.dropAction = 'none';
263
+ return;
264
+ }
265
+ if (!event.mimeData.hasData(CONTENTS_MIME)) {
266
+ return;
267
+ }
268
+ event.dropAction = event.proposedAction;
269
+
270
+ let target = event.target as HTMLElement;
271
+ while (target && target.parentElement) {
272
+ if (target.classList.contains(DROP_TARGET_CLASS)) {
273
+ target.classList.remove(DROP_TARGET_CLASS);
274
+ break;
275
+ }
276
+ target = target.parentElement;
277
+ }
278
+
279
+ // Get the path based on the target node.
280
+ const index = ArrayExt.findFirstIndex(
281
+ this._crumbs,
282
+ node => node === target
283
+ );
284
+ if (index === -1) {
285
+ return;
286
+ }
287
+
288
+ const model = this._model;
289
+ const path = PathExt.resolve(model.path, BREAD_CRUMB_PATHS[index]);
290
+ const manager = model.manager;
291
+
292
+ // Move all of the items.
293
+ const promises: Promise<any>[] = [];
294
+ const oldPaths = event.mimeData.getData(CONTENTS_MIME) as string[];
295
+ for (const oldPath of oldPaths) {
296
+ const localOldPath = manager.services.contents.localPath(oldPath);
297
+ const name = PathExt.basename(localOldPath);
298
+ const newPath = PathExt.join(path, name);
299
+ promises.push(renameFile(manager, oldPath, newPath));
300
+ }
301
+ void Promise.all(promises).catch(err => {
302
+ return showErrorMessage(this._trans.__('Move Error'), err);
303
+ });
304
+ }
305
+
306
+ protected translator: ITranslator;
307
+ private _trans: TranslationBundle;
308
+ private _model: FileBrowserModel;
309
+ private _hasPreferred: boolean;
310
+ private _crumbs: ReadonlyArray<HTMLElement>;
311
+ private _crumbSeps: ReadonlyArray<HTMLElement>;
312
+ }
313
+
314
+ /**
315
+ * The namespace for the `BreadCrumbs` class statics.
316
+ */
317
+ export namespace BreadCrumbs {
318
+ /**
319
+ * An options object for initializing a bread crumb widget.
320
+ */
321
+ export interface IOptions {
322
+ /**
323
+ * A file browser model instance.
324
+ */
325
+ model: FileBrowserModel;
326
+
327
+ /**
328
+ * The application language translator.
329
+ */
330
+ translator?: ITranslator;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * The namespace for the crumbs private data.
336
+ */
337
+ namespace Private {
338
+ /**
339
+ * Breadcrumb item list enum.
340
+ */
341
+ export enum Crumb {
342
+ Home,
343
+ Ellipsis,
344
+ Parent,
345
+ Current,
346
+ Preferred
347
+ }
348
+
349
+ /**
350
+ * Populate the breadcrumb node.
351
+ */
352
+ export function updateCrumbs(
353
+ breadcrumbs: ReadonlyArray<HTMLElement>,
354
+ separators: ReadonlyArray<HTMLElement>,
355
+ path: string,
356
+ hasPreferred: boolean
357
+ ): void {
358
+ const node = breadcrumbs[0].parentNode as HTMLElement;
359
+
360
+ // Remove all but the home or preferred node.
361
+ const firstChild = node.firstChild as HTMLElement;
362
+ while (firstChild && firstChild.nextSibling) {
363
+ node.removeChild(firstChild.nextSibling);
364
+ }
365
+
366
+ if (hasPreferred) {
367
+ node.appendChild(breadcrumbs[Crumb.Home]);
368
+ node.appendChild(separators[0]);
369
+ } else {
370
+ node.appendChild(separators[0]);
371
+ }
372
+
373
+ const parts = path.split('/');
374
+ if (parts.length > 2) {
375
+ node.appendChild(breadcrumbs[Crumb.Ellipsis]);
376
+ const grandParent = parts.slice(0, parts.length - 2).join('/');
377
+ breadcrumbs[Crumb.Ellipsis].title = grandParent;
378
+ node.appendChild(separators[1]);
379
+ }
380
+
381
+ if (path) {
382
+ if (parts.length >= 2) {
383
+ breadcrumbs[Crumb.Parent].textContent = parts[parts.length - 2];
384
+ node.appendChild(breadcrumbs[Crumb.Parent]);
385
+ const parent = parts.slice(0, parts.length - 1).join('/');
386
+ breadcrumbs[Crumb.Parent].title = parent;
387
+ node.appendChild(separators[2]);
388
+ }
389
+ breadcrumbs[Crumb.Current].textContent = parts[parts.length - 1];
390
+ node.appendChild(breadcrumbs[Crumb.Current]);
391
+ breadcrumbs[Crumb.Current].title = path;
392
+ node.appendChild(separators[3]);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Create the breadcrumb nodes.
398
+ */
399
+ export function createCrumbs(): ReadonlyArray<HTMLElement> {
400
+ const home = rootIcon.element({
401
+ className: BREADCRUMB_ROOT_CLASS,
402
+ tag: 'span',
403
+ title: PageConfig.getOption('serverRoot') || 'Jupyter Server Root',
404
+ stylesheet: 'breadCrumb'
405
+ });
406
+ const ellipsis = ellipsesIcon.element({
407
+ className: BREADCRUMB_ITEM_CLASS,
408
+ tag: 'span',
409
+ stylesheet: 'breadCrumb'
410
+ });
411
+ const parent = document.createElement('span');
412
+ parent.className = BREADCRUMB_ITEM_CLASS;
413
+ const current = document.createElement('span');
414
+ current.className = BREADCRUMB_ITEM_CLASS;
415
+ const preferred = preferredIcon.element({
416
+ className: BREADCRUMB_PREFERRED_CLASS,
417
+ tag: 'span',
418
+ title: PageConfig.getOption('preferredPath') || 'Jupyter Preferred Path',
419
+ stylesheet: 'breadCrumb'
420
+ });
421
+ return [home, ellipsis, parent, current, preferred];
422
+ }
423
+
424
+ /**
425
+ * Create the breadcrumb separator nodes.
426
+ */
427
+ export function createCrumbSeparators(): ReadonlyArray<HTMLElement> {
428
+ const items: HTMLElement[] = [];
429
+ // The maximum number of directories that will be shown in the crumbs
430
+ const MAX_DIRECTORIES = 2;
431
+
432
+ // Make separators for after each directory, one at the beginning, and one
433
+ // after a possible ellipsis.
434
+ for (let i = 0; i < MAX_DIRECTORIES + 2; i++) {
435
+ const item = document.createElement('span');
436
+ item.textContent = '/';
437
+ items.push(item);
438
+ }
439
+ return items;
440
+ }
441
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+ /**
4
+ * @packageDocumentation
5
+ * @module filebrowser
6
+ */
7
+
8
+ export * from './browser';
9
+ export * from './crumbs';
10
+ export * from './listing';
11
+ export * from './model';
12
+ export * from './opendialog';
13
+ export * from './tokens';
14
+ export * from './upload';
15
+ export * from './uploadstatus';