@jupyterlab/filebrowser 4.6.0-alpha.2 → 4.6.0-alpha.4
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/lib/browser.d.ts +12 -5
- package/lib/browser.js +17 -0
- package/lib/browser.js.map +1 -1
- package/lib/crumbs.d.ts +25 -3
- package/lib/crumbs.js +187 -5
- package/lib/crumbs.js.map +1 -1
- package/lib/listing.d.ts +13 -6
- package/lib/listing.js +36 -12
- package/lib/listing.js.map +1 -1
- package/lib/model.d.ts +8 -8
- package/lib/model.js.map +1 -1
- package/lib/opendialog.d.ts +4 -4
- package/lib/opendialog.js.map +1 -1
- package/lib/tokens.d.ts +14 -0
- package/lib/tokens.js +1 -0
- package/lib/tokens.js.map +1 -1
- package/lib/upload.d.ts +4 -4
- package/lib/upload.js.map +1 -1
- package/lib/uploadstatus.d.ts +3 -3
- package/lib/uploadstatus.js.map +1 -1
- package/package.json +14 -14
- package/src/browser.ts +28 -7
- package/src/crumbs.ts +231 -13
- package/src/listing.ts +54 -33
- package/src/model.ts +18 -17
- package/src/opendialog.ts +15 -14
- package/src/tokens.ts +20 -0
- package/src/upload.ts +6 -8
- package/src/uploadstatus.tsx +5 -8
package/src/crumbs.ts
CHANGED
|
@@ -4,21 +4,19 @@
|
|
|
4
4
|
import { DOMUtils, showErrorMessage } from '@jupyterlab/apputils';
|
|
5
5
|
import { PageConfig, PathExt } from '@jupyterlab/coreutils';
|
|
6
6
|
import { renameFile } from '@jupyterlab/docmanager';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
nullTranslator,
|
|
10
|
-
TranslationBundle
|
|
11
|
-
} from '@jupyterlab/translation';
|
|
7
|
+
import type { ITranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
8
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
12
9
|
import {
|
|
13
10
|
ellipsesIcon,
|
|
14
11
|
homeIcon as preferredIcon,
|
|
15
12
|
folderIcon as rootIcon
|
|
16
13
|
} from '@jupyterlab/ui-components';
|
|
17
14
|
import { JSONExt } from '@lumino/coreutils';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
15
|
+
import { Throttler } from '@lumino/polling';
|
|
16
|
+
import type { Drag } from '@lumino/dragdrop';
|
|
17
|
+
import type { Message } from '@lumino/messaging';
|
|
20
18
|
import { Widget } from '@lumino/widgets';
|
|
21
|
-
import { FileBrowserModel } from './model';
|
|
19
|
+
import type { FileBrowserModel } from './model';
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
22
|
* The class name added to the breadcrumb node.
|
|
@@ -45,6 +43,11 @@ const BREADCRUMB_ITEM_CLASS = 'jp-BreadCrumbs-item';
|
|
|
45
43
|
*/
|
|
46
44
|
const BREADCRUMB_ELLIPSIS_CLASS = 'jp-BreadCrumbs-ellipsis';
|
|
47
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The class name for the breadcrumbs separator node
|
|
48
|
+
*/
|
|
49
|
+
const BREADCRUMB_SEPARATOR_CLASS = 'jp-BreadCrumbs-separator';
|
|
50
|
+
|
|
48
51
|
/**
|
|
49
52
|
* The mime type for a contents drag object.
|
|
50
53
|
*/
|
|
@@ -81,6 +84,19 @@ export class BreadCrumbs extends Widget {
|
|
|
81
84
|
}
|
|
82
85
|
this.node.appendChild(this._crumbs[Private.Crumb.Home]);
|
|
83
86
|
this._model.refreshed.connect(this.update, this);
|
|
87
|
+
this._resizeThrottler = new Throttler(() => this._onResize(), 50);
|
|
88
|
+
this._resizeObserver = new ResizeObserver(entries => {
|
|
89
|
+
const entry = entries[0];
|
|
90
|
+
if (!entry) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const newWidth = entry.contentRect.width;
|
|
94
|
+
if (this._lastRenderedWidth > 0 && newWidth < this._lastRenderedWidth) {
|
|
95
|
+
this._onResize();
|
|
96
|
+
} else {
|
|
97
|
+
void this._resizeThrottler.invoke();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
/**
|
|
@@ -148,6 +164,18 @@ export class BreadCrumbs extends Widget {
|
|
|
148
164
|
this._minimumRightItems = value;
|
|
149
165
|
}
|
|
150
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Dispose of the resources held by the widget.
|
|
169
|
+
*/
|
|
170
|
+
dispose(): void {
|
|
171
|
+
if (this.isDisposed) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this._resizeObserver.disconnect();
|
|
175
|
+
this._resizeThrottler.dispose();
|
|
176
|
+
super.dispose();
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
/**
|
|
152
180
|
* A message handler invoked on an `'after-attach'` message.
|
|
153
181
|
*/
|
|
@@ -159,6 +187,7 @@ export class BreadCrumbs extends Widget {
|
|
|
159
187
|
node.addEventListener('lm-dragenter', this);
|
|
160
188
|
node.addEventListener('lm-dragleave', this);
|
|
161
189
|
node.addEventListener('lm-dragover', this);
|
|
190
|
+
this._resizeObserver.observe(node);
|
|
162
191
|
node.addEventListener('lm-drop', this);
|
|
163
192
|
}
|
|
164
193
|
|
|
@@ -173,6 +202,7 @@ export class BreadCrumbs extends Widget {
|
|
|
173
202
|
node.removeEventListener('lm-dragleave', this);
|
|
174
203
|
node.removeEventListener('lm-dragover', this);
|
|
175
204
|
node.removeEventListener('lm-drop', this);
|
|
205
|
+
this._resizeObserver.unobserve(node);
|
|
176
206
|
}
|
|
177
207
|
|
|
178
208
|
/**
|
|
@@ -182,12 +212,21 @@ export class BreadCrumbs extends Widget {
|
|
|
182
212
|
// Update the breadcrumb list.
|
|
183
213
|
const contents = this._model.manager.services.contents;
|
|
184
214
|
const localPath = contents.localPath(this._model.path);
|
|
215
|
+
|
|
216
|
+
// Invalidate cached widths if the path changed
|
|
217
|
+
if (this._previousState && this._previousState.path !== localPath) {
|
|
218
|
+
this._cachedWidths = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Calculate adaptive items based on available width
|
|
222
|
+
const adaptiveItems = this._calculateAdaptiveItems(localPath);
|
|
223
|
+
|
|
185
224
|
const state = {
|
|
186
225
|
path: localPath,
|
|
187
226
|
hasPreferred: this._hasPreferred,
|
|
188
227
|
fullPath: this._fullPath,
|
|
189
|
-
minimumLeftItems:
|
|
190
|
-
minimumRightItems:
|
|
228
|
+
minimumLeftItems: adaptiveItems.left,
|
|
229
|
+
minimumRightItems: adaptiveItems.right
|
|
191
230
|
};
|
|
192
231
|
if (this._previousState && JSONExt.deepEqual(state, this._previousState)) {
|
|
193
232
|
return;
|
|
@@ -391,6 +430,176 @@ export class BreadCrumbs extends Widget {
|
|
|
391
430
|
return elements;
|
|
392
431
|
}
|
|
393
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Handle resize events with throttling.
|
|
435
|
+
*/
|
|
436
|
+
private _onResize(): void {
|
|
437
|
+
if (this.isDisposed || !this.isAttached) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// Force recalculation by clearing previous state
|
|
441
|
+
this._previousState = null;
|
|
442
|
+
this.update();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Measure ALL breadcrumb item widths by rendering them off-screen.
|
|
447
|
+
* This ensures we have accurate widths for every path segment,
|
|
448
|
+
* including those currently hidden behind the ellipsis.
|
|
449
|
+
*/
|
|
450
|
+
private _measureAllItemWidths(parts: string[]): void {
|
|
451
|
+
const node = this.node;
|
|
452
|
+
|
|
453
|
+
// Measure fixed elements that are already in the DOM
|
|
454
|
+
const home = this._crumbs[Private.Crumb.Home];
|
|
455
|
+
const ellipsis = this._crumbs[Private.Crumb.Ellipsis];
|
|
456
|
+
const preferred = this._crumbs[Private.Crumb.Preferred];
|
|
457
|
+
const separators = node.getElementsByClassName(BREADCRUMB_SEPARATOR_CLASS);
|
|
458
|
+
const separator = separators.length > 0 ? separators[0] : null;
|
|
459
|
+
|
|
460
|
+
// Create an off-screen container to measure all items
|
|
461
|
+
const measurer = document.createElement('div');
|
|
462
|
+
measurer.style.position = 'absolute';
|
|
463
|
+
measurer.style.visibility = 'hidden';
|
|
464
|
+
measurer.style.height = '0';
|
|
465
|
+
measurer.style.overflow = 'hidden';
|
|
466
|
+
measurer.style.whiteSpace = 'nowrap';
|
|
467
|
+
// Inherit font styles from the breadcrumb node
|
|
468
|
+
measurer.className = BREADCRUMB_CLASS;
|
|
469
|
+
node.appendChild(measurer);
|
|
470
|
+
|
|
471
|
+
// Create and measure each breadcrumb item for every path segment
|
|
472
|
+
const itemWidths: number[] = [];
|
|
473
|
+
for (let i = 0; i < parts.length; i++) {
|
|
474
|
+
const elem = document.createElement('span');
|
|
475
|
+
elem.className = BREADCRUMB_ITEM_CLASS;
|
|
476
|
+
elem.textContent = parts[i];
|
|
477
|
+
measurer.appendChild(elem);
|
|
478
|
+
const measured = elem.getBoundingClientRect().width;
|
|
479
|
+
// Fall back to a character-based estimate if layout is not available.
|
|
480
|
+
itemWidths.push(
|
|
481
|
+
(measured > 0 ? measured : Math.max(parts[i].length * 8, 20)) + 4
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Clean up
|
|
486
|
+
node.removeChild(measurer);
|
|
487
|
+
|
|
488
|
+
this._cachedWidths = {
|
|
489
|
+
home: (home.getBoundingClientRect().width || 22) + 4,
|
|
490
|
+
ellipsis: (ellipsis.getBoundingClientRect().width || 28) + 4,
|
|
491
|
+
separator: separator?.getBoundingClientRect().width || 4,
|
|
492
|
+
preferred: this._hasPreferred
|
|
493
|
+
? (preferred.getBoundingClientRect().width || 22) + 4
|
|
494
|
+
: 0,
|
|
495
|
+
itemWidths: itemWidths
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Calculate adaptive left/right items based on available width.
|
|
501
|
+
*/
|
|
502
|
+
private _calculateAdaptiveItems(path: string): {
|
|
503
|
+
left: number;
|
|
504
|
+
right: number;
|
|
505
|
+
} {
|
|
506
|
+
// Reset last rendered width to avoid stale data on early returns
|
|
507
|
+
this._lastRenderedWidth = 0;
|
|
508
|
+
|
|
509
|
+
const parts = path.split('/').filter(part => part !== '');
|
|
510
|
+
const totalParts = parts.length;
|
|
511
|
+
|
|
512
|
+
// If fullPath is enabled or there are no parts, use minimum settings
|
|
513
|
+
if (this._fullPath || totalParts === 0) {
|
|
514
|
+
return { left: this._minimumLeftItems, right: this._minimumRightItems };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// If total parts fit within minimums, no adaptation needed
|
|
518
|
+
const minTotal = this._minimumLeftItems + this._minimumRightItems;
|
|
519
|
+
if (totalParts <= minTotal) {
|
|
520
|
+
return { left: this._minimumLeftItems, right: this._minimumRightItems };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const containerWidth = this.node.clientWidth;
|
|
524
|
+
if (containerWidth === 0) {
|
|
525
|
+
return { left: this._minimumLeftItems, right: this._minimumRightItems };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Ensure we have accurate measurements for ALL items
|
|
529
|
+
if (
|
|
530
|
+
!this._cachedWidths ||
|
|
531
|
+
this._cachedWidths.itemWidths.length !== totalParts
|
|
532
|
+
) {
|
|
533
|
+
this._measureAllItemWidths(parts);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const homeWidth = this._cachedWidths!.home;
|
|
537
|
+
const separatorWidth = this._cachedWidths!.separator;
|
|
538
|
+
const ellipsisWidth = this._cachedWidths!.ellipsis;
|
|
539
|
+
const preferredWidth = this._cachedWidths!.preferred;
|
|
540
|
+
const itemWidths = this._cachedWidths!.itemWidths;
|
|
541
|
+
|
|
542
|
+
// Calculate available space for items
|
|
543
|
+
let fixedOverhead = homeWidth + separatorWidth;
|
|
544
|
+
if (this._hasPreferred) {
|
|
545
|
+
fixedOverhead += preferredWidth + separatorWidth;
|
|
546
|
+
}
|
|
547
|
+
const availableForItems = containerWidth - fixedOverhead;
|
|
548
|
+
|
|
549
|
+
// Check if all parts can fit without ellipsis
|
|
550
|
+
let totalWidth = 0;
|
|
551
|
+
for (let i = 0; i < totalParts; i++) {
|
|
552
|
+
totalWidth += itemWidths[i] + separatorWidth;
|
|
553
|
+
}
|
|
554
|
+
if (totalWidth <= availableForItems) {
|
|
555
|
+
this._lastRenderedWidth = fixedOverhead + totalWidth;
|
|
556
|
+
return { left: totalParts, right: 0 };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// calculate how many right items fit
|
|
560
|
+
const ellipsisOverhead = ellipsisWidth + separatorWidth;
|
|
561
|
+
const availableWithEllipsis = availableForItems - ellipsisOverhead;
|
|
562
|
+
|
|
563
|
+
// Account for left items first
|
|
564
|
+
let leftUsed = 0;
|
|
565
|
+
for (let i = 0; i < this._minimumLeftItems && i < totalParts; i++) {
|
|
566
|
+
leftUsed += itemWidths[i] + separatorWidth;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const availableForRight = availableWithEllipsis - leftUsed;
|
|
570
|
+
|
|
571
|
+
// Fill right items from the end
|
|
572
|
+
let rightItems = 0;
|
|
573
|
+
let usedWidth = 0;
|
|
574
|
+
for (let i = totalParts - 1; i >= this._minimumLeftItems; i--) {
|
|
575
|
+
const w = itemWidths[i] + separatorWidth;
|
|
576
|
+
if (usedWidth + w <= availableForRight) {
|
|
577
|
+
usedWidth += w;
|
|
578
|
+
rightItems++;
|
|
579
|
+
} else {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Ensure minimums are respected
|
|
585
|
+
const finalRight = Math.max(rightItems, this._minimumRightItems);
|
|
586
|
+
// Track the total rendered width for the immediate-collapse check.
|
|
587
|
+
// If minimums forced extra items, recalculate; otherwise reuse usedWidth.
|
|
588
|
+
let rightUsed = usedWidth;
|
|
589
|
+
if (finalRight > rightItems) {
|
|
590
|
+
rightUsed = 0;
|
|
591
|
+
for (let i = totalParts - finalRight; i < totalParts; i++) {
|
|
592
|
+
rightUsed += itemWidths[i] + separatorWidth;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
this._lastRenderedWidth =
|
|
596
|
+
fixedOverhead + ellipsisOverhead + leftUsed + rightUsed;
|
|
597
|
+
return {
|
|
598
|
+
left: this._minimumLeftItems,
|
|
599
|
+
right: finalRight
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
394
603
|
protected translator: ITranslator;
|
|
395
604
|
private _trans: TranslationBundle;
|
|
396
605
|
private _model: FileBrowserModel;
|
|
@@ -400,6 +609,16 @@ export class BreadCrumbs extends Widget {
|
|
|
400
609
|
private _previousState: Private.ICrumbsState | null = null;
|
|
401
610
|
private _minimumLeftItems: number;
|
|
402
611
|
private _minimumRightItems: number;
|
|
612
|
+
private _resizeObserver: ResizeObserver;
|
|
613
|
+
private _resizeThrottler: Throttler;
|
|
614
|
+
private _cachedWidths: {
|
|
615
|
+
home: number;
|
|
616
|
+
separator: number;
|
|
617
|
+
ellipsis: number;
|
|
618
|
+
preferred: number;
|
|
619
|
+
itemWidths: number[];
|
|
620
|
+
} | null = null;
|
|
621
|
+
private _lastRenderedWidth = 0;
|
|
403
622
|
}
|
|
404
623
|
|
|
405
624
|
/**
|
|
@@ -534,9 +753,7 @@ namespace Private {
|
|
|
534
753
|
const elemPath = parts.slice(0, i + 1).join('/');
|
|
535
754
|
const elem = createBreadcrumbElement(parts[i], elemPath);
|
|
536
755
|
node.appendChild(elem);
|
|
537
|
-
|
|
538
|
-
separator.textContent = '/';
|
|
539
|
-
node.appendChild(separator);
|
|
756
|
+
node.appendChild(createCrumbSeparator());
|
|
540
757
|
}
|
|
541
758
|
}
|
|
542
759
|
}
|
|
@@ -592,6 +809,7 @@ namespace Private {
|
|
|
592
809
|
*/
|
|
593
810
|
export function createCrumbSeparator(): HTMLElement {
|
|
594
811
|
const item = document.createElement('span');
|
|
812
|
+
item.className = BREADCRUMB_SEPARATOR_CLASS;
|
|
595
813
|
item.textContent = '/';
|
|
596
814
|
return item;
|
|
597
815
|
}
|
package/src/listing.ts
CHANGED
|
@@ -8,19 +8,13 @@ import {
|
|
|
8
8
|
showErrorMessage
|
|
9
9
|
} from '@jupyterlab/apputils';
|
|
10
10
|
import { PageConfig, PathExt, Time } from '@jupyterlab/coreutils';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
isValidFileName,
|
|
14
|
-
renameFile
|
|
15
|
-
} from '@jupyterlab/docmanager';
|
|
11
|
+
import type { IDocumentManager } from '@jupyterlab/docmanager';
|
|
12
|
+
import { isValidFileName, renameFile } from '@jupyterlab/docmanager';
|
|
16
13
|
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
17
|
-
import { Contents } from '@jupyterlab/services';
|
|
18
|
-
import { IStateDB } from '@jupyterlab/statedb';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
nullTranslator,
|
|
22
|
-
TranslationBundle
|
|
23
|
-
} from '@jupyterlab/translation';
|
|
14
|
+
import type { Contents } from '@jupyterlab/services';
|
|
15
|
+
import type { IStateDB } from '@jupyterlab/statedb';
|
|
16
|
+
import type { ITranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
17
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
24
18
|
import {
|
|
25
19
|
caretDownIcon,
|
|
26
20
|
caretUpIcon,
|
|
@@ -28,19 +22,19 @@ import {
|
|
|
28
22
|
LabIcon
|
|
29
23
|
} from '@jupyterlab/ui-components';
|
|
30
24
|
import { ArrayExt, filter, StringExt } from '@lumino/algorithm';
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
PromiseDelegate,
|
|
34
|
-
ReadonlyJSONObject
|
|
35
|
-
} from '@lumino/coreutils';
|
|
25
|
+
import type { ReadonlyJSONObject } from '@lumino/coreutils';
|
|
26
|
+
import { MimeData, PromiseDelegate } from '@lumino/coreutils';
|
|
36
27
|
import { ElementExt } from '@lumino/domutils';
|
|
37
|
-
import {
|
|
28
|
+
import type { IDisposable } from '@lumino/disposable';
|
|
29
|
+
import { DisposableDelegate } from '@lumino/disposable';
|
|
38
30
|
import { Drag } from '@lumino/dragdrop';
|
|
39
|
-
import { Message
|
|
40
|
-
import {
|
|
31
|
+
import type { Message } from '@lumino/messaging';
|
|
32
|
+
import { MessageLoop } from '@lumino/messaging';
|
|
33
|
+
import type { ISignal } from '@lumino/signaling';
|
|
34
|
+
import { Signal } from '@lumino/signaling';
|
|
41
35
|
import { h, VirtualDOM } from '@lumino/virtualdom';
|
|
42
36
|
import { Widget } from '@lumino/widgets';
|
|
43
|
-
import { FilterFileBrowserModel } from './model';
|
|
37
|
+
import type { FilterFileBrowserModel } from './model';
|
|
44
38
|
|
|
45
39
|
/**
|
|
46
40
|
* The class name added to DirListing widget.
|
|
@@ -356,6 +350,7 @@ export class DirListing extends Widget {
|
|
|
356
350
|
this.model.items(),
|
|
357
351
|
state,
|
|
358
352
|
this._sortNotebooksFirst,
|
|
353
|
+
this._sortFileNamesNaturally,
|
|
359
354
|
this.translator
|
|
360
355
|
);
|
|
361
356
|
this._sortState = state;
|
|
@@ -962,8 +957,8 @@ export class DirListing extends Widget {
|
|
|
962
957
|
this._modifiedWidth < 100
|
|
963
958
|
? 'narrow'
|
|
964
959
|
: this._modifiedWidth > 120
|
|
965
|
-
|
|
966
|
-
|
|
960
|
+
? 'long'
|
|
961
|
+
: 'short';
|
|
967
962
|
}
|
|
968
963
|
|
|
969
964
|
/**
|
|
@@ -1361,6 +1356,19 @@ export class DirListing extends Widget {
|
|
|
1361
1356
|
}
|
|
1362
1357
|
}
|
|
1363
1358
|
|
|
1359
|
+
/**
|
|
1360
|
+
* Update the setting to sort file names naturally
|
|
1361
|
+
* vs lexicographically. Default is true (natural).
|
|
1362
|
+
* This sorts the items again if the internal value is modified.
|
|
1363
|
+
*/
|
|
1364
|
+
setSortFileNamesNaturally(natural: boolean): void {
|
|
1365
|
+
const previousValue = this._sortFileNamesNaturally;
|
|
1366
|
+
this._sortFileNamesNaturally = natural;
|
|
1367
|
+
if (this._sortFileNamesNaturally !== previousValue) {
|
|
1368
|
+
this.sort(this._sortState);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1364
1372
|
/**
|
|
1365
1373
|
* Update the setting to allow single click navigation.
|
|
1366
1374
|
* This enables opening files/directories with a single click.
|
|
@@ -2121,7 +2129,7 @@ export class DirListing extends Widget {
|
|
|
2121
2129
|
} as DirListing.IContentsThunk);
|
|
2122
2130
|
}
|
|
2123
2131
|
|
|
2124
|
-
if (item
|
|
2132
|
+
if (item.type !== 'directory') {
|
|
2125
2133
|
const otherPaths = selectedPaths.slice(1).reverse();
|
|
2126
2134
|
this._drag.mimeData.setData(FACTORY_MIME, () => {
|
|
2127
2135
|
if (!item) {
|
|
@@ -2541,6 +2549,9 @@ export class DirListing extends Widget {
|
|
|
2541
2549
|
* Handle a `pathChanged` signal from the model.
|
|
2542
2550
|
*/
|
|
2543
2551
|
private _onPathChanged(): void {
|
|
2552
|
+
// Check if the directory listing (or any of its children) has focus.
|
|
2553
|
+
const hasFocus = this.node.contains(document.activeElement);
|
|
2554
|
+
|
|
2544
2555
|
// Reset the selection.
|
|
2545
2556
|
this.clearSelectedItems();
|
|
2546
2557
|
// Update the sorted items.
|
|
@@ -2548,7 +2559,16 @@ export class DirListing extends Widget {
|
|
|
2548
2559
|
// Reset focus. But wait until the DOM has been updated (hence
|
|
2549
2560
|
// `requestAnimationFrame`).
|
|
2550
2561
|
requestAnimationFrame(() => {
|
|
2551
|
-
this.
|
|
2562
|
+
if (this.isDisposed) {
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
// Only focus the first item if the listing (or a child) previously had focus.
|
|
2566
|
+
if (hasFocus) {
|
|
2567
|
+
this._focusItem(0);
|
|
2568
|
+
} else {
|
|
2569
|
+
// Otherwise, just reset the internal focus index without moving DOM focus.
|
|
2570
|
+
this._focusIndex = 0;
|
|
2571
|
+
}
|
|
2552
2572
|
});
|
|
2553
2573
|
}
|
|
2554
2574
|
|
|
@@ -2645,6 +2665,7 @@ export class DirListing extends Widget {
|
|
|
2645
2665
|
last_modified: null
|
|
2646
2666
|
};
|
|
2647
2667
|
private _sortNotebooksFirst = false;
|
|
2668
|
+
private _sortFileNamesNaturally = true;
|
|
2648
2669
|
private _allowSingleClick = false;
|
|
2649
2670
|
private _allowDragDropUpload = true;
|
|
2650
2671
|
// _focusIndex should never be set outside the range [0, this._items.length - 1]
|
|
@@ -3679,6 +3700,7 @@ namespace Private {
|
|
|
3679
3700
|
items: Iterable<Contents.IModel>,
|
|
3680
3701
|
state: DirListing.ISortState,
|
|
3681
3702
|
sortNotebooksFirst: boolean = false,
|
|
3703
|
+
sortFileNamesNaturally: boolean = true,
|
|
3682
3704
|
translator: ITranslator
|
|
3683
3705
|
): Contents.IModel[] {
|
|
3684
3706
|
const copy = Array.from(items);
|
|
@@ -3710,6 +3732,7 @@ namespace Private {
|
|
|
3710
3732
|
|
|
3711
3733
|
/**
|
|
3712
3734
|
* Compare two items by their name using `translator.languageCode`, with fallback to `navigator.language`.
|
|
3735
|
+
* When sortFileNamesNaturally is true, uses natural order.
|
|
3713
3736
|
*/
|
|
3714
3737
|
function compareByName(a: Contents.IModel, b: Contents.IModel) {
|
|
3715
3738
|
// Wokaround for Chromium invalid language code on CI, see
|
|
@@ -3718,19 +3741,17 @@ namespace Private {
|
|
|
3718
3741
|
const languageCode = (
|
|
3719
3742
|
translator.languageCode ?? navigatorLanguage
|
|
3720
3743
|
).replace('_', '-');
|
|
3744
|
+
const localeOptions: Intl.CollatorOptions = {
|
|
3745
|
+
numeric: sortFileNamesNaturally,
|
|
3746
|
+
sensitivity: 'base'
|
|
3747
|
+
};
|
|
3721
3748
|
try {
|
|
3722
|
-
return a.name.localeCompare(b.name, languageCode,
|
|
3723
|
-
numeric: true,
|
|
3724
|
-
sensitivity: 'base'
|
|
3725
|
-
});
|
|
3749
|
+
return a.name.localeCompare(b.name, languageCode, localeOptions);
|
|
3726
3750
|
} catch (e) {
|
|
3727
3751
|
console.warn(
|
|
3728
3752
|
`localeCompare failed to compare ${a.name} and ${b.name} under languageCode: ${languageCode}`
|
|
3729
3753
|
);
|
|
3730
|
-
return a.name.localeCompare(b.name, navigatorLanguage,
|
|
3731
|
-
numeric: true,
|
|
3732
|
-
sensitivity: 'base'
|
|
3733
|
-
});
|
|
3754
|
+
return a.name.localeCompare(b.name, navigatorLanguage, localeOptions);
|
|
3734
3755
|
}
|
|
3735
3756
|
}
|
|
3736
3757
|
|
package/src/model.ts
CHANGED
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
3
|
|
|
4
4
|
import { Dialog, showDialog } from '@jupyterlab/apputils';
|
|
5
|
-
import { IChangedArgs
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from '@jupyterlab/
|
|
14
|
-
import { IFilterBoxProps, IScore } from '@jupyterlab/ui-components';
|
|
5
|
+
import type { IChangedArgs } from '@jupyterlab/coreutils';
|
|
6
|
+
import { PageConfig, PathExt } from '@jupyterlab/coreutils';
|
|
7
|
+
import type { IDocumentManager } from '@jupyterlab/docmanager';
|
|
8
|
+
import { shouldOverwrite } from '@jupyterlab/docmanager';
|
|
9
|
+
import type { Contents, KernelSpec, Session } from '@jupyterlab/services';
|
|
10
|
+
import type { IStateDB } from '@jupyterlab/statedb';
|
|
11
|
+
import type { ITranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
12
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
13
|
+
import type { IFilterBoxProps, IScore } from '@jupyterlab/ui-components';
|
|
15
14
|
import { ArrayExt, filter } from '@lumino/algorithm';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
import type { ReadonlyJSONObject } from '@lumino/coreutils';
|
|
16
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
17
|
+
import type { IDisposable } from '@lumino/disposable';
|
|
18
18
|
import { Poll } from '@lumino/polling';
|
|
19
|
-
import { ISignal
|
|
19
|
+
import type { ISignal } from '@lumino/signaling';
|
|
20
|
+
import { Signal } from '@lumino/signaling';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* The default duration of the auto-refresh in ms
|
|
@@ -633,10 +634,10 @@ export class FileBrowserModel implements IDisposable {
|
|
|
633
634
|
prefix + PathExt.dirname(oldValue.path) === path
|
|
634
635
|
? oldValue
|
|
635
636
|
: newValue &&
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
637
|
+
newValue.path &&
|
|
638
|
+
prefix + PathExt.dirname(newValue.path) === path
|
|
639
|
+
? newValue
|
|
640
|
+
: undefined;
|
|
640
641
|
|
|
641
642
|
// If either the old value or the new value is in the current path, update.
|
|
642
643
|
if (value) {
|
package/src/opendialog.ts
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
import { Dialog, setToolbar, ToolbarButton } from '@jupyterlab/apputils';
|
|
5
5
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
6
|
-
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
7
|
-
import { Contents } from '@jupyterlab/services';
|
|
8
|
-
import { ITranslator
|
|
9
|
-
import {
|
|
6
|
+
import type { IDocumentManager } from '@jupyterlab/docmanager';
|
|
7
|
+
import type { Contents } from '@jupyterlab/services';
|
|
8
|
+
import type { ITranslator } from '@jupyterlab/translation';
|
|
9
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
10
|
+
import type { IScore } from '@jupyterlab/ui-components';
|
|
11
|
+
import { newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
|
|
10
12
|
import { PanelLayout, Widget } from '@lumino/widgets';
|
|
11
13
|
import { FileBrowser } from './browser';
|
|
12
14
|
import { FilterFileBrowserModel } from './model';
|
|
13
|
-
import { IFileBrowserFactory } from './tokens';
|
|
15
|
+
import type { IFileBrowserFactory } from './tokens';
|
|
14
16
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -30,16 +32,15 @@ export namespace FileDialog {
|
|
|
30
32
|
/**
|
|
31
33
|
* Options for the open directory dialog
|
|
32
34
|
*/
|
|
33
|
-
export interface IDirectoryOptions
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'body' | 'buttons' | 'defaultButton'
|
|
40
|
-
>
|
|
35
|
+
export interface IDirectoryOptions extends Partial<
|
|
36
|
+
Pick<
|
|
37
|
+
Dialog.IOptions<Promise<Contents.IModel[]>>,
|
|
38
|
+
Exclude<
|
|
39
|
+
keyof Dialog.IOptions<Promise<Contents.IModel[]>>,
|
|
40
|
+
'body' | 'buttons' | 'defaultButton'
|
|
41
41
|
>
|
|
42
|
-
>
|
|
42
|
+
>
|
|
43
|
+
> {
|
|
43
44
|
/**
|
|
44
45
|
* Document manager
|
|
45
46
|
*/
|
package/src/tokens.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { WidgetTracker } from '@jupyterlab/apputils';
|
|
|
5
5
|
import type { IStateDB } from '@jupyterlab/statedb';
|
|
6
6
|
import { Token } from '@lumino/coreutils';
|
|
7
7
|
import type { FileBrowser } from './browser';
|
|
8
|
+
import type { DirListing } from './listing';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The file browser factory token.
|
|
@@ -24,6 +25,17 @@ export const IDefaultFileBrowser = new Token<IDefaultFileBrowser>(
|
|
|
24
25
|
'A service for the default file browser.'
|
|
25
26
|
);
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The default file browser renderer token.
|
|
30
|
+
*/
|
|
31
|
+
export interface IDefaultFileBrowserRenderer extends DirListing.IRenderer {}
|
|
32
|
+
|
|
33
|
+
export const IDefaultFileBrowserRenderer =
|
|
34
|
+
new Token<IDefaultFileBrowserRenderer>(
|
|
35
|
+
'@jupyterlab/filebrowser:IDefaultFileBrowserRenderer',
|
|
36
|
+
'A service for overriding the default file browser directory listing renderer.'
|
|
37
|
+
);
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* Default file browser type.
|
|
29
41
|
*/
|
|
@@ -120,6 +132,14 @@ export namespace IFileBrowserFactory {
|
|
|
120
132
|
* Whether to allow file uploads. Defaults to `true`.
|
|
121
133
|
*/
|
|
122
134
|
allowFileUploads?: boolean;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* An optional renderer for the directory listing area.
|
|
138
|
+
*
|
|
139
|
+
* #### Notes
|
|
140
|
+
* If not provided, the default renderer will be used.
|
|
141
|
+
*/
|
|
142
|
+
renderer?: DirListing.IRenderer;
|
|
123
143
|
}
|
|
124
144
|
}
|
|
125
145
|
|
package/src/upload.ts
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
3
|
|
|
4
4
|
import { showErrorMessage } from '@jupyterlab/apputils';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
nullTranslator,
|
|
8
|
-
TranslationBundle
|
|
9
|
-
} from '@jupyterlab/translation';
|
|
5
|
+
import type { ITranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
6
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
10
7
|
import { fileUploadIcon, ToolbarButton } from '@jupyterlab/ui-components';
|
|
11
|
-
import { FileBrowserModel } from './model';
|
|
12
|
-
import { Contents } from '@jupyterlab/services';
|
|
13
|
-
import { ISignal
|
|
8
|
+
import type { FileBrowserModel } from './model';
|
|
9
|
+
import type { Contents } from '@jupyterlab/services';
|
|
10
|
+
import type { ISignal } from '@lumino/signaling';
|
|
11
|
+
import { Signal } from '@lumino/signaling';
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* A widget which provides an upload button.
|