@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/browser.d.ts +15 -18
- package/lib/browser.js +50 -64
- package/lib/browser.js.map +1 -1
- package/lib/crumbs.d.ts +1 -0
- package/lib/crumbs.js +41 -10
- package/lib/crumbs.js.map +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/listing.d.ts +54 -12
- package/lib/listing.js +296 -56
- package/lib/listing.js.map +1 -1
- package/lib/model.d.ts +19 -9
- package/lib/model.js +32 -11
- package/lib/model.js.map +1 -1
- package/lib/opendialog.d.ts +2 -1
- package/lib/opendialog.js +42 -10
- package/lib/opendialog.js.map +1 -1
- package/lib/tokens.d.ts +16 -8
- package/lib/tokens.js +9 -2
- package/lib/tokens.js.map +1 -1
- package/lib/upload.d.ts +4 -0
- package/lib/upload.js +1 -0
- package/lib/upload.js.map +1 -1
- package/lib/uploadstatus.js +4 -4
- package/lib/uploadstatus.js.map +1 -1
- package/package.json +27 -28
- package/src/browser.ts +444 -0
- package/src/crumbs.ts +441 -0
- package/src/index.ts +15 -0
- package/src/listing.ts +2609 -0
- package/src/model.ts +832 -0
- package/src/opendialog.ts +257 -0
- package/src/tokens.ts +122 -0
- package/src/upload.ts +119 -0
- package/src/uploadstatus.tsx +259 -0
- package/style/base.css +83 -62
- package/lib/search.d.ts +0 -15
- package/lib/search.js +0 -92
- package/lib/search.js.map +0 -1
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,
|
|
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
|
|
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
|
-
|
|
314
|
+
for (const path of this._clipboard) {
|
|
296
315
|
if (this._isCut) {
|
|
297
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 (
|
|
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(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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';
|
|
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
|
|
745
|
-
if (
|
|
746
|
-
|
|
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(
|
|
1094
|
-
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) ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1566
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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 =
|
|
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
|
|
2037
|
+
return '0 B';
|
|
1798
2038
|
}
|
|
1799
2039
|
const dm = decimalPoint || 2;
|
|
1800
|
-
const sizes = ['
|
|
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];
|