@jupyterlab/filebrowser 4.0.0-beta.1 → 4.0.0-rc.0
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 +9 -2
- package/lib/browser.js +42 -41
- package/lib/browser.js.map +1 -1
- package/lib/listing.d.ts +40 -5
- package/lib/listing.js +365 -140
- package/lib/listing.js.map +1 -1
- package/lib/opendialog.js +1 -1
- package/lib/opendialog.js.map +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tokens.js.map +1 -1
- package/package.json +21 -21
- package/src/browser.ts +52 -51
- package/src/listing.ts +424 -166
- package/src/opendialog.ts +1 -1
- package/src/tokens.ts +8 -3
- package/style/base.css +11 -5
package/lib/listing.js
CHANGED
|
@@ -180,6 +180,8 @@ export class DirListing extends Widget {
|
|
|
180
180
|
this._inRename = false;
|
|
181
181
|
this._isDirty = false;
|
|
182
182
|
this._hiddenColumns = new Set();
|
|
183
|
+
// _focusIndex should never be set outside the range [0, this._items.length - 1]
|
|
184
|
+
this._focusIndex = 0;
|
|
183
185
|
this.addClass(DIR_LISTING_CLASS);
|
|
184
186
|
this.translator = options.translator || nullTranslator;
|
|
185
187
|
this._trans = this.translator.load('jupyterlab');
|
|
@@ -365,6 +367,15 @@ export class DirListing extends Widget {
|
|
|
365
367
|
if (!this.isDisposed && result.button.accept) {
|
|
366
368
|
await this._delete(items.map(item => item.path));
|
|
367
369
|
}
|
|
370
|
+
// Re-focus
|
|
371
|
+
let focusIndex = this._focusIndex;
|
|
372
|
+
const lastIndexAfterDelete = this._sortedItems.length - items.length - 1;
|
|
373
|
+
if (focusIndex > lastIndexAfterDelete) {
|
|
374
|
+
// If the focus index after deleting items is out of bounds, set it to the
|
|
375
|
+
// last item.
|
|
376
|
+
focusIndex = Math.max(0, lastIndexAfterDelete);
|
|
377
|
+
}
|
|
378
|
+
this._focusItem(focusIndex);
|
|
368
379
|
}
|
|
369
380
|
/**
|
|
370
381
|
* Duplicate the currently selected item(s).
|
|
@@ -532,7 +543,7 @@ export class DirListing extends Widget {
|
|
|
532
543
|
* Select an item by name.
|
|
533
544
|
*
|
|
534
545
|
* @param name - The name of the item to select.
|
|
535
|
-
* @param focus - Whether to move focus the selected item.
|
|
546
|
+
* @param focus - Whether to move focus to the selected item.
|
|
536
547
|
*
|
|
537
548
|
* @returns A promise that resolves when the name is selected.
|
|
538
549
|
*/
|
|
@@ -693,16 +704,23 @@ export class DirListing extends Widget {
|
|
|
693
704
|
nodes.push(node);
|
|
694
705
|
content.appendChild(node);
|
|
695
706
|
}
|
|
696
|
-
|
|
697
|
-
|
|
707
|
+
nodes.forEach((node, i) => {
|
|
708
|
+
// Remove extra classes from the nodes.
|
|
698
709
|
node.classList.remove(SELECTED_CLASS);
|
|
699
710
|
node.classList.remove(RUNNING_CLASS);
|
|
700
711
|
node.classList.remove(CUT_CLASS);
|
|
712
|
+
// Uncheck each file checkbox
|
|
701
713
|
const checkbox = renderer.getCheckboxNode(node);
|
|
702
714
|
if (checkbox) {
|
|
703
|
-
// Uncheck each file checkbox
|
|
704
715
|
checkbox.checked = false;
|
|
705
716
|
}
|
|
717
|
+
// Handle `tabIndex`
|
|
718
|
+
const nameNode = renderer.getNameNode(node);
|
|
719
|
+
if (nameNode) {
|
|
720
|
+
// Must check if the name node is there because it gets replaced by the
|
|
721
|
+
// edit node when editing the name of the file or directory.
|
|
722
|
+
nameNode.tabIndex = i === this._focusIndex ? 0 : -1;
|
|
723
|
+
}
|
|
706
724
|
});
|
|
707
725
|
// Put the check-all checkbox in the header into the correct state
|
|
708
726
|
const checkAllCheckbox = renderer.getCheckboxNode(this.headerNode);
|
|
@@ -723,7 +741,7 @@ export class DirListing extends Widget {
|
|
|
723
741
|
? trans.__('Deselect all files and directories')
|
|
724
742
|
: trans.__('Select all files and directories'));
|
|
725
743
|
}
|
|
726
|
-
//
|
|
744
|
+
// Update item nodes based on widget state.
|
|
727
745
|
items.forEach((item, i) => {
|
|
728
746
|
const node = nodes[i];
|
|
729
747
|
const ft = this._manager.registry.getFileTypeForModel(item);
|
|
@@ -824,6 +842,13 @@ export class DirListing extends Widget {
|
|
|
824
842
|
}
|
|
825
843
|
return;
|
|
826
844
|
}
|
|
845
|
+
else {
|
|
846
|
+
// Focus the selected file on click to ensure a couple of things:
|
|
847
|
+
// 1. If a user clicks on the item node, its name node will receive focus.
|
|
848
|
+
// 2. If a user clicks on blank space in the directory listing, the
|
|
849
|
+
// previously focussed item will be focussed.
|
|
850
|
+
this._focusItem(this._focusIndex);
|
|
851
|
+
}
|
|
827
852
|
}
|
|
828
853
|
/**
|
|
829
854
|
* Handle the `'scroll'` event for the widget.
|
|
@@ -889,11 +914,11 @@ export class DirListing extends Widget {
|
|
|
889
914
|
}
|
|
890
915
|
this._softSelection = '';
|
|
891
916
|
}
|
|
892
|
-
// Re-focus
|
|
893
|
-
//
|
|
894
|
-
//
|
|
917
|
+
// Re-focus. This is needed because nodes corresponding to files selected in
|
|
918
|
+
// mousedown handler will not retain the focus as mousedown event is always
|
|
919
|
+
// followed by a blur/focus event.
|
|
895
920
|
if (event.button === 0) {
|
|
896
|
-
this.
|
|
921
|
+
this._focusItem(this._focusIndex);
|
|
897
922
|
}
|
|
898
923
|
// Remove the drag listeners if necessary.
|
|
899
924
|
if (event.button !== 0 || !this._drag) {
|
|
@@ -939,10 +964,102 @@ export class DirListing extends Widget {
|
|
|
939
964
|
this._manager.openOrReveal(path);
|
|
940
965
|
}
|
|
941
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* Calculate the next focus index, given the current focus index and a
|
|
969
|
+
* direction, keeping within the bounds of the directory listing.
|
|
970
|
+
*
|
|
971
|
+
* @param index Current focus index
|
|
972
|
+
* @param direction -1 (up) or 1 (down)
|
|
973
|
+
* @returns The next focus index, which could be the same as the current focus
|
|
974
|
+
* index if at the boundary.
|
|
975
|
+
*/
|
|
976
|
+
_getNextFocusIndex(index, direction) {
|
|
977
|
+
const nextIndex = index + direction;
|
|
978
|
+
if (nextIndex === -1 || nextIndex === this._items.length) {
|
|
979
|
+
// keep focus index within bounds
|
|
980
|
+
return index;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
return nextIndex;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Handle the up or down arrow key.
|
|
988
|
+
*
|
|
989
|
+
* @param event The keyboard event
|
|
990
|
+
* @param direction -1 (up) or 1 (down)
|
|
991
|
+
*/
|
|
992
|
+
_handleArrowY(event, direction) {
|
|
993
|
+
// We only handle the `ctrl` and `shift` modifiers. If other modifiers are
|
|
994
|
+
// present, then do nothing.
|
|
995
|
+
if (event.altKey || event.metaKey) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
// If folder is empty, there's nothing to do with the up/down key.
|
|
999
|
+
if (!this._items.length) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// Don't handle the arrow key press if it's not on directory item. This
|
|
1003
|
+
// avoids a confusing user experience that can result from when the user
|
|
1004
|
+
// moves the selection and focus index apart (via ctrl + up/down). The last
|
|
1005
|
+
// selected item remains highlighted but the last focussed item loses its
|
|
1006
|
+
// focus ring if it's not actively focussed. This forces the user to
|
|
1007
|
+
// visibly reveal the last focussed item before moving the focus.
|
|
1008
|
+
if (!event.target.classList.contains(ITEM_TEXT_CLASS)) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
event.stopPropagation();
|
|
1012
|
+
event.preventDefault();
|
|
1013
|
+
const focusIndex = this._focusIndex;
|
|
1014
|
+
let nextFocusIndex = this._getNextFocusIndex(focusIndex, direction);
|
|
1015
|
+
// The following if-block allows the first press of the down arrow to select
|
|
1016
|
+
// the first (rather than the second) file/directory in the list. This is
|
|
1017
|
+
// the situation when the page first loads or when a user changes directory.
|
|
1018
|
+
if (direction > 0 &&
|
|
1019
|
+
focusIndex === 0 &&
|
|
1020
|
+
!event.ctrlKey &&
|
|
1021
|
+
Object.keys(this.selection).length === 0) {
|
|
1022
|
+
nextFocusIndex = 0;
|
|
1023
|
+
}
|
|
1024
|
+
// Shift key indicates multi-selection. Either the user is trying to grow
|
|
1025
|
+
// the selection, or shrink it.
|
|
1026
|
+
if (event.shiftKey) {
|
|
1027
|
+
this._handleMultiSelect(nextFocusIndex);
|
|
1028
|
+
}
|
|
1029
|
+
else if (!event.ctrlKey) {
|
|
1030
|
+
// If neither the shift nor ctrl keys were used with the up/down arrow,
|
|
1031
|
+
// then we treat it as a normal, unmodified key press and select the
|
|
1032
|
+
// next item.
|
|
1033
|
+
this._selectItem(nextFocusIndex, event.shiftKey, false /* focus = false because we call focus method directly following this */);
|
|
1034
|
+
}
|
|
1035
|
+
this._focusItem(nextFocusIndex);
|
|
1036
|
+
this.update();
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* cd ..
|
|
1040
|
+
*
|
|
1041
|
+
* Go up one level in the directory tree.
|
|
1042
|
+
*/
|
|
1043
|
+
async goUp() {
|
|
1044
|
+
const model = this.model;
|
|
1045
|
+
if (model.path === model.rootPath) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
await model.cd('..');
|
|
1050
|
+
}
|
|
1051
|
+
catch (reason) {
|
|
1052
|
+
console.warn(`Failed to go to parent directory of ${model.path}`, reason);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
942
1055
|
/**
|
|
943
1056
|
* Handle the `'keydown'` event for the widget.
|
|
944
1057
|
*/
|
|
945
1058
|
evtKeydown(event) {
|
|
1059
|
+
// Do not handle any keydown events here if in the middle of a file rename.
|
|
1060
|
+
if (this._inRename) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
946
1063
|
switch (event.keyCode) {
|
|
947
1064
|
case 13: {
|
|
948
1065
|
// Enter
|
|
@@ -952,34 +1069,64 @@ export class DirListing extends Widget {
|
|
|
952
1069
|
}
|
|
953
1070
|
event.preventDefault();
|
|
954
1071
|
event.stopPropagation();
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1072
|
+
for (const item of this.selectedItems()) {
|
|
1073
|
+
this.handleOpen(item);
|
|
1074
|
+
}
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
case 38:
|
|
1078
|
+
// Up arrow
|
|
1079
|
+
this._handleArrowY(event, -1);
|
|
1080
|
+
return;
|
|
1081
|
+
case 40:
|
|
1082
|
+
// Down arrow
|
|
1083
|
+
this._handleArrowY(event, 1);
|
|
1084
|
+
return;
|
|
1085
|
+
case 32: {
|
|
1086
|
+
// Space
|
|
1087
|
+
if (event.ctrlKey) {
|
|
1088
|
+
// Follow the Windows and Ubuntu convention: you must press `ctrl` +
|
|
1089
|
+
// `space` in order to toggle whether an item is selected.
|
|
1090
|
+
// However, do not handle if any other modifiers were pressed.
|
|
1091
|
+
if (event.metaKey || event.shiftKey || event.altKey) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
// Make sure the ctrl+space key stroke was on a valid, focussed target.
|
|
1095
|
+
const node = this._items[this._focusIndex];
|
|
1096
|
+
if (!(
|
|
1097
|
+
// Event must have occurred within a node whose item can be toggled.
|
|
1098
|
+
(node.contains(event.target) &&
|
|
1099
|
+
// That node must also contain the currently focussed element.
|
|
1100
|
+
node.contains(document.activeElement)))) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
event.stopPropagation();
|
|
1104
|
+
// Prevent default, otherwise the container will scroll.
|
|
1105
|
+
event.preventDefault();
|
|
1106
|
+
// Toggle item selected
|
|
1107
|
+
const { path } = this._sortedItems[this._focusIndex];
|
|
1108
|
+
if (this.selection[path]) {
|
|
1109
|
+
delete this.selection[path];
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
this.selection[path] = true;
|
|
1113
|
+
}
|
|
1114
|
+
this.update();
|
|
1115
|
+
// Key was handled, so return.
|
|
960
1116
|
return;
|
|
961
1117
|
}
|
|
962
|
-
const item = this._sortedItems[i];
|
|
963
|
-
this.handleOpen(item);
|
|
964
1118
|
break;
|
|
965
1119
|
}
|
|
966
|
-
case 38: // Up arrow
|
|
967
|
-
this.selectPrevious(event.shiftKey);
|
|
968
|
-
event.stopPropagation();
|
|
969
|
-
event.preventDefault();
|
|
970
|
-
break;
|
|
971
|
-
case 40: // Down arrow
|
|
972
|
-
this.selectNext(event.shiftKey);
|
|
973
|
-
event.stopPropagation();
|
|
974
|
-
event.preventDefault();
|
|
975
|
-
break;
|
|
976
|
-
default:
|
|
977
|
-
break;
|
|
978
1120
|
}
|
|
979
1121
|
// Detects printable characters typed by the user.
|
|
980
1122
|
// Not all browsers support .key, but it discharges us from reconstructing
|
|
981
1123
|
// characters from key codes.
|
|
982
|
-
if (
|
|
1124
|
+
if (event.key !== undefined &&
|
|
1125
|
+
event.key.length === 1 &&
|
|
1126
|
+
// Don't gobble up the space key on the check-all checkbox (which the
|
|
1127
|
+
// browser treats as a click event).
|
|
1128
|
+
!((event.key === ' ' || event.keyCode === 32) &&
|
|
1129
|
+
event.target.type === 'checkbox')) {
|
|
983
1130
|
if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
|
|
984
1131
|
return;
|
|
985
1132
|
}
|
|
@@ -1270,10 +1417,12 @@ export class DirListing extends Widget {
|
|
|
1270
1417
|
else {
|
|
1271
1418
|
this.selection[path] = true;
|
|
1272
1419
|
}
|
|
1420
|
+
this._focusItem(index);
|
|
1273
1421
|
// Handle multiple select.
|
|
1274
1422
|
}
|
|
1275
1423
|
else if (event.shiftKey) {
|
|
1276
|
-
this._handleMultiSelect(
|
|
1424
|
+
this._handleMultiSelect(index);
|
|
1425
|
+
this._focusItem(index);
|
|
1277
1426
|
// Handle a 'soft' selection
|
|
1278
1427
|
}
|
|
1279
1428
|
else if (path in this.selection && selected.length > 1) {
|
|
@@ -1282,68 +1431,121 @@ export class DirListing extends Widget {
|
|
|
1282
1431
|
}
|
|
1283
1432
|
else {
|
|
1284
1433
|
// Select only the given item.
|
|
1285
|
-
return this._selectItem(index, false);
|
|
1434
|
+
return this._selectItem(index, false, true);
|
|
1286
1435
|
}
|
|
1287
1436
|
this.update();
|
|
1288
1437
|
}
|
|
1289
1438
|
/**
|
|
1290
|
-
* (Re-)focus
|
|
1439
|
+
* (Re-)focus an item in the directory listing.
|
|
1291
1440
|
*
|
|
1292
|
-
*
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
if (
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
index = ArrayExt.findFirstIndex(this._sortedItems, value => value.path === selected[0]);
|
|
1303
|
-
}
|
|
1304
|
-
if (index === -1) {
|
|
1441
|
+
* @param index The index of the item node to focus
|
|
1442
|
+
*/
|
|
1443
|
+
_focusItem(index) {
|
|
1444
|
+
const items = this._items;
|
|
1445
|
+
if (items.length === 0) {
|
|
1446
|
+
// Focus the top node if the folder is empty and therefore there are no
|
|
1447
|
+
// items inside the folder to focus.
|
|
1448
|
+
this._focusIndex = 0;
|
|
1449
|
+
this.node.focus();
|
|
1305
1450
|
return;
|
|
1306
1451
|
}
|
|
1307
|
-
|
|
1308
|
-
const node =
|
|
1309
|
-
const
|
|
1310
|
-
if (
|
|
1311
|
-
text
|
|
1452
|
+
this._focusIndex = index;
|
|
1453
|
+
const node = items[index];
|
|
1454
|
+
const nameNode = this.renderer.getNameNode(node);
|
|
1455
|
+
if (nameNode) {
|
|
1456
|
+
// Make the filename text node focusable so that it receives keyboard
|
|
1457
|
+
// events; text node was specifically chosen to receive shortcuts because
|
|
1458
|
+
// it gets substituted with input element during file name edits which
|
|
1459
|
+
// conveniently deactivates irrelevant shortcuts.
|
|
1460
|
+
nameNode.tabIndex = 0;
|
|
1461
|
+
nameNode.focus();
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Are all of the items between two provided indices selected?
|
|
1466
|
+
*
|
|
1467
|
+
* The items at the indices are not considered.
|
|
1468
|
+
*
|
|
1469
|
+
* @param j Index of one item.
|
|
1470
|
+
* @param k Index of another item. Note: may be less or greater than first
|
|
1471
|
+
* index.
|
|
1472
|
+
* @returns True if and only if all items between the j and k are selected.
|
|
1473
|
+
* Returns undefined if j and k are the same.
|
|
1474
|
+
*/
|
|
1475
|
+
_allSelectedBetween(j, k) {
|
|
1476
|
+
if (j === k) {
|
|
1477
|
+
return;
|
|
1312
1478
|
}
|
|
1479
|
+
const [start, end] = j < k ? [j + 1, k] : [k + 1, j];
|
|
1480
|
+
return this._sortedItems
|
|
1481
|
+
.slice(start, end)
|
|
1482
|
+
.reduce((result, item) => result && this.selection[item.path], true);
|
|
1313
1483
|
}
|
|
1314
1484
|
/**
|
|
1315
1485
|
* Handle a multiple select on a file item node.
|
|
1316
1486
|
*/
|
|
1317
|
-
_handleMultiSelect(
|
|
1318
|
-
// Find the "nearest selected".
|
|
1487
|
+
_handleMultiSelect(index) {
|
|
1319
1488
|
const items = this._sortedItems;
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1489
|
+
const fromIndex = this._focusIndex;
|
|
1490
|
+
const target = items[index];
|
|
1491
|
+
let shouldAdd = true;
|
|
1492
|
+
if (index === fromIndex) {
|
|
1493
|
+
// This follows the convention in Ubuntu and Windows, which is to allow
|
|
1494
|
+
// the focussed item to gain but not lose selected status on shift-click.
|
|
1495
|
+
// (MacOS is irrelevant here because MacOS Finder has no notion of a
|
|
1496
|
+
// focused-but-not-selected state.)
|
|
1497
|
+
this.selection[target.path] = true;
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
// If the target and all items in-between are selected, then we assume that
|
|
1501
|
+
// the user is trying to shrink rather than grow the group of selected
|
|
1502
|
+
// items.
|
|
1503
|
+
if (this.selection[target.path]) {
|
|
1504
|
+
// However, there is a special case when the distance between the from-
|
|
1505
|
+
// and to- index is just one (for example, when the user is pressing the
|
|
1506
|
+
// shift key plus arrow-up/down). If and only if the situation looks like
|
|
1507
|
+
// the following when going down (or reverse when going up) ...
|
|
1508
|
+
//
|
|
1509
|
+
// - [ante-anchor / previous item] unselected (or boundary)
|
|
1510
|
+
// - [anchor / currently focussed item / item at from-index] selected
|
|
1511
|
+
// - [target / next item / item at to-index] selected
|
|
1512
|
+
//
|
|
1513
|
+
// ... then we shrink the selection / unselect the currently focussed
|
|
1514
|
+
// item.
|
|
1515
|
+
if (Math.abs(index - fromIndex) === 1) {
|
|
1516
|
+
const anchor = items[fromIndex];
|
|
1517
|
+
const anteAnchor = items[fromIndex + (index < fromIndex ? 1 : -1)];
|
|
1518
|
+
if (
|
|
1519
|
+
// Currently focussed item is selected
|
|
1520
|
+
this.selection[anchor.path] &&
|
|
1521
|
+
// Item on other side of focussed item (away from target) is either a
|
|
1522
|
+
// boundary or unselected
|
|
1523
|
+
(!anteAnchor || !this.selection[anteAnchor.path])) {
|
|
1524
|
+
delete this.selection[anchor.path];
|
|
1334
1525
|
}
|
|
1335
1526
|
}
|
|
1527
|
+
else if (this._allSelectedBetween(fromIndex, index)) {
|
|
1528
|
+
shouldAdd = false;
|
|
1529
|
+
}
|
|
1336
1530
|
}
|
|
1337
|
-
//
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1531
|
+
// Select (or unselect) the rows between chosen index (target) and the last
|
|
1532
|
+
// focussed.
|
|
1533
|
+
const step = fromIndex < index ? 1 : -1;
|
|
1534
|
+
for (let i = fromIndex; i !== index + step; i += step) {
|
|
1535
|
+
if (shouldAdd) {
|
|
1536
|
+
if (i === fromIndex) {
|
|
1537
|
+
// Do not change the selection state of the starting (fromIndex) item.
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1345
1540
|
this.selection[items[i].path] = true;
|
|
1346
1541
|
}
|
|
1542
|
+
else {
|
|
1543
|
+
if (i === index) {
|
|
1544
|
+
// Do not unselect the target item.
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
delete this.selection[items[i].path];
|
|
1548
|
+
}
|
|
1347
1549
|
}
|
|
1348
1550
|
}
|
|
1349
1551
|
/**
|
|
@@ -1366,57 +1568,85 @@ export class DirListing extends Widget {
|
|
|
1366
1568
|
/**
|
|
1367
1569
|
* Allow the user to rename item on a given row.
|
|
1368
1570
|
*/
|
|
1369
|
-
_doRename() {
|
|
1571
|
+
async _doRename() {
|
|
1370
1572
|
this._inRename = true;
|
|
1573
|
+
const selectedPaths = Object.keys(this.selection);
|
|
1574
|
+
// Bail out if nothing has been selected.
|
|
1575
|
+
if (selectedPaths.length === 0) {
|
|
1576
|
+
this._inRename = false;
|
|
1577
|
+
return Promise.resolve('');
|
|
1578
|
+
}
|
|
1579
|
+
// Figure out which selected path to use for the rename.
|
|
1371
1580
|
const items = this._sortedItems;
|
|
1372
|
-
|
|
1581
|
+
let { path } = items[this._focusIndex];
|
|
1582
|
+
if (!this.selection[path]) {
|
|
1583
|
+
// If the currently focused item is not selected, then choose the last
|
|
1584
|
+
// selected item.
|
|
1585
|
+
path = selectedPaths.slice(-1)[0];
|
|
1586
|
+
}
|
|
1587
|
+
// Get the corresponding model, nodes, and file name.
|
|
1373
1588
|
const index = ArrayExt.findFirstIndex(items, value => value.path === path);
|
|
1374
1589
|
const row = this._items[index];
|
|
1375
1590
|
const item = items[index];
|
|
1376
1591
|
const nameNode = this.renderer.getNameNode(row);
|
|
1377
1592
|
const original = item.name;
|
|
1593
|
+
// Seed the text input with current file name, and select and focus it.
|
|
1378
1594
|
this._editNode.value = original;
|
|
1379
|
-
this._selectItem(index, false);
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1595
|
+
this._selectItem(index, false, true);
|
|
1596
|
+
// Wait for user input
|
|
1597
|
+
const newName = await Private.userInputForRename(nameNode, this._editNode, original);
|
|
1598
|
+
// Check if the widget was disposed during the `await`.
|
|
1599
|
+
if (this.isDisposed) {
|
|
1600
|
+
this._inRename = false;
|
|
1601
|
+
throw new Error('File browser is disposed.');
|
|
1602
|
+
}
|
|
1603
|
+
let finalFilename = newName;
|
|
1604
|
+
if (!newName || newName === original) {
|
|
1605
|
+
finalFilename = original;
|
|
1606
|
+
}
|
|
1607
|
+
else if (!isValidFileName(newName)) {
|
|
1608
|
+
void showErrorMessage(this._trans.__('Rename Error'), Error(this._trans._p('showErrorMessage', '"%1" is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"', newName)));
|
|
1609
|
+
finalFilename = original;
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
// Attempt rename at the file system level.
|
|
1395
1613
|
const manager = this._manager;
|
|
1396
1614
|
const oldPath = PathExt.join(this._model.path, original);
|
|
1397
1615
|
const newPath = PathExt.join(this._model.path, newName);
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1616
|
+
try {
|
|
1617
|
+
await renameFile(manager, oldPath, newPath);
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1401
1620
|
if (error !== 'File not renamed') {
|
|
1402
1621
|
void showErrorMessage(this._trans._p('showErrorMessage', 'Rename Error'), error);
|
|
1403
1622
|
}
|
|
1623
|
+
finalFilename = original;
|
|
1624
|
+
}
|
|
1625
|
+
// Check if the widget was disposed during the `await`.
|
|
1626
|
+
if (this.isDisposed) {
|
|
1404
1627
|
this._inRename = false;
|
|
1405
|
-
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1628
|
+
throw new Error('File browser is disposed.');
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
// If nothing else has been selected, then select the renamed file. In
|
|
1632
|
+
// other words, don't select the renamed file if the user has clicked
|
|
1633
|
+
// away to some other file.
|
|
1634
|
+
if (!this.isDisposed &&
|
|
1635
|
+
Object.keys(this.selection).length === 1 &&
|
|
1636
|
+
// We haven't updated the instance yet to reflect the rename, so unless
|
|
1637
|
+
// the user or something else has updated the selection, the original file
|
|
1638
|
+
// path and not the new file path will be in `this.selection`.
|
|
1639
|
+
this.selection[item.path]) {
|
|
1640
|
+
try {
|
|
1641
|
+
await this.selectItemByName(finalFilename, true);
|
|
1642
|
+
}
|
|
1643
|
+
catch (_a) {
|
|
1644
|
+
// do nothing
|
|
1645
|
+
console.warn('After rename, failed to select file', finalFilename);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
this._inRename = false;
|
|
1649
|
+
return finalFilename;
|
|
1420
1650
|
}
|
|
1421
1651
|
/**
|
|
1422
1652
|
* Select a given item.
|
|
@@ -1429,8 +1659,8 @@ export class DirListing extends Widget {
|
|
|
1429
1659
|
}
|
|
1430
1660
|
const path = items[index].path;
|
|
1431
1661
|
this.selection[path] = true;
|
|
1432
|
-
if (
|
|
1433
|
-
this.
|
|
1662
|
+
if (focus) {
|
|
1663
|
+
this._focusItem(index);
|
|
1434
1664
|
}
|
|
1435
1665
|
this.update();
|
|
1436
1666
|
}
|
|
@@ -1463,6 +1693,11 @@ export class DirListing extends Widget {
|
|
|
1463
1693
|
this.clearSelectedItems();
|
|
1464
1694
|
// Update the sorted items.
|
|
1465
1695
|
this.sort(this.sortState);
|
|
1696
|
+
// Reset focus. But wait until the DOM has been updated (hence
|
|
1697
|
+
// `requestAnimationFrame`).
|
|
1698
|
+
requestAnimationFrame(() => {
|
|
1699
|
+
this._focusItem(0);
|
|
1700
|
+
});
|
|
1466
1701
|
}
|
|
1467
1702
|
/**
|
|
1468
1703
|
* Handle a `fileChanged` signal from the model.
|
|
@@ -1513,7 +1748,8 @@ export class DirListing extends Widget {
|
|
|
1513
1748
|
header.className = HEADER_CLASS;
|
|
1514
1749
|
node.appendChild(header);
|
|
1515
1750
|
node.appendChild(content);
|
|
1516
|
-
node.
|
|
1751
|
+
// Set to -1 to allow calling this.node.focus().
|
|
1752
|
+
node.tabIndex = -1;
|
|
1517
1753
|
return node;
|
|
1518
1754
|
}
|
|
1519
1755
|
/**
|
|
@@ -1679,11 +1915,6 @@ export class DirListing extends Widget {
|
|
|
1679
1915
|
node.appendChild(text);
|
|
1680
1916
|
node.appendChild(modified);
|
|
1681
1917
|
node.appendChild(fileSize);
|
|
1682
|
-
// Make the text note focusable so that it receives keyboard events;
|
|
1683
|
-
// text node was specifically chosen to receive shortcuts because
|
|
1684
|
-
// text element gets substituted with input area during file name edits
|
|
1685
|
-
// which conveniently deactivate irrelevant shortcuts.
|
|
1686
|
-
text.tabIndex = 0;
|
|
1687
1918
|
if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
|
|
1688
1919
|
modified.classList.add(MODIFIED_COLUMN_HIDDEN);
|
|
1689
1920
|
}
|
|
@@ -1716,11 +1947,6 @@ export class DirListing extends Widget {
|
|
|
1716
1947
|
// Wrap the checkbox in a label element in order to increase its hit area.
|
|
1717
1948
|
const labelWrapper = document.createElement('label');
|
|
1718
1949
|
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
1950
|
const checkbox = document.createElement('input');
|
|
1725
1951
|
checkbox.type = 'checkbox';
|
|
1726
1952
|
// Prevent the user from clicking (via mouse, keyboard, or touch) the
|
|
@@ -1729,6 +1955,15 @@ export class DirListing extends Widget {
|
|
|
1729
1955
|
checkbox.addEventListener('click', event => {
|
|
1730
1956
|
event.preventDefault();
|
|
1731
1957
|
});
|
|
1958
|
+
// The individual file checkboxes are visible on hover, but the header
|
|
1959
|
+
// check-all checkbox is always visible.
|
|
1960
|
+
if (options === null || options === void 0 ? void 0 : options.alwaysVisible) {
|
|
1961
|
+
labelWrapper.classList.add('jp-mod-visible');
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
// Disable tabbing to all other checkboxes.
|
|
1965
|
+
checkbox.tabIndex = -1;
|
|
1966
|
+
}
|
|
1732
1967
|
labelWrapper.appendChild(checkbox);
|
|
1733
1968
|
return labelWrapper;
|
|
1734
1969
|
}
|
|
@@ -1934,7 +2169,7 @@ var Private;
|
|
|
1934
2169
|
*
|
|
1935
2170
|
* @returns Boolean indicating whether the name changed.
|
|
1936
2171
|
*/
|
|
1937
|
-
function
|
|
2172
|
+
function userInputForRename(text, edit, original) {
|
|
1938
2173
|
const parent = text.parentElement;
|
|
1939
2174
|
parent.replaceChild(edit, text);
|
|
1940
2175
|
edit.focus();
|
|
@@ -1945,7 +2180,7 @@ var Private;
|
|
|
1945
2180
|
else {
|
|
1946
2181
|
edit.setSelectionRange(0, index);
|
|
1947
2182
|
}
|
|
1948
|
-
return new Promise(
|
|
2183
|
+
return new Promise(resolve => {
|
|
1949
2184
|
edit.onblur = () => {
|
|
1950
2185
|
parent.replaceChild(text, edit);
|
|
1951
2186
|
resolve(edit.value);
|
|
@@ -1962,20 +2197,10 @@ var Private;
|
|
|
1962
2197
|
event.preventDefault();
|
|
1963
2198
|
edit.value = original;
|
|
1964
2199
|
edit.blur();
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
if (edit.selectionStart !== edit.selectionEnd) {
|
|
1970
|
-
edit.selectionStart = edit.selectionEnd = 0;
|
|
1971
|
-
}
|
|
1972
|
-
break;
|
|
1973
|
-
case 40: // Down arrow
|
|
1974
|
-
event.stopPropagation();
|
|
1975
|
-
event.preventDefault();
|
|
1976
|
-
if (edit.selectionStart !== edit.selectionEnd) {
|
|
1977
|
-
edit.selectionStart = edit.selectionEnd = edit.value.length;
|
|
1978
|
-
}
|
|
2200
|
+
// Put focus back on the text node. That way the user can, for
|
|
2201
|
+
// example, press the keyboard shortcut to go back into edit mode,
|
|
2202
|
+
// and it will work.
|
|
2203
|
+
text.focus();
|
|
1979
2204
|
break;
|
|
1980
2205
|
default:
|
|
1981
2206
|
break;
|
|
@@ -1983,7 +2208,7 @@ var Private;
|
|
|
1983
2208
|
};
|
|
1984
2209
|
});
|
|
1985
2210
|
}
|
|
1986
|
-
Private.
|
|
2211
|
+
Private.userInputForRename = userInputForRename;
|
|
1987
2212
|
/**
|
|
1988
2213
|
* Sort a list of items by sort state as a new array.
|
|
1989
2214
|
*/
|