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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/listing.js CHANGED
@@ -6,7 +6,7 @@ import { isValidFileName, renameFile } from '@jupyterlab/docmanager';
6
6
  import { DocumentRegistry } from '@jupyterlab/docregistry';
7
7
  import { nullTranslator } from '@jupyterlab/translation';
8
8
  import { caretDownIcon, caretUpIcon, classes, LabIcon } from '@jupyterlab/ui-components';
9
- import { ArrayExt, ArrayIterator, each, filter, find, StringExt, toArray } from '@lumino/algorithm';
9
+ import { ArrayExt, filter, StringExt } from '@lumino/algorithm';
10
10
  import { MimeData, PromiseDelegate } from '@lumino/coreutils';
11
11
  import { ElementExt } from '@lumino/domutils';
12
12
  import { Drag } from '@lumino/dragdrop';
@@ -54,6 +54,15 @@ const ITEM_ICON_CLASS = 'jp-DirListing-itemIcon';
54
54
  * The class name added to the listing item modified cell.
55
55
  */
56
56
  const ITEM_MODIFIED_CLASS = 'jp-DirListing-itemModified';
57
+ /**
58
+ * The class name added to the listing item file size cell.
59
+ */
60
+ const ITEM_FILE_SIZE_CLASS = 'jp-DirListing-itemFileSize';
61
+ /**
62
+ * The class name added to the label element that wraps each item's checkbox and
63
+ * the header's check-all checkbox.
64
+ */
65
+ const CHECKBOX_WRAPPER_CLASS = 'jp-DirListing-checkboxWrapper';
57
66
  /**
58
67
  * The class name added to the dir listing editor node.
59
68
  */
@@ -66,6 +75,10 @@ const NAME_ID_CLASS = 'jp-id-name';
66
75
  * The class name added to the modified column header cell.
67
76
  */
68
77
  const MODIFIED_ID_CLASS = 'jp-id-modified';
78
+ /**
79
+ * The class name added to the file size column header cell.
80
+ */
81
+ const FILE_SIZE_ID_CLASS = 'jp-id-filesize';
69
82
  /**
70
83
  * The class name added to the narrow column header cell.
71
84
  */
@@ -74,6 +87,10 @@ const NARROW_ID_CLASS = 'jp-id-narrow';
74
87
  * The class name added to the modified column header cell and modified item cell when hidden.
75
88
  */
76
89
  const MODIFIED_COLUMN_HIDDEN = 'jp-LastModified-hidden';
90
+ /**
91
+ * The class name added to the size column header cell and size item cell when hidden.
92
+ */
93
+ const FILE_SIZE_COLUMN_HIDDEN = 'jp-FileSize-hidden';
77
94
  /**
78
95
  * The mime type for a contents drag object.
79
96
  */
@@ -175,6 +192,8 @@ export class DirListing extends Widget {
175
192
  this._manager = this._model.manager;
176
193
  this._renderer = options.renderer || DirListing.defaultRenderer;
177
194
  const headerNode = DOMUtils.findElement(this.node, HEADER_CLASS);
195
+ // hide the file size column by default
196
+ this._hiddenColumns.add('file_size');
178
197
  this._renderer.populateHeaderNode(headerNode, this.translator, this._hiddenColumns);
179
198
  this._manager.activateRequested.connect(this._onActivateRequested, this);
180
199
  }
