@jupyterlab/filebrowser 4.3.0-alpha.2 → 4.3.0-beta.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 +12 -0
- package/lib/browser.js +20 -1
- package/lib/browser.js.map +1 -1
- package/lib/listing.d.ts +129 -15
- package/lib/listing.js +574 -208
- package/lib/listing.js.map +1 -1
- package/lib/model.d.ts +2 -1
- package/lib/model.js +5 -4
- package/lib/model.js.map +1 -1
- package/package.json +11 -11
- package/src/browser.ts +28 -1
- package/src/listing.ts +791 -220
- package/src/model.ts +7 -4
- package/style/base.css +68 -39
package/src/listing.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '@jupyterlab/docmanager';
|
|
16
16
|
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
17
17
|
import { Contents } from '@jupyterlab/services';
|
|
18
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
18
19
|
import {
|
|
19
20
|
ITranslator,
|
|
20
21
|
nullTranslator,
|
|
@@ -27,8 +28,13 @@ import {
|
|
|
27
28
|
LabIcon
|
|
28
29
|
} from '@jupyterlab/ui-components';
|
|
29
30
|
import { ArrayExt, filter, StringExt } from '@lumino/algorithm';
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
MimeData,
|
|
33
|
+
PromiseDelegate,
|
|
34
|
+
ReadonlyJSONObject
|
|
35
|
+
} from '@lumino/coreutils';
|
|
31
36
|
import { ElementExt } from '@lumino/domutils';
|
|
37
|
+
import { DisposableDelegate, IDisposable } from '@lumino/disposable';
|
|
32
38
|
import { Drag } from '@lumino/dragdrop';
|
|
33
39
|
import { Message, MessageLoop } from '@lumino/messaging';
|
|
34
40
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
@@ -76,6 +82,11 @@ const ITEM_CLASS = 'jp-DirListing-item';
|
|
|
76
82
|
*/
|
|
77
83
|
const ITEM_TEXT_CLASS = 'jp-DirListing-itemText';
|
|
78
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The class name added to the listing item text cell.
|
|
87
|
+
*/
|
|
88
|
+
const ITEM_NAME_COLUMN_CLASS = 'jp-DirListing-itemName';
|
|
89
|
+
|
|
79
90
|
/**
|
|
80
91
|
* The class name added to the listing item icon cell.
|
|
81
92
|
*/
|
|
@@ -117,21 +128,6 @@ const MODIFIED_ID_CLASS = 'jp-id-modified';
|
|
|
117
128
|
*/
|
|
118
129
|
const FILE_SIZE_ID_CLASS = 'jp-id-filesize';
|
|
119
130
|
|
|
120
|
-
/**
|
|
121
|
-
* The class name added to the narrow column header cell.
|
|
122
|
-
*/
|
|
123
|
-
const NARROW_ID_CLASS = 'jp-id-narrow';
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* The class name added to the modified column header cell and modified item cell when hidden.
|
|
127
|
-
*/
|
|
128
|
-
const MODIFIED_COLUMN_HIDDEN = 'jp-LastModified-hidden';
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The class name added to the size column header cell and size item cell when hidden.
|
|
132
|
-
*/
|
|
133
|
-
const FILE_SIZE_COLUMN_HIDDEN = 'jp-FileSize-hidden';
|
|
134
|
-
|
|
135
131
|
/**
|
|
136
132
|
* The mime type for a contents drag object.
|
|
137
133
|
*/
|
|
@@ -157,6 +153,11 @@ const SELECTED_CLASS = 'jp-mod-selected';
|
|
|
157
153
|
*/
|
|
158
154
|
const DRAG_ICON_CLASS = 'jp-DragIcon';
|
|
159
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The class name added to column resize handle.
|
|
158
|
+
*/
|
|
159
|
+
const RESIZE_HANDLE_CLASS = 'jp-DirListing-resizeHandle';
|
|
160
|
+
|
|
160
161
|
/**
|
|
161
162
|
* The class name added to the widget when there are items on the clipboard.
|
|
162
163
|
*/
|
|
@@ -177,6 +178,11 @@ const MULTI_SELECTED_CLASS = 'jp-mod-multiSelected';
|
|
|
177
178
|
*/
|
|
178
179
|
const RUNNING_CLASS = 'jp-mod-running';
|
|
179
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The class name added to indicate the active element.
|
|
183
|
+
*/
|
|
184
|
+
const ACTIVE_CLASS = 'jp-mod-active';
|
|
185
|
+
|
|
180
186
|
/**
|
|
181
187
|
* The class name added for a descending sort.
|
|
182
188
|
*/
|
|
@@ -226,6 +232,7 @@ export class DirListing extends Widget {
|
|
|
226
232
|
this._editNode.className = EDITOR_CLASS;
|
|
227
233
|
this._manager = this._model.manager;
|
|
228
234
|
this._renderer = options.renderer || DirListing.defaultRenderer;
|
|
235
|
+
this._state = options.state || null;
|
|
229
236
|
|
|
230
237
|
// Get the width of the "modified" column
|
|
231
238
|
this._updateModifiedSize(this.node);
|
|
@@ -236,7 +243,8 @@ export class DirListing extends Widget {
|
|
|
236
243
|
this._renderer.populateHeaderNode(
|
|
237
244
|
headerNode,
|
|
238
245
|
this.translator,
|
|
239
|
-
this._hiddenColumns
|
|
246
|
+
this._hiddenColumns,
|
|
247
|
+
this._columnSizes
|
|
240
248
|
);
|
|
241
249
|
this._manager.activateRequested.connect(this._onActivateRequested, this);
|
|
242
250
|
}
|
|
@@ -493,6 +501,45 @@ export class DirListing extends Widget {
|
|
|
493
501
|
);
|
|
494
502
|
}
|
|
495
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Restore the state of the file browser listing.
|
|
506
|
+
*
|
|
507
|
+
* @param id - The unique ID that is used to construct a state database key.
|
|
508
|
+
*
|
|
509
|
+
*/
|
|
510
|
+
async restore(id: string): Promise<void> {
|
|
511
|
+
const key = `file-browser-${id}:columns`;
|
|
512
|
+
const state = this._state;
|
|
513
|
+
this._stateColumnsKey = key;
|
|
514
|
+
|
|
515
|
+
if (!state) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const columns = await state.fetch(key);
|
|
521
|
+
|
|
522
|
+
if (!columns) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const sizes = (columns as ReadonlyJSONObject)['sizes'] as
|
|
527
|
+
| Record<DirListing.IColumn['id'], number | null>
|
|
528
|
+
| undefined;
|
|
529
|
+
|
|
530
|
+
if (!sizes) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const [key, size] of Object.entries(sizes)) {
|
|
534
|
+
this._columnSizes[key as DirListing.IColumn['id']] = size;
|
|
535
|
+
}
|
|
536
|
+
this._updateColumnSizes();
|
|
537
|
+
} catch (error) {
|
|
538
|
+
await state.remove(key);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
private _stateColumnsKey: string;
|
|
542
|
+
|
|
496
543
|
/**
|
|
497
544
|
* Shut down kernels on the applicable currently selected items.
|
|
498
545
|
*
|
|
@@ -756,6 +803,7 @@ export class DirListing extends Widget {
|
|
|
756
803
|
protected onAfterAttach(msg: Message): void {
|
|
757
804
|
super.onAfterAttach(msg);
|
|
758
805
|
const node = this.node;
|
|
806
|
+
this._width = this.node.getBoundingClientRect().width;
|
|
759
807
|
const content = DOMUtils.findElement(node, CONTENT_CLASS);
|
|
760
808
|
node.addEventListener('mousedown', this);
|
|
761
809
|
node.addEventListener('keydown', this);
|
|
@@ -809,11 +857,16 @@ export class DirListing extends Widget {
|
|
|
809
857
|
}
|
|
810
858
|
}
|
|
811
859
|
|
|
812
|
-
|
|
860
|
+
/**
|
|
861
|
+
* Update the modified column's size
|
|
862
|
+
*/
|
|
813
863
|
private _updateModifiedSize(node: HTMLElement) {
|
|
814
864
|
// Look for the modified column's header
|
|
815
865
|
const modified = DOMUtils.findElement(node, MODIFIED_ID_CLASS);
|
|
816
|
-
this._modifiedWidth =
|
|
866
|
+
this._modifiedWidth =
|
|
867
|
+
this._columnSizes['last_modified'] ??
|
|
868
|
+
modified?.getBoundingClientRect().width ??
|
|
869
|
+
83;
|
|
817
870
|
this._modifiedStyle =
|
|
818
871
|
this._modifiedWidth < 100
|
|
819
872
|
? 'narrow'
|
|
@@ -822,7 +875,21 @@ export class DirListing extends Widget {
|
|
|
822
875
|
: 'short';
|
|
823
876
|
}
|
|
824
877
|
|
|
825
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Rerender item nodes' modified dates, if the modified style has changed.
|
|
880
|
+
*/
|
|
881
|
+
private _updateModifiedStyleAndSize() {
|
|
882
|
+
const oldModifiedStyle = this._modifiedStyle;
|
|
883
|
+
// Update both size and style
|
|
884
|
+
this._updateModifiedSize(this.node);
|
|
885
|
+
if (oldModifiedStyle !== this._modifiedStyle) {
|
|
886
|
+
this.updateModified(this._sortedItems, this._items);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Update only the modified dates.
|
|
892
|
+
*/
|
|
826
893
|
protected updateModified(items: Contents.IModel[], nodes: HTMLElement[]) {
|
|
827
894
|
items.forEach((item, i) => {
|
|
828
895
|
const node = nodes[i];
|
|
@@ -846,9 +913,21 @@ export class DirListing extends Widget {
|
|
|
846
913
|
}
|
|
847
914
|
|
|
848
915
|
// Update item nodes based on widget state.
|
|
849
|
-
protected updateNodes(
|
|
916
|
+
protected updateNodes(
|
|
917
|
+
items: Contents.IModel[],
|
|
918
|
+
nodes: HTMLElement[],
|
|
919
|
+
sizeOnly = false
|
|
920
|
+
) {
|
|
850
921
|
items.forEach((item, i) => {
|
|
851
922
|
const node = nodes[i];
|
|
923
|
+
if (sizeOnly && this.renderer.updateItemSize) {
|
|
924
|
+
return this.renderer.updateItemSize(
|
|
925
|
+
node,
|
|
926
|
+
item,
|
|
927
|
+
this._modifiedStyle,
|
|
928
|
+
this._columnSizes
|
|
929
|
+
);
|
|
930
|
+
}
|
|
852
931
|
const ft = this._manager.registry.getFileTypeForModel(item);
|
|
853
932
|
this.renderer.updateItemNode(
|
|
854
933
|
node,
|
|
@@ -857,7 +936,8 @@ export class DirListing extends Widget {
|
|
|
857
936
|
this.translator,
|
|
858
937
|
this._hiddenColumns,
|
|
859
938
|
this.selection[item.path],
|
|
860
|
-
this._modifiedStyle
|
|
939
|
+
this._modifiedStyle,
|
|
940
|
+
this._columnSizes
|
|
861
941
|
);
|
|
862
942
|
if (
|
|
863
943
|
this.selection[item.path] &&
|
|
@@ -925,7 +1005,10 @@ export class DirListing extends Widget {
|
|
|
925
1005
|
|
|
926
1006
|
// Add any missing item nodes.
|
|
927
1007
|
while (nodes.length < items.length) {
|
|
928
|
-
const node = renderer.createItemNode(
|
|
1008
|
+
const node = renderer.createItemNode(
|
|
1009
|
+
this._hiddenColumns,
|
|
1010
|
+
this._columnSizes
|
|
1011
|
+
);
|
|
929
1012
|
node.classList.add(ITEM_CLASS);
|
|
930
1013
|
nodes.push(node);
|
|
931
1014
|
content.appendChild(node);
|
|
@@ -984,15 +1067,8 @@ export class DirListing extends Widget {
|
|
|
984
1067
|
onResize(msg: Widget.ResizeMessage): void {
|
|
985
1068
|
const { width } =
|
|
986
1069
|
msg.width === -1 ? this.node.getBoundingClientRect() : msg;
|
|
987
|
-
this.
|
|
988
|
-
|
|
989
|
-
// Rerender item nodes' modified dates, if the modified style has changed.
|
|
990
|
-
const oldModifiedStyle = this._modifiedStyle;
|
|
991
|
-
// Update both size and style
|
|
992
|
-
this._updateModifiedSize(this.node);
|
|
993
|
-
if (oldModifiedStyle !== this._modifiedStyle) {
|
|
994
|
-
this.updateModified(this._sortedItems, this._items);
|
|
995
|
-
}
|
|
1070
|
+
this._width = width;
|
|
1071
|
+
this._updateColumnSizes();
|
|
996
1072
|
}
|
|
997
1073
|
|
|
998
1074
|
setColumnVisibility(
|
|
@@ -1009,8 +1085,114 @@ export class DirListing extends Widget {
|
|
|
1009
1085
|
this._renderer.populateHeaderNode(
|
|
1010
1086
|
this.headerNode,
|
|
1011
1087
|
this.translator,
|
|
1012
|
-
this._hiddenColumns
|
|
1088
|
+
this._hiddenColumns,
|
|
1089
|
+
this._columnSizes
|
|
1013
1090
|
);
|
|
1091
|
+
|
|
1092
|
+
this._updateColumnSizes();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private _updateColumnSizes() {
|
|
1096
|
+
// adjust column sizes so that they add up to the total width available, preserving ratios
|
|
1097
|
+
// and removing width from the last column so that it fills the width available;
|
|
1098
|
+
const visibleColumns = this._visibleColumns.map(column => ({
|
|
1099
|
+
...column,
|
|
1100
|
+
element: DOMUtils.findElement(this.node, column.className)
|
|
1101
|
+
}));
|
|
1102
|
+
|
|
1103
|
+
// read from DOM
|
|
1104
|
+
let total = 0;
|
|
1105
|
+
for (const column of visibleColumns) {
|
|
1106
|
+
let size = this._columnSizes[column.id];
|
|
1107
|
+
if (size === null) {
|
|
1108
|
+
size = column.element.getBoundingClientRect().width;
|
|
1109
|
+
}
|
|
1110
|
+
// restrict the minimum and maximum width
|
|
1111
|
+
size = Math.max(size, column.minWidth);
|
|
1112
|
+
if (this._width) {
|
|
1113
|
+
let reservedForOtherColums = 0;
|
|
1114
|
+
for (const other of visibleColumns) {
|
|
1115
|
+
if (other.id === column.id) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
reservedForOtherColums += other.minWidth;
|
|
1119
|
+
}
|
|
1120
|
+
size = Math.min(size, this._width - reservedForOtherColums);
|
|
1121
|
+
}
|
|
1122
|
+
this._columnSizes[column.id] = size;
|
|
1123
|
+
total += size;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Ensure that total fits
|
|
1127
|
+
if (this._width && total > this._width) {
|
|
1128
|
+
for (const column of visibleColumns) {
|
|
1129
|
+
this._columnSizes[column.id] =
|
|
1130
|
+
(this._columnSizes[column.id]! / total) * this._width;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Write to DOM
|
|
1135
|
+
for (const column of visibleColumns) {
|
|
1136
|
+
const size = this._columnSizes[column.id];
|
|
1137
|
+
column.element.style.width = size === null ? '' : size + 'px';
|
|
1138
|
+
}
|
|
1139
|
+
this._updateModifiedStyleAndSize();
|
|
1140
|
+
|
|
1141
|
+
// Refresh sizes on the per item widths
|
|
1142
|
+
if (this.isVisible) {
|
|
1143
|
+
const items = this._items;
|
|
1144
|
+
if (items.length !== 0) {
|
|
1145
|
+
this.updateNodes(this._sortedItems, this._items, true);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (this._state && this._stateColumnsKey) {
|
|
1150
|
+
void this._state.save(this._stateColumnsKey, {
|
|
1151
|
+
sizes: this._columnSizes
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
private get _visibleColumns() {
|
|
1157
|
+
return DirListing.columns.filter(
|
|
1158
|
+
column => column.id === 'name' || !this._hiddenColumns?.has(column.id)
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
private _setColumnSize(
|
|
1163
|
+
name: DirListing.ResizableColumn,
|
|
1164
|
+
size: number | null
|
|
1165
|
+
): void {
|
|
1166
|
+
const previousSize = this._columnSizes[name];
|
|
1167
|
+
if (previousSize && size && size > previousSize) {
|
|
1168
|
+
// check if we can resize up
|
|
1169
|
+
let total = 0;
|
|
1170
|
+
let before = true;
|
|
1171
|
+
for (const column of this._visibleColumns) {
|
|
1172
|
+
if (column.id === name) {
|
|
1173
|
+
// add proposed size for the current columns
|
|
1174
|
+
total += size;
|
|
1175
|
+
before = false;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (before) {
|
|
1179
|
+
// add size as-is for columns before
|
|
1180
|
+
const element = DOMUtils.findElement(this.node, column.className);
|
|
1181
|
+
total +=
|
|
1182
|
+
this._columnSizes[column.id] ??
|
|
1183
|
+
element.getBoundingClientRect().width;
|
|
1184
|
+
} else {
|
|
1185
|
+
// add minimum acceptable size for columns after
|
|
1186
|
+
total += column.minWidth;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (this._width && total > this._width) {
|
|
1190
|
+
// up sizing is no longer possible
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
this._columnSizes[name] = size;
|
|
1195
|
+
this._updateColumnSizes();
|
|
1014
1196
|
}
|
|
1015
1197
|
|
|
1016
1198
|
/**
|
|
@@ -1025,6 +1207,14 @@ export class DirListing extends Widget {
|
|
|
1025
1207
|
}
|
|
1026
1208
|
}
|
|
1027
1209
|
|
|
1210
|
+
/**
|
|
1211
|
+
* Update the setting to allow single click navigation.
|
|
1212
|
+
* This enables opening files/directories with a single click.
|
|
1213
|
+
*/
|
|
1214
|
+
setAllowSingleClickNavigation(isEnabled: boolean) {
|
|
1215
|
+
this._allowSingleClick = isEnabled;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1028
1218
|
/**
|
|
1029
1219
|
* Would this click (or other event type) hit the checkbox by default?
|
|
1030
1220
|
*/
|
|
@@ -1113,6 +1303,43 @@ export class DirListing extends Widget {
|
|
|
1113
1303
|
let index = Private.hitTestNodes(this._items, event);
|
|
1114
1304
|
|
|
1115
1305
|
if (index === -1) {
|
|
1306
|
+
// Left mouse press for drag or resize start.
|
|
1307
|
+
if (event.button === 0) {
|
|
1308
|
+
const resizeHandle = event.target;
|
|
1309
|
+
if (
|
|
1310
|
+
resizeHandle instanceof HTMLElement &&
|
|
1311
|
+
resizeHandle.classList.contains(RESIZE_HANDLE_CLASS)
|
|
1312
|
+
) {
|
|
1313
|
+
const columnId = resizeHandle.dataset.column as
|
|
1314
|
+
| DirListing.ResizableColumn
|
|
1315
|
+
| undefined;
|
|
1316
|
+
if (!columnId) {
|
|
1317
|
+
throw Error(
|
|
1318
|
+
'Column resize handle is missing data-column attribute'
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const column = DirListing.columns.find(c => c.id === columnId);
|
|
1322
|
+
if (!column) {
|
|
1323
|
+
throw Error(`Column with identifier ${columnId} not found`);
|
|
1324
|
+
}
|
|
1325
|
+
const element = DOMUtils.findElement(this.node, column.className);
|
|
1326
|
+
resizeHandle.classList.add(ACTIVE_CLASS);
|
|
1327
|
+
const cursorOverride = Drag.overrideCursor('col-resize');
|
|
1328
|
+
|
|
1329
|
+
this._resizeData = {
|
|
1330
|
+
pressX: event.clientX,
|
|
1331
|
+
column: columnId,
|
|
1332
|
+
initialSize: element.getBoundingClientRect().width,
|
|
1333
|
+
overrides: new DisposableDelegate(() => {
|
|
1334
|
+
cursorOverride.dispose();
|
|
1335
|
+
resizeHandle.classList.remove(ACTIVE_CLASS);
|
|
1336
|
+
})
|
|
1337
|
+
};
|
|
1338
|
+
document.addEventListener('mouseup', this, true);
|
|
1339
|
+
document.addEventListener('mousemove', this, true);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1116
1343
|
return;
|
|
1117
1344
|
}
|
|
1118
1345
|
|
|
@@ -1128,7 +1355,7 @@ export class DirListing extends Widget {
|
|
|
1128
1355
|
return;
|
|
1129
1356
|
}
|
|
1130
1357
|
|
|
1131
|
-
// Left mouse press for drag start.
|
|
1358
|
+
// Left mouse press for drag or resize start.
|
|
1132
1359
|
if (event.button === 0) {
|
|
1133
1360
|
this._dragData = {
|
|
1134
1361
|
pressX: event.clientX,
|
|
@@ -1138,6 +1365,10 @@ export class DirListing extends Widget {
|
|
|
1138
1365
|
document.addEventListener('mouseup', this, true);
|
|
1139
1366
|
document.addEventListener('mousemove', this, true);
|
|
1140
1367
|
}
|
|
1368
|
+
|
|
1369
|
+
if (this._allowSingleClick) {
|
|
1370
|
+
this.evtDblClick(event as MouseEvent);
|
|
1371
|
+
}
|
|
1141
1372
|
}
|
|
1142
1373
|
|
|
1143
1374
|
/**
|
|
@@ -1162,12 +1393,21 @@ export class DirListing extends Widget {
|
|
|
1162
1393
|
this._focusItem(this._focusIndex);
|
|
1163
1394
|
}
|
|
1164
1395
|
|
|
1396
|
+
// Remove the resize listeners if necessary.
|
|
1397
|
+
if (this._resizeData) {
|
|
1398
|
+
this._resizeData.overrides.dispose();
|
|
1399
|
+
document.removeEventListener('mousemove', this, true);
|
|
1400
|
+
document.removeEventListener('mouseup', this, true);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1165
1404
|
// Remove the drag listeners if necessary.
|
|
1166
1405
|
if (event.button !== 0 || !this._drag) {
|
|
1167
1406
|
document.removeEventListener('mousemove', this, true);
|
|
1168
1407
|
document.removeEventListener('mouseup', this, true);
|
|
1169
1408
|
return;
|
|
1170
1409
|
}
|
|
1410
|
+
|
|
1171
1411
|
event.preventDefault();
|
|
1172
1412
|
event.stopPropagation();
|
|
1173
1413
|
}
|
|
@@ -1179,6 +1419,12 @@ export class DirListing extends Widget {
|
|
|
1179
1419
|
event.preventDefault();
|
|
1180
1420
|
event.stopPropagation();
|
|
1181
1421
|
|
|
1422
|
+
if (this._resizeData) {
|
|
1423
|
+
const { initialSize, column, pressX } = this._resizeData;
|
|
1424
|
+
this._setColumnSize(column, initialSize + event.clientX - pressX);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1182
1428
|
// Bail if we are the one dragging.
|
|
1183
1429
|
if (this._drag || !this._dragData) {
|
|
1184
1430
|
return;
|
|
@@ -1468,31 +1714,70 @@ export class DirListing extends Widget {
|
|
|
1468
1714
|
* Handle the `drop` event for the widget.
|
|
1469
1715
|
*/
|
|
1470
1716
|
protected evtNativeDrop(event: DragEvent): void {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1717
|
+
// Prevent navigation
|
|
1718
|
+
event.preventDefault();
|
|
1719
|
+
|
|
1720
|
+
const items = event.dataTransfer?.items;
|
|
1721
|
+
if (!items) {
|
|
1722
|
+
// Fallback to simple upload of files (if any)
|
|
1723
|
+
const files = event.dataTransfer?.files;
|
|
1724
|
+
if (!files || files.length === 0) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const promises = [];
|
|
1728
|
+
for (const file of files) {
|
|
1729
|
+
const promise = this._model.upload(file);
|
|
1730
|
+
promises.push(promise);
|
|
1731
|
+
}
|
|
1732
|
+
Promise.all(promises)
|
|
1733
|
+
.then(() => this._allUploaded.emit())
|
|
1734
|
+
.catch(err => {
|
|
1735
|
+
console.error('Error while uploading files: ', err);
|
|
1736
|
+
});
|
|
1477
1737
|
return;
|
|
1478
1738
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
if (entry
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1739
|
+
|
|
1740
|
+
const uploadEntry = async (entry: FileSystemEntry, path: string) => {
|
|
1741
|
+
if (Private.isDirectoryEntry(entry)) {
|
|
1742
|
+
const dirPath = await Private.createDirectory(
|
|
1743
|
+
this._model.manager,
|
|
1744
|
+
path,
|
|
1745
|
+
entry.name
|
|
1746
|
+
);
|
|
1747
|
+
const directoryReader = entry.createReader();
|
|
1748
|
+
|
|
1749
|
+
const allEntries = await Private.collectEntries(directoryReader);
|
|
1750
|
+
for (const childEntry of allEntries) {
|
|
1751
|
+
await uploadEntry(childEntry, dirPath);
|
|
1752
|
+
}
|
|
1753
|
+
} else if (Private.isFileEntry(entry)) {
|
|
1754
|
+
const file = await Private.readFile(entry);
|
|
1755
|
+
await this._model.upload(file, path);
|
|
1490
1756
|
}
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
const promises = [];
|
|
1760
|
+
for (const item of items) {
|
|
1761
|
+
const entry = Private.defensiveGetAsEntry(item);
|
|
1762
|
+
|
|
1763
|
+
if (!entry) {
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
const promise = uploadEntry(entry, this._model.path ?? '/');
|
|
1767
|
+
promises.push(promise);
|
|
1491
1768
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1769
|
+
Promise.all(promises)
|
|
1770
|
+
.then(() => this._allUploaded.emit())
|
|
1771
|
+
.catch(err => {
|
|
1772
|
+
console.error('Error while uploading files: ', err);
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* Signal emitted on when all files were uploaded after native drag.
|
|
1778
|
+
*/
|
|
1779
|
+
protected get allUploaded(): ISignal<DirListing, void> {
|
|
1780
|
+
return this._allUploaded;
|
|
1496
1781
|
}
|
|
1497
1782
|
|
|
1498
1783
|
/**
|
|
@@ -2130,6 +2415,24 @@ export class DirListing extends Widget {
|
|
|
2130
2415
|
pressY: number;
|
|
2131
2416
|
index: number;
|
|
2132
2417
|
} | null = null;
|
|
2418
|
+
private _resizeData: {
|
|
2419
|
+
/**
|
|
2420
|
+
* Cursor position when the resize started.
|
|
2421
|
+
*/
|
|
2422
|
+
pressX: number;
|
|
2423
|
+
/**
|
|
2424
|
+
* Identifier of the column being resized.
|
|
2425
|
+
*/
|
|
2426
|
+
column: DirListing.ResizableColumn;
|
|
2427
|
+
/**
|
|
2428
|
+
* Size of the column when the cursor grabbed the resize handle.
|
|
2429
|
+
*/
|
|
2430
|
+
initialSize: number;
|
|
2431
|
+
/**
|
|
2432
|
+
* The disposable to clear the cursor override and resize handle.
|
|
2433
|
+
*/
|
|
2434
|
+
readonly overrides: IDisposable;
|
|
2435
|
+
} | null = null;
|
|
2133
2436
|
private _selectTimer = -1;
|
|
2134
2437
|
private _isCut = false;
|
|
2135
2438
|
private _prevPath = '';
|
|
@@ -2143,12 +2446,22 @@ export class DirListing extends Widget {
|
|
|
2143
2446
|
private _inRename = false;
|
|
2144
2447
|
private _isDirty = false;
|
|
2145
2448
|
private _hiddenColumns = new Set<DirListing.ToggleableColumn>();
|
|
2449
|
+
private _columnSizes: Record<DirListing.IColumn['id'], number | null> = {
|
|
2450
|
+
name: null,
|
|
2451
|
+
file_size: null,
|
|
2452
|
+
is_selected: null,
|
|
2453
|
+
last_modified: null
|
|
2454
|
+
};
|
|
2146
2455
|
private _sortNotebooksFirst = false;
|
|
2456
|
+
private _allowSingleClick = false;
|
|
2147
2457
|
// _focusIndex should never be set outside the range [0, this._items.length - 1]
|
|
2148
2458
|
private _focusIndex = 0;
|
|
2149
2459
|
// Width of the "last modified" column for an individual file
|
|
2150
2460
|
private _modifiedWidth: number;
|
|
2151
2461
|
private _modifiedStyle: Time.HumanStyle;
|
|
2462
|
+
private _allUploaded = new Signal<DirListing, void>(this);
|
|
2463
|
+
private _width: number | null = null;
|
|
2464
|
+
private _state: IStateDB | null = null;
|
|
2152
2465
|
}
|
|
2153
2466
|
|
|
2154
2467
|
/**
|
|
@@ -2175,6 +2488,12 @@ export namespace DirListing {
|
|
|
2175
2488
|
* A language translator.
|
|
2176
2489
|
*/
|
|
2177
2490
|
translator?: ITranslator;
|
|
2491
|
+
|
|
2492
|
+
/**
|
|
2493
|
+
* An optional state database. If provided, the widget will restore
|
|
2494
|
+
* the columns sizes
|
|
2495
|
+
*/
|
|
2496
|
+
state?: IStateDB;
|
|
2178
2497
|
}
|
|
2179
2498
|
|
|
2180
2499
|
/**
|
|
@@ -2189,7 +2508,7 @@ export namespace DirListing {
|
|
|
2189
2508
|
/**
|
|
2190
2509
|
* The sort key.
|
|
2191
2510
|
*/
|
|
2192
|
-
key:
|
|
2511
|
+
key: SortableColumn;
|
|
2193
2512
|
}
|
|
2194
2513
|
|
|
2195
2514
|
/**
|
|
@@ -2197,6 +2516,16 @@ export namespace DirListing {
|
|
|
2197
2516
|
*/
|
|
2198
2517
|
export type ToggleableColumn = 'last_modified' | 'is_selected' | 'file_size';
|
|
2199
2518
|
|
|
2519
|
+
/**
|
|
2520
|
+
* Resizable columns.
|
|
2521
|
+
*/
|
|
2522
|
+
export type ResizableColumn = 'name' | 'last_modified' | 'file_size';
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* Sortable columns.
|
|
2526
|
+
*/
|
|
2527
|
+
export type SortableColumn = 'name' | 'last_modified' | 'file_size';
|
|
2528
|
+
|
|
2200
2529
|
/**
|
|
2201
2530
|
* A file contents model thunk.
|
|
2202
2531
|
*
|
|
@@ -2233,7 +2562,8 @@ export namespace DirListing {
|
|
|
2233
2562
|
populateHeaderNode(
|
|
2234
2563
|
node: HTMLElement,
|
|
2235
2564
|
translator?: ITranslator,
|
|
2236
|
-
hiddenColumns?: Set<DirListing.ToggleableColumn
|
|
2565
|
+
hiddenColumns?: Set<DirListing.ToggleableColumn>,
|
|
2566
|
+
columnsSizes?: Record<IColumn['id'], number | null>
|
|
2237
2567
|
): void;
|
|
2238
2568
|
|
|
2239
2569
|
/**
|
|
@@ -2253,7 +2583,8 @@ export namespace DirListing {
|
|
|
2253
2583
|
* @returns A new DOM node to use as a content item.
|
|
2254
2584
|
*/
|
|
2255
2585
|
createItemNode(
|
|
2256
|
-
hiddenColumns?: Set<DirListing.ToggleableColumn
|
|
2586
|
+
hiddenColumns?: Set<DirListing.ToggleableColumn>,
|
|
2587
|
+
columnsSizes?: Record<IColumn['id'], number | null>
|
|
2257
2588
|
): HTMLElement;
|
|
2258
2589
|
|
|
2259
2590
|
/**
|
|
@@ -2289,7 +2620,18 @@ export namespace DirListing {
|
|
|
2289
2620
|
translator?: ITranslator,
|
|
2290
2621
|
hiddenColumns?: Set<DirListing.ToggleableColumn>,
|
|
2291
2622
|
selected?: boolean,
|
|
2292
|
-
modifiedStyle?: Time.HumanStyle
|
|
2623
|
+
modifiedStyle?: Time.HumanStyle,
|
|
2624
|
+
columnsSizes?: Record<IColumn['id'], number | null>
|
|
2625
|
+
): void;
|
|
2626
|
+
|
|
2627
|
+
/**
|
|
2628
|
+
* Update size of item nodes, assuming that model has not changed.
|
|
2629
|
+
*/
|
|
2630
|
+
updateItemSize?(
|
|
2631
|
+
node: HTMLElement,
|
|
2632
|
+
model: Contents.IModel,
|
|
2633
|
+
modifiedStyle?: Time.HumanStyle,
|
|
2634
|
+
columnsSizes?: Record<IColumn['id'], number | null>
|
|
2293
2635
|
): void;
|
|
2294
2636
|
|
|
2295
2637
|
/**
|
|
@@ -2334,6 +2676,91 @@ export namespace DirListing {
|
|
|
2334
2676
|
): HTMLElement;
|
|
2335
2677
|
}
|
|
2336
2678
|
|
|
2679
|
+
interface IBaseColumn {
|
|
2680
|
+
/**
|
|
2681
|
+
* Name of the header class, must be unique among other columns.
|
|
2682
|
+
*/
|
|
2683
|
+
className: string;
|
|
2684
|
+
/**
|
|
2685
|
+
* Name of the item class, must be unique among other columns.
|
|
2686
|
+
*/
|
|
2687
|
+
itemClassName: string;
|
|
2688
|
+
/**
|
|
2689
|
+
* Minimum size the column should occupy.
|
|
2690
|
+
*/
|
|
2691
|
+
minWidth: number;
|
|
2692
|
+
}
|
|
2693
|
+
interface IFixedColumn extends IBaseColumn {
|
|
2694
|
+
id: 'is_selected';
|
|
2695
|
+
resizable: false;
|
|
2696
|
+
sortable: false;
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Sortable column.
|
|
2700
|
+
*/
|
|
2701
|
+
export interface ISortableColumn extends IBaseColumn {
|
|
2702
|
+
id: SortableColumn;
|
|
2703
|
+
sortable: true;
|
|
2704
|
+
caretSide: 'left' | 'right';
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Resizable column.
|
|
2708
|
+
*/
|
|
2709
|
+
export interface IResizableColumn extends IBaseColumn {
|
|
2710
|
+
id: ResizableColumn;
|
|
2711
|
+
resizable: true;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
/**
|
|
2715
|
+
* Columns types supported by DirListing.
|
|
2716
|
+
*/
|
|
2717
|
+
export type IColumn =
|
|
2718
|
+
| IFixedColumn
|
|
2719
|
+
| ISortableColumn
|
|
2720
|
+
| IResizableColumn
|
|
2721
|
+
| (ISortableColumn & IResizableColumn);
|
|
2722
|
+
|
|
2723
|
+
/**
|
|
2724
|
+
* Column definitions.
|
|
2725
|
+
*/
|
|
2726
|
+
export const columns: IColumn[] = [
|
|
2727
|
+
{
|
|
2728
|
+
id: 'is_selected' as const,
|
|
2729
|
+
className: CHECKBOX_WRAPPER_CLASS,
|
|
2730
|
+
itemClassName: CHECKBOX_WRAPPER_CLASS,
|
|
2731
|
+
minWidth: 18,
|
|
2732
|
+
resizable: false,
|
|
2733
|
+
sortable: false
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
id: 'name' as const,
|
|
2737
|
+
className: NAME_ID_CLASS,
|
|
2738
|
+
itemClassName: ITEM_NAME_COLUMN_CLASS,
|
|
2739
|
+
minWidth: 60,
|
|
2740
|
+
resizable: true,
|
|
2741
|
+
sortable: true,
|
|
2742
|
+
caretSide: 'right'
|
|
2743
|
+
},
|
|
2744
|
+
{
|
|
2745
|
+
id: 'last_modified' as const,
|
|
2746
|
+
className: MODIFIED_ID_CLASS,
|
|
2747
|
+
itemClassName: ITEM_MODIFIED_CLASS,
|
|
2748
|
+
minWidth: 60,
|
|
2749
|
+
resizable: true,
|
|
2750
|
+
sortable: true,
|
|
2751
|
+
caretSide: 'left'
|
|
2752
|
+
},
|
|
2753
|
+
{
|
|
2754
|
+
id: 'file_size' as const,
|
|
2755
|
+
className: FILE_SIZE_ID_CLASS,
|
|
2756
|
+
itemClassName: ITEM_FILE_SIZE_CLASS,
|
|
2757
|
+
minWidth: 60,
|
|
2758
|
+
resizable: true,
|
|
2759
|
+
sortable: true,
|
|
2760
|
+
caretSide: 'left'
|
|
2761
|
+
}
|
|
2762
|
+
];
|
|
2763
|
+
|
|
2337
2764
|
/**
|
|
2338
2765
|
* The default implementation of an `IRenderer`.
|
|
2339
2766
|
*/
|
|
@@ -2364,47 +2791,57 @@ export namespace DirListing {
|
|
|
2364
2791
|
populateHeaderNode(
|
|
2365
2792
|
node: HTMLElement,
|
|
2366
2793
|
translator?: ITranslator,
|
|
2367
|
-
hiddenColumns?: Set<DirListing.ToggleableColumn
|
|
2794
|
+
hiddenColumns?: Set<DirListing.ToggleableColumn>,
|
|
2795
|
+
columnsSizes?: Record<DirListing.IColumn['id'], number | null>
|
|
2368
2796
|
): void {
|
|
2369
2797
|
translator = translator || nullTranslator;
|
|
2370
2798
|
const trans = translator.load('jupyterlab');
|
|
2371
|
-
const name = this.createHeaderItemNode(trans.__('Name'));
|
|
2372
|
-
const narrow = document.createElement('div');
|
|
2373
|
-
const modified = this._createHeaderItemNodeWithSizes({
|
|
2374
|
-
small: trans.__('Modified'),
|
|
2375
|
-
large: trans.__('Last Modified')
|
|
2376
|
-
});
|
|
2377
|
-
const fileSize = this.createHeaderItemNode(trans.__('File Size'));
|
|
2378
|
-
name.classList.add(NAME_ID_CLASS);
|
|
2379
|
-
name.classList.add(SELECTED_CLASS);
|
|
2380
|
-
modified.classList.add(MODIFIED_ID_CLASS);
|
|
2381
|
-
fileSize.classList.add(FILE_SIZE_ID_CLASS);
|
|
2382
|
-
narrow.classList.add(NARROW_ID_CLASS);
|
|
2383
|
-
narrow.textContent = '...';
|
|
2384
|
-
if (!hiddenColumns?.has('is_selected')) {
|
|
2385
|
-
const checkboxWrapper = this.createCheckboxWrapperNode({
|
|
2386
|
-
alwaysVisible: true,
|
|
2387
|
-
headerNode: true
|
|
2388
|
-
});
|
|
2389
|
-
node.appendChild(checkboxWrapper);
|
|
2390
|
-
}
|
|
2391
|
-
node.appendChild(name);
|
|
2392
|
-
node.appendChild(narrow);
|
|
2393
|
-
node.appendChild(modified);
|
|
2394
|
-
node.appendChild(fileSize);
|
|
2395
2799
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2800
|
+
const elementCreators = {
|
|
2801
|
+
name: () => this.createHeaderItemNode(trans.__('Name')),
|
|
2802
|
+
last_modified: () =>
|
|
2803
|
+
this._createHeaderItemNodeWithSizes({
|
|
2804
|
+
small: trans.__('Modified'),
|
|
2805
|
+
large: trans.__('Last Modified')
|
|
2806
|
+
}),
|
|
2807
|
+
file_size: () => this.createHeaderItemNode(trans.__('File Size')),
|
|
2808
|
+
is_selected: () =>
|
|
2809
|
+
this.createCheckboxWrapperNode({
|
|
2810
|
+
alwaysVisible: true,
|
|
2811
|
+
headerNode: true
|
|
2812
|
+
})
|
|
2813
|
+
};
|
|
2401
2814
|
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2815
|
+
const visibleColumns = columns.filter(
|
|
2816
|
+
column => column.id === 'name' || !hiddenColumns?.has(column.id)
|
|
2817
|
+
);
|
|
2818
|
+
|
|
2819
|
+
for (const column of visibleColumns) {
|
|
2820
|
+
const createElement = elementCreators[column.id];
|
|
2821
|
+
const element = createElement();
|
|
2822
|
+
element.classList.add(column.className);
|
|
2823
|
+
const isLastVisible =
|
|
2824
|
+
column.id === visibleColumns[visibleColumns.length - 1].id;
|
|
2825
|
+
|
|
2826
|
+
if (columnsSizes) {
|
|
2827
|
+
const size = columnsSizes[column.id];
|
|
2828
|
+
if (!isLastVisible) {
|
|
2829
|
+
element.style.width = size + 'px';
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
node.appendChild(element);
|
|
2833
|
+
|
|
2834
|
+
if (Private.isResizable(column) && !isLastVisible) {
|
|
2835
|
+
const resizer = document.createElement('div');
|
|
2836
|
+
resizer.classList.add(RESIZE_HANDLE_CLASS);
|
|
2837
|
+
resizer.dataset.column = column.id;
|
|
2838
|
+
node.appendChild(resizer);
|
|
2839
|
+
}
|
|
2406
2840
|
}
|
|
2407
2841
|
|
|
2842
|
+
const name = DOMUtils.findElement(node, NAME_ID_CLASS);
|
|
2843
|
+
name.classList.add(SELECTED_CLASS);
|
|
2844
|
+
|
|
2408
2845
|
// set the initial caret icon
|
|
2409
2846
|
Private.updateCaret(
|
|
2410
2847
|
DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS),
|
|
@@ -2423,92 +2860,57 @@ export namespace DirListing {
|
|
|
2423
2860
|
* @returns The sort state of the header after the click event.
|
|
2424
2861
|
*/
|
|
2425
2862
|
handleHeaderClick(node: HTMLElement, event: MouseEvent): ISortState | null {
|
|
2426
|
-
const name = DOMUtils.findElement(node, NAME_ID_CLASS);
|
|
2427
|
-
const modified = DOMUtils.findElement(node, MODIFIED_ID_CLASS);
|
|
2428
|
-
const fileSize = DOMUtils.findElement(node, FILE_SIZE_ID_CLASS);
|
|
2429
2863
|
const state: ISortState = { direction: 'ascending', key: 'name' };
|
|
2430
2864
|
const target = event.target as HTMLElement;
|
|
2431
2865
|
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
)
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
const nameIcon = DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS);
|
|
2441
|
-
|
|
2442
|
-
if (name.contains(target)) {
|
|
2443
|
-
if (name.classList.contains(SELECTED_CLASS)) {
|
|
2444
|
-
if (!name.classList.contains(DESCENDING_CLASS)) {
|
|
2445
|
-
state.direction = 'descending';
|
|
2446
|
-
name.classList.add(DESCENDING_CLASS);
|
|
2447
|
-
Private.updateCaret(nameIcon, 'right', 'down');
|
|
2448
|
-
} else {
|
|
2449
|
-
name.classList.remove(DESCENDING_CLASS);
|
|
2450
|
-
Private.updateCaret(nameIcon, 'right', 'up');
|
|
2451
|
-
}
|
|
2452
|
-
} else {
|
|
2453
|
-
name.classList.remove(DESCENDING_CLASS);
|
|
2454
|
-
Private.updateCaret(nameIcon, 'right', 'up');
|
|
2866
|
+
const sortableColumns = DirListing.columns.filter(Private.isSortable);
|
|
2867
|
+
console.log(sortableColumns);
|
|
2868
|
+
|
|
2869
|
+
for (const column of sortableColumns) {
|
|
2870
|
+
const header = node.querySelector(`.${column.className}`);
|
|
2871
|
+
if (!header) {
|
|
2872
|
+
// skip if the column is hidden
|
|
2873
|
+
continue;
|
|
2455
2874
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
Private.updateCaret(modifiedIcon, 'left', 'down');
|
|
2875
|
+
if (header.contains(target)) {
|
|
2876
|
+
state.key = column.id;
|
|
2877
|
+
const headerIcon = DOMUtils.findElement(
|
|
2878
|
+
header as HTMLElement,
|
|
2879
|
+
HEADER_ITEM_ICON_CLASS
|
|
2880
|
+
);
|
|
2881
|
+
if (header.classList.contains(SELECTED_CLASS)) {
|
|
2882
|
+
if (!header.classList.contains(DESCENDING_CLASS)) {
|
|
2883
|
+
state.direction = 'descending';
|
|
2884
|
+
header.classList.add(DESCENDING_CLASS);
|
|
2885
|
+
Private.updateCaret(headerIcon, column.caretSide, 'down');
|
|
2886
|
+
} else {
|
|
2887
|
+
header.classList.remove(DESCENDING_CLASS);
|
|
2888
|
+
Private.updateCaret(headerIcon, column.caretSide, 'up');
|
|
2889
|
+
}
|
|
2472
2890
|
} else {
|
|
2473
|
-
|
|
2474
|
-
Private.updateCaret(
|
|
2891
|
+
header.classList.remove(DESCENDING_CLASS);
|
|
2892
|
+
Private.updateCaret(headerIcon, column.caretSide, 'up');
|
|
2475
2893
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
state.direction = 'descending';
|
|
2494
|
-
fileSize.classList.add(DESCENDING_CLASS);
|
|
2495
|
-
Private.updateCaret(fileSizeIcon, 'left', 'down');
|
|
2496
|
-
} else {
|
|
2497
|
-
fileSize.classList.remove(DESCENDING_CLASS);
|
|
2498
|
-
Private.updateCaret(fileSizeIcon, 'left', 'up');
|
|
2894
|
+
header.classList.add(SELECTED_CLASS);
|
|
2895
|
+
for (const otherColumn of sortableColumns) {
|
|
2896
|
+
if (otherColumn.id === column.id) {
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
const otherHeader = node.querySelector(`.${otherColumn.className}`);
|
|
2900
|
+
if (!otherHeader) {
|
|
2901
|
+
// skip if hidden
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
otherHeader.classList.remove(SELECTED_CLASS);
|
|
2905
|
+
otherHeader.classList.remove(DESCENDING_CLASS);
|
|
2906
|
+
const otherHeaderIcon = DOMUtils.findElement(
|
|
2907
|
+
otherHeader as HTMLElement,
|
|
2908
|
+
HEADER_ITEM_ICON_CLASS
|
|
2909
|
+
);
|
|
2910
|
+
Private.updateCaret(otherHeaderIcon, otherColumn.caretSide);
|
|
2499
2911
|
}
|
|
2500
|
-
|
|
2501
|
-
fileSize.classList.remove(DESCENDING_CLASS);
|
|
2502
|
-
Private.updateCaret(fileSizeIcon, 'left', 'up');
|
|
2912
|
+
return state;
|
|
2503
2913
|
}
|
|
2504
|
-
fileSize.classList.add(SELECTED_CLASS);
|
|
2505
|
-
name.classList.remove(SELECTED_CLASS);
|
|
2506
|
-
name.classList.remove(DESCENDING_CLASS);
|
|
2507
|
-
modified.classList.remove(SELECTED_CLASS);
|
|
2508
|
-
modified.classList.remove(DESCENDING_CLASS);
|
|
2509
|
-
Private.updateCaret(nameIcon, 'right');
|
|
2510
|
-
Private.updateCaret(modifiedIcon, 'left');
|
|
2511
|
-
return state;
|
|
2512
2914
|
}
|
|
2513
2915
|
return state;
|
|
2514
2916
|
}
|
|
@@ -2519,36 +2921,23 @@ export namespace DirListing {
|
|
|
2519
2921
|
* @returns A new DOM node to use as a content item.
|
|
2520
2922
|
*/
|
|
2521
2923
|
createItemNode(
|
|
2522
|
-
hiddenColumns?: Set<DirListing.ToggleableColumn
|
|
2924
|
+
hiddenColumns?: Set<DirListing.ToggleableColumn>,
|
|
2925
|
+
columnsSizes?: Record<DirListing.IColumn['id'], number | null>
|
|
2523
2926
|
): HTMLElement {
|
|
2524
2927
|
const node = document.createElement('li');
|
|
2525
|
-
const icon = document.createElement('span');
|
|
2526
|
-
const text = document.createElement('span');
|
|
2527
|
-
const modified = document.createElement('span');
|
|
2528
|
-
const fileSize = document.createElement('span');
|
|
2529
|
-
icon.className = ITEM_ICON_CLASS;
|
|
2530
|
-
text.className = ITEM_TEXT_CLASS;
|
|
2531
|
-
modified.className = ITEM_MODIFIED_CLASS;
|
|
2532
|
-
fileSize.className = ITEM_FILE_SIZE_CLASS;
|
|
2533
|
-
if (!hiddenColumns?.has('is_selected')) {
|
|
2534
|
-
const checkboxWrapper = this.createCheckboxWrapperNode();
|
|
2535
|
-
node.appendChild(checkboxWrapper);
|
|
2536
|
-
}
|
|
2537
|
-
node.appendChild(icon);
|
|
2538
|
-
node.appendChild(text);
|
|
2539
|
-
node.appendChild(modified);
|
|
2540
|
-
node.appendChild(fileSize);
|
|
2541
2928
|
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2929
|
+
for (const column of columns) {
|
|
2930
|
+
if (column.id != 'name' && hiddenColumns?.has(column.id)) {
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
const createElement = this.itemFactories[column.id];
|
|
2934
|
+
const element = createElement();
|
|
2935
|
+
node.appendChild(element);
|
|
2547
2936
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2937
|
+
if (columnsSizes) {
|
|
2938
|
+
const size = columnsSizes[column.id];
|
|
2939
|
+
element.style.width = size + 'px';
|
|
2940
|
+
}
|
|
2552
2941
|
}
|
|
2553
2942
|
|
|
2554
2943
|
return node;
|
|
@@ -2614,16 +3003,29 @@ export namespace DirListing {
|
|
|
2614
3003
|
modifiedDate: string,
|
|
2615
3004
|
modifiedStyle: Time.HumanStyle
|
|
2616
3005
|
): void {
|
|
2617
|
-
|
|
2618
|
-
|
|
3006
|
+
// Formatting dates is expensive (0.1-0.2ms per call,
|
|
3007
|
+
// so over 150 files can easily already choke the renderer),
|
|
3008
|
+
// let's do the bare minimum check of comparing if an update
|
|
3009
|
+
// is needed using a last update cache:
|
|
3010
|
+
const previousUpdate = this._modifiedColumnLastUpdate.get(modified);
|
|
3011
|
+
if (
|
|
3012
|
+
previousUpdate?.date === modifiedDate &&
|
|
3013
|
+
previousUpdate?.style === modifiedStyle
|
|
3014
|
+
) {
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
2619
3017
|
|
|
2620
3018
|
const parsedDate = new Date(modifiedDate);
|
|
2621
3019
|
// Render the date in one of multiple formats, depending on the container's size
|
|
2622
|
-
modText = Time.formatHuman(parsedDate, modifiedStyle);
|
|
2623
|
-
modTitle = Time.format(parsedDate);
|
|
3020
|
+
const modText = Time.formatHuman(parsedDate, modifiedStyle);
|
|
3021
|
+
const modTitle = Time.format(parsedDate);
|
|
2624
3022
|
|
|
2625
3023
|
modified.textContent = modText;
|
|
2626
3024
|
modified.title = modTitle;
|
|
3025
|
+
this._modifiedColumnLastUpdate.set(modified, {
|
|
3026
|
+
date: modifiedDate,
|
|
3027
|
+
style: modifiedStyle
|
|
3028
|
+
});
|
|
2627
3029
|
}
|
|
2628
3030
|
|
|
2629
3031
|
/**
|
|
@@ -2643,7 +3045,8 @@ export namespace DirListing {
|
|
|
2643
3045
|
translator?: ITranslator,
|
|
2644
3046
|
hiddenColumns?: Set<DirListing.ToggleableColumn>,
|
|
2645
3047
|
selected?: boolean,
|
|
2646
|
-
modifiedStyle?: Time.HumanStyle
|
|
3048
|
+
modifiedStyle?: Time.HumanStyle,
|
|
3049
|
+
columnsSizes?: Record<DirListing.IColumn['id'], number | null>
|
|
2647
3050
|
): void {
|
|
2648
3051
|
if (selected) {
|
|
2649
3052
|
node.classList.add(SELECTED_CLASS);
|
|
@@ -2657,8 +3060,13 @@ export namespace DirListing {
|
|
|
2657
3060
|
|
|
2658
3061
|
const iconContainer = DOMUtils.findElement(node, ITEM_ICON_CLASS);
|
|
2659
3062
|
const text = DOMUtils.findElement(node, ITEM_TEXT_CLASS);
|
|
2660
|
-
const
|
|
2661
|
-
|
|
3063
|
+
const nameColumn = DOMUtils.findElement(node, ITEM_NAME_COLUMN_CLASS);
|
|
3064
|
+
let modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS) as
|
|
3065
|
+
| HTMLElement
|
|
3066
|
+
| undefined;
|
|
3067
|
+
let fileSize = DOMUtils.findElement(node, ITEM_FILE_SIZE_CLASS) as
|
|
3068
|
+
| HTMLElement
|
|
3069
|
+
| undefined;
|
|
2662
3070
|
const checkboxWrapper = DOMUtils.findElement(
|
|
2663
3071
|
node,
|
|
2664
3072
|
CHECKBOX_WRAPPER_CLASS
|
|
@@ -2669,19 +3077,23 @@ export namespace DirListing {
|
|
|
2669
3077
|
node.removeChild(checkboxWrapper);
|
|
2670
3078
|
} else if (showFileCheckboxes && !checkboxWrapper) {
|
|
2671
3079
|
const checkboxWrapper = this.createCheckboxWrapperNode();
|
|
2672
|
-
|
|
3080
|
+
nameColumn.insertAdjacentElement('beforebegin', checkboxWrapper);
|
|
2673
3081
|
}
|
|
2674
3082
|
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
3083
|
+
const showModified = !hiddenColumns?.has('last_modified');
|
|
3084
|
+
if (modified && !showModified) {
|
|
3085
|
+
node.removeChild(modified);
|
|
3086
|
+
} else if (showModified && !modified) {
|
|
3087
|
+
modified = this.itemFactories.last_modified();
|
|
3088
|
+
nameColumn.insertAdjacentElement('afterend', modified);
|
|
2679
3089
|
}
|
|
2680
3090
|
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
3091
|
+
const showFileSize = !hiddenColumns?.has('file_size');
|
|
3092
|
+
if (fileSize && !showFileSize) {
|
|
3093
|
+
node.removeChild(fileSize);
|
|
3094
|
+
} else if (showFileSize && !fileSize) {
|
|
3095
|
+
fileSize = this.itemFactories.file_size();
|
|
3096
|
+
(modified ?? nameColumn).insertAdjacentElement('afterend', fileSize);
|
|
2685
3097
|
}
|
|
2686
3098
|
|
|
2687
3099
|
// render the file item's icon
|
|
@@ -2698,12 +3110,14 @@ export namespace DirListing {
|
|
|
2698
3110
|
// add file size to pop up if its available
|
|
2699
3111
|
if (model.size !== null && model.size !== undefined) {
|
|
2700
3112
|
const fileSizeText = Private.formatFileSize(model.size, 1, 1024);
|
|
2701
|
-
fileSize
|
|
3113
|
+
if (fileSize) {
|
|
3114
|
+
fileSize.textContent = fileSizeText;
|
|
3115
|
+
}
|
|
2702
3116
|
hoverText += trans.__(
|
|
2703
3117
|
'\nSize: %1',
|
|
2704
3118
|
Private.formatFileSize(model.size, 1, 1024)
|
|
2705
3119
|
);
|
|
2706
|
-
} else {
|
|
3120
|
+
} else if (fileSize) {
|
|
2707
3121
|
fileSize.textContent = '';
|
|
2708
3122
|
}
|
|
2709
3123
|
if (model.path) {
|
|
@@ -2746,7 +3160,7 @@ export namespace DirListing {
|
|
|
2746
3160
|
// Adds an aria-label to the checkbox element.
|
|
2747
3161
|
const checkbox = checkboxWrapper?.querySelector(
|
|
2748
3162
|
'input[type="checkbox"]'
|
|
2749
|
-
) as HTMLInputElement;
|
|
3163
|
+
) as HTMLInputElement | undefined;
|
|
2750
3164
|
|
|
2751
3165
|
if (checkbox) {
|
|
2752
3166
|
let ariaLabel: string;
|
|
@@ -2763,7 +3177,36 @@ export namespace DirListing {
|
|
|
2763
3177
|
checkbox.checked = selected ?? false;
|
|
2764
3178
|
}
|
|
2765
3179
|
|
|
2766
|
-
|
|
3180
|
+
this.updateItemSize(node, model, modifiedStyle, columnsSizes);
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
/**
|
|
3184
|
+
* Update size of item nodes, assuming that model has not changed.
|
|
3185
|
+
*/
|
|
3186
|
+
updateItemSize(
|
|
3187
|
+
node: HTMLElement,
|
|
3188
|
+
model: Contents.IModel,
|
|
3189
|
+
modifiedStyle?: Time.HumanStyle,
|
|
3190
|
+
columnsSizes?: Record<DirListing.IColumn['id'], number | null>
|
|
3191
|
+
): void {
|
|
3192
|
+
if (columnsSizes) {
|
|
3193
|
+
for (const column of columns) {
|
|
3194
|
+
const element = DOMUtils.findElement(node, column.itemClassName);
|
|
3195
|
+
if (!element) {
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
const sizeSpec = columnsSizes[column.id];
|
|
3199
|
+
const newWidth = sizeSpec === null ? '' : sizeSpec + 'px';
|
|
3200
|
+
if (newWidth !== element.style.width) {
|
|
3201
|
+
element.style.width = newWidth;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
let modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS) as
|
|
3206
|
+
| HTMLElement
|
|
3207
|
+
| undefined;
|
|
3208
|
+
|
|
3209
|
+
if (model.last_modified && modified) {
|
|
2767
3210
|
this.updateItemModified(
|
|
2768
3211
|
modified,
|
|
2769
3212
|
model.last_modified,
|
|
@@ -2835,6 +3278,34 @@ export namespace DirListing {
|
|
|
2835
3278
|
return dragImage;
|
|
2836
3279
|
}
|
|
2837
3280
|
|
|
3281
|
+
/**
|
|
3282
|
+
* Factories for individual parts of the item.
|
|
3283
|
+
*/
|
|
3284
|
+
protected itemFactories = {
|
|
3285
|
+
name: () => {
|
|
3286
|
+
const name = document.createElement('span');
|
|
3287
|
+
const icon = document.createElement('span');
|
|
3288
|
+
const text = document.createElement('span');
|
|
3289
|
+
icon.className = ITEM_ICON_CLASS;
|
|
3290
|
+
text.className = ITEM_TEXT_CLASS;
|
|
3291
|
+
name.className = ITEM_NAME_COLUMN_CLASS;
|
|
3292
|
+
name.appendChild(icon);
|
|
3293
|
+
name.appendChild(text);
|
|
3294
|
+
return name;
|
|
3295
|
+
},
|
|
3296
|
+
last_modified: () => {
|
|
3297
|
+
const modified = document.createElement('span');
|
|
3298
|
+
modified.className = ITEM_MODIFIED_CLASS;
|
|
3299
|
+
return modified;
|
|
3300
|
+
},
|
|
3301
|
+
file_size: () => {
|
|
3302
|
+
const fileSize = document.createElement('span');
|
|
3303
|
+
fileSize.className = ITEM_FILE_SIZE_CLASS;
|
|
3304
|
+
return fileSize;
|
|
3305
|
+
},
|
|
3306
|
+
is_selected: () => this.createCheckboxWrapperNode()
|
|
3307
|
+
};
|
|
3308
|
+
|
|
2838
3309
|
/**
|
|
2839
3310
|
* Create a node for a header item.
|
|
2840
3311
|
*/
|
|
@@ -2873,6 +3344,14 @@ export namespace DirListing {
|
|
|
2873
3344
|
node.appendChild(icon);
|
|
2874
3345
|
return node;
|
|
2875
3346
|
}
|
|
3347
|
+
|
|
3348
|
+
/**
|
|
3349
|
+
* Register of most recent arguments for last modified column update.
|
|
3350
|
+
*/
|
|
3351
|
+
private _modifiedColumnLastUpdate = new WeakMap<
|
|
3352
|
+
HTMLElement,
|
|
3353
|
+
{ date: string; style: Time.HumanStyle }
|
|
3354
|
+
>();
|
|
2876
3355
|
}
|
|
2877
3356
|
|
|
2878
3357
|
/**
|
|
@@ -3017,6 +3496,24 @@ namespace Private {
|
|
|
3017
3496
|
return copy;
|
|
3018
3497
|
}
|
|
3019
3498
|
|
|
3499
|
+
/**
|
|
3500
|
+
* Check if the column is resizable.
|
|
3501
|
+
*/
|
|
3502
|
+
export const isResizable = (
|
|
3503
|
+
column: DirListing.IColumn
|
|
3504
|
+
): column is DirListing.IResizableColumn => {
|
|
3505
|
+
return 'resizable' in column && column.resizable;
|
|
3506
|
+
};
|
|
3507
|
+
|
|
3508
|
+
/**
|
|
3509
|
+
* Check if the column is sortable.
|
|
3510
|
+
*/
|
|
3511
|
+
export const isSortable = (
|
|
3512
|
+
column: DirListing.IColumn
|
|
3513
|
+
): column is DirListing.ISortableColumn => {
|
|
3514
|
+
return 'sortable' in column && column.sortable;
|
|
3515
|
+
};
|
|
3516
|
+
|
|
3020
3517
|
/**
|
|
3021
3518
|
* Get the index of the node at a client position, or `-1`.
|
|
3022
3519
|
*/
|
|
@@ -3066,13 +3563,87 @@ namespace Private {
|
|
|
3066
3563
|
(state === 'down' ? caretDownIcon : caretUpIcon).element({
|
|
3067
3564
|
container,
|
|
3068
3565
|
tag: 'span',
|
|
3069
|
-
stylesheet: 'listingHeaderItem'
|
|
3070
|
-
|
|
3071
|
-
float
|
|
3566
|
+
stylesheet: 'listingHeaderItem'
|
|
3072
3567
|
});
|
|
3568
|
+
if (float === 'left') {
|
|
3569
|
+
container.style.order = '-1';
|
|
3570
|
+
} else {
|
|
3571
|
+
container.style.order = '';
|
|
3572
|
+
}
|
|
3073
3573
|
} else {
|
|
3074
3574
|
LabIcon.remove(container);
|
|
3075
3575
|
container.className = HEADER_ITEM_ICON_CLASS;
|
|
3076
3576
|
}
|
|
3077
3577
|
}
|
|
3578
|
+
|
|
3579
|
+
export async function createDirectory(
|
|
3580
|
+
manager: IDocumentManager,
|
|
3581
|
+
path: string,
|
|
3582
|
+
name: string
|
|
3583
|
+
): Promise<string> {
|
|
3584
|
+
const model = await manager.newUntitled({
|
|
3585
|
+
path: path,
|
|
3586
|
+
type: 'directory'
|
|
3587
|
+
});
|
|
3588
|
+
const tmpDirPath = PathExt.join(path, model.name);
|
|
3589
|
+
const dirPath = PathExt.join(path, name);
|
|
3590
|
+
try {
|
|
3591
|
+
await manager.rename(tmpDirPath, dirPath);
|
|
3592
|
+
} catch (e) {
|
|
3593
|
+
// The `dirPath` already exists, remove the temporary new directory
|
|
3594
|
+
await manager.deleteFile(tmpDirPath);
|
|
3595
|
+
}
|
|
3596
|
+
return dirPath;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
export function isDirectoryEntry(
|
|
3600
|
+
entry: FileSystemEntry
|
|
3601
|
+
): entry is FileSystemDirectoryEntry {
|
|
3602
|
+
return entry.isDirectory;
|
|
3603
|
+
}
|
|
3604
|
+
export function isFileEntry(
|
|
3605
|
+
entry: FileSystemEntry
|
|
3606
|
+
): entry is FileSystemFileEntry {
|
|
3607
|
+
return entry.isFile;
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
export function defensiveGetAsEntry(
|
|
3611
|
+
item: DataTransferItem
|
|
3612
|
+
): FileSystemEntry | null {
|
|
3613
|
+
if (item.webkitGetAsEntry) {
|
|
3614
|
+
return item.webkitGetAsEntry();
|
|
3615
|
+
}
|
|
3616
|
+
if ('getAsEntry' in item) {
|
|
3617
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
|
|
3618
|
+
return (item['getAsEntry'] as () => FileSystemEntry | null)();
|
|
3619
|
+
}
|
|
3620
|
+
return null;
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
function readEntries(reader: FileSystemDirectoryReader) {
|
|
3624
|
+
return new Promise<FileSystemEntry[]>((resolve, reject) =>
|
|
3625
|
+
reader.readEntries(resolve, reject)
|
|
3626
|
+
);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
export function readFile(entry: FileSystemFileEntry) {
|
|
3630
|
+
return new Promise<File>((resolve, reject) => entry.file(resolve, reject));
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
export async function collectEntries(reader: FileSystemDirectoryReader) {
|
|
3634
|
+
// Spec requires calling `readEntries` until these are exhausted;
|
|
3635
|
+
// in practice this is only required in Chromium-based browsers for >100 files.
|
|
3636
|
+
// https://issues.chromium.org/issues/41110876
|
|
3637
|
+
const allEntries: FileSystemEntry[] = [];
|
|
3638
|
+
let done = false;
|
|
3639
|
+
while (!done) {
|
|
3640
|
+
const entries = await readEntries(reader);
|
|
3641
|
+
if (entries.length === 0) {
|
|
3642
|
+
done = true;
|
|
3643
|
+
} else {
|
|
3644
|
+
allEntries.push(...entries);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
return allEntries;
|
|
3648
|
+
}
|
|
3078
3649
|
}
|