@jupyterlab/filebrowser 4.6.0-alpha.3 → 4.6.0-alpha.5

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/listing.js CHANGED
@@ -63,6 +63,10 @@ const ITEM_MODIFIED_CLASS = 'jp-DirListing-itemModified';
63
63
  * The class name added to the listing item file size cell.
64
64
  */
65
65
  const ITEM_FILE_SIZE_CLASS = 'jp-DirListing-itemFileSize';
66
+ /**
67
+ * The class name added to the listing item created cell.
68
+ */
69
+ const ITEM_CREATED_CLASS = 'jp-DirListing-itemCreated';
66
70
  /**
67
71
  * The class name added to the label element that wraps each item's checkbox and
68
72
  * the header's check-all checkbox.
@@ -84,6 +88,10 @@ const MODIFIED_ID_CLASS = 'jp-id-modified';
84
88
  * The class name added to the file size column header cell.
85
89
  */
86
90
  const FILE_SIZE_ID_CLASS = 'jp-id-filesize';
91
+ /**
92
+ * The class name added to the created column header cell.
93
+ */
94
+ const CREATED_ID_CLASS = 'jp-id-created';
87
95
  /**
88
96
  * The mime type for a contents drag object.
89
97
  */
@@ -192,7 +200,8 @@ export class DirListing extends Widget {
192
200
  name: null,
193
201
  file_size: null,
194
202
  is_selected: null,
195
- last_modified: null
203
+ last_modified: null,
204
+ date_created: null
196
205
  };
197
206
  this._sortNotebooksFirst = false;
198
207
  this._sortFileNamesNaturally = true;
@@ -200,6 +209,9 @@ export class DirListing extends Widget {
200
209
  this._allowDragDropUpload = true;
201
210
  // _focusIndex should never be set outside the range [0, this._items.length - 1]
202
211
  this._focusIndex = 0;
212
+ this._focusPath = '';
213
+ this._modifiedStyle = 'short';
214
+ this._createdStyle = 'short';
203
215
  this._allUploaded = new Signal(this);
204
216
  this._width = null;
205
217
  this._state = null;
@@ -228,9 +240,13 @@ export class DirListing extends Widget {
228
240
  });
229
241
  // Get the width of the "modified" column
230
242
  this._updateModifiedSize(this.node);
243
+ // Get the width of the "created" column
244
+ this._updateCreatedSize(this.node);
231
245
  const headerNode = DOMUtils.findElement(this.node, HEADER_CLASS);
232
246
  // hide the file size column by default
233
247
  this._hiddenColumns.add('file_size');
248
+ // hide the date created column by default
249
+ this._hiddenColumns.add('date_created');
234
250
  this._renderer.populateHeaderNode(headerNode, this.translator, this._hiddenColumns, this._columnSizes);
235
251
  this._manager.activateRequested.connect(this._onActivateRequested, this);
236
252
  }
@@ -319,6 +335,7 @@ export class DirListing extends Widget {
319
335
  this._sortedItems = Private.sort(this.model.items(), state, this._sortNotebooksFirst, this._sortFileNamesNaturally, this.translator);
320
336
  this._sortState = state;
321
337
  this.update();
338
+ this._saveState();
322
339
  }
