@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/src/listing.ts
CHANGED
|
@@ -437,6 +437,16 @@ export class DirListing extends Widget {
|
|
|
437
437
|
if (!this.isDisposed && result.button.accept) {
|
|
438
438
|
await this._delete(items.map(item => item.path));
|
|
439
439
|
}
|
|
440
|
+
|
|
441
|
+
// Re-focus
|
|
442
|
+
let focusIndex = this._focusIndex;
|
|
443
|
+
const lastIndexAfterDelete = this._sortedItems.length - items.length - 1;
|
|
444
|
+
if (focusIndex > lastIndexAfterDelete) {
|
|
445
|
+
// If the focus index after deleting items is out of bounds, set it to the
|
|
446
|
+
// last item.
|
|
447
|
+
focusIndex = Math.max(0, lastIndexAfterDelete);
|
|
448
|
+
}
|
|
449
|
+
this._focusItem(focusIndex);
|
|
440
450
|
}
|
|
441
451
|
|
|
442
452
|
/**
|
|
@@ -628,7 +638,7 @@ export class DirListing extends Widget {
|
|
|
628
638
|
* Select an item by name.
|
|
629
639
|
*
|
|
630
640
|
* @param name - The name of the item to select.
|
|
631
|
-
* @param focus - Whether to move focus the selected item.
|
|
641
|
+
* @param focus - Whether to move focus to the selected item.
|
|
632
642
|
*
|
|
633
643
|
* @returns A promise that resolves when the name is selected.
|
|
634
644
|
*/
|
|
@@ -799,16 +809,25 @@ export class DirListing extends Widget {
|
|
|
799
809
|
content.appendChild(node);
|
|
800
810
|
}
|
|
801
811
|
|
|
802
|
-
|
|
803
|
-
|
|
812
|
+
nodes.forEach((node, i) => {
|
|
813
|
+
// Remove extra classes from the nodes.
|
|
804
814
|
node.classList.remove(SELECTED_CLASS);
|
|
805
815
|
node.classList.remove(RUNNING_CLASS);
|
|
806
816
|
node.classList.remove(CUT_CLASS);
|
|
817
|
+
|
|
818
|
+
// Uncheck each file checkbox
|
|
807
819
|
const checkbox = renderer.getCheckboxNode(node);
|
|
808
820
|
if (checkbox) {
|
|
809
|
-
// Uncheck each file checkbox
|
|
810
821
|
checkbox.checked = false;
|
|
811
822
|
}
|
|
823
|
+
|
|
824
|
+
// Handle `tabIndex`
|
|
825
|
+
const nameNode = renderer.getNameNode(node);
|
|
826
|
+
if (nameNode) {
|
|
827
|
+
// Must check if the name node is there because it gets replaced by the
|
|
828
|
+
// edit node when editing the name of the file or directory.
|
|
829
|
+
nameNode.tabIndex = i === this._focusIndex ? 0 : -1;
|
|
830
|
+
}
|
|
812
831
|
});
|
|
813
832
|
|
|
814
833
|
// Put the check-all checkbox in the header into the correct state
|
|
@@ -835,7 +854,7 @@ export class DirListing extends Widget {
|
|
|
835
854
|
);
|
|
836
855
|
}
|
|
837
856
|
|
|
838
|
-
//
|
|
857
|
+
// Update item nodes based on widget state.
|
|
839
858
|
items.forEach((item, i) => {
|
|
840
859
|
const node = nodes[i];
|
|
841
860
|
const ft = this._manager.registry.getFileTypeForModel(item);
|
|
@@ -966,6 +985,12 @@ export class DirListing extends Widget {
|
|
|
966
985
|
}
|
|
967
986
|
}
|
|
968
987
|
return;
|
|
988
|
+
} else {
|
|
989
|
+
// Focus the selected file on click to ensure a couple of things:
|
|
990
|
+
// 1. If a user clicks on the item node, its name node will receive focus.
|
|
991
|
+
// 2. If a user clicks on blank space in the directory listing, the
|
|
992
|
+
// previously focussed item will be focussed.
|
|
993
|
+
this._focusItem(this._focusIndex);
|
|
969
994
|
}
|
|
970
995
|
}
|
|
971
996
|
|
|
@@ -1041,11 +1066,11 @@ export class DirListing extends Widget {
|
|
|
1041
1066
|
}
|
|
1042
1067
|
this._softSelection = '';
|
|
1043
1068
|
}
|
|
1044
|
-
// Re-focus
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1069
|
+
// Re-focus. This is needed because nodes corresponding to files selected in
|
|
1070
|
+
// mousedown handler will not retain the focus as mousedown event is always
|
|
1071
|
+
// followed by a blur/focus event.
|
|
1047
1072
|
if (event.button === 0) {
|
|
1048
|
-
this.
|
|
1073
|
+
this._focusItem(this._focusIndex);
|
|
1049
1074
|
}
|
|
1050
1075
|
|
|
1051
1076
|
// Remove the drag listeners if necessary.
|
|
@@ -1102,10 +1127,116 @@ export class DirListing extends Widget {
|
|
|
1102
1127
|
}
|
|
1103
1128
|
}
|
|
1104
1129
|
|
|
1130
|
+
/**
|
|
1131
|
+
* Calculate the next focus index, given the current focus index and a
|
|
1132
|
+
* direction, keeping within the bounds of the directory listing.
|
|
1133
|
+
*
|
|
1134
|
+
* @param index Current focus index
|
|
1135
|
+
* @param direction -1 (up) or 1 (down)
|
|
1136
|
+
* @returns The next focus index, which could be the same as the current focus
|
|
1137
|
+
* index if at the boundary.
|
|
1138
|
+
*/
|
|
1139
|
+
private _getNextFocusIndex(index: number, direction: number): number {
|
|
1140
|
+
const nextIndex = index + direction;
|
|
1141
|
+
if (nextIndex === -1 || nextIndex === this._items.length) {
|
|
1142
|
+
// keep focus index within bounds
|
|
1143
|
+
return index;
|
|
1144
|
+
} else {
|
|
1145
|
+
return nextIndex;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Handle the up or down arrow key.
|
|
1151
|
+
*
|
|
1152
|
+
* @param event The keyboard event
|
|
1153
|
+
* @param direction -1 (up) or 1 (down)
|
|
1154
|
+
*/
|
|
1155
|
+
private _handleArrowY(event: KeyboardEvent, direction: number) {
|
|
1156
|
+
// We only handle the `ctrl` and `shift` modifiers. If other modifiers are
|
|
1157
|
+
// present, then do nothing.
|
|
1158
|
+
if (event.altKey || event.metaKey) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// If folder is empty, there's nothing to do with the up/down key.
|
|
1163
|
+
if (!this._items.length) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Don't handle the arrow key press if it's not on directory item. This
|
|
1168
|
+
// avoids a confusing user experience that can result from when the user
|
|
1169
|
+
// moves the selection and focus index apart (via ctrl + up/down). The last
|
|
1170
|
+
// selected item remains highlighted but the last focussed item loses its
|
|
1171
|
+
// focus ring if it's not actively focussed. This forces the user to
|
|
1172
|
+
// visibly reveal the last focussed item before moving the focus.
|
|
1173
|
+
if (!(event.target as HTMLElement).classList.contains(ITEM_TEXT_CLASS)) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
event.stopPropagation();
|
|
1178
|
+
event.preventDefault();
|
|
1179
|
+
|
|
1180
|
+
const focusIndex = this._focusIndex;
|
|
1181
|
+
let nextFocusIndex = this._getNextFocusIndex(focusIndex, direction);
|
|
1182
|
+
|
|
1183
|
+
// The following if-block allows the first press of the down arrow to select
|
|
1184
|
+
// the first (rather than the second) file/directory in the list. This is
|
|
1185
|
+
// the situation when the page first loads or when a user changes directory.
|
|
1186
|
+
if (
|
|
1187
|
+
direction > 0 &&
|
|
1188
|
+
focusIndex === 0 &&
|
|
1189
|
+
!event.ctrlKey &&
|
|
1190
|
+
Object.keys(this.selection).length === 0
|
|
1191
|
+
) {
|
|
1192
|
+
nextFocusIndex = 0;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Shift key indicates multi-selection. Either the user is trying to grow
|
|
1196
|
+
// the selection, or shrink it.
|
|
1197
|
+
if (event.shiftKey) {
|
|
1198
|
+
this._handleMultiSelect(nextFocusIndex);
|
|
1199
|
+
} else if (!event.ctrlKey) {
|
|
1200
|
+
// If neither the shift nor ctrl keys were used with the up/down arrow,
|
|
1201
|
+
// then we treat it as a normal, unmodified key press and select the
|
|
1202
|
+
// next item.
|
|
1203
|
+
this._selectItem(
|
|
1204
|
+
nextFocusIndex,
|
|
1205
|
+
event.shiftKey,
|
|
1206
|
+
false /* focus = false because we call focus method directly following this */
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
this._focusItem(nextFocusIndex);
|
|
1211
|
+
this.update();
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* cd ..
|
|
1216
|
+
*
|
|
1217
|
+
* Go up one level in the directory tree.
|
|
1218
|
+
*/
|
|
1219
|
+
async goUp() {
|
|
1220
|
+
const model = this.model;
|
|
1221
|
+
if (model.path === model.rootPath) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
try {
|
|
1225
|
+
await model.cd('..');
|
|
1226
|
+
} catch (reason) {
|
|
1227
|
+
console.warn(`Failed to go to parent directory of ${model.path}`, reason);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1105
1231
|
/**
|
|
1106
1232
|
* Handle the `'keydown'` event for the widget.
|
|
1107
1233
|
*/
|
|
1108
1234
|
protected evtKeydown(event: KeyboardEvent): void {
|
|
1235
|
+
// Do not handle any keydown events here if in the middle of a file rename.
|
|
1236
|
+
if (this._inRename) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1109
1240
|
switch (event.keyCode) {
|
|
1110
1241
|
case 13: {
|
|
1111
1242
|
// Enter
|
|
@@ -1115,37 +1246,78 @@ export class DirListing extends Widget {
|
|
|
1115
1246
|
}
|
|
1116
1247
|
event.preventDefault();
|
|
1117
1248
|
event.stopPropagation();
|
|
1249
|
+
for (const item of this.selectedItems()) {
|
|
1250
|
+
this.handleOpen(item);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
case 38:
|
|
1255
|
+
// Up arrow
|
|
1256
|
+
this._handleArrowY(event, -1);
|
|
1257
|
+
return;
|
|
1258
|
+
case 40:
|
|
1259
|
+
// Down arrow
|
|
1260
|
+
this._handleArrowY(event, 1);
|
|
1261
|
+
return;
|
|
1262
|
+
case 32: {
|
|
1263
|
+
// Space
|
|
1264
|
+
if (event.ctrlKey) {
|
|
1265
|
+
// Follow the Windows and Ubuntu convention: you must press `ctrl` +
|
|
1266
|
+
// `space` in order to toggle whether an item is selected.
|
|
1267
|
+
|
|
1268
|
+
// However, do not handle if any other modifiers were pressed.
|
|
1269
|
+
if (event.metaKey || event.shiftKey || event.altKey) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Make sure the ctrl+space key stroke was on a valid, focussed target.
|
|
1274
|
+
const node = this._items[this._focusIndex];
|
|
1275
|
+
if (
|
|
1276
|
+
!(
|
|
1277
|
+
// Event must have occurred within a node whose item can be toggled.
|
|
1278
|
+
(
|
|
1279
|
+
node.contains(event.target as HTMLElement) &&
|
|
1280
|
+
// That node must also contain the currently focussed element.
|
|
1281
|
+
node.contains(document.activeElement)
|
|
1282
|
+
)
|
|
1283
|
+
)
|
|
1284
|
+
) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
event.stopPropagation();
|
|
1289
|
+
// Prevent default, otherwise the container will scroll.
|
|
1290
|
+
event.preventDefault();
|
|
1118
1291
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1292
|
+
// Toggle item selected
|
|
1293
|
+
const { path } = this._sortedItems[this._focusIndex];
|
|
1294
|
+
if (this.selection[path]) {
|
|
1295
|
+
delete this.selection[path];
|
|
1296
|
+
} else {
|
|
1297
|
+
this.selection[path] = true;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
this.update();
|
|
1301
|
+
// Key was handled, so return.
|
|
1124
1302
|
return;
|
|
1125
1303
|
}
|
|
1126
|
-
|
|
1127
|
-
const item = this._sortedItems[i];
|
|
1128
|
-
this.handleOpen(item);
|
|
1129
1304
|
break;
|
|
1130
1305
|
}
|
|
1131
|
-
case 38: // Up arrow
|
|
1132
|
-
this.selectPrevious(event.shiftKey);
|
|
1133
|
-
event.stopPropagation();
|
|
1134
|
-
event.preventDefault();
|
|
1135
|
-
break;
|
|
1136
|
-
case 40: // Down arrow
|
|
1137
|
-
this.selectNext(event.shiftKey);
|
|
1138
|
-
event.stopPropagation();
|
|
1139
|
-
event.preventDefault();
|
|
1140
|
-
break;
|
|
1141
|
-
default:
|
|
1142
|
-
break;
|
|
1143
1306
|
}
|
|
1144
1307
|
|
|
1145
1308
|
// Detects printable characters typed by the user.
|
|
1146
1309
|
// Not all browsers support .key, but it discharges us from reconstructing
|
|
1147
1310
|
// characters from key codes.
|
|
1148
|
-
if (
|
|
1311
|
+
if (
|
|
1312
|
+
event.key !== undefined &&
|
|
1313
|
+
event.key.length === 1 &&
|
|
1314
|
+
// Don't gobble up the space key on the check-all checkbox (which the
|
|
1315
|
+
// browser treats as a click event).
|
|
1316
|
+
!(
|
|
1317
|
+
(event.key === ' ' || event.keyCode === 32) &&
|
|
1318
|
+
(event.target as HTMLInputElement).type === 'checkbox'
|
|
1319
|
+
)
|
|
1320
|
+
) {
|
|
1149
1321
|
if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
|
|
1150
1322
|
return;
|
|
1151
1323
|
}
|
|
@@ -1486,11 +1658,11 @@ export class DirListing extends Widget {
|
|
|
1486
1658
|
} else {
|
|
1487
1659
|
this.selection[path] = true;
|
|
1488
1660
|
}
|
|
1489
|
-
|
|
1661
|
+
this._focusItem(index);
|
|
1490
1662
|
// Handle multiple select.
|
|
1491
1663
|
} else if (event.shiftKey) {
|
|
1492
|
-
this._handleMultiSelect(
|
|
1493
|
-
|
|
1664
|
+
this._handleMultiSelect(index);
|
|
1665
|
+
this._focusItem(index);
|
|
1494
1666
|
// Handle a 'soft' selection
|
|
1495
1667
|
} else if (path in this.selection && selected.length > 1) {
|
|
1496
1668
|
this._softSelection = path;
|
|
@@ -1498,75 +1670,125 @@ export class DirListing extends Widget {
|
|
|
1498
1670
|
// Default to selecting the only the item.
|
|
1499
1671
|
} else {
|
|
1500
1672
|
// Select only the given item.
|
|
1501
|
-
return this._selectItem(index, false);
|
|
1673
|
+
return this._selectItem(index, false, true);
|
|
1502
1674
|
}
|
|
1503
1675
|
this.update();
|
|
1504
1676
|
}
|
|
1505
1677
|
|
|
1506
1678
|
/**
|
|
1507
|
-
* (Re-)focus
|
|
1679
|
+
* (Re-)focus an item in the directory listing.
|
|
1508
1680
|
*
|
|
1509
|
-
*
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
if (
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
}
|
|
1519
|
-
index = ArrayExt.findFirstIndex(
|
|
1520
|
-
this._sortedItems,
|
|
1521
|
-
value => value.path === selected[0]
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
if (index === -1) {
|
|
1681
|
+
* @param index The index of the item node to focus
|
|
1682
|
+
*/
|
|
1683
|
+
private _focusItem(index: number): void {
|
|
1684
|
+
const items = this._items;
|
|
1685
|
+
if (items.length === 0) {
|
|
1686
|
+
// Focus the top node if the folder is empty and therefore there are no
|
|
1687
|
+
// items inside the folder to focus.
|
|
1688
|
+
this._focusIndex = 0;
|
|
1689
|
+
this.node.focus();
|
|
1525
1690
|
return;
|
|
1526
1691
|
}
|
|
1527
|
-
|
|
1528
|
-
const node =
|
|
1529
|
-
const
|
|
1530
|
-
if (
|
|
1531
|
-
text
|
|
1692
|
+
this._focusIndex = index;
|
|
1693
|
+
const node = items[index];
|
|
1694
|
+
const nameNode = this.renderer.getNameNode(node);
|
|
1695
|
+
if (nameNode) {
|
|
1696
|
+
// Make the filename text node focusable so that it receives keyboard
|
|
1697
|
+
// events; text node was specifically chosen to receive shortcuts because
|
|
1698
|
+
// it gets substituted with input element during file name edits which
|
|
1699
|
+
// conveniently deactivates irrelevant shortcuts.
|
|
1700
|
+
nameNode.tabIndex = 0;
|
|
1701
|
+
nameNode.focus();
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Are all of the items between two provided indices selected?
|
|
1707
|
+
*
|
|
1708
|
+
* The items at the indices are not considered.
|
|
1709
|
+
*
|
|
1710
|
+
* @param j Index of one item.
|
|
1711
|
+
* @param k Index of another item. Note: may be less or greater than first
|
|
1712
|
+
* index.
|
|
1713
|
+
* @returns True if and only if all items between the j and k are selected.
|
|
1714
|
+
* Returns undefined if j and k are the same.
|
|
1715
|
+
*/
|
|
1716
|
+
private _allSelectedBetween(j: number, k: number): boolean | void {
|
|
1717
|
+
if (j === k) {
|
|
1718
|
+
return;
|
|
1532
1719
|
}
|
|
1720
|
+
const [start, end] = j < k ? [j + 1, k] : [k + 1, j];
|
|
1721
|
+
return this._sortedItems
|
|
1722
|
+
.slice(start, end)
|
|
1723
|
+
.reduce((result, item) => result && this.selection[item.path], true);
|
|
1533
1724
|
}
|
|
1534
1725
|
|
|
1535
1726
|
/**
|
|
1536
1727
|
* Handle a multiple select on a file item node.
|
|
1537
1728
|
*/
|
|
1538
|
-
private _handleMultiSelect(
|
|
1539
|
-
// Find the "nearest selected".
|
|
1729
|
+
private _handleMultiSelect(index: number): void {
|
|
1540
1730
|
const items = this._sortedItems;
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
nearestIndex = i;
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1731
|
+
const fromIndex = this._focusIndex;
|
|
1732
|
+
const target = items[index];
|
|
1733
|
+
let shouldAdd = true;
|
|
1734
|
+
|
|
1735
|
+
if (index === fromIndex) {
|
|
1736
|
+
// This follows the convention in Ubuntu and Windows, which is to allow
|
|
1737
|
+
// the focussed item to gain but not lose selected status on shift-click.
|
|
1738
|
+
// (MacOS is irrelevant here because MacOS Finder has no notion of a
|
|
1739
|
+
// focused-but-not-selected state.)
|
|
1740
|
+
this.selection[target.path] = true;
|
|
1741
|
+
return;
|
|
1556
1742
|
}
|
|
1557
1743
|
|
|
1558
|
-
//
|
|
1559
|
-
|
|
1560
|
-
|
|
1744
|
+
// If the target and all items in-between are selected, then we assume that
|
|
1745
|
+
// the user is trying to shrink rather than grow the group of selected
|
|
1746
|
+
// items.
|
|
1747
|
+
if (this.selection[target.path]) {
|
|
1748
|
+
// However, there is a special case when the distance between the from-
|
|
1749
|
+
// and to- index is just one (for example, when the user is pressing the
|
|
1750
|
+
// shift key plus arrow-up/down). If and only if the situation looks like
|
|
1751
|
+
// the following when going down (or reverse when going up) ...
|
|
1752
|
+
//
|
|
1753
|
+
// - [ante-anchor / previous item] unselected (or boundary)
|
|
1754
|
+
// - [anchor / currently focussed item / item at from-index] selected
|
|
1755
|
+
// - [target / next item / item at to-index] selected
|
|
1756
|
+
//
|
|
1757
|
+
// ... then we shrink the selection / unselect the currently focussed
|
|
1758
|
+
// item.
|
|
1759
|
+
if (Math.abs(index - fromIndex) === 1) {
|
|
1760
|
+
const anchor = items[fromIndex];
|
|
1761
|
+
const anteAnchor = items[fromIndex + (index < fromIndex ? 1 : -1)];
|
|
1762
|
+
if (
|
|
1763
|
+
// Currently focussed item is selected
|
|
1764
|
+
this.selection[anchor.path] &&
|
|
1765
|
+
// Item on other side of focussed item (away from target) is either a
|
|
1766
|
+
// boundary or unselected
|
|
1767
|
+
(!anteAnchor || !this.selection[anteAnchor.path])
|
|
1768
|
+
) {
|
|
1769
|
+
delete this.selection[anchor.path];
|
|
1770
|
+
}
|
|
1771
|
+
} else if (this._allSelectedBetween(fromIndex, index)) {
|
|
1772
|
+
shouldAdd = false;
|
|
1773
|
+
}
|
|
1561
1774
|
}
|
|
1562
1775
|
|
|
1563
|
-
// Select the rows between
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1776
|
+
// Select (or unselect) the rows between chosen index (target) and the last
|
|
1777
|
+
// focussed.
|
|
1778
|
+
const step = fromIndex < index ? 1 : -1;
|
|
1779
|
+
for (let i = fromIndex; i !== index + step; i += step) {
|
|
1780
|
+
if (shouldAdd) {
|
|
1781
|
+
if (i === fromIndex) {
|
|
1782
|
+
// Do not change the selection state of the starting (fromIndex) item.
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1569
1785
|
this.selection[items[i].path] = true;
|
|
1786
|
+
} else {
|
|
1787
|
+
if (i === index) {
|
|
1788
|
+
// Do not unselect the target item.
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
delete this.selection[items[i].path];
|
|
1570
1792
|
}
|
|
1571
1793
|
}
|
|
1572
1794
|
}
|
|
@@ -1600,74 +1822,112 @@ export class DirListing extends Widget {
|
|
|
1600
1822
|
/**
|
|
1601
1823
|
* Allow the user to rename item on a given row.
|
|
1602
1824
|
*/
|
|
1603
|
-
private _doRename(): Promise<string> {
|
|
1825
|
+
private async _doRename(): Promise<string> {
|
|
1604
1826
|
this._inRename = true;
|
|
1827
|
+
|
|
1828
|
+
const selectedPaths = Object.keys(this.selection);
|
|
1829
|
+
|
|
1830
|
+
// Bail out if nothing has been selected.
|
|
1831
|
+
if (selectedPaths.length === 0) {
|
|
1832
|
+
this._inRename = false;
|
|
1833
|
+
return Promise.resolve('');
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Figure out which selected path to use for the rename.
|
|
1605
1837
|
const items = this._sortedItems;
|
|
1606
|
-
|
|
1838
|
+
let { path } = items[this._focusIndex];
|
|
1839
|
+
if (!this.selection[path]) {
|
|
1840
|
+
// If the currently focused item is not selected, then choose the last
|
|
1841
|
+
// selected item.
|
|
1842
|
+
path = selectedPaths.slice(-1)[0];
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Get the corresponding model, nodes, and file name.
|
|
1607
1846
|
const index = ArrayExt.findFirstIndex(items, value => value.path === path);
|
|
1608
1847
|
const row = this._items[index];
|
|
1609
1848
|
const item = items[index];
|
|
1610
1849
|
const nameNode = this.renderer.getNameNode(row);
|
|
1611
1850
|
const original = item.name;
|
|
1851
|
+
|
|
1852
|
+
// Seed the text input with current file name, and select and focus it.
|
|
1612
1853
|
this._editNode.value = original;
|
|
1613
|
-
this._selectItem(index, false);
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1854
|
+
this._selectItem(index, false, true);
|
|
1855
|
+
|
|
1856
|
+
// Wait for user input
|
|
1857
|
+
const newName = await Private.userInputForRename(
|
|
1858
|
+
nameNode,
|
|
1859
|
+
this._editNode,
|
|
1860
|
+
original
|
|
1861
|
+
);
|
|
1862
|
+
|
|
1863
|
+
// Check if the widget was disposed during the `await`.
|
|
1864
|
+
if (this.isDisposed) {
|
|
1865
|
+
this._inRename = false;
|
|
1866
|
+
throw new Error('File browser is disposed.');
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
let finalFilename = newName;
|
|
1870
|
+
|
|
1871
|
+
if (!newName || newName === original) {
|
|
1872
|
+
finalFilename = original;
|
|
1873
|
+
} else if (!isValidFileName(newName)) {
|
|
1874
|
+
void showErrorMessage(
|
|
1875
|
+
this._trans.__('Rename Error'),
|
|
1876
|
+
Error(
|
|
1877
|
+
this._trans._p(
|
|
1878
|
+
'showErrorMessage',
|
|
1879
|
+
'"%1" is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"',
|
|
1880
|
+
newName
|
|
1881
|
+
)
|
|
1882
|
+
)
|
|
1883
|
+
);
|
|
1884
|
+
finalFilename = original;
|
|
1885
|
+
} else {
|
|
1886
|
+
// Attempt rename at the file system level.
|
|
1887
|
+
|
|
1888
|
+
const manager = this._manager;
|
|
1889
|
+
const oldPath = PathExt.join(this._model.path, original);
|
|
1890
|
+
const newPath = PathExt.join(this._model.path, newName);
|
|
1891
|
+
try {
|
|
1892
|
+
await renameFile(manager, oldPath, newPath);
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
if (error !== 'File not renamed') {
|
|
1623
1895
|
void showErrorMessage(
|
|
1624
|
-
this._trans.
|
|
1625
|
-
|
|
1626
|
-
this._trans._p(
|
|
1627
|
-
'showErrorMessage',
|
|
1628
|
-
'"%1" is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"',
|
|
1629
|
-
newName
|
|
1630
|
-
)
|
|
1631
|
-
)
|
|
1896
|
+
this._trans._p('showErrorMessage', 'Rename Error'),
|
|
1897
|
+
error
|
|
1632
1898
|
);
|
|
1633
|
-
this._inRename = false;
|
|
1634
|
-
return original;
|
|
1635
1899
|
}
|
|
1900
|
+
finalFilename = original;
|
|
1901
|
+
}
|
|
1636
1902
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1903
|
+
// Check if the widget was disposed during the `await`.
|
|
1904
|
+
if (this.isDisposed) {
|
|
1905
|
+
this._inRename = false;
|
|
1906
|
+
throw new Error('File browser is disposed.');
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1641
1909
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
if (this.isDisposed) {
|
|
1659
|
-
this._inRename = false;
|
|
1660
|
-
throw new Error('File browser is disposed.');
|
|
1661
|
-
}
|
|
1662
|
-
if (this._inRename) {
|
|
1663
|
-
// No need to catch because `newName` will always exit.
|
|
1664
|
-
void this.selectItemByName(newName);
|
|
1665
|
-
}
|
|
1666
|
-
this._inRename = false;
|
|
1667
|
-
return newName;
|
|
1668
|
-
});
|
|
1910
|
+
// If nothing else has been selected, then select the renamed file. In
|
|
1911
|
+
// other words, don't select the renamed file if the user has clicked
|
|
1912
|
+
// away to some other file.
|
|
1913
|
+
if (
|
|
1914
|
+
!this.isDisposed &&
|
|
1915
|
+
Object.keys(this.selection).length === 1 &&
|
|
1916
|
+
// We haven't updated the instance yet to reflect the rename, so unless
|
|
1917
|
+
// the user or something else has updated the selection, the original file
|
|
1918
|
+
// path and not the new file path will be in `this.selection`.
|
|
1919
|
+
this.selection[item.path]
|
|
1920
|
+
) {
|
|
1921
|
+
try {
|
|
1922
|
+
await this.selectItemByName(finalFilename, true);
|
|
1923
|
+
} catch {
|
|
1924
|
+
// do nothing
|
|
1925
|
+
console.warn('After rename, failed to select file', finalFilename);
|
|
1669
1926
|
}
|
|
1670
|
-
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
this._inRename = false;
|
|
1930
|
+
return finalFilename;
|
|
1671
1931
|
}
|
|
1672
1932
|
|
|
1673
1933
|
/**
|
|
@@ -1686,8 +1946,8 @@ export class DirListing extends Widget {
|
|
|
1686
1946
|
const path = items[index].path;
|
|
1687
1947
|
this.selection[path] = true;
|
|
1688
1948
|
|
|
1689
|
-
if (
|
|
1690
|
-
this.
|
|
1949
|
+
if (focus) {
|
|
1950
|
+
this._focusItem(index);
|
|
1691
1951
|
}
|
|
1692
1952
|
this.update();
|
|
1693
1953
|
}
|
|
@@ -1721,6 +1981,11 @@ export class DirListing extends Widget {
|
|
|
1721
1981
|
this.clearSelectedItems();
|
|
1722
1982
|
// Update the sorted items.
|
|
1723
1983
|
this.sort(this.sortState);
|
|
1984
|
+
// Reset focus. But wait until the DOM has been updated (hence
|
|
1985
|
+
// `requestAnimationFrame`).
|
|
1986
|
+
requestAnimationFrame(() => {
|
|
1987
|
+
this._focusItem(0);
|
|
1988
|
+
});
|
|
1724
1989
|
}
|
|
1725
1990
|
|
|
1726
1991
|
/**
|
|
@@ -1789,6 +2054,8 @@ export class DirListing extends Widget {
|
|
|
1789
2054
|
private _inRename = false;
|
|
1790
2055
|
private _isDirty = false;
|
|
1791
2056
|
private _hiddenColumns = new Set<DirListing.ToggleableColumn>();
|
|
2057
|
+
// _focusIndex should never be set outside the range [0, this._items.length - 1]
|
|
2058
|
+
private _focusIndex = 0;
|
|
1792
2059
|
}
|
|
1793
2060
|
|
|
1794
2061
|
/**
|
|
@@ -1971,7 +2238,8 @@ export namespace DirListing {
|
|
|
1971
2238
|
header.className = HEADER_CLASS;
|
|
1972
2239
|
node.appendChild(header);
|
|
1973
2240
|
node.appendChild(content);
|
|
1974
|
-
node.
|
|
2241
|
+
// Set to -1 to allow calling this.node.focus().
|
|
2242
|
+
node.tabIndex = -1;
|
|
1975
2243
|
return node;
|
|
1976
2244
|
}
|
|
1977
2245
|
|
|
@@ -2154,12 +2422,6 @@ export namespace DirListing {
|
|
|
2154
2422
|
node.appendChild(modified);
|
|
2155
2423
|
node.appendChild(fileSize);
|
|
2156
2424
|
|
|
2157
|
-
// Make the text note focusable so that it receives keyboard events;
|
|
2158
|
-
// text node was specifically chosen to receive shortcuts because
|
|
2159
|
-
// text element gets substituted with input area during file name edits
|
|
2160
|
-
// which conveniently deactivate irrelevant shortcuts.
|
|
2161
|
-
text.tabIndex = 0;
|
|
2162
|
-
|
|
2163
2425
|
if (hiddenColumns?.has('last_modified')) {
|
|
2164
2426
|
modified.classList.add(MODIFIED_COLUMN_HIDDEN);
|
|
2165
2427
|
} else {
|
|
@@ -2195,11 +2457,7 @@ export namespace DirListing {
|
|
|
2195
2457
|
// Wrap the checkbox in a label element in order to increase its hit area.
|
|
2196
2458
|
const labelWrapper = document.createElement('label');
|
|
2197
2459
|
labelWrapper.classList.add(CHECKBOX_WRAPPER_CLASS);
|
|
2198
|
-
|
|
2199
|
-
// check-all checkbox is always visible.
|
|
2200
|
-
if (options?.alwaysVisible) {
|
|
2201
|
-
labelWrapper.classList.add('jp-mod-visible');
|
|
2202
|
-
}
|
|
2460
|
+
|
|
2203
2461
|
const checkbox = document.createElement('input');
|
|
2204
2462
|
checkbox.type = 'checkbox';
|
|
2205
2463
|
// Prevent the user from clicking (via mouse, keyboard, or touch) the
|
|
@@ -2208,6 +2466,16 @@ export namespace DirListing {
|
|
|
2208
2466
|
checkbox.addEventListener('click', event => {
|
|
2209
2467
|
event.preventDefault();
|
|
2210
2468
|
});
|
|
2469
|
+
|
|
2470
|
+
// The individual file checkboxes are visible on hover, but the header
|
|
2471
|
+
// check-all checkbox is always visible.
|
|
2472
|
+
if (options?.alwaysVisible) {
|
|
2473
|
+
labelWrapper.classList.add('jp-mod-visible');
|
|
2474
|
+
} else {
|
|
2475
|
+
// Disable tabbing to all other checkboxes.
|
|
2476
|
+
checkbox.tabIndex = -1;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2211
2479
|
labelWrapper.appendChild(checkbox);
|
|
2212
2480
|
return labelWrapper;
|
|
2213
2481
|
}
|
|
@@ -2453,7 +2721,7 @@ namespace Private {
|
|
|
2453
2721
|
*
|
|
2454
2722
|
* @returns Boolean indicating whether the name changed.
|
|
2455
2723
|
*/
|
|
2456
|
-
export function
|
|
2724
|
+
export function userInputForRename(
|
|
2457
2725
|
text: HTMLElement,
|
|
2458
2726
|
edit: HTMLInputElement,
|
|
2459
2727
|
original: string
|
|
@@ -2468,7 +2736,7 @@ namespace Private {
|
|
|
2468
2736
|
edit.setSelectionRange(0, index);
|
|
2469
2737
|
}
|
|
2470
2738
|
|
|
2471
|
-
return new Promise<string>(
|
|
2739
|
+
return new Promise<string>(resolve => {
|
|
2472
2740
|
edit.onblur = () => {
|
|
2473
2741
|
parent.replaceChild(text, edit);
|
|
2474
2742
|
resolve(edit.value);
|
|
@@ -2485,20 +2753,10 @@ namespace Private {
|
|
|
2485
2753
|
event.preventDefault();
|
|
2486
2754
|
edit.value = original;
|
|
2487
2755
|
edit.blur();
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
if (edit.selectionStart !== edit.selectionEnd) {
|
|
2493
|
-
edit.selectionStart = edit.selectionEnd = 0;
|
|
2494
|
-
}
|
|
2495
|
-
break;
|
|
2496
|
-
case 40: // Down arrow
|
|
2497
|
-
event.stopPropagation();
|
|
2498
|
-
event.preventDefault();
|
|
2499
|
-
if (edit.selectionStart !== edit.selectionEnd) {
|
|
2500
|
-
edit.selectionStart = edit.selectionEnd = edit.value.length;
|
|
2501
|
-
}
|
|
2756
|
+
// Put focus back on the text node. That way the user can, for
|
|
2757
|
+
// example, press the keyboard shortcut to go back into edit mode,
|
|
2758
|
+
// and it will work.
|
|
2759
|
+
text.focus();
|
|
2502
2760
|
break;
|
|
2503
2761
|
default:
|
|
2504
2762
|
break;
|