@jupyterlab/filebrowser 4.6.0-alpha.4 → 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/src/listing.ts CHANGED
@@ -96,6 +96,11 @@ const ITEM_MODIFIED_CLASS = 'jp-DirListing-itemModified';
96
96
  */
97
97
  const ITEM_FILE_SIZE_CLASS = 'jp-DirListing-itemFileSize';
98
98
 
99
+ /**
100
+ * The class name added to the listing item created cell.
101
+ */
102
+ const ITEM_CREATED_CLASS = 'jp-DirListing-itemCreated';
103
+
99
104
  /**
100
105
  * The class name added to the label element that wraps each item's checkbox and
101
106
  * the header's check-all checkbox.
@@ -122,6 +127,11 @@ const MODIFIED_ID_CLASS = 'jp-id-modified';
122
127
  */
123
128
  const FILE_SIZE_ID_CLASS = 'jp-id-filesize';
124
129
 
130
+ /**
131
+ * The class name added to the created column header cell.
132
+ */
133
+ const CREATED_ID_CLASS = 'jp-id-created';
134
+
125
135
  /**
126
136
  * The mime type for a contents drag object.
127
137
  */
@@ -241,10 +251,14 @@ export class DirListing extends Widget {
241
251
 
242
252
  // Get the width of the "modified" column
243
253
  this._updateModifiedSize(this.node);
254
+ // Get the width of the "created" column
255
+ this._updateCreatedSize(this.node);
244
256
 
245
257
  const headerNode = DOMUtils.findElement(this.node, HEADER_CLASS);
246
258
  // hide the file size column by default
247
259
  this._hiddenColumns.add('file_size');
260
+ // hide the date created column by default
261
+ this._hiddenColumns.add('date_created');
248
262
  this._renderer.populateHeaderNode(
249
263
  headerNode,
250
264
  this.translator,
@@ -355,6 +369,7 @@ export class DirListing extends Widget {
355
369
  );
356
370
  this._sortState = state;
357
371
  this.update();
372
+ this._saveState();
358
373
  }
359
374
 
360
375
  /**
@@ -575,19 +590,62 @@ export class DirListing extends Widget {
575
590
  | Record<DirListing.IColumn['id'], number | null>
576
591
  | undefined;
577
592
 
578
- if (!sizes) {
579
- return;
593
+ const sortState = (columns as ReadonlyJSONObject)[
594
+ 'sortState'
595
+ ] as unknown as DirListing.ISortState | undefined;
596
+
597
+ // Set sort state before updating column sizes so that any
598
+ // state save triggered by _updateColumnSizes includes the
599
+ // correct sort state rather than the default.
600
+ if (sortState) {
601
+ this._sortState = sortState;
580
602
  }
581
- for (const [key, size] of Object.entries(sizes)) {
582
- this._columnSizes[key as DirListing.IColumn['id']] = size;
603
+
604
+ if (sizes) {
605
+ for (const [key, size] of Object.entries(sizes)) {
606
+ this._columnSizes[key as DirListing.IColumn['id']] = size;
607
+ }
608
+ this._updateColumnSizes();
609
+ }
610
+
611
+ // Sort items and rebuild the header to reflect the restored
612
+ // sort state without calling sort() to avoid a redundant save.
613
+ if (sortState) {
614
+ this._sortedItems = Private.sort(
615
+ this.model.items(),
616
+ sortState,
617
+ this._sortNotebooksFirst,
618
+ this._sortFileNamesNaturally,
619
+ this.translator
620
+ );
621
+ this.headerNode.innerHTML = '';
622
+ this._renderer.populateHeaderNode(
623
+ this.headerNode,
624
+ this.translator,
625
+ this._hiddenColumns,
626
+ this._columnSizes,
627
+ sortState
628
+ );
629
+ this.update();
583
630
  }
584
- this._updateColumnSizes();
585
631
  } catch (error) {
586
632
  await state.remove(key);
587
633
  }
588
634
  }
589
635
  private _stateColumnsKey: string;
590
636
 
637
+ /**
638
+ * Save the current column sizes and sort state to the state database.
639
+ */
640
+ private _saveState(): void {
641
+ if (this._state && this._stateColumnsKey) {
642
+ void this._state.save(this._stateColumnsKey, {
643
+ sizes: this._columnSizes,
644
+ sortState: { ...this._sortState }
645
+ });
646
+ }
647
+ }
648
+
591
649
  /**
592
650
  * Shut down kernels on the applicable currently selected items.
593
651
  *
@@ -739,6 +797,39 @@ export class DirListing extends Widget {
739
797
  }
740
798
  }
741
799
 
800
+ /**
801
+ * Move focus to selected item, or to the first item if no item is selected.
802
+ */
803
+ private _focusContent(): void {
804
+ const items = this._sortedItems;
805
+ if (items.length === 0) {
806
+ this._focusItem(0);
807
+ return;
808
+ }
809
+
810
+ let index =
811
+ this._focusPath !== ''
812
+ ? ArrayExt.findFirstIndex(
813
+ items,
814
+ value => value.path === this._focusPath
815
+ )
816
+ : -1;
817
+
818
+ // Fallbacks if path is missing/not found.
819
+ if (index === -1) {
820
+ index = Math.min(Math.max(this._focusIndex, 0), items.length - 1);
821
+ }
822
+
823
+ this._selectItem(index, false, true);
824
+ }
825
+
826
+ /**
827
+ * Handle `'activate-request'` messages.
828
+ */
829
+ protected onActivateRequest(msg: Message): void {
830
+ this._focusContent();
831
+ }
832
+
742
833
  /**
743
834
  * Select an item by name.
744
835
  *
@@ -944,21 +1035,42 @@ export class DirListing extends Widget {
944
1035
  }
945
1036
 
946
1037
  /**
947
- * Update the modified column's size
1038
+ * Update the 'modified' column's size
948
1039
  */
949
1040
  private _updateModifiedSize(node: HTMLElement) {
950
- // Look for the modified column's header
951
- const modified = DOMUtils.findElement(node, MODIFIED_ID_CLASS);
952
- this._modifiedWidth =
953
- this._columnSizes['last_modified'] ??
954
- modified?.getBoundingClientRect().width ??
1041
+ this._modifiedStyle = this._computeDateColumnStyle(
1042
+ node,
1043
+ MODIFIED_ID_CLASS,
1044
+ 'last_modified'
1045
+ );
1046
+ }
1047
+
1048
+ /**
1049
+ * Update the 'date created' column's size
1050
+ */
1051
+ private _updateCreatedSize(node: HTMLElement) {
1052
+ this._createdStyle = this._computeDateColumnStyle(
1053
+ node,
1054
+ CREATED_ID_CLASS,
1055
+ 'date_created'
1056
+ );
1057
+ }
1058
+
1059
+ /**
1060
+ * Compute the Intl.RelativeTimeFormat style for a date column based on
1061
+ * its pixel width: < 100px → 'narrow', 100–120px → 'short', > 120px → 'long'.
1062
+ */
1063
+ private _computeDateColumnStyle(
1064
+ node: HTMLElement,
1065
+ columnClassName: string,
1066
+ columnName: 'last_modified' | 'date_created'
1067
+ ): Time.HumanStyle {
1068
+ const columnElement = DOMUtils.findElement(node, columnClassName);
1069
+ const width =
1070
+ this._columnSizes[columnName] ??
1071
+ columnElement?.getBoundingClientRect().width ??
955
1072
  83;
956
- this._modifiedStyle =
957
- this._modifiedWidth < 100
958
- ? 'narrow'
959
- : this._modifiedWidth > 120
960
- ? 'long'
961
- : 'short';
1073
+ return width < 100 ? 'narrow' : width > 120 ? 'long' : 'short';
962
1074
  }
963
1075
 
964
1076
  /**
@@ -966,13 +1078,23 @@ export class DirListing extends Widget {
966
1078
  */
967
1079
  private _updateModifiedStyleAndSize() {
968
1080
  const oldModifiedStyle = this._modifiedStyle;
969
- // Update both size and style
970
1081
  this._updateModifiedSize(this.node);
971
1082
  if (oldModifiedStyle !== this._modifiedStyle) {
972
1083
  this.updateModified(this._sortedItems, this._items);
973
1084
  }
974
1085
  }
975
1086
 
1087
+ /**
1088
+ * Rerender item nodes' created dates, if the created style has changed.
1089
+ */
1090
+ private _updateCreatedStyleAndSize() {
1091
+ const oldCreatedStyle = this._createdStyle;
1092
+ this._updateCreatedSize(this.node);
1093
+ if (oldCreatedStyle !== this._createdStyle) {
1094
+ this.updateCreated(this._sortedItems, this._items);
1095
+ }
1096
+ }
1097
+
976
1098
  /**
977
1099
  * Update only the modified dates.
978
1100
  */
@@ -981,6 +1103,9 @@ export class DirListing extends Widget {
981
1103
  const node = nodes[i];
982
1104
  if (node && item.last_modified) {
983
1105
  const modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
1106
+ if (!modified) {
1107
+ return;
1108
+ }
984
1109
  if (this.renderer.updateItemModified !== undefined) {
985
1110
  this.renderer.updateItemModified(
986
1111
  modified,
@@ -998,6 +1123,34 @@ export class DirListing extends Widget {
998
1123
  });
999
1124
  }
1000
1125
 
1126
+ /**
1127
+ * Update only the created dates.
1128
+ */
1129
+ protected updateCreated(items: Contents.IModel[], nodes: HTMLElement[]) {
1130
+ items.forEach((item, i) => {
1131
+ const node = nodes[i];
1132
+ if (node && item.created) {
1133
+ const created = DOMUtils.findElement(node, ITEM_CREATED_CLASS);
1134
+ if (!created) {
1135
+ return;
1136
+ }
1137
+ if (this.renderer.updateItemCreated !== undefined) {
1138
+ this.renderer.updateItemCreated(
1139
+ created,
1140
+ item.created,
1141
+ this._createdStyle
1142
+ );
1143
+ } else {
1144
+ DirListing.defaultRenderer.updateItemCreated(
1145
+ created,
1146
+ item.created,
1147
+ this._createdStyle
1148
+ );
1149
+ }
1150
+ }
1151
+ });
1152
+ }
1153
+
1001
1154
  // Update item nodes based on widget state.
1002
1155
  protected updateNodes(
1003
1156
  items: Contents.IModel[],
@@ -1015,7 +1168,8 @@ export class DirListing extends Widget {
1015
1168
  node,
1016
1169
  item,
1017
1170
  this._modifiedStyle,
1018
- this._columnSizes
1171
+ this._columnSizes,
1172
+ this._createdStyle
1019
1173
  );
1020
1174
  }
1021
1175
  const ft = this._manager.registry.getFileTypeForModel(item);
@@ -1027,7 +1181,8 @@ export class DirListing extends Widget {
1027
1181
  this._hiddenColumns,
1028
1182
  this.selection[item.path],
1029
1183
  this._modifiedStyle,
1030
- this._columnSizes
1184
+ this._columnSizes,
1185
+ this._createdStyle
1031
1186
  );
1032
1187
  if (
1033
1188
  this.selection[item.path] &&
@@ -1195,7 +1350,8 @@ export class DirListing extends Widget {
1195
1350
  this.headerNode,
1196
1351
  this.translator,
1197
1352
  this._hiddenColumns,
1198
- this._columnSizes
1353
+ this._columnSizes,
1354
+ this._sortState
1199
1355
  );
1200
1356
 
1201
1357
  this._updateColumnSizes();
@@ -1286,6 +1442,7 @@ export class DirListing extends Widget {
1286
1442
  column.element.style.width = size === null ? '' : size + 'px';
1287
1443
  }
1288
1444
  this._updateModifiedStyleAndSize();
1445
+ this._updateCreatedStyleAndSize();
1289
1446
 
1290
1447
  // Refresh sizes on the per item widths
1291
1448
  if (this.isVisible) {
@@ -1295,11 +1452,7 @@ export class DirListing extends Widget {
1295
1452
  }
1296
1453
  }
1297
1454
 
1298
- if (this._state && this._stateColumnsKey) {
1299
- void this._state.save(this._stateColumnsKey, {
1300
- sizes: this._columnSizes
1301
- });
1302
- }
1455
+ this._saveState();
1303
1456
  }
1304
1457
 
1305
1458
  private get _visibleColumns() {
@@ -1729,6 +1882,10 @@ export class DirListing extends Widget {
1729
1882
  if (model.path === model.rootPath) {
1730
1883
  return;
1731
1884
  }
1885
+ // Check if we're at the navigation root boundary
1886
+ if (model.root && model.path === model.root) {
1887
+ return;
1888
+ }
1732
1889
  try {
1733
1890
  await model.cd('..');
1734
1891
  } catch (reason) {
@@ -1745,9 +1902,8 @@ export class DirListing extends Widget {
1745
1902
  return;
1746
1903
  }
1747
1904
 
1748
- switch (event.keyCode) {
1749
- case 13: {
1750
- // Enter
1905
+ switch (event.key) {
1906
+ case 'Enter': {
1751
1907
  // Do nothing if any modifier keys are pressed.
1752
1908
  if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
1753
1909
  return;
@@ -1759,16 +1915,14 @@ export class DirListing extends Widget {
1759
1915
  }
1760
1916
  return;
1761
1917
  }
1762
- case 38:
1763
- // Up arrow
1918
+ case 'ArrowUp':
1764
1919
  this._handleArrowY(event, -1);
1765
1920
  return;
1766
- case 40:
1767
- // Down arrow
1921
+ case 'ArrowDown':
1768
1922
  this._handleArrowY(event, 1);
1769
1923
  return;
1770
- case 32: {
1771
- // Space
1924
+ case ' ':
1925
+ case 'Spacebar': {
1772
1926
  if (event.ctrlKey) {
1773
1927
  // Follow the Windows and Ubuntu convention: you must press `ctrl` +
1774
1928
  // `space` in order to toggle whether an item is selected.
@@ -1823,7 +1977,7 @@ export class DirListing extends Widget {
1823
1977
  // Don't gobble up the space key on the check-all checkbox (which the
1824
1978
  // browser treats as a click event).
1825
1979
  !(
1826
- (event.key === ' ' || event.keyCode === 32) &&
1980
+ (event.key === ' ' || event.key === 'Spacebar') &&
1827
1981
  (event.target as HTMLInputElement).type === 'checkbox'
1828
1982
  )
1829
1983
  ) {
@@ -2240,11 +2394,14 @@ export class DirListing extends Widget {
2240
2394
  // Focus the top node if the folder is empty and therefore there are no
2241
2395
  // items inside the folder to focus.
2242
2396
  this._focusIndex = 0;
2397
+ this._focusPath = '';
2243
2398
  this.node.focus();
2244
2399
  return;
2245
2400
  }
2246
- this._focusIndex = index;
2247
- const node = items[index];
2401
+ const clampedIndex = Math.min(Math.max(index, 0), items.length - 1);
2402
+ this._focusIndex = clampedIndex;
2403
+ this._focusPath = this._sortedItems[clampedIndex]?.path ?? '';
2404
+ const node = items[clampedIndex];
2248
2405
  const nameNode = this.renderer.getNameNode(node);
2249
2406
  if (nameNode) {
2250
2407
  // Make the filename text node focusable so that it receives keyboard
@@ -2662,7 +2819,8 @@ export class DirListing extends Widget {
2662
2819
  name: null,
2663
2820
  file_size: null,
2664
2821
  is_selected: null,
2665
- last_modified: null
2822
+ last_modified: null,
2823
+ date_created: null
2666
2824
  };
2667
2825
  private _sortNotebooksFirst = false;
2668
2826
  private _sortFileNamesNaturally = true;
@@ -2670,9 +2828,9 @@ export class DirListing extends Widget {
2670
2828
  private _allowDragDropUpload = true;
2671
2829
  // _focusIndex should never be set outside the range [0, this._items.length - 1]
2672
2830
  private _focusIndex = 0;
2673
- // Width of the "last modified" column for an individual file
2674
- private _modifiedWidth: number;
2675
- private _modifiedStyle: Time.HumanStyle;
2831
+ private _focusPath = '';
2832
+ private _modifiedStyle: Time.HumanStyle = 'short';
2833
+ private _createdStyle: Time.HumanStyle = 'short';
2676
2834
  private _allUploaded = new Signal<DirListing, void>(this);
2677
2835
  private _width: number | null = null;
2678
2836
  private _state: IStateDB | null = null;
@@ -2747,17 +2905,29 @@ export namespace DirListing {
2747
2905
  /**
2748
2906
  * Toggleable columns.
2749
2907
  */
2750
- export type ToggleableColumn = 'last_modified' | 'is_selected' | 'file_size';
2908
+ export type ToggleableColumn =
2909
+ | 'last_modified'
2910
+ | 'is_selected'
2911
+ | 'file_size'
2912
+ | 'date_created';
2751
2913
 
2752
2914
  /**
2753
2915
  * Resizable columns.
2754
2916
  */
2755
- export type ResizableColumn = 'name' | 'last_modified' | 'file_size';
2917
+ export type ResizableColumn =
2918
+ | 'name'
2919
+ | 'last_modified'
2920
+ | 'file_size'
2921
+ | 'date_created';
2756
2922
 
2757
2923
  /**
2758
2924
  * Sortable columns.
2759
2925
  */
2760
- export type SortableColumn = 'name' | 'last_modified' | 'file_size';
2926
+ export type SortableColumn =
2927
+ | 'name'
2928
+ | 'last_modified'
2929
+ | 'file_size'
2930
+ | 'date_created';
2761
2931
 
2762
2932
  /**
2763
2933
  * A file contents model thunk.
@@ -2796,7 +2966,8 @@ export namespace DirListing {
2796
2966
  node: HTMLElement,
2797
2967
  translator?: ITranslator,
2798
2968
  hiddenColumns?: Set<DirListing.ToggleableColumn>,
2799
- columnsSizes?: Record<IColumn['id'], number | null>
2969
+ columnsSizes?: Record<IColumn['id'], number | null>,
2970
+ sortState?: ISortState
2800
2971
  ): void;
2801
2972
 
2802
2973
  /**
@@ -2835,6 +3006,21 @@ export namespace DirListing {
2835
3006
  modifiedStyle: Time.HumanStyle
2836
3007
  ): void;
2837
3008
 
3009
+ /**
3010
+ * Update an item's created date.
3011
+ *
3012
+ * @param created - Element containing the file's created date.
3013
+ *
3014
+ * @param createdDate - String representation of the created date.
3015
+ *
3016
+ * @param createdStyle - The date style for the created column: narrow, short, or long
3017
+ */
3018
+ updateItemCreated?(
3019
+ created: HTMLElement,
3020
+ createdDate: string,
3021
+ createdStyle: Time.HumanStyle
3022
+ ): void;
3023
+
2838
3024
  /**
2839
3025
  * Update an item node to reflect the current state of a model.
2840
3026
  *
@@ -2854,7 +3040,8 @@ export namespace DirListing {
2854
3040
  hiddenColumns?: Set<DirListing.ToggleableColumn>,
2855
3041
  selected?: boolean,
2856
3042
  modifiedStyle?: Time.HumanStyle,
2857
- columnsSizes?: Record<IColumn['id'], number | null>
3043
+ columnsSizes?: Record<IColumn['id'], number | null>,
3044
+ createdStyle?: Time.HumanStyle
2858
3045
  ): void;
2859
3046
 
2860
3047
  /**
@@ -2864,7 +3051,8 @@ export namespace DirListing {
2864
3051
  node: HTMLElement,
2865
3052
  model: Contents.IModel,
2866
3053
  modifiedStyle?: Time.HumanStyle,
2867
- columnsSizes?: Record<IColumn['id'], number | null>
3054
+ columnsSizes?: Record<IColumn['id'], number | null>,
3055
+ createdStyle?: Time.HumanStyle
2868
3056
  ): void;
2869
3057
 
2870
3058
  /**
@@ -2992,6 +3180,16 @@ export namespace DirListing {
2992
3180
  caretSide: 'left',
2993
3181
  grow: 1
2994
3182
  },
3183
+ {
3184
+ id: 'date_created' as const,
3185
+ className: CREATED_ID_CLASS,
3186
+ itemClassName: ITEM_CREATED_CLASS,
3187
+ minWidth: 60,
3188
+ resizable: true,
3189
+ sortable: true,
3190
+ caretSide: 'left',
3191
+ grow: 1
3192
+ },
2995
3193
  {
2996
3194
  id: 'file_size' as const,
2997
3195
  className: FILE_SIZE_ID_CLASS,
@@ -3035,7 +3233,8 @@ export namespace DirListing {
3035
3233
  node: HTMLElement,
3036
3234
  translator?: ITranslator,
3037
3235
  hiddenColumns?: Set<DirListing.ToggleableColumn>,
3038
- columnsSizes?: Record<DirListing.IColumn['id'], number | null>
3236
+ columnsSizes?: Record<DirListing.IColumn['id'], number | null>,
3237
+ sortState?: DirListing.ISortState
3039
3238
  ): void {
3040
3239
  translator = translator || nullTranslator;
3041
3240
  const trans = translator.load('jupyterlab');
@@ -3052,6 +3251,11 @@ export namespace DirListing {
3052
3251
  small: trans.__('Size'),
3053
3252
  large: trans.__('File Size')
3054
3253
  }),
3254
+ date_created: () =>
3255
+ this._createHeaderItemNodeWithSizes({
3256
+ small: trans.__('Created'),
3257
+ large: trans.__('Date Created')
3258
+ }),
3055
3259
  is_selected: () =>
3056
3260
  this.createCheckboxWrapperNode({
3057
3261
  alwaysVisible: true,
@@ -3086,14 +3290,9 @@ export namespace DirListing {
3086
3290
  }
3087
3291
  }
3088
3292
 
3089
- const name = DOMUtils.findElement(node, NAME_ID_CLASS);
3090
- name.classList.add(SELECTED_CLASS);
3091
-
3092
- // set the initial caret icon
3093
- Private.updateCaret(
3094
- DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS),
3095
- 'right',
3096
- 'up'
3293
+ this._updateSortIndicator(
3294
+ node,
3295
+ sortState ?? { direction: 'ascending', key: 'name' }
3097
3296
  );
3098
3297
  }
3099
3298
 
@@ -3107,58 +3306,59 @@ export namespace DirListing {
3107
3306
  * @returns The sort state of the header after the click event.
3108
3307
  */
3109
3308
  handleHeaderClick(node: HTMLElement, event: MouseEvent): ISortState | null {
3110
- const state: ISortState = { direction: 'ascending', key: 'name' };
3111
3309
  const target = event.target as HTMLElement;
3112
-
3113
3310
  const sortableColumns = DirListing.columns.filter(Private.isSortable);
3114
3311
 
3115
3312
  for (const column of sortableColumns) {
3116
3313
  const header = node.querySelector(`.${column.className}`);
3117
3314
  if (!header) {
3118
- // skip if the column is hidden
3119
3315
  continue;
3120
3316
  }
3121
3317
  if (header.contains(target)) {
3122
- state.key = column.id;
3123
- const headerIcon = DOMUtils.findElement(
3124
- header as HTMLElement,
3125
- HEADER_ITEM_ICON_CLASS
3126
- );
3127
- if (header.classList.contains(SELECTED_CLASS)) {
3128
- if (!header.classList.contains(DESCENDING_CLASS)) {
3129
- state.direction = 'descending';
3130
- header.classList.add(DESCENDING_CLASS);
3131
- Private.updateCaret(headerIcon, column.caretSide, 'down');
3132
- } else {
3133
- header.classList.remove(DESCENDING_CLASS);
3134
- Private.updateCaret(headerIcon, column.caretSide, 'up');
3135
- }
3318
+ const isSelected = header.classList.contains(SELECTED_CLASS);
3319
+ const isDescending = header.classList.contains(DESCENDING_CLASS);
3320
+ let direction: 'ascending' | 'descending' = 'ascending';
3321
+ if (isSelected && !isDescending) {
3322
+ direction = 'descending';
3323
+ }
3324
+ const state: ISortState = { direction, key: column.id };
3325
+ this._updateSortIndicator(node, state);
3326
+ return state;
3327
+ }
3328
+ }
3329
+ return { direction: 'ascending', key: 'name' };
3330
+ }
3331
+
3332
+ private _updateSortIndicator(
3333
+ node: HTMLElement,
3334
+ sortState: DirListing.ISortState
3335
+ ): void {
3336
+ const sortableColumns = DirListing.columns.filter(Private.isSortable);
3337
+
3338
+ for (const column of sortableColumns) {
3339
+ const header = node.querySelector(`.${column.className}`);
3340
+ if (!header) {
3341
+ continue;
3342
+ }
3343
+ const headerIcon = DOMUtils.findElement(
3344
+ header as HTMLElement,
3345
+ HEADER_ITEM_ICON_CLASS
3346
+ );
3347
+ if (column.id === sortState.key) {
3348
+ header.classList.add(SELECTED_CLASS);
3349
+ if (sortState.direction === 'descending') {
3350
+ header.classList.add(DESCENDING_CLASS);
3351
+ Private.updateCaret(headerIcon, column.caretSide, 'down');
3136
3352
  } else {
3137
3353
  header.classList.remove(DESCENDING_CLASS);
3138
3354
  Private.updateCaret(headerIcon, column.caretSide, 'up');
3139
3355
  }
3140
- header.classList.add(SELECTED_CLASS);
3141
- for (const otherColumn of sortableColumns) {
3142
- if (otherColumn.id === column.id) {
3143
- continue;
3144
- }
3145
- const otherHeader = node.querySelector(`.${otherColumn.className}`);
3146
- if (!otherHeader) {
3147
- // skip if hidden
3148
- continue;
3149
- }
3150
- otherHeader.classList.remove(SELECTED_CLASS);
3151
- otherHeader.classList.remove(DESCENDING_CLASS);
3152
- const otherHeaderIcon = DOMUtils.findElement(
3153
- otherHeader as HTMLElement,
3154
- HEADER_ITEM_ICON_CLASS
3155
- );
3156
- Private.updateCaret(otherHeaderIcon, otherColumn.caretSide);
3157
- }
3158
- return state;
3356
+ } else {
3357
+ header.classList.remove(SELECTED_CLASS);
3358
+ header.classList.remove(DESCENDING_CLASS);
3359
+ Private.updateCaret(headerIcon, column.caretSide);
3159
3360
  }
3160
3361
  }
3161
- return state;
3162
3362
  }
3163
3363
 
3164
3364
  /**
@@ -3249,29 +3449,71 @@ export namespace DirListing {
3249
3449
  modifiedDate: string,
3250
3450
  modifiedStyle: Time.HumanStyle
3251
3451
  ): void {
3252
- // Formatting dates is expensive (0.1-0.2ms per call,
3253
- // so over 150 files can easily already choke the renderer),
3254
- // let's do the bare minimum check of comparing if an update
3255
- // is needed using a last update cache:
3256
- const previousUpdate = this._modifiedColumnLastUpdate.get(modified);
3452
+ this._updateItemDate(
3453
+ modified,
3454
+ modifiedDate,
3455
+ modifiedStyle,
3456
+ this._modifiedColumnLastUpdate
3457
+ );
3458
+ }
3459
+
3460
+ /**
3461
+ * Update an item's created date.
3462
+ *
3463
+ * @param created - Element containing the file's created date.
3464
+ *
3465
+ * @param createdDate - String representation of the created date.
3466
+ *
3467
+ * @param createdStyle - The date style for the created column: narrow, short, or long
3468
+ */
3469
+ updateItemCreated(
3470
+ created: HTMLElement,
3471
+ createdDate: string,
3472
+ createdStyle: Time.HumanStyle
3473
+ ): void {
3474
+ this._updateItemDate(
3475
+ created,
3476
+ createdDate,
3477
+ createdStyle,
3478
+ this._createdColumnLastUpdate
3479
+ );
3480
+ }
3481
+
3482
+ /**
3483
+ * Shared implementation for updating a date column element.
3484
+ *
3485
+ * Formatting dates is expensive (0.1-0.2ms per call, so over 150 files
3486
+ * can easily already choke the renderer), so we do the bare minimum check
3487
+ * of comparing if an update is needed using a last update cache.
3488
+ *
3489
+ * @param element - The date column element to update.
3490
+ *
3491
+ * @param dateString - String representation of the date.
3492
+ *
3493
+ * @param style - The date style: narrow, short, or long.
3494
+ *
3495
+ * @param cache - The WeakMap cache tracking the last rendered state for
3496
+ * this column.
3497
+ */
3498
+ private _updateItemDate(
3499
+ element: HTMLElement,
3500
+ dateString: string,
3501
+ style: Time.HumanStyle,
3502
+ cache: WeakMap<HTMLElement, { date: string; style: Time.HumanStyle }>
3503
+ ): void {
3504
+ const previousUpdate = cache.get(element);
3257
3505
  if (
3258
- previousUpdate?.date === modifiedDate &&
3259
- previousUpdate?.style === modifiedStyle
3506
+ previousUpdate?.date === dateString &&
3507
+ previousUpdate?.style === style
3260
3508
  ) {
3261
3509
  return;
3262
3510
  }
3263
3511
 
3264
- const parsedDate = new Date(modifiedDate);
3512
+ const parsedDate = new Date(dateString);
3265
3513
  // Render the date in one of multiple formats, depending on the container's size
3266
- const modText = Time.formatHuman(parsedDate, modifiedStyle);
3267
- const modTitle = Time.format(parsedDate);
3268
-
3269
- modified.textContent = modText;
3270
- modified.title = modTitle;
3271
- this._modifiedColumnLastUpdate.set(modified, {
3272
- date: modifiedDate,
3273
- style: modifiedStyle
3274
- });
3514
+ element.textContent = Time.formatHuman(parsedDate, style);
3515
+ element.title = Time.format(parsedDate);
3516
+ cache.set(element, { date: dateString, style });
3275
3517
  }
3276
3518
 
3277
3519
  /**
@@ -3292,7 +3534,8 @@ export namespace DirListing {
3292
3534
  hiddenColumns?: Set<DirListing.ToggleableColumn>,
3293
3535
  selected?: boolean,
3294
3536
  modifiedStyle?: Time.HumanStyle,
3295
- columnsSizes?: Record<DirListing.IColumn['id'], number | null>
3537
+ columnsSizes?: Record<DirListing.IColumn['id'], number | null>,
3538
+ createdStyle?: Time.HumanStyle
3296
3539
  ): void {
3297
3540
  if (selected) {
3298
3541
  node.classList.add(SELECTED_CLASS);
@@ -3308,8 +3551,10 @@ export namespace DirListing {
3308
3551
  name: model.name,
3309
3552
  selected,
3310
3553
  lastModified: model.last_modified,
3554
+ created: model.created,
3311
3555
  modifiedStyle,
3312
- hiddenColumns,
3556
+ createdStyle,
3557
+ hiddenColumns: Array.from(hiddenColumns ?? []).sort(),
3313
3558
  columnsSizes,
3314
3559
  fileSize: model.size
3315
3560
  });
@@ -3336,6 +3581,9 @@ export namespace DirListing {
3336
3581
  let fileSize = DOMUtils.findElement(node, ITEM_FILE_SIZE_CLASS) as
3337
3582
  | HTMLElement
3338
3583
  | undefined;
3584
+ let created = DOMUtils.findElement(node, ITEM_CREATED_CLASS) as
3585
+ | HTMLElement
3586
+ | undefined;
3339
3587
 
3340
3588
  const showFileCheckboxes = !hiddenColumns?.has('is_selected');
3341
3589
  if (checkboxWrapper && !showFileCheckboxes) {
@@ -3348,17 +3596,30 @@ export namespace DirListing {
3348
3596
  const showModified = !hiddenColumns?.has('last_modified');
3349
3597
  if (modified && !showModified) {
3350
3598
  node.removeChild(modified);
3599
+ modified = undefined;
3351
3600
  } else if (showModified && !modified) {
3352
3601
  modified = this.itemFactories.last_modified();
3353
3602
  nameColumn.insertAdjacentElement('afterend', modified);
3354
3603
  }
3355
3604
 
3605
+ const showCreated = !hiddenColumns?.has('date_created');
3606
+ if (created && !showCreated) {
3607
+ node.removeChild(created);
3608
+ created = undefined;
3609
+ } else if (showCreated && !created) {
3610
+ created = this.itemFactories.date_created();
3611
+ (modified ?? nameColumn).insertAdjacentElement('afterend', created);
3612
+ }
3613
+
3356
3614
  const showFileSize = !hiddenColumns?.has('file_size');
3357
3615
  if (fileSize && !showFileSize) {
3358
3616
  node.removeChild(fileSize);
3359
3617
  } else if (showFileSize && !fileSize) {
3360
3618
  fileSize = this.itemFactories.file_size();
3361
- (modified ?? nameColumn).insertAdjacentElement('afterend', fileSize);
3619
+ const insertAfter = created ?? modified ?? nameColumn;
3620
+ if (insertAfter) {
3621
+ insertAfter.insertAdjacentElement('afterend', fileSize);
3622
+ }
3362
3623
  }
3363
3624
 
3364
3625
  // render the file item's icon
@@ -3441,7 +3702,13 @@ export namespace DirListing {
3441
3702
  checkbox.checked = selected ?? false;
3442
3703
  }
3443
3704
 
3444
- this.updateItemSize(node, model, modifiedStyle, columnsSizes);
3705
+ this.updateItemSize(
3706
+ node,
3707
+ model,
3708
+ modifiedStyle,
3709
+ columnsSizes,
3710
+ createdStyle
3711
+ );
3445
3712
  }
3446
3713
 
3447
3714
  /**
@@ -3451,7 +3718,8 @@ export namespace DirListing {
3451
3718
  node: HTMLElement,
3452
3719
  model: Contents.IModel,
3453
3720
  modifiedStyle?: Time.HumanStyle,
3454
- columnsSizes?: Record<DirListing.IColumn['id'], number | null>
3721
+ columnsSizes?: Record<DirListing.IColumn['id'], number | null>,
3722
+ createdStyle?: Time.HumanStyle
3455
3723
  ): void {
3456
3724
  if (columnsSizes) {
3457
3725
  for (const column of columns) {
@@ -3477,6 +3745,14 @@ export namespace DirListing {
3477
3745
  modifiedStyle ?? 'short'
3478
3746
  );
3479
3747
  }
3748
+
3749
+ let created = DOMUtils.findElement(node, ITEM_CREATED_CLASS) as
3750
+ | HTMLElement
3751
+ | undefined;
3752
+
3753
+ if (model.created && created) {
3754
+ this.updateItemCreated(created, model.created, createdStyle ?? 'short');
3755
+ }
3480
3756
  }
3481
3757
 
3482
3758
  /**
@@ -3581,6 +3857,11 @@ export namespace DirListing {
3581
3857
  fileSize.className = ITEM_FILE_SIZE_CLASS;
3582
3858
  return fileSize;
3583
3859
  },
3860
+ date_created: () => {
3861
+ const created = document.createElement('span');
3862
+ created.className = ITEM_CREATED_CLASS;
3863
+ return created;
3864
+ },
3584
3865
  is_selected: () => this.createCheckboxWrapperNode()
3585
3866
  };
3586
3867
 
@@ -3631,6 +3912,14 @@ export namespace DirListing {
3631
3912
  { date: string; style: Time.HumanStyle }
3632
3913
  >();
3633
3914
 
3915
+ /**
3916
+ * Register of most recent arguments for date created column update.
3917
+ */
3918
+ private _createdColumnLastUpdate = new WeakMap<
3919
+ HTMLElement,
3920
+ { date: string; style: Time.HumanStyle }
3921
+ >();
3922
+
3634
3923
  private _lastRenderedState = new WeakMap<HTMLElement, string>();
3635
3924
  }
3636
3925
 
@@ -3670,13 +3959,13 @@ namespace Private {
3670
3959
  resolve(edit.value);
3671
3960
  };
3672
3961
  edit.onkeydown = (event: KeyboardEvent) => {
3673
- switch (event.keyCode) {
3674
- case 13: // Enter
3962
+ switch (event.key) {
3963
+ case 'Enter':
3675
3964
  event.stopPropagation();
3676
3965
  event.preventDefault();
3677
3966
  edit.blur();
3678
3967
  break;
3679
- case 27: // Escape
3968
+ case 'Escape':
3680
3969
  event.stopPropagation();
3681
3970
  event.preventDefault();
3682
3971
  edit.value = original;
@@ -3792,6 +4081,16 @@ namespace Private {
3792
4081
  return (b.size ?? 0) - (a.size ?? 0);
3793
4082
  })
3794
4083
  );
4084
+ } else if (state.key === 'date_created') {
4085
+ // Sort by date created
4086
+ copy.sort(
4087
+ compare((a: Contents.IModel, b: Contents.IModel) => {
4088
+ return (
4089
+ new Date(a.created ?? '').getTime() -
4090
+ new Date(b.created ?? '').getTime()
4091
+ );
4092
+ })
4093
+ );
3795
4094
  } else {
3796
4095
  // Sort by name
3797
4096
  copy.sort(