323
340
  /**
324
341
  * Rename the first currently selected item.
@@ -491,18 +508,43 @@ export class DirListing extends Widget {
491
508
  return;
492
509
  }
493
510
  const sizes = columns['sizes'];
494
- if (!sizes) {
495
- return;
496
- }
497
- for (const [key, size] of Object.entries(sizes)) {
498
- this._columnSizes[key] = size;
511
+ const sortState = columns['sortState'];
512
+ // Set sort state before updating column sizes so that any
513
+ // state save triggered by _updateColumnSizes includes the
514
+ // correct sort state rather than the default.
515
+ if (sortState) {
516
+ this._sortState = sortState;
517
+ }
518
+ if (sizes) {
519
+ for (const [key, size] of Object.entries(sizes)) {
520
+ this._columnSizes[key] = size;
521
+ }
522
+ this._updateColumnSizes();
523
+ }
524
+ // Sort items and rebuild the header to reflect the restored
525
+ // sort state without calling sort() to avoid a redundant save.
526
+ if (sortState) {
527
+ this._sortedItems = Private.sort(this.model.items(), sortState, this._sortNotebooksFirst, this._sortFileNamesNaturally, this.translator);
528
+ this.headerNode.innerHTML = '';
529
+ this._renderer.populateHeaderNode(this.headerNode, this.translator, this._hiddenColumns, this._columnSizes, sortState);
530
+ this.update();
499
531
  }
500
- this._updateColumnSizes();
501
532
  }
502
533
  catch (error) {
503
534
  await state.remove(key);
504
535
  }
505
536
  }
537
+ /**
538
+ * Save the current column sizes and sort state to the state database.
539
+ */
540
+ _saveState() {
541
+ if (this._state && this._stateColumnsKey) {
542
+ void this._state.save(this._stateColumnsKey, {
543
+ sizes: this._columnSizes,
544
+ sortState: { ...this._sortState }
545
+ });
546
+ }
547
+ }
506
548
  /**
507
549
  * Shut down kernels on the applicable currently selected items.
508
550
  *
@@ -639,6 +681,30 @@ export class DirListing extends Widget {
639
681
  this._selectionChanged.emit();
640
682
  }
641
683
  }
684
+ /**
685
+ * Move focus to selected item, or to the first item if no item is selected.
686
+ */
687
+ _focusContent() {
688
+ const items = this._sortedItems;
689
+ if (items.length === 0) {
690
+ this._focusItem(0);
691
+ return;
692
+ }
693
+ let index = this._focusPath !== ''
694
+ ? ArrayExt.findFirstIndex(items, value => value.path === this._focusPath)
695
+ : -1;
696
+ // Fallbacks if path is missing/not found.
697
+ if (index === -1) {
698
+ index = Math.min(Math.max(this._focusIndex, 0), items.length - 1);
699
+ }
700
+ this._selectItem(index, false, true);
701
+ }
702
+ /**
703
+ * Handle `'activate-request'` messages.
704
+ */
705
+ onActivateRequest(msg) {
706
+ this._focusContent();
707
+ }
642
708
  /**
643
709
  * Select an item by name.
644
710
  *
@@ -826,32 +892,47 @@ export class DirListing extends Widget {
826
892
  return width - this._paddingWidth * 2 - this._contentScrollbarWidth;
827
893
  }
828
894
  /**
829
- * Update the modified column's size
895
+ * Update the 'modified' column's size
830
896
  */
831
897
  _updateModifiedSize(node) {
898
+ this._modifiedStyle = this._computeDateColumnStyle(node, MODIFIED_ID_CLASS, 'last_modified');
899
+ }
900
+ /**
901
+ * Update the 'date created' column's size
902
+ */
903
+ _updateCreatedSize(node) {
904
+ this._createdStyle = this._computeDateColumnStyle(node, CREATED_ID_CLASS, 'date_created');
905
+ }
906
+ /**
907
+ * Compute the Intl.RelativeTimeFormat style for a date column based on
908
+ * its pixel width: < 100px → 'narrow', 100–120px → 'short', > 120px → 'long'.
909
+ */
910
+ _computeDateColumnStyle(node, columnClassName, columnName) {
832
911
  var _a, _b;
833
- // Look for the modified column's header
834
- const modified = DOMUtils.findElement(node, MODIFIED_ID_CLASS);
835
- this._modifiedWidth =
836
- (_b = (_a = this._columnSizes['last_modified']) !== null && _a !== void 0 ? _a : modified === null || modified === void 0 ? void 0 : modified.getBoundingClientRect().width) !== null && _b !== void 0 ? _b : 83;
837
- this._modifiedStyle =
838
- this._modifiedWidth < 100
839
- ? 'narrow'
840
- : this._modifiedWidth > 120
841
- ? 'long'
842
- : 'short';
912
+ const columnElement = DOMUtils.findElement(node, columnClassName);
913
+ const width = (_b = (_a = this._columnSizes[columnName]) !== null && _a !== void 0 ? _a : columnElement === null || columnElement === void 0 ? void 0 : columnElement.getBoundingClientRect().width) !== null && _b !== void 0 ? _b : 83;
914
+ return width < 100 ? 'narrow' : width > 120 ? 'long' : 'short';
843
915
  }
844
916
  /**
845
917
  * Rerender item nodes' modified dates, if the modified style has changed.
846
918
  */
