UNPKG

90.6 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Dialog, DOMUtils, showDialog, showErrorMessage } from '@jupyterlab/apputils';
4import { PathExt, Time } from '@jupyterlab/coreutils';
5import { isValidFileName, renameFile } from '@jupyterlab/docmanager';
6import { DocumentRegistry } from '@jupyterlab/docregistry';
7import { nullTranslator } from '@jupyterlab/translation';
8import { caretDownIcon, caretUpIcon, classes, LabIcon } from '@jupyterlab/ui-components';
9import { ArrayExt, filter, StringExt } from '@lumino/algorithm';
10import { MimeData, PromiseDelegate } from '@lumino/coreutils';
11import { ElementExt } from '@lumino/domutils';
12import { Drag } from '@lumino/dragdrop';
13import { MessageLoop } from '@lumino/messaging';
14import { Signal } from '@lumino/signaling';
15import { h, VirtualDOM } from '@lumino/virtualdom';
16import { Widget } from '@lumino/widgets';
17/**
18 * The class name added to DirListing widget.
19 */
20const DIR_LISTING_CLASS = 'jp-DirListing';
21/**
22 * The class name added to a dir listing header node.
23 */
24const HEADER_CLASS = 'jp-DirListing-header';
25/**
26 * The class name added to a dir listing list header cell.
27 */
28const HEADER_ITEM_CLASS = 'jp-DirListing-headerItem';
29/**
30 * The class name added to a header cell text node.
31 */
32const HEADER_ITEM_TEXT_CLASS = 'jp-DirListing-headerItemText';
33/**
34 * The class name added to a header cell icon node.
35 */
36const HEADER_ITEM_ICON_CLASS = 'jp-DirListing-headerItemIcon';
37/**
38 * The class name added to the dir listing content node.
39 */
40const CONTENT_CLASS = 'jp-DirListing-content';
41/**
42 * The class name added to dir listing content item.
43 */
44const ITEM_CLASS = 'jp-DirListing-item';
45/**
46 * The class name added to the listing item text cell.
47 */
48const ITEM_TEXT_CLASS = 'jp-DirListing-itemText';
49/**
50 * The class name added to the listing item icon cell.
51 */
52const ITEM_ICON_CLASS = 'jp-DirListing-itemIcon';
53/**
54 * The class name added to the listing item modified cell.
55 */
56const ITEM_MODIFIED_CLASS = 'jp-DirListing-itemModified';
57/**
58 * The class name added to the listing item file size cell.
59 */
60const ITEM_FILE_SIZE_CLASS = 'jp-DirListing-itemFileSize';
61/**
62 * The class name added to the label element that wraps each item's checkbox and
63 * the header's check-all checkbox.
64 */
65const CHECKBOX_WRAPPER_CLASS = 'jp-DirListing-checkboxWrapper';
66/**
67 * The class name added to the dir listing editor node.
68 */
69const EDITOR_CLASS = 'jp-DirListing-editor';
70/**
71 * The class name added to the name column header cell.
72 */
73const NAME_ID_CLASS = 'jp-id-name';
74/**
75 * The class name added to the modified column header cell.
76 */
77const MODIFIED_ID_CLASS = 'jp-id-modified';
78/**
79 * The class name added to the file size column header cell.
80 */
81const FILE_SIZE_ID_CLASS = 'jp-id-filesize';
82/**
83 * The class name added to the narrow column header cell.
84 */
85const NARROW_ID_CLASS = 'jp-id-narrow';
86/**
87 * The class name added to the modified column header cell and modified item cell when hidden.
88 */
89const MODIFIED_COLUMN_HIDDEN = 'jp-LastModified-hidden';
90/**
91 * The class name added to the size column header cell and size item cell when hidden.
92 */
93const FILE_SIZE_COLUMN_HIDDEN = 'jp-FileSize-hidden';
94/**
95 * The mime type for a contents drag object.
96 */
97const CONTENTS_MIME = 'application/x-jupyter-icontents';
98/**
99 * The mime type for a rich contents drag object.
100 */
101const CONTENTS_MIME_RICH = 'application/x-jupyter-icontentsrich';
102/**
103 * The class name added to drop targets.
104 */
105const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
106/**
107 * The class name added to selected rows.
108 */
109const SELECTED_CLASS = 'jp-mod-selected';
110/**
111 * The class name added to drag state icons to add space between the icon and the file name
112 */
113const DRAG_ICON_CLASS = 'jp-DragIcon';
114/**
115 * The class name added to the widget when there are items on the clipboard.
116 */
117const CLIPBOARD_CLASS = 'jp-mod-clipboard';
118/**
119 * The class name added to cut rows.
120 */
121const CUT_CLASS = 'jp-mod-cut';
122/**
123 * The class name added when there are more than one selected rows.
124 */
125const MULTI_SELECTED_CLASS = 'jp-mod-multiSelected';
126/**
127 * The class name added to indicate running notebook.
128 */
129const RUNNING_CLASS = 'jp-mod-running';
130/**
131 * The class name added for a descending sort.
132 */
133const DESCENDING_CLASS = 'jp-mod-descending';
134/**
135 * The maximum duration between two key presses when selecting files by prefix.
136 */
137const PREFIX_APPEND_DURATION = 1000;
138/**
139 * The threshold in pixels to start a drag event.
140 */
141const DRAG_THRESHOLD = 5;
142/**
143 * A boolean indicating whether the platform is Mac.
144 */
145const IS_MAC = !!navigator.platform.match(/Mac/i);
146/**
147 * The factory MIME type supported by lumino dock panels.
148 */
149const FACTORY_MIME = 'application/vnd.lumino.widget-factory';
150/**
151 * A widget which hosts a file list area.
152 */
153export class DirListing extends Widget {
154 /**
155 * Construct a new file browser directory listing widget.
156 *
157 * @param model - The file browser view model.
158 */
159 constructor(options) {
160 super({
161 node: (options.renderer || DirListing.defaultRenderer).createNode()
162 });
163 this._items = [];
164 this._sortedItems = [];
165 this._sortState = {
166 direction: 'ascending',
167 key: 'name'
168 };
169 this._onItemOpened = new Signal(this);
170 this._drag = null;
171 this._dragData = null;
172 this._selectTimer = -1;
173 this._isCut = false;
174 this._prevPath = '';
175 this._clipboard = [];
176 this._softSelection = '';
177 this.selection = Object.create(null);
178 this._searchPrefix = '';
179 this._searchPrefixTimer = -1;
180 this._inRename = false;
181 this._isDirty = false;
182 this._hiddenColumns = new Set();
183 this._sortNotebooksFirst = false;
184 // _focusIndex should never be set outside the range [0, this._items.length - 1]
185 this._focusIndex = 0;
186 this.addClass(DIR_LISTING_CLASS);
187 this.translator = options.translator || nullTranslator;
188 this._trans = this.translator.load('jupyterlab');
189 this._model = options.model;
190 this._model.fileChanged.connect(this._onFileChanged, this);
191 this._model.refreshed.connect(this._onModelRefreshed, this);
192 this._model.pathChanged.connect(this._onPathChanged, this);
193 this._editNode = document.createElement('input');
194 this._editNode.className = EDITOR_CLASS;
195 this._manager = this._model.manager;
196 this._renderer = options.renderer || DirListing.defaultRenderer;
197 const headerNode = DOMUtils.findElement(this.node, HEADER_CLASS);
198 // hide the file size column by default
199 this._hiddenColumns.add('file_size');
200 this._renderer.populateHeaderNode(headerNode, this.translator, this._hiddenColumns);
201 this._manager.activateRequested.connect(this._onActivateRequested, this);
202 }
203 /**
204 * Dispose of the resources held by the directory listing.
205 */
206 dispose() {
207 this._items.length = 0;
208 this._sortedItems.length = 0;
209 this._clipboard.length = 0;
210 super.dispose();
211 }
212 /**
213 * Get the model used by the listing.
214 */
215 get model() {
216 return this._model;
217 }
218 /**
219 * Get the dir listing header node.
220 *
221 * #### Notes
222 * This is the node which holds the header cells.
223 *
224 * Modifying this node directly can lead to undefined behavior.
225 */
226 get headerNode() {
227 return DOMUtils.findElement(this.node, HEADER_CLASS);
228 }
229 /**
230 * Get the dir listing content node.
231 *
232 * #### Notes
233 * This is the node which holds the item nodes.
234 *
235 * Modifying this node directly can lead to undefined behavior.
236 */
237 get contentNode() {
238 return DOMUtils.findElement(this.node, CONTENT_CLASS);
239 }
240 /**
241 * The renderer instance used by the directory listing.
242 */
243 get renderer() {
244 return this._renderer;
245 }
246 /**
247 * The current sort state.
248 */
249 get sortState() {
250 return this._sortState;
251 }
252 /**
253 * A signal fired when an item is opened.
254 */
255 get onItemOpened() {
256 return this._onItemOpened;
257 }
258 /**
259 * Create an iterator over the listing's selected items.
260 *
261 * @returns A new iterator over the listing's selected items.
262 */
263 selectedItems() {
264 const items = this._sortedItems;
265 return filter(items, item => this.selection[item.path]);
266 }
267 /**
268 * Create an iterator over the listing's sorted items.
269 *
270 * @returns A new iterator over the listing's sorted items.
271 */
272 sortedItems() {
273 return this._sortedItems[Symbol.iterator]();
274 }
275 /**
276 * Sort the items using a sort condition.
277 */
278 sort(state) {
279 this._sortedItems = Private.sort(this.model.items(), state, this._sortNotebooksFirst);
280 this._sortState = state;
281 this.update();
282 }
283 /**
284 * Rename the first currently selected item.
285 *
286 * @returns A promise that resolves with the new name of the item.
287 */
288 rename() {
289 return this._doRename();
290 }
291 /**
292 * Cut the selected items.
293 */
294 cut() {
295 this._isCut = true;
296 this._copy();
297 this.update();
298 }
299 /**
300 * Copy the selected items.
301 */
302 copy() {
303 this._copy();
304 }
305 /**
306 * Paste the items from the clipboard.
307 *
308 * @returns A promise that resolves when the operation is complete.
309 */
310 paste() {
311 if (!this._clipboard.length) {
312 this._isCut = false;
313 return Promise.resolve(undefined);
314 }
315 const basePath = this._model.path;
316 const promises = [];
317 for (const path of this._clipboard) {
318 if (this._isCut) {
319 const localPath = this._manager.services.contents.localPath(path);
320 const parts = localPath.split('/');
321 const name = parts[parts.length - 1];
322 const newPath = PathExt.join(basePath, name);
323 promises.push(this._model.manager.rename(path, newPath));
324 }
325 else {
326 promises.push(this._model.manager.copy(path, basePath));
327 }
328 }
329 // Remove any cut modifiers.
330 for (const item of this._items) {
331 item.classList.remove(CUT_CLASS);
332 }
333 this._clipboard.length = 0;
334 this._isCut = false;
335 this.removeClass(CLIPBOARD_CLASS);
336 return Promise.all(promises)
337 .then(() => {
338 return undefined;
339 })
340 .catch(error => {
341 void showErrorMessage(this._trans._p('showErrorMessage', 'Paste Error'), error);
342 });
343 }
344 /**
345 * Delete the currently selected item(s).
346 *
347 * @returns A promise that resolves when the operation is complete.
348 */
349 async delete() {
350 const items = this._sortedItems.filter(item => this.selection[item.path]);
351 if (!items.length) {
352 return;
353 }
354 const message = items.length === 1
355 ? this._trans.__('Are you sure you want to permanently delete: %1?', items[0].name)
356 : this._trans._n('Are you sure you want to permanently delete the %1 selected item?', 'Are you sure you want to permanently delete the %1 selected items?', items.length);
357 const result = await showDialog({
358 title: this._trans.__('Delete'),
359 body: message,
360 buttons: [
361 Dialog.cancelButton({ label: this._trans.__('Cancel') }),
362 Dialog.warnButton({ label: this._trans.__('Delete') })
363 ],
364 // By default focus on "Cancel" to protect from accidental deletion
365 // ("delete" and "Enter" are next to each other on many keyboards).
366 defaultButton: 0
367 });
368 if (!this.isDisposed && result.button.accept) {
369 await this._delete(items.map(item => item.path));
370 }
371 // Re-focus
372 let focusIndex = this._focusIndex;
373 const lastIndexAfterDelete = this._sortedItems.length - items.length - 1;
374 if (focusIndex > lastIndexAfterDelete) {
375 // If the focus index after deleting items is out of bounds, set it to the
376 // last item.
377 focusIndex = Math.max(0, lastIndexAfterDelete);
378 }
379 this._focusItem(focusIndex);
380 }
381 /**
382 * Duplicate the currently selected item(s).
383 *
384 * @returns A promise that resolves when the operation is complete.
385 */
386 duplicate() {
387 const basePath = this._model.path;
388 const promises = [];
389 for (const item of this.selectedItems()) {
390 if (item.type !== 'directory') {
391 promises.push(this._model.manager.copy(item.path, basePath));
392 }
393 }
394 return Promise.all(promises)
395 .then(() => {
396 return undefined;
397 })
398 .catch(error => {
399 void showErrorMessage(this._trans._p('showErrorMessage', 'Duplicate file'), error);
400 });
401 }
402 /**
403 * Download the currently selected item(s).
404 */
405 async download() {
406 await Promise.all(Array.from(this.selectedItems())
407 .filter(item => item.type !== 'directory')
408 .map(item => this._model.download(item.path)));
409 }
410 /**
411 * Shut down kernels on the applicable currently selected items.
412 *
413 * @returns A promise that resolves when the operation is complete.
414 */
415 shutdownKernels() {
416 const model = this._model;
417 const items = this._sortedItems;
418 const paths = items.map(item => item.path);
419 const promises = Array.from(this._model.sessions())
420 .filter(session => {
421 const index = ArrayExt.firstIndexOf(paths, session.path);
422 return this.selection[items[index].path];
423 })
424 .map(session => model.manager.services.sessions.shutdown(session.id));
425 return Promise.all(promises)
426 .then(() => {
427 return undefined;
428 })
429 .catch(error => {
430 void showErrorMessage(this._trans._p('showErrorMessage', 'Shut down kernel'), error);
431 });
432 }
433 /**
434 * Select next item.
435 *
436 * @param keepExisting - Whether to keep the current selection and add to it.
437 */
438 selectNext(keepExisting = false) {
439 let index = -1;
440 const selected = Object.keys(this.selection);
441 const items = this._sortedItems;
442 if (selected.length === 1 || keepExisting) {
443 // Select the next item.
444 const path = selected[selected.length - 1];
445 index = ArrayExt.findFirstIndex(items, value => value.path === path);
446 index += 1;
447 if (index === this._items.length) {
448 index = 0;
449 }
450 }
451 else if (selected.length === 0) {
452 // Select the first item.
453 index = 0;
454 }
455 else {
456 // Select the last selected item.
457 const path = selected[selected.length - 1];
458 index = ArrayExt.findFirstIndex(items, value => value.path === path);
459 }
460 if (index !== -1) {
461 this._selectItem(index, keepExisting);
462 ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
463 }
464 }
465 /**
466 * Select previous item.
467 *
468 * @param keepExisting - Whether to keep the current selection and add to it.
469 */
470 selectPrevious(keepExisting = false) {
471 let index = -1;
472 const selected = Object.keys(this.selection);
473 const items = this._sortedItems;
474 if (selected.length === 1 || keepExisting) {
475 // Select the previous item.
476 const path = selected[0];
477 index = ArrayExt.findFirstIndex(items, value => value.path === path);
478 index -= 1;
479 if (index === -1) {
480 index = this._items.length - 1;
481 }
482 }
483 else if (selected.length === 0) {
484 // Select the last item.
485 index = this._items.length - 1;
486 }
487 else {
488 // Select the first selected item.
489 const path = selected[0];
490 index = ArrayExt.findFirstIndex(items, value => value.path === path);
491 }
492 if (index !== -1) {
493 this._selectItem(index, keepExisting);
494 ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
495 }
496 }
497 /**
498 * Select the first item that starts with prefix being typed.
499 */
500 selectByPrefix() {
501 const prefix = this._searchPrefix.toLowerCase();
502 const items = this._sortedItems;
503 const index = ArrayExt.findFirstIndex(items, value => {
504 return value.name.toLowerCase().substr(0, prefix.length) === prefix;
505 });
506 if (index !== -1) {
507 this._selectItem(index, false);
508 ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
509 }
510 }
511 /**
512 * Get whether an item is selected by name.
513 *
514 * @param name - The name of of the item.
515 *
516 * @returns Whether the item is selected.
517 */
518 isSelected(name) {
519 const items = this._sortedItems;
520 return (Array.from(filter(items, item => item.name === name && this.selection[item.path])).length !== 0);
521 }
522 /**
523 * Find a model given a click.
524 *
525 * @param event - The mouse event.
526 *
527 * @returns The model for the selected file.
528 */
529 modelForClick(event) {
530 const items = this._sortedItems;
531 const index = Private.hitTestNodes(this._items, event);
532 if (index !== -1) {
533 return items[index];
534 }
535 return undefined;
536 }
537 /**
538 * Clear the selected items.
539 */
540 clearSelectedItems() {
541 this.selection = Object.create(null);
542 }
543 /**
544 * Select an item by name.
545 *
546 * @param name - The name of the item to select.
547 * @param focus - Whether to move focus to the selected item.
548 *
549 * @returns A promise that resolves when the name is selected.
550 */
551 async selectItemByName(name, focus = false) {
552 return this._selectItemByName(name, focus);
553 }
554 /**
555 * Select an item by name.
556 *
557 * @param name - The name of the item to select.
558 * @param focus - Whether to move focus to the selected item.
559 * @param force - Whether to proceed with selection even if the file was already selected.
560 *
561 * @returns A promise that resolves when the name is selected.
562 */
563 async _selectItemByName(name, focus = false, force = false) {
564 if (!force && this.isSelected(name)) {
565 // Avoid API polling and DOM updates if already selected
566 return;
567 }
568 // Make sure the file is available.
569 await this.model.refresh();
570 if (this.isDisposed) {
571 throw new Error('File browser is disposed.');
572 }
573 const items = this._sortedItems;
574 const index = ArrayExt.findFirstIndex(items, value => value.name === name);
575 if (index === -1) {
576 throw new Error('Item does not exist.');
577 }
578 this._selectItem(index, false, focus);
579 MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
580 ElementExt.scrollIntoViewIfNeeded(this.contentNode, this._items[index]);
581 }
582 /**
583 * Handle the DOM events for the directory listing.
584 *
585 * @param event - The DOM event sent to the widget.
586 *
587 * #### Notes
588 * This method implements the DOM `EventListener` interface and is
589 * called in response to events on the panel's DOM node. It should
590 * not be called directly by user code.
591 */
592 handleEvent(event) {
593 switch (event.type) {
594 case 'mousedown':
595 this._evtMousedown(event);
596 break;
597 case 'mouseup':
598 this._evtMouseup(event);
599 break;
600 case 'mousemove':
601 this._evtMousemove(event);
602 break;
603 case 'keydown':
604 this.evtKeydown(event);
605 break;
606 case 'click':
607 this._evtClick(event);
608 break;
609 case 'dblclick':
610 this.evtDblClick(event);
611 break;
612 case 'dragenter':
613 case 'dragover':
614 this.addClass('jp-mod-native-drop');
615 event.preventDefault();
616 break;
617 case 'dragleave':
618 case 'dragend':
619 this.removeClass('jp-mod-native-drop');
620 break;
621 case 'drop':
622 this.removeClass('jp-mod-native-drop');
623 this.evtNativeDrop(event);
624 break;
625 case 'scroll':
626 this._evtScroll(event);
627 break;
628 case 'lm-dragenter':
629 this.evtDragEnter(event);
630 break;
631 case 'lm-dragleave':
632 this.evtDragLeave(event);
633 break;
634 case 'lm-dragover':
635 this.evtDragOver(event);
636 break;
637 case 'lm-drop':
638 this.evtDrop(event);
639 break;
640 default:
641 break;
642 }
643 }
644 /**
645 * A message handler invoked on an `'after-attach'` message.
646 */
647 onAfterAttach(msg) {
648 super.onAfterAttach(msg);
649 const node = this.node;
650 const content = DOMUtils.findElement(node, CONTENT_CLASS);
651 node.addEventListener('mousedown', this);
652 node.addEventListener('keydown', this);
653 node.addEventListener('click', this);
654 node.addEventListener('dblclick', this);
655 content.addEventListener('dragenter', this);
656 content.addEventListener('dragover', this);
657 content.addEventListener('dragleave', this);
658 content.addEventListener('dragend', this);
659 content.addEventListener('drop', this);
660 content.addEventListener('scroll', this);
661 content.addEventListener('lm-dragenter', this);
662 content.addEventListener('lm-dragleave', this);
663 content.addEventListener('lm-dragover', this);
664 content.addEventListener('lm-drop', this);
665 }
666 /**
667 * A message handler invoked on a `'before-detach'` message.
668 */
669 onBeforeDetach(msg) {
670 super.onBeforeDetach(msg);
671 const node = this.node;
672 const content = DOMUtils.findElement(node, CONTENT_CLASS);
673 node.removeEventListener('mousedown', this);
674 node.removeEventListener('keydown', this);
675 node.removeEventListener('click', this);
676 node.removeEventListener('dblclick', this);
677 content.removeEventListener('scroll', this);
678 content.removeEventListener('dragover', this);
679 content.removeEventListener('dragover', this);
680 content.removeEventListener('dragleave', this);
681 content.removeEventListener('dragend', this);
682 content.removeEventListener('drop', this);
683 content.removeEventListener('lm-dragenter', this);
684 content.removeEventListener('lm-dragleave', this);
685 content.removeEventListener('lm-dragover', this);
686 content.removeEventListener('lm-drop', this);
687 document.removeEventListener('mousemove', this, true);
688 document.removeEventListener('mouseup', this, true);
689 }
690 /**
691 * A message handler invoked on an `'after-show'` message.
692 */
693 onAfterShow(msg) {
694 if (this._isDirty) {
695 // Update the sorted items.
696 this.sort(this.sortState);
697 this.update();
698 }
699 }
700 /**
701 * A handler invoked on an `'update-request'` message.
702 */
703 onUpdateRequest(msg) {
704 var _a;
705 this._isDirty = false;
706 // Fetch common variables.
707 const items = this._sortedItems;
708 const nodes = this._items;
709 const content = DOMUtils.findElement(this.node, CONTENT_CLASS);
710 const renderer = this._renderer;
711 this.removeClass(MULTI_SELECTED_CLASS);
712 this.removeClass(SELECTED_CLASS);
713 // Remove any excess item nodes.
714 while (nodes.length > items.length) {
715 content.removeChild(nodes.pop());
716 }
717 // Add any missing item nodes.
718 while (nodes.length < items.length) {
719 const node = renderer.createItemNode(this._hiddenColumns);
720 node.classList.add(ITEM_CLASS);
721 nodes.push(node);
722 content.appendChild(node);
723 }
724 nodes.forEach((node, i) => {
725 // Remove extra classes from the nodes.
726 node.classList.remove(SELECTED_CLASS);
727 node.classList.remove(RUNNING_CLASS);
728 node.classList.remove(CUT_CLASS);
729 // Uncheck each file checkbox
730 const checkbox = renderer.getCheckboxNode(node);
731 if (checkbox) {
732 checkbox.checked = false;
733 }
734 // Handle `tabIndex`
735 const nameNode = renderer.getNameNode(node);
736 if (nameNode) {
737 // Must check if the name node is there because it gets replaced by the
738 // edit node when editing the name of the file or directory.
739 nameNode.tabIndex = i === this._focusIndex ? 0 : -1;
740 }
741 });
742 // Put the check-all checkbox in the header into the correct state
743 const checkAllCheckbox = renderer.getCheckboxNode(this.headerNode);
744 if (checkAllCheckbox) {
745 const totalSelected = Object.keys(this.selection).length;
746 const allSelected = items.length > 0 && totalSelected === items.length;
747 const someSelected = !allSelected && totalSelected > 0;
748 checkAllCheckbox.checked = allSelected;
749 checkAllCheckbox.indeterminate = someSelected;
750 // Stash the state in data attributes so we can access them in the click
751 // handler (because in the click handler, checkbox.checked and
752 // checkbox.indeterminate do not hold the previous value; they hold the
753 // next value).
754 checkAllCheckbox.dataset.checked = String(allSelected);
755 checkAllCheckbox.dataset.indeterminate = String(someSelected);
756 const trans = this.translator.load('jupyterlab');
757 checkAllCheckbox === null || checkAllCheckbox === void 0 ? void 0 : checkAllCheckbox.setAttribute('aria-label', allSelected || someSelected
758 ? trans.__('Deselect all files and directories')
759 : trans.__('Select all files and directories'));
760 }
761 // Update item nodes based on widget state.
762 items.forEach((item, i) => {
763 const node = nodes[i];
764 const ft = this._manager.registry.getFileTypeForModel(item);
765 renderer.updateItemNode(node, item, ft, this.translator, this._hiddenColumns, this.selection[item.path]);
766 if (this.selection[item.path] &&
767 this._isCut &&
768 this._model.path === this._prevPath) {
769 node.classList.add(CUT_CLASS);
770 }
771 // add metadata to the node
772 node.setAttribute('data-isdir', item.type === 'directory' ? 'true' : 'false');
773 });
774 // Handle the selectors on the widget node.
775 const selected = Object.keys(this.selection).length;
776 if (selected) {
777 this.addClass(SELECTED_CLASS);
778 if (selected > 1) {
779 this.addClass(MULTI_SELECTED_CLASS);
780 }
781 }
782 // Handle file session statuses.
783 const paths = items.map(item => item.path);
784 for (const session of this._model.sessions()) {
785 const index = ArrayExt.firstIndexOf(paths, session.path);
786 const node = nodes[index];
787 // Node may have been filtered out.
788 if (node) {
789 let name = (_a = session.kernel) === null || _a === void 0 ? void 0 : _a.name;
790 const specs = this._model.specs;
791 node.classList.add(RUNNING_CLASS);
792 if (specs && name) {
793 const spec = specs.kernelspecs[name];
794 name = spec ? spec.display_name : this._trans.__('unknown');
795 }
796 node.title = this._trans.__('%1\nKernel: %2', node.title, name);
797 }
798 }
799 this._prevPath = this._model.path;
800 }
801 onResize(msg) {
802 const { width } = msg.width === -1 ? this.node.getBoundingClientRect() : msg;
803 this.toggleClass('jp-DirListing-narrow', width < 250);
804 }
805 setColumnVisibility(name, visible) {
806 if (visible) {
807 this._hiddenColumns.delete(name);
808 }
809 else {
810 this._hiddenColumns.add(name);
811 }
812 this.headerNode.innerHTML = '';
813 this._renderer.populateHeaderNode(this.headerNode, this.translator, this._hiddenColumns);
814 }
815 /**
816 * Update the setting to sort notebooks above files.
817 * This sorts the items again if the internal value is modified.
818 */
819 setNotebooksFirstSorting(isEnabled) {
820 let previousValue = this._sortNotebooksFirst;
821 this._sortNotebooksFirst = isEnabled;
822 if (this._sortNotebooksFirst !== previousValue) {
823 this.sort(this._sortState);
824 }
825 }
826 /**
827 * Would this click (or other event type) hit the checkbox by default?
828 */
829 isWithinCheckboxHitArea(event) {
830 let element = event.target;
831 while (element) {
832 if (element.classList.contains(CHECKBOX_WRAPPER_CLASS)) {
833 return true;
834 }
835 element = element.parentElement;
836 }
837 return false;
838 }
839 /**
840 * Handle the `'click'` event for the widget.
841 */
842 _evtClick(event) {
843 const target = event.target;
844 const header = this.headerNode;
845 const renderer = this._renderer;
846 if (header.contains(target)) {
847 const checkbox = renderer.getCheckboxNode(header);
848 if (checkbox && this.isWithinCheckboxHitArea(event)) {
849 const previouslyUnchecked = checkbox.dataset.indeterminate === 'false' &&
850 checkbox.dataset.checked === 'false';
851 // The only time a click on the check-all checkbox should check all is
852 // when it was previously unchecked; otherwise, if the checkbox was
853 // either checked (all selected) or indeterminate (some selected), the
854 // click should clear all.
855 if (previouslyUnchecked) {
856 // Select all items
857 this._sortedItems.forEach((item) => (this.selection[item.path] = true));
858 }
859 else {
860 // Unselect all items
861 this.clearSelectedItems();
862 }
863 this.update();
864 }
865 else {
866 const state = this.renderer.handleHeaderClick(header, event);
867 if (state) {
868 this.sort(state);
869 }
870 }
871 return;
872 }
873 else {
874 // Focus the selected file on click to ensure a couple of things:
875 // 1. If a user clicks on the item node, its name node will receive focus.
876 // 2. If a user clicks on blank space in the directory listing, the
877 // previously focussed item will be focussed.
878 this._focusItem(this._focusIndex);
879 }
880 }
881 /**
882 * Handle the `'scroll'` event for the widget.
883 */
884 _evtScroll(event) {
885 this.headerNode.scrollLeft = this.contentNode.scrollLeft;
886 }
887 /**
888 * Handle the `'mousedown'` event for the widget.
889 */
890 _evtMousedown(event) {
891 // Bail if clicking within the edit node
892 if (event.target === this._editNode) {
893 return;
894 }
895 // Blur the edit node if necessary.
896 if (this._editNode.parentNode) {
897 if (this._editNode !== event.target) {
898 this._editNode.focus();
899 this._editNode.blur();
900 clearTimeout(this._selectTimer);
901 }
902 else {
903 return;
904 }
905 }
906 let index = Private.hitTestNodes(this._items, event);
907 if (index === -1) {
908 return;
909 }
910 this.handleFileSelect(event);
911 if (event.button !== 0) {
912 clearTimeout(this._selectTimer);
913 }
914 // Check for clearing a context menu.
915 const newContext = (IS_MAC && event.ctrlKey) || event.button === 2;
916 if (newContext) {
917 return;
918 }
919 // Left mouse press for drag start.
920 if (event.button === 0) {
921 this._dragData = {
922 pressX: event.clientX,
923 pressY: event.clientY,
924 index: index
925 };
926 document.addEventListener('mouseup', this, true);
927 document.addEventListener('mousemove', this, true);
928 }
929 }
930 /**
931 * Handle the `'mouseup'` event for the widget.
932 */
933 _evtMouseup(event) {
934 // Handle any soft selection from the previous mouse down.
935 if (this._softSelection) {
936 const altered = event.metaKey || event.shiftKey || event.ctrlKey;
937 // See if we need to clear the other selection.
938 if (!altered && event.button === 0) {
939 this.clearSelectedItems();
940 this.selection[this._softSelection] = true;
941 this.update();
942 }
943 this._softSelection = '';
944 }
945 // Re-focus. This is needed because nodes corresponding to files selected in
946 // mousedown handler will not retain the focus as mousedown event is always
947 // followed by a blur/focus event.
948 if (event.button === 0) {
949 this._focusItem(this._focusIndex);
950 }
951 // Remove the drag listeners if necessary.
952 if (event.button !== 0 || !this._drag) {
953 document.removeEventListener('mousemove', this, true);
954 document.removeEventListener('mouseup', this, true);
955 return;
956 }
957 event.preventDefault();
958 event.stopPropagation();
959 }
960 /**
961 * Handle the `'mousemove'` event for the widget.
962 */
963 _evtMousemove(event) {
964 event.preventDefault();
965 event.stopPropagation();
966 // Bail if we are the one dragging.
967 if (this._drag || !this._dragData) {
968 return;
969 }
970 // Check for a drag initialization.
971 const data = this._dragData;
972 const dx = Math.abs(event.clientX - data.pressX);
973 const dy = Math.abs(event.clientY - data.pressY);
974 if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
975 return;
976 }
977 this._startDrag(data.index, event.clientX, event.clientY);
978 }
979 /**
980 * Handle the opening of an item.
981 */
982 handleOpen(item) {
983 this._onItemOpened.emit(item);
984 if (item.type === 'directory') {
985 const localPath = this._manager.services.contents.localPath(item.path);
986 this._model
987 .cd(`/${localPath}`)
988 .catch(error => showErrorMessage(this._trans._p('showErrorMessage', 'Open directory'), error));
989 }
990 else {
991 const path = item.path;
992 this._manager.openOrReveal(path);
993 }
994 }
995 /**
996 * Calculate the next focus index, given the current focus index and a
997 * direction, keeping within the bounds of the directory listing.
998 *
999 * @param index Current focus index
1000 * @param direction -1 (up) or 1 (down)
1001 * @returns The next focus index, which could be the same as the current focus
1002 * index if at the boundary.
1003 */
1004 _getNextFocusIndex(index, direction) {
1005 const nextIndex = index + direction;
1006 if (nextIndex === -1 || nextIndex === this._items.length) {
1007 // keep focus index within bounds
1008 return index;
1009 }
1010 else {
1011 return nextIndex;
1012 }
1013 }
1014 /**
1015 * Handle the up or down arrow key.
1016 *
1017 * @param event The keyboard event
1018 * @param direction -1 (up) or 1 (down)
1019 */
1020 _handleArrowY(event, direction) {
1021 // We only handle the `ctrl` and `shift` modifiers. If other modifiers are
1022 // present, then do nothing.
1023 if (event.altKey || event.metaKey) {
1024 return;
1025 }
1026 // If folder is empty, there's nothing to do with the up/down key.
1027 if (!this._items.length) {
1028 return;
1029 }
1030 // Don't handle the arrow key press if it's not on directory item. This
1031 // avoids a confusing user experience that can result from when the user
1032 // moves the selection and focus index apart (via ctrl + up/down). The last
1033 // selected item remains highlighted but the last focussed item loses its
1034 // focus ring if it's not actively focussed. This forces the user to
1035 // visibly reveal the last focussed item before moving the focus.
1036 if (!event.target.classList.contains(ITEM_TEXT_CLASS)) {
1037 return;
1038 }
1039 event.stopPropagation();
1040 event.preventDefault();
1041 const focusIndex = this._focusIndex;
1042 let nextFocusIndex = this._getNextFocusIndex(focusIndex, direction);
1043 // The following if-block allows the first press of the down arrow to select
1044 // the first (rather than the second) file/directory in the list. This is
1045 // the situation when the page first loads or when a user changes directory.
1046 if (direction > 0 &&
1047 focusIndex === 0 &&
1048 !event.ctrlKey &&
1049 Object.keys(this.selection).length === 0) {
1050 nextFocusIndex = 0;
1051 }
1052 // Shift key indicates multi-selection. Either the user is trying to grow
1053 // the selection, or shrink it.
1054 if (event.shiftKey) {
1055 this._handleMultiSelect(nextFocusIndex);
1056 }
1057 else if (!event.ctrlKey) {
1058 // If neither the shift nor ctrl keys were used with the up/down arrow,
1059 // then we treat it as a normal, unmodified key press and select the
1060 // next item.
1061 this._selectItem(nextFocusIndex, event.shiftKey, false /* focus = false because we call focus method directly following this */);
1062 }
1063 this._focusItem(nextFocusIndex);
1064 this.update();
1065 }
1066 /**
1067 * cd ..
1068 *
1069 * Go up one level in the directory tree.
1070 */
1071 async goUp() {
1072 const model = this.model;
1073 if (model.path === model.rootPath) {
1074 return;
1075 }
1076 try {
1077 await model.cd('..');
1078 }
1079 catch (reason) {
1080 console.warn(`Failed to go to parent directory of ${model.path}`, reason);
1081 }
1082 }
1083 /**
1084 * Handle the `'keydown'` event for the widget.
1085 */
1086 evtKeydown(event) {
1087 // Do not handle any keydown events here if in the middle of a file rename.
1088 if (this._inRename) {
1089 return;
1090 }
1091 switch (event.keyCode) {
1092 case 13: {
1093 // Enter
1094 // Do nothing if any modifier keys are pressed.
1095 if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
1096 return;
1097 }
1098 event.preventDefault();
1099 event.stopPropagation();
1100 for (const item of this.selectedItems()) {
1101 this.handleOpen(item);
1102 }
1103 return;
1104 }
1105 case 38:
1106 // Up arrow
1107 this._handleArrowY(event, -1);
1108 return;
1109 case 40:
1110 // Down arrow
1111 this._handleArrowY(event, 1);
1112 return;
1113 case 32: {
1114 // Space
1115 if (event.ctrlKey) {
1116 // Follow the Windows and Ubuntu convention: you must press `ctrl` +
1117 // `space` in order to toggle whether an item is selected.
1118 // However, do not handle if any other modifiers were pressed.
1119 if (event.metaKey || event.shiftKey || event.altKey) {
1120 return;
1121 }
1122 // Make sure the ctrl+space key stroke was on a valid, focussed target.
1123 const node = this._items[this._focusIndex];
1124 if (!(
1125 // Event must have occurred within a node whose item can be toggled.
1126 (node.contains(event.target) &&
1127 // That node must also contain the currently focussed element.
1128 node.contains(document.activeElement)))) {
1129 return;
1130 }
1131 event.stopPropagation();
1132 // Prevent default, otherwise the container will scroll.
1133 event.preventDefault();
1134 // Toggle item selected
1135 const { path } = this._sortedItems[this._focusIndex];
1136 if (this.selection[path]) {
1137 delete this.selection[path];
1138 }
1139 else {
1140 this.selection[path] = true;
1141 }
1142 this.update();
1143 // Key was handled, so return.
1144 return;
1145 }
1146 break;
1147 }
1148 }
1149 // Detects printable characters typed by the user.
1150 // Not all browsers support .key, but it discharges us from reconstructing
1151 // characters from key codes.
1152 if (event.key !== undefined &&
1153 event.key.length === 1 &&
1154 // Don't gobble up the space key on the check-all checkbox (which the
1155 // browser treats as a click event).
1156 !((event.key === ' ' || event.keyCode === 32) &&
1157 event.target.type === 'checkbox')) {
1158 if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
1159 return;
1160 }
1161 this._searchPrefix += event.key;
1162 clearTimeout(this._searchPrefixTimer);
1163 this._searchPrefixTimer = window.setTimeout(() => {
1164 this._searchPrefix = '';
1165 }, PREFIX_APPEND_DURATION);
1166 this.selectByPrefix();
1167 event.stopPropagation();
1168 event.preventDefault();
1169 }
1170 }
1171 /**
1172 * Handle the `'dblclick'` event for the widget.
1173 */
1174 evtDblClick(event) {
1175 // Do nothing if it's not a left mouse press.
1176 if (event.button !== 0) {
1177 return;
1178 }
1179 // Do nothing if any modifier keys are pressed.
1180 if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
1181 return;
1182 }
1183 // Do nothing if the double click is on a checkbox. (Otherwise a rapid
1184 // check-uncheck on the checkbox will cause the adjacent file/folder to
1185 // open, which is probably not what the user intended.)
1186 if (this.isWithinCheckboxHitArea(event)) {
1187 return;
1188 }
1189 // Stop the event propagation.
1190 event.preventDefault();
1191 event.stopPropagation();
1192 clearTimeout(this._selectTimer);
1193 this._editNode.blur();
1194 // Find a valid double click target.
1195 const target = event.target;
1196 const i = ArrayExt.findFirstIndex(this._items, node => node.contains(target));
1197 if (i === -1) {
1198 return;
1199 }
1200 const item = this._sortedItems[i];
1201 this.handleOpen(item);
1202 }
1203 /**
1204 * Handle the `drop` event for the widget.
1205 */
1206 evtNativeDrop(event) {
1207 var _a, _b, _c;
1208 const files = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files;
1209 if (!files || files.length === 0) {
1210 return;
1211 }
1212 const length = (_b = event.dataTransfer) === null || _b === void 0 ? void 0 : _b.items.length;
1213 if (!length) {
1214 return;
1215 }
1216 for (let i = 0; i < length; i++) {
1217 let entry = (_c = event.dataTransfer) === null || _c === void 0 ? void 0 : _c.items[i].webkitGetAsEntry();
1218 if (entry === null || entry === void 0 ? void 0 : entry.isDirectory) {
1219 console.log('currently not supporting drag + drop for folders');
1220 void showDialog({
1221 title: this._trans.__('Error Uploading Folder'),
1222 body: this._trans.__('Drag and Drop is currently not supported for folders'),
1223 buttons: [Dialog.cancelButton({ label: this._trans.__('Close') })]
1224 });
1225 }
1226 }
1227 event.preventDefault();
1228 for (let i = 0; i < files.length; i++) {
1229 void this._model.upload(files[i]);
1230 }
1231 }
1232 /**
1233 * Handle the `'lm-dragenter'` event for the widget.
1234 */
1235 evtDragEnter(event) {
1236 if (event.mimeData.hasData(CONTENTS_MIME)) {
1237 const index = Private.hitTestNodes(this._items, event);
1238 if (index === -1) {
1239 return;
1240 }
1241 const item = this._sortedItems[index];
1242 if (item.type !== 'directory' || this.selection[item.path]) {
1243 return;
1244 }
1245 const target = event.target;
1246 target.classList.add(DROP_TARGET_CLASS);
1247 event.preventDefault();
1248 event.stopPropagation();
1249 }
1250 }
1251 /**
1252 * Handle the `'lm-dragleave'` event for the widget.
1253 */
1254 evtDragLeave(event) {
1255 event.preventDefault();
1256 event.stopPropagation();
1257 const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
1258 if (dropTarget) {
1259 dropTarget.classList.remove(DROP_TARGET_CLASS);
1260 }
1261 }
1262 /**
1263 * Handle the `'lm-dragover'` event for the widget.
1264 */
1265 evtDragOver(event) {
1266 event.preventDefault();
1267 event.stopPropagation();
1268 event.dropAction = event.proposedAction;
1269 const dropTarget = DOMUtils.findElement(this.node, DROP_TARGET_CLASS);
1270 if (dropTarget) {
1271 dropTarget.classList.remove(DROP_TARGET_CLASS);
1272 }
1273 const index = Private.hitTestNodes(this._items, event);
1274 this._items[index].classList.add(DROP_TARGET_CLASS);
1275 }
1276 /**
1277 * Handle the `'lm-drop'` event for the widget.
1278 */
1279 evtDrop(event) {
1280 event.preventDefault();
1281 event.stopPropagation();
1282 clearTimeout(this._selectTimer);
1283 if (event.proposedAction === 'none') {
1284 event.dropAction = 'none';
1285 return;
1286 }
1287 if (!event.mimeData.hasData(CONTENTS_MIME)) {
1288 return;
1289 }
1290 let target = event.target;
1291 while (target && target.parentElement) {
1292 if (target.classList.contains(DROP_TARGET_CLASS)) {
1293 target.classList.remove(DROP_TARGET_CLASS);
1294 break;
1295 }
1296 target = target.parentElement;
1297 }
1298 // Get the path based on the target node.
1299 const index = ArrayExt.firstIndexOf(this._items, target);
1300 const items = this._sortedItems;
1301 let basePath = this._model.path;
1302 if (items[index].type === 'directory') {
1303 basePath = PathExt.join(basePath, items[index].name);
1304 }
1305 const manager = this._manager;
1306 // Handle the items.
1307 const promises = [];
1308 const paths = event.mimeData.getData(CONTENTS_MIME);
1309 if (event.ctrlKey && event.proposedAction === 'move') {
1310 event.dropAction = 'copy';
1311 }
1312 else {
1313 event.dropAction = event.proposedAction;
1314 }
1315 for (const path of paths) {
1316 const localPath = manager.services.contents.localPath(path);
1317 const name = PathExt.basename(localPath);
1318 const newPath = PathExt.join(basePath, name);
1319 // Skip files that are not moving.
1320 if (newPath === path) {
1321 continue;
1322 }
1323 if (event.dropAction === 'copy') {
1324 promises.push(manager.copy(path, basePath));
1325 }
1326 else {
1327 promises.push(renameFile(manager, path, newPath));
1328 }
1329 }
1330 Promise.all(promises).catch(error => {
1331 void showErrorMessage(this._trans._p('showErrorMessage', 'Error while copying/moving files'), error);
1332 });
1333 }
1334 /**
1335 * Start a drag event.
1336 */
1337 _startDrag(index, clientX, clientY) {
1338 let selectedPaths = Object.keys(this.selection);
1339 const source = this._items[index];
1340 const items = this._sortedItems;
1341 let selectedItems;
1342 let item;
1343 // If the source node is not selected, use just that node.
1344 if (!source.classList.contains(SELECTED_CLASS)) {
1345 item = items[index];
1346 selectedPaths = [item.path];
1347 selectedItems = [item];
1348 }
1349 else {
1350 const path = selectedPaths[0];
1351 item = items.find(value => value.path === path);
1352 selectedItems = this.selectedItems();
1353 }
1354 if (!item) {
1355 return;
1356 }
1357 // Create the drag image.
1358 const ft = this._manager.registry.getFileTypeForModel(item);
1359 const dragImage = this.renderer.createDragImage(source, selectedPaths.length, this._trans, ft);
1360 // Set up the drag event.
1361 this._drag = new Drag({
1362 dragImage,
1363 mimeData: new MimeData(),
1364 supportedActions: 'move',
1365 proposedAction: 'move'
1366 });
1367 this._drag.mimeData.setData(CONTENTS_MIME, selectedPaths);
1368 // Add thunks for getting mime data content.
1369 // We thunk the content so we don't try to make a network call
1370 // when it's not needed. E.g. just moving files around
1371 // in a filebrowser
1372 const services = this.model.manager.services;
1373 for (const item of selectedItems) {
1374 this._drag.mimeData.setData(CONTENTS_MIME_RICH, {
1375 model: item,
1376 withContent: async () => {
1377 return await services.contents.get(item.path);
1378 }
1379 });
1380 }
1381 if (item && item.type !== 'directory') {
1382 const otherPaths = selectedPaths.slice(1).reverse();
1383 this._drag.mimeData.setData(FACTORY_MIME, () => {
1384 if (!item) {
1385 return;
1386 }
1387 const path = item.path;
1388 let widget = this._manager.findWidget(path);
1389 if (!widget) {
1390 widget = this._manager.open(item.path);
1391 }
1392 if (otherPaths.length) {
1393 const firstWidgetPlaced = new PromiseDelegate();
1394 void firstWidgetPlaced.promise.then(() => {
1395 let prevWidget = widget;
1396 otherPaths.forEach(path => {
1397 const options = {
1398 ref: prevWidget === null || prevWidget === void 0 ? void 0 : prevWidget.id,
1399 mode: 'tab-after'
1400 };
1401 prevWidget = this._manager.openOrReveal(path, void 0, void 0, options);
1402 this._manager.openOrReveal(item.path);
1403 });
1404 });
1405 firstWidgetPlaced.resolve(void 0);
1406 }
1407 return widget;
1408 });
1409 }
1410 // Start the drag and remove the mousemove and mouseup listeners.
1411 document.removeEventListener('mousemove', this, true);
1412 document.removeEventListener('mouseup', this, true);
1413 clearTimeout(this._selectTimer);
1414 void this._drag.start(clientX, clientY).then(action => {
1415 this._drag = null;
1416 clearTimeout(this._selectTimer);
1417 });
1418 }
1419 /**
1420 * Handle selection on a file node.
1421 */
1422 handleFileSelect(event) {
1423 // Fetch common variables.
1424 const items = this._sortedItems;
1425 const index = Private.hitTestNodes(this._items, event);
1426 clearTimeout(this._selectTimer);
1427 if (index === -1) {
1428 return;
1429 }
1430 // Clear any existing soft selection.
1431 this._softSelection = '';
1432 const path = items[index].path;
1433 const selected = Object.keys(this.selection);
1434 const isLeftClickOnCheckbox = event.button === 0 &&
1435 // On Mac, a left-click with the ctrlKey is treated as a right-click.
1436 !(IS_MAC && event.ctrlKey) &&
1437 this.isWithinCheckboxHitArea(event);
1438 // Handle toggling.
1439 if ((IS_MAC && event.metaKey) ||
1440 (!IS_MAC && event.ctrlKey) ||
1441 isLeftClickOnCheckbox) {
1442 if (this.selection[path]) {
1443 delete this.selection[path];
1444 }
1445 else {
1446 this.selection[path] = true;
1447 }
1448 this._focusItem(index);
1449 // Handle multiple select.
1450 }
1451 else if (event.shiftKey) {
1452 this._handleMultiSelect(index);
1453 this._focusItem(index);
1454 // Handle a 'soft' selection
1455 }
1456 else if (path in this.selection && selected.length > 1) {
1457 this._softSelection = path;
1458 // Default to selecting the only the item.
1459 }
1460 else {
1461 // Select only the given item.
1462 return this._selectItem(index, false, true);
1463 }
1464 this.update();
1465 }
1466 /**
1467 * (Re-)focus an item in the directory listing.
1468 *
1469 * @param index The index of the item node to focus
1470 */
1471 _focusItem(index) {
1472 const items = this._items;
1473 if (items.length === 0) {
1474 // Focus the top node if the folder is empty and therefore there are no
1475 // items inside the folder to focus.
1476 this._focusIndex = 0;
1477 this.node.focus();
1478 return;
1479 }
1480 this._focusIndex = index;
1481 const node = items[index];
1482 const nameNode = this.renderer.getNameNode(node);
1483 if (nameNode) {
1484 // Make the filename text node focusable so that it receives keyboard
1485 // events; text node was specifically chosen to receive shortcuts because
1486 // it gets substituted with input element during file name edits which
1487 // conveniently deactivates irrelevant shortcuts.
1488 nameNode.tabIndex = 0;
1489 nameNode.focus();
1490 }
1491 }
1492 /**
1493 * Are all of the items between two provided indices selected?
1494 *
1495 * The items at the indices are not considered.
1496 *
1497 * @param j Index of one item.
1498 * @param k Index of another item. Note: may be less or greater than first
1499 * index.
1500 * @returns True if and only if all items between the j and k are selected.
1501 * Returns undefined if j and k are the same.
1502 */
1503 _allSelectedBetween(j, k) {
1504 if (j === k) {
1505 return;
1506 }
1507 const [start, end] = j < k ? [j + 1, k] : [k + 1, j];
1508 return this._sortedItems
1509 .slice(start, end)
1510 .reduce((result, item) => result && this.selection[item.path], true);
1511 }
1512 /**
1513 * Handle a multiple select on a file item node.
1514 */
1515 _handleMultiSelect(index) {
1516 const items = this._sortedItems;
1517 const fromIndex = this._focusIndex;
1518 const target = items[index];
1519 let shouldAdd = true;
1520 if (index === fromIndex) {
1521 // This follows the convention in Ubuntu and Windows, which is to allow
1522 // the focussed item to gain but not lose selected status on shift-click.
1523 // (MacOS is irrelevant here because MacOS Finder has no notion of a
1524 // focused-but-not-selected state.)
1525 this.selection[target.path] = true;
1526 return;
1527 }
1528 // If the target and all items in-between are selected, then we assume that
1529 // the user is trying to shrink rather than grow the group of selected
1530 // items.
1531 if (this.selection[target.path]) {
1532 // However, there is a special case when the distance between the from-
1533 // and to- index is just one (for example, when the user is pressing the
1534 // shift key plus arrow-up/down). If and only if the situation looks like
1535 // the following when going down (or reverse when going up) ...
1536 //
1537 // - [ante-anchor / previous item] unselected (or boundary)
1538 // - [anchor / currently focussed item / item at from-index] selected
1539 // - [target / next item / item at to-index] selected
1540 //
1541 // ... then we shrink the selection / unselect the currently focussed
1542 // item.
1543 if (Math.abs(index - fromIndex) === 1) {
1544 const anchor = items[fromIndex];
1545 const anteAnchor = items[fromIndex + (index < fromIndex ? 1 : -1)];
1546 if (
1547 // Currently focussed item is selected
1548 this.selection[anchor.path] &&
1549 // Item on other side of focussed item (away from target) is either a
1550 // boundary or unselected
1551 (!anteAnchor || !this.selection[anteAnchor.path])) {
1552 delete this.selection[anchor.path];
1553 }
1554 }
1555 else if (this._allSelectedBetween(fromIndex, index)) {
1556 shouldAdd = false;
1557 }
1558 }
1559 // Select (or unselect) the rows between chosen index (target) and the last
1560 // focussed.
1561 const step = fromIndex < index ? 1 : -1;
1562 for (let i = fromIndex; i !== index + step; i += step) {
1563 if (shouldAdd) {
1564 if (i === fromIndex) {
1565 // Do not change the selection state of the starting (fromIndex) item.
1566 continue;
1567 }
1568 this.selection[items[i].path] = true;
1569 }
1570 else {
1571 if (i === index) {
1572 // Do not unselect the target item.
1573 continue;
1574 }
1575 delete this.selection[items[i].path];
1576 }
1577 }
1578 }
1579 /**
1580 * Copy the selected items, and optionally cut as well.
1581 */
1582 _copy() {
1583 this._clipboard.length = 0;
1584 for (const item of this.selectedItems()) {
1585 this._clipboard.push(item.path);
1586 }
1587 }
1588 /**
1589 * Delete the files with the given paths.
1590 */
1591 async _delete(paths) {
1592 await Promise.all(paths.map(path => this._model.manager.deleteFile(path).catch(err => {
1593 void showErrorMessage(this._trans._p('showErrorMessage', 'Delete Failed'), err);
1594 })));
1595 }
1596 /**
1597 * Allow the user to rename item on a given row.
1598 */
1599 async _doRename() {
1600 this._inRename = true;
1601 const selectedPaths = Object.keys(this.selection);
1602 // Bail out if nothing has been selected.
1603 if (selectedPaths.length === 0) {
1604 this._inRename = false;
1605 return Promise.resolve('');
1606 }
1607 // Figure out which selected path to use for the rename.
1608 const items = this._sortedItems;
1609 let { path } = items[this._focusIndex];
1610 if (!this.selection[path]) {
1611 // If the currently focused item is not selected, then choose the last
1612 // selected item.
1613 path = selectedPaths.slice(-1)[0];
1614 }
1615 // Get the corresponding model, nodes, and file name.
1616 const index = ArrayExt.findFirstIndex(items, value => value.path === path);
1617 const row = this._items[index];
1618 const item = items[index];
1619 const nameNode = this.renderer.getNameNode(row);
1620 const original = item.name;
1621 // Seed the text input with current file name, and select and focus it.
1622 this._editNode.value = original;
1623 this._selectItem(index, false, true);
1624 // Wait for user input
1625 const newName = await Private.userInputForRename(nameNode, this._editNode, original);
1626 // Check if the widget was disposed during the `await`.
1627 if (this.isDisposed) {
1628 this._inRename = false;
1629 throw new Error('File browser is disposed.');
1630 }
1631 let finalFilename = newName;
1632 if (!newName || newName === original) {
1633 finalFilename = original;
1634 }
1635 else if (!isValidFileName(newName)) {
1636 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)));
1637 finalFilename = original;
1638 }
1639 else {
1640 // Attempt rename at the file system level.
1641 const manager = this._manager;
1642 const oldPath = PathExt.join(this._model.path, original);
1643 const newPath = PathExt.join(this._model.path, newName);
1644 try {
1645 await renameFile(manager, oldPath, newPath);
1646 }
1647 catch (error) {
1648 if (error !== 'File not renamed') {
1649 void showErrorMessage(this._trans._p('showErrorMessage', 'Rename Error'), error);
1650 }
1651 finalFilename = original;
1652 }
1653 // Check if the widget was disposed during the `await`.
1654 if (this.isDisposed) {
1655 this._inRename = false;
1656 throw new Error('File browser is disposed.');
1657 }
1658 }
1659 // If nothing else has been selected, then select the renamed file. In
1660 // other words, don't select the renamed file if the user has clicked
1661 // away to some other file.
1662 if (!this.isDisposed &&
1663 Object.keys(this.selection).length === 1 &&
1664 // We haven't updated the instance yet to reflect the rename, so unless
1665 // the user or something else has updated the selection, the original file
1666 // path and not the new file path will be in `this.selection`.
1667 this.selection[item.path]) {
1668 try {
1669 await this._selectItemByName(finalFilename, true, true);
1670 }
1671 catch (_a) {
1672 // do nothing
1673 console.warn('After rename, failed to select file', finalFilename);
1674 }
1675 }
1676 this._inRename = false;
1677 return finalFilename;
1678 }
1679 /**
1680 * Select a given item.
1681 */
1682 _selectItem(index, keepExisting, focus = true) {
1683 // Selected the given row(s)
1684 const items = this._sortedItems;
1685 if (!keepExisting) {
1686 this.clearSelectedItems();
1687 }
1688 const path = items[index].path;
1689 this.selection[path] = true;
1690 if (focus) {
1691 this._focusItem(index);
1692 }
1693 this.update();
1694 }
1695 /**
1696 * Handle the `refreshed` signal from the model.
1697 */
1698 _onModelRefreshed() {
1699 // Update the selection.
1700 const existing = Object.keys(this.selection);
1701 this.clearSelectedItems();
1702 for (const item of this._model.items()) {
1703 const path = item.path;
1704 if (existing.indexOf(path) !== -1) {
1705 this.selection[path] = true;
1706 }
1707 }
1708 if (this.isVisible) {
1709 // Update the sorted items.
1710 this.sort(this.sortState);
1711 }
1712 else {
1713 this._isDirty = true;
1714 }
1715 }
1716 /**
1717 * Handle a `pathChanged` signal from the model.
1718 */
1719 _onPathChanged() {
1720 // Reset the selection.
1721 this.clearSelectedItems();
1722 // Update the sorted items.
1723 this.sort(this.sortState);
1724 // Reset focus. But wait until the DOM has been updated (hence
1725 // `requestAnimationFrame`).
1726 requestAnimationFrame(() => {
1727 this._focusItem(0);
1728 });
1729 }
1730 /**
1731 * Handle a `fileChanged` signal from the model.
1732 */
1733 _onFileChanged(sender, args) {
1734 const newValue = args.newValue;
1735 if (!newValue) {
1736 return;
1737 }
1738 const name = newValue.name;
1739 if (args.type !== 'new' || !name) {
1740 return;
1741 }
1742 void this.selectItemByName(name).catch(() => {
1743 /* Ignore if file does not exist. */
1744 });
1745 }
1746 /**
1747 * Handle an `activateRequested` signal from the manager.
1748 */
1749 _onActivateRequested(sender, args) {
1750 const dirname = PathExt.dirname(args);
1751 if (dirname !== this._model.path) {
1752 return;
1753 }
1754 const basename = PathExt.basename(args);
1755 this.selectItemByName(basename).catch(() => {
1756 /* Ignore if file does not exist. */
1757 });
1758 }
1759}
1760/**
1761 * The namespace for the `DirListing` class statics.
1762 */
1763(function (DirListing) {
1764 /**
1765 * The default implementation of an `IRenderer`.
1766 */
1767 class Renderer {
1768 /**
1769 * Create the DOM node for a dir listing.
1770 */
1771 createNode() {
1772 const node = document.createElement('div');
1773 const header = document.createElement('div');
1774 const content = document.createElement('ul');
1775 // Allow the node to scroll while dragging items.
1776 content.setAttribute('data-lm-dragscroll', 'true');
1777 content.className = CONTENT_CLASS;
1778 header.className = HEADER_CLASS;
1779 node.appendChild(header);
1780 node.appendChild(content);
1781 // Set to -1 to allow calling this.node.focus().
1782 node.tabIndex = -1;
1783 return node;
1784 }
1785 /**
1786 * Populate and empty header node for a dir listing.
1787 *
1788 * @param node - The header node to populate.
1789 */
1790 populateHeaderNode(node, translator, hiddenColumns) {
1791 translator = translator || nullTranslator;
1792 const trans = translator.load('jupyterlab');
1793 const name = this.createHeaderItemNode(trans.__('Name'));
1794 const narrow = document.createElement('div');
1795 const modified = this.createHeaderItemNode(trans.__('Last Modified'));
1796 const fileSize = this.createHeaderItemNode(trans.__('File Size'));
1797 name.classList.add(NAME_ID_CLASS);
1798 name.classList.add(SELECTED_CLASS);
1799 modified.classList.add(MODIFIED_ID_CLASS);
1800 fileSize.classList.add(FILE_SIZE_ID_CLASS);
1801 narrow.classList.add(NARROW_ID_CLASS);
1802 narrow.textContent = '...';
1803 if (!(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'))) {
1804 const checkboxWrapper = this.createCheckboxWrapperNode({
1805 alwaysVisible: true
1806 });
1807 node.appendChild(checkboxWrapper);
1808 }
1809 node.appendChild(name);
1810 node.appendChild(narrow);
1811 node.appendChild(modified);
1812 node.appendChild(fileSize);
1813 if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
1814 modified.classList.add(MODIFIED_COLUMN_HIDDEN);
1815 }
1816 else {
1817 modified.classList.remove(MODIFIED_COLUMN_HIDDEN);
1818 }
1819 if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size')) {
1820 fileSize.classList.add(FILE_SIZE_COLUMN_HIDDEN);
1821 }
1822 else {
1823 fileSize.classList.remove(FILE_SIZE_COLUMN_HIDDEN);
1824 }
1825 // set the initial caret icon
1826 Private.updateCaret(DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS), 'right', 'up');
1827 }
1828 /**
1829 * Handle a header click.
1830 *
1831 * @param node - A node populated by [[populateHeaderNode]].
1832 *
1833 * @param event - A click event on the node.
1834 *
1835 * @returns The sort state of the header after the click event.
1836 */
1837 handleHeaderClick(node, event) {
1838 const name = DOMUtils.findElement(node, NAME_ID_CLASS);
1839 const modified = DOMUtils.findElement(node, MODIFIED_ID_CLASS);
1840 const fileSize = DOMUtils.findElement(node, FILE_SIZE_ID_CLASS);
1841 const state = { direction: 'ascending', key: 'name' };
1842 const target = event.target;
1843 const modifiedIcon = DOMUtils.findElement(modified, HEADER_ITEM_ICON_CLASS);
1844 const fileSizeIcon = DOMUtils.findElement(fileSize, HEADER_ITEM_ICON_CLASS);
1845 const nameIcon = DOMUtils.findElement(name, HEADER_ITEM_ICON_CLASS);
1846 if (name.contains(target)) {
1847 if (name.classList.contains(SELECTED_CLASS)) {
1848 if (!name.classList.contains(DESCENDING_CLASS)) {
1849 state.direction = 'descending';
1850 name.classList.add(DESCENDING_CLASS);
1851 Private.updateCaret(nameIcon, 'right', 'down');
1852 }
1853 else {
1854 name.classList.remove(DESCENDING_CLASS);
1855 Private.updateCaret(nameIcon, 'right', 'up');
1856 }
1857 }
1858 else {
1859 name.classList.remove(DESCENDING_CLASS);
1860 Private.updateCaret(nameIcon, 'right', 'up');
1861 }
1862 name.classList.add(SELECTED_CLASS);
1863 modified.classList.remove(SELECTED_CLASS);
1864 modified.classList.remove(DESCENDING_CLASS);
1865 fileSize.classList.remove(SELECTED_CLASS);
1866 fileSize.classList.remove(DESCENDING_CLASS);
1867 Private.updateCaret(modifiedIcon, 'left');
1868 Private.updateCaret(fileSizeIcon, 'left');
1869 return state;
1870 }
1871 if (modified.contains(target)) {
1872 state.key = 'last_modified';
1873 if (modified.classList.contains(SELECTED_CLASS)) {
1874 if (!modified.classList.contains(DESCENDING_CLASS)) {
1875 state.direction = 'descending';
1876 modified.classList.add(DESCENDING_CLASS);
1877 Private.updateCaret(modifiedIcon, 'left', 'down');
1878 }
1879 else {
1880 modified.classList.remove(DESCENDING_CLASS);
1881 Private.updateCaret(modifiedIcon, 'left', 'up');
1882 }
1883 }
1884 else {
1885 modified.classList.remove(DESCENDING_CLASS);
1886 Private.updateCaret(modifiedIcon, 'left', 'up');
1887 }
1888 modified.classList.add(SELECTED_CLASS);
1889 name.classList.remove(SELECTED_CLASS);
1890 name.classList.remove(DESCENDING_CLASS);
1891 fileSize.classList.remove(SELECTED_CLASS);
1892 fileSize.classList.remove(DESCENDING_CLASS);
1893 Private.updateCaret(nameIcon, 'right');
1894 Private.updateCaret(fileSizeIcon, 'left');
1895 return state;
1896 }
1897 if (fileSize.contains(target)) {
1898 state.key = 'file_size';
1899 if (fileSize.classList.contains(SELECTED_CLASS)) {
1900 if (!fileSize.classList.contains(DESCENDING_CLASS)) {
1901 state.direction = 'descending';
1902 fileSize.classList.add(DESCENDING_CLASS);
1903 Private.updateCaret(fileSizeIcon, 'left', 'down');
1904 }
1905 else {
1906 fileSize.classList.remove(DESCENDING_CLASS);
1907 Private.updateCaret(fileSizeIcon, 'left', 'up');
1908 }
1909 }
1910 else {
1911 fileSize.classList.remove(DESCENDING_CLASS);
1912 Private.updateCaret(fileSizeIcon, 'left', 'up');
1913 }
1914 fileSize.classList.add(SELECTED_CLASS);
1915 name.classList.remove(SELECTED_CLASS);
1916 name.classList.remove(DESCENDING_CLASS);
1917 modified.classList.remove(SELECTED_CLASS);
1918 modified.classList.remove(DESCENDING_CLASS);
1919 Private.updateCaret(nameIcon, 'right');
1920 Private.updateCaret(modifiedIcon, 'left');
1921 return state;
1922 }
1923 return state;
1924 }
1925 /**
1926 * Create a new item node for a dir listing.
1927 *
1928 * @returns A new DOM node to use as a content item.
1929 */
1930 createItemNode(hiddenColumns) {
1931 const node = document.createElement('li');
1932 const icon = document.createElement('span');
1933 const text = document.createElement('span');
1934 const modified = document.createElement('span');
1935 const fileSize = document.createElement('span');
1936 icon.className = ITEM_ICON_CLASS;
1937 text.className = ITEM_TEXT_CLASS;
1938 modified.className = ITEM_MODIFIED_CLASS;
1939 fileSize.className = ITEM_FILE_SIZE_CLASS;
1940 if (!(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'))) {
1941 const checkboxWrapper = this.createCheckboxWrapperNode();
1942 node.appendChild(checkboxWrapper);
1943 }
1944 node.appendChild(icon);
1945 node.appendChild(text);
1946 node.appendChild(modified);
1947 node.appendChild(fileSize);
1948 if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
1949 modified.classList.add(MODIFIED_COLUMN_HIDDEN);
1950 }
1951 else {
1952 modified.classList.remove(MODIFIED_COLUMN_HIDDEN);
1953 }
1954 if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size')) {
1955 fileSize.classList.add(FILE_SIZE_COLUMN_HIDDEN);
1956 }
1957 else {
1958 fileSize.classList.remove(FILE_SIZE_COLUMN_HIDDEN);
1959 }
1960 return node;
1961 }
1962 /**
1963 * Creates a node containing a checkbox.
1964 *
1965 * We wrap the checkbox in a label element in order to increase its hit
1966 * area. This is because the padding of the checkbox itself cannot be
1967 * increased via CSS, as the CSS/form compatibility table at the following
1968 * url from MDN shows:
1969 * https://developer.mozilla.org/en-US/docs/Learn/Forms/Property_compatibility_table_for_form_controls#check_boxes_and_radio_buttons
1970 *
1971 * @param [options]
1972 * @params options.alwaysVisible Should the checkbox be visible even when
1973 * not hovered?
1974 * @returns A new DOM node that contains a checkbox.
1975 */
1976 createCheckboxWrapperNode(options) {
1977 // Wrap the checkbox in a label element in order to increase its hit area.
1978 const labelWrapper = document.createElement('label');
1979 labelWrapper.classList.add(CHECKBOX_WRAPPER_CLASS);
1980 const checkbox = document.createElement('input');
1981 checkbox.type = 'checkbox';
1982 // Prevent the user from clicking (via mouse, keyboard, or touch) the
1983 // checkbox since other code handles the mouse and keyboard events and
1984 // controls the checked state of the checkbox.
1985 checkbox.addEventListener('click', event => {
1986 event.preventDefault();
1987 });
1988 // The individual file checkboxes are visible on hover, but the header
1989 // check-all checkbox is always visible.
1990 if (options === null || options === void 0 ? void 0 : options.alwaysVisible) {
1991 labelWrapper.classList.add('jp-mod-visible');
1992 }
1993 else {
1994 // Disable tabbing to all other checkboxes.
1995 checkbox.tabIndex = -1;
1996 }
1997 labelWrapper.appendChild(checkbox);
1998 return labelWrapper;
1999 }
2000 /**
2001 * Update an item node to reflect the current state of a model.
2002 *
2003 * @param node - A node created by [[createItemNode]].
2004 *
2005 * @param model - The model object to use for the item state.
2006 *
2007 * @param fileType - The file type of the item, if applicable.
2008 *
2009 */
2010 updateItemNode(node, model, fileType, translator, hiddenColumns, selected) {
2011 if (selected) {
2012 node.classList.add(SELECTED_CLASS);
2013 }
2014 fileType =
2015 fileType || DocumentRegistry.getDefaultTextFileType(translator);
2016 const { icon, iconClass, name } = fileType;
2017 translator = translator || nullTranslator;
2018 const trans = translator.load('jupyterlab');
2019 const iconContainer = DOMUtils.findElement(node, ITEM_ICON_CLASS);
2020 const text = DOMUtils.findElement(node, ITEM_TEXT_CLASS);
2021 const modified = DOMUtils.findElement(node, ITEM_MODIFIED_CLASS);
2022 const fileSize = DOMUtils.findElement(node, ITEM_FILE_SIZE_CLASS);
2023 const checkboxWrapper = DOMUtils.findElement(node, CHECKBOX_WRAPPER_CLASS);
2024 const showFileCheckboxes = !(hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('is_selected'));
2025 if (checkboxWrapper && !showFileCheckboxes) {
2026 node.removeChild(checkboxWrapper);
2027 }
2028 else if (showFileCheckboxes && !checkboxWrapper) {
2029 const checkboxWrapper = this.createCheckboxWrapperNode();
2030 node.insertBefore(checkboxWrapper, iconContainer);
2031 }
2032 if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('last_modified')) {
2033 modified.classList.add(MODIFIED_COLUMN_HIDDEN);
2034 }
2035 else {
2036 modified.classList.remove(MODIFIED_COLUMN_HIDDEN);
2037 }
2038 if (hiddenColumns === null || hiddenColumns === void 0 ? void 0 : hiddenColumns.has('file_size')) {
2039 fileSize.classList.add(FILE_SIZE_COLUMN_HIDDEN);
2040 }
2041 else {
2042 fileSize.classList.remove(FILE_SIZE_COLUMN_HIDDEN);
2043 }
2044 // render the file item's icon
2045 LabIcon.resolveElement({
2046 icon,
2047 iconClass: classes(iconClass, 'jp-Icon'),
2048 container: iconContainer,
2049 className: ITEM_ICON_CLASS,
2050 stylesheet: 'listing'
2051 });
2052 let hoverText = trans.__('Name: %1', model.name);
2053 // add file size to pop up if its available
2054 if (model.size !== null && model.size !== undefined) {
2055 const fileSizeText = Private.formatFileSize(model.size, 1, 1024);
2056 fileSize.textContent = fileSizeText;
2057 hoverText += trans.__('\nSize: %1', Private.formatFileSize(model.size, 1, 1024));
2058 }
2059 else {
2060 fileSize.textContent = '';
2061 }
2062 if (model.path) {
2063 const dirname = PathExt.dirname(model.path);
2064 if (dirname) {
2065 hoverText += trans.__('\nPath: %1', dirname.substr(0, 50));
2066 if (dirname.length > 50) {
2067 hoverText += '...';
2068 }
2069 }
2070 }
2071 if (model.created) {
2072 hoverText += trans.__('\nCreated: %1', Time.format(new Date(model.created)));
2073 }
2074 if (model.last_modified) {
2075 hoverText += trans.__('\nModified: %1', Time.format(new Date(model.last_modified)));
2076 }
2077 hoverText += trans.__('\nWritable: %1', model.writable);
2078 node.title = hoverText;
2079 node.setAttribute('data-file-type', name);
2080 if (model.name.startsWith('.')) {
2081 node.setAttribute('data-is-dot', 'true');
2082 }
2083 else {
2084 node.removeAttribute('data-is-dot');
2085 }
2086 // If an item is being edited currently, its text node is unavailable.
2087 const indices = !model.indices ? [] : model.indices;
2088 let highlightedName = StringExt.highlight(model.name, indices, h.mark);
2089 if (text) {
2090 VirtualDOM.render(h.span(highlightedName), text);
2091 }
2092 // Adds an aria-label to the checkbox element.
2093 const checkbox = checkboxWrapper === null || checkboxWrapper === void 0 ? void 0 : checkboxWrapper.querySelector('input[type="checkbox"]');
2094 if (checkbox) {
2095 let ariaLabel;
2096 if (fileType.contentType === 'directory') {
2097 ariaLabel = selected
2098 ? trans.__('Deselect directory "%1"', highlightedName)
2099 : trans.__('Select directory "%1"', highlightedName);
2100 }
2101 else {
2102 ariaLabel = selected
2103 ? trans.__('Deselect file "%1"', highlightedName)
2104 : trans.__('Select file "%1"', highlightedName);
2105 }
2106 checkbox.setAttribute('aria-label', ariaLabel);
2107 checkbox.checked = selected !== null && selected !== void 0 ? selected : false;
2108 }
2109 let modText = '';
2110 let modTitle = '';
2111 if (model.last_modified) {
2112 modText = Time.formatHuman(new Date(model.last_modified));
2113 modTitle = Time.format(new Date(model.last_modified));
2114 }
2115 modified.textContent = modText;
2116 modified.title = modTitle;
2117 }
2118 /**
2119 * Get the node containing the file name.
2120 *
2121 * @param node - A node created by [[createItemNode]].
2122 *
2123 * @returns The node containing the file name.
2124 */
2125 getNameNode(node) {
2126 return DOMUtils.findElement(node, ITEM_TEXT_CLASS);
2127 }
2128 /**
2129 * Get the checkbox input element node.
2130 *
2131 * @param node A node created by [[createItemNode]] or
2132 * [[createHeaderItemNode]]
2133 *
2134 * @returns The checkbox node.
2135 */
2136 getCheckboxNode(node) {
2137 return node.querySelector(`.${CHECKBOX_WRAPPER_CLASS} input[type=checkbox]`);
2138 }
2139 /**
2140 * Create a drag image for an item.
2141 *
2142 * @param node - A node created by [[createItemNode]].
2143 *
2144 * @param count - The number of items being dragged.
2145 *
2146 * @param fileType - The file type of the item, if applicable.
2147 *
2148 * @returns An element to use as the drag image.
2149 */
2150 createDragImage(node, count, trans, fileType) {
2151 const dragImage = node.cloneNode(true);
2152 const modified = DOMUtils.findElement(dragImage, ITEM_MODIFIED_CLASS);
2153 const icon = DOMUtils.findElement(dragImage, ITEM_ICON_CLASS);
2154 dragImage.removeChild(modified);
2155 if (!fileType) {
2156 icon.textContent = '';
2157 icon.className = '';
2158 }
2159 else {
2160 icon.textContent = fileType.iconLabel || '';
2161 icon.className = fileType.iconClass || '';
2162 }
2163 icon.classList.add(DRAG_ICON_CLASS);
2164 if (count > 1) {
2165 const nameNode = DOMUtils.findElement(dragImage, ITEM_TEXT_CLASS);
2166 nameNode.textContent = trans._n('%1 Item', '%1 Items', count);
2167 }
2168 return dragImage;
2169 }
2170 /**
2171 * Create a node for a header item.
2172 */
2173 createHeaderItemNode(label) {
2174 const node = document.createElement('div');
2175 const text = document.createElement('span');
2176 const icon = document.createElement('span');
2177 node.className = HEADER_ITEM_CLASS;
2178 text.className = HEADER_ITEM_TEXT_CLASS;
2179 icon.className = HEADER_ITEM_ICON_CLASS;
2180 text.textContent = label;
2181 node.appendChild(text);
2182 node.appendChild(icon);
2183 return node;
2184 }
2185 }
2186 DirListing.Renderer = Renderer;
2187 /**
2188 * The default `IRenderer` instance.
2189 */
2190 DirListing.defaultRenderer = new Renderer();
2191})(DirListing || (DirListing = {}));
2192/**
2193 * The namespace for the listing private data.
2194 */
2195var Private;
2196(function (Private) {
2197 /**
2198 * Handle editing text on a node.
2199 *
2200 * @returns Boolean indicating whether the name changed.
2201 */
2202 function userInputForRename(text, edit, original) {
2203 const parent = text.parentElement;
2204 parent.replaceChild(edit, text);
2205 edit.focus();
2206 const index = edit.value.lastIndexOf('.');
2207 if (index === -1) {
2208 edit.setSelectionRange(0, edit.value.length);
2209 }
2210 else {
2211 edit.setSelectionRange(0, index);
2212 }
2213 return new Promise(resolve => {
2214 edit.onblur = () => {
2215 parent.replaceChild(text, edit);
2216 resolve(edit.value);
2217 };
2218 edit.onkeydown = (event) => {
2219 switch (event.keyCode) {
2220 case 13: // Enter
2221 event.stopPropagation();
2222 event.preventDefault();
2223 edit.blur();
2224 break;
2225 case 27: // Escape
2226 event.stopPropagation();
2227 event.preventDefault();
2228 edit.value = original;
2229 edit.blur();
2230 // Put focus back on the text node. That way the user can, for
2231 // example, press the keyboard shortcut to go back into edit mode,
2232 // and it will work.
2233 text.focus();
2234 break;
2235 default:
2236 break;
2237 }
2238 };
2239 });
2240 }
2241 Private.userInputForRename = userInputForRename;
2242 /**
2243 * Sort a list of items by sort state as a new array.
2244 */
2245 function sort(items, state, sortNotebooksFirst = false) {
2246 const copy = Array.from(items);
2247 const reverse = state.direction === 'descending' ? 1 : -1;
2248 /**
2249 * Compares two items and returns whether they should have a fixed priority.
2250 * The fixed priority enables to always sort the directories above the other files. And to sort the notebook above other files if the `sortNotebooksFirst` is true.
2251 */
2252 function isPriorityOverridden(a, b) {
2253 if (sortNotebooksFirst) {
2254 return a.type !== b.type;
2255 }
2256 return (a.type === 'directory') !== (b.type === 'directory');
2257 }
2258 /**
2259 * Returns the priority of a file.
2260 */
2261 function getPriority(item) {
2262 if (item.type === 'directory') {
2263 return 2;
2264 }
2265 if (item.type === 'notebook' && sortNotebooksFirst) {
2266 return 1;
2267 }
2268 return 0;
2269 }
2270 function compare(compare) {
2271 return (a, b) => {
2272 // Group directory first, then notebooks, then files
2273 if (isPriorityOverridden(a, b)) {
2274 return getPriority(b) - getPriority(a);
2275 }
2276 const compared = compare(a, b);
2277 if (compared !== 0) {
2278 return compared * reverse;
2279 }
2280 // Default sorting is alphabetical ascending
2281 return a.name.localeCompare(b.name);
2282 };
2283 }
2284 if (state.key === 'last_modified') {
2285 // Sort by last modified
2286 copy.sort(compare((a, b) => {
2287 return (new Date(a.last_modified).getTime() -
2288 new Date(b.last_modified).getTime());
2289 }));
2290 }
2291 else if (state.key === 'file_size') {
2292 // Sort by size
2293 copy.sort(compare((a, b) => {
2294 var _a, _b;
2295 return ((_a = a.size) !== null && _a !== void 0 ? _a : 0) - ((_b = b.size) !== null && _b !== void 0 ? _b : 0);
2296 }));
2297 }
2298 else {
2299 // Sort by name
2300 copy.sort(compare((a, b) => {
2301 return b.name.localeCompare(a.name);
2302 }));
2303 }
2304 return copy;
2305 }
2306 Private.sort = sort;
2307 /**
2308 * Get the index of the node at a client position, or `-1`.
2309 */
2310 function hitTestNodes(nodes, event) {
2311 return ArrayExt.findFirstIndex(nodes, node => ElementExt.hitTest(node, event.clientX, event.clientY) ||
2312 event.target === node);
2313 }
2314 Private.hitTestNodes = hitTestNodes;
2315 /**
2316 * Format bytes to human readable string.
2317 */
2318 function formatFileSize(bytes, decimalPoint, k) {
2319 // https://www.codexworld.com/how-to/convert-file-size-bytes-kb-mb-gb-javascript/
2320 if (bytes === 0) {
2321 return '0 B';
2322 }
2323 const dm = decimalPoint || 2;
2324 const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
2325 const i = Math.floor(Math.log(bytes) / Math.log(k));
2326 if (i >= 0 && i < sizes.length) {
2327 return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
2328 }
2329 else {
2330 return String(bytes);
2331 }
2332 }
2333 Private.formatFileSize = formatFileSize;
2334 /**
2335 * Update an inline svg caret icon in a node.
2336 */
2337 function updateCaret(container, float, state) {
2338 if (state) {
2339 (state === 'down' ? caretDownIcon : caretUpIcon).element({
2340 container,
2341 tag: 'span',
2342 stylesheet: 'listingHeaderItem',
2343 float
2344 });
2345 }
2346 else {
2347 LabIcon.remove(container);
2348 container.className = HEADER_ITEM_ICON_CLASS;
2349 }
2350 }
2351 Private.updateCaret = updateCaret;
2352})(Private || (Private = {}));
2353//# sourceMappingURL=listing.js.map
\No newline at end of file