@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/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
- ITranslator,
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 { Drag } from '@lumino/dragdrop';
19
- import { Message } from '@lumino/messaging';
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: this._minimumLeftItems,
190
- minimumRightItems: this._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
- const separator = document.createElement('span');
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
- IDocumentManager,
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
- ITranslator,
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
- MimeData,
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 { DisposableDelegate, IDisposable } from '@lumino/disposable';
28
+ import type { IDisposable } from '@lumino/disposable';
29
+ import { DisposableDelegate } from '@lumino/disposable';
38
30
  import { Drag } from '@lumino/dragdrop';
39
- import { Message, MessageLoop } from '@lumino/messaging';
40
- import { ISignal, Signal } from '@lumino/signaling';
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
- ? 'long'
966
- : 'short';
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 && item.type !== 'directory') {
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._focusItem(0);
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, PageConfig, PathExt } from '@jupyterlab/coreutils';
6
- import { IDocumentManager, shouldOverwrite } from '@jupyterlab/docmanager';
7
- import { Contents, KernelSpec, Session } from '@jupyterlab/services';
8
- import { IStateDB } from '@jupyterlab/statedb';
9
- import {
10
- ITranslator,
11
- nullTranslator,
12
- TranslationBundle
13
- } from '@jupyterlab/translation';
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 { PromiseDelegate, ReadonlyJSONObject } from '@lumino/coreutils';
17
- import { IDisposable } from '@lumino/disposable';
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, Signal } from '@lumino/signaling';
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
- newValue.path &&
637
- prefix + PathExt.dirname(newValue.path) === path
638
- ? newValue
639
- : undefined;
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, nullTranslator } from '@jupyterlab/translation';
9
- import { IScore, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
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
- extends Partial<
35
- Pick<
36
- Dialog.IOptions<Promise<Contents.IModel[]>>,
37
- Exclude<
38
- keyof Dialog.IOptions<Promise<Contents.IModel[]>>,
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
- ITranslator,
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, Signal } from '@lumino/signaling';
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.