847
919
  _updateModifiedStyleAndSize() {
848
920
  const oldModifiedStyle = this._modifiedStyle;
849
- // Update both size and style
850
921
  this._updateModifiedSize(this.node);
851
922
  if (oldModifiedStyle !== this._modifiedStyle) {
852
923
  this.updateModified(this._sortedItems, this._items);
853
924
  }
854
925
  }
926
+ /**
927
+ * Rerender item nodes' created dates, if the created style has changed.
928
+ */
929
+ _updateCreatedStyleAndSize() {
930
+ const oldCreatedStyle = this._createdStyle;
931
+ this._updateCreatedSize(this.node);
932
+ if (oldCreatedStyle !== this._createdStyle) {
933
+ this.updateCreated(this._sortedItems, this._items);
934
+ }
935
+ }
855
936
  /**
856
937
  * Update only the modified dates.
857
938
  */
@@ -860,6 +941,9 @@ export class DirListing extends Widget {
860
941
  const node = nodes[i];
861
942
  if (node && item.last_modified) {
862
943
  const modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
944
+ if (!modified) {
945
+ return;
946
+ }
863
947
  if (this.renderer.updateItemModified !== undefined) {
864
948
  this.renderer.updateItemModified(modified, item.last_modified, this._modifiedStyle);
865
949
  }
@@ -869,6 +953,26 @@ export class DirListing extends Widget {
869
953
  }
870
954
  });
871
955
  }
956
+ /**
957
+ * Update only the created dates.
958
+ */
959
+ updateCreated(items, nodes) {
960
+ items.forEach((item, i) => {
961
+ const node = nodes[i];
962
+ if (node && item.created) {
963
+ const created = DOMUtils.findElement(node, ITEM_CREATED_CLASS);
964
+ if (!created) {
965
+ return;
966
+ }
967
+ if (this.renderer.updateItemCreated !== undefined) {
968
+ this.renderer.updateItemCreated(created, item.created, this._createdStyle);
969
+ }
970
+ else {
971
+ DirListing.defaultRenderer.updateItemCreated(created, item.created, this._createdStyle);
972
+ }
973
+ }
974
+ });
975
+ }
872
976
  // Update item nodes based on widget state.
873
977
  updateNodes(items, nodes, sizeOnly = false) {
874
978
  var _a;
@@ -879,10 +983,10 @@ export class DirListing extends Widget {
879
983
  // short-circuit in case if node is not yet ready
880
984
  return;
881
985
  }
882
- return this.renderer.updateItemSize(node, item, this._modifiedStyle, this._columnSizes);
986
+ return this.renderer.updateItemSize(node, item, this._modifiedStyle, this._columnSizes, this._createdStyle);
883
987
  }
884
988
  const ft = this._manager.registry.getFileTypeForModel(item);
885
- this.renderer.updateItemNode(node, item, ft, this.translator, this._hiddenColumns, this.selection[item.path], this._modifiedStyle, this._columnSizes);
989
+ this.renderer.updateItemNode(node, item, ft, this.translator, this._hiddenColumns, this.selection[item.path], this._modifiedStyle, this._columnSizes, this._createdStyle);
886
990
  if (this.selection[item.path] &&
887
991
  this._isCut &&
888
992
  this._model.path === this._prevPath) {
@@ -1011,7 +1115,7 @@ export class DirListing extends Widget {
1011
1115
  this._hiddenColumns.add(name);
1012
1116
  }
1013
1117
  this.headerNode.innerHTML = '';
1014
- this._renderer.populateHeaderNode(this.headerNode, this.translator, this._hiddenColumns, this._columnSizes);
1118
+ this._renderer.populateHeaderNode(this.headerNode, this.translator, this._hiddenColumns, this._columnSizes, this._sortState);
1015
1119
  this._updateColumnSizes();
1016
1120
  }
1017
1121
  _updateColumnSizes(doNotGrowBeforeInclusive = null) {
@@ -1090,6 +1194,7 @@ export class DirListing extends Widget {
1090
1194
  column.element.style.width = size === null ? '' : size + 'px';
1091
1195
  }
1092
1196
  this._updateModifiedStyleAndSize();
1197
+ this._updateCreatedStyleAndSize();
1093
1198
  // Refresh sizes on the per item widths
1094
1199
  if (this.isVisible) {
1095
1200
  const items = this._items;
@@ -1097,11 +1202,7 @@ export class DirListing extends Widget {
1097
1202
  this.updateNodes(this._sortedItems, this._items, true);
1098
1203
  }
1099
1204
  }
1100
- if (this._state && this._stateColumnsKey) {
1101
- void this._state.save(this._stateColumnsKey, {
1102
- sizes: this._columnSizes
1103
- });
1104
- }
1205
+ this._saveState();
1105
1206
  }