@@ -248,7 +267,7 @@ export class DirListing extends Widget {
248
267
  * @returns A new iterator over the listing's sorted items.
249
268
  */
250
269
  sortedItems() {
251
- return new ArrayIterator(this._sortedItems);
270
+ return this._sortedItems[Symbol.iterator]();
252
271
  }
253
272
  /**
254
273
  * Sort the items using a sort condition.
@@ -292,9 +311,10 @@ export class DirListing extends Widget {
292
311
  }
293
312
  const basePath = this._model.path;
294
313
  const promises = [];
295
- each(this._clipboard, path => {
314
+ for (const path of this._clipboard) {
296
315
  if (this._isCut) {
297
- const parts = path.split('/');
316
+ const localPath = this._manager.services.contents.localPath(path);
317
+ const parts = localPath.split('/');
298
318
  const name = parts[parts.length - 1];
299
319
  const newPath = PathExt.join(basePath, name);
300
320
  promises.push(this._model.manager.rename(path, newPath));
@@ -302,11 +322,11 @@ export class DirListing extends Widget {
302
322
  else {
303
323
  promises.push(this._model.manager.copy(path, basePath));
304
324
  }
305
- });
325
+ }
306
326
  // Remove any cut modifiers.
307
- each(this._items, item => {
327
+ for (const item of this._items) {
308
328
  item.classList.remove(CUT_CLASS);
309
- });
329
+ }
310
330
  this._clipboard.length = 0;
311
331
  this._isCut = false;
312
332
  this.removeClass(CLIPBOARD_CLASS);
@@ -354,11 +374,11 @@ export class DirListing extends Widget {
354
374
  duplicate() {
355
375
  const basePath = this._model.path;
356
376
  const promises = [];
357
- each(this.selectedItems(), item => {
377
+ for (const item of this.selectedItems()) {
358
378
  if (item.type !== 'directory') {
359
379
  promises.push(this._model.manager.copy(item.path, basePath));
360
380
  }
361
- });
381
+ }
362
382
  return Promise.all(promises)
363
383
  .then(() => {
364
384
  return undefined;
@@ -371,7 +391,7 @@ export class DirListing extends Widget {
371
391
  * Download the currently selected item(s).
372
392
  */
373
393
  async download() {
374
- await Promise.all(toArray(this.selectedItems())
394
+ await Promise.all(Array.from(this.selectedItems())
375
395
  .filter(item => item.type !== 'directory')
376
396
  .map(item => this._model.download(item.path)));
377
397
  }
@@ -384,7 +404,7 @@ export class DirListing extends Widget {
384
404
  const model = this._model;
385
405
  const items = this._sortedItems;
386
406
  const paths = items.map(item => item.path);
387
- const promises = toArray(this._model.sessions())
407
+ const promises = Array.from(this._model.sessions())
388
408
  .filter(session => {
389
409
  const index = ArrayExt.firstIndexOf(paths, session.path);
390
410
  return this.selection[items[index].path];
@@ -485,7 +505,7 @@ export class DirListing extends Widget {
485
505
  */
486
506
  isSelected(name) {
487
507
  const items = this._sortedItems;
488
- return (toArray(filter(items, item => item.name === name && this.selection[item.path])).length !== 0);
508
+ return (Array.from(filter(items, item => item.name === name && this.selection[item.path])).length !== 0);
489
509
  }
490
510
  /**
491
511
  * Find a model given a click.
@@ -653,6 +673,7 @@ export class DirListing extends Widget {
653
673
  * A handler invoked on an `'update-request'` message.
654
674
  */
655
675
  onUpdateRequest(msg) {
676
+ var _a;
656
677
  this._isDirty = false;
657
678
  // Fetch common variables.
658
679
  const items = this._sortedItems;
@@ -673,21 +694,44 @@ export class DirListing extends Widget {
673
694
  content.appendChild(node);
674
695
  }
675
696
  // Remove extra classes from the nodes.
676
- nodes.forEach(item => {
677
- item.classList.remove(SELECTED_CLASS);
678
- item.classList.remove(RUNNING_CLASS);
679
- item.classList.remove(CUT_CLASS);
697
+ nodes.forEach(node => {
698
+ node.classList.remove(SELECTED_CLASS);
699
+ node.classList.remove(RUNNING_CLASS);
700
+ node.classList.remove(CUT_CLASS);
701
+ const checkbox = renderer.getCheckboxNode(node);
702
+ if (checkbox) {
703
+ // Uncheck each file checkbox
704
+ checkbox.checked = false;
705
+ }
680
706
  });
707
+ // Put the check-all checkbox in the header into the correct state
708
+ const checkAllCheckbox = renderer.getCheckboxNode(this.headerNode);
709
+ if (checkAllCheckbox) {
710
+ const totalSelected = Object.keys(this.selection).length;
711
+ const allSelected = items.length > 0 && totalSelected === items.length;
712
+ const someSelected = !allSelected && totalSelected > 0;
713
+ checkAllCheckbox.checked = allSelected;
714
+ checkAllCheckbox.indeterminate = someSelected;
715
+ // Stash the state in data attributes so we can access them in the click
716
+ // handler (because in the click handler, checkbox.checked and
717
+ // checkbox.indeterminate do not hold the previous value; they hold the
718
+ // next value).
719
+ checkAllCheckbox.dataset.checked = String(allSelected);
720
+ checkAllCheckbox.dataset.indeterminate = String(someSelected);
721
+ const trans = this.translator.load('jupyterlab');
722
+ checkAllCheckbox === null || checkAllCheckbox === void 0 ? void 0 : checkAllCheckbox.setAttribute('aria-label', allSelected || someSelected
723
+ ? trans.__('Deselect all files and directories')
724
+ : trans.__('Select all files and directories'));
725
+ }
681
726
  // Add extra classes to item nodes based on widget state.
682
727
  items.forEach((item, i) => {
683
728
  const node = nodes[i];
684
729
  const ft = this._manager.registry.getFileTypeForModel(item);
685
- renderer.updateItemNode(node, item, ft, this.translator, this._hiddenColumns);
686
- if (this.selection[item.path]) {
687
- node.classList.add(SELECTED_CLASS);
688
- if (this._isCut && this._model.path === this._prevPath) {
689
- node.classList.add(CUT_CLASS);
690
- }
730
+ renderer.updateItemNode(node, item, ft, this.translator, this._hiddenColumns, this.selection[item.path]);
731
+ if (this.selection[item.path] &&
732
+ this._isCut &&
733
+ this._model.path === this._prevPath) {
734
+ node.classList.add(CUT_CLASS);
691
735
  }
692
736
  // add metadata to the node
693
737
  node.setAttribute('data-isdir', item.type === 'directory' ? 'true' : 'false');
@@ -702,8 +746,7 @@ export class DirListing extends Widget {
702
746
  }
703
747
  // Handle file session statuses.
704
748
  const paths = items.map(item => item.path);
705
- each(this._model.sessions(), session => {
706
- var _a;
749
+ for (const session of this._model.sessions()) {
707
750
  const index = ArrayExt.firstIndexOf(paths, session.path);
708
751
  const node = nodes[index];
709
752
  // Node may have been filtered out.
@@ -713,11 +756,11 @@ export class DirListing extends Widget {
713
756
  node.classList.add(RUNNING_CLASS);
714
757
  if (specs && name) {
715
758
  const spec = specs.kernelspecs[name];
716
- name = spec ? spec.display_name : 'unknown'; // FIXME-TRANS: Is this localizable?
759
+ name = spec ? spec.display_name : this._trans.__('unknown');
717
760
  }
718
761
  node.title = this._trans.__('%1\nKernel: %2', node.title, name);
719
762
  }
720
- });
763
+ }
721
764
  this._prevPath = this._model.path;
722
765
  }
723
766
  onResize(msg) {
@@ -734,16 +777,50 @@ export class DirListing extends Widget {
734
777
  this.headerNode.innerHTML = '';
735
778
  this._renderer.populateHeaderNode(this.headerNode, this.translator, this._hiddenColumns);
736
779
  }
780
+ /**
781
+ * Would this click (or other event type) hit the checkbox by default?
782
+ */
783
+ isWithinCheckboxHitArea(event) {
784
+ let element = event.target;
785
+ while (element) {
786
+ if (element.classList.contains(CHECKBOX_WRAPPER_CLASS)) {
787
+ return true;
788
+ }
789
+ element = element.parentElement;
790
+ }
791
+ return false;
792
+ }
737
793
  /**
738
794
  * Handle the `'click'` event for the widget.
739
795
  */
740
796
  _evtClick(event) {
741
797
  const target = event.target;
742
798
  const header = this.headerNode;
799
+ const renderer = this._renderer;
743
800
  if (header.contains(target)) {
744
- const state = this.renderer.handleHeaderClick(header, event);
745
- if (state) {
746
- this.sort(state);
801
+ const checkbox = renderer.getCheckboxNode(header);
802
+ if (checkbox && this.isWithinCheckboxHitArea(event)) {
803
+ const previouslyUnchecked = checkbox.dataset.indeterminate === 'false' &&
804
+ checkbox.dataset.checked === 'false';
805
+ // The only time a click on the check-all checkbox should check all is
806
+ // when it was previously unchecked; otherwise, if the checkbox was
807
+ // either checked (all selected) or indeterminate (some selected), the
808
+ // click should clear all.
809
+ if (previouslyUnchecked) {
810
+ // Select all items
811
+ this._sortedItems.forEach((item) => (this.selection[item.path] = true));
812
+ }
813
+ else {
814
+ // Unselect all items
815
+ this.clearSelectedItems();
816
+ }
817
+ this.update();
818
+ }
819
+ else {
820
+ const state = this.renderer.handleHeaderClick(header, event);
821
+ if (state) {
822
+ this.sort(state);
823
+ }
747
824
  }
748
825
  return;
749
826
  }
@@ -928,6 +1005,12 @@ export class DirListing extends Widget {
928
1005
  if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
929
1006
  return;
930
1007
  }
1008
+ // Do nothing if the double click is on a checkbox. (Otherwise a rapid
1009
+ // check-uncheck on the checkbox will cause the adjacent file/folder to
1010
+ // open, which is probably not what the user intended.)
1011
+ if (this.isWithinCheckboxHitArea(event)) {
1012
+ return;
1013
+ }
931
1014
  // Stop the event propagation.
932
1015
  event.preventDefault();
933
1016
  event.stopPropagation();
@@ -1090,8 +1173,8 @@ export class DirListing extends Widget {
1090
1173
  }
1091
1174
  else {
1092
1175
  const path = selectedPaths[0];
1093
- item = find(items, value => value.path === path);
1094
- selectedItems = toArray(this.selectedItems());
1176
+ item = items.find(value => value.path === path);
1177
+ selectedItems = this.selectedItems();
1095
1178
  }
1096
1179
  if (!item) {
1097
1180
  return;
@@ -1173,8 +1256,14 @@ export class DirListing extends Widget {
1173
1256
  this._softSelection = '';
1174
1257
  const path = items[index].path;
1175
1258
  const selected = Object.keys(this.selection);
1259
+ const isLeftClickOnCheckbox = event.button === 0 &&
1260
+ // On Mac, a left-click with the ctrlKey is treated as a right-click.
1261
+ !(IS_MAC && event.ctrlKey) &&
1262
+ this.isWithinCheckboxHitArea(event);
1176
1263
  // Handle toggling.
1177
- if ((IS_MAC && event.metaKey) || (!IS_MAC && event.ctrlKey)) {
1264
+ if ((IS_MAC && event.metaKey) ||
1265
+ (!IS_MAC && event.ctrlKey) ||
1266
+ isLeftClickOnCheckbox) {
1178
1267
  if (this.selection[path]) {
1179
1268
  delete this.selection[path];
1180
1269
  }
@@ -1262,9 +1351,9 @@ export class DirListing extends Widget {
1262
1351
  */
1263
1352
  _copy() {
1264
1353
  this._clipboard.length = 0;
1265
- each(this.selectedItems(), item => {
1354
+ for (const item of this.selectedItems()) {
1266
1355
  this._clipboard.push(item.path);
1267
- });
1356
+ }
1268
1357
  }
1269
1358
  /**
1270
1359
  * Delete the files with the given paths.
@@ -1352,12 +1441,12 @@ export class DirListing extends Widget {
1352
1441
  // Update the selection.
1353
1442
  const existing = Object.keys(this.selection);
1354
1443
  this.clearSelectedItems();
1355
- each(this._model.items(), item => {
1444
+ for (const item of this._model.items()) {
1356
1445
  const path = item.path;
1357
1446
  if (existing.indexOf(path) !== -1) {
1358
1447
  this.selection[path] = true;
1359
1448
  }
1360
- });
1449
+ }
1361
1450
  if (this.isVisible) {
1362
1451
  // Update the sorted items.
1363
1452
  this.sort(this.sortState);
@@ -1433,26 +1522,40 @@ export class DirListing extends Widget {
1433
1522
  * @param node - The header node to populate.
1434
1523
  */
1435
1524
  populateHeaderNode(node, translator, hiddenColumns) {
1436
- var _a;
1437
1525
  translator = translator || nullTranslator;
1438
1526
  const trans = translator.load('jupyterlab');
1439
1527
  const name = this.createHeaderItemNode(trans.__('Name'));
1440
1528
  const narrow = document.createElement('div');
1441
1529
  const modified = this.createHeaderItemNode(trans.__('Last Modified'));
1530
+ const fileSize = this.createHeaderItemNode(trans.__('File Size'));
1442
1531
  name.classList.add(NAME_ID_CLASS);
1443
1532
  name.classList.add(SELECTED_CLASS);
1444
1533
  modified.classList.add(MODIFIED_ID_CLASS);
1534
+ fileSize.classList.add(FILE_SIZE_ID_CLASS);
1445
1535
  narrow.classList.add(NARROW_ID_CLASS);
1446
1536
  narrow.textContent = '...';
1537
+ if (!(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'))) {
1538
+ const checkboxWrapper = this.createCheckboxWrapperNode({
1539
+ alwaysVisible: true
1540
+ });
1541
+ node.appendChild(checkboxWrapper);
1542
+ }
1447
1543
  node.appendChild(name);
1448
1544
  node.appendChild(narrow);
1449
1545
  node.appendChild(modified);
1450
- if ((_a = hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has) === null || _a === void 0 ? void 0 : _a.call(hiddenColumns, 'last_modified')) {
1546
+ node.appendChild(fileSize);
1547
+ if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
1451
1548
  modified.classList.add(MODIFIED_COLUMN_HIDDEN);
1452
1549
  }
1453
1550
  else {
1454
1551
  modified.classList.remove(MODIFIED_COLUMN_HIDDEN);
1455
1552
  }
1553
+ if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size')) {
1554
+ fileSize.classList.add(FILE_SIZE_COLUMN_HIDDEN);
1555
+ }
1556
+ else {
1557
+ fileSize.classList.remove(FILE_SIZE_COLUMN_HIDDEN);
1558
+ }
1456
1559
  // set the initial caret icon
1457
1560
  Private.updateCaret(DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS), 'right', 'up');
1458
1561
  }
@@ -1468,11 +1571,13 @@ export class DirListing extends Widget {
1468
1571
  handleHeaderClick(node, event) {
1469
1572
  const name = DOMUtils.findElement(node, NAME_ID_CLASS);
1470
1573
  const modified = DOMUtils.findElement(node, MODIFIED_ID_CLASS);
1574
+ const fileSize = DOMUtils.findElement(node, FILE_SIZE_ID_CLASS);
1471
1575
  const state = { direction: 'ascending', key: 'name' };
1472
1576
  const target = event.target;
1577
+ const modifiedIcon = DOMUtils.findElement(modified, HEADER_ITEM_ICON_CLASS);
1578
+ const fileSizeIcon = DOMUtils.findElement(fileSize, HEADER_ITEM_ICON_CLASS);
1579
+ const nameIcon = DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS);
1473
1580
  if (name.contains(target)) {
1474
- const modifiedIcon = DOMUtils.findElement(modified, HEADER_ITEM_ICON_CLASS);
1475
- const nameIcon = DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS);
1476
1581
  if (name.classList.contains(SELECTED_CLASS)) {
1477
1582
  if (!name.classList.contains(DESCENDING_CLASS)) {
1478
1583
  state.direction = 'descending';
@@ -1491,12 +1596,13 @@ export class DirListing extends Widget {
1491
1596
  name.classList.add(SELECTED_CLASS);
1492
1597
  modified.classList.remove(SELECTED_CLASS);
1493
1598
  modified.classList.remove(DESCENDING_CLASS);
1599
+ fileSize.classList.remove(SELECTED_CLASS);
1600
+ fileSize.classList.remove(DESCENDING_CLASS);
1494
1601
  Private.updateCaret(modifiedIcon, 'left');
1602
+ Private.updateCaret(fileSizeIcon, 'left');
1495
1603
  return state;
1496
1604
  }
1497
1605
  if (modified.contains(target)) {
1498
- const modifiedIcon = DOMUtils.findElement(modified, HEADER_ITEM_ICON_CLASS);
1499
- const nameIcon = DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS);
1500
1606
  state.key = 'last_modified';
1501
1607
  if (modified.classList.contains(SELECTED_CLASS)) {
1502
1608
  if (!modified.classList.contains(DESCENDING_CLASS)) {
@@ -1516,7 +1622,36 @@ export class DirListing extends Widget {
1516
1622
  modified.classList.add(SELECTED_CLASS);
1517
1623
  name.classList.remove(SELECTED_CLASS);
1518
1624
  name.classList.remove(DESCENDING_CLASS);
1625
+ fileSize.classList.remove(SELECTED_CLASS);
1626
+ fileSize.classList.remove(DESCENDING_CLASS);
1627
+ Private.updateCaret(nameIcon, 'right');
1628
+ Private.updateCaret(fileSizeIcon, 'left');
1629
+ return state;
1630
+ }
1631
+ if (fileSize.contains(target)) {
1632
+ state.key = 'file_size';
1633
+ if (fileSize.classList.contains(SELECTED_CLASS)) {
1634
+ if (!fileSize.classList.contains(DESCENDING_CLASS)) {
1635
+ state.direction = 'descending';
1636
+ fileSize.classList.add(DESCENDING_CLASS);
1637
+ Private.updateCaret(fileSizeIcon, 'left', 'down');
1638
+ }
1639
+ else {
1640
+ fileSize.classList.remove(DESCENDING_CLASS);
1641
+ Private.updateCaret(fileSizeIcon, 'left', 'up');
1642
+ }
1643
+ }
1644
+ else {
1645
+ fileSize.classList.remove(DESCENDING_CLASS);
1646
+ Private.updateCaret(fileSizeIcon, 'left', 'up');
1647
+ }
1648
+ fileSize.classList.add(SELECTED_CLASS);
1649
+ name.classList.remove(SELECTED_CLASS);
1650
+ name.classList.remove(DESCENDING_CLASS);
1651
+ modified.classList.remove(SELECTED_CLASS);
1652
+ modified.classList.remove(DESCENDING_CLASS);
1519
1653
  Private.updateCaret(nameIcon, 'right');
1654
+ Private.updateCaret(modifiedIcon, 'left');
1520
1655
  return state;
1521
1656
  }
1522
1657
  return state;
@@ -1527,30 +1662,76 @@ export class DirListing extends Widget {
1527
1662
  * @returns A new DOM node to use as a content item.
1528
1663
  */
1529
1664
  createItemNode(hiddenColumns) {
1530
- var _a;
1531
1665
  const node = document.createElement('li');
1532
1666
  const icon = document.createElement('span');
1533
1667
  const text = document.createElement('span');
1534
1668
  const modified = document.createElement('span');
1669
+ const fileSize = document.createElement('span');
1535
1670
  icon.className = ITEM_ICON_CLASS;
1536
1671
  text.className = ITEM_TEXT_CLASS;
1537
1672
  modified.className = ITEM_MODIFIED_CLASS;
1673
+ fileSize.className = ITEM_FILE_SIZE_CLASS;
1674
+ if (!(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'))) {
1675
+ const checkboxWrapper = this.createCheckboxWrapperNode();
1676
+ node.appendChild(checkboxWrapper);
1677
+ }
1538
1678
  node.appendChild(icon);
1539
1679
  node.appendChild(text);
1540
1680
  node.appendChild(modified);
1681
+ node.appendChild(fileSize);
1541
1682
  // Make the text note focusable so that it receives keyboard events;
1542
1683
  // text node was specifically chosen to receive shortcuts because
1543
1684
  // text element gets substituted with input area during file name edits
1544
1685
  // which conveniently deactivate irrelevant shortcuts.
1545
1686
  text.tabIndex = 0;
1546
- if ((_a = hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has) === null || _a === void 0 ? void 0 : _a.call(hiddenColumns, 'last_modified')) {
1687
+ if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
1547
1688
  modified.classList.add(MODIFIED_COLUMN_HIDDEN);
1548
1689
  }
1549
1690
  else {
1550
1691
  modified.classList.remove(MODIFIED_COLUMN_HIDDEN);
1551
1692
  }
1693
+ if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size')) {
1694
+ fileSize.classList.add(FILE_SIZE_COLUMN_HIDDEN);
1695
+ }
1696
+ else {
1697
+ fileSize.classList.remove(FILE_SIZE_COLUMN_HIDDEN);
1698
+ }
1552
1699
  return node;
1553
1700
  }
1701
+ /**
1702
+ * Creates a node containing a checkbox.
1703
+ *
1704
+ * We wrap the checkbox in a label element in order to increase its hit
1705
+ * area. This is because the padding of the checkbox itself cannot be
1706
+ * increased via CSS, as the CSS/form compatibility table at the following
1707
+ * url from MDN shows:
1708
+ * https://developer.mozilla.org/en-US/docs/Learn/Forms/Property_compatibility_table_for_form_controls#check_boxes_and_radio_buttons
1709
+ *
1710
+ * @param [options]
1711
+ * @params options.alwaysVisible Should the checkbox be visible even when
1712
+ * not hovered?
1713
+ * @returns A new DOM node that contains a checkbox.
1714
+ */
1715
+ createCheckboxWrapperNode(options) {
1716
+ // Wrap the checkbox in a label element in order to increase its hit area.
1717
+ const labelWrapper = document.createElement('label');
1718
+ labelWrapper.classList.add(CHECKBOX_WRAPPER_CLASS);
1719
+ // The individual file checkboxes are visible on hover, but the header
1720
+ // check-all checkbox is always visible.
1721
+ if (options === null || options === void 0 ? void 0 : options.alwaysVisible) {
1722
+ labelWrapper.classList.add('jp-mod-visible');
1723
+ }
1724
+ const checkbox = document.createElement('input');
1725
+ checkbox.type = 'checkbox';
1726
+ // Prevent the user from clicking (via mouse, keyboard, or touch) the
1727
+ // checkbox since other code handles the mouse and keyboard events and
1728
+ // controls the checked state of the checkbox.
1729
+ checkbox.addEventListener('click', event => {
1730
+ event.preventDefault();
1731
+ });
1732
+ labelWrapper.appendChild(checkbox);
1733
+ return labelWrapper;
1734
+ }
1554
1735
  /**
1555
1736
  * Update an item node to reflect the current state of a model.
1556
1737
  *
@@ -1561,9 +1742,10 @@ export class DirListing extends Widget {
1561
1742
  * @param fileType - The file type of the item, if applicable.
1562
1743
  *
1563
1744
  */
1564
- updateItemNode(node, model, fileType, translator, hiddenColumns) {
1565
- var _a;
1566
- translator = translator || nullTranslator;
1745
+ updateItemNode(node, model, fileType, translator, hiddenColumns, selected) {
1746
+ if (selected) {
1747
+ node.classList.add(SELECTED_CLASS);
1748
+ }
1567
1749
  fileType =
1568
1750
  fileType || DocumentRegistry.getDefaultTextFileType(translator);
1569
1751
  const { icon, iconClass, name } = fileType;
@@ -1572,12 +1754,28 @@ export class DirListing extends Widget {
1572
1754
  const iconContainer = DOMUtils.findElement(node, ITEM_ICON_CLASS);
1573
1755
  const text = DOMUtils.findElement(node, ITEM_TEXT_CLASS);
1574
1756
  const modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
1575
- if ((_a = hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has) === null || _a === void 0 ? void 0 : _a.call(hiddenColumns, 'last_modified')) {
1757
+ const fileSize = DOMUtils.findElement(node, ITEM_FILE_SIZE_CLASS);
1758
+ const checkboxWrapper = DOMUtils.findElement(node, CHECKBOX_WRAPPER_CLASS);
1759
+ const showFileCheckboxes = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'));
1760
+ if (checkboxWrapper && !showFileCheckboxes) {
1761
+ node.removeChild(checkboxWrapper);
1762
+ }
1763
+ else if (showFileCheckboxes && !checkboxWrapper) {
1764
+ const checkboxWrapper = this.createCheckboxWrapperNode();
1765
+ node.insertBefore(checkboxWrapper, iconContainer);
1766
+ }
1767
+ if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
1576
1768
  modified.classList.add(MODIFIED_COLUMN_HIDDEN);
1577
1769
  }
1578
1770
  else {
1579
1771
  modified.classList.remove(MODIFIED_COLUMN_HIDDEN);
1580
1772
  }
1773
+ if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size')) {
1774
+ fileSize.classList.add(FILE_SIZE_COLUMN_HIDDEN);
1775
+ }
1776
+ else {
1777
+ fileSize.classList.remove(FILE_SIZE_COLUMN_HIDDEN);
1778
+ }
1581
1779
  // render the file item's icon
1582
1780
  LabIcon.resolveElement({
1583
1781
  icon,
@@ -1589,8 +1787,13 @@ export class DirListing extends Widget {
1589
1787
  let hoverText = trans.__('Name: %1', model.name);
1590
1788
  // add file size to pop up if its available
1591
1789
  if (model.size !== null && model.size !== undefined) {
1790
+ const fileSizeText = Private.formatFileSize(model.size, 1, 1024);
1791
+ fileSize.textContent = fileSizeText;
1592
1792
  hoverText += trans.__('\nSize: %1', Private.formatFileSize(model.size, 1, 1024));
1593
1793
  }
1794
+ else {
1795
+ fileSize.textContent = '';
1796
+ }
1594
1797
  if (model.path) {
1595
1798
  const dirname = PathExt.dirname(model.path);
1596
1799
  if (dirname) {
@@ -1601,10 +1804,10 @@ export class DirListing extends Widget {
1601
1804
  }
1602
1805
  }
1603
1806
  if (model.created) {
1604
- hoverText += trans.__('\nCreated: %1', Time.format(new Date(model.created), 'YYYY-MM-DD HH:mm:ss'));
1807
+ hoverText += trans.__('\nCreated: %1', Time.format(new Date(model.created)));
1605
1808
  }
1606
1809
  if (model.last_modified) {
1607
- hoverText += trans.__('\nModified: %1', Time.format(new Date(model.last_modified), 'YYYY-MM-DD HH:mm:ss'));
1810
+ hoverText += trans.__('\nModified: %1', Time.format(new Date(model.last_modified)));
1608
1811
  }
1609
1812
  hoverText += trans.__('\nWritable: %1', model.writable);
1610
1813
  node.title = hoverText;
@@ -1616,16 +1819,33 @@ export class DirListing extends Widget {
1616
1819
  node.removeAttribute('data-is-dot');
1617
1820
  }
1618
1821
  // If an item is being edited currently, its text node is unavailable.
1822
+ const indices = !model.indices ? [] : model.indices;
1823
+ let highlightedName = StringExt.highlight(model.name, indices, h.mark);
1619
1824
  if (text) {
1620
- const indices = !model.indices ? [] : model.indices;
1621
- let highlightedName = StringExt.highlight(model.name, indices, h.mark);
1622
1825
  VirtualDOM.render(h.span(highlightedName), text);
1623
1826
  }
1827
+ // Adds an aria-label to the checkbox element.
1828
+ const checkbox = checkboxWrapper === null || checkboxWrapper === void 0 ? void 0 : checkboxWrapper.querySelector('input[type="checkbox"]');
1829
+ if (checkbox) {
1830
+ let ariaLabel;
1831
+ if (fileType.contentType === 'directory') {
1832
+ ariaLabel = selected
1833
+ ? trans.__('Deselect directory "%1"', highlightedName)
1834
+ : trans.__('Select directory "%1"', highlightedName);
1835
+ }
1836
+ else {
1837
+ ariaLabel = selected
1838
+ ? trans.__('Deselect file "%1"', highlightedName)
1839
+ : trans.__('Select file "%1"', highlightedName);
1840
+ }
1841
+ checkbox.setAttribute('aria-label', ariaLabel);
1842
+ checkbox.checked = selected !== null && selected !== void 0 ? selected : false;
1843
+ }
1624
1844
  let modText = '';
1625
1845
  let modTitle = '';
1626
1846
  if (model.last_modified) {
1627
1847
  modText = Time.formatHuman(new Date(model.last_modified));
1628
- modTitle = Time.format(new Date(model.last_modified), 'lll');
1848
+ modTitle = Time.format(new Date(model.last_modified));
1629
1849
  }
1630
1850
  modified.textContent = modText;
1631
1851
  modified.title = modTitle;
@@ -1640,6 +1860,17 @@ export class DirListing extends Widget {
1640
1860
  getNameNode(node) {
1641
1861
  return DOMUtils.findElement(node, ITEM_TEXT_CLASS);
1642
1862
  }
1863
+ /**
1864
+ * Get the checkbox input element node.
1865
+ *
1866
+ * @param node A node created by [[createItemNode]] or
1867
+ * [[createHeaderItemNode]]
1868
+ *
1869
+ * @returns The checkbox node.
1870
+ */
1871
+ getCheckboxNode(node) {
1872
+ return node.querySelector(`.${CHECKBOX_WRAPPER_CLASS} input[type=checkbox]`);
1873
+ }
1643
1874
  /**
1644
1875
  * Create a drag image for an item.
1645
1876
  *
@@ -1757,7 +1988,7 @@ var Private;
1757
1988
  * Sort a list of items by sort state as a new array.
1758
1989
  */
1759
1990
  function sort(items, state) {
1760
- const copy = toArray(items);
1991
+ const copy = Array.from(items);
1761
1992
  const reverse = state.direction === 'descending' ? 1 : -1;
1762
1993
  if (state.key === 'last_modified') {
1763
1994
  // Sort by last modified (grouping directories first)
@@ -1769,6 +2000,15 @@ var Private;
1769
2000
  return t1 - t2 || (valA - valB) * reverse;
1770
2001
  });
1771
2002
  }
2003
+ else if (state.key === 'file_size') {
2004
+ // Sort by size (grouping directories first)
2005
+ copy.sort((a, b) => {
2006
+ var _a, _b;
2007
+ const t1 = a.type === 'directory' ? 0 : 1;
2008
+ const t2 = b.type === 'directory' ? 0 : 1;
2009
+ return t1 - t2 || (((_a = a.size) !== null && _a !== void 0 ? _a : 0) - ((_b = b.size) !== null && _b !== void 0 ? _b : 0)) * reverse;
2010
+ });
2011
+ }
1772
2012
  else {
1773
2013
  // Sort by name (grouping directories first)
1774
2014
  copy.sort((a, b) => {
@@ -1794,10 +2034,10 @@ var Private;
1794
2034
  function formatFileSize(bytes, decimalPoint, k) {
1795
2035
  // https://www.codexworld.com/how-to/convert-file-size-bytes-kb-mb-gb-javascript/
1796
2036
  if (bytes === 0) {
1797
- return '0 Bytes';
2037
+ return '0 B';
1798
2038
  }
1799
2039
  const dm = decimalPoint || 2;
1800
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
2040
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
1801
2041
  const i = Math.floor(Math.log(bytes) / Math.log(k));
1802
2042
  if (i >= 0 && i < sizes.length) {
1803
2043
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];