1106
1207
  get _visibleColumns() {
1107
1208
  return DirListing.columns.filter(column => { var _a; return column.id === 'name' || !((_a = this._hiddenColumns) === null || _a === void 0 ? void 0 : _a.has(column.id)); });
@@ -1474,6 +1575,10 @@ export class DirListing extends Widget {
1474
1575
  if (model.path === model.rootPath) {
1475
1576
  return;
1476
1577
  }
1578
+ // Check if we're at the navigation root boundary
1579
+ if (model.root && model.path === model.root) {
1580
+ return;
1581
+ }
1477
1582
  try {
1478
1583
  await model.cd('..');
1479
1584
  }
@@ -1489,9 +1594,8 @@ export class DirListing extends Widget {
1489
1594
  if (this._inRename) {
1490
1595
  return;
1491
1596
  }
1492
- switch (event.keyCode) {
1493
- case 13: {
1494
- // Enter
1597
+ switch (event.key) {
1598
+ case 'Enter': {
1495
1599
  // Do nothing if any modifier keys are pressed.
1496
1600
  if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
1497
1601
  return;
@@ -1503,16 +1607,14 @@ export class DirListing extends Widget {
1503
1607
  }
1504
1608
  return;
1505
1609
  }
1506
- case 38:
1507
- // Up arrow
1610
+ case 'ArrowUp':
1508
1611
  this._handleArrowY(event, -1);
1509
1612
  return;
1510
- case 40:
1511
- // Down arrow
1613
+ case 'ArrowDown':
1512
1614
  this._handleArrowY(event, 1);
1513
1615
  return;
1514
- case 32: {
1515
- // Space
1616
+ case ' ':
1617
+ case 'Spacebar': {
1516
1618
  if (event.ctrlKey) {
1517
1619
  // Follow the Windows and Ubuntu convention: you must press `ctrl` +
1518
1620
  // `space` in order to toggle whether an item is selected.
@@ -1555,7 +1657,7 @@ export class DirListing extends Widget {
1555
1657
  event.key.length === 1 &&
1556
1658
  // Don't gobble up the space key on the check-all checkbox (which the
1557
1659
  // browser treats as a click event).
1558
- !((event.key === ' ' || event.keyCode === 32) &&
1660
+ !((event.key === ' ' || event.key === 'Spacebar') &&
1559
1661
  event.target.type === 'checkbox')) {
1560
1662
  if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
1561
1663
  return;
@@ -1908,16 +2010,20 @@ export class DirListing extends Widget {
1908
2010
  * @param index The index of the item node to focus
1909
2011
  */
1910
2012
  _focusItem(index) {
2013
+ var _a, _b;
1911
2014
  const items = this._items;
1912
2015
  if (items.length === 0) {
1913
2016
  // Focus the top node if the folder is empty and therefore there are no
1914
2017
  // items inside the folder to focus.
1915
2018
  this._focusIndex = 0;
2019
+ this._focusPath = '';
1916
2020
  this.node.focus();
1917
2021
  return;
1918
2022
  }
1919
- this._focusIndex = index;
1920
- const node = items[index];
2023
+ const clampedIndex = Math.min(Math.max(index, 0), items.length - 1);
2024
+ this._focusIndex = clampedIndex;
2025
+ this._focusPath = (_b = (_a = this._sortedItems[clampedIndex]) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '';
2026
+ const node = items[clampedIndex];
1921
2027
  const nameNode = this.renderer.getNameNode(node);
1922
2028
  if (nameNode) {
1923
2029
  // Make the filename text node focusable so that it receives keyboard
@@ -2260,6 +2366,16 @@ export class DirListing extends Widget {
2260
2366
  caretSide: 'left',
2261
2367
  grow: 1
2262
2368
  },
2369
+ {
2370
+ id: 'date_created',
2371
+ className: CREATED_ID_CLASS,
2372
+ itemClassName: ITEM_CREATED_CLASS,
2373
+ minWidth: 60,
2374
+ resizable: true,
2375
+ sortable: true,
2376
+ caretSide: 'left',
2377
+ grow: 1
2378
+ },
2263
2379
  {
2264
2380
  id: 'file_size',
2265
2381
  className: FILE_SIZE_ID_CLASS,
@@ -2301,12 +2417,21 @@ export class DirListing extends Widget {
2301
2417
  fileSize.className = ITEM_FILE_SIZE_CLASS;
2302
2418
  return fileSize;
2303
2419
  },
2420
+ date_created: () => {
2421
+ const created = document.createElement('span');
2422
+ created.className = ITEM_CREATED_CLASS;
2423
+ return created;
2424
+ },
2304
2425
  is_selected: () => this.createCheckboxWrapperNode()
2305
2426
  };
2306
2427
  /**
2307
2428
  * Register of most recent arguments for last modified column update.
2308
2429
  */
2309
2430
  this._modifiedColumnLastUpdate = new WeakMap();
2431
+ /**
2432
+ * Register of most recent arguments for date created column update.
2433
+ */
2434
+ this._createdColumnLastUpdate = new WeakMap();
2310
2435
  this._lastRenderedState = new WeakMap();
2311
2436
  }
2312
2437
  /**
@@ -2331,7 +2456,7 @@ export class DirListing extends Widget {
2331
2456
  *
2332
2457
  * @param node - The header node to populate.
2333
2458
  */
2334
- populateHeaderNode(node, translator, hiddenColumns, columnsSizes) {
2459
+ populateHeaderNode(node, translator, hiddenColumns, columnsSizes, sortState) {
2335
2460
  translator = translator || nullTranslator;
2336
2461
  const trans = translator.load('jupyterlab');
2337
2462
  const elementCreators = {
@@ -2344,6 +2469,10 @@ export class DirListing extends Widget {
2344
2469
  small: trans.__('Size'),
2345
2470
  large: trans.__('File Size')
2346
2471
  }),
2472
+ date_created: () => this._createHeaderItemNodeWithSizes({
2473
+ small: trans.__('Created'),
2474
+ large: trans.__('Date Created')
2475
+ }),
2347
2476
  is_selected: () => this.createCheckboxWrapperNode({
2348
2477
  alwaysVisible: true,
2349
2478
  headerNode: true
@@ -2369,10 +2498,7 @@ export class DirListing extends Widget {
2369
2498
  node.appendChild(resizer);
2370
2499
  }
2371
2500
  }
2372
- const name = DOMUtils.findElement(node, NAME_ID_CLASS);
2373
- name.classList.add(SELECTED_CLASS);
2374
- // set the initial caret icon
2375
- Private.updateCaret(DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS), 'right', 'up');
2501
+ this._updateSortIndicator(node, sortState !== null && sortState !== void 0 ? sortState : { direction: 'ascending', key: 'name' });
2376
2502
  }
2377
2503
  /**
2378
2504
  * Handle a header click.
@@ -2384,52 +2510,52 @@ export class DirListing extends Widget {
2384
2510
  * @returns The sort state of the header after the click event.
2385
2511
  */
2386
2512
  handleHeaderClick(node, event) {
2387
- const state = { direction: 'ascending', key: 'name' };
2388
2513
  const target = event.target;
2389
2514
  const sortableColumns = DirListing.columns.filter(Private.isSortable);
2390
2515
  for (const column of sortableColumns) {
2391
2516
  const header = node.querySelector(`.${column.className}`);
2392
2517
  if (!header) {
2393
- // skip if the column is hidden
2394
2518
  continue;
2395
2519
  }
2396
2520
  if (header.contains(target)) {
2397
- state.key = column.id;
2398
- const headerIcon = DOMUtils.findElement(header, HEADER_ITEM_ICON_CLASS);
2399
- if (header.classList.contains(SELECTED_CLASS)) {
2400
- if (!header.classList.contains(DESCENDING_CLASS)) {
2401
- state.direction = 'descending';
2402
- header.classList.add(DESCENDING_CLASS);
2403
- Private.updateCaret(headerIcon, column.caretSide, 'down');
2404
- }
2405
- else {
2406
- header.classList.remove(DESCENDING_CLASS);
2407
- Private.updateCaret(headerIcon, column.caretSide, 'up');
2408
- }
2521
+ const isSelected = header.classList.contains(SELECTED_CLASS);
2522
+ const isDescending = header.classList.contains(DESCENDING_CLASS);
2523
+ let direction = 'ascending';
2524
+ if (isSelected && !isDescending) {
2525
+ direction = 'descending';
2526
+ }
2527
+ const state = { direction, key: column.id };
2528
+ this._updateSortIndicator(node, state);
2529
+ return state;
2530
+ }
2531
+ }
2532
+ return { direction: 'ascending', key: 'name' };
2533
+ }
2534
+ _updateSortIndicator(node, sortState) {
2535
+ const sortableColumns = DirListing.columns.filter(Private.isSortable);
2536
+ for (const column of sortableColumns) {
2537
+ const header = node.querySelector(`.${column.className}`);
2538
+ if (!header) {
2539
+ continue;
2540
+ }
2541
+ const headerIcon = DOMUtils.findElement(header, HEADER_ITEM_ICON_CLASS);
2542
+ if (column.id === sortState.key) {
2543
+ header.classList.add(SELECTED_CLASS);
2544
+ if (sortState.direction === 'descending') {
2545
+ header.classList.add(DESCENDING_CLASS);
2546
+ Private.updateCaret(headerIcon, column.caretSide, 'down');
2409
2547
  }
2410
2548
  else {
2411
2549
  header.classList.remove(DESCENDING_CLASS);
2412
2550
  Private.updateCaret(headerIcon, column.caretSide, 'up');
2413
2551
  }
2414
- header.classList.add(SELECTED_CLASS);
2415
- for (const otherColumn of sortableColumns) {
2416
- if (otherColumn.id === column.id) {
2417
- continue;
2418
- }
2419
- const otherHeader = node.querySelector(`.${otherColumn.className}`);
2420
- if (!otherHeader) {
2421
- // skip if hidden
2422
- continue;
2423
- }
2424
- otherHeader.classList.remove(SELECTED_CLASS);
2425
- otherHeader.classList.remove(DESCENDING_CLASS);
2426
- const otherHeaderIcon = DOMUtils.findElement(otherHeader, HEADER_ITEM_ICON_CLASS);
2427
- Private.updateCaret(otherHeaderIcon, otherColumn.caretSide);
2428
- }
2429
- return state;
2552
+ }
2553
+ else {
2554
+ header.classList.remove(SELECTED_CLASS);
2555
+ header.classList.remove(DESCENDING_CLASS);
2556
+ Private.updateCaret(headerIcon, column.caretSide);
2430
2557
  }
2431
2558
  }
2432
- return state;
2433
2559
  }
2434
2560
  /**
2435
2561
  * Create a new item node for a dir listing.
@@ -2502,25 +2628,47 @@ export class DirListing extends Widget {
2502
2628
  * @param modifiedStyle - The date style for the modified column: narrow, short, or long
2503
2629
  */
2504
2630
  updateItemModified(modified, modifiedDate, modifiedStyle) {
2505
- // Formatting dates is expensive (0.1-0.2ms per call,
2506
- // so over 150 files can easily already choke the renderer),
2507
- // let's do the bare minimum check of comparing if an update
2508
- // is needed using a last update cache:
2509
- const previousUpdate = this._modifiedColumnLastUpdate.get(modified);
2510
- if ((previousUpdate === null || previousUpdate === void 0 ? void 0 : previousUpdate.date) === modifiedDate &&
2511
- (previousUpdate === null || previousUpdate === void 0 ? void 0 : previousUpdate.style) === modifiedStyle) {
2631
+ this._updateItemDate(modified, modifiedDate, modifiedStyle, this._modifiedColumnLastUpdate);
2632
+ }
2633
+ /**
2634
+ * Update an item's created date.
2635
+ *
2636
+ * @param created - Element containing the file's created date.
2637
+ *
2638
+ * @param createdDate - String representation of the created date.
2639
+ *
2640
+ * @param createdStyle - The date style for the created column: narrow, short, or long
2641
+ */
2642
+ updateItemCreated(created, createdDate, createdStyle) {
2643
+ this._updateItemDate(created, createdDate, createdStyle, this._createdColumnLastUpdate);
2644
+ }
2645
+ /**
2646
+ * Shared implementation for updating a date column element.
2647
+ *
2648
+ * Formatting dates is expensive (0.1-0.2ms per call, so over 150 files
2649
+ * can easily already choke the renderer), so we do the bare minimum check
2650
+ * of comparing if an update is needed using a last update cache.
2651
+ *
2652
+ * @param element - The date column element to update.
2653
+ *
2654
+ * @param dateString - String representation of the date.
2655
+ *
2656
+ * @param style - The date style: narrow, short, or long.
2657
+ *
2658
+ * @param cache - The WeakMap cache tracking the last rendered state for
2659
+ * this column.
2660
+ */
2661
+ _updateItemDate(element, dateString, style, cache) {
2662
+ const previousUpdate = cache.get(element);
2663
+ if ((previousUpdate === null || previousUpdate === void 0 ? void 0 : previousUpdate.date) === dateString &&
2664
+ (previousUpdate === null || previousUpdate === void 0 ? void 0 : previousUpdate.style) === style) {
2512
2665
  return;
2513
2666
  }
2514
- const parsedDate = new Date(modifiedDate);
2667
+ const parsedDate = new Date(dateString);
2515
2668
  // Render the date in one of multiple formats, depending on the container's size
2516
- const modText = Time.formatHuman(parsedDate, modifiedStyle);
2517
- const modTitle = Time.format(parsedDate);
2518
- modified.textContent = modText;
2519
- modified.title = modTitle;
2520
- this._modifiedColumnLastUpdate.set(modified, {
2521
- date: modifiedDate,
2522
- style: modifiedStyle
2523
- });
2669
+ element.textContent = Time.formatHuman(parsedDate, style);
2670
+ element.title = Time.format(parsedDate);
2671
+ cache.set(element, { date: dateString, style });
2524
2672
  }
2525
2673
  /**
2526
2674
  * Update an item node to reflect the current state of a model.
@@ -2532,7 +2680,8 @@ export class DirListing extends Widget {
2532
2680
  * @param fileType - The file type of the item, if applicable.
2533
2681
  *
2534
2682
  */
2535
- updateItemNode(node, model, fileType, translator, hiddenColumns, selected, modifiedStyle, columnsSizes) {
2683
+ updateItemNode(node, model, fileType, translator, hiddenColumns, selected, modifiedStyle, columnsSizes, createdStyle) {
2684
+ var _a;
2536
2685
  if (selected) {
2537
2686
  node.classList.add(SELECTED_CLASS);
2538
2687
  }
@@ -2546,8 +2695,10 @@ export class DirListing extends Widget {
2546
2695
  name: model.name,
2547
2696
  selected,
2548
2697
  lastModified: model.last_modified,
2698
+ created: model.created,
2549
2699
  modifiedStyle,
2550
- hiddenColumns,
2700
+ createdStyle,
2701
+ hiddenColumns: Array.from(hiddenColumns !== null && hiddenColumns !== void 0 ? hiddenColumns : []).sort(),
2551
2702
  columnsSizes,
2552
2703
  fileSize: model.size
2553
2704
  });
@@ -2563,6 +2714,7 @@ export class DirListing extends Widget {
2563
2714
  const nameColumn = DOMUtils.findElement(node, ITEM_NAME_COLUMN_CLASS);
2564
2715
  let modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
2565
2716
  let fileSize = DOMUtils.findElement(node, ITEM_FILE_SIZE_CLASS);
2717
+ let created = DOMUtils.findElement(node, ITEM_CREATED_CLASS);
2566
2718
  const showFileCheckboxes = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'));
2567
2719
  if (checkboxWrapper && !showFileCheckboxes) {
2568
2720
  node.removeChild(checkboxWrapper);
@@ -2574,18 +2726,31 @@ export class DirListing extends Widget {
2574
2726
  const showModified = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified'));
2575
2727
  if (modified && !showModified) {
2576
2728
  node.removeChild(modified);
2729
+ modified = undefined;
2577
2730
  }
2578
2731
  else if (showModified && !modified) {
2579
2732
  modified = this.itemFactories.last_modified();
2580
2733
  nameColumn.insertAdjacentElement('afterend', modified);
2581
2734
  }
2735
+ const showCreated = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('date_created'));
2736
+ if (created && !showCreated) {
2737
+ node.removeChild(created);
2738
+ created = undefined;
2739
+ }
2740
+ else if (showCreated && !created) {
2741
+ created = this.itemFactories.date_created();
2742
+ (modified !== null && modified !== void 0 ? modified : nameColumn).insertAdjacentElement('afterend', created);
2743
+ }
2582
2744
  const showFileSize = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size'));
2583
2745
  if (fileSize && !showFileSize) {
2584
2746
  node.removeChild(fileSize);
2585
2747
  }
2586
2748
  else if (showFileSize && !fileSize) {
2587
2749
  fileSize = this.itemFactories.file_size();
2588
- (modified !== null && modified !== void 0 ? modified : nameColumn).insertAdjacentElement('afterend', fileSize);
2750
+ const insertAfter = (_a = created !== null && created !== void 0 ? created : modified) !== null && _a !== void 0 ? _a : nameColumn;
2751
+ if (insertAfter) {
2752
+ insertAfter.insertAdjacentElement('afterend', fileSize);
2753
+ }
2589
2754
  }
2590
2755
  // render the file item's icon
2591
2756
  requestAnimationFrame(() => {
@@ -2655,12 +2820,12 @@ export class DirListing extends Widget {
2655
2820
  checkbox.setAttribute('aria-label', ariaLabel);
2656
2821
  checkbox.checked = selected !== null && selected !== void 0 ? selected : false;
2657
2822
  }
2658
- this.updateItemSize(node, model, modifiedStyle, columnsSizes);
2823
+ this.updateItemSize(node, model, modifiedStyle, columnsSizes, createdStyle);
2659
2824
  }
2660
2825
  /**
2661
2826
  * Update size of item nodes, assuming that model has not changed.
2662
2827
  */
2663
- updateItemSize(node, model, modifiedStyle, columnsSizes) {
2828
+ updateItemSize(node, model, modifiedStyle, columnsSizes, createdStyle) {
2664
2829
  if (columnsSizes) {
2665
2830
  for (const column of DirListing.columns) {
2666
2831
  const element = DOMUtils.findElement(node, column.itemClassName);
@@ -2678,6 +2843,10 @@ export class DirListing extends Widget {
2678
2843
  if (model.last_modified && modified) {
2679
2844
  this.updateItemModified(modified, model.last_modified, modifiedStyle !== null && modifiedStyle !== void 0 ? modifiedStyle : 'short');
2680
2845
  }
2846
+ let created = DOMUtils.findElement(node, ITEM_CREATED_CLASS);
2847
+ if (model.created && created) {
2848
+ this.updateItemCreated(created, model.created, createdStyle !== null && createdStyle !== void 0 ? createdStyle : 'short');
2849
+ }
2681
2850
  }
2682
2851
  /**
2683
2852
  * Get the node containing the file name.
@@ -2805,13 +2974,13 @@ var Private;
2805
2974
  resolve(edit.value);
2806
2975
  };
2807
2976
  edit.onkeydown = (event) => {
2808
- switch (event.keyCode) {
2809
- case 13: // Enter
2977
+ switch (event.key) {
2978
+ case 'Enter':
2810
2979
  event.stopPropagation();
2811
2980
  event.preventDefault();
2812
2981
  edit.blur();
2813
2982
  break;
2814
- case 27: // Escape
2983
+ case 'Escape':
2815
2984
  event.stopPropagation();
2816
2985
  event.preventDefault();
2817
2986
  edit.value = original;
@@ -2906,6 +3075,14 @@ var Private;
2906
3075
  return ((_a = b.size) !== null && _a !== void 0 ? _a : 0) - ((_b = a.size) !== null && _b !== void 0 ? _b : 0);
2907
3076
  }));
2908
3077
  }
3078
+ else if (state.key === 'date_created') {
3079
+ // Sort by date created
3080
+ copy.sort(compare((a, b) => {
3081
+ var _a, _b;
3082
+ return (new Date((_a = a.created) !== null && _a !== void 0 ? _a : '').getTime() -
3083
+ new Date((_b = b.created) !== null && _b !== void 0 ? _b : '').getTime());
3084
+ }));
3085
+ }
2909
3086
  else {
2910
3087
  // Sort by name
2911
3088
  copy.sort(compare((a, b